uikit-react-public 0.29.3 → 0.29.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 +14 -3
- package/dist/components/Overlay/Overlay.stories.d.ts +31 -2
- package/dist/components/Overlay/__tests__/Overlay.test.d.ts +1 -0
- package/dist/components/Overlay/index.d.ts +1 -1
- package/dist/components/Select/Select.stories.d.ts +12 -0
- package/dist/components/Select/Select.types.d.ts +7 -0
- package/dist/components/Select/subcomponents/CustomSelect.d.ts +1 -1
- package/dist/components/Select/subcomponents/Panel.d.ts +8 -2
- package/dist/index.js +4783 -4662
- package/lib/components/Label/Label.tsx +6 -1
- package/lib/components/Label/__tests__/__snapshots__/Label.test.tsx.snap +2 -1
- package/lib/components/Overlay/Overlay.tsx +67 -21
- package/lib/components/Overlay/__tests__/Overlay.test.tsx +81 -0
- package/lib/components/Overlay/index.ts +1 -1
- package/lib/components/Select/Select.stories.tsx +7 -0
- package/lib/components/Select/Select.tsx +7 -0
- package/lib/components/Select/Select.types.ts +8 -0
- package/lib/components/Select/__tests__/Select.test.tsx +181 -1
- package/lib/components/Select/subcomponents/CustomSelect.tsx +109 -27
- package/lib/components/Select/subcomponents/Panel.tsx +40 -10
- package/package.json +1 -1
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import {
|
|
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
|
-
<
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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 = ({
|
|
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
|
-
|
|
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:
|
|
24
|
-
max-height:
|
|
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
|
|
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
|