@navikt/ds-react 7.30.1 → 7.31.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 (92) hide show
  1. package/cjs/form/checkbox/Checkbox.js +1 -1
  2. package/cjs/form/checkbox/Checkbox.js.map +1 -1
  3. package/cjs/form/combobox/Combobox.js +15 -13
  4. package/cjs/form/combobox/Combobox.js.map +1 -1
  5. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +53 -3
  6. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  7. package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  8. package/cjs/form/combobox/Input/InputController.js +15 -14
  9. package/cjs/form/combobox/Input/InputController.js.map +1 -1
  10. package/cjs/form/radio/Radio.js +1 -1
  11. package/cjs/form/radio/Radio.js.map +1 -1
  12. package/cjs/overlays/floating/Floating.d.ts +11 -0
  13. package/cjs/overlays/floating/Floating.js +32 -8
  14. package/cjs/overlays/floating/Floating.js.map +1 -1
  15. package/cjs/overlays/overlay/hooks/useAnimationsFinished.d.ts +27 -0
  16. package/cjs/overlays/overlay/hooks/useAnimationsFinished.js +138 -0
  17. package/cjs/overlays/overlay/hooks/useAnimationsFinished.js.map +1 -0
  18. package/cjs/overlays/overlay/hooks/useEventCallback.d.ts +6 -0
  19. package/cjs/overlays/overlay/hooks/useEventCallback.js +89 -0
  20. package/cjs/overlays/overlay/hooks/useEventCallback.js.map +1 -0
  21. package/cjs/overlays/overlay/hooks/useLatestRef.d.ts +5 -0
  22. package/cjs/overlays/overlay/hooks/useLatestRef.js +23 -0
  23. package/cjs/overlays/overlay/hooks/useLatestRef.js.map +1 -0
  24. package/cjs/overlays/overlay/hooks/useOpenChangeComplete.d.ts +31 -0
  25. package/cjs/overlays/overlay/hooks/useOpenChangeComplete.js +35 -0
  26. package/cjs/overlays/overlay/hooks/useOpenChangeComplete.js.map +1 -0
  27. package/cjs/overlays/overlay/hooks/useRefWithInit.d.ts +7 -0
  28. package/cjs/overlays/overlay/hooks/useRefWithInit.js +14 -0
  29. package/cjs/overlays/overlay/hooks/useRefWithInit.js.map +1 -0
  30. package/cjs/table/ExpandableRow.d.ts +1 -1
  31. package/cjs/table/ExpandableRow.js +2 -10
  32. package/cjs/table/ExpandableRow.js.map +1 -1
  33. package/cjs/table/Row.d.ts +7 -0
  34. package/cjs/table/Row.js +13 -2
  35. package/cjs/table/Row.js.map +1 -1
  36. package/cjs/table/Table.utils.d.ts +9 -0
  37. package/cjs/table/Table.utils.js +57 -0
  38. package/cjs/table/Table.utils.js.map +1 -0
  39. package/esm/form/checkbox/Checkbox.js +1 -1
  40. package/esm/form/checkbox/Checkbox.js.map +1 -1
  41. package/esm/form/combobox/Combobox.js +15 -13
  42. package/esm/form/combobox/Combobox.js.map +1 -1
  43. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +21 -4
  44. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  45. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  46. package/esm/form/combobox/Input/InputController.js +15 -14
  47. package/esm/form/combobox/Input/InputController.js.map +1 -1
  48. package/esm/form/radio/Radio.js +1 -1
  49. package/esm/form/radio/Radio.js.map +1 -1
  50. package/esm/overlays/floating/Floating.d.ts +11 -0
  51. package/esm/overlays/floating/Floating.js +32 -8
  52. package/esm/overlays/floating/Floating.js.map +1 -1
  53. package/esm/overlays/overlay/hooks/useAnimationsFinished.d.ts +27 -0
  54. package/esm/overlays/overlay/hooks/useAnimationsFinished.js +99 -0
  55. package/esm/overlays/overlay/hooks/useAnimationsFinished.js.map +1 -0
  56. package/esm/overlays/overlay/hooks/useEventCallback.d.ts +6 -0
  57. package/esm/overlays/overlay/hooks/useEventCallback.js +53 -0
  58. package/esm/overlays/overlay/hooks/useEventCallback.js.map +1 -0
  59. package/esm/overlays/overlay/hooks/useLatestRef.d.ts +5 -0
  60. package/esm/overlays/overlay/hooks/useLatestRef.js +20 -0
  61. package/esm/overlays/overlay/hooks/useLatestRef.js.map +1 -0
  62. package/esm/overlays/overlay/hooks/useOpenChangeComplete.d.ts +31 -0
  63. package/esm/overlays/overlay/hooks/useOpenChangeComplete.js +32 -0
  64. package/esm/overlays/overlay/hooks/useOpenChangeComplete.js.map +1 -0
  65. package/esm/overlays/overlay/hooks/useRefWithInit.d.ts +7 -0
  66. package/esm/overlays/overlay/hooks/useRefWithInit.js +12 -0
  67. package/esm/overlays/overlay/hooks/useRefWithInit.js.map +1 -0
  68. package/esm/table/ExpandableRow.d.ts +1 -1
  69. package/esm/table/ExpandableRow.js +2 -10
  70. package/esm/table/ExpandableRow.js.map +1 -1
  71. package/esm/table/Row.d.ts +7 -0
  72. package/esm/table/Row.js +13 -2
  73. package/esm/table/Row.js.map +1 -1
  74. package/esm/table/Table.utils.d.ts +9 -0
  75. package/esm/table/Table.utils.js +55 -0
  76. package/esm/table/Table.utils.js.map +1 -0
  77. package/package.json +3 -3
  78. package/src/form/checkbox/Checkbox.tsx +5 -3
  79. package/src/form/combobox/Combobox.tsx +44 -41
  80. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +29 -4
  81. package/src/form/combobox/FilteredOptions/useVirtualFocus.ts +1 -0
  82. package/src/form/combobox/Input/InputController.tsx +33 -29
  83. package/src/form/radio/Radio.tsx +5 -3
  84. package/src/overlays/floating/Floating.tsx +110 -59
  85. package/src/overlays/overlay/hooks/useAnimationsFinished.ts +117 -0
  86. package/src/overlays/overlay/hooks/useEventCallback.ts +73 -0
  87. package/src/overlays/overlay/hooks/useLatestRef.ts +25 -0
  88. package/src/overlays/overlay/hooks/useOpenChangeComplete.ts +66 -0
  89. package/src/overlays/overlay/hooks/useRefWithInit.ts +25 -0
  90. package/src/table/ExpandableRow.tsx +4 -17
  91. package/src/table/Row.tsx +33 -1
  92. package/src/table/Table.utils.ts +65 -0
