@arbor-education/design-system.components 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/component-library.md +15 -14
- package/dist/components/articleCard/ArticleCard.d.ts +2 -2
- package/dist/components/articleCard/ArticleCard.d.ts.map +1 -1
- package/dist/components/articleCard/ArticleCard.js +3 -3
- package/dist/components/articleCard/ArticleCard.js.map +1 -1
- package/dist/components/articleCard/ArticleCard.stories.d.ts +11 -3
- package/dist/components/articleCard/ArticleCard.stories.d.ts.map +1 -1
- package/dist/components/articleCard/ArticleCard.stories.js +16 -11
- package/dist/components/articleCard/ArticleCard.stories.js.map +1 -1
- package/dist/components/iconText/IconText.d.ts +43 -0
- package/dist/components/iconText/IconText.d.ts.map +1 -0
- package/dist/components/iconText/IconText.js +29 -0
- package/dist/components/iconText/IconText.js.map +1 -0
- package/dist/components/{icoText/IcoText.stories.d.ts → iconText/IconText.stories.d.ts} +8 -9
- package/dist/components/iconText/IconText.stories.d.ts.map +1 -0
- package/dist/components/{icoText/IcoText.stories.js → iconText/IconText.stories.js} +81 -81
- package/dist/components/iconText/IconText.stories.js.map +1 -0
- package/dist/components/iconText/IconText.test.d.ts +2 -0
- package/dist/components/iconText/IconText.test.d.ts.map +1 -0
- package/dist/components/{icoText/IcoText.test.js → iconText/IconText.test.js} +6 -6
- package/dist/components/iconText/IconText.test.js.map +1 -0
- package/dist/components/modal/Modal.d.ts +1 -0
- package/dist/components/modal/Modal.d.ts.map +1 -1
- package/dist/components/modal/Modal.js +2 -2
- package/dist/components/modal/Modal.js.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js +2 -2
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.js.map +1 -1
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.test.js +6 -0
- package/dist/components/table/cellRenderers/SelectDropdownCellRenderer.test.js.map +1 -1
- package/dist/components/tag/Tag.d.ts +14 -1
- package/dist/components/tag/Tag.d.ts.map +1 -1
- package/dist/components/tag/Tag.js +9 -3
- package/dist/components/tag/Tag.js.map +1 -1
- package/dist/components/tag/Tag.stories.d.ts +1 -1
- package/dist/components/tag/Tag.stories.d.ts.map +1 -1
- package/dist/components/tag/Tag.stories.js +3 -3
- package/dist/components/tag/Tag.stories.js.map +1 -1
- package/dist/components/tag/Tag.test.js +36 -5
- package/dist/components/tag/Tag.test.js.map +1 -1
- package/dist/components/tagList/TagList.d.ts +49 -0
- package/dist/components/tagList/TagList.d.ts.map +1 -0
- package/dist/components/tagList/TagList.js +114 -0
- package/dist/components/tagList/TagList.js.map +1 -0
- package/dist/components/tagList/TagList.stories.d.ts +130 -0
- package/dist/components/tagList/TagList.stories.d.ts.map +1 -0
- package/dist/components/tagList/TagList.stories.js +443 -0
- package/dist/components/tagList/TagList.stories.js.map +1 -0
- package/dist/components/{icoText/IcoText.test.d.ts → tagList/TagList.test.d.ts} +1 -1
- package/dist/components/tagList/TagList.test.d.ts.map +1 -0
- package/dist/components/tagList/TagList.test.js +246 -0
- package/dist/components/tagList/TagList.test.js.map +1 -0
- package/dist/components/tagList/useTagListCollapsedLayout.d.ts +19 -0
- package/dist/components/tagList/useTagListCollapsedLayout.d.ts.map +1 -0
- package/dist/components/tagList/useTagListCollapsedLayout.js +48 -0
- package/dist/components/tagList/useTagListCollapsedLayout.js.map +1 -0
- package/dist/components/tagList/useVisibleTags.d.ts +18 -0
- package/dist/components/tagList/useVisibleTags.d.ts.map +1 -0
- package/dist/components/tagList/useVisibleTags.js +41 -0
- package/dist/components/tagList/useVisibleTags.js.map +1 -0
- package/dist/index.css +130 -10
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/hooks/useElementWidth.d.ts +2 -0
- package/dist/utils/hooks/useElementWidth.d.ts.map +1 -0
- package/dist/utils/hooks/useElementWidth.js +30 -0
- package/dist/utils/hooks/useElementWidth.js.map +1 -0
- package/dist/utils/hooks/useMeasuredChildWidths.d.ts +8 -0
- package/dist/utils/hooks/useMeasuredChildWidths.d.ts.map +1 -0
- package/dist/utils/hooks/useMeasuredChildWidths.js +26 -0
- package/dist/utils/hooks/useMeasuredChildWidths.js.map +1 -0
- package/dist/utils/hooks/useRovingFocus.d.ts +18 -0
- package/dist/utils/hooks/useRovingFocus.d.ts.map +1 -0
- package/dist/utils/hooks/useRovingFocus.js +130 -0
- package/dist/utils/hooks/useRovingFocus.js.map +1 -0
- package/dist/utils/hooks/useRovingFocus.test.d.ts +2 -0
- package/dist/utils/hooks/useRovingFocus.test.d.ts.map +1 -0
- package/dist/utils/hooks/useRovingFocus.test.js +59 -0
- package/dist/utils/hooks/useRovingFocus.test.js.map +1 -0
- package/dist/utils/spacedWidths.d.ts +3 -0
- package/dist/utils/spacedWidths.d.ts.map +1 -0
- package/dist/utils/spacedWidths.js +28 -0
- package/dist/utils/spacedWidths.js.map +1 -0
- package/dist/utils/spacedWidths.test.d.ts +2 -0
- package/dist/utils/spacedWidths.test.d.ts.map +1 -0
- package/dist/utils/spacedWidths.test.js +17 -0
- package/dist/utils/spacedWidths.test.js.map +1 -0
- package/package.json +1 -1
- package/src/components/articleCard/ArticleCard.stories.tsx +17 -12
- package/src/components/articleCard/ArticleCard.tsx +9 -9
- package/src/components/{icoText/IcoText.stories.tsx → iconText/IconText.stories.tsx} +112 -112
- package/src/components/{icoText/IcoText.test.tsx → iconText/IconText.test.tsx} +10 -10
- package/src/components/{icoText/IcoText.tsx → iconText/IconText.tsx} +27 -20
- package/src/components/modal/Modal.tsx +5 -1
- package/src/components/table/cellRenderers/SelectDropdownCellRenderer.test.tsx +12 -0
- package/src/components/table/cellRenderers/SelectDropdownCellRenderer.tsx +2 -2
- package/src/components/tag/Tag.stories.tsx +4 -4
- package/src/components/tag/Tag.test.tsx +62 -5
- package/src/components/tag/Tag.tsx +61 -3
- package/src/components/tag/tag.scss +80 -9
- package/src/components/tagList/TagList.stories.tsx +564 -0
- package/src/components/tagList/TagList.test.tsx +342 -0
- package/src/components/tagList/TagList.tsx +296 -0
- package/src/components/tagList/tagList.scss +56 -0
- package/src/components/tagList/useTagListCollapsedLayout.ts +83 -0
- package/src/components/tagList/useVisibleTags.ts +74 -0
- package/src/index.scss +2 -1
- package/src/index.ts +3 -1
- package/src/tokens.scss +2 -1
- package/src/utils/hooks/useElementWidth.ts +39 -0
- package/src/utils/hooks/useMeasuredChildWidths.ts +39 -0
- package/src/utils/hooks/useRovingFocus.test.tsx +105 -0
- package/src/utils/hooks/useRovingFocus.ts +163 -0
- package/src/utils/spacedWidths.test.ts +20 -0
- package/src/utils/spacedWidths.ts +37 -0
- package/dist/components/icoText/IcoText.d.ts +0 -37
- package/dist/components/icoText/IcoText.d.ts.map +0 -1
- package/dist/components/icoText/IcoText.js +0 -29
- package/dist/components/icoText/IcoText.js.map +0 -1
- package/dist/components/icoText/IcoText.stories.d.ts.map +0 -1
- package/dist/components/icoText/IcoText.stories.js.map +0 -1
- package/dist/components/icoText/IcoText.test.d.ts.map +0 -1
- package/dist/components/icoText/IcoText.test.js.map +0 -1
- /package/src/components/{icoText/icoText.scss → iconText/iconText.scss} +0 -0
|
@@ -3,20 +3,20 @@ import type { IconName } from 'Components/icon/allowedIcons';
|
|
|
3
3
|
import { Icon } from 'Components/icon/Icon';
|
|
4
4
|
import { Children, isValidElement } from 'react';
|
|
5
5
|
|
|
6
|
-
export type
|
|
6
|
+
export type IconTextProps = {
|
|
7
7
|
children?: React.ReactNode;
|
|
8
8
|
className?: string;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
export type
|
|
11
|
+
export type IconTextHeadingProps = React.HTMLAttributes<HTMLHeadingElement> & {
|
|
12
12
|
children: React.ReactNode;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export type
|
|
15
|
+
export type IconTextParagraphProps = React.HTMLAttributes<HTMLParagraphElement> & {
|
|
16
16
|
children: React.ReactNode;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export type
|
|
19
|
+
export type IconTextIconProps = {
|
|
20
20
|
className?: string;
|
|
21
21
|
color?: Icon.Props['color'];
|
|
22
22
|
name: IconName;
|
|
@@ -24,33 +24,33 @@ export type IcoTextIconProps = {
|
|
|
24
24
|
size?: 12 | 16 | 24;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
const
|
|
27
|
+
const IconTextHeading = ({
|
|
28
28
|
children,
|
|
29
29
|
className,
|
|
30
30
|
...rest
|
|
31
|
-
}:
|
|
31
|
+
}: IconTextHeadingProps): React.JSX.Element => (
|
|
32
32
|
<h4 className={classNames('ds-ico-text__heading', className)} {...rest}>
|
|
33
33
|
{children}
|
|
34
34
|
</h4>
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const IconTextParagraph = ({
|
|
38
38
|
children,
|
|
39
39
|
className,
|
|
40
40
|
...rest
|
|
41
|
-
}:
|
|
41
|
+
}: IconTextParagraphProps): React.JSX.Element => (
|
|
42
42
|
<p className={classNames('ds-ico-text__paragraph', className)} {...rest}>
|
|
43
43
|
{children}
|
|
44
44
|
</p>
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const IconTextIcon = ({
|
|
48
48
|
className,
|
|
49
49
|
color,
|
|
50
50
|
name,
|
|
51
51
|
screenReaderText,
|
|
52
52
|
size = 24,
|
|
53
|
-
}:
|
|
53
|
+
}: IconTextIconProps): React.JSX.Element => (
|
|
54
54
|
<Icon
|
|
55
55
|
name={name}
|
|
56
56
|
className={classNames('ds-ico-text__icon', className)}
|
|
@@ -60,12 +60,12 @@ const IcoTextIcon = ({
|
|
|
60
60
|
/>
|
|
61
61
|
);
|
|
62
62
|
|
|
63
|
-
const
|
|
63
|
+
const IconTextRoot = ({ children, className }: IconTextProps): React.JSX.Element => {
|
|
64
64
|
const iconChildren: React.ReactNode[] = [];
|
|
65
65
|
const contentChildren: React.ReactNode[] = [];
|
|
66
66
|
|
|
67
67
|
Children.forEach(children, (child) => {
|
|
68
|
-
if (isValidElement(child) && child.type ===
|
|
68
|
+
if (isValidElement(child) && child.type === IconTextIcon) {
|
|
69
69
|
iconChildren.push(child);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
@@ -81,13 +81,20 @@ const IcoTextRoot = ({ children, className }: IcoTextProps): React.JSX.Element =
|
|
|
81
81
|
);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
IconTextHeading.displayName = 'IconText.Heading';
|
|
85
|
+
IconTextParagraph.displayName = 'IconText.Paragraph';
|
|
86
|
+
IconTextIcon.displayName = 'IconText.Icon';
|
|
87
|
+
IconTextRoot.displayName = 'IconText';
|
|
88
88
|
|
|
89
|
-
export const
|
|
90
|
-
Heading:
|
|
91
|
-
Paragraph:
|
|
92
|
-
Icon:
|
|
89
|
+
export const IconText = Object.assign(IconTextRoot, {
|
|
90
|
+
Heading: IconTextHeading,
|
|
91
|
+
Paragraph: IconTextParagraph,
|
|
92
|
+
Icon: IconTextIcon,
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
export namespace IconText {
|
|
96
|
+
export type Props = IconTextProps;
|
|
97
|
+
export type HeadingProps = IconTextHeadingProps;
|
|
98
|
+
export type ParagraphProps = IconTextParagraphProps;
|
|
99
|
+
export type IconProps = IconTextIconProps;
|
|
100
|
+
}
|
|
@@ -20,6 +20,8 @@ export type ModalProps = {
|
|
|
20
20
|
overlayClassName?: string;
|
|
21
21
|
open?: boolean;
|
|
22
22
|
portalTarget?: HTMLElement | null;
|
|
23
|
+
// Optional id for the dialog content so external triggers can point aria-controls at it.
|
|
24
|
+
contentId?: string;
|
|
23
25
|
children?: React.ReactNode;
|
|
24
26
|
hideCloseButton?: boolean;
|
|
25
27
|
closeHandler?: ModalContextValue['closeHandler'];
|
|
@@ -32,6 +34,7 @@ export const Modal = (props: ModalProps) => {
|
|
|
32
34
|
overlayClassName,
|
|
33
35
|
open,
|
|
34
36
|
portalTarget,
|
|
37
|
+
contentId,
|
|
35
38
|
children,
|
|
36
39
|
closeHandler,
|
|
37
40
|
hideCloseButton = false,
|
|
@@ -46,7 +49,8 @@ export const Modal = (props: ModalProps) => {
|
|
|
46
49
|
<Dialog.Root open={open}>
|
|
47
50
|
<Dialog.Portal container={portalTarget}>
|
|
48
51
|
<Dialog.Overlay ref={overlayRef} className={classNames('ds-modal__overlay', overlayClassName)}>
|
|
49
|
-
|
|
52
|
+
{/* Preserve an optional target for trigger/content relationships such as FilterBar button-to-dialog wiring. */}
|
|
53
|
+
<Dialog.Content id={contentId} className={classNames('ds-modal__container', className)}>
|
|
50
54
|
{title && <ModalHeader><ModalTitle>{title}</ModalTitle></ModalHeader>}
|
|
51
55
|
{children}
|
|
52
56
|
{!hideCloseButton && <ModalCloseButon className="ds-modal__close-button--top-right" />}
|
|
@@ -130,6 +130,18 @@ describe('SelectDropdownCellRenderer', () => {
|
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
test('updates the displayed label when the value prop changes (e.g. on paste)', () => {
|
|
134
|
+
const { rerender } = render(
|
|
135
|
+
<SelectDropdownCellRenderer {...createMockProps({ value: 'option1' })} />,
|
|
136
|
+
);
|
|
137
|
+
expect(screen.getByRole('button')).toHaveTextContent('Option 1');
|
|
138
|
+
|
|
139
|
+
rerender(
|
|
140
|
+
<SelectDropdownCellRenderer {...createMockProps({ value: 'option2' })} />,
|
|
141
|
+
);
|
|
142
|
+
expect(screen.getByRole('button')).toHaveTextContent('Option 2');
|
|
143
|
+
});
|
|
144
|
+
|
|
133
145
|
test('applies both modifiers when backgroundColor and fillCell are set together', () => {
|
|
134
146
|
const { container } = render(
|
|
135
147
|
<SelectDropdownCellRenderer
|
|
@@ -73,7 +73,7 @@ export const SelectDropdownCellRenderer = (
|
|
|
73
73
|
}));
|
|
74
74
|
|
|
75
75
|
const valueStr = value != null && value !== '' ? String(value) : '';
|
|
76
|
-
const
|
|
76
|
+
const selectedValues = valueStr ? [valueStr] : [];
|
|
77
77
|
|
|
78
78
|
const [isOpen, setIsOpen] = useState(false);
|
|
79
79
|
|
|
@@ -108,7 +108,7 @@ export const SelectDropdownCellRenderer = (
|
|
|
108
108
|
alwaysShowPlaceholder={alwaysShowPlaceholder}
|
|
109
109
|
options={normalisedOptions}
|
|
110
110
|
placeholder={placeholder}
|
|
111
|
-
|
|
111
|
+
selectedValues={selectedValues}
|
|
112
112
|
open={isOpen}
|
|
113
113
|
onOpenChange={setIsOpen}
|
|
114
114
|
multiple={false}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
1
|
import {
|
|
3
2
|
Controls,
|
|
4
3
|
Heading as DocHeading,
|
|
5
|
-
Markdown,
|
|
6
4
|
Primary as DocPrimary,
|
|
5
|
+
Markdown,
|
|
7
6
|
Stories,
|
|
8
7
|
Subtitle,
|
|
9
8
|
Title,
|
|
10
9
|
} from '@storybook/addon-docs/blocks';
|
|
11
|
-
import {
|
|
12
|
-
import { fn } from 'storybook/test';
|
|
10
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
13
11
|
import { Dot } from 'Components/dot/Dot';
|
|
14
12
|
import { Icon } from 'Components/icon/Icon';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { fn } from 'storybook/test';
|
|
15
15
|
import { Tag, type TagColor } from './Tag.js';
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -108,8 +108,48 @@ describe('Tag', () => {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
describe('onClick', () => {
|
|
112
|
+
test('renders a primary action button when onClick is provided', () => {
|
|
113
|
+
render(<Tag onClick={() => {}}>Clickable</Tag>);
|
|
114
|
+
expect(screen.getByRole('button', { name: 'Clickable' })).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('uses a custom action label when provided', () => {
|
|
118
|
+
render(<Tag onClick={() => {}} actionLabel="Open filter for Alice Johnson">Alice Johnson</Tag>);
|
|
119
|
+
expect(screen.getByRole('button', { name: 'Open filter for Alice Johnson' })).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('allows composite parents to remove the primary action from the tab order', () => {
|
|
123
|
+
render(
|
|
124
|
+
<Tag onClick={() => {}} actionButtonTabIndex={-1}>
|
|
125
|
+
Clickable
|
|
126
|
+
</Tag>,
|
|
127
|
+
);
|
|
128
|
+
expect(screen.getByRole('button', { name: 'Clickable' })).toHaveAttribute('tabindex', '-1');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('fires onClick callback when the primary action is clicked', async () => {
|
|
132
|
+
const onClick = vi.fn();
|
|
133
|
+
render(<Tag onClick={onClick}>Clickable</Tag>);
|
|
134
|
+
|
|
135
|
+
await userEvent.click(screen.getByRole('button', { name: 'Clickable' }));
|
|
136
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('forwards aria-controls and aria-expanded to the primary action button', () => {
|
|
140
|
+
render(
|
|
141
|
+
<Tag onClick={() => {}} ariaControls="filter-modal" ariaExpanded={true}>
|
|
142
|
+
Clickable
|
|
143
|
+
</Tag>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByRole('button', { name: 'Clickable' })).toHaveAttribute('aria-controls', 'filter-modal');
|
|
147
|
+
expect(screen.getByRole('button', { name: 'Clickable' })).toHaveAttribute('aria-expanded', 'true');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
111
151
|
describe('combined slots', () => {
|
|
112
|
-
test('renders
|
|
152
|
+
test('renders non-clickable removable tags through the shared content wrapper', () => {
|
|
113
153
|
const { container } = render(
|
|
114
154
|
<Tag
|
|
115
155
|
slotStart={<span>S</span>}
|
|
@@ -122,10 +162,27 @@ describe('Tag', () => {
|
|
|
122
162
|
|
|
123
163
|
const tag = container.querySelector('.ds-tag')!;
|
|
124
164
|
const children = Array.from(tag.children);
|
|
125
|
-
expect(children[0]).toHaveClass('ds-
|
|
126
|
-
expect(children[1]).toHaveClass('ds-
|
|
127
|
-
|
|
128
|
-
|
|
165
|
+
expect(children[0]).toHaveClass('ds-tag__content');
|
|
166
|
+
expect(children[1]).toHaveClass('ds-tag__remove');
|
|
167
|
+
|
|
168
|
+
const contentChildren = Array.from(children[0]!.children);
|
|
169
|
+
expect(contentChildren[0]).toHaveClass('ds-tag__slot-start');
|
|
170
|
+
expect(contentChildren[1]).toHaveClass('ds-tag__label');
|
|
171
|
+
expect(contentChildren[2]).toHaveClass('ds-tag__slot-end');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('renders clickable tags through the same shared content wrapper', () => {
|
|
175
|
+
const { container } = render(
|
|
176
|
+
<Tag
|
|
177
|
+
slotStart={<span>S</span>}
|
|
178
|
+
slotEnd={<span>E</span>}
|
|
179
|
+
onClick={() => {}}
|
|
180
|
+
>
|
|
181
|
+
Label
|
|
182
|
+
</Tag>,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(container.querySelector('.ds-tag__content.ds-tag__action')).toBeInTheDocument();
|
|
129
186
|
});
|
|
130
187
|
});
|
|
131
188
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import { Icon } from 'Components/icon/Icon';
|
|
3
|
+
import type { ButtonHTMLAttributes } from 'react';
|
|
3
4
|
|
|
4
5
|
export type TagColor = 'neutral'
|
|
5
6
|
| 'orange'
|
|
@@ -16,9 +17,21 @@ export type TagProps = {
|
|
|
16
17
|
selected?: boolean;
|
|
17
18
|
slotStart?: React.ReactNode;
|
|
18
19
|
slotEnd?: React.ReactNode;
|
|
20
|
+
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
21
|
+
onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
|
|
22
|
+
onFocus?: React.FocusEventHandler<HTMLButtonElement>;
|
|
23
|
+
actionLabel?: string;
|
|
24
|
+
actionButtonTabIndex?: 0 | -1;
|
|
25
|
+
actionRef?: React.Ref<HTMLButtonElement>;
|
|
19
26
|
onRemove?: () => void;
|
|
20
27
|
removeLabel?: string;
|
|
28
|
+
onRemoveKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
|
|
29
|
+
onRemoveFocus?: React.FocusEventHandler<HTMLButtonElement>;
|
|
21
30
|
removeButtonTabIndex?: 0 | -1;
|
|
31
|
+
removeButtonRef?: React.Ref<HTMLButtonElement>;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
ariaControls?: string;
|
|
34
|
+
ariaExpanded?: ButtonHTMLAttributes<HTMLButtonElement>['aria-expanded'];
|
|
22
35
|
};
|
|
23
36
|
|
|
24
37
|
export const Tag = ({
|
|
@@ -27,18 +40,59 @@ export const Tag = ({
|
|
|
27
40
|
selected = false,
|
|
28
41
|
slotStart,
|
|
29
42
|
slotEnd,
|
|
43
|
+
onClick,
|
|
44
|
+
onKeyDown,
|
|
45
|
+
onFocus,
|
|
46
|
+
actionLabel,
|
|
47
|
+
actionButtonTabIndex = 0,
|
|
48
|
+
actionRef,
|
|
30
49
|
onRemove,
|
|
31
50
|
removeLabel = 'Remove',
|
|
51
|
+
onRemoveKeyDown,
|
|
52
|
+
onRemoveFocus,
|
|
32
53
|
removeButtonTabIndex = 0,
|
|
54
|
+
removeButtonRef,
|
|
55
|
+
disabled = false,
|
|
56
|
+
ariaControls,
|
|
57
|
+
ariaExpanded,
|
|
33
58
|
}: TagProps): React.JSX.Element => {
|
|
59
|
+
const tagContent = onClick
|
|
60
|
+
? (
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
className="ds-tag__content ds-tag__action"
|
|
64
|
+
aria-label={actionLabel}
|
|
65
|
+
aria-controls={ariaControls}
|
|
66
|
+
aria-expanded={ariaExpanded}
|
|
67
|
+
onClick={onClick}
|
|
68
|
+
onKeyDown={onKeyDown}
|
|
69
|
+
onFocus={onFocus}
|
|
70
|
+
tabIndex={actionButtonTabIndex}
|
|
71
|
+
ref={actionRef}
|
|
72
|
+
disabled={disabled}
|
|
73
|
+
>
|
|
74
|
+
{slotStart && <span className="ds-tag__slot-start">{slotStart}</span>}
|
|
75
|
+
<span className="ds-tag__label">{children}</span>
|
|
76
|
+
{slotEnd && <span className="ds-tag__slot-end">{slotEnd}</span>}
|
|
77
|
+
</button>
|
|
78
|
+
)
|
|
79
|
+
: (
|
|
80
|
+
<span className="ds-tag__content">
|
|
81
|
+
{slotStart && <span className="ds-tag__slot-start">{slotStart}</span>}
|
|
82
|
+
<span className="ds-tag__label">{children}</span>
|
|
83
|
+
{slotEnd && <span className="ds-tag__slot-end">{slotEnd}</span>}
|
|
84
|
+
</span>
|
|
85
|
+
);
|
|
86
|
+
|
|
34
87
|
return (
|
|
35
88
|
<span className={classNames('ds-tag', `ds-tag--${color}`, {
|
|
36
89
|
'ds-tag--selected': selected,
|
|
90
|
+
'ds-tag--interactive': Boolean(onClick),
|
|
91
|
+
'ds-tag--removable': Boolean(onRemove),
|
|
92
|
+
'ds-tag--disabled': disabled,
|
|
37
93
|
})}
|
|
38
94
|
>
|
|
39
|
-
{
|
|
40
|
-
<span className="ds-tag__label">{children}</span>
|
|
41
|
-
{slotEnd && <span className="ds-tag__slot-end">{slotEnd}</span>}
|
|
95
|
+
{tagContent}
|
|
42
96
|
{onRemove && (
|
|
43
97
|
<button
|
|
44
98
|
type="button"
|
|
@@ -48,7 +102,11 @@ export const Tag = ({
|
|
|
48
102
|
e.stopPropagation();
|
|
49
103
|
onRemove();
|
|
50
104
|
}}
|
|
105
|
+
onKeyDown={onRemoveKeyDown}
|
|
106
|
+
onFocus={onRemoveFocus}
|
|
51
107
|
tabIndex={removeButtonTabIndex}
|
|
108
|
+
ref={removeButtonRef}
|
|
109
|
+
disabled={disabled}
|
|
52
110
|
>
|
|
53
111
|
<Icon name="x" size={12} />
|
|
54
112
|
</button>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
.ds-tag {
|
|
2
2
|
display: inline-flex;
|
|
3
|
-
padding: 0 var(--tag-spacing-horizontal);
|
|
4
3
|
align-items: center;
|
|
5
4
|
gap: var(--tag-spacing-gap-horizontal);
|
|
6
5
|
border-radius: var(--tag-radius);
|
|
@@ -13,6 +12,7 @@
|
|
|
13
12
|
height: var(--tag-height);
|
|
14
13
|
font-style: normal;
|
|
15
14
|
line-height: 150%;
|
|
15
|
+
overflow: visible;
|
|
16
16
|
|
|
17
17
|
&--neutral {
|
|
18
18
|
color: var(--tag-neutral-color-text);
|
|
@@ -60,6 +60,68 @@
|
|
|
60
60
|
border-color: var(--tag-selected-color-border);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
.ds-tag__label {
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
text-overflow: ellipsis;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&--disabled {
|
|
69
|
+
opacity: 0.6;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ds-tag__content {
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: inherit;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
height: 100%;
|
|
79
|
+
padding: 0 var(--tag-spacing-horizontal);
|
|
80
|
+
border-radius: inherit;
|
|
81
|
+
box-sizing: border-box;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.ds-tag__action {
|
|
85
|
+
border: none;
|
|
86
|
+
background: transparent;
|
|
87
|
+
color: inherit;
|
|
88
|
+
font: inherit;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
|
|
91
|
+
&:focus {
|
|
92
|
+
outline: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&:focus-visible {
|
|
96
|
+
outline: none;
|
|
97
|
+
background-color: var(--color-grey-100);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&:hover {
|
|
101
|
+
background-color: var(--color-grey-100);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
&:disabled {
|
|
105
|
+
cursor: not-allowed;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.ds-tag--removable {
|
|
110
|
+
gap: 0;
|
|
111
|
+
|
|
112
|
+
.ds-tag__label {
|
|
113
|
+
padding: 0 var(--tag-spacing-horizontal) 0 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.ds-tag__content {
|
|
117
|
+
gap: var(--tag-spacing-horizontal);
|
|
118
|
+
padding-right: 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.ds-tag:has(.ds-tag__action:focus-visible) {
|
|
123
|
+
outline: var(--focus-border) solid var(--focus-color-focus);
|
|
124
|
+
outline-offset: 2px;
|
|
63
125
|
}
|
|
64
126
|
|
|
65
127
|
.ds-tag__slot-start {
|
|
@@ -68,11 +130,6 @@
|
|
|
68
130
|
flex-shrink: 0;
|
|
69
131
|
}
|
|
70
132
|
|
|
71
|
-
.ds-tag__label {
|
|
72
|
-
overflow: hidden;
|
|
73
|
-
text-overflow: ellipsis;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
133
|
.ds-tag__slot-end {
|
|
77
134
|
display: flex;
|
|
78
135
|
align-items: center;
|
|
@@ -80,19 +137,33 @@
|
|
|
80
137
|
}
|
|
81
138
|
|
|
82
139
|
.ds-tag__remove {
|
|
83
|
-
display: flex;
|
|
140
|
+
display: inline-flex;
|
|
84
141
|
align-items: center;
|
|
85
142
|
justify-content: center;
|
|
86
143
|
border: none;
|
|
87
144
|
background: transparent;
|
|
145
|
+
align-self: stretch;
|
|
146
|
+
min-width: calc(var(--tag-height) - (var(--border-weight) * 2));
|
|
88
147
|
padding: 0;
|
|
89
148
|
cursor: pointer;
|
|
90
149
|
color: inherit;
|
|
91
|
-
border-radius:
|
|
150
|
+
border-radius: var(--tag-radius);
|
|
92
151
|
flex-shrink: 0;
|
|
152
|
+
line-height: 0;
|
|
153
|
+
|
|
154
|
+
&:focus {
|
|
155
|
+
outline: none;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
&:focus-visible {
|
|
159
|
+
outline: var(--focus-border) solid var(--focus-color-focus);
|
|
160
|
+
outline-offset: 2px;
|
|
161
|
+
border-radius: var(--tag-radius);
|
|
162
|
+
background-color: var(--color-grey-100);
|
|
163
|
+
}
|
|
93
164
|
|
|
94
165
|
&:hover {
|
|
95
|
-
background-color: var(--color-grey-
|
|
166
|
+
background-color: var(--color-grey-100);
|
|
96
167
|
}
|
|
97
168
|
|
|
98
169
|
&:disabled {
|