uikit-react-public 0.49.4 → 0.49.6
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/dist/components/Overlay/Overlay.d.ts +2 -2
- package/dist/components/Overlay/Overlay.stories.d.ts +13 -1
- package/dist/components/Select/index.d.ts +1 -1
- package/dist/components/Select/subcomponents/Panel.d.ts +1 -2
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +15 -0
- package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +18 -0
- package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +8 -0
- package/dist/components/ToggleGroup/__tests__/ToggleGroup.test.d.ts +1 -0
- package/dist/components/ToggleGroup/index.d.ts +4 -0
- package/dist/components/ToggleGroup/subcomponents/Item.d.ts +11 -0
- package/dist/components/ToggleGroup/subcomponents/Item.stories.d.ts +331 -0
- package/dist/components/ToggleGroup/subcomponents/__tests__/Item.test.d.ts +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/index.js +8351 -8121
- package/lib/components/Icon/svgImports.ts +4 -2
- package/lib/components/Overlay/Overlay.tsx +9 -3
- package/lib/components/Select/Select.mdx +1 -0
- package/lib/components/Select/__tests__/Select.test.tsx +137 -4
- package/lib/components/Select/index.ts +5 -1
- package/lib/components/Select/subcomponents/CustomSelect.tsx +42 -43
- package/lib/components/Select/subcomponents/Panel.tsx +2 -8
- package/lib/components/ToggleGroup/ToggleGroup.mdx +31 -0
- package/lib/components/ToggleGroup/ToggleGroup.stories.tsx +120 -0
- package/lib/components/ToggleGroup/ToggleGroup.tsx +144 -0
- package/lib/components/ToggleGroup/ToggleGroupContext.tsx +25 -0
- package/lib/components/ToggleGroup/__tests__/ToggleGroup.test.tsx +468 -0
- package/lib/components/ToggleGroup/index.ts +4 -0
- package/lib/components/ToggleGroup/subcomponents/Item.stories.tsx +63 -0
- package/lib/components/ToggleGroup/subcomponents/Item.tsx +233 -0
- package/lib/components/ToggleGroup/subcomponents/__tests__/Item.test.tsx +195 -0
- package/lib/components/index.ts +3 -0
- package/package.json +3 -3
- package/dist/components/Icon/svgImports.d.ts +0 -8
|
@@ -321,7 +321,9 @@ interface SVGRProps {
|
|
|
321
321
|
|
|
322
322
|
type IconComponent = (props: SVGProps<SVGSVGElement> & SVGRProps) => ReactNode;
|
|
323
323
|
|
|
324
|
-
|
|
324
|
+
// `satisfies` keeps literal key names (needed for icon name autocomplete/type-checking)
|
|
325
|
+
// while still validating every value matches IconComponent.
|
|
326
|
+
const iconSvgs = {
|
|
325
327
|
Activity,
|
|
326
328
|
Airplay,
|
|
327
329
|
AlertCircle,
|
|
@@ -618,6 +620,6 @@ const iconSvgs: Record<string, IconComponent> = {
|
|
|
618
620
|
Zap,
|
|
619
621
|
ZoomIn,
|
|
620
622
|
ZoomOut,
|
|
621
|
-
}
|
|
623
|
+
} satisfies Record<string, IconComponent>;
|
|
622
624
|
|
|
623
625
|
export default iconSvgs;
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
size as fuiSize,
|
|
20
20
|
autoUpdate,
|
|
21
21
|
Placement,
|
|
22
|
+
type Derivable,
|
|
22
23
|
type FlipOptions,
|
|
23
24
|
type ShiftOptions,
|
|
24
25
|
type Strategy,
|
|
@@ -47,7 +48,7 @@ export interface OverlayProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
47
48
|
strategy?: Strategy;
|
|
48
49
|
blanket?: boolean;
|
|
49
50
|
flip?: boolean | FlipOptions;
|
|
50
|
-
shift?: boolean | ShiftOptions
|
|
51
|
+
shift?: boolean | ShiftOptions | Derivable<ShiftOptions>;
|
|
51
52
|
offset?: number;
|
|
52
53
|
size?: boolean | OverlaySizeOptions;
|
|
53
54
|
arrow?: ReactElement;
|
|
@@ -99,8 +100,7 @@ const Overlay = forwardRef<Ref, OverlayProps>(
|
|
|
99
100
|
...(typeof flip === 'object' ? flip : {}),
|
|
100
101
|
})
|
|
101
102
|
);
|
|
102
|
-
if (shift)
|
|
103
|
-
fuiMiddleware.push(fuiShift(typeof shift === 'object' ? shift : {}));
|
|
103
|
+
if (shift) fuiMiddleware.push(fuiShift(shift === true ? {} : shift));
|
|
104
104
|
if (size) {
|
|
105
105
|
const sizeOptions = typeof size === 'object' ? size : {};
|
|
106
106
|
const { padding = 0, matchReferenceWidth = false } = sizeOptions;
|
|
@@ -111,6 +111,12 @@ const Overlay = forwardRef<Ref, OverlayProps>(
|
|
|
111
111
|
apply({ availableWidth, availableHeight, elements, rects }) {
|
|
112
112
|
const { style } = elements.floating;
|
|
113
113
|
|
|
114
|
+
// CSS var applies before remeasurement; React props update too late.
|
|
115
|
+
style.setProperty(
|
|
116
|
+
'--ucl-overlay-available-height',
|
|
117
|
+
`${Math.max(0, availableHeight)}px`
|
|
118
|
+
);
|
|
119
|
+
|
|
114
120
|
onSizeChange?.({
|
|
115
121
|
referenceWidth: rects.reference.width,
|
|
116
122
|
availableWidth: Math.max(0, availableWidth),
|
|
@@ -172,6 +172,7 @@ For very long labels, set `lineBreak` to allow breaking the text inside options.
|
|
|
172
172
|
- In `selectionBehaviour="commit"`, arrow keys move highlight without changing value until Enter/click.
|
|
173
173
|
- Tabbing away from an open custom dropdown closes it and lets focus move normally to the next or previous control.
|
|
174
174
|
- Native variant leverages built-in browser semantics.
|
|
175
|
+
- Tabbing to a filterable custom Select focuses the closed combobox. Typing starts filtering immediately; Enter, Space, an arrow key, or a click also opens the panel. Focus moves to the filter input when the panel opens.
|
|
175
176
|
- The filter input (when `filterable` is true) is focusable when the panel opens; Escape and arrow keys bubble to the parent so the dropdown still closes/navigates.
|
|
176
177
|
- In the custom variant, `clearable` emits `null` as the cleared value.
|
|
177
178
|
- `filterInputProps` lets you forward ARIA hooks like `aria-describedby` to the filter input.
|
|
@@ -116,11 +116,70 @@ describe('Select', () => {
|
|
|
116
116
|
expect(overlay).toHaveClass('ucl-overlay');
|
|
117
117
|
await waitFor(() => {
|
|
118
118
|
expect(overlay).toHaveStyle({
|
|
119
|
-
position: '
|
|
119
|
+
position: 'fixed',
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
test.each([
|
|
125
|
+
{ fieldTop: 200, menuHeight: 120, menuTop: 248 },
|
|
126
|
+
{ fieldTop: 500, menuHeight: 120, menuTop: 380 },
|
|
127
|
+
{ fieldTop: 280, menuHeight: 400, menuTop: 328 },
|
|
128
|
+
])(
|
|
129
|
+
'anchors the menu without covering the input at $fieldTop with height $menuHeight',
|
|
130
|
+
async ({ fieldTop, menuHeight, menuTop }) => {
|
|
131
|
+
vi.spyOn(document.documentElement, 'clientWidth', 'get').mockReturnValue(
|
|
132
|
+
800
|
|
133
|
+
);
|
|
134
|
+
vi.spyOn(document.documentElement, 'clientHeight', 'get').mockReturnValue(
|
|
135
|
+
600
|
|
136
|
+
);
|
|
137
|
+
vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockReturnValue(
|
|
138
|
+
240
|
|
139
|
+
);
|
|
140
|
+
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(
|
|
141
|
+
menuHeight
|
|
142
|
+
);
|
|
143
|
+
vi.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(
|
|
144
|
+
function (this: Element) {
|
|
145
|
+
return this.getAttribute('role') === 'combobox'
|
|
146
|
+
? new DOMRect(80, fieldTop, 240, 48)
|
|
147
|
+
: new DOMRect(0, 0, 240, menuHeight);
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const user = userEvent.setup();
|
|
153
|
+
const result = render(
|
|
154
|
+
<ThemeContextProvider>
|
|
155
|
+
<Select
|
|
156
|
+
filterable
|
|
157
|
+
options={defaultOptions}
|
|
158
|
+
value='2'
|
|
159
|
+
onValueChange={() => {}}
|
|
160
|
+
/>
|
|
161
|
+
</ThemeContextProvider>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
await user.click(result.getByRole('combobox'));
|
|
165
|
+
|
|
166
|
+
const expectAnchoredMenu = () => {
|
|
167
|
+
expect(result.getByRole('listbox').parentElement).toHaveStyle({
|
|
168
|
+
position: 'fixed',
|
|
169
|
+
left: '80px',
|
|
170
|
+
top: `${menuTop}px`,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
await waitFor(expectAnchoredMenu);
|
|
174
|
+
|
|
175
|
+
await user.type(result.getByRole('searchbox'), '2');
|
|
176
|
+
await waitFor(expectAnchoredMenu);
|
|
177
|
+
} finally {
|
|
178
|
+
vi.restoreAllMocks();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
124
183
|
test('dropdownWidth defaults to content width', async () => {
|
|
125
184
|
const user = userEvent.setup();
|
|
126
185
|
const renderResult = render(
|
|
@@ -743,7 +802,7 @@ describe('Select', () => {
|
|
|
743
802
|
expect(filterInput).toHaveAttribute('aria-describedby', 'filter-hint');
|
|
744
803
|
});
|
|
745
804
|
|
|
746
|
-
test('
|
|
805
|
+
test('keeps a filterable select closed on keyboard focus', async () => {
|
|
747
806
|
const user = userEvent.setup();
|
|
748
807
|
const result = render(
|
|
749
808
|
<ThemeContextProvider>
|
|
@@ -758,11 +817,79 @@ describe('Select', () => {
|
|
|
758
817
|
|
|
759
818
|
await user.tab();
|
|
760
819
|
|
|
761
|
-
const
|
|
762
|
-
expect(
|
|
820
|
+
const combobox = result.getByRole('combobox');
|
|
821
|
+
expect(combobox).toHaveFocus();
|
|
822
|
+
expect(combobox).toHaveAttribute('aria-expanded', 'false');
|
|
823
|
+
expect(result.queryByRole('searchbox')).not.toBeInTheDocument();
|
|
824
|
+
expect(result.queryByRole('listbox')).not.toBeInTheDocument();
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
test('does not duplicate the selected option text while closed', async () => {
|
|
828
|
+
const user = userEvent.setup();
|
|
829
|
+
const result = render(
|
|
830
|
+
<ThemeContextProvider>
|
|
831
|
+
<Select
|
|
832
|
+
filterable
|
|
833
|
+
options={defaultOptions}
|
|
834
|
+
value={defaultOptions[1].value}
|
|
835
|
+
onValueChange={() => {}}
|
|
836
|
+
/>
|
|
837
|
+
</ThemeContextProvider>
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
await user.tab();
|
|
841
|
+
|
|
842
|
+
const combobox = result.getByRole('combobox');
|
|
843
|
+
expect(combobox).toHaveTextContent(defaultOptions[1].label);
|
|
844
|
+
expect(
|
|
845
|
+
result.queryByText(`${defaultOptions[1].label} selected`)
|
|
846
|
+
).not.toBeInTheDocument();
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
test('opens a focused filterable select with ArrowDown and focuses its filter', async () => {
|
|
850
|
+
const user = userEvent.setup();
|
|
851
|
+
const result = render(
|
|
852
|
+
<ThemeContextProvider>
|
|
853
|
+
<Select
|
|
854
|
+
filterable
|
|
855
|
+
options={defaultOptions}
|
|
856
|
+
value=''
|
|
857
|
+
onValueChange={() => {}}
|
|
858
|
+
/>
|
|
859
|
+
</ThemeContextProvider>
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
await user.tab();
|
|
863
|
+
await user.keyboard('{ArrowDown}');
|
|
864
|
+
|
|
865
|
+
expect(result.getByRole('searchbox')).toHaveFocus();
|
|
763
866
|
expect(result.getByRole('listbox')).toBeInTheDocument();
|
|
764
867
|
});
|
|
765
868
|
|
|
869
|
+
test('opens and filters when typing on a focused closed filterable select', async () => {
|
|
870
|
+
const user = userEvent.setup();
|
|
871
|
+
const result = render(
|
|
872
|
+
<ThemeContextProvider>
|
|
873
|
+
<Select
|
|
874
|
+
filterable
|
|
875
|
+
options={defaultOptions}
|
|
876
|
+
value=''
|
|
877
|
+
onValueChange={() => {}}
|
|
878
|
+
/>
|
|
879
|
+
</ThemeContextProvider>
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
await user.tab();
|
|
883
|
+
await user.keyboard('2');
|
|
884
|
+
|
|
885
|
+
expect(result.getByRole('searchbox')).toHaveFocus();
|
|
886
|
+
expect(result.getByRole('searchbox')).toHaveValue('2');
|
|
887
|
+
expect(result.getAllByRole('option')).toHaveLength(1);
|
|
888
|
+
expect(
|
|
889
|
+
result.getByRole('option', { name: 'Option 2' })
|
|
890
|
+
).toBeInTheDocument();
|
|
891
|
+
});
|
|
892
|
+
|
|
766
893
|
test('pressing Escape closes dropdown and clears filter', async () => {
|
|
767
894
|
const user = userEvent.setup();
|
|
768
895
|
const result = render(
|
|
@@ -939,6 +1066,9 @@ describe('Select', () => {
|
|
|
939
1066
|
|
|
940
1067
|
await user.tab();
|
|
941
1068
|
await user.tab();
|
|
1069
|
+
expect(result.getByRole('combobox')).toHaveFocus();
|
|
1070
|
+
|
|
1071
|
+
await user.keyboard('{Enter}');
|
|
942
1072
|
expect(result.getByRole('searchbox')).toHaveFocus();
|
|
943
1073
|
expect(result.getByRole('listbox')).toBeInTheDocument();
|
|
944
1074
|
|
|
@@ -985,6 +1115,9 @@ describe('Select', () => {
|
|
|
985
1115
|
);
|
|
986
1116
|
|
|
987
1117
|
await user.tab();
|
|
1118
|
+
expect(result.getByRole('combobox')).toHaveFocus();
|
|
1119
|
+
|
|
1120
|
+
await user.keyboard('{Enter}');
|
|
988
1121
|
const searchbox = result.getByRole('searchbox');
|
|
989
1122
|
expect(searchbox).toHaveFocus();
|
|
990
1123
|
expect(result.getByRole('listbox')).toBeInTheDocument();
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
useLayoutEffect,
|
|
6
6
|
useMemo,
|
|
7
7
|
useId,
|
|
8
|
+
useCallback,
|
|
8
9
|
} from 'react';
|
|
9
10
|
import { css, cx } from '@emotion/css';
|
|
10
11
|
import { VisibleField, Panel, CustomOption, FilterInput } from '.';
|
|
@@ -93,23 +94,25 @@ const CustomSelect = <T extends string | number>({
|
|
|
93
94
|
|
|
94
95
|
const internalRef = useRef<HTMLDivElement>(null);
|
|
95
96
|
const effectiveRef = internalRef;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
97
|
+
|
|
98
|
+
// Keep the anchor attached while the newly opened Overlay reads its ref.
|
|
99
|
+
const setRefs = useCallback(
|
|
100
|
+
(node: HTMLDivElement | null) => {
|
|
101
|
+
internalRef.current = node;
|
|
102
|
+
if (typeof ref === 'function') {
|
|
103
|
+
ref(node);
|
|
104
|
+
} else if (
|
|
105
|
+
ref &&
|
|
106
|
+
'current' in
|
|
107
|
+
(ref as React.RefObject<HTMLDivElement | HTMLSelectElement | null>)
|
|
108
|
+
) {
|
|
109
|
+
(
|
|
110
|
+
ref as React.RefObject<HTMLDivElement | HTMLSelectElement | null>
|
|
111
|
+
).current = node;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
[ref]
|
|
115
|
+
);
|
|
113
116
|
|
|
114
117
|
const [theme] = useTheme();
|
|
115
118
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -371,10 +374,6 @@ const CustomSelect = <T extends string | number>({
|
|
|
371
374
|
|
|
372
375
|
const handleClick = (event: React.MouseEvent) => {
|
|
373
376
|
if (disabled) return;
|
|
374
|
-
if (openedViaFocusRef.current) {
|
|
375
|
-
openedViaFocusRef.current = false;
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
377
|
if (!isOpen) {
|
|
379
378
|
openPanel();
|
|
380
379
|
if (filterable && filterInputRef.current) {
|
|
@@ -485,6 +484,20 @@ const CustomSelect = <T extends string | number>({
|
|
|
485
484
|
|
|
486
485
|
if (disabled) return;
|
|
487
486
|
|
|
487
|
+
const isAltGraph = event.getModifierState('AltGraph');
|
|
488
|
+
const isPrintableCharacter =
|
|
489
|
+
event.key.length === 1 &&
|
|
490
|
+
event.key !== ' ' &&
|
|
491
|
+
!event.metaKey &&
|
|
492
|
+
((!event.altKey && !event.ctrlKey) || isAltGraph);
|
|
493
|
+
|
|
494
|
+
if (filterable && !isOpen && isPrintableCharacter) {
|
|
495
|
+
event.preventDefault();
|
|
496
|
+
setFilterText(event.key);
|
|
497
|
+
openPanel();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
488
501
|
if (event.key === 'Enter') {
|
|
489
502
|
if (!isOpen) {
|
|
490
503
|
openPanel();
|
|
@@ -507,7 +520,6 @@ const CustomSelect = <T extends string | number>({
|
|
|
507
520
|
closePanel();
|
|
508
521
|
// Return focus to the combobox so the screen reader stays on the control
|
|
509
522
|
// instead of falling back to the document/page title.
|
|
510
|
-
skipOpenOnFocusRef.current = true;
|
|
511
523
|
effectiveRef.current?.focus();
|
|
512
524
|
return;
|
|
513
525
|
}
|
|
@@ -521,7 +533,6 @@ const CustomSelect = <T extends string | number>({
|
|
|
521
533
|
}
|
|
522
534
|
if (isOpen && event.key === 'Escape') {
|
|
523
535
|
closePanel();
|
|
524
|
-
skipOpenOnFocusRef.current = true;
|
|
525
536
|
event.currentTarget.focus();
|
|
526
537
|
return;
|
|
527
538
|
}
|
|
@@ -572,19 +583,6 @@ const CustomSelect = <T extends string | number>({
|
|
|
572
583
|
}
|
|
573
584
|
};
|
|
574
585
|
|
|
575
|
-
const handleFocus = (event: React.FocusEvent<HTMLDivElement>) => {
|
|
576
|
-
if (disabled) return;
|
|
577
|
-
if (skipOpenOnFocusRef.current) {
|
|
578
|
-
skipOpenOnFocusRef.current = false;
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
const isKeyboardFocus = event.currentTarget.matches(':focus-visible');
|
|
582
|
-
if (filterable && isKeyboardFocus) {
|
|
583
|
-
openedViaFocusRef.current = true;
|
|
584
|
-
openPanel();
|
|
585
|
-
if (filterInputRef.current) filterInputRef.current.focus();
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
586
|
const { md } = theme.typography.body;
|
|
589
587
|
|
|
590
588
|
const baseStyle = css`
|
|
@@ -668,7 +666,6 @@ const CustomSelect = <T extends string | number>({
|
|
|
668
666
|
<div
|
|
669
667
|
onClick={handleClick}
|
|
670
668
|
onKeyDown={handleKeyDown}
|
|
671
|
-
onFocus={handleFocus}
|
|
672
669
|
onBlur={handleBlur}
|
|
673
670
|
tabIndex={disabled || (filterable && isOpen) ? -1 : 0}
|
|
674
671
|
className={style}
|
|
@@ -710,7 +707,7 @@ const CustomSelect = <T extends string | number>({
|
|
|
710
707
|
/>
|
|
711
708
|
)}
|
|
712
709
|
</VisibleField>
|
|
713
|
-
{filterable && selectedOption && (
|
|
710
|
+
{filterable && isOpen && selectedOption && (
|
|
714
711
|
<span
|
|
715
712
|
id={selectionStatusId}
|
|
716
713
|
className={visuallyHiddenStyle}
|
|
@@ -729,12 +726,15 @@ const CustomSelect = <T extends string | number>({
|
|
|
729
726
|
<Overlay
|
|
730
727
|
reference={effectiveRef}
|
|
731
728
|
placement='bottom-start'
|
|
729
|
+
strategy='fixed'
|
|
732
730
|
flip={{ padding: overlayViewportPadding }}
|
|
733
|
-
shift={{
|
|
731
|
+
shift={({ rects }) => ({
|
|
734
732
|
padding: overlayViewportPadding,
|
|
735
|
-
mainAxis:
|
|
736
|
-
|
|
737
|
-
|
|
733
|
+
mainAxis:
|
|
734
|
+
dropdownWidth === 'content' &&
|
|
735
|
+
rects.floating.width > rects.reference.width,
|
|
736
|
+
crossAxis: false,
|
|
737
|
+
})}
|
|
738
738
|
size={{
|
|
739
739
|
matchReferenceWidth: true,
|
|
740
740
|
padding: overlayViewportPadding,
|
|
@@ -747,7 +747,6 @@ const CustomSelect = <T extends string | number>({
|
|
|
747
747
|
referenceWidth={
|
|
748
748
|
overlaySize?.referenceWidth ?? panelReferenceWidth ?? undefined
|
|
749
749
|
}
|
|
750
|
-
availableHeight={overlaySize?.availableHeight}
|
|
751
750
|
maxWidth={panelMaxWidth ?? undefined}
|
|
752
751
|
id={listboxId}
|
|
753
752
|
role='listbox'
|
|
@@ -7,7 +7,6 @@ const NAME = 'ucl-uikit-select__panel';
|
|
|
7
7
|
type PanelProps = React.ComponentPropsWithoutRef<'div'> & {
|
|
8
8
|
dropdownWidth?: SelectDropdownWidth;
|
|
9
9
|
referenceWidth?: number;
|
|
10
|
-
availableHeight?: number;
|
|
11
10
|
maxWidth?: number;
|
|
12
11
|
};
|
|
13
12
|
|
|
@@ -15,7 +14,6 @@ const Panel = ({
|
|
|
15
14
|
className,
|
|
16
15
|
dropdownWidth = 'content',
|
|
17
16
|
referenceWidth,
|
|
18
|
-
availableHeight,
|
|
19
17
|
maxWidth,
|
|
20
18
|
...props
|
|
21
19
|
}: PanelProps) => {
|
|
@@ -32,19 +30,15 @@ const Panel = ({
|
|
|
32
30
|
typeof maxWidth === 'number'
|
|
33
31
|
? `min(${maxWidth}px, calc(100vw - (${theme.margin.m32} * 2)))`
|
|
34
32
|
: `calc(100vw - (${theme.margin.m32} * 2))`;
|
|
35
|
-
const maxHeightValue =
|
|
36
|
-
typeof availableHeight === 'number'
|
|
37
|
-
? `min(400px, ${availableHeight}px)`
|
|
38
|
-
: '400px';
|
|
39
33
|
|
|
40
34
|
const baseStyle = css`
|
|
41
35
|
min-width: ${referenceWidthValue};
|
|
42
36
|
width: fit-content;
|
|
43
37
|
max-width: ${maxWidthValue};
|
|
44
|
-
max-height:
|
|
38
|
+
max-height: min(400px, var(--ucl-overlay-available-height, 400px));
|
|
45
39
|
overflow-y: auto;
|
|
46
40
|
overflow-x: hidden;
|
|
47
|
-
box-sizing:
|
|
41
|
+
box-sizing: border-box;
|
|
48
42
|
padding: ${theme.padding.p8} 0;
|
|
49
43
|
border: ${theme.border.b1} solid ${theme.colour.border.subtle};
|
|
50
44
|
background-color: ${theme.colour.fill.inverse};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as ToggleGroupStories from "./ToggleGroup.stories";
|
|
2
|
+
import { Meta, Title, Subtitle, Canvas, Controls } from "@storybook/addon-docs/blocks";
|
|
3
|
+
|
|
4
|
+
<Meta of={ToggleGroupStories} />
|
|
5
|
+
<Title />
|
|
6
|
+
<Subtitle>A mutually-exclusive horizontal bar of radio buttons.</Subtitle>
|
|
7
|
+
|
|
8
|
+
Use a `<ToggleGroup>` when you want the user to switch between two or more views, modes, or states that sit side by side (e.g. list / grid, day / week / month).
|
|
9
|
+
|
|
10
|
+
<Canvas of={ToggleGroupStories.Default} sourceState="hidden" />
|
|
11
|
+
|
|
12
|
+
## ToggleGroup.Item components
|
|
13
|
+
|
|
14
|
+
### Icons
|
|
15
|
+
Add any `<Icon>` to the left of the `<Item>`.
|
|
16
|
+
|
|
17
|
+
<Canvas of={ToggleGroupStories.WithIcons} />
|
|
18
|
+
|
|
19
|
+
### Sublabels
|
|
20
|
+
Add a `sublabel` to the `<Item>`.
|
|
21
|
+
|
|
22
|
+
<Canvas of={ToggleGroupStories.WithSublabels} />
|
|
23
|
+
|
|
24
|
+
### Disabled
|
|
25
|
+
Disable a given `<Item>`.
|
|
26
|
+
|
|
27
|
+
<Canvas of={ToggleGroupStories.Disabled} />
|
|
28
|
+
|
|
29
|
+
## Props
|
|
30
|
+
|
|
31
|
+
<Controls of={ToggleGroupStories.Default} />
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import ToggleGroup from './ToggleGroup';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/ToggleGroup',
|
|
7
|
+
component: ToggleGroup,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
} satisfies Meta<typeof ToggleGroup>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
defaultValue: 'option-1',
|
|
21
|
+
children: (
|
|
22
|
+
<>
|
|
23
|
+
<ToggleGroup.Item
|
|
24
|
+
value='option-1'
|
|
25
|
+
label='Option 1'
|
|
26
|
+
/>
|
|
27
|
+
<ToggleGroup.Item
|
|
28
|
+
value='option-2'
|
|
29
|
+
label='Option 2'
|
|
30
|
+
/>
|
|
31
|
+
<ToggleGroup.Item
|
|
32
|
+
value='option-3'
|
|
33
|
+
label='Option 3'
|
|
34
|
+
/>
|
|
35
|
+
</>
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const WithIcons: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
defaultValue: 'option-1',
|
|
43
|
+
children: (
|
|
44
|
+
<>
|
|
45
|
+
<ToggleGroup.Item
|
|
46
|
+
value='option-1'
|
|
47
|
+
label='Option 1'
|
|
48
|
+
icon='BookOpen'
|
|
49
|
+
/>
|
|
50
|
+
<ToggleGroup.Item
|
|
51
|
+
value='option-2'
|
|
52
|
+
label='Option 2'
|
|
53
|
+
icon='Calendar'
|
|
54
|
+
/>
|
|
55
|
+
<ToggleGroup.Item
|
|
56
|
+
value='option-3'
|
|
57
|
+
label='Option 3'
|
|
58
|
+
icon='Star'
|
|
59
|
+
/>
|
|
60
|
+
<ToggleGroup.Item
|
|
61
|
+
value='option-4'
|
|
62
|
+
label='Option 4'
|
|
63
|
+
icon='User'
|
|
64
|
+
/>
|
|
65
|
+
</>
|
|
66
|
+
),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const WithSublabels: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
defaultValue: 'option-1',
|
|
73
|
+
children: (
|
|
74
|
+
<>
|
|
75
|
+
<ToggleGroup.Item
|
|
76
|
+
value='option-1'
|
|
77
|
+
label='Option 1'
|
|
78
|
+
sublabel='Sublabel 1'
|
|
79
|
+
/>
|
|
80
|
+
<ToggleGroup.Item
|
|
81
|
+
value='option-2'
|
|
82
|
+
label='Option 2'
|
|
83
|
+
sublabel='Sublabel 2'
|
|
84
|
+
icon='User'
|
|
85
|
+
/>
|
|
86
|
+
<ToggleGroup.Item
|
|
87
|
+
value='option-3'
|
|
88
|
+
label='Option 3'
|
|
89
|
+
sublabel='Sublabel 3'
|
|
90
|
+
/>
|
|
91
|
+
</>
|
|
92
|
+
),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const Disabled: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
defaultValue: 'option-1',
|
|
99
|
+
children: (
|
|
100
|
+
<>
|
|
101
|
+
<ToggleGroup.Item
|
|
102
|
+
value='option-1'
|
|
103
|
+
label='Option 1'
|
|
104
|
+
/>
|
|
105
|
+
<ToggleGroup.Item
|
|
106
|
+
value='option-2'
|
|
107
|
+
label='Option 2'
|
|
108
|
+
icon='Calendar'
|
|
109
|
+
disabled
|
|
110
|
+
/>
|
|
111
|
+
<ToggleGroup.Item
|
|
112
|
+
value='option-3'
|
|
113
|
+
label='Option 3'
|
|
114
|
+
sublabel='Sublabel 3'
|
|
115
|
+
disabled
|
|
116
|
+
/>
|
|
117
|
+
</>
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
};
|