Skip to content

Commit

Permalink
feat!: match query params when determining highlighted items (#7139)
Browse files Browse the repository at this point in the history
  • Loading branch information
vursen committed Feb 22, 2024
1 parent d649adb commit c86e1d3
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 33 deletions.
10 changes: 7 additions & 3 deletions packages/component-base/src/url-utils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
*/

/**
* Check if two paths can be resolved as URLs
* with the same origin and pathname.
* Checks if two paths match based on their origin, pathname, and query parameters.
*
* The function matches an actual URL against an expected URL to see if they share
* the same base origin (like https://example.com), the same path (like /path/to/page),
* and if the actual URL contains at least all the query parameters with the same values
* from the expected URL.
*/
export declare function matchPaths(path1: string, path2: string): boolean;
export declare function matchPaths(actual: string, expected: string): boolean;
38 changes: 30 additions & 8 deletions packages/component-base/src/url-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,37 @@
*/

/**
* Check if two paths can be resolved as URLs
* with the same origin and pathname.
* Checks if one set of URL parameters contains all the parameters
* with the same values from another set.
*
* @param {string} path1
* @param {string} path2
* @param {URLSearchParams} actual
* @param {URLSearchParams} expected
*/
export function matchPaths(path1, path2) {
function containsQueryParams(actual, expected) {
return [...expected.entries()].every(([key, value]) => {
return actual.getAll(key).includes(value);
});
}

/**
* Checks if two paths match based on their origin, pathname, and query parameters.
*
* The function matches an actual URL against an expected URL to see if they share
* the same base origin (like https://example.com), the same path (like /path/to/page),
* and if the actual URL contains at least all the query parameters with the same values
* from the expected URL.
*
* @param {string} actual The actual URL to match.
* @param {string} expected The expected URL to match.
*/
export function matchPaths(actual, expected) {
const base = document.baseURI;
const url1 = new URL(path1, base);
const url2 = new URL(path2, base);
return url1.origin === url2.origin && url1.pathname === url2.pathname;
const actualUrl = new URL(actual, base);
const expectedUrl = new URL(expected, base);

return (
actualUrl.origin === expectedUrl.origin &&
actualUrl.pathname === expectedUrl.pathname &&
containsQueryParams(actualUrl.searchParams, expectedUrl.searchParams)
);
}
78 changes: 68 additions & 10 deletions packages/component-base/test/url-utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,18 @@ import { matchPaths } from '../src/url-utils.js';

describe('url-utils', () => {
describe('matchPaths', () => {
let documentBaseURI;

const paths = ['', '/', '/path', 'base/path'];

beforeEach(() => {
documentBaseURI = sinon.stub(document, 'baseURI').value('http://localhost/');
});

afterEach(() => {
documentBaseURI.restore();
});

it('should return true when paths match', () => {
paths.forEach((path) => expect(matchPaths(path, path)).to.be.true);
});
Expand Down Expand Up @@ -35,20 +45,68 @@ describe('url-utils', () => {
});
});

describe('base url', () => {
let baseUri;
it('should use document.baseURI as a base url', () => {
documentBaseURI.value('https://vaadin.com/docs/');
expect(matchPaths('https://vaadin.com/docs/components', 'components')).to.be.true;
});

beforeEach(() => {
baseUri = sinon.stub(document, 'baseURI');
});
describe('query params', () => {
it('should return true when query params match', () => {
expect(matchPaths('/products', '/products')).to.be.true;
expect(matchPaths('/products?c=socks', '/products')).to.be.true;
expect(matchPaths('/products?c=pants', '/products')).to.be.true;
expect(matchPaths('/products?c=', '/products')).to.be.true;
expect(matchPaths('/products?c=socks&item=5', '/products')).to.be.true;
expect(matchPaths('/products?item=5&c=socks', '/products')).to.be.true;
expect(matchPaths('/products?c=socks&c=pants', '/products')).to.be.true;
expect(matchPaths('/products?socks', '/products')).to.be.true;
expect(matchPaths('/products?socks=', '/products')).to.be.true;

afterEach(() => {
baseUri.restore();
expect(matchPaths('/products?c=socks', '/products?c=socks')).to.be.true;
expect(matchPaths('/products?c=socks&item=5', '/products?c=socks')).to.be.true;
expect(matchPaths('/products?item=5&c=socks', '/products?c=socks')).to.be.true;
expect(matchPaths('/products?c=socks&c=pants', '/products?c=socks')).to.be.true;

expect(matchPaths('/products?c=', '/products?c=')).to.be.true;

expect(matchPaths('/products?c=socks&c=pants', '/products?c=socks&c=pants')).to.be.true;

expect(matchPaths('/products?socks', '/products?socks')).to.be.true;
expect(matchPaths('/products?socks=', '/products?socks')).to.be.true;
});

it('should use document.baseURI as a base url', () => {
baseUri.value('https://vaadin.com/docs/');
expect(matchPaths('https://vaadin.com/docs/components', 'components')).to.be.true;
it('should return false when query params do not match', () => {
expect(matchPaths('/products', '/products?c=socks')).to.be.false;
expect(matchPaths('/products?c=pants', '/products?c=socks')).to.be.false;
expect(matchPaths('/products?c=', '/products?c=socks')).to.be.false;
expect(matchPaths('/products?socks', '/products?c=socks')).to.be.false;
expect(matchPaths('/products?socks=', '/products?c=socks')).to.be.false;

expect(matchPaths('/products', '/products?c=')).to.be.false;
expect(matchPaths('/products?c=socks', '/products?c=')).to.be.false;
expect(matchPaths('/products?c=pants', '/products?c=')).to.be.false;
expect(matchPaths('/products?c=socks&item=5', '/products?c=')).to.be.false;
expect(matchPaths('/products?item=5&c=socks', '/products?c=')).to.be.false;
expect(matchPaths('/products?c=socks&c=pants', '/products?c=')).to.be.false;
expect(matchPaths('/products?socks', '/products?c=')).to.be.false;
expect(matchPaths('/products?socks=', '/products?c=')).to.be.false;

expect(matchPaths('/products', '/products?c=socks&c=pants')).to.be.false;
expect(matchPaths('/products?c=socks', '/products?c=socks&c=pants')).to.be.false;
expect(matchPaths('/products?c=pants', '/products?c=socks&c=pants')).to.be.false;
expect(matchPaths('/products?c=', '/products?c=socks&c=pants')).to.be.false;
expect(matchPaths('/products?c=socks&item=5', '/products?c=socks&c=pants')).to.be.false;
expect(matchPaths('/products?item=5&c=socks', '/products?c=socks&c=pants')).to.be.false;
expect(matchPaths('/products?socks', '/products?c=socks&c=pants')).to.be.false;
expect(matchPaths('/products?socks=', '/products?c=socks&c=pants')).to.be.false;

expect(matchPaths('/products', '/products?socks')).to.be.false;
expect(matchPaths('/products?c=socks', '/products?socks')).to.be.false;
expect(matchPaths('/products?c=pants', '/products?socks')).to.be.false;
expect(matchPaths('/products?c=', '/products?socks')).to.be.false;
expect(matchPaths('/products?c=socks&item=5', '/products?socks')).to.be.false;
expect(matchPaths('/products?item=5&c=socks', '/products?socks')).to.be.false;
expect(matchPaths('/products?c=socks&c=pants', '/products?socks')).to.be.false;
});
});
});
Expand Down
11 changes: 8 additions & 3 deletions packages/side-nav/src/vaadin-side-nav-item.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,14 @@ declare class SideNavItem extends SideNavChildrenMixin(DisabledMixin(ElementMixi
expanded: boolean;

/**
* Whether the path of the item matches the current path.
* Set when the item is appended to DOM or when navigated back
* to the page that contains this item using the browser.
* Whether the item's path matches the current browser URL.
*
* A match occurs when both share the same base origin (like https://example.com),
* the same path (like /path/to/page), and the browser URL contains all
* the query parameters with the same values from the item's path.
*
* The state is updated when the item is added to the DOM or when the browser
* navigates to a new page.
*/
readonly current: boolean;

Expand Down
18 changes: 11 additions & 7 deletions packages/side-nav/src/vaadin-side-nav-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,14 @@ class SideNavItem extends SideNavChildrenMixin(DisabledMixin(ElementMixin(Themab
},

/**
* Whether the path of the item matches the current path.
* Set when the item is appended to DOM or when navigated back
* to the page that contains this item using the browser.
* Whether the item's path matches the current browser URL.
*
* A match occurs when both share the same base origin (like https://example.com),
* the same path (like /path/to/page), and the browser URL contains at least
* all the query parameters with the same values from the item's path.
*
* The state is updated when the item is added to the DOM or when the browser
* navigates to a new page.
*
* @type {boolean}
*/
Expand Down Expand Up @@ -266,10 +271,9 @@ class SideNavItem extends SideNavChildrenMixin(DisabledMixin(ElementMixin(Themab
if (this.path == null) {
return false;
}
return (
matchPaths(document.location.pathname, this.path) ||
this.pathAliases.some((alias) => matchPaths(document.location.pathname, alias))
);

const browserPath = `${document.location.pathname}${document.location.search}`;
return matchPaths(browserPath, this.path) || this.pathAliases.some((alias) => matchPaths(browserPath, alias));
}
}

Expand Down
11 changes: 10 additions & 1 deletion packages/side-nav/test/dom/side-nav-item.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { expect } from '@esm-bundle/chai';
import { fixtureSync, nextFrame, nextRender } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import '../../src/vaadin-side-nav-item.js';
import '@vaadin/icon';
import '@vaadin/icons';

describe('vaadin-side-nav-item', () => {
let sideNavItem;
let sideNavItem, documentBaseURI;

beforeEach(() => {
documentBaseURI = sinon.stub(document, 'baseURI').value('http://localhost/');
});

afterEach(() => {
documentBaseURI.restore();
});

beforeEach(async () => {
sideNavItem = fixtureSync(
Expand Down
10 changes: 9 additions & 1 deletion packages/side-nav/test/side-nav-item.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import sinon from 'sinon';
import '../vaadin-side-nav-item.js';

describe('side-nav-item', () => {
let item;
let item, documentBaseURI;

beforeEach(() => {
documentBaseURI = sinon.stub(document, 'baseURI').value('http://localhost/');
});

afterEach(() => {
documentBaseURI.restore();
});

describe('custom element definition', () => {
let tagName;
Expand Down

0 comments on commit c86e1d3

Please sign in to comment.