Skip to content

Commit

Permalink
feat: client side routing
Browse files Browse the repository at this point in the history
  • Loading branch information
ValeraS committed Nov 7, 2024
1 parent 75be05e commit 2f6be54
Show file tree
Hide file tree
Showing 15 changed files with 506 additions and 197 deletions.
67 changes: 45 additions & 22 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import React from 'react';

import type {DOMProps, QAProps} from '../types';
import {useLinkProps} from '../lab/router/router';
import type {DOMProps, Href, QAProps, RouterOptions} from '../types';
import {block} from '../utils/cn';
import {isIcon, isSvg} from '../utils/common';
import {eventBroker} from '../utils/event-broker';
Expand Down Expand Up @@ -49,7 +50,7 @@ export interface ButtonProps extends DOMProps, QAProps {
id?: string;
type?: 'button' | 'submit' | 'reset';
component?: React.ElementType;
href?: string;
href?: Href;
target?: string;
rel?: string;
extraProps?:
Expand All @@ -62,6 +63,7 @@ export interface ButtonProps extends DOMProps, QAProps {
onBlur?: React.FocusEventHandler<HTMLButtonElement | HTMLAnchorElement>;
/** Button content. You can mix button text with `<Icon/>` component */
children?: React.ReactNode;
routerOptions?: RouterOptions;
}

const b = block('button');
Expand All @@ -79,9 +81,6 @@ const ButtonWithHandlers = React.forwardRef<HTMLElement, ButtonProps>(function B
tabIndex,
type = 'button',
component,
href,
target,
rel,
extraProps,
onClick,
onMouseEnter,
Expand All @@ -93,6 +92,7 @@ const ButtonWithHandlers = React.forwardRef<HTMLElement, ButtonProps>(function B
style,
className,
qa,
...props
},
ref,
) {
Expand Down Expand Up @@ -137,37 +137,60 @@ const ButtonWithHandlers = React.forwardRef<HTMLElement, ButtonProps>(function B
'data-qa': qa,
};

if (typeof href === 'string' || component) {
const linkProps = {
href,
target,
rel: target === '_blank' && !rel ? 'noopener noreferrer' : rel,
};
const linkProps = useLinkProps({
...extraProps,
...props,
onClick: (e) => {
if (disabled) {
e.preventDefault();
return;
}

if (typeof onClick === 'function') {
onClick(e);
}
},
});

if (component) {
return React.createElement(
component || 'a',
component,
{
...extraProps,
...commonProps,
...(component ? {} : linkProps),
ref: ref as React.Ref<HTMLAnchorElement>,
ref,
'aria-disabled': disabled || loading,
},
prepareChildren(children),
);
} else {
}

if (props.href) {
return (
<button
{...(extraProps as React.ButtonHTMLAttributes<HTMLButtonElement>)}
<a
{...(extraProps as React.ButtonHTMLAttributes<HTMLAnchorElement>)}
{...commonProps}
ref={ref as React.Ref<HTMLButtonElement>}
type={type}
disabled={disabled || loading}
aria-pressed={selected}
{...linkProps}
ref={ref as React.Ref<HTMLAnchorElement>}
aria-disabled={disabled || loading}
>
{prepareChildren(children)}
</button>
</a>
);
}

return (
<button
{...(extraProps as React.ButtonHTMLAttributes<HTMLButtonElement>)}
{...commonProps}
ref={ref as React.Ref<HTMLButtonElement>}
type={type}
disabled={disabled || loading}
aria-pressed={selected}
>
{prepareChildren(children)}
</button>
);
});

ButtonWithHandlers.displayName = 'Button';
Expand Down
23 changes: 16 additions & 7 deletions src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import React from 'react';

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

Expand All @@ -15,7 +16,7 @@ export interface LinkProps extends DOMProps, QAProps {
visitable?: boolean;
underline?: boolean;
title?: string;
href: string;
href: Href;
target?: string;
rel?: string;
id?: string;
Expand All @@ -24,6 +25,7 @@ export interface LinkProps extends DOMProps, QAProps {
onFocus?: React.FocusEventHandler<HTMLAnchorElement>;
onBlur?: React.FocusEventHandler<HTMLAnchorElement>;
extraProps?: React.AnchorHTMLAttributes<HTMLAnchorElement>;
routerOptions?: RouterOptions;
}

const b = block('link');
Expand All @@ -46,18 +48,22 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link
style,
className,
qa,
routerOptions,
},
ref,
) {
const handleClickCapture = React.useCallback((event: React.SyntheticEvent) => {
const handleClickCapture = (event: React.SyntheticEvent) => {
eventBroker.publish({
componentId: 'Link',
eventId: 'click',
domEvent: event,
});
}, []);
};

const commonProps = {
href,
target,
rel,
title,
onClick,
onClickCapture: handleClickCapture,
Expand All @@ -69,10 +75,13 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link
'data-qa': qa,
};

const relProp = target === '_blank' && !rel ? 'noopener noreferrer' : rel;

return (
<a {...extraProps} {...commonProps} ref={ref} href={href} target={target} rel={relProp}>
<a
{...extraProps}
{...commonProps}
{...useLinkProps({...extraProps, ...commonProps, routerOptions})}
ref={ref}
>
{children}
</a>
);
Expand Down
42 changes: 29 additions & 13 deletions src/components/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import React from 'react';

import {useActionHandlers} from '../../hooks';
import type {DOMProps, QAProps} from '../types';
import {useLinkProps} from '../lab/router/router';
import type {DOMProps, Href, QAProps, RouterOptions} from '../types';
import {block} from '../utils/cn';
import {eventBroker} from '../utils/event-broker';

Expand All @@ -18,7 +19,7 @@ export interface MenuItemProps extends DOMProps, QAProps {
disabled?: boolean;
active?: boolean;
selected?: boolean;
href?: string;
href?: Href;
target?: string;
rel?: string;
onClick?: React.MouseEventHandler<HTMLDivElement | HTMLAnchorElement>;
Expand All @@ -27,6 +28,7 @@ export interface MenuItemProps extends DOMProps, QAProps {
| React.HTMLAttributes<HTMLDivElement>
| React.AnchorHTMLAttributes<HTMLAnchorElement>;
children?: React.ReactNode;
routerOptions?: RouterOptions;
}

export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>(function MenuItem(
Expand All @@ -38,20 +40,31 @@ export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>(function Me
disabled,
active,
selected,
href,
target,
rel,
onClick,
style,
className,
theme,
extraProps,
children,
qa,
...props
},
ref,
) {
const {onKeyDown} = useActionHandlers(onClick);
const handleClick = (e: React.MouseEvent<HTMLDivElement | HTMLAnchorElement>) => {
if (disabled) {
e.preventDefault();
return;
}

if (typeof onClick === 'function') {
onClick(e);
}
};

const linkProps = useLinkProps({...extraProps, ...props, onClick: handleClick});

const {onKeyDown} = useActionHandlers(linkProps.onClick);

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

const defaultProps = {
role: 'menuitem',
onKeyDown: onClick && !disabled ? onKeyDown : undefined,
onKeyDown,
};

const commonProps = {
...linkProps,
title,
onClick: disabled ? undefined : onClick,
onClickCapture: disabled ? undefined : handleClickCapture,
style,
tabIndex: disabled ? -1 : 0,
className: b(
'item',
{disabled, active, selected, theme, interactive: Boolean(onClick) || Boolean(href)},
{
disabled,
active,
selected,
theme,
interactive: Boolean(onClick) || Boolean(props.href),
},
className,
),
'data-qa': qa,
Expand All @@ -96,15 +115,12 @@ export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>(function Me
];
let item;

if (href) {
if (props.href) {
item = (
<a
{...defaultProps}
{...(extraProps as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
{...commonProps}
href={href}
target={target}
rel={rel}
>
{content}
</a>
Expand Down
5 changes: 4 additions & 1 deletion src/components/Toc/TocItem/TocItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React from 'react';

import {useActionHandlers} from '../../../hooks';
import {useLinkProps} from '../../lab/router/router';
import {block} from '../../utils/cn';
import type {TocItem as TocItemType} from '../types';

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

const {onKeyDown} = useActionHandlers(handleClick);

const linkProps = useLinkProps({...props, onClick: handleClick});

const item =
href === undefined ? (
<div
Expand All @@ -41,7 +44,7 @@ export const TocItem = (props: TocItemProps) => {
{content}
</div>
) : (
<a href={href} onClick={handleClick} className={b('section-link')}>
<a {...linkProps} className={b('section-link')}>
{content}
</a>
);
Expand Down
5 changes: 4 additions & 1 deletion src/components/Toc/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type {Href, RouterOptions} from '../types';

export interface TocItem {
value?: string;
content?: React.ReactNode;
href?: string;
href?: Href;
items?: TocItem[];
routerOptions?: RouterOptions;
}
25 changes: 6 additions & 19 deletions src/components/lab/Breadcrumbs/BreadcrumbItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@

import React from 'react';

import type {Href, RouterOptions} from '../../types';
import {filterDOMProps} from '../../utils/filterDOMProps';
import {useLinkProps} from '../router/router';

import type {BreadcrumbsItemProps} from './Breadcrumbs';
import {b, shouldClientNavigate} from './utils';
import {b} from './utils';

interface BreadcrumbProps extends BreadcrumbsItemProps {
onAction?: () => void;
current?: boolean;
itemType?: 'link' | 'menu';
disabled?: boolean;
navigate?: (href: Href, routerOptions: RouterOptions | undefined) => void;
}
export function BreadcrumbItem(props: BreadcrumbProps) {
const Element = props.href ? 'a' : 'span';
Expand All @@ -33,14 +32,6 @@ export function BreadcrumbItem(props: BreadcrumbProps) {
if (typeof props.onAction === 'function') {
props.onAction();
}

const target = event.currentTarget;
if (typeof props.navigate === 'function' && target instanceof HTMLAnchorElement) {
if (props.href && !event.isDefaultPrevented() && shouldClientNavigate(target, event)) {
event.preventDefault();
props.navigate(props.href, props.routerOptions);
}
}
};

const isDisabled = props.disabled || props.current;
Expand All @@ -49,14 +40,10 @@ export function BreadcrumbItem(props: BreadcrumbProps) {
onClick: handleAction,
'aria-disabled': isDisabled ? true : undefined,
};

const linkDomProps = useLinkProps({...props, onClick: handleAction});
if (Element === 'a') {
linkProps.href = props.href;
linkProps.hrefLang = props.hrefLang;
linkProps.target = props.target;
linkProps.rel = props.target === '_blank' && !props.rel ? 'noopener noreferrer' : props.rel;
linkProps.download = props.download;
linkProps.ping = props.ping;
linkProps.referrerPolicy = props.referrerPolicy;
linkProps = {...linkProps, ...linkDomProps};
} else {
linkProps.role = 'link';
linkProps.tabIndex = isDisabled ? undefined : 0;
Expand All @@ -68,7 +55,7 @@ export function BreadcrumbItem(props: BreadcrumbProps) {
}

if (props.current) {
linkProps['aria-current'] = 'page';
linkProps['aria-current'] = props['aria-current'] ?? 'page';
}

if (props.itemType === 'menu') {
Expand Down
Loading

0 comments on commit 2f6be54

Please sign in to comment.