@@ -10,6 +10,13 @@ export interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
10
10
  * @default true
11
11
  */
12
12
  shadeOnHover?: boolean;
13
+ /**
14
+ * Click handler for row. This differs from onClick by not being called
15
+ * when clicking on interactive elements within the row (buttons, links, inputs etc).
16
+ *
17
+ * **Warning:** This will not be accessible by keyboard! Provide an alternative way to select the row, e.g. a checkbox or a button.
18
+ */
19
+ onRowClick?: (event: React.MouseEvent<HTMLTableRowElement>) => void;
13
20
  }
14
21
  export type RowType = React.ForwardRefExoticComponent<RowProps & React.RefAttributes<HTMLTableRowElement>>;
15
22
  export declare const Row: RowType;
package/esm/table/Row.js CHANGED
@@ -11,13 +11,24 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  };
12
12
  import React, { forwardRef } from "react";
13
13
  import { useRenameCSS } from "../theme/Theme.js";
14
+ import { composeEventHandlers } from "../util/composeEventHandlers.js";
15
+ import { isElementInteractiveTarget } from "./Table.utils.js";
14
16
  export const Row = forwardRef((_a, ref) => {
15
- var { className, selected = false, shadeOnHover = true } = _a, rest = __rest(_a, ["className", "selected", "shadeOnHover"]);
17
+ var { className, selected = false, shadeOnHover = true, onClick, onRowClick } = _a, rest = __rest(_a, ["className", "selected", "shadeOnHover", "onClick", "onRowClick"]);
16
18
  const { cn } = useRenameCSS();
19
+ const handleRowClick = (event) => {
20
+ if (!onRowClick) {
21
+ return;
22
+ }
23
+ if (isElementInteractiveTarget(event.target)) {
24
+ return;
25
+ }
26
+ onRowClick(event);
27
+ };
17
28
  return (React.createElement("tr", Object.assign({}, rest, { ref: ref, className: cn("navds-table__row", className, {
18
29
  "navds-table__row--selected": selected,
19
30
  "navds-table__row--shade-on-hover": shadeOnHover,
20
- }) })));
31
+ }), onClick: composeEventHandlers(onClick, handleRowClick), "data-interactive": !!onRowClick })));
21
32
  });
