uikit-react-public 0.49.5 → 0.50.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 (30) hide show
  1. package/dist/components/Filters/Filter.d.ts +20 -0
  2. package/dist/components/Filters/FilterPanel.d.ts +16 -0
  3. package/dist/components/Filters/Filters.context.d.ts +8 -0
  4. package/dist/components/Filters/Filters.d.ts +30 -0
  5. package/dist/components/Filters/Filters.stories.d.ts +17 -0
  6. package/dist/components/Filters/QuickFilter.d.ts +8 -0
  7. package/dist/components/Filters/__tests__/Filters.test.d.ts +1 -0
  8. package/dist/components/Filters/index.d.ts +6 -0
  9. package/dist/components/Overlay/Overlay.d.ts +3 -2
  10. package/dist/components/Overlay/Overlay.stories.d.ts +14 -1
  11. package/dist/components/Select/subcomponents/Panel.d.ts +1 -2
  12. package/dist/components/index.d.ts +2 -0
  13. package/dist/index.js +6833 -6236
  14. package/lib/components/Filters/Filter.tsx +327 -0
  15. package/lib/components/Filters/FilterPanel.tsx +152 -0
  16. package/lib/components/Filters/Filters.context.ts +12 -0
  17. package/lib/components/Filters/Filters.mdx +75 -0
  18. package/lib/components/Filters/Filters.stories.tsx +354 -0
  19. package/lib/components/Filters/Filters.tsx +316 -0
  20. package/lib/components/Filters/QuickFilter.tsx +89 -0
  21. package/lib/components/Filters/__tests__/Filters.test.tsx +230 -0
  22. package/lib/components/Filters/index.ts +6 -0
  23. package/lib/components/Overlay/Overlay.tsx +18 -7
  24. package/lib/components/Overlay/__tests__/Overlay.test.tsx +39 -0
  25. package/lib/components/Select/Select.mdx +1 -0
  26. package/lib/components/Select/__tests__/Select.test.tsx +137 -4
  27. package/lib/components/Select/subcomponents/CustomSelect.tsx +42 -43
  28. package/lib/components/Select/subcomponents/Panel.tsx +2 -8
  29. package/lib/components/index.ts +8 -0
  30. package/package.json +1 -1
@@ -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};
@@ -37,6 +37,14 @@ export type { BlanketProps } from './Blanket';
37
37
  export { default as Overlay } from './Overlay';
38
38
  export type { OverlayProps } from './Overlay';
39
39
 
40
+ export { default as Filters, Filter, QuickFilter } from './Filters';
41
+ export type {
42
+ FiltersProps,
43
+ FiltersSubComponents,
44
+ FilterProps,
45
+ QuickFilterProps,
46
+ } from './Filters';
47
+
40
48
  export { default as Snackbar } from './Snackbar';
41
49
  export { Snackbars } from './Snackbar';
42
50
  export type {
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.49.5",
5
+ "version": "0.50.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",