diff --git a/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.classes.ts b/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.classes.ts new file mode 100644 index 0000000..921cfc2 --- /dev/null +++ b/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.classes.ts @@ -0,0 +1,20 @@ +export const breadcrumbClasses = { + root: `lsd-breadcrumb`, + list: `lsd-breadcrumb-list`, + + trigger: `lsd-breadcrumb-trigger`, + triggerLabel: `lsd-breadcrumb-trigger__label`, + triggerIcons: `lsd-breadcrumb-trigger-icons`, + triggerIcon: `lsd-breadcrumb-trigger-icons__icon`, + triggerMenuIcon: `lsd-breadcrumb-trigger-icons__menu-icon`, + + listBox: 'lsd-breadcrumb-list-box', + listBoxLarge: 'lsd-breadcrumb-list-box-large', + listBoxMedium: 'lsd-breadcrumb-list-box-medium', + + open: 'lsd-breadcrumb--open', + disabled: 'lsd-breadcrumb--disabled', + small: `lsd-breadcrumb--small`, + medium: `lsd-breadcrumb--medium`, + large: `lsd-breadcrumb--large`, +} diff --git a/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.stories.tsx b/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.stories.tsx new file mode 100644 index 0000000..b2698f3 --- /dev/null +++ b/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.stories.tsx @@ -0,0 +1,41 @@ +import { Meta, Story } from '@storybook/react' +import { Breadcrumb, BreadcrumbProps } from './Breadcrumb' + +export default { + title: 'Breadcrumb', + component: Breadcrumb, + argTypes: { + size: { + type: { + name: 'enum', + value: ['small', 'medium', 'large'], + }, + }, + maxItems: { + control: { + type: 'number', + min: 2, + max: 6, + }, + }, + }, +} as Meta + +export const Root: Story = (args) => ( +
+ +
+) + +Root.args = { + size: 'large', + disabled: false, + onChange: undefined, + options: new Array(6).fill(null).map((value, index) => ({ + name: `${index}`, + value: `Breadcrumb`, + link: `/${index + 1}`, + })), + ellipsis: false, + maxItems: 6, +} diff --git a/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.styles.ts b/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.styles.ts new file mode 100644 index 0000000..f5b0fbd --- /dev/null +++ b/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.styles.ts @@ -0,0 +1,57 @@ +import { css } from '@emotion/react' +import { breadcrumbClasses } from './Breadcrumb.classes' + +export const BreadcrumbStyles = css` + .${breadcrumbClasses.root} { + } + + .${breadcrumbClasses.list} { + display: flex; + flex-direction: row; + align-items: flex-start; + list-style-type: none; + margin: 0; + padding: 0; + } + + .${breadcrumbClasses.disabled} { + .${breadcrumbClasses.list} { + opacity: 0.34; + cursor: initial; + pointer-events: none; + } + } + + .${breadcrumbClasses.listBox} { + display: flex; + flex-direction: column; + max-height: 400px; + max-width: 148px; + overflow: auto; + border: 1px solid rgb(var(--lsd-border-primary)); + margin-top: 10px; + position: absolute; + } + + // Portal cannot be ralatively positioned + .${breadcrumbClasses.listBoxLarge} { + margin-left: 92px; + } + + // Portal cannot be ralatively positioned + .${breadcrumbClasses.listBoxMedium} { + margin-left: 82px; + } + + .${breadcrumbClasses.listBox} > a { + &:not(:last-child) { + border-bottom: 1px solid rgb(var(--lsd-border-primary)); + } + padding: 8px 12px; + cursor: pointer; + &:hover, + &:focus { + text-decoration: underline; + } + } +` diff --git a/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.tsx b/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000..66f9a27 --- /dev/null +++ b/packages/lsd-react/src/components/Breadcrumb/Breadcrumb.tsx @@ -0,0 +1,133 @@ +import clsx from 'clsx' +import React, { useEffect, useRef, useState } from 'react' +import { BreadcrumbItem } from '../BreadcrumbItem' +import { breadcrumbItemClasses } from '../BreadcrumbItem/BreadcrumbItem.classes' +import { ListBox } from '../ListBox' +import { Portal } from '../PortalProvider/Portal' +import { Typography } from '../Typography' +import { breadcrumbClasses } from './Breadcrumb.classes' + +export type BreadcrumbOption = { + name: string + value: string + link: string +} + +export type BreadcrumbProps = Omit< + React.HTMLAttributes, + 'label' | 'disabled' | 'value' | 'onChange' +> & { + disabled?: boolean + ellipsis?: boolean + maxItems?: number + size?: 'small' | 'medium' | 'large' + options?: BreadcrumbOption[] + value?: string | string[] + onChange?: (value: string | string[]) => void +} + +export const Breadcrumb: React.FC & { + classes: typeof breadcrumbClasses +} = ({ + size = 'large', + disabled = false, + ellipsis = false, + maxItems, + + value = [], + onChange, + options = [], + ...props +}) => { + const ref = useRef(null) + const [open, setOpen] = useState(false) + + const onTrigger = () => { + !disabled && setOpen((value) => !value) + } + + useEffect(() => { + if (disabled && open) setOpen(false) + }, [open, disabled]) + + return ( +
+
    + {!ellipsis || maxItems === options.length + ? options.map((opt, idx) => ( + + )) + : options.map((opt, idx) => { + if (idx === 1) + return ( + + ) + else if ( + maxItems && + maxItems > 1 && + maxItems < options.length && + idx > 1 && + idx < options.length - maxItems + 1 + ) + return null + else + return ( + + ) + })} +
+ {ellipsis && maxItems && ( + + setOpen(false)} + className={clsx( + breadcrumbClasses.listBox, + size === 'large' + ? breadcrumbClasses.listBoxLarge + : breadcrumbClasses.listBoxMedium, + )} + > + {options.slice(1, options.length - maxItems + 1).map((opt) => ( + + {opt.value} + + ))} + + + )} +
+ ) +} + +Breadcrumb.classes = breadcrumbClasses diff --git a/packages/lsd-react/src/components/Breadcrumb/index.ts b/packages/lsd-react/src/components/Breadcrumb/index.ts new file mode 100644 index 0000000..e01e180 --- /dev/null +++ b/packages/lsd-react/src/components/Breadcrumb/index.ts @@ -0,0 +1 @@ +export * from './Breadcrumb' diff --git a/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.classes.ts b/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.classes.ts new file mode 100644 index 0000000..c6c8541 --- /dev/null +++ b/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.classes.ts @@ -0,0 +1,15 @@ +export const breadcrumbItemClasses = { + root: `lsd-breadcrumb-item`, + label: `lsd-breadcrumb-item__label`, + + listElement: `lsd-breadcrumb-item-list-element`, + listElementCurrentPage: `lsd-breadcrumb-item-list-element-current-page`, + listElementLink: `lsd-breadcrumb-item-list-element__link`, + + disabled: 'lsd-breadcrumb-item--disabled', + selected: 'lsd-breadcrumb-item--selected', + + small: `lsd-breadcrumb-item--small`, + medium: `lsd-breadcrumb-item--medium`, + large: `lsd-breadcrumb-item--large`, +} diff --git a/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.stories.tsx b/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.stories.tsx new file mode 100644 index 0000000..17c2815 --- /dev/null +++ b/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.stories.tsx @@ -0,0 +1,27 @@ +import { Meta, Story } from '@storybook/react' +import { BreadcrumbItem, BreadcrumbItemProps } from './BreadcrumbItem' + +export default { + title: 'BreadcrumbItem', + component: BreadcrumbItem, + argTypes: { + size: { + type: { + name: 'enum', + value: ['small', 'medium', 'large'], + }, + }, + }, +} as Meta + +export const Root: Story = (args) => ( +
+ +
+) + +Root.args = { + label: 'label', + size: 'large', + current: true, +} diff --git a/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.styles.ts b/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.styles.ts new file mode 100644 index 0000000..f8d963b --- /dev/null +++ b/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.styles.ts @@ -0,0 +1,56 @@ +import { css } from '@emotion/react' +import { breadcrumbClasses } from '../Breadcrumb/Breadcrumb.classes' +import { breadcrumbItemClasses } from './BreadcrumbItem.classes' + +export const BreadcrumbItemStyles = css` + .${breadcrumbItemClasses.root} { + } + + .${breadcrumbClasses.list} li + li::before { + display: inline-block; + margin-inline: 10px; + content: '/'; + } + + .${breadcrumbItemClasses.listElement} { + list-style-type: none; + } + + .${breadcrumbItemClasses.listElementLink} { + text-decoration: none; + cursor: pointer; + } + + .${breadcrumbItemClasses.listElementCurrentPage} { + border: 1px solid #000000; + padding: 4px 12px; + } + + .${breadcrumbClasses.root}:not(.${breadcrumbClasses.disabled}) { + .${breadcrumbItemClasses.listElementLink} { + &:hover, + &:focus { + text-decoration: underline; + } + } + } + + .${breadcrumbItemClasses.label} { + } + + .${breadcrumbItemClasses.disabled} { + opacity: 0.34; + } + + .${breadcrumbItemClasses.small} { + padding: 6px 10px; + } + + .${breadcrumbItemClasses.medium} { + padding: 6px 12px; + } + + .${breadcrumbItemClasses.large} { + padding: 10px 14px; + } +` diff --git a/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.tsx b/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.tsx new file mode 100644 index 0000000..2b4ead4 --- /dev/null +++ b/packages/lsd-react/src/components/BreadcrumbItem/BreadcrumbItem.tsx @@ -0,0 +1,45 @@ +import clsx from 'clsx' +import React from 'react' +import { Typography } from '../Typography' +import { breadcrumbItemClasses } from './BreadcrumbItem.classes' + +export type BreadcrumbItemProps = React.HTMLAttributes & { + size: 'small' | 'medium' | 'large' + label: string + link?: string + current?: boolean + disabled?: boolean + selected?: boolean + onClick?: () => void +} + +export const BreadcrumbItem: React.FC & { + classes: typeof breadcrumbItemClasses +} = ({ label, link, size = 'large', current, onClick, selected }) => { + return ( +
  • + + {label} + +
  • + ) +} + +BreadcrumbItem.classes = breadcrumbItemClasses diff --git a/packages/lsd-react/src/components/BreadcrumbItem/index.ts b/packages/lsd-react/src/components/BreadcrumbItem/index.ts new file mode 100644 index 0000000..18ca049 --- /dev/null +++ b/packages/lsd-react/src/components/BreadcrumbItem/index.ts @@ -0,0 +1 @@ +export * from './BreadcrumbItem' diff --git a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx index ce7d4ce..bdc53fa 100644 --- a/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx +++ b/packages/lsd-react/src/components/CSSBaseline/CSSBaseline.tsx @@ -8,6 +8,8 @@ import { IconTagStyles } from '../IconTag/IconTag.styles' import { ListBoxStyles } from '../ListBox/ListBox.styles' import { TabItemStyles } from '../TabItem/TabItem.styles' import { TabsStyles } from '../Tabs/Tabs.styles' +import { BreadcrumbStyles } from '../Breadcrumb/Breadcrumb.styles' +import { BreadcrumbItemStyles } from '../BreadcrumbItem/BreadcrumbItem.styles' import { defaultThemes, Theme, withTheme } from '../Theme' import { TypographyStyles } from '../Typography/Typography.styles' @@ -22,6 +24,8 @@ const componentStyles: Array | SerializedStyles> = DropdownStyles, DropdownItemStyles, IconTagStyles, + BreadcrumbStyles, + BreadcrumbItemStyles, ] export const CSSBaseline: React.FC<{ theme?: Theme }> = ({