22
33
  export default Row;
23
34
  //# sourceMappingURL=Row.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Row.js","sourceRoot":"","sources":["../../src/table/Row.tsx"],"names":[],"mappings":";;;;;;;;;;;AAAA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAmB9C,MAAM,CAAC,MAAM,GAAG,GAAY,UAAU,CACpC,CAAC,EAA6D,EAAE,GAAG,EAAE,EAAE;QAAtE,EAAE,SAAS,EAAE,QAAQ,GAAG,KAAK,EAAE,YAAY,GAAG,IAAI,OAAW,EAAN,IAAI,cAA3D,yCAA6D,CAAF;IAC1D,MAAM,EAAE,EAAE,EAAE,GAAG,YAAY,EAAE,CAAC;IAC9B,OAAO,CACL,4CACM,IAAI,IACR,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,EAAE,CAAC,kBAAkB,EAAE,SAAS,EAAE;YAC3C,4BAA4B,EAAE,QAAQ;YACtC,kCAAkC,EAAE,YAAY;SACjD,CAAC,IACF,CACH,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,eAAe,GAAG,CAAC"}
1
+ {"version":3,"file":"Row.js","sourceRoot":"","sources":["../../src/table/Row.tsx"],"names":[],"mappings":";;;;;;;;;;;AAAA,OAAO,KAAK,EAAE,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AA0B3D,MAAM,CAAC,MAAM,GAAG,GAAY,UAAU,CACpC,CACE,EAOC,EACD,GAAG,EACH,EAAE;QATF,EACE,SAAS,EACT,QAAQ,GAAG,KAAK,EAChB,YAAY,GAAG,IAAI,EACnB,OAAO,EACP,UAAU,OAEX,EADI,IAAI,cANT,kEAOC,CADQ;IAIT,MAAM,EAAE,EAAE,EAAE,GAAG,YAAY,EAAE,CAAC;IAE9B,MAAM,cAAc,GAAG,CAAC,KAA4C,EAAE,EAAE;QACtE,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,IAAI,0BAA0B,CAAC,KAAK,CAAC,MAAqB,CAAC,EAAE,CAAC;YAC5D,OAAO;QACT,CAAC;QACD,UAAU,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC,CAAC;IAEF,OAAO,CACL,4CACM,IAAI,IACR,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,EAAE,CAAC,kBAAkB,EAAE,SAAS,EAAE;YAC3C,4BAA4B,EAAE,QAAQ;YACtC,kCAAkC,EAAE,YAAY;SACjD,CAAC,EACF,OAAO,EAAE,oBAAoB,CAAC,OAAO,EAAE,cAAc,CAAC,sBACpC,CAAC,CAAC,UAAU,IAC9B,CACH,CAAC;AACJ,CAAC,CACF,CAAC;AAEF,eAAe,GAAG,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Walks up from the event target until TR/TH (row / header) or root.
3
+ * Returns true if any ancestor is inherently interactive, explicitly focusable,
4
+ * or has an interactive ARIA role.
5
+ * Used to decide whether a row click should be treated as a row selection
6
+ * or ignored because the user interacted with an embedded control.
7
+ */
8
+ declare function isElementInteractiveTarget(element: HTMLElement | null): boolean;
9
+ export { isElementInteractiveTarget };
@@ -0,0 +1,55 @@
1
+ const INTERACTIVE_TAGS = new Set([
2
+ "BUTTON",
3
+ "A",
4
+ "INPUT",
5
+ "SELECT",
6
+ "TEXTAREA",
7
+ "DETAILS",
8
+ "SUMMARY",
9
+ "LABEL",
10
+ ]);
11
+ const INTERACTIVE_ROLES = new Set([
12
+ "button",
13
+ "link",
14
+ "checkbox",
15
+ "radio",
16
+ "switch",
17
+ "menuitem",
18
+ "option",
19
+ "tab",
20
+ "textbox",
21
+ "combobox",
22
+ "spinbutton",
23
+ "slider",
24
+ ]);
25
+ /**
26
+ * Walks up from the event target until TR/TH (row / header) or root.
27
+ * Returns true if any ancestor is inherently interactive, explicitly focusable,
28
+ * or has an interactive ARIA role.
29
+ * Used to decide whether a row click should be treated as a row selection
30
+ * or ignored because the user interacted with an embedded control.
31
+ */
32
+ function isElementInteractiveTarget(element) {
33
+ for (let node = element; node && node.nodeName !== "TR" && node.nodeName !== "TH"; node = node.parentElement) {
34
+ const tag = node.nodeName;
35
+ /* Native interactive tag */
36
+ if (INTERACTIVE_TAGS.has(tag)) {
37
+ return true;
38
+ }
39
+ /* Explicit interactive role */
40
+ const role = node.getAttribute("role");
41
+ if (role && INTERACTIVE_ROLES.has(role)) {
42
+ return true;
43
+ }
44
+ /* Focusable via tabindex (exclude -1) */
45
+ if (node.hasAttribute("tabindex")) {
46
+ const ti = node.getAttribute("tabindex");
47
+ if (ti !== "-1") {
48
+ return true;
49
+ }
50
+ }
51
+ }
52
+ return false;
53
+ }
54
+ export { isElementInteractiveTarget };
55
+ //# sourceMappingURL=Table.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Table.utils.js","sourceRoot":"","sources":["../../src/table/Table.utils.ts"],"names":[],"mappings":"AAAA,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,QAAQ;IACR,GAAG;IACH,OAAO;IACP,QAAQ;IACR,UAAU;IACV,SAAS;IACT,SAAS;IACT,OAAO;CACR,CAAC,CAAC;AAEH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,QAAQ;IACR,MAAM;IACN,UAAU;IACV,OAAO;IACP,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,KAAK;IACL,SAAS;IACT,UAAU;IACV,YAAY;IACZ,QAAQ;CACT,CAAC,CAAC;AAEH;;;;;;GAMG;AACH,SAAS,0BAA0B,CAAC,OAA2B;IAC7D,KACE,IAAI,IAAI,GAAuB,OAAO,EACtC,IAAI,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,EACxD,IAAI,GAAG,IAAI,CAAC,aAAa,EACzB,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC;QAE1B,4BAA4B;QAC5B,IAAI,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,+BAA+B;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,IAAI,IAAI,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,yCAAyC;QACzC,IAAI,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,MAAM,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;YACzC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBAChB,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,OAAO,EAAE,0BAA0B,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navikt/ds-react",
3
- "version": "7.30.1",
3
+ "version": "7.31.0",
4
4
  "description": "React components from the Norwegian Labour and Welfare Administration.",
5
5
  "author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
6
6
  "license": "MIT",
@@ -650,8 +650,8 @@
650
650
  "dependencies": {
651
651
  "@floating-ui/react": "0.27.8",
652
652
  "@floating-ui/react-dom": "^2.0.9",
653
- "@navikt/aksel-icons": "^7.30.1",
654
- "@navikt/ds-tokens": "^7.30.1",
653
+ "@navikt/aksel-icons": "^7.31.0",
654
+ "@navikt/ds-tokens": "^7.31.0",
655
655
  "clsx": "^2.1.0",
656
656
  "date-fns": "^4.0.0",
657
657
  "react-day-picker": "9.7.0"
@@ -42,9 +42,11 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
42
42
  "readOnly",
43
43
  ])}
