uikit-react-public 0.29.5 → 0.30.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.
Files changed (148) hide show
  1. package/dist/components/Accordion/Accordion.stories.d.ts +1 -1
  2. package/dist/components/Alert/Alert.stories.d.ts +1 -1
  3. package/dist/components/AppHeader/AppHeader.stories.d.ts +1 -1
  4. package/dist/components/AppMenu/AppMenu.stories.d.ts +1 -1
  5. package/dist/components/Avatar/Avatar.stories.d.ts +1 -1
  6. package/dist/components/Badge/Badge.stories.d.ts +1 -1
  7. package/dist/components/BaseCheckbox/BaseCheckbox.stories.d.ts +1 -1
  8. package/dist/components/Blanket/Blanket.stories.d.ts +1 -1
  9. package/dist/components/Breadcrumbs/Breadcrumbs.stories.d.ts +1 -1
  10. package/dist/components/Button/Button.stories.d.ts +1 -1
  11. package/dist/components/Calendar/Calendar.stories.d.ts +1 -1
  12. package/dist/components/Calendar/subcomponents/Day.stories.d.ts +1 -1
  13. package/dist/components/Checkbox/Checkbox.stories.d.ts +1 -1
  14. package/dist/components/Chip/Chip.stories.d.ts +1 -1
  15. package/dist/components/Datepicker/Datepicker.stories.d.ts +1 -1
  16. package/dist/components/Dialog/Dialog.stories.d.ts +1 -1
  17. package/dist/components/Divider/Divider.stories.d.ts +1 -1
  18. package/dist/components/Dropdown/Dropdown.stories.d.ts +1 -1
  19. package/dist/components/FeedbackDialog/FeedbackDialog.stories.d.ts +1 -1
  20. package/dist/components/Field/Field.stories.d.ts +1 -1
  21. package/dist/components/FileInput/FileInput.stories.d.ts +1 -1
  22. package/dist/components/Footer/Footer.stories.d.ts +1 -1
  23. package/dist/components/Header/Header.stories.d.ts +1 -1
  24. package/dist/components/Heading/Heading.stories.d.ts +1 -1
  25. package/dist/components/Icon/Icon.stories.d.ts +1 -1
  26. package/dist/components/IconButton/IconButton.stories.d.ts +1 -1
  27. package/dist/components/Input/Input.stories.d.ts +1 -1
  28. package/dist/components/Label/Label.stories.d.ts +1 -1
  29. package/dist/components/Layout/Layout.stories.d.ts +1 -1
  30. package/dist/components/Link/Link.stories.d.ts +1 -1
  31. package/dist/components/Main/Main.stories.d.ts +1 -1
  32. package/dist/components/Modal/Modal.stories.d.ts +1 -1
  33. package/dist/components/NativeDatepicker/NativeDatepicker.stories.d.ts +1 -1
  34. package/dist/components/Overlay/Overlay.d.ts +14 -3
  35. package/dist/components/Overlay/Overlay.stories.d.ts +33 -4
  36. package/dist/components/Overlay/__tests__/Overlay.test.d.ts +1 -0
  37. package/dist/components/Overlay/index.d.ts +1 -1
  38. package/dist/components/Pagination/Pagination.stories.d.ts +1 -1
  39. package/dist/components/Paragraph/Paragraph.stories.d.ts +1 -1
  40. package/dist/components/Radio/Radio.stories.d.ts +1 -1
  41. package/dist/components/Search/Search.stories.d.ts +1 -1
  42. package/dist/components/Select/Select.stories.d.ts +13 -1
  43. package/dist/components/Select/Select.types.d.ts +7 -0
  44. package/dist/components/Select/subcomponents/CustomSelect.d.ts +1 -1
  45. package/dist/components/Select/subcomponents/Panel.d.ts +8 -2
  46. package/dist/components/Snackbar/Snackbar.stories.d.ts +1 -1
  47. package/dist/components/Spinner/Spinner.stories.d.ts +1 -1
  48. package/dist/components/StandaloneLink/StandaloneLink.stories.d.ts +1 -1
  49. package/dist/components/Table/Table.stories.d.ts +1 -1
  50. package/dist/components/Table/subcomponents/Cell/Cell.stories.d.ts +2 -2
  51. package/dist/components/Table/subcomponents/HeadCell/HeadCell.stories.d.ts +2 -2
  52. package/dist/components/Tabs/Tab.d.ts +11 -5
  53. package/dist/components/Tabs/TabContext.d.ts +14 -8
  54. package/dist/components/Tabs/Tabs.d.ts +25 -8
  55. package/dist/components/Tabs/Tabs.stories.d.ts +5 -9
  56. package/dist/components/Tabs/TabsList.d.ts +9 -0
  57. package/dist/components/Tabs/TabsPanel.d.ts +10 -0
  58. package/dist/components/Tabs/index.d.ts +2 -1
  59. package/dist/components/Textarea/Textarea.stories.d.ts +1 -1
  60. package/dist/components/Timepicker/Timepicker.stories.d.ts +1 -1
  61. package/dist/components/Toggle/Toggle.stories.d.ts +1 -1
  62. package/dist/components/Tooltip/Tooltip.stories.d.ts +1 -1
  63. package/dist/components/UclLogo/UclLogo.stories.d.ts +1 -1
  64. package/dist/components/WeekPicker/WeekPicker.stories.d.ts +1 -1
  65. package/dist/components/index.d.ts +1 -1
  66. package/dist/index.js +5094 -4723
  67. package/lib/Welcome.mdx +1 -1
  68. package/lib/components/Accordion/Accordion.stories.tsx +1 -1
  69. package/lib/components/Alert/Alert.mdx +1 -1
  70. package/lib/components/Alert/Alert.stories.tsx +1 -1
  71. package/lib/components/AppHeader/AppHeader.stories.tsx +1 -1
  72. package/lib/components/AppMenu/AppMenu.stories.tsx +1 -1
  73. package/lib/components/Avatar/Avatar.mdx +1 -1
  74. package/lib/components/Avatar/Avatar.stories.tsx +1 -1
  75. package/lib/components/Badge/Badge.stories.tsx +1 -1
  76. package/lib/components/BaseCheckbox/BaseCheckbox.stories.tsx +2 -2
  77. package/lib/components/Blanket/Blanket.stories.tsx +1 -1
  78. package/lib/components/Breadcrumbs/Breadcrumbs.stories.tsx +1 -1
  79. package/lib/components/Button/Button.mdx +1 -1
  80. package/lib/components/Button/Button.stories.tsx +2 -2
  81. package/lib/components/Calendar/Calendar.stories.tsx +2 -2
  82. package/lib/components/Calendar/subcomponents/Day.stories.tsx +1 -1
  83. package/lib/components/Checkbox/Checkbox.stories.tsx +2 -2
  84. package/lib/components/Chip/Chip.stories.tsx +2 -2
  85. package/lib/components/Datepicker/Datepicker.stories.tsx +2 -2
  86. package/lib/components/Dialog/Dialog.stories.tsx +1 -1
  87. package/lib/components/Divider/Divider.stories.tsx +1 -1
  88. package/lib/components/Dropdown/Dropdown.stories.tsx +1 -1
  89. package/lib/components/FeedbackDialog/FeedbackDialog.stories.tsx +1 -1
  90. package/lib/components/Field/Field.stories.tsx +2 -2
  91. package/lib/components/FileInput/FileInput.stories.tsx +1 -1
  92. package/lib/components/Footer/Footer.stories.tsx +1 -1
  93. package/lib/components/Header/Header.mdx +1 -1
  94. package/lib/components/Header/Header.stories.tsx +1 -1
  95. package/lib/components/Heading/Documentation.mdx +1 -1
  96. package/lib/components/Heading/Heading.stories.tsx +1 -1
  97. package/lib/components/Icon/Icon.stories.tsx +1 -1
  98. package/lib/components/IconButton/IconButton.stories.tsx +1 -1
  99. package/lib/components/Input/Documentation.mdx +1 -1
  100. package/lib/components/Input/Input.stories.tsx +1 -1
  101. package/lib/components/Label/Label.stories.tsx +1 -1
  102. package/lib/components/Layout/Layout.stories.tsx +1 -1
  103. package/lib/components/Link/Link.stories.tsx +1 -1
  104. package/lib/components/Main/Main.stories.tsx +1 -1
  105. package/lib/components/Modal/Modal.stories.tsx +1 -1
  106. package/lib/components/NativeDatepicker/NativeDatepicker.stories.tsx +2 -2
  107. package/lib/components/Overlay/Overlay.stories.tsx +1 -1
  108. package/lib/components/Overlay/Overlay.tsx +64 -21
  109. package/lib/components/Overlay/__tests__/Overlay.test.tsx +81 -0
  110. package/lib/components/Overlay/index.ts +1 -1
  111. package/lib/components/Pagination/Pagination.stories.tsx +1 -1
  112. package/lib/components/Paragraph/Paragraph.stories.tsx +1 -1
  113. package/lib/components/Radio/Radio.stories.tsx +2 -2
  114. package/lib/components/Search/Search.stories.tsx +1 -1
  115. package/lib/components/Select/Select.mdx +1 -1
  116. package/lib/components/Select/Select.stories.tsx +9 -2
  117. package/lib/components/Select/Select.tsx +7 -0
  118. package/lib/components/Select/Select.types.ts +9 -2
  119. package/lib/components/Select/__tests__/Select.test.tsx +181 -1
  120. package/lib/components/Select/subcomponents/CustomSelect.tsx +109 -27
  121. package/lib/components/Select/subcomponents/Panel.tsx +40 -10
  122. package/lib/components/Snackbar/Snackbar.stories.tsx +1 -1
  123. package/lib/components/Spinner/Spinner.stories.tsx +1 -1
  124. package/lib/components/StandaloneLink/StandaloneLink.stories.tsx +1 -1
  125. package/lib/components/Table/Table.stories.tsx +1 -1
  126. package/lib/components/Table/subcomponents/Cell/Cell.stories.tsx +2 -2
  127. package/lib/components/Table/subcomponents/HeadCell/HeadCell.stories.tsx +1 -1
  128. package/lib/components/Tabs/Tab.tsx +209 -36
  129. package/lib/components/Tabs/TabContext.tsx +20 -7
  130. package/lib/components/Tabs/Tabs.stories.tsx +87 -68
  131. package/lib/components/Tabs/Tabs.tsx +129 -37
  132. package/lib/components/Tabs/TabsList.tsx +134 -0
  133. package/lib/components/Tabs/TabsPanel.tsx +55 -0
  134. package/lib/components/Tabs/__tests__/Tabs.test.tsx +173 -105
  135. package/lib/components/Tabs/index.ts +8 -1
  136. package/lib/components/Textarea/Textarea.stories.tsx +1 -1
  137. package/lib/components/Timepicker/Timepicker.stories.tsx +1 -1
  138. package/lib/components/Toggle/Documentation.mdx +1 -1
  139. package/lib/components/Toggle/Toggle.stories.tsx +1 -1
  140. package/lib/components/Tooltip/Tooltip.stories.tsx +1 -1
  141. package/lib/components/UclLogo/UclLogo.stories.tsx +1 -1
  142. package/lib/components/WeekPicker/WeekPicker.stories.tsx +2 -2
  143. package/lib/components/common/Common.mdx +1 -1
  144. package/lib/components/index.ts +7 -1
  145. package/lib/theme/Icons.mdx +1 -1
  146. package/lib/theme/Typography.mdx +1 -1
  147. package/package.json +8 -11
  148. package/lib/components/Tabs/__tests__/__snapshots__/Tabs.test.tsx.snap +0 -185
