Tab Link List
View .mdRoute-aware CoreAI TabList for React Router navigation.
Default
Loading interactive example. Full source is available below.
View example source
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { MemoryRouter } from "react-router-dom";
import { Routes } from "react-router-dom";
import { Route } from "react-router-dom";
import type { TabListProps as CoreAITabListProps } from "@coreai-ds/react-components";
import type { TabProps as CoreAITabProps } from "@coreai-ds/react-components";
import type { NavLinkProps } from "react-router-dom";
import * as React from "react";
import { useLocation } from "react-router-dom";
import { useResolvedPath } from "react-router-dom";
import { parsePath } from "react-router-dom";
import { resolvePath } from "react-router-dom";
import { matchPath } from "react-router-dom";
import { TabList as CoreAITabList } from "@coreai-ds/react-components";
import type { LinkProps as RouterLinkProps } from "react-router-dom";
import type { NavLinkProps as RouterNavLinkProps } from "react-router-dom";
import { NavLink as RouterNavLink } from "react-router-dom";
import { Tab as CoreAITab } from "@coreai-ds/react-components";
type TabRouterProps = Omit<NavLinkProps, 'children' | 'className' | 'style'>;
type TabLinkProps = Omit<CoreAITabProps, keyof TabRouterProps | 'as' | 'value'> & TabRouterProps;
interface TabLinkListProps extends Omit<CoreAITabListProps, 'children' | 'selectedValue'> {
links: Array<Omit<TabLinkProps, 'children'> & {
label: React.ReactNode;
}>;
}
function pathFor(to: NavLinkProps['to']): string {
return typeof to === 'string' ? to : (to.pathname ?? '');
}
const routerLinkPropNames = [
'defaultShouldRevalidate',
'discover',
'download',
'hrefLang',
'mask',
'media',
'ping',
'prefetch',
'preventScrollReset',
'referrerPolicy',
'relative',
'rel',
'reloadDocument',
'replace',
'state',
'target',
'to',
'type',
'viewTransition',
] as const satisfies readonly (keyof RouterLinkProps)[];
type RouterLinkSlotProps = Pick<RouterLinkProps, (typeof routerLinkPropNames)[number]>;
type RouterNavLinkSlotProps = RouterLinkSlotProps & Pick<RouterNavLinkProps, 'caseSensitive' | 'end'>;
type SlotRootProps = React.ComponentPropsWithoutRef<'a'> & {
as?: unknown;
disabled?: boolean;
value?: unknown;
};
type ComponentProps<T> = Omit<T, keyof RouterLinkSlotProps>;
function splitRouterLinkProps<T extends RouterLinkSlotProps>(props: T): [
RouterLinkSlotProps,
ComponentProps<T>
] {
const routerProps: Record<string, unknown> = {};
const componentProps: Record<string, unknown> = { ...props };
for (const name of routerLinkPropNames) {
routerProps[name] = props[name];
delete componentProps[name];
}
return [routerProps as RouterLinkSlotProps, componentProps as ComponentProps<T>];
}
function useStableRouterLinkProps(props: RouterLinkSlotProps): RouterLinkSlotProps {
const { defaultShouldRevalidate, discover, download, hrefLang, mask, media, ping, prefetch, preventScrollReset, referrerPolicy, relative, rel, reloadDocument, replace, state, target, to, type, viewTransition, } = props;
return React.useMemo(() => ({
defaultShouldRevalidate,
discover,
download,
hrefLang,
mask,
media,
ping,
prefetch,
preventScrollReset,
referrerPolicy,
relative,
rel,
reloadDocument,
replace,
state,
target,
to,
type,
viewTransition,
}), [
defaultShouldRevalidate,
discover,
download,
hrefLang,
mask,
media,
ping,
prefetch,
preventScrollReset,
referrerPolicy,
relative,
rel,
reloadDocument,
replace,
state,
target,
to,
type,
viewTransition,
]);
}
function useRouterNavLinkSlot<T extends RouterNavLinkSlotProps>(props: T, mode: 'anchor' | 'tab' = 'anchor'): [
React.ForwardRefExoticComponent<SlotRootProps & React.RefAttributes<HTMLAnchorElement>>,
Omit<ComponentProps<T>, 'caseSensitive' | 'end'>
] {
const { caseSensitive, end, ...linkProps } = props;
const [routerProps, componentProps] = splitRouterLinkProps(linkProps as T);
const stableRouterProps = useStableRouterLinkProps(routerProps);
const RouterNavLinkSlot = React.useMemo(() => React.forwardRef<HTMLAnchorElement, SlotRootProps>(({ as: _as, disabled, href: _href, onClick, tabIndex, type: _type, value: _value, ...slotProps }, slotRef) => {
const handleClick: React.MouseEventHandler<HTMLAnchorElement> = (event) => {
if (mode === 'tab' && disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
onClick?.(event);
};
return (<RouterNavLink {...slotProps} {...stableRouterProps} target={stableRouterProps.target ?? slotProps.target} rel={stableRouterProps.rel ?? slotProps.rel} aria-disabled={mode === 'tab' && disabled ? true : undefined} caseSensitive={caseSensitive} end={end} onClick={handleClick} ref={slotRef} tabIndex={mode === 'tab' && disabled ? -1 : tabIndex}/>);
}), [caseSensitive, end, mode, stableRouterProps]);
return [RouterNavLinkSlot, componentProps];
}
const TabLink = React.forwardRef<HTMLAnchorElement, TabLinkProps>((props, ref) => {
const value = typeof props.to === 'string' ? props.to : (props.to.pathname ?? '');
const [RouterLinkSlot, tabProps] = useRouterNavLinkSlot(props, 'tab');
return (<CoreAITab {...(tabProps as CoreAITabProps)} as={RouterLinkSlot as unknown as 'button'} ref={ref as unknown as React.Ref<HTMLButtonElement>} value={value}/>);
});
function TabLinkList({ links, ...props }: TabLinkListProps): React.JSX.Element {
const { pathname } = useLocation();
const base = useResolvedPath('.').pathname;
const selectedValue = links.find(({ caseSensitive, end, to }) => {
const destination = typeof to === 'string' ? parsePath(to) : to;
const path = resolvePath(destination, destination.pathname ? base : pathname).pathname;
return matchPath({ path, caseSensitive, end: path === '/' || (end ?? false) }, pathname);
});
return (<CoreAITabList {...props} selectedValue={selectedValue ? pathFor(selectedValue.to) : ''}>
{links.map(({ label, ...linkProps }) => (<TabLink key={pathFor(linkProps.to)} {...linkProps}>
{label}
</TabLink>))}
</CoreAITabList>);
}
export const Default = () => (<MemoryRouter initialEntries={['/projects/coreai/overview']}>
<Routes>
<Route path="/projects/coreai" element={<TabLinkList aria-label="Project sections" links={[
{ label: 'Overview', to: 'overview' },
{ label: 'Activity', to: 'activity?view=recent' },
]}/>}>
<Route path=":section" element={null}/>
</Route>
</Routes>
</MemoryRouter>);
export default Default;
Using the React Router template
Copy all files listed under Code into your project together. The
RouterLinkSlots.tsx helper is supplied with the template source; it is not an
npm export.
Implementation
CoreAI 0.12.0; Fluent 9.74.9.
Copy this template into your app. It is not an npm export. Adapt actions and navigation to your product.
Template source: TabLinkList.tsx
Copy this file into your app. Do not import this template from the package.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as React from 'react';
import {
TabList as CoreAITabList,
type TabListProps as CoreAITabListProps,
} from '@coreai-ds/react-components';
import {
matchPath,
parsePath,
resolvePath,
useLocation,
useResolvedPath,
type NavLinkProps,
} from 'react-router-dom';
import { TabLink, type TabLinkProps } from './TabLink';
export interface TabLinkListProps extends Omit<CoreAITabListProps, 'children' | 'selectedValue'> {
links: Array<Omit<TabLinkProps, 'children'> & { label: React.ReactNode }>;
}
function pathFor(to: NavLinkProps['to']): string {
return typeof to === 'string' ? to : (to.pathname ?? '');
}
export function TabLinkList({ links, ...props }: TabLinkListProps): React.JSX.Element {
const { pathname } = useLocation();
const base = useResolvedPath('.').pathname;
const selectedValue = links.find(({ caseSensitive, end, to }) => {
const destination = typeof to === 'string' ? parsePath(to) : to;
const path = resolvePath(destination, destination.pathname ? base : pathname).pathname;
return matchPath({ path, caseSensitive, end: path === '/' || (end ?? false) }, pathname);
});
return (
<CoreAITabList {...props} selectedValue={selectedValue ? pathFor(selectedValue.to) : ''}>
{links.map(({ label, ...linkProps }) => (
<TabLink key={pathFor(linkProps.to)} {...linkProps}>
{label}
</TabLink>
))}
</CoreAITabList>
);
}
Template source: TabLink.tsx
Copy this file into your app. Do not import this template from the package.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as React from 'react';
import { Tab as CoreAITab, type TabProps as CoreAITabProps } from '@coreai-ds/react-components';
import type { NavLinkProps } from 'react-router-dom';
import { useRouterNavLinkSlot } from './RouterLinkSlots';
type TabRouterProps = Omit<NavLinkProps, 'children' | 'className' | 'style'>;
export type TabLinkProps = Omit<CoreAITabProps, keyof TabRouterProps | 'as' | 'value'> &
TabRouterProps;
export const TabLink = React.forwardRef<HTMLAnchorElement, TabLinkProps>((props, ref) => {
const value = typeof props.to === 'string' ? props.to : (props.to.pathname ?? '');
const [RouterLinkSlot, tabProps] = useRouterNavLinkSlot(props, 'tab');
return (
<CoreAITab
{...(tabProps as CoreAITabProps)}
as={RouterLinkSlot as unknown as 'button'}
ref={ref as unknown as React.Ref<HTMLButtonElement>}
value={value}
/>
);
});
TabLink.displayName = 'TabLink';
Template source: RouterLinkSlots.tsx
Copy this file into your app. Do not import this template from the package.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import * as React from 'react';
import {
Link as RouterLink,
NavLink as RouterNavLink,
type LinkProps as RouterLinkProps,
type NavLinkProps as RouterNavLinkProps,
} from 'react-router-dom';
const routerLinkPropNames = [
'defaultShouldRevalidate',
'discover',
'download',
'hrefLang',
'mask',
'media',
'ping',
'prefetch',
'preventScrollReset',
'referrerPolicy',
'relative',
'rel',
'reloadDocument',
'replace',
'state',
'target',
'to',
'type',
'viewTransition',
] as const satisfies readonly (keyof RouterLinkProps)[];
type RouterLinkSlotProps = Pick<RouterLinkProps, (typeof routerLinkPropNames)[number]>;
type ComponentProps<T> = Omit<T, keyof RouterLinkSlotProps>;
type SlotRootProps = React.ComponentPropsWithoutRef<'a'> & {
as?: unknown;
disabled?: boolean;
value?: unknown;
};
function splitRouterLinkProps<T extends RouterLinkSlotProps>(
props: T,
): [RouterLinkSlotProps, ComponentProps<T>] {
const routerProps: Record<string, unknown> = {};
const componentProps: Record<string, unknown> = { ...props };
for (const name of routerLinkPropNames) {
routerProps[name] = props[name];
delete componentProps[name];
}
return [routerProps as RouterLinkSlotProps, componentProps as ComponentProps<T>];
}
function useStableRouterLinkProps(props: RouterLinkSlotProps): RouterLinkSlotProps {
const {
defaultShouldRevalidate,
discover,
download,
hrefLang,
mask,
media,
ping,
prefetch,
preventScrollReset,
referrerPolicy,
relative,
rel,
reloadDocument,
replace,
state,
target,
to,
type,
viewTransition,
} = props;
return React.useMemo(
() => ({
defaultShouldRevalidate,
discover,
download,
hrefLang,
mask,
media,
ping,
prefetch,
preventScrollReset,
referrerPolicy,
relative,
rel,
reloadDocument,
replace,
state,
target,
to,
type,
viewTransition,
}),
[
defaultShouldRevalidate,
discover,
download,
hrefLang,
mask,
media,
ping,
prefetch,
preventScrollReset,
referrerPolicy,
relative,
rel,
reloadDocument,
replace,
state,
target,
to,
type,
viewTransition,
],
);
}
export function useRouterLinkSlot<T extends RouterLinkSlotProps>(
props: T,
): [
React.ForwardRefExoticComponent<SlotRootProps & React.RefAttributes<HTMLAnchorElement>>,
ComponentProps<T>,
] {
const [routerProps, componentProps] = splitRouterLinkProps(props);
const stableRouterProps = useStableRouterLinkProps(routerProps);
const RouterLinkSlot = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, SlotRootProps>(
(
{ as: _as, disabled, href: _href, onClick, tabIndex, type: _type, ...slotProps },
slotRef,
) => {
const isDisabled =
disabled ||
slotProps['aria-disabled'] === true ||
slotProps['aria-disabled'] === 'true';
const handleClick: React.MouseEventHandler<HTMLAnchorElement> = (event) => {
if (isDisabled) {
event.preventDefault();
event.stopPropagation();
return;
}
onClick?.(event);
};
return (
<RouterLink
{...slotProps}
{...stableRouterProps}
target={stableRouterProps.target ?? slotProps.target}
rel={stableRouterProps.rel ?? slotProps.rel}
aria-disabled={isDisabled ? true : slotProps['aria-disabled']}
onClick={handleClick}
ref={slotRef}
tabIndex={tabIndex ?? (isDisabled ? -1 : undefined)}
/>
);
},
),
[stableRouterProps],
);
return [RouterLinkSlot, componentProps];
}
type RouterNavLinkSlotProps = RouterLinkSlotProps &
Pick<RouterNavLinkProps, 'caseSensitive' | 'end'>;
export function useRouterNavLinkSlot<T extends RouterNavLinkSlotProps>(
props: T,
mode: 'anchor' | 'tab' = 'anchor',
): [
React.ForwardRefExoticComponent<SlotRootProps & React.RefAttributes<HTMLAnchorElement>>,
Omit<ComponentProps<T>, 'caseSensitive' | 'end'>,
] {
const { caseSensitive, end, ...linkProps } = props;
const [routerProps, componentProps] = splitRouterLinkProps(linkProps as T);
const stableRouterProps = useStableRouterLinkProps(routerProps);
const RouterNavLinkSlot = React.useMemo(
() =>
React.forwardRef<HTMLAnchorElement, SlotRootProps>(
(
{
as: _as,
disabled,
href: _href,
onClick,
tabIndex,
type: _type,
value: _value,
...slotProps
},
slotRef,
) => {
const handleClick: React.MouseEventHandler<HTMLAnchorElement> = (event) => {
if (mode === 'tab' && disabled) {
event.preventDefault();
event.stopPropagation();
return;
}
onClick?.(event);
};
return (
<RouterNavLink
{...slotProps}
{...stableRouterProps}
target={stableRouterProps.target ?? slotProps.target}
rel={stableRouterProps.rel ?? slotProps.rel}
aria-disabled={mode === 'tab' && disabled ? true : undefined}
caseSensitive={caseSensitive}
end={end}
onClick={handleClick}
ref={slotRef}
tabIndex={mode === 'tab' && disabled ? -1 : tabIndex}
/>
);
},
),
[caseSensitive, end, mode, stableRouterProps],
);
return [RouterNavLinkSlot, componentProps];
}