44
44
  {...omit(inputProps, ["aria-invalid", "aria-describedby"])}
45
- aria-describedby={cl(inputProps["aria-describedby"], {
46
- [descriptionId]: props.description,
47
- })}
45
+ aria-describedby={
46
+ cl(inputProps["aria-describedby"], {
47
+ [descriptionId]: props.description,
48
+ }) || undefined
49
+ }
48
50
  type="checkbox"
49
51
  className={cn("navds-checkbox__input")}
50
52
  ref={(el) => {
@@ -1,4 +1,5 @@
1
1
  import React, { forwardRef } from "react";
2
+ import { Floating } from "../../overlays/floating/Floating";
2
3
  import { useRenameCSS } from "../../theme/Theme";
3
4
  import { BodyShort, ErrorMessage, Label } from "../../typography";
4
5
  import { ReadOnlyIconWithTitle } from "../ReadOnlyIcon";
@@ -34,52 +35,54 @@ export const Combobox = forwardRef<
34
35
  } = useInputContext();
35
36
 
36
37
  return (
37
- <ComboboxWrapper
38
- className={className}
39
- hasError={hasError}
40
- inputProps={inputProps}
41
- inputSize={size}
42
- toggleIsListOpen={toggleIsListOpen}
43
- >
44
- <Label
45
- htmlFor={inputProps.id}
46
- size={size}
47
- className={cn("navds-form-field__label", {
48
- "navds-sr-only": hideLabel,
49
- })}
38
+ <Floating>
39
+ <ComboboxWrapper
40
+ className={className}
41
+ hasError={hasError}
42
+ inputProps={inputProps}
43
+ inputSize={size}
44
+ toggleIsListOpen={toggleIsListOpen}
50
45
  >
51
- {readOnly && <ReadOnlyIconWithTitle />}
52
- {label}
53
- </Label>
54
- {!!description && (
55
- <BodyShort
56
- as="div"
57
- className={cn("navds-form-field__description", {
46
+ <Label
47
+ htmlFor={inputProps.id}
48
+ size={size}
49
+ className={cn("navds-form-field__label", {
58
50
  "navds-sr-only": hideLabel,
59
51
  })}
60
- id={inputDescriptionId}
61
- size={size}
62
52
  >
63
- {description}
64
- </BodyShort>
65
- )}
66
- <div className={cn("navds-combobox__wrapper")}>
67
- <InputController ref={ref} {...rest} />
68
- <FilteredOptions />
69
- </div>
70
- <div
71
- className={cn("navds-form-field__error")}
72
- id={errorId}
73
- aria-relevant="additions removals"
74
- aria-live="polite"
75
- >
76
- {showErrorMsg && (
77
- <ErrorMessage size={size} showIcon>
78
- {error}
79
- </ErrorMessage>
53
+ {readOnly && <ReadOnlyIconWithTitle />}
54
+ {label}
55
+ </Label>
56
+ {!!description && (
57
+ <BodyShort
58
+ as="div"
59
+ className={cn("navds-form-field__description", {
60
+ "navds-sr-only": hideLabel,
61
+ })}
62
+ id={inputDescriptionId}
63
+ size={size}
64
+ >
65
+ {description}
66
+ </BodyShort>
80
67
  )}
81
- </div>
82
- </ComboboxWrapper>
68
+ <div className={cn("navds-combobox__wrapper")}>
69
+ <InputController ref={ref} {...rest} />
70
+ <FilteredOptions />
71
+ </div>
72
+ <div
73
+ className={cn("navds-form-field__error")}
74
+ id={errorId}
75
+ aria-relevant="additions removals"
76
+ aria-live="polite"
77
+ >
78
+ {showErrorMsg && (
79
+ <ErrorMessage size={size} showIcon>
80
+ {error}
81
+ </ErrorMessage>
82
+ )}
83
+ </div>
84
+ </ComboboxWrapper>
85
+ </Floating>
83
86
  );
84
87
  });
85
88
 
@@ -1,5 +1,7 @@
1
- import React from "react";
2
- import { useRenameCSS } from "../../../theme/Theme";
1
+ import React, { useState } from "react";
2
+ import { Floating } from "../../../overlays/floating/Floating";
3
+ import { useRenameCSS, useThemeInternal } from "../../../theme/Theme";
4
+ import { useClientLayoutEffect } from "../../../util";
3
5
  import { useInputContext } from "../Input/Input.context";
4
6
  import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
5
7
  import AddNewOption from "./AddNewOption";
@@ -12,6 +14,7 @@ import { useFilteredOptionsContext } from "./filteredOptionsContext";
12
14
 
13
15
  const FilteredOptions = () => {
14
16
  const { cn } = useRenameCSS();
17
+ const themeContext = useThemeInternal(false);
15
18
  const {
16
19
  inputProps: { id },
17
20
  } = useInputContext();
@@ -25,6 +28,16 @@ const FilteredOptions = () => {
25
28
  isMouseLastUsedInputDevice,
26
29
  isValueNew,
27
30
  } = useFilteredOptionsContext();
31
+ const [localOpen, setLocalOpen] = useState(isListOpen);
32
+
33
+ /**
34
+ * This is a dirty hack to make the positioning-logic in Floating base the "flip" on the static 290px max-height,
35
+ * instead of the dynamic one based on available space. Without this, the list won't flip to top when there's
36
+ * not enough space below the input.
37
+ */
38
+ useClientLayoutEffect(() => {
39
+ queueMicrotask(() => setLocalOpen(isListOpen));
40
+ }, [isListOpen]);
28
41
 
29
42
  const { maxSelected, isMultiSelect } = useSelectedOptionsContext();
30
43
 
@@ -37,14 +50,26 @@ const FilteredOptions = () => {
37
50
  (allowNewValues && isValueNew && !maxSelected.isLimitReached) || // Render add new option
38
51
  filteredOptions.length > 0; // Render filtered options
39
52
 
53
+ const height = themeContext?.isDarkside ? "316px" : "290px";
54
+
40
55
  return (
41
- <div
56
+ <Floating.Content
42
57
  className={cn("navds-combobox__list", {
43
58
  "navds-combobox__list--closed": !isListOpen,
44
59
  "navds-combobox__list--with-hover": isMouseLastUsedInputDevice,
45
60
  })}
46
61
  id={filteredOptionsUtil.getFilteredOptionsId(id)}
47
62
  tabIndex={-1}
63
+ sideOffset={8}
64
+ side="bottom"
65
+ fallbackPlacements={["top"]}
66
+ enabled={isListOpen}
67
+ style={{
68
+ maxHeight: localOpen
69
+ ? `min(${height}, var(--ac-floating-available-height))`
70
+ : `${height}`,
71
+ }}
72
+ autoUpdateWhileMounted={false}
48
73
  >
49
74
  {shouldRenderNonSelectables && (
50
75
  <div
@@ -74,7 +99,7 @@ const FilteredOptions = () => {
74
99
  ))}
75
100
  </ul>
76
101
  )}
77
- </div>
102
+ </Floating.Content>
78
103
  );
79
104
  };
80
105
 
@@ -64,6 +64,7 @@ const useVirtualFocus = (
64
64
 
65
65
  const moveFocusDown = () => {
66
66
  const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
67
+
67
68
  if (!activeElement) {
68
69
  setActiveAndScrollToElement(elementsAbleToReceiveFocus[0]);
69
70
  return;
@@ -1,5 +1,8 @@
1
+ /* eslint-disable jsx-a11y/click-events-have-key-events */
2
+
1
3
  /* eslint-disable jsx-a11y/no-static-element-interactions */
2
4
  import React, { forwardRef } from "react";
5
+ import { Floating } from "../../../overlays/floating/Floating";
3
6
  import { useRenameCSS } from "../../../theme/Theme";
4
7
  import { useMergeRefs } from "../../../util/hooks";
5
8
  import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
@@ -54,42 +57,43 @@ export const InputController = forwardRef<
54
57
  const mergedInputRef = useMergeRefs(inputRef, ref);
55
58
 
56
59
  return (
57
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events
58
- <div
59
- className={cn("navds-combobox__wrapper-inner navds-text-field__input", {
60
- "navds-combobox__wrapper-inner--virtually-unfocused":
61
- activeDecendantId !== undefined,
62
- })}
63
- onClick={() => {
64
- if (inputProps.disabled || readOnly) {
65
- return;
66
- }
60
+ <Floating.Anchor asChild>
61
+ <div
62
+ className={cn("navds-combobox__wrapper-inner navds-text-field__input", {
63
+ "navds-combobox__wrapper-inner--virtually-unfocused":
64
+ activeDecendantId !== undefined,
65
+ })}
66
+ onClick={() => {
67
+ if (inputProps.disabled || readOnly) {
68
+ return;
69
+ }
67
70
 
68
- toggleIsListOpen(true);
69
- focusInput();
70
- }}
71
- >
72
- {!shouldShowSelectedOptions ? (
73
- <Input
74
- id={inputProps.id}
75
- ref={mergedInputRef}
76
- inputClassName={inputClassName}
77
- readOnly={readOnly}
78
- {...rest}
79
- />
80
- ) : (
81
- <SelectedOptions selectedOptions={selectedOptions} size={size}>
71
+ toggleIsListOpen(true);
72
+ focusInput();
73
+ }}
74
+ >
75
+ {!shouldShowSelectedOptions ? (
82
76
  <Input
83
77
  id={inputProps.id}
84
78
  ref={mergedInputRef}
85
79
  inputClassName={inputClassName}
86
- shouldShowSelectedOptions={shouldShowSelectedOptions}
87
80
  readOnly={readOnly}
88
81
  {...rest}
89
82
  />
90
- </SelectedOptions>
91
- )}
92
- {toggleListButton && <ToggleListButton ref={toggleOpenButtonRef} />}
93
- </div>
83
+ ) : (
84
+ <SelectedOptions selectedOptions={selectedOptions} size={size}>
85
+ <Input
86
+ id={inputProps.id}
87
+ ref={mergedInputRef}
88
+ inputClassName={inputClassName}
89
+ shouldShowSelectedOptions={shouldShowSelectedOptions}
90
+ readOnly={readOnly}
91
+ {...rest}
92
+ />
93
+ </SelectedOptions>
94
+ )}
95
+ {toggleListButton && <ToggleListButton ref={toggleOpenButtonRef} />}
96
+ </div>
97
+ </Floating.Anchor>
94
98
  );
95
99
  });
@@ -25,9 +25,11 @@ export const Radio = forwardRef<HTMLInputElement, RadioProps>((props, ref) => {
25
25
  <input
26
26
  {...omit(props, ["children", "size", "description", "readOnly"])}
27
27
  {...omit(inputProps, ["aria-invalid", "aria-describedby"])}
28
- aria-describedby={cl(inputProps["aria-describedby"], {
29
- [descriptionId]: props.description,
30
- })}
28
+ aria-describedby={
29
+ cl(inputProps["aria-describedby"], {
30
+ [descriptionId]: props.description,
31
+ }) || undefined
32
+ }
31
33
  className={cn("navds-radio__input")}
32
34
  ref={ref}
33
35
  />
@@ -18,6 +18,7 @@ import React, {
18
18
  useRef,
19
19
  useState,
20
20
  } from "react";
21
+ import { useModalContext } from "../../modal/Modal.context";
21
22
  import { Slot } from "../../slot/Slot";
22
23
  import { createContext } from "../../util/create-context";
23
24
  import {
@@ -26,6 +27,7 @@ import {
26
27
  useMergeRefs,
27
28
  } from "../../util/hooks";
28
29
  import { AsChildProps } from "../../util/types";
30
+ import { useOpenChangeComplete } from "../overlay/hooks/useOpenChangeComplete";
29
31
  import {
30
32
  type Align,
31
33
  type Measurable,
@@ -188,7 +190,17 @@ interface FloatingContentProps extends HTMLAttributes<HTMLDivElement> {
188
190
  collisionPadding?: number | Partial<Record<Side, number>>;
189
191
  hideWhenDetached?: boolean;
190
192
  updatePositionStrategy?: "optimized" | "always";
193
+ fallbackPlacements?: FlipOptions["fallbackPlacements"];
191
194
  onPlaced?: () => void;
195
+ /**
196
+ * @default true
197
+ */
198
+ enabled?: boolean;
199
+ /**
200
+ * Only use this option if your floating element is conditionally rendered, not hidden with CSS.
201
+ * @default true
202
+ */
203
+ autoUpdateWhileMounted?: boolean;
192
204
  arrow?: {
193
205
  className?: string;
194
206
  padding?: number;
@@ -212,11 +224,15 @@ const FloatingContent = forwardRef<HTMLDivElement, FloatingContentProps>(
212
224
  updatePositionStrategy = "optimized",
213
225
  onPlaced,
214
226
  arrow: _arrow,
227
+ fallbackPlacements,
228
+ enabled = true,
229
+ autoUpdateWhileMounted = true,
215
230
  ...contentProps
216
231
  }: FloatingContentProps,
217
232
  forwardedRef,
218
233
  ) => {
219
234
  const context = useFloatingContext();
235
+ const modalContext = useModalContext(false);
220
236
 
221
237
  const arrowDefaults = {
222
238
  padding: 5,
@@ -256,68 +272,103 @@ const FloatingContent = forwardRef<HTMLDivElement, FloatingContentProps>(
256
272
  altBoundary: hasExplicitBoundaries,
257
273
  /* https://floating-ui.com/docs/flip#fallbackaxissidedirection */
258
274
  fallbackAxisSideDirection: "end",
275
+ fallbackPlacements,
259
276
  };
260
277
 
261
- const { refs, floatingStyles, placement, isPositioned, middlewareData } =
262
- useFloating({
263
- // default to `fixed` strategy so users don't have to pick and we also avoid focus scroll issues
264
- strategy: "fixed",
265
- placement: desiredPlacement,
266
- whileElementsMounted: (...args) => {
267
- const cleanup = autoUpdate(...args, {
268
- animationFrame: updatePositionStrategy === "always",
269
- });
270
- return cleanup;
271
- },
272
- elements: {
273
- reference: context.anchor,
274
- },
275
- middleware: [
276
- offset({
277
- mainAxis: sideOffset + arrowHeight,
278
- alignmentAxis: alignOffset,
278
+ const {
279
+ refs,
280
+ floatingStyles,
281
+ placement,
282
+ isPositioned,
283
+ middlewareData,
284
+ elements: floatingElements,
285
+ update,
286
+ } = useFloating({
287
+ open: enabled,
288
+ // default to `fixed` strategy so users don't have to pick and we also avoid focus scroll issues
289
+ strategy: "fixed",
290
+ placement: desiredPlacement,
291
+ whileElementsMounted: autoUpdateWhileMounted
292
+ ? (...args) => {
293
+ const cleanup = autoUpdate(...args, {
294
+ animationFrame: updatePositionStrategy === "always",
295
+ });
296
+ return cleanup;
297
+ }
298
+ : undefined,
299
+ elements: {
300
+ reference: context.anchor,
301
+ },
302
+ middleware: [
303
+ offset({
304
+ mainAxis: sideOffset + arrowHeight,
305
+ alignmentAxis: alignOffset,
306
+ }),
307
+ avoidCollisions &&
308
+ shift({
309
+ mainAxis: true,
310
+ crossAxis: false,
311
+ limiter: limitShift(),
279
312
  }),
280
- avoidCollisions &&
281
- shift({
282
- mainAxis: true,
283
- crossAxis: false,
284
- limiter: limitShift(),
285
- }),
286
- avoidCollisions && flip({ ...detectOverflowOptions }),
287
- size({
288
- ...detectOverflowOptions,
289
- apply: ({ elements, rects, availableWidth, availableHeight }) => {
290
- const { width: anchorWidth, height: anchorHeight } =
291
- rects.reference;
292
- const contentStyle = elements.floating.style;
293
- /**
294
- * Allows styling and animations based on the available space.
295
- */
296
- contentStyle.setProperty(
297
- "--ac-floating-available-width",
298
- `${availableWidth}px`,
299
- );
300
- contentStyle.setProperty(
301
- "--ac-floating-available-height",
302
- `${availableHeight}px`,
303
- );
304
- contentStyle.setProperty(
305
- "--ac-floating-anchor-width",
306
- `${anchorWidth}px`,
307
- );
308
- contentStyle.setProperty(
309
- "--ac-floating-anchor-height",
310
- `${anchorHeight}px`,
311
- );
312
- },
313
- }),
314
- arrow &&
315
- floatingArrow({ element: arrow, padding: arrowDefaults.padding }),
316
- transformOrigin({ arrowWidth, arrowHeight }),
317
- hideWhenDetached &&
318
- hide({ strategy: "referenceHidden", ...detectOverflowOptions }),
319
- ],
320
- });
313
+ avoidCollisions && flip({ ...detectOverflowOptions }),
314
+ size({
315
+ ...detectOverflowOptions,
316
+ apply: ({ elements, rects, availableWidth, availableHeight }) => {
317
+ const { width: anchorWidth, height: anchorHeight } =
318
+ rects.reference;
319
+ const contentStyle = elements.floating.style;
320
+ /**
321
+ * Allows styling and animations based on the available space.
322
+ */
323
+ contentStyle.setProperty(
324
+ "--ac-floating-available-width",
325
+ `${availableWidth}px`,
326
+ );
327
+ contentStyle.setProperty(
328
+ "--ac-floating-available-height",
329
+ `${availableHeight}px`,
330
+ );
331
+ contentStyle.setProperty(
332
+ "--ac-floating-anchor-width",
333
+ `${anchorWidth}px`,
334
+ );
335
+ contentStyle.setProperty(
336
+ "--ac-floating-anchor-height",
337
+ `${anchorHeight}px`,
338
+ );
339
+ },
340
+ }),
341
+ arrow &&
342
+ floatingArrow({ element: arrow, padding: arrowDefaults.padding }),
343
+ transformOrigin({ arrowWidth, arrowHeight }),
344
+ hideWhenDetached &&
345
+ hide({ strategy: "referenceHidden", ...detectOverflowOptions }),
346
+ ],
347
+ });
348
+
349
+ useEffect(() => {
350
+ if (autoUpdateWhileMounted || !enabled) {
351
+ return;
352
+ }
353
+ if (floatingElements.reference && floatingElements.floating) {
354
+ const cleanup = autoUpdate(
355
+ floatingElements.reference,
356
+ floatingElements.floating,
357
+ update,
358
+ );
359
+
360
+ return () => {
361
+ cleanup();
362
+ };
363
+ }
364
+ }, [autoUpdateWhileMounted, enabled, floatingElements, update]);
365
+
366
+ useOpenChangeComplete({
367
+ enabled: !!modalContext?.ref,
368
+ open: enabled,
369
+ ref: modalContext?.ref,
370
+ onComplete: update,
371
+ });
321
372
 
322
373
  const [placedSide, placedAlign] = getSideAndAlignFromPlacement(placement);
323
374