Skip to content

Commit

Permalink
feat: Support custom search param propagation (#519)
Browse files Browse the repository at this point in the history
This also removes the non-standard `v-panel` from default behaviour.

```diff
+ <ScoobieLinkProvider propagateSearchParams={['debug', 'v']}>
    <BraidProvider linkComponent={ScoobieLink} theme={apacTheme}>
      <TextLink href="/root-relative">Internal link</TextLink>
    </BraidProvider>
+ </ScoobieLinkProvider>
```
  • Loading branch information
72636c authored Jun 19, 2023
1 parent 5a9a173 commit 25f6709
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 30 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,12 @@ export const MyFirstInlineCode = () => (

Render an internal link with the same opinions as our [MdxProvider](#mdxprovider):

- Internal links pass through the `v` and `v-panel` URL parameters for UI version switching
- Internal links pass through the `v` or [ScoobieLinkProvider] URL parameters for UI version switching

Unlike [SmartTextLink](#smarttextlink), this is not bound to a parent [Text] as it has no underlying [TextLink].
It can be used to make complex components navigable rather than just sections of text.

[scoobielinkprovider]: #scoobielinkprovider
[text]: https://seek-oss.github.io/braid-design-system/components/Text/
[textlink]: https://seek-oss.github.io/braid-design-system/components/TextLink/

Expand Down Expand Up @@ -398,7 +399,7 @@ export const Component = () => (

Render all underlying links as follows:

- Internal links pass through the `v` and `v-panel` URL parameters for UI version switching
- Internal links pass through the `v` or [ScoobieLinkProvider] URL parameters for UI version switching
- External links open in a new tab
- Links with a [`download` attribute] prompt the user with a file download

Expand All @@ -421,6 +422,25 @@ export const Component = () => (

[braidprovider]: https://seek-oss.github.io/braid-design-system/components/BraidProvider

### ScoobieLinkProvider

Propagate a custom set of URL parameters on internal links.

```tsx
import { BraidProvider, TextLink } from 'braid-design-system';
import apacTheme from 'braid-design-system/themes/apac';
import React from 'react';
import { ScoobieLink } from 'scoobie';

export const Component = () => (
<ScoobieLinkProvider propagateSearchParams={['debug', 'v']}>
<BraidProvider linkComponent={ScoobieLink} theme={apacTheme}>
<TextLink href="/root-relative">Internal link</TextLink>
</BraidProvider>
</ScoobieLinkProvider>
);
```

### SmartTextLink

Render a text link with the same opinions as our [MdxProvider](#mdxprovider):
Expand Down
9 changes: 8 additions & 1 deletion src/components/InternalLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { NavLink, useLocation } from 'react-router-dom';

import { parseInternalHref } from '../private/url';

import { useScoobieLink } from './ScoobieLinkProvider';

import * as styles from './InternalLink.css';

interface Props
Expand All @@ -18,7 +20,12 @@ export const InternalLink = forwardRef<HTMLAnchorElement, Props>(
({ className, href, reset = true, state, ...restProps }, ref) => {
const location = useLocation();

const to = { ...parseInternalHref(href, location), state };
const { propagateSearchParams } = useScoobieLink();

const to = {
...parseInternalHref(href, location, propagateSearchParams),
state,
};

return (
<NavLink
Expand Down
29 changes: 29 additions & 0 deletions src/components/ScoobieLinkProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { type ReactNode, createContext, useContext } from 'react';

interface ScoobieLinkContext {
propagateSearchParams: string[];
}

const ctx = createContext<ScoobieLinkContext>({
propagateSearchParams: ['v'],
});

interface ScoobieLinkProviderProps {
children: ReactNode;

/**
* The search parameters to propagate on internal links.
*
* This defaults to `['v']` in the absence of a `ScoobieLinkProvider`.
*/
propagateSearchParams: string[];
}

export const ScoobieLinkProvider = ({
children,
...value
}: ScoobieLinkProviderProps) => {
<ctx.Provider value={value}>{children}</ctx.Provider>;
};

export const useScoobieLink = () => useContext(ctx);
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { InlineCode } from './components/InlineCode';
export { InternalLink } from './components/InternalLink';
export { MdxProvider } from './components/MdxProvider';
export { ScoobieLink } from './components/ScoobieLink';
export { ScoobieLinkProvider } from './components/ScoobieLinkProvider';
export { SmartTextLink } from './components/SmartTextLink';
export { Table } from './components/Table';
export { TableRow } from './components/TableRow';
Expand Down
53 changes: 41 additions & 12 deletions src/private/url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ describe('isExternalHref', () => {

describe('parseInternalHref', () => {
it('preferences the v URL parameter from location', () => {
const to = parseInternalHref('/hello?v=v1&b=b1', {
pathname: '/',
search: '?a=1&v=2&b=3',
});
const to = parseInternalHref(
'/hello?v=v1&b=b1',
{
pathname: '/',
search: '?a=1&v=2&b=3',
},
['v'],
);

expect(to).toEqual({
hash: '',
Expand All @@ -37,10 +41,14 @@ describe('parseInternalHref', () => {
});

it('preferences the v-panel URL parameter from location', () => {
const to = parseInternalHref('/hello?v-panel=v1&b=b1', {
pathname: '/',
search: '?a=1&v-panel=2&b=3',
});
const to = parseInternalHref(
'/hello?v-panel=v1&b=b1',
{
pathname: '/',
search: '?a=1&v-panel=2&b=3',
},
['v', 'v-panel'],
);

expect(to).toEqual({
hash: '',
Expand All @@ -49,6 +57,23 @@ describe('parseInternalHref', () => {
});
});

it('propagates multiple parameters from location', () => {
const to = parseInternalHref(
'/hello?d=4',
{
pathname: '/',
search: '?a=1&b=2&c=3',
},
['a', 'b'],
);

expect(to).toEqual({
hash: '',
pathname: '/hello',
search: 'd=4&a=1&b=2',
});
});

describe.each(['/page-1', '/page-1/'])(
'given pathname %s',
(inputPathname) => {
Expand Down Expand Up @@ -117,10 +142,14 @@ describe('parseInternalHref', () => {
},
],
])('handles %s', (_, inputHref, expected) => {
const to = parseInternalHref(inputHref, {
pathname: inputPathname,
search: '',
});
const to = parseInternalHref(
inputHref,
{
pathname: inputPathname,
search: '',
},
['debug'],
);

expect(to).toEqual(expected);
});
Expand Down
23 changes: 8 additions & 15 deletions src/private/url.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
const EXAMPLE_BASE_URL = 'https://example.com';

const parseVersionParams = (search: string) => {
const urlSearchParams = new URLSearchParams(search);

return {
v: urlSearchParams.get('v'),
vPanel: urlSearchParams.get('v-panel'),
};
};

const hrefToUrl = (href: string, pathname: string) => {
if (href.startsWith('/')) {
return new URL(`${EXAMPLE_BASE_URL}${href}`);
Expand All @@ -27,16 +18,18 @@ export const parseInternalHref = (
pathname: string;
search: string;
},
propagateSearchParams: string[],
) => {
const { hash, pathname, searchParams } = hrefToUrl(href, location.pathname);

const { v, vPanel } = parseVersionParams(location.search);
const priorSearchParams = new URLSearchParams(location.search);

if (v !== null) {
searchParams.set('v', v);
}
if (vPanel !== null) {
searchParams.set('v-panel', vPanel);
for (const key of propagateSearchParams) {
const value = priorSearchParams.get(key);

if (value !== null) {
searchParams.set(key, value);
}
}

const search = searchParams.toString();
Expand Down

0 comments on commit 25f6709

Please sign in to comment.