Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Breadcrumbs): replace navigate with custom item component #2054

Merged
merged 3 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 0 additions & 96 deletions src/components/Breadcrumbs/BreadcrumbItem.tsx

This file was deleted.

37 changes: 17 additions & 20 deletions src/components/Breadcrumbs/Breadcrumbs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ $block: '.#{variables.$ns}breadcrumbs';
&:last-child {
font-weight: var(--g-text-accent-font-weight);
overflow: hidden;
margin: -2px;
padding: 2px;

#{$block}__link {
@include mixins.overflow-ellipsis();
Expand Down Expand Up @@ -65,6 +67,7 @@ $block: '.#{variables.$ns}breadcrumbs';

&:focus-visible {
outline: 2px solid var(--g-color-line-focus);
outline-offset: 0;
}
}

Expand All @@ -77,34 +80,28 @@ $block: '.#{variables.$ns}breadcrumbs';

&__more-button {
--g-button-border-radius: var(--g-focus-border-radius);
--g-button-focus-outline-offset: -2px;
}

&__menu {
margin-inline: calc(-1 * var(--g-spacing-2));
}

&__item:first-child &__menu {
margin-inline-start: 0;
}
&-popup {
--g-list-item-view-spacer-size: 8px;
max-width: 320px;
padding: var(--g-spacing-1);
}

&__popup_staircase {
$menu: '.#{variables.$ns}menu';
$staircaseLength: 10;
#{$menu} {
#{$menu}__list-item {
#{$menu}__item[class] {
padding-inline-start: 8px * $staircaseLength;
}
}
&-link {
text-decoration: none;
cursor: default;

@for $i from 0 through $staircaseLength {
#{$menu}__list-item:nth-child(#{$i}) {
#{$menu}__item[class] {
padding-inline-start: 8px * $i;
}
}
&:not([aria-disabled]) {
cursor: pointer;
}
}
}

&__item:first-child &__menu {
margin-inline-start: 0;
}
}
147 changes: 55 additions & 92 deletions src/components/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,27 @@
import * as React from 'react';

import {useForkRef, useResizeObserver} from '../../hooks';
import {Button} from '../Button';
import {DropdownMenu} from '../DropdownMenu';
import type {PopupPlacement} from '../Popup';
import type {AriaLabelingProps, DOMProps, Href, Key, QAProps, RouterOptions} from '../types';
import type {AriaLabelingProps, DOMProps, Key, QAProps} from '../types';
import {filterDOMProps} from '../utils/filterDOMProps';

import {BreadcrumbItem} from './BreadcrumbItem';
import {BreadcrumbsDropdownMenu} from './BreadcrumbsDropdownMenu';
import {BreadcrumbsItem} from './BreadcrumbsItem';
import type {BreadcrumbsItemInnerProps} from './BreadcrumbsItem';
import {BreadcrumbsSeparator} from './BreadcrumbsSeparator';
import i18n from './i18n';
import {b, shouldClientNavigate} from './utils';
import {b} from './utils';

import './Breadcrumbs.scss';

export interface BreadcrumbsItemProps {
children: React.ReactNode;
title?: string;
href?: Href;
hrefLang?: string;
target?: React.HTMLAttributeAnchorTarget;
rel?: string;
download?: boolean | string;
ping?: string;
referrerPolicy?: React.HTMLAttributeReferrerPolicy;
'aria-label'?: string;
'aria-current'?: React.AriaAttributes['aria-current'];
routerOptions?: RouterOptions;
disabled?: boolean;
}

function Item(_props: BreadcrumbsItemProps): React.ReactElement | null {
return null;
}

export interface BreadcrumbsProps extends DOMProps, AriaLabelingProps, QAProps {
id?: string;
showRoot?: boolean;
separator?: React.ReactNode;
maxItems?: number;
popupStyle?: 'staircase';
popupPlacement?: PopupPlacement;
children: React.ReactElement<BreadcrumbsItemProps> | React.ReactElement<BreadcrumbsItemProps>[];
navigate?: (href: Href, routerOptions: RouterOptions | undefined) => void;
itemComponent?: React.ElementType;
children: React.ReactNode;
disabled?: boolean;
onAction?: (key: Key) => void;
}
Expand All @@ -60,7 +39,7 @@
React.Children.forEach(props.children, (child, index) => {
if (React.isValidElement(child)) {
if (child.key === undefined || child.key === null) {
child = React.cloneElement(child, {key: index});

Check warning on line 42 in src/components/Breadcrumbs/Breadcrumbs.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Assignment to function parameter 'child'
}
items.push(child);
}
Expand Down Expand Up @@ -155,7 +134,6 @@
}
});