@@ -14,6 +14,7 @@ const Select = (<T extends string | number = string>(
14
14
  filterable,
15
15
  clearable,
16
16
  onValueChange,
17
+ dropdownWidth,
17
18
  ref,
18
19
  ...rest
19
20
  } = props;
@@ -25,6 +26,11 @@ const Select = (<T extends string | number = string>(
25
26
  if (clearable) {
26
27
  console.warn('clearable is not supported on native Select; ignoring.');
27
28
  }
29
+ if (dropdownWidth) {
30
+ console.warn(
31
+ 'dropdownWidth is not supported on native Select; ignoring.'
32
+ );
33
+ }
28
34
  const { value, ...nativeRest } = rest;
29
35
  const {
30
36
  value: nativeAttrValue,
@@ -51,6 +57,7 @@ const Select = (<T extends string | number = string>(
51
57
  onValueChange={onValueChange}
52
58
  filterable={filterable}
53
59
  clearable={clearable}
60
+ dropdownWidth={dropdownWidth}
54
61
  ref={ref as React.Ref<HTMLDivElement>}
55
62
  {...rest}
56
63
  />
@@ -37,6 +37,8 @@ export type FilterInputProps = Omit<
37
37
  | 'aria-label'
38
38
  >;
39
39
 
40
+ export type SelectDropdownWidth = 'content' | 'match-select';
41
+
40
42
  type SelectBaseProps<T = string | number> = Omit<
41
43
  React.HTMLAttributes<HTMLElement>,
42
44
  'onChange'
@@ -91,6 +93,12 @@ type SelectBaseProps<T = string | number> = Omit<
91
93
  * Allow long option labels to wrap instead of truncating
92
94
  */
93
95
  lineBreak?: boolean;
96
+ /**
97
+ * Controls the width of the custom options panel.
98
+ * - `content` (default): panel is at least the Select width and can grow to fit option labels.
99
+ * - `match-select`: panel width is capped to the Select width.
100
+ */
101
+ dropdownWidth?: SelectDropdownWidth;
94
102
  /**
95
103
  * Custom className for the root element
96
104
  */
@@ -141,8 +149,7 @@ export type ClearableSelectProps<T = string | number> = SelectBaseProps<T> & {
141
149
  * Public props for <Select>, used by both custom and native render paths.
142
150
  */
143
151
  export type SelectProps<T = string | number> =
144
- | NonClearableSelectProps<T>
145
- | ClearableSelectProps<T>;
152
+ NonClearableSelectProps<T> | ClearableSelectProps<T>;
146
153
 
147
154
  export type InternalSelectProps<T = string | number> = SelectBaseProps<T> & {
148
155
  value?: T | null;
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, test, vi, beforeAll } from 'vitest';
2
2
  import { useState } from 'react';
3
- import { createEvent, fireEvent, render } from '@testing-library/react';
3
+ import {
4
+ createEvent,
5
+ fireEvent,
6
+ render,
7
+ waitFor,
8
+ } from '@testing-library/react';
4
9
  import userEvent from '@testing-library/user-event';
5
10
  import Select from '../Select';
6
11
  import { ThemeContextProvider } from '../../../theme/useTheme';
@@ -91,6 +96,181 @@ describe('Select', () => {
91
96
  expect(options[2].textContent).toBe(defaultOptions[2].label);
92
97
  });
93
98
 
99
+ test('renders the open panel inside an overlay', async () => {
100
+ const user = userEvent.setup();
101
+ const renderResult = render(
102
+ <ThemeContextProvider>
103
+ <Select
104
+ options={defaultOptions}
105
+ value=''
106
+ onValueChange={() => {}}
107
+ />
108
+ </ThemeContextProvider>
109
+ );
110
+
111
+ await user.click(renderResult.getByRole('combobox'));
112
+
113
+ const panel = renderResult.getByTestId('ucl-uikit-select__panel');
114
+ const overlay = panel.parentElement;
115
+
116
+ expect(overlay).toHaveClass('ucl-overlay');
117
+ await waitFor(() => {
118
+ expect(overlay).toHaveStyle({
119
+ position: 'absolute',
120
+ });
121
+ });
122
+ });
123
+
124
+ test('dropdownWidth defaults to content width', async () => {
125
+ const user = userEvent.setup();
126
+ const renderResult = render(
127
+ <ThemeContextProvider>
128
+ <Select
129
+ options={defaultOptions}
130
+ value=''
131
+ onValueChange={() => {}}
132
+ />
133
+ </ThemeContextProvider>
134
+ );
135
+
136
+ await user.click(renderResult.getByRole('combobox'));
137
+
138
+ expect(renderResult.getByRole('listbox')).toHaveStyle({
139
+ width: 'fit-content',
140
+ });
141
+ });
142
+
143
+ test('dropdownWidth=match-select caps panel width to the select width', async () => {
144
+ const user = userEvent.setup();
145
+ const getBoundingClientRectSpy = vi.spyOn(
146
+ Element.prototype,
147
+ 'getBoundingClientRect'
148
+ );
149
+
150
+ getBoundingClientRectSpy.mockImplementation(() => ({
151
+ x: 0,
152
+ y: 0,
153
+ top: 0,
154
+ left: 0,
155
+ right: 240,
156
+ bottom: 48,
157
+ width: 240,
158
+ height: 48,
159
+ toJSON: () => {},
160
+ }));
161
+
162
+ const renderResult = render(
163
+ <ThemeContextProvider>
164
+ <Select
165
+ dropdownWidth='match-select'
166
+ options={defaultOptions}
167
+ value=''
168
+ onValueChange={() => {}}
169
+ />
170
+ </ThemeContextProvider>
171
+ );
172
+
173
+ await user.click(renderResult.getByRole('combobox'));
174
+
175
+ await waitFor(() => {
176
+ expect(renderResult.getByRole('listbox')).toHaveStyle({
177
+ width: '240px',
178
+ maxWidth: '240px',
179
+ });
180
+ });
181
+
182
+ getBoundingClientRectSpy.mockRestore();
183
+ });
184
+
185
+ test('open dropdown closes when clicking outside', async () => {
186
+ const user = userEvent.setup();
187
+ const renderResult = render(
188
+ <ThemeContextProvider>
189
+ <div>
190
+ <Select
191
+ options={defaultOptions}
192
+ value=''
193
+ onValueChange={() => {}}
194
+ />
195
+ <button type='button'>Outside</button>
196
+ </div>
197
+ </ThemeContextProvider>
198
+ );
199
+
200
+ await user.click(renderResult.getByRole('combobox'));
201
+ expect(renderResult.getByRole('listbox')).toBeInTheDocument();
202
+
203
+ await user.click(renderResult.getByRole('button', { name: 'Outside' }));
204
+
205
+ expect(renderResult.queryByRole('listbox')).not.toBeInTheDocument();
206
+ });
207
+
208
+ test('panel width is capped to dialog body width inside a dialog', async () => {
209
+ const user = userEvent.setup();
210
+ const getBoundingClientRectSpy = vi.spyOn(
211
+ Element.prototype,
212
+ 'getBoundingClientRect'
213
+ );
214
+
215
+ getBoundingClientRectSpy.mockImplementation(function (this: Element) {
216
+ if (this.getAttribute('role') === 'document') {
217
+ return {
218
+ x: 0,
219
+ y: 0,
220
+ top: 0,
221
+ left: 0,
222
+ right: 431,
223
+ bottom: 200,
224
+ width: 431,
225
+ height: 200,
226
+ toJSON: () => {},
227
+ };
228
+ }
229
+
230
+ return {
231
+ x: 0,
232
+ y: 0,
233
+ top: 0,
234
+ left: 0,
235
+ right: 400,
236
+ bottom: 48,
237
+ width: 400,
238
+ height: 48,
239
+ toJSON: () => {},
240
+ };
241
+ });
242
+
243
+ const result = render(
244
+ <ThemeContextProvider>
245
+ <dialog open>
246
+ <div role='document'>
247
+ <Select
248
+ options={[
249
+ {
250
+ label:
251
+ 'A very long option label that should be truncated in the dialog',
252
+ value: 'long',
253
+ },
254
+ ]}
255
+ value=''
256
+ onValueChange={() => {}}
257
+ />
258
+ </div>
259
+ </dialog>
260
+ </ThemeContextProvider>
261
+ );
262
+
263
+ await user.click(result.getByRole('combobox'));
264
+
265
+ await waitFor(() => {
266
+ expect(result.getByRole('listbox')).toHaveStyle({
267
+ maxWidth: 'min(431px, calc(100vw - (32px * 2)))',
268
+ });
269
+ });
270
+
271
+ getBoundingClientRectSpy.mockRestore();
272
+ });
273
+
94
274
  test('Cannot be used when disabled', async () => {
95
275
  const user = userEvent.setup();
96
276
  const renderResult = render(
@@ -1,7 +1,16 @@
1
- import { useState, useRef, useEffect, useMemo, useId } from 'react';
1
+ import {
2
+ useState,
3
+ useRef,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useId,
8
+ } from 'react';
2
9
  import { css, cx } from '@emotion/css';
3
10
  import { VisibleField, Panel, CustomOption, FilterInput } from '.';
4
11
  import { useTheme } from '../../../theme';
12
+ import Overlay from '../../Overlay';
13
+ import type { OverlaySize } from '../../Overlay';
5
14
  import type { InternalSelectProps } from '../Select.types';
6
15
 
7
16
  const NAME = 'ucl-uikit-select';
@@ -32,6 +41,7 @@ const CustomSelect = <T extends string | number>({
32
41
  clearable = false,
33
42
  placeholder,
34
43
  lineBreak = false,
44
+ dropdownWidth = 'content',
35
45
  filterInputProps,
36
46
  width,
37
47
  testId = NAME,
@@ -107,11 +117,17 @@ const CustomSelect = <T extends string | number>({
107
117
  const [selectedOptionIndex, setSelectedOptionIndex] = useState<number | null>(
108
118
  null
109
119
  );
120
+ const [panelMaxWidth, setPanelMaxWidth] = useState<number | null>(null);
121
+ const [panelReferenceWidth, setPanelReferenceWidth] = useState<number | null>(
122
+ null
123
+ );
124
+ const [overlaySize, setOverlaySize] = useState<OverlaySize | null>(null);
110
125
  const filterInputRef = useRef<HTMLInputElement | null>(null);
111
126
  const clearButtonRef = useRef<HTMLButtonElement | null>(null);
112
127
  const reactId = useId();
113
128
  const idBase = props.id ?? `${testId}-${reactId.replace(/[:]/g, '')}`;
114
129
  const listboxId = `${idBase}-listbox`;
130
+ const overlayViewportPadding = parseFloat(String(theme.margin.m32));
115
131
 
116
132
  // Returns a list of indexes of options that are currently visible based on the filter text
117
133
  const visibleOptionIndexes = useMemo(() => {
@@ -259,6 +275,35 @@ const CustomSelect = <T extends string | number>({
259
275
  }
260
276
  }, [filterable, isOpen]);
261
277
 
278
+ useLayoutEffect(() => {
279
+ if (!isOpen) {
280
+ setPanelMaxWidth(null);
281
+ setPanelReferenceWidth(null);
282
+ setOverlaySize(null);
283
+ return;
284
+ }
285
+
286
+ const updatePanelMaxWidth = () => {
287
+ const referenceElement = effectiveRef.current;
288
+ const dialogElement = referenceElement?.closest('dialog');
289
+ const dialogBodyElement =
290
+ dialogElement?.querySelector<HTMLElement>('[role="document"]');
291
+ const dialogBodyRect = dialogBodyElement?.getBoundingClientRect();
292
+ const referenceRect = referenceElement?.getBoundingClientRect();
293
+
294
+ setPanelReferenceWidth(referenceRect?.width ?? null);
295
+ setPanelMaxWidth(
296
+ dialogBodyRect && referenceRect
297
+ ? Math.max(0, dialogBodyRect.right - referenceRect.left)
298
+ : null
299
+ );
300
+ };
301
+
302
+ updatePanelMaxWidth();
303
+ window.addEventListener('resize', updatePanelMaxWidth);
304
+ return () => window.removeEventListener('resize', updatePanelMaxWidth);
305
+ }, [effectiveRef, isOpen]);
306
+
262
307
  const togglePanel = () => {
263
308
  if (!disabled) setIsOpen((prev) => !prev);
264
309
  };
@@ -274,6 +319,21 @@ const CustomSelect = <T extends string | number>({
274
319
  }
275
320
  };
276
321
 
322
+ const handleOverlaySizeChange = (nextSize: OverlaySize) => {
323
+ setOverlaySize((prevSize) => {
324
+ if (
325
+ prevSize &&
326
+ prevSize.referenceWidth === nextSize.referenceWidth &&
327
+ prevSize.availableWidth === nextSize.availableWidth &&
328
+ prevSize.availableHeight === nextSize.availableHeight
329
+ ) {
330
+ return prevSize;
331
+ }
332
+
333
+ return nextSize;
334
+ });
335
+ };
336
+
277
337
  const handleClick = (event: React.MouseEvent) => {
278
338
  if (disabled) return;
279
339
  if (openedViaFocusRef.current) {
@@ -567,33 +627,55 @@ const CustomSelect = <T extends string | number>({
567
627
  )}
568
628
  </VisibleField>
569
629
  {isOpen && (
570
- <Panel
571
- className={panelClassName}
572
- id={listboxId}
573
- role='listbox'
630
+ <Overlay
631
+ reference={effectiveRef}
632
+ placement='bottom-start'
633
+ flip={{ padding: overlayViewportPadding }}
634
+ shift={{
635
+ padding: overlayViewportPadding,
636
+ mainAxis: false,
637
+ crossAxis: true,
638
+ }}
639
+ size={{
640
+ matchReferenceWidth: true,
641
+ padding: overlayViewportPadding,
642
+ }}
643
+ onSizeChange={handleOverlaySizeChange}
574
644
  >
575
- {visibleOptions.map((option, index) => (
576
- <CustomOption<T>
577
- key={`${String(option.value)}-${visibleOptionIndexes[index]}`}
578
- id={`${idBase}-option-${visibleOptionIndexes[index]}`}
579
- value={option.value}
580
- optionIndex={visibleOptionIndexes[index]}
581
- isSelected={highlightedVisibleIndex === index}
582
- onSelect={handleSelect}
583
- lineBreak={lineBreak}
584
- role='option'
585
- aria-selected={highlightedVisibleIndex === index}
586
- aria-posinset={index + 1}
587
- aria-setsize={visibleOptions.length}
588
- {...option.optionProps}
589
- >
590
- {option.label}
591
- </CustomOption>
592
- ))}
593
- {visibleOptions.length === 0 && (
594
- <div className={noOptionsStyle}>No options</div>
595
- )}
596
- </Panel>
645
+ <Panel
646
+ className={panelClassName}
647
+ dropdownWidth={dropdownWidth}
648
+ referenceWidth={
649
+ overlaySize?.referenceWidth ?? panelReferenceWidth ?? undefined
650
+ }
651
+ availableHeight={overlaySize?.availableHeight}
652
+ maxWidth={panelMaxWidth ?? undefined}
653
+ id={listboxId}
654
+ role='listbox'
655
+ >
656
+ {visibleOptions.map((option, index) => (
657
+ <CustomOption<T>
658
+ key={`${String(option.value)}-${visibleOptionIndexes[index]}`}
659
+ id={`${idBase}-option-${visibleOptionIndexes[index]}`}
660
+ value={option.value}
661
+ optionIndex={visibleOptionIndexes[index]}
662
+ isSelected={highlightedVisibleIndex === index}
663
+ onSelect={handleSelect}
664
+ lineBreak={lineBreak}
665
+ role='option'
666
+ aria-selected={highlightedVisibleIndex === index}
667
+ aria-posinset={index + 1}
668
+ aria-setsize={visibleOptions.length}
669
+ {...option.optionProps}
670
+ >
671
+ {option.label}
672
+ </CustomOption>
673
+ ))}
674
+ {visibleOptions.length === 0 && (
675
+ <div className={noOptionsStyle}>No options</div>
676
+ )}
677
+ </Panel>
678
+ </Overlay>
597
679
  )}
598
680
  </div>
599
681
  );
@@ -1,11 +1,24 @@
1
1
  import { css, cx } from '@emotion/css';
2
2
  import { useTheme } from '../../../theme';
3
+ import type { SelectDropdownWidth } from '../Select.types';
3
4
 
4
5
  const NAME = 'ucl-uikit-select__panel';
5
6
 
6
- type PanelProps = React.ComponentPropsWithoutRef<'div'>;
7
+ type PanelProps = React.ComponentPropsWithoutRef<'div'> & {
8
+ dropdownWidth?: SelectDropdownWidth;
9
+ referenceWidth?: number;
10
+ availableHeight?: number;
11
+ maxWidth?: number;
12
+ };
7
13
 
8
- const Panel = ({ className, ...props }: PanelProps) => {
14
+ const Panel = ({
15
+ className,
16
+ dropdownWidth = 'content',
17
+ referenceWidth,
18
+ availableHeight,
19
+ maxWidth,
20
+ ...props
21
+ }: PanelProps) => {
9
22
  const [theme] = useTheme();
10
23
 
11
24
  const handleClick = (event: React.MouseEvent) => {
@@ -13,15 +26,22 @@ const Panel = ({ className, ...props }: PanelProps) => {
13
26
  event.stopPropagation();
14
27
  };
15
28
 
29
+ const referenceWidthValue =
30
+ typeof referenceWidth === 'number' ? `${referenceWidth}px` : '100%';
31
+ const maxWidthValue =
32
+ typeof maxWidth === 'number'
33
+ ? `min(${maxWidth}px, calc(100vw - (${theme.margin.m32} * 2)))`
34
+ : `calc(100vw - (${theme.margin.m32} * 2))`;
35
+ const maxHeightValue =
36
+ typeof availableHeight === 'number'
37
+ ? `min(400px, ${availableHeight}px)`
38
+ : '400px';
39
+
16
40
  const baseStyle = css`
17
- position: absolute;
18
- top: 46px;
19
- left: -1px; // -1px to align with the border of the field
20
- z-index: 10; // Required: panel must be 'above' subsquent DOM elements
21
- min-width: 100%;
41
+ min-width: ${referenceWidthValue};
22
42
  width: fit-content;
23
- max-width: calc(100vw - 64px);
24
- max-height: 400px;
43
+ max-width: ${maxWidthValue};
44
+ max-height: ${maxHeightValue};
25
45
  overflow-y: auto;
26
46
  overflow-x: hidden;
27
47
  box-sizing: content-box;
@@ -30,7 +50,17 @@ const Panel = ({ className, ...props }: PanelProps) => {
30
50
  background-color: ${theme.colour.fill.inverse};
31
51
  `;
32
52
 
33
- const style = cx(NAME, baseStyle, className);
53
+ const matchSelectWidthStyle = css`
54
+ width: ${referenceWidthValue};
55
+ max-width: ${referenceWidthValue};
56
+ `;
57
+
58
+ const style = cx(
59
+ NAME,
60
+ baseStyle,
61
+ dropdownWidth === 'match-select' && matchSelectWidthStyle,
62
+ className
63
+ );
34
64
 
35
65
  return (
36
66
  <div
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { css } from '@emotion/css';
3
3
  import Snackbar from './Snackbar';
4
4
 
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
  import { css } from '@emotion/css';
3
3
  import Spinner from './Spinner';
4
4
  import { theme } from '../../theme';
@@ -1,4 +1,4 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
2
 
3
3
  import Link from './StandaloneLink';
4
4
 
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import type { Meta, StoryObj } from '@storybook/react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
  import Table from './Table';
4
4
  import Badge from '../Badge/Badge';
5
5
  import Icon from '../Icon/Icon';
@@ -1,5 +1,5 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { useArgs } from '@storybook/preview-api';
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { useArgs } from 'storybook/preview-api';
3
3
  import { css } from '@emotion/css';
4
4
  import Row from '../Row';
5
5
  import Cell from './Cell';
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import type { Meta, StoryObj } from '@storybook/react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
3
  import HeadCell from './HeadCell';
4
4
  import Head from '../Head';
5
5
  import { SortOrder } from '../../Table.types';