Skip to content

Commit 25f6709

Browse files
authored
feat: Support custom search param propagation (#519)
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> ```
1 parent 5a9a173 commit 25f6709

File tree

6 files changed

+109
-30
lines changed

6 files changed

+109
-30
lines changed

README.md

+22-2
Original file line numberDiff line numberDiff line change
@@ -345,11 +345,12 @@ export const MyFirstInlineCode = () => (
345345

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

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

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

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

@@ -398,7 +399,7 @@ export const Component = () => (
398399

399400
Render all underlying links as follows:
400401

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

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

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

425+
### ScoobieLinkProvider
426+
427+
Propagate a custom set of URL parameters on internal links.
428+
429+
```tsx
430+
import { BraidProvider, TextLink } from 'braid-design-system';
431+
import apacTheme from 'braid-design-system/themes/apac';
432+
import React from 'react';
433+
import { ScoobieLink } from 'scoobie';
434+
435+
export const Component = () => (
436+
<ScoobieLinkProvider propagateSearchParams={['debug', 'v']}>
437+
<BraidProvider linkComponent={ScoobieLink} theme={apacTheme}>
438+
<TextLink href="/root-relative">Internal link</TextLink>
439+
</BraidProvider>
440+
</ScoobieLinkProvider>
441+
);
442+
```
443+
424444
### SmartTextLink
425445

426446
Render a text link with the same opinions as our [MdxProvider](#mdxprovider):

src/components/InternalLink.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { NavLink, useLocation } from 'react-router-dom';
44

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

7+
import { useScoobieLink } from './ScoobieLinkProvider';
8+
79
import * as styles from './InternalLink.css';
810

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

21-
const to = { ...parseInternalHref(href, location), state };
23+
const { propagateSearchParams } = useScoobieLink();
24+
25+
const to = {
26+
...parseInternalHref(href, location, propagateSearchParams),
27+
state,
28+
};
2229

2330
return (
2431
<NavLink
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React, { type ReactNode, createContext, useContext } from 'react';
2+
3+
interface ScoobieLinkContext {
4+
propagateSearchParams: string[];
5+
}
6+
7+
const ctx = createContext<ScoobieLinkContext>({
8+
propagateSearchParams: ['v'],
9+
});
10+
11+
interface ScoobieLinkProviderProps {
12+
children: ReactNode;
13+
14+
/**
15+
* The search parameters to propagate on internal links.
16+
*
17+
* This defaults to `['v']` in the absence of a `ScoobieLinkProvider`.
18+
*/
19+
propagateSearchParams: string[];
20+
}
21+
22+
export const ScoobieLinkProvider = ({
23+
children,
24+
...value
25+
}: ScoobieLinkProviderProps) => {
26+
<ctx.Provider value={value}>{children}</ctx.Provider>;
27+
};
28+
29+
export const useScoobieLink = () => useContext(ctx);

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { InlineCode } from './components/InlineCode';
55
export { InternalLink } from './components/InternalLink';
66
export { MdxProvider } from './components/MdxProvider';
77
export { ScoobieLink } from './components/ScoobieLink';
8+
export { ScoobieLinkProvider } from './components/ScoobieLinkProvider';
89
export { SmartTextLink } from './components/SmartTextLink';
910
export { Table } from './components/Table';
1011
export { TableRow } from './components/TableRow';

src/private/url.test.ts

+41-12
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ describe('isExternalHref', () => {
2424

2525
describe('parseInternalHref', () => {
2626
it('preferences the v URL parameter from location', () => {
27-
const to = parseInternalHref('/hello?v=v1&b=b1', {
28-
pathname: '/',
29-
search: '?a=1&v=2&b=3',
30-
});
27+
const to = parseInternalHref(
28+
'/hello?v=v1&b=b1',
29+
{
30+
pathname: '/',
31+
search: '?a=1&v=2&b=3',
32+
},
33+
['v'],
34+
);
3135

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

3943
it('preferences the v-panel URL parameter from location', () => {
40-
const to = parseInternalHref('/hello?v-panel=v1&b=b1', {
41-
pathname: '/',
42-
search: '?a=1&v-panel=2&b=3',
43-
});
44+
const to = parseInternalHref(
45+
'/hello?v-panel=v1&b=b1',
46+
{
47+
pathname: '/',
48+
search: '?a=1&v-panel=2&b=3',
49+
},
50+
['v', 'v-panel'],
51+
);
4452

4553
expect(to).toEqual({
4654
hash: '',
@@ -49,6 +57,23 @@ describe('parseInternalHref', () => {
4957
});
5058
});
5159

60+
it('propagates multiple parameters from location', () => {
61+
const to = parseInternalHref(
62+
'/hello?d=4',
63+
{
64+
pathname: '/',
65+
search: '?a=1&b=2&c=3',
66+
},
67+
['a', 'b'],
68+
);
69+
70+
expect(to).toEqual({
71+
hash: '',
72+
pathname: '/hello',
73+
search: 'd=4&a=1&b=2',
74+
});
75+
});
76+
5277
describe.each(['/page-1', '/page-1/'])(
5378
'given pathname %s',
5479
(inputPathname) => {
@@ -117,10 +142,14 @@ describe('parseInternalHref', () => {
117142
},
118143
],
119144
])('handles %s', (_, inputHref, expected) => {
120-
const to = parseInternalHref(inputHref, {
121-
pathname: inputPathname,
122-
search: '',
123-
});
145+
const to = parseInternalHref(
146+
inputHref,
147+
{
148+
pathname: inputPathname,
149+
search: '',
150+
},
151+
['debug'],
152+
);
124153

125154
expect(to).toEqual(expected);
126155
});

src/private/url.ts

+8-15
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
const EXAMPLE_BASE_URL = 'https://example.com';
22

3-
const parseVersionParams = (search: string) => {
4-
const urlSearchParams = new URLSearchParams(search);
5-
6-
return {
7-
v: urlSearchParams.get('v'),
8-
vPanel: urlSearchParams.get('v-panel'),
9-
};
10-
};
11-
123
const hrefToUrl = (href: string, pathname: string) => {
134
if (href.startsWith('/')) {
145
return new URL(`${EXAMPLE_BASE_URL}${href}`);
@@ -27,16 +18,18 @@ export const parseInternalHref = (
2718
pathname: string;
2819
search: string;
2920
},
21+
propagateSearchParams: string[],
3022
) => {
3123
const { hash, pathname, searchParams } = hrefToUrl(href, location.pathname);
3224

33-
const { v, vPanel } = parseVersionParams(location.search);
25+
const priorSearchParams = new URLSearchParams(location.search);
3426

35-
if (v !== null) {
36-
searchParams.set('v', v);
37-
}
38-
if (vPanel !== null) {
39-
searchParams.set('v-panel', vPanel);
27+
for (const key of propagateSearchParams) {
28+
const value = priorSearchParams.get(key);
29+
30+
if (value !== null) {
31+
searchParams.set(key, value);
32+
}
4033
}
4134

4235
const search = searchParams.toString();

0 commit comments

Comments
 (0)