const {navigate} = props;
let contents = items;
if (items.length > visibleItemsCount) {
contents = [];
Expand All @@ -170,61 +148,40 @@
}
const hiddenItems = breadcrumbs.slice(0, -endItems);
const menuItem = (
<BreadcrumbItem itemType="menu">
<DropdownMenu
items={hiddenItems.map((el, index) => {
return {
...el.props,
text: el.props.children,
disabled: props.disabled,
items: [],
action: (event) => {
if (typeof props.onAction === 'function') {
props.onAction(el.key ?? index);
}

// TODO: move this logic to DropdownMenu
const target = event.currentTarget;
if (
typeof navigate === 'function' &&
target instanceof HTMLAnchorElement
) {
if (el.props.href && shouldClientNavigate(target, event)) {
event.preventDefault();
navigate(el.props.href, el.props.routerOptions);
}
}
},
};
})}
popupProps={{
className: b('popup', {
staircase: props.popupStyle === 'staircase',
}),
placement: props.popupPlacement,
}}
renderSwitcher={({onClick}) => (
<Button
title={i18n('label_more')}
className={b('more-button')}
onClick={onClick}
size="s"
view="flat"
disabled={props.disabled}
>
<Button.Icon>...</Button.Icon>
</Button>
)}
/>
</BreadcrumbItem>
<BreadcrumbsDropdownMenu
disabled={props.disabled}
popupPlacement={props.popupPlacement}
popupStyle={props.popupStyle}
data-breadcrumbs-menu-item={true}
>
{hiddenItems.map((child, index) => {
const Component = props.itemComponent ?? BreadcrumbsItem;
const key = child.key ?? index;
const handleAction = () => {
if (typeof props.onAction === 'function') {
props.onAction(key);
}
};
const innerProps: BreadcrumbsItemInnerProps = {
__index: index,
__disabled: props.disabled || child.props.disabled,
__onAction: handleAction,
};
return (
<Component {...child.props} key={key} {...innerProps}>
{child.props.children}
</Component>
);
})}
</BreadcrumbsDropdownMenu>
);

contents.push(menuItem);
contents.push(...breadcrumbs.slice(-endItems));
}

const lastIndex = contents.length - 1;
const breadcrumbItems = contents.map((child, index) => {
const breadcrumbsItems = contents.map((child, index) => {
const isCurrent = index === lastIndex;
const key = child.key ?? index;
const handleAction = () => {
Expand All @@ -233,18 +190,26 @@
}
};

const {'data-breadcrumbs-menu-item': isMenu, ...childProps} = child.props;
let item: React.ReactNode;
if (isMenu) {
item = child;
} else {
const Component = props.itemComponent ?? BreadcrumbsItem;
const innerProps: BreadcrumbsItemInnerProps = {
__current: isCurrent,
__disabled: props.disabled || childProps.disabled,
__onAction: handleAction,
};
item = (
<Component {...childProps} key={key} {...innerProps}>
{childProps.children}
</Component>
);
}
return (
<li key={index} className={b('item', {calculating: !calculated})}>
<BreadcrumbItem
{...child.props}
key={key}
current={isCurrent}
disabled={props.disabled || child.props.disabled}
onAction={handleAction}
navigate={navigate}
>
{child.props.children}
</BreadcrumbItem>
{item}
{isCurrent ? null : <BreadcrumbsSeparator separator={props.separator} />}
</li>
);
Expand All @@ -257,18 +222,16 @@
className={b(null, props.className)}
style={props.style}
>
{breadcrumbItems}
{breadcrumbsItems}
</ol>
);
}) as unknown as BreadcrumbsComponent;

type BreadcrumbsComponent = React.FunctionComponent<
BreadcrumbsProps & {ref?: React.Ref<HTMLElement>}
> & {
Item: typeof Item;
Item: typeof BreadcrumbsItem;
};

Breadcrumbs.Item = Item;
Breadcrumbs.Item = BreadcrumbsItem;
Breadcrumbs.displayName = 'Breadcrumbs';

export {Item as BreadcrumbsItem};
Loading
Loading