@arbor-education/design-system.components 0.22.0 → 0.23.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 +10 -0
- package/component-library.md +62 -0
- package/dist/components/combobox/Combobox.js +1 -1
- package/dist/components/combobox/Combobox.js.map +1 -1
- package/dist/components/combobox/Combobox.stories.d.ts +4 -0
- package/dist/components/combobox/Combobox.stories.d.ts.map +1 -1
- package/dist/components/combobox/Combobox.stories.js +144 -12
- package/dist/components/combobox/Combobox.stories.js.map +1 -1
- package/dist/components/combobox/Combobox.test.js +22 -0
- package/dist/components/combobox/Combobox.test.js.map +1 -1
- package/dist/components/combobox/ComboboxButtonTrigger.d.ts +4 -4
- package/dist/components/combobox/ComboboxButtonTrigger.d.ts.map +1 -1
- package/dist/components/combobox/ComboboxButtonTrigger.js +35 -40
- package/dist/components/combobox/ComboboxButtonTrigger.js.map +1 -1
- package/dist/components/combobox/ComboboxTrigger.d.ts.map +1 -1
- package/dist/components/combobox/ComboboxTrigger.js +11 -4
- package/dist/components/combobox/ComboboxTrigger.js.map +1 -1
- package/dist/components/combobox/useVisibleTriggerTags.d.ts +21 -0
- package/dist/components/combobox/useVisibleTriggerTags.d.ts.map +1 -0
- package/dist/components/combobox/useVisibleTriggerTags.js +46 -0
- package/dist/components/combobox/useVisibleTriggerTags.js.map +1 -0
- package/dist/components/combobox/useVisibleTriggerTags.test.d.ts +2 -0
- package/dist/components/combobox/useVisibleTriggerTags.test.d.ts.map +1 -0
- package/dist/components/combobox/useVisibleTriggerTags.test.js +81 -0
- package/dist/components/combobox/useVisibleTriggerTags.test.js.map +1 -0
- package/dist/components/filterBar/FilterBar.d.ts +71 -0
- package/dist/components/filterBar/FilterBar.d.ts.map +1 -0
- package/dist/components/filterBar/FilterBar.js +89 -0
- package/dist/components/filterBar/FilterBar.js.map +1 -0
- package/dist/components/filterBar/FilterBar.stories.d.ts +170 -0
- package/dist/components/filterBar/FilterBar.stories.d.ts.map +1 -0
- package/dist/components/filterBar/FilterBar.stories.js +894 -0
- package/dist/components/filterBar/FilterBar.stories.js.map +1 -0
- package/dist/components/filterBar/FilterBar.test.d.ts +2 -0
- package/dist/components/filterBar/FilterBar.test.d.ts.map +1 -0
- package/dist/components/filterBar/FilterBar.test.js +164 -0
- package/dist/components/filterBar/FilterBar.test.js.map +1 -0
- package/dist/components/icon/allowedIcons.d.ts +1 -0
- package/dist/components/icon/allowedIcons.d.ts.map +1 -1
- package/dist/components/icon/allowedIcons.js +2 -1
- package/dist/components/icon/allowedIcons.js.map +1 -1
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.d.ts.map +1 -1
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.js +13 -2
- package/dist/components/table/cellRenderers/ComboboxCellRenderer.test.js.map +1 -1
- package/dist/index.css +142 -3
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/combobox/Combobox.stories.tsx +186 -12
- package/src/components/combobox/Combobox.test.tsx +53 -0
- package/src/components/combobox/Combobox.tsx +3 -3
- package/src/components/combobox/ComboboxButtonTrigger.tsx +52 -56
- package/src/components/combobox/ComboboxTrigger.tsx +19 -16
- package/src/components/combobox/combobox.scss +8 -3
- package/src/components/combobox/useVisibleTriggerTags.test.tsx +91 -0
- package/src/components/combobox/useVisibleTriggerTags.ts +83 -0
- package/src/components/filterBar/FilterBar.stories.tsx +1199 -0
- package/src/components/filterBar/FilterBar.test.tsx +248 -0
- package/src/components/filterBar/FilterBar.tsx +298 -0
- package/src/components/filterBar/filterBar.scss +143 -0
- package/src/components/icon/allowedIcons.tsx +3 -1
- package/src/components/table/cellRenderers/ComboboxCellRenderer.test.tsx +20 -3
- package/src/index.scss +1 -0
- package/src/index.ts +10 -0
- package/src/tokens.scss +1 -0
- package/dist/components/combobox/useElementWidth.d.ts +0 -2
- package/dist/components/combobox/useElementWidth.d.ts.map +0 -1
- package/dist/components/combobox/useElementWidth.js +0 -31
- package/dist/components/combobox/useElementWidth.js.map +0 -1
- package/dist/components/combobox/useVisibleChips.d.ts +0 -21
- package/dist/components/combobox/useVisibleChips.d.ts.map +0 -1
- package/dist/components/combobox/useVisibleChips.js +0 -59
- package/dist/components/combobox/useVisibleChips.js.map +0 -1
- package/dist/components/combobox/useVisibleChips.test.d.ts +0 -2
- package/dist/components/combobox/useVisibleChips.test.d.ts.map +0 -1
- package/dist/components/combobox/useVisibleChips.test.js +0 -81
- package/dist/components/combobox/useVisibleChips.test.js.map +0 -1
- package/src/components/combobox/useElementWidth.ts +0 -40
- package/src/components/combobox/useVisibleChips.test.tsx +0 -91
- package/src/components/combobox/useVisibleChips.ts +0 -100
|
@@ -2,11 +2,13 @@ import classNames from 'classnames';
|
|
|
2
2
|
import { Badge } from 'Components/badge/Badge';
|
|
3
3
|
import { Icon } from 'Components/icon/Icon';
|
|
4
4
|
import { Tag } from 'Components/tag/Tag';
|
|
5
|
+
import { TagList } from 'Components/tagList/TagList';
|
|
6
|
+
import { useMeasuredChildWidths } from 'Utils/hooks/useMeasuredChildWidths';
|
|
7
|
+
import { useElementWidth } from 'Utils/hooks/useElementWidth';
|
|
5
8
|
import { Popover } from 'radix-ui';
|
|
6
9
|
import { useMemo, useRef } from 'react';
|
|
7
10
|
import type { ComboboxAriaInvalid, ComboboxOption, ComboboxSelectedValueDisplay } from './types.js';
|
|
8
|
-
import {
|
|
9
|
-
import { useVisibleChips } from './useVisibleChips.js';
|
|
11
|
+
import { useVisibleTriggerTags } from './useVisibleTriggerTags.js';
|
|
10
12
|
|
|
11
13
|
export type ComboboxButtonTriggerProps = {
|
|
12
14
|
triggerRef: React.RefObject<HTMLDivElement | null>;
|
|
@@ -21,9 +23,9 @@ export type ComboboxButtonTriggerProps = {
|
|
|
21
23
|
showDropdownTrigger: boolean;
|
|
22
24
|
selectedValueDisplay: ComboboxSelectedValueDisplay;
|
|
23
25
|
triggerEndContent?: React.ReactNode;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
selectedTags: ComboboxOption[];
|
|
27
|
+
selectedTagValuesSet: Set<string>;
|
|
28
|
+
focusedTagIndex: number | null;
|
|
27
29
|
resolveTagLabel: (opt: ComboboxOption) => string;
|
|
28
30
|
removeValue: (value: string) => void;
|
|
29
31
|
handleTriggerClick: () => void;
|
|
@@ -46,9 +48,9 @@ export const ComboboxButtonTrigger = ({
|
|
|
46
48
|
showDropdownTrigger,
|
|
47
49
|
selectedValueDisplay,
|
|
48
50
|
triggerEndContent,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
selectedTags,
|
|
52
|
+
selectedTagValuesSet,
|
|
53
|
+
focusedTagIndex,
|
|
52
54
|
resolveTagLabel,
|
|
53
55
|
removeValue,
|
|
54
56
|
handleTriggerClick,
|
|
@@ -62,65 +64,47 @@ export const ComboboxButtonTrigger = ({
|
|
|
62
64
|
const ellipsisProbeRef = useRef<HTMLSpanElement>(null);
|
|
63
65
|
const badgeProbeRef = useRef<HTMLSpanElement>(null);
|
|
64
66
|
|
|
65
|
-
const
|
|
67
|
+
const tagWatchKey = useMemo(
|
|
66
68
|
() =>
|
|
67
|
-
|
|
69
|
+
selectedTags
|
|
68
70
|
.map(opt => `${opt.value}:${resolveTagLabel(opt)}:${opt.iconName ?? ''}`)
|
|
69
71
|
.join('|'),
|
|
70
|
-
[resolveTagLabel,
|
|
72
|
+
[resolveTagLabel, selectedTags],
|
|
71
73
|
);
|
|
72
74
|
|
|
73
75
|
const selectedValueText = useMemo(
|
|
74
|
-
() =>
|
|
75
|
-
[resolveTagLabel,
|
|
76
|
+
() => selectedTags.map(resolveTagLabel).join(', '),
|
|
77
|
+
[resolveTagLabel, selectedTags],
|
|
76
78
|
);
|
|
77
79
|
const usesTagDisplay = selectedValueDisplay === 'tags';
|
|
78
|
-
const shouldShowBadge = showSelectionCountBadge &&
|
|
80
|
+
const shouldShowBadge = showSelectionCountBadge && selectedTags.length > 0;
|
|
79
81
|
|
|
80
|
-
const contentWidth = useElementWidth(contentRef, `${
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const measurementTrackElement = measureTrackRef.current;
|
|
82
|
+
const contentWidth = useElementWidth(contentRef, `${tagWatchKey}-${isOpen}-${shouldShowBadge}`);
|
|
83
|
+
const ellipsisWidth = useElementWidth(ellipsisProbeRef, `${tagWatchKey}-${isOpen}`);
|
|
84
|
+
const badgeWidth = useElementWidth(badgeProbeRef, `${tagWatchKey}-${isOpen}-${shouldShowBadge}`);
|
|
85
|
+
const { childGap: tagGap, childWidths: tagWidths } = useMeasuredChildWidths(measureTrackRef, `${tagWatchKey}-${isOpen}`);
|
|
85
86
|
|
|
86
|
-
const
|
|
87
|
-
const el = measureTrackRef.current;
|
|
88
|
-
if (!el) return 0;
|
|
89
|
-
const styles = getComputedStyle(el);
|
|
90
|
-
const parsed = Number.parseFloat(styles.columnGap || styles.gap || '0');
|
|
91
|
-
return Number.isFinite(parsed) ? parsed : 0;
|
|
92
|
-
}, [measurementTrackElement, measurementTrackWidth]);
|
|
93
|
-
|
|
94
|
-
const chipWidths = useMemo(() => {
|
|
95
|
-
const el = measureTrackRef.current;
|
|
96
|
-
if (!el) return [];
|
|
97
|
-
return Array.from(el.children).map((child) => {
|
|
98
|
-
const width = (child as HTMLElement).getBoundingClientRect().width;
|
|
99
|
-
return Number.isFinite(width) ? width : 0;
|
|
100
|
-
});
|
|
101
|
-
}, [chipWatchKey, measurementTrackElement, measurementTrackWidth]);
|
|
102
|
-
|
|
103
|
-
const layout = useVisibleChips({
|
|
87
|
+
const layout = useVisibleTriggerTags({
|
|
104
88
|
containerWidth: contentWidth,
|
|
105
|
-
|
|
106
|
-
|
|
89
|
+
tagWidths,
|
|
90
|
+
tagGap,
|
|
107
91
|
badgeWidth,
|
|
108
92
|
ellipsisWidth,
|
|
109
93
|
showBadge: shouldShowBadge,
|
|
110
94
|
});
|
|
111
95
|
|
|
112
|
-
const canMeasure = contentWidth > 0 &&
|
|
113
|
-
const
|
|
114
|
-
? layout.
|
|
115
|
-
:
|
|
96
|
+
const canMeasure = contentWidth > 0 && tagWidths.length === selectedTags.length;
|
|
97
|
+
const visibleTags = canMeasure
|
|
98
|
+
? layout.visibleTagIndices.map(index => selectedTags[index]!).filter(Boolean)
|
|
99
|
+
: selectedTags;
|
|
116
100
|
const showEllipsis = usesTagDisplay && (canMeasure ? layout.showEllipsis : false);
|
|
117
101
|
const showBadge = usesTagDisplay && (canMeasure ? layout.showBadge : shouldShowBadge);
|
|
118
102
|
|
|
119
|
-
const renderSelectionTag = (opt: ComboboxOption,
|
|
103
|
+
const renderSelectionTag = (opt: ComboboxOption, tagIdx: number, onRemove?: () => void) => (
|
|
120
104
|
<Tag
|
|
121
105
|
key={opt.value}
|
|
122
106
|
color="neutral"
|
|
123
|
-
selected={
|
|
107
|
+
selected={selectedTagValuesSet.has(opt.value) || focusedTagIndex === tagIdx}
|
|
124
108
|
slotStart={opt.iconName ? <Icon name={opt.iconName} size={12} /> : undefined}
|
|
125
109
|
onRemove={onRemove}
|
|
126
110
|
removeLabel={`Remove ${resolveTagLabel(opt)}`}
|
|
@@ -132,10 +116,20 @@ export const ComboboxButtonTrigger = ({
|
|
|
132
116
|
|
|
133
117
|
const renderSelectionCountBadge = (withA11yLabel: boolean) => (
|
|
134
118
|
<Badge colour="salmon" a11yLabel={withA11yLabel ? selectionCountA11yLabel : undefined}>
|
|
135
|
-
{
|
|
119
|
+
{selectedTags.length}
|
|
136
120
|
</Badge>
|
|
137
121
|
);
|
|
138
122
|
|
|
123
|
+
const visibleTagItems = visibleTags.map((opt, tagIdx) => ({
|
|
124
|
+
id: opt.value,
|
|
125
|
+
children: resolveTagLabel(opt),
|
|
126
|
+
color: 'neutral' as const,
|
|
127
|
+
selected: selectedTagValuesSet.has(opt.value) || focusedTagIndex === tagIdx,
|
|
128
|
+
slotStart: opt.iconName ? <Icon name={opt.iconName} size={12} /> : undefined,
|
|
129
|
+
onRemove: disabled ? undefined : () => removeValue(opt.value),
|
|
130
|
+
removeLabel: `Remove ${resolveTagLabel(opt)}`,
|
|
131
|
+
}));
|
|
132
|
+
|
|
139
133
|
return (
|
|
140
134
|
<Popover.Anchor asChild>
|
|
141
135
|
<div
|
|
@@ -160,13 +154,15 @@ export const ComboboxButtonTrigger = ({
|
|
|
160
154
|
{usesTagDisplay
|
|
161
155
|
? (
|
|
162
156
|
<div className="ds-combobox__button-tags-viewport">
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
{selectedTags.length === 0
|
|
158
|
+
? <span className="ds-combobox__button-placeholder">{placeholder}</span>
|
|
159
|
+
: (
|
|
160
|
+
<TagList
|
|
161
|
+
items={visibleTagItems}
|
|
162
|
+
returnFocusRef={triggerRef}
|
|
163
|
+
className="ds-combobox__tag-list"
|
|
164
|
+
/>
|
|
165
|
+
)}
|
|
170
166
|
{showEllipsis && <span className="ds-combobox__button-ellipsis" aria-hidden="true">…</span>}
|
|
171
167
|
</div>
|
|
172
168
|
)
|
|
@@ -202,11 +198,11 @@ export const ComboboxButtonTrigger = ({
|
|
|
202
198
|
|
|
203
199
|
{usesTagDisplay && (
|
|
204
200
|
<div className="ds-combobox__measure" aria-hidden="true">
|
|
205
|
-
{/* Mirror the rendered
|
|
201
|
+
{/* Mirror the rendered tags off-screen so width calculations use the real Tag layout. */}
|
|
206
202
|
<div className="ds-combobox__button-tags-track" ref={measureTrackRef}>
|
|
207
|
-
{
|
|
203
|
+
{selectedTags.map((opt, tagIdx) => (
|
|
208
204
|
<span key={`measure-${opt.value}`} className="ds-combobox__measure-chip">
|
|
209
|
-
{renderSelectionTag(opt,
|
|
205
|
+
{renderSelectionTag(opt, tagIdx, disabled ? undefined : () => {})}
|
|
210
206
|
</span>
|
|
211
207
|
))}
|
|
212
208
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import { Icon } from 'Components/icon/Icon';
|
|
3
|
-
import {
|
|
3
|
+
import { TagList } from 'Components/tagList/TagList';
|
|
4
4
|
import { Popover } from 'radix-ui';
|
|
5
5
|
import type { ComboboxAriaInvalid, ComboboxOption, ComboboxSelectedValueDisplay } from './types.js';
|
|
6
6
|
|
|
@@ -74,6 +74,15 @@ export const ComboboxTrigger = (props: ComboboxTriggerProps): React.JSX.Element
|
|
|
74
74
|
= selectedValueDisplay === 'text'
|
|
75
75
|
&& query.length === 0
|
|
76
76
|
&& selectedValueText.length > 0;
|
|
77
|
+
const selectedTagItems = selectedChips.map(opt => ({
|
|
78
|
+
id: opt.value,
|
|
79
|
+
children: resolveTagLabel(opt),
|
|
80
|
+
color: 'neutral' as const,
|
|
81
|
+
selected: selectedChipValuesSet.has(opt.value),
|
|
82
|
+
slotStart: opt.iconName ? <Icon name={opt.iconName} size={12} /> : undefined,
|
|
83
|
+
onRemove: disabled ? undefined : () => removeValue(opt.value),
|
|
84
|
+
removeLabel: `Remove ${resolveTagLabel(opt)}`,
|
|
85
|
+
}));
|
|
77
86
|
|
|
78
87
|
return (
|
|
79
88
|
<Popover.Anchor asChild>
|
|
@@ -87,21 +96,15 @@ export const ComboboxTrigger = (props: ComboboxTriggerProps): React.JSX.Element
|
|
|
87
96
|
onClick={handleTriggerClick}
|
|
88
97
|
>
|
|
89
98
|
<div className="ds-combobox__chips-and-input">
|
|
90
|
-
{selectedValueDisplay === 'tags'
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
removeButtonTabIndex={-1}
|
|
100
|
-
>
|
|
101
|
-
{resolveTagLabel(opt)}
|
|
102
|
-
</Tag>
|
|
103
|
-
))
|
|
104
|
-
: null}
|
|
99
|
+
{selectedValueDisplay === 'tags' && (
|
|
100
|
+
<TagList
|
|
101
|
+
items={selectedTagItems}
|
|
102
|
+
wrap
|
|
103
|
+
highlightedItemIndex={focusedChipIndex}
|
|
104
|
+
returnFocusRef={inputRef}
|
|
105
|
+
className="ds-combobox__tag-list"
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
105
108
|
{showSelectedValueText && (
|
|
106
109
|
<span className="ds-combobox__selected-value">
|
|
107
110
|
{selectedValueText}
|
|
@@ -9,16 +9,14 @@
|
|
|
9
9
|
align-items: center;
|
|
10
10
|
gap: var(--spacing-small);
|
|
11
11
|
min-height: var(--form-field-text-medium-height);
|
|
12
|
-
padding: var(--spacing-xsmall) var(--spacing-small);
|
|
12
|
+
padding: calc(var(--spacing-xsmall) - (var(--border-weight))) var(--spacing-small);
|
|
13
13
|
border: var(--border-weight) solid var(--form-field-combobox-default-color-border);
|
|
14
14
|
border-radius: var(--form-field-radius);
|
|
15
15
|
background-color: var(--form-field-combobox-default-color-background);
|
|
16
16
|
color: var(--form-field-combobox-default-color-text);
|
|
17
17
|
cursor: text;
|
|
18
18
|
transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s;
|
|
19
|
-
box-sizing: border-box;
|
|
20
19
|
font-style: normal;
|
|
21
|
-
line-height: 150%;
|
|
22
20
|
|
|
23
21
|
&:hover:not(.ds-combobox__trigger--disabled) {
|
|
24
22
|
border-color: var(--form-field-combobox-hover-color-border);
|
|
@@ -70,11 +68,18 @@
|
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
.ds-combobox__button-tags-viewport {
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
73
|
flex: 1;
|
|
74
74
|
min-width: 0;
|
|
75
75
|
overflow: hidden;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
.ds-combobox__tag-list {
|
|
79
|
+
flex: 1;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
.ds-combobox__button-tags-track {
|
|
79
84
|
display: inline-flex;
|
|
80
85
|
width: auto;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { computeVisibleTriggerTagsLayout } from './useVisibleTriggerTags.js';
|
|
3
|
+
|
|
4
|
+
describe('computeVisibleTriggerTagsLayout', () => {
|
|
5
|
+
test('shows all tags and no ellipsis when there is no overflow', () => {
|
|
6
|
+
const result = computeVisibleTriggerTagsLayout({
|
|
7
|
+
containerWidth: 300,
|
|
8
|
+
tagWidths: [70, 80, 60],
|
|
9
|
+
tagGap: 4,
|
|
10
|
+
badgeWidth: 24,
|
|
11
|
+
ellipsisWidth: 12,
|
|
12
|
+
showBadge: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(result).toEqual({
|
|
16
|
+
visibleTagIndices: [0, 1, 2],
|
|
17
|
+
hiddenTagCount: 0,
|
|
18
|
+
showBadge: true,
|
|
19
|
+
showEllipsis: false,
|
|
20
|
+
hasOverflow: false,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('reserves badge and ellipsis before fitting tags when overflow exists', () => {
|
|
25
|
+
const result = computeVisibleTriggerTagsLayout({
|
|
26
|
+
containerWidth: 160,
|
|
27
|
+
tagWidths: [60, 60, 60],
|
|
28
|
+
tagGap: 4,
|
|
29
|
+
badgeWidth: 24,
|
|
30
|
+
ellipsisWidth: 12,
|
|
31
|
+
showBadge: true,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(result.visibleTagIndices).toEqual([0]);
|
|
35
|
+
expect(result.hiddenTagCount).toBe(2);
|
|
36
|
+
expect(result.showEllipsis).toBe(true);
|
|
37
|
+
expect(result.hasOverflow).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('allows zero tags when badge and ellipsis take priority in tiny space', () => {
|
|
41
|
+
const result = computeVisibleTriggerTagsLayout({
|
|
42
|
+
containerWidth: 36,
|
|
43
|
+
tagWidths: [80, 80],
|
|
44
|
+
tagGap: 4,
|
|
45
|
+
badgeWidth: 24,
|
|
46
|
+
ellipsisWidth: 12,
|
|
47
|
+
showBadge: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(result.visibleTagIndices).toEqual([]);
|
|
51
|
+
expect(result.hiddenTagCount).toBe(2);
|
|
52
|
+
expect(result.showEllipsis).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('reserving badge space reduces visible count', () => {
|
|
56
|
+
const withBadge = computeVisibleTriggerTagsLayout({
|
|
57
|
+
containerWidth: 180,
|
|
58
|
+
tagWidths: [60, 60, 60],
|
|
59
|
+
tagGap: 4,
|
|
60
|
+
badgeWidth: 24,
|
|
61
|
+
ellipsisWidth: 12,
|
|
62
|
+
showBadge: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const withoutBadge = computeVisibleTriggerTagsLayout({
|
|
66
|
+
containerWidth: 180,
|
|
67
|
+
tagWidths: [60, 60, 60],
|
|
68
|
+
tagGap: 4,
|
|
69
|
+
badgeWidth: 24,
|
|
70
|
+
ellipsisWidth: 12,
|
|
71
|
+
showBadge: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(withoutBadge.visibleTagIndices.length).toBeGreaterThanOrEqual(withBadge.visibleTagIndices.length);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('uses safety buffer to avoid a clipped final tag on boundary widths', () => {
|
|
78
|
+
const result = computeVisibleTriggerTagsLayout({
|
|
79
|
+
containerWidth: 195.5,
|
|
80
|
+
tagWidths: [73.4, 73.4, 20],
|
|
81
|
+
tagGap: 4,
|
|
82
|
+
badgeWidth: 24.2,
|
|
83
|
+
ellipsisWidth: 11.6,
|
|
84
|
+
showBadge: true,
|
|
85
|
+
safetyBuffer: 1,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result.visibleTagIndices).toEqual([0]);
|
|
89
|
+
expect(result.showEllipsis).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { fitSpacedWidths } from 'Utils/spacedWidths';
|
|
3
|
+
|
|
4
|
+
type ComputeVisibleTriggerTagsLayoutParams = {
|
|
5
|
+
containerWidth: number;
|
|
6
|
+
tagWidths: number[];
|
|
7
|
+
tagGap: number;
|
|
8
|
+
badgeWidth: number;
|
|
9
|
+
ellipsisWidth: number;
|
|
10
|
+
showBadge: boolean;
|
|
11
|
+
safetyBuffer?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type VisibleTriggerTagsLayout = {
|
|
15
|
+
visibleTagIndices: number[];
|
|
16
|
+
hiddenTagCount: number;
|
|
17
|
+
showBadge: boolean;
|
|
18
|
+
showEllipsis: boolean;
|
|
19
|
+
hasOverflow: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const computeVisibleTriggerTagsLayout = ({
|
|
23
|
+
containerWidth,
|
|
24
|
+
tagWidths,
|
|
25
|
+
tagGap,
|
|
26
|
+
badgeWidth,
|
|
27
|
+
ellipsisWidth,
|
|
28
|
+
showBadge,
|
|
29
|
+
safetyBuffer = 1,
|
|
30
|
+
}: ComputeVisibleTriggerTagsLayoutParams): VisibleTriggerTagsLayout => {
|
|
31
|
+
const selectedCount = tagWidths.length;
|
|
32
|
+
const shouldShowBadge = showBadge && selectedCount > 0;
|
|
33
|
+
|
|
34
|
+
if (selectedCount === 0 || containerWidth <= 0) {
|
|
35
|
+
return {
|
|
36
|
+
visibleTagIndices: [],
|
|
37
|
+
hiddenTagCount: 0,
|
|
38
|
+
showBadge: shouldShowBadge,
|
|
39
|
+
showEllipsis: false,
|
|
40
|
+
hasOverflow: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const badgeReserve = shouldShowBadge
|
|
45
|
+
? badgeWidth + tagGap
|
|
46
|
+
: 0;
|
|
47
|
+
|
|
48
|
+
const availableWithoutEllipsis = Math.max(0, containerWidth - badgeReserve - safetyBuffer);
|
|
49
|
+
const countWithoutEllipsis = fitSpacedWidths(availableWithoutEllipsis, tagWidths, tagGap);
|
|
50
|
+
const hasOverflow = countWithoutEllipsis < selectedCount;
|
|
51
|
+
|
|
52
|
+
const ellipsisReserve = hasOverflow
|
|
53
|
+
? ellipsisWidth + tagGap
|
|
54
|
+
: 0;
|
|
55
|
+
const availableForTags = Math.max(0, containerWidth - badgeReserve - ellipsisReserve - safetyBuffer);
|
|
56
|
+
const visibleCount = fitSpacedWidths(availableForTags, tagWidths, tagGap);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
visibleTagIndices: Array.from({ length: visibleCount }, (_, index) => index),
|
|
60
|
+
hiddenTagCount: Math.max(0, selectedCount - visibleCount),
|
|
61
|
+
showBadge: shouldShowBadge,
|
|
62
|
+
showEllipsis: visibleCount < selectedCount,
|
|
63
|
+
hasOverflow: visibleCount < selectedCount,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type UseVisibleTriggerTagsParams = ComputeVisibleTriggerTagsLayoutParams;
|
|
68
|
+
|
|
69
|
+
export const useVisibleTriggerTags = (params: UseVisibleTriggerTagsParams): VisibleTriggerTagsLayout =>
|
|
70
|
+
// Deps list each field instead of `[params]`: callers often pass a new `params` object every
|
|
71
|
+
// render; depending on object identity would rerun the layout every time even when values are unchanged.
|
|
72
|
+
useMemo(
|
|
73
|
+
() => computeVisibleTriggerTagsLayout(params),
|
|
74
|
+
[
|
|
75
|
+
params.badgeWidth,
|
|
76
|
+
params.containerWidth,
|
|
77
|
+
params.ellipsisWidth,
|
|
78
|
+
params.safetyBuffer,
|
|
79
|
+
params.showBadge,
|
|
80
|
+
params.tagGap,
|
|
81
|
+
params.tagWidths,
|
|
82
|
+
],
|
|
83
|
+
);
|