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.
@@ -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
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uikit-react-public",
3
3
  "private": false,
4
4
  "license": "UNLICENSED",
5
- "version": "0.29.3",
5
+ "version": "0.29.6",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",