Skip to content

Commit 2f6be54

Browse files
committed
feat: client side routing
1 parent 75be05e commit 2f6be54

File tree

15 files changed

+506
-197
lines changed

15 files changed

+506
-197
lines changed

src/components/Button/Button.tsx

+45-22
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import React from 'react';
44

5-
import type {DOMProps, QAProps} from '../types';
5+
import {useLinkProps} from '../lab/router/router';
6+
import type {DOMProps, Href, QAProps, RouterOptions} from '../types';
67
import {block} from '../utils/cn';
78
import {isIcon, isSvg} from '../utils/common';
89
import {eventBroker} from '../utils/event-broker';
@@ -49,7 +50,7 @@ export interface ButtonProps extends DOMProps, QAProps {
4950
id?: string;
5051
type?: 'button' | 'submit' | 'reset';
5152
component?: React.ElementType;
52-
href?: string;
53+
href?: Href;
5354
target?: string;
5455
rel?: string;
5556
extraProps?:
@@ -62,6 +63,7 @@ export interface ButtonProps extends DOMProps, QAProps {
6263
onBlur?: React.FocusEventHandler<HTMLButtonElement | HTMLAnchorElement>;
6364
/** Button content. You can mix button text with `<Icon/>` component */
6465
children?: React.ReactNode;
66+
routerOptions?: RouterOptions;
6567
}
6668

6769
const b = block('button');
@@ -79,9 +81,6 @@ const ButtonWithHandlers = React.forwardRef<HTMLElement, ButtonProps>(function B
7981
tabIndex,
8082
type = 'button',
8183
component,
82-
href,
83-
target,
84-
rel,
8584
extraProps,
8685
onClick,
8786
onMouseEnter,
@@ -93,6 +92,7 @@ const ButtonWithHandlers = React.forwardRef<HTMLElement, ButtonProps>(function B
9392
style,
9493
className,
9594
qa,
95+
...props
9696
},
9797
ref,
9898
) {
@@ -137,37 +137,60 @@ const ButtonWithHandlers = React.forwardRef<HTMLElement, ButtonProps>(function B
137137
'data-qa': qa,
138138
};
139139

140-
if (typeof href === 'string' || component) {
141-
const linkProps = {
142-
href,
143-
target,
144-
rel: target === '_blank' && !rel ? 'noopener noreferrer' : rel,
145-
};
140+
const linkProps = useLinkProps({
141+
...extraProps,
142+
...props,
143+
onClick: (e) => {
144+
if (disabled) {
145+
e.preventDefault();
146+
return;
147+
}
148+
149+
if (typeof onClick === 'function') {
150+
onClick(e);
151+
}
152+
},
153+
});
154+
155+
if (component) {
146156
return React.createElement(
147-
component || 'a',
157+
component,
148158
{
149159
...extraProps,
150160
...commonProps,
151-
...(component ? {} : linkProps),
152-
ref: ref as React.Ref<HTMLAnchorElement>,
161+
ref,
153162
'aria-disabled': disabled || loading,
154163
},
155164
prepareChildren(children),
156165
);
157-
} else {
166+
}
167+
168+
if (props.href) {
158169
return (
159-
<button
160-
{...(extraProps as React.ButtonHTMLAttributes<HTMLButtonElement>)}
170+
<a
171+
{...(extraProps as React.ButtonHTMLAttributes<HTMLAnchorElement>)}
161172
{...commonProps}
162-
ref={ref as React.Ref<HTMLButtonElement>}
163-
type={type}
164-
disabled={disabled || loading}
165-
aria-pressed={selected}
173+
{...linkProps}
174+
ref={ref as React.Ref<HTMLAnchorElement>}
175+
aria-disabled={disabled || loading}
166176
>
167177
{prepareChildren(children)}
168-
</button>
178+
</a>
169179
);
170180
}
181+
182+
return (
183+
<button
184+
{...(extraProps as React.ButtonHTMLAttributes<HTMLButtonElement>)}
185+
{...commonProps}
186+
ref={ref as React.Ref<HTMLButtonElement>}
187+
type={type}
188+
disabled={disabled || loading}
189+
aria-pressed={selected}
190+
>
191+
{prepareChildren(children)}
192+
</button>
193+
);
171194
});
172195

173196
ButtonWithHandlers.displayName = 'Button';

src/components/Link/Link.tsx

+16-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import React from 'react';
44

5-
import type {DOMProps, QAProps} from '../types';
5+
import {useLinkProps} from '../lab/router/router';
6+
import type {DOMProps, Href, QAProps, RouterOptions} from '../types';
67
import {block} from '../utils/cn';
78
import {eventBroker} from '../utils/event-broker';
89

@@ -15,7 +16,7 @@ export interface LinkProps extends DOMProps, QAProps {
1516
visitable?: boolean;
1617
underline?: boolean;
1718
title?: string;
18-
href: string;
19+
href: Href;
1920
target?: string;
2021
rel?: string;
2122
id?: string;
@@ -24,6 +25,7 @@ export interface LinkProps extends DOMProps, QAProps {
2425
onFocus?: React.FocusEventHandler<HTMLAnchorElement>;
2526
onBlur?: React.FocusEventHandler<HTMLAnchorElement>;
2627
extraProps?: React.AnchorHTMLAttributes<HTMLAnchorElement>;
28+
routerOptions?: RouterOptions;
2729
}
2830

2931
const b = block('link');
@@ -46,18 +48,22 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link
4648
style,
4749
className,
4850
qa,
51+
routerOptions,
4952
},
5053
ref,
5154
) {
52-
const handleClickCapture = React.useCallback((event: React.SyntheticEvent) => {
55+
const handleClickCapture = (event: React.SyntheticEvent) => {
5356
eventBroker.publish({
5457
componentId: 'Link',
5558
eventId: 'click',
5659
domEvent: event,
5760
});
58-
}, []);
61+
};
5962

6063
const commonProps = {
64+
href,
65+
target,
66+
rel,
6167
title,
6268
onClick,
6369
onClickCapture: handleClickCapture,
@@ -69,10 +75,13 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link
6975
'data-qa': qa,
7076
};
7177

72-
const relProp = target === '_blank' && !rel ? 'noopener noreferrer' : rel;
73-
7478
return (
75-
<a {...extraProps} {...commonProps} ref={ref} href={href} target={target} rel={relProp}>
79+
<a
80+
{...extraProps}
81+
{...commonProps}
82+
{...useLinkProps({...extraProps, ...commonProps, routerOptions})}
83+
ref={ref}
84+
>
7685
{children}
7786
</a>
7887
);

src/components/Menu/MenuItem.tsx

+29-13
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import React from 'react';
44

55
import {useActionHandlers} from '../../hooks';
6-
import type {DOMProps, QAProps} from '../types';
6+
import {useLinkProps} from '../lab/router/router';
7+
import type {DOMProps, Href, QAProps, RouterOptions} from '../types';
78
import {block} from '../utils/cn';
89
import {eventBroker} from '../utils/event-broker';
910

@@ -18,7 +19,7 @@ export interface MenuItemProps extends DOMProps, QAProps {
1819
disabled?: boolean;
1920
active?: boolean;
2021
selected?: boolean;
21-
href?: string;
22+
href?: Href;
2223
target?: string;
2324
rel?: string;
2425
onClick?: React.MouseEventHandler<HTMLDivElement | HTMLAnchorElement>;
@@ -27,6 +28,7 @@ export interface MenuItemProps extends DOMProps, QAProps {
2728
| React.HTMLAttributes<HTMLDivElement>
2829
| React.AnchorHTMLAttributes<HTMLAnchorElement>;
2930
children?: React.ReactNode;
31+
routerOptions?: RouterOptions;
3032
}
3133

3234
export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>(function MenuItem(
@@ -38,20 +40,31 @@ export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>(function Me
3840
disabled,
3941
active,
4042
selected,
41-
href,
42-
target,
43-
rel,
4443
onClick,
4544
style,
4645
className,
4746
theme,
4847
extraProps,
4948
children,
5049
qa,
50+
...props
5151
},
5252
ref,
5353
) {
54-
const {onKeyDown} = useActionHandlers(onClick);
54+
const handleClick = (e: React.MouseEvent<HTMLDivElement | HTMLAnchorElement>) => {
55+
if (disabled) {
56+
e.preventDefault();
57+
return;
58+
}
59+
60+
if (typeof onClick === 'function') {
61+
onClick(e);
62+
}
63+
};
64+
65+
const linkProps = useLinkProps({...extraProps, ...props, onClick: handleClick});
66+
67+
const {onKeyDown} = useActionHandlers(linkProps.onClick);
5568

5669
const handleClickCapture = React.useCallback((event: React.SyntheticEvent) => {
5770
eventBroker.publish({
@@ -63,18 +76,24 @@ export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>(function Me
6376

6477
const defaultProps = {
6578
role: 'menuitem',
66-
onKeyDown: onClick && !disabled ? onKeyDown : undefined,
79+
onKeyDown,
6780
};
6881

6982
const commonProps = {
83+
...linkProps,
7084
title,
71-
onClick: disabled ? undefined : onClick,
7285
onClickCapture: disabled ? undefined : handleClickCapture,
7386
style,
7487
tabIndex: disabled ? -1 : 0,
7588
className: b(
7689
'item',
77-
{disabled, active, selected, theme, interactive: Boolean(onClick) || Boolean(href)},
90+
{
91+
disabled,
92+
active,
93+
selected,
94+
theme,
95+
interactive: Boolean(onClick) || Boolean(props.href),
96+
},
7897
className,
7998
),
8099
'data-qa': qa,
@@ -96,15 +115,12 @@ export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>(function Me
96115
];
97116
let item;
98117

99-
if (href) {
118+
if (props.href) {
100119
item = (
101120
<a
102121
{...defaultProps}
103122
{...(extraProps as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
104123
{...commonProps}
105-
href={href}
106-
target={target}
107-
rel={rel}
108124
>
109125
{content}
110126
</a>

src/components/Toc/TocItem/TocItem.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import React from 'react';
44

55
import {useActionHandlers} from '../../../hooks';
6+
import {useLinkProps} from '../../lab/router/router';
67
import {block} from '../../utils/cn';
78
import type {TocItem as TocItemType} from '../types';
89

@@ -29,6 +30,8 @@ export const TocItem = (props: TocItemProps) => {
2930

3031
const {onKeyDown} = useActionHandlers(handleClick);
3132

33+
const linkProps = useLinkProps({...props, onClick: handleClick});
34+
3235
const item =
3336
href === undefined ? (
3437
<div
@@ -41,7 +44,7 @@ export const TocItem = (props: TocItemProps) => {
4144
{content}
4245
</div>
4346
) : (
44-
<a href={href} onClick={handleClick} className={b('section-link')}>
47+
<a {...linkProps} className={b('section-link')}>
4548
{content}
4649
</a>
4750
);

src/components/Toc/types.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import type {Href, RouterOptions} from '../types';
2+
13
export interface TocItem {
24
value?: string;
35
content?: React.ReactNode;
4-
href?: string;
6+
href?: Href;
57
items?: TocItem[];
8+
routerOptions?: RouterOptions;
69
}

src/components/lab/Breadcrumbs/BreadcrumbItem.tsx

+6-19
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@
22

33
import React from 'react';
44

5-
import type {Href, RouterOptions} from '../../types';
65
import {filterDOMProps} from '../../utils/filterDOMProps';
6+
import {useLinkProps} from '../router/router';
77

88
import type {BreadcrumbsItemProps} from './Breadcrumbs';
9-
import {b, shouldClientNavigate} from './utils';
9+
import {b} from './utils';
1010

1111
interface BreadcrumbProps extends BreadcrumbsItemProps {
1212
onAction?: () => void;
1313
current?: boolean;
1414
itemType?: 'link' | 'menu';
1515
disabled?: boolean;
16-
navigate?: (href: Href, routerOptions: RouterOptions | undefined) => void;
1716
}
1817
export function BreadcrumbItem(props: BreadcrumbProps) {
1918
const Element = props.href ? 'a' : 'span';
@@ -33,14 +32,6 @@ export function BreadcrumbItem(props: BreadcrumbProps) {
3332
if (typeof props.onAction === 'function') {
3433
props.onAction();
3534
}
36-
37-
const target = event.currentTarget;
38-
if (typeof props.navigate === 'function' && target instanceof HTMLAnchorElement) {
39-
if (props.href && !event.isDefaultPrevented() && shouldClientNavigate(target, event)) {
40-
event.preventDefault();
41-
props.navigate(props.href, props.routerOptions);
42-
}
43-
}
4435
};
4536

4637
const isDisabled = props.disabled || props.current;
@@ -49,14 +40,10 @@ export function BreadcrumbItem(props: BreadcrumbProps) {
4940
onClick: handleAction,
5041
'aria-disabled': isDisabled ? true : undefined,
5142
};
43+
44+
const linkDomProps = useLinkProps({...props, onClick: handleAction});
5245
if (Element === 'a') {
53-
linkProps.href = props.href;
54-
linkProps.hrefLang = props.hrefLang;
55-
linkProps.target = props.target;
56-
linkProps.rel = props.target === '_blank' && !props.rel ? 'noopener noreferrer' : props.rel;
57-
linkProps.download = props.download;
58-
linkProps.ping = props.ping;
59-
linkProps.referrerPolicy = props.referrerPolicy;
46+
linkProps = {...linkProps, ...linkDomProps};
6047
} else {
6148
linkProps.role = 'link';
6249
linkProps.tabIndex = isDisabled ? undefined : 0;
@@ -68,7 +55,7 @@ export function BreadcrumbItem(props: BreadcrumbProps) {
6855
}
6956

7057
if (props.current) {
71-
linkProps['aria-current'] = 'page';
58+
linkProps['aria-current'] = props['aria-current'] ?? 'page';
7259
}
7360

7461
if (props.itemType === 'menu') {

0 commit comments

Comments
 (0)