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.
Files changed (33) hide show
  1. package/dist/components/Overlay/Overlay.d.ts +2 -2
  2. package/dist/components/Overlay/Overlay.stories.d.ts +13 -1
  3. package/dist/components/Select/index.d.ts +1 -1
  4. package/dist/components/Select/subcomponents/Panel.d.ts +1 -2
  5. package/dist/components/ToggleGroup/ToggleGroup.d.ts +15 -0
  6. package/dist/components/ToggleGroup/ToggleGroup.stories.d.ts +18 -0
  7. package/dist/components/ToggleGroup/ToggleGroupContext.d.ts +8 -0
  8. package/dist/components/ToggleGroup/__tests__/ToggleGroup.test.d.ts +1 -0
  9. package/dist/components/ToggleGroup/index.d.ts +4 -0
  10. package/dist/components/ToggleGroup/subcomponents/Item.d.ts +11 -0
  11. package/dist/components/ToggleGroup/subcomponents/Item.stories.d.ts +331 -0
  12. package/dist/components/ToggleGroup/subcomponents/__tests__/Item.test.d.ts +1 -0
  13. package/dist/components/index.d.ts +2 -0
  14. package/dist/index.js +8351 -8121
  15. package/lib/components/Icon/svgImports.ts +4 -2
  16. package/lib/components/Overlay/Overlay.tsx +9 -3
  17. package/lib/components/Select/Select.mdx +1 -0
  18. package/lib/components/Select/__tests__/Select.test.tsx +137 -4
  19. package/lib/components/Select/index.ts +5 -1
  20. package/lib/components/Select/subcomponents/CustomSelect.tsx +42 -43
  21. package/lib/components/Select/subcomponents/Panel.tsx +2 -8
  22. package/lib/components/ToggleGroup/ToggleGroup.mdx +31 -0
  23. package/lib/components/ToggleGroup/ToggleGroup.stories.tsx +120 -0
  24. package/lib/components/ToggleGroup/ToggleGroup.tsx +144 -0
  25. package/lib/components/ToggleGroup/ToggleGroupContext.tsx +25 -0
  26. package/lib/components/ToggleGroup/__tests__/ToggleGroup.test.tsx +468 -0
  27. package/lib/components/ToggleGroup/index.ts +4 -0
  28. package/lib/components/ToggleGroup/subcomponents/Item.stories.tsx +63 -0
  29. package/lib/components/ToggleGroup/subcomponents/Item.tsx +233 -0
  30. package/lib/components/ToggleGroup/subcomponents/__tests__/Item.test.tsx +195 -0
  31. package/lib/components/index.ts +3 -0
  32. package/package.json +3 -3
  33. 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
- const iconSvgs: Record<string, IconComponent> = {
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: 'absolute',
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('opens filter input on keyboard focus', async () => {
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 filterInput = await result.findByRole('searchbox');
762
- expect(filterInput).toHaveFocus();
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();
@@ -1,2 +1,6 @@
1
1
  export { default } from './Select';
2
- export type { SelectProps, OptionData } from './Select.types';
2
+ export type {
3
+ SelectProps,
4
+ NonClearableSelectProps,
5
+ OptionData,
6
+ } from './Select.types';
@@ -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
- const openedViaFocusRef = useRef(false);
97
- const skipOpenOnFocusRef = useRef(false);
98
-
99
- const setRefs = (node: HTMLDivElement | null) => {
100
- internalRef.current = node;
101
- if (typeof ref === 'function') {
102
- ref(node);
103
- } else if (
104
- ref &&
105
- 'current' in
106
- (ref as React.RefObject<HTMLDivElement | HTMLSelectElement | null>)
107
- ) {
108
- (
109
- ref as React.RefObject<HTMLDivElement | HTMLSelectElement | null>
110
- ).current = node;
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: false,
736
- crossAxis: true,
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: ${maxHeightValue};
38
+ max-height: min(400px, var(--ucl-overlay-available-height, 400px));
45
39
  overflow-y: auto;
46
40
  overflow-x: hidden;
47
- box-sizing: content-box;
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
+ };