@navikt/ds-react 7.30.0 → 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 (238) hide show
  1. package/cjs/accordion/AccordionContent.js +1 -1
  2. package/cjs/accordion/AccordionContent.js.map +1 -1
  3. package/cjs/accordion/AccordionHeader.js +1 -1
  4. package/cjs/accordion/AccordionHeader.js.map +1 -1
  5. package/cjs/chips/Removable.js +1 -1
  6. package/cjs/chips/Removable.js.map +1 -1
  7. package/cjs/chips/Toggle.js +1 -1
  8. package/cjs/chips/Toggle.js.map +1 -1
  9. package/cjs/copybutton/CopyButton.js +8 -4
  10. package/cjs/copybutton/CopyButton.js.map +1 -1
  11. package/cjs/expansion-card/ExpansionCardContent.js +2 -1
  12. package/cjs/expansion-card/ExpansionCardContent.js.map +1 -1
  13. package/cjs/form/checkbox/Checkbox.js +42 -0
  14. package/cjs/form/checkbox/Checkbox.js.map +1 -1
  15. package/cjs/form/checkbox/useCheckbox.d.ts +2 -2
  16. package/cjs/form/checkbox/useCheckbox.js +5 -5
  17. package/cjs/form/checkbox/useCheckbox.js.map +1 -1
  18. package/cjs/form/combobox/Combobox.js +15 -13
  19. package/cjs/form/combobox/Combobox.js.map +1 -1
  20. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +53 -3
  21. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  22. package/cjs/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  23. package/cjs/form/combobox/Input/InputController.js +15 -14
  24. package/cjs/form/combobox/Input/InputController.js.map +1 -1
  25. package/cjs/form/radio/Radio.js +18 -0
  26. package/cjs/form/radio/Radio.js.map +1 -1
  27. package/cjs/form/radio/useRadio.d.ts +2 -2
  28. package/cjs/form/radio/useRadio.js +5 -5
  29. package/cjs/form/radio/useRadio.js.map +1 -1
  30. package/cjs/form/search/Search.js +1 -1
  31. package/cjs/form/search/Search.js.map +1 -1
  32. package/cjs/guide-panel/GuidePanel.js +3 -3
  33. package/cjs/guide-panel/GuidePanel.js.map +1 -1
  34. package/cjs/help-text/HelpText.js +1 -1
  35. package/cjs/help-text/HelpText.js.map +1 -1
  36. package/cjs/internal-header/InternalHeader.js +1 -1
  37. package/cjs/internal-header/InternalHeader.js.map +1 -1
  38. package/cjs/layout/base/BasePrimitive.js +1 -1
  39. package/cjs/layout/base/BasePrimitive.js.map +1 -1
  40. package/cjs/layout/bleed/Bleed.js +1 -1
  41. package/cjs/layout/bleed/Bleed.js.map +1 -1
  42. package/cjs/layout/box/Box.js +4 -4
  43. package/cjs/layout/box/Box.js.map +1 -1
  44. package/cjs/layout/grid/HGrid.js +1 -1
  45. package/cjs/layout/grid/HGrid.js.map +1 -1
  46. package/cjs/layout/page/Page.js +3 -1
  47. package/cjs/layout/page/Page.js.map +1 -1
  48. package/cjs/layout/stack/Stack.js +1 -1
  49. package/cjs/layout/stack/Stack.js.map +1 -1
  50. package/cjs/link/Link.js +1 -1
  51. package/cjs/link/Link.js.map +1 -1
  52. package/cjs/list/List.js +1 -1
  53. package/cjs/list/List.js.map +1 -1
  54. package/cjs/overlays/action-menu/ActionMenu.js +1 -1
  55. package/cjs/overlays/action-menu/ActionMenu.js.map +1 -1
  56. package/cjs/overlays/floating/Floating.d.ts +11 -0
  57. package/cjs/overlays/floating/Floating.js +32 -8
  58. package/cjs/overlays/floating/Floating.js.map +1 -1
  59. package/cjs/overlays/overlay/hooks/useAnimationsFinished.d.ts +27 -0
  60. package/cjs/overlays/overlay/hooks/useAnimationsFinished.js +138 -0
  61. package/cjs/overlays/overlay/hooks/useAnimationsFinished.js.map +1 -0
  62. package/cjs/overlays/overlay/hooks/useEventCallback.d.ts +6 -0
  63. package/cjs/overlays/overlay/hooks/useEventCallback.js +89 -0
  64. package/cjs/overlays/overlay/hooks/useEventCallback.js.map +1 -0
  65. package/cjs/overlays/overlay/hooks/useLatestRef.d.ts +5 -0
  66. package/cjs/overlays/overlay/hooks/useLatestRef.js +23 -0
  67. package/cjs/overlays/overlay/hooks/useLatestRef.js.map +1 -0
  68. package/cjs/overlays/overlay/hooks/useOpenChangeComplete.d.ts +31 -0
  69. package/cjs/overlays/overlay/hooks/useOpenChangeComplete.js +35 -0
  70. package/cjs/overlays/overlay/hooks/useOpenChangeComplete.js.map +1 -0
  71. package/cjs/overlays/overlay/hooks/useRefWithInit.d.ts +7 -0
  72. package/cjs/overlays/overlay/hooks/useRefWithInit.js +14 -0
  73. package/cjs/overlays/overlay/hooks/useRefWithInit.js.map +1 -0
  74. package/cjs/pagination/PaginationItem.js +1 -1
  75. package/cjs/pagination/PaginationItem.js.map +1 -1
  76. package/cjs/popover/Popover.js +2 -2
  77. package/cjs/popover/Popover.js.map +1 -1
  78. package/cjs/portal/Portal.js +1 -1
  79. package/cjs/portal/Portal.js.map +1 -1
  80. package/cjs/table/ExpandableRow.d.ts +1 -1
  81. package/cjs/table/ExpandableRow.js +2 -10
  82. package/cjs/table/ExpandableRow.js.map +1 -1
  83. package/cjs/table/Row.d.ts +7 -0
  84. package/cjs/table/Row.js +13 -2
  85. package/cjs/table/Row.js.map +1 -1
  86. package/cjs/table/Table.utils.d.ts +9 -0
  87. package/cjs/table/Table.utils.js +57 -0
  88. package/cjs/table/Table.utils.js.map +1 -0
  89. package/cjs/theme/Theme.d.ts +6 -1
  90. package/cjs/theme/Theme.js +10 -2
  91. package/cjs/theme/Theme.js.map +1 -1
  92. package/cjs/timeline/Pin.js +1 -1
  93. package/cjs/timeline/Pin.js.map +1 -1
  94. package/cjs/timeline/period/ClickablePeriod.js +1 -1
  95. package/cjs/timeline/period/ClickablePeriod.js.map +1 -1
  96. package/cjs/toggle-group/ToggleGroup.js +1 -1
  97. package/cjs/toggle-group/ToggleGroup.js.map +1 -1
  98. package/esm/accordion/AccordionContent.js +1 -1
  99. package/esm/accordion/AccordionContent.js.map +1 -1
  100. package/esm/accordion/AccordionHeader.js +1 -1
  101. package/esm/accordion/AccordionHeader.js.map +1 -1
  102. package/esm/chips/Removable.js +1 -1
  103. package/esm/chips/Removable.js.map +1 -1
  104. package/esm/chips/Toggle.js +1 -1
  105. package/esm/chips/Toggle.js.map +1 -1
  106. package/esm/copybutton/CopyButton.js +8 -4
  107. package/esm/copybutton/CopyButton.js.map +1 -1
  108. package/esm/expansion-card/ExpansionCardContent.js +3 -2
  109. package/esm/expansion-card/ExpansionCardContent.js.map +1 -1
  110. package/esm/form/checkbox/Checkbox.js +44 -2
  111. package/esm/form/checkbox/Checkbox.js.map +1 -1
  112. package/esm/form/checkbox/useCheckbox.d.ts +2 -2
  113. package/esm/form/checkbox/useCheckbox.js +5 -5
  114. package/esm/form/checkbox/useCheckbox.js.map +1 -1
  115. package/esm/form/combobox/Combobox.js +15 -13
  116. package/esm/form/combobox/Combobox.js.map +1 -1
  117. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +21 -4
  118. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  119. package/esm/form/combobox/FilteredOptions/useVirtualFocus.js.map +1 -1
  120. package/esm/form/combobox/Input/InputController.js +15 -14
  121. package/esm/form/combobox/Input/InputController.js.map +1 -1
  122. package/esm/form/radio/Radio.js +17 -2
  123. package/esm/form/radio/Radio.js.map +1 -1
  124. package/esm/form/radio/useRadio.d.ts +2 -2
  125. package/esm/form/radio/useRadio.js +5 -5
  126. package/esm/form/radio/useRadio.js.map +1 -1
  127. package/esm/form/search/Search.js +1 -1
  128. package/esm/form/search/Search.js.map +1 -1
  129. package/esm/guide-panel/GuidePanel.js +3 -3
  130. package/esm/guide-panel/GuidePanel.js.map +1 -1
  131. package/esm/help-text/HelpText.js +1 -1
  132. package/esm/help-text/HelpText.js.map +1 -1
  133. package/esm/internal-header/InternalHeader.js +1 -1
  134. package/esm/internal-header/InternalHeader.js.map +1 -1
  135. package/esm/layout/base/BasePrimitive.js +1 -1
  136. package/esm/layout/base/BasePrimitive.js.map +1 -1
  137. package/esm/layout/bleed/Bleed.js +1 -1
  138. package/esm/layout/bleed/Bleed.js.map +1 -1
  139. package/esm/layout/box/Box.js +4 -4
  140. package/esm/layout/box/Box.js.map +1 -1
  141. package/esm/layout/grid/HGrid.js +1 -1
  142. package/esm/layout/grid/HGrid.js.map +1 -1
  143. package/esm/layout/page/Page.js +3 -1
  144. package/esm/layout/page/Page.js.map +1 -1
  145. package/esm/layout/stack/Stack.js +1 -1
  146. package/esm/layout/stack/Stack.js.map +1 -1
  147. package/esm/link/Link.js +1 -1
  148. package/esm/link/Link.js.map +1 -1
  149. package/esm/list/List.js +1 -1
  150. package/esm/list/List.js.map +1 -1
  151. package/esm/overlays/action-menu/ActionMenu.js +1 -1
  152. package/esm/overlays/action-menu/ActionMenu.js.map +1 -1
  153. package/esm/overlays/floating/Floating.d.ts +11 -0
  154. package/esm/overlays/floating/Floating.js +32 -8
  155. package/esm/overlays/floating/Floating.js.map +1 -1
  156. package/esm/overlays/overlay/hooks/useAnimationsFinished.d.ts +27 -0
  157. package/esm/overlays/overlay/hooks/useAnimationsFinished.js +99 -0
  158. package/esm/overlays/overlay/hooks/useAnimationsFinished.js.map +1 -0
  159. package/esm/overlays/overlay/hooks/useEventCallback.d.ts +6 -0
  160. package/esm/overlays/overlay/hooks/useEventCallback.js +53 -0
  161. package/esm/overlays/overlay/hooks/useEventCallback.js.map +1 -0
  162. package/esm/overlays/overlay/hooks/useLatestRef.d.ts +5 -0
  163. package/esm/overlays/overlay/hooks/useLatestRef.js +20 -0
  164. package/esm/overlays/overlay/hooks/useLatestRef.js.map +1 -0
  165. package/esm/overlays/overlay/hooks/useOpenChangeComplete.d.ts +31 -0
  166. package/esm/overlays/overlay/hooks/useOpenChangeComplete.js +32 -0
  167. package/esm/overlays/overlay/hooks/useOpenChangeComplete.js.map +1 -0
  168. package/esm/overlays/overlay/hooks/useRefWithInit.d.ts +7 -0
  169. package/esm/overlays/overlay/hooks/useRefWithInit.js +12 -0
  170. package/esm/overlays/overlay/hooks/useRefWithInit.js.map +1 -0
  171. package/esm/pagination/PaginationItem.js +1 -1
  172. package/esm/pagination/PaginationItem.js.map +1 -1
  173. package/esm/popover/Popover.js +2 -2
  174. package/esm/popover/Popover.js.map +1 -1
  175. package/esm/portal/Portal.js +1 -1
  176. package/esm/portal/Portal.js.map +1 -1
  177. package/esm/table/ExpandableRow.d.ts +1 -1
  178. package/esm/table/ExpandableRow.js +2 -10
  179. package/esm/table/ExpandableRow.js.map +1 -1
  180. package/esm/table/Row.d.ts +7 -0
  181. package/esm/table/Row.js +13 -2
  182. package/esm/table/Row.js.map +1 -1
  183. package/esm/table/Table.utils.d.ts +9 -0
  184. package/esm/table/Table.utils.js +55 -0
  185. package/esm/table/Table.utils.js.map +1 -0
  186. package/esm/theme/Theme.d.ts +6 -1
  187. package/esm/theme/Theme.js +10 -2
  188. package/esm/theme/Theme.js.map +1 -1
  189. package/esm/timeline/Pin.js +1 -1
  190. package/esm/timeline/Pin.js.map +1 -1
  191. package/esm/timeline/period/ClickablePeriod.js +1 -1
  192. package/esm/timeline/period/ClickablePeriod.js.map +1 -1
  193. package/esm/toggle-group/ToggleGroup.js +1 -1
  194. package/esm/toggle-group/ToggleGroup.js.map +1 -1
  195. package/package.json +3 -3
  196. package/src/accordion/AccordionContent.tsx +1 -1
  197. package/src/accordion/AccordionHeader.tsx +1 -1
  198. package/src/chips/Removable.tsx +1 -1
  199. package/src/chips/Toggle.tsx +1 -1
  200. package/src/copybutton/CopyButton.tsx +8 -4
  201. package/src/expansion-card/ExpansionCardContent.tsx +3 -1
  202. package/src/form/checkbox/Checkbox.tsx +93 -2
  203. package/src/form/checkbox/useCheckbox.ts +5 -5
  204. package/src/form/combobox/Combobox.tsx +44 -41
  205. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +29 -4
  206. package/src/form/combobox/FilteredOptions/useVirtualFocus.ts +1 -0
  207. package/src/form/combobox/Input/InputController.tsx +33 -29
  208. package/src/form/radio/Radio.tsx +49 -2
  209. package/src/form/radio/useRadio.ts +5 -5
  210. package/src/form/search/Search.tsx +1 -1
  211. package/src/guide-panel/GuidePanel.tsx +3 -3
  212. package/src/help-text/HelpText.tsx +2 -2
  213. package/src/internal-header/InternalHeader.tsx +1 -1
  214. package/src/layout/base/BasePrimitive.tsx +1 -1
  215. package/src/layout/bleed/Bleed.tsx +1 -1
  216. package/src/layout/box/Box.tsx +5 -4
  217. package/src/layout/grid/HGrid.tsx +1 -1
  218. package/src/layout/page/Page.tsx +5 -1
  219. package/src/layout/stack/Stack.tsx +1 -1
  220. package/src/link/Link.tsx +1 -1
  221. package/src/list/List.tsx +1 -1
  222. package/src/overlays/action-menu/ActionMenu.tsx +1 -1
  223. package/src/overlays/floating/Floating.tsx +110 -59
  224. package/src/overlays/overlay/hooks/useAnimationsFinished.ts +117 -0
  225. package/src/overlays/overlay/hooks/useEventCallback.ts +73 -0
  226. package/src/overlays/overlay/hooks/useLatestRef.ts +25 -0
  227. package/src/overlays/overlay/hooks/useOpenChangeComplete.ts +66 -0
  228. package/src/overlays/overlay/hooks/useRefWithInit.ts +25 -0
  229. package/src/pagination/PaginationItem.tsx +1 -1
  230. package/src/popover/Popover.tsx +2 -2
  231. package/src/portal/Portal.tsx +1 -1
  232. package/src/table/ExpandableRow.tsx +4 -17
  233. package/src/table/Row.tsx +33 -1
  234. package/src/table/Table.utils.ts +65 -0
  235. package/src/theme/Theme.tsx +14 -3
  236. package/src/timeline/Pin.tsx +1 -1
  237. package/src/timeline/period/ClickablePeriod.tsx +1 -1
  238. package/src/toggle-group/ToggleGroup.tsx +1 -1
@@ -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
 
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import React, { useCallback, useEffect } from "react";
4
+ import ReactDOM from "react-dom";
5
+ import { useEventCallback } from "./useEventCallback";
6
+
7
+ /**
8
+ * Returns a stable function that, when invoked, waits for all current CSS/Web Animations
9
+ * on a target element (and its subtree) to finish before executing a callback.
10
+ *
11
+ * Why:
12
+ * - Coordinate logic (unmount, focus restore, measuring) after exit / enter animations.
13
+ * - Avoid `animationend` event bookkeeping across multiple animations / nested elements.
14
+ * - Batch detection using `requestAnimationFrame` so freshly-applied animations are discoverable.
15
+ *
16
+ * Mechanics:
17
+ * 1. Resolves the concrete `HTMLElement` (direct element or from ref) – early no-op if missing.
18
+ * 2. If `getAnimations` is unsupported or animations are globally disabled (`AKSEL_ANIMATIONS_DISABLED`),
19
+ * runs the callback immediately.
20
+ * 3. Schedules a frame so style/animation changes applied this render are committed.
21
+ * 4. Optionally schedules an additional frame (`waitForNextTick=true`) to catch animations that
22
+ * start only after layout (e.g. when an `open` class triggers the animation).
23
+ * 5. Captures all current animations, waits on their `.finished` promises (using `Promise.allSettled`
24
+ * so rejections don't block), then `flushSync` executes the callback (ensures React state updates
25
+ * inside run before the browser paints the next frame).
26
+ * 6. If an `AbortSignal` aborts while waiting, it silently cancels execution.
27
+ *
28
+ * @param elementOrRef HTMLElement or ref to observe.
29
+ * @param waitForNextTick If true, waits an extra frame to ensure enter animations are detectable.
30
+ * @returns Stable function (identity preserved) accepting (fn, abortSignal?).
31
+ */
32
+ export function useAnimationsFinished(
33
+ elementOrRef: React.RefObject<HTMLElement | null> | HTMLElement | null,
34
+ waitForNextTick = false,
35
+ ) {
36
+ const rootFrameRef = React.useRef<number | null>(null);
37
+ const nestedFrameRef = React.useRef<number | null>(null);
38
+
39
+ const cancelScheduled = useCallback(() => {
40
+ for (const ref of [rootFrameRef, nestedFrameRef]) {
41
+ if (ref.current !== null) {
42
+ cancelAnimationFrame(ref.current);
43
+ ref.current = null;
44
+ }
45
+ }
46
+ }, []);
47
+
48
+ /* Unmount cleanup */
49
+ useEffect(() => {
50
+ return () => cancelScheduled();
51
+ }, [cancelScheduled]);
52
+
53
+ return useEventCallback(
54
+ (
55
+ /**
56
+ * A function to execute once all animations have finished.
57
+ */
58
+ fnToExecute: () => void,
59
+ /**
60
+ * An optional [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that
61
+ * can be used to abort `fnToExecute` before all the animations have finished.
62
+ * @default null
63
+ */
64
+ signal: AbortSignal | null = null,
65
+ ) => {
66
+ // Cancel any in-flight scheduling from a previous invocation (next-frame debounce semantics)
67
+ cancelScheduled();
68
+
69
+ if (elementOrRef == null) {
70
+ return;
71
+ }
72
+
73
+ const element =
74
+ "current" in elementOrRef ? elementOrRef.current : elementOrRef;
75
+ if (element == null) {
76
+ return;
77
+ }
78
+
79
+ // Fast path: no Web Animations API support OR animations globally disabled.
80
+ if (
81
+ typeof element.getAnimations !== "function" ||
82
+ // Flag hook for test envs.
83
+ (globalThis as any).AKSEL_ANIMATIONS_DISABLED
84
+ ) {
85
+ fnToExecute();
86
+ return;
87
+ }
88
+
89
+ rootFrameRef.current = requestAnimationFrame(() => {
90
+ function exec() {
91
+ if (!element) {
92
+ return;
93
+ }
94
+ // Collect animations present at this moment; we don't continuously observe
95
+ // if new animations start after these settle, caller should invoke again.
96
+ Promise.allSettled(
97
+ element.getAnimations().map((anim) => anim.finished),
98
+ ).then(() => {
99
+ if (signal?.aborted) return;
100
+ // Ensure any state updates inside the callback are flushed synchronously,
101
+ // guaranteeing that dependent logic observes the current
102
+ // tree rather than a stale in-progress update.
103
+ ReactDOM.flushSync(fnToExecute);
104
+ });
105
+ }
106
+
107
+ // Some animations (e.g. triggered by a class applied this same frame) only
108
+ // become observable after an extra frame; opt-in via flag.
109
+ if (waitForNextTick) {
110
+ nestedFrameRef.current = requestAnimationFrame(exec);
111
+ } else {
112
+ exec();
113
+ }
114
+ });
115
+ },
116
+ );
117
+ }
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Stable event callback: returns a function whose identity never changes but always
5
+ * invokes the latest `callback`. Avoids stale closures without re‑creating handlers.
6
+ *
7
+ * Why not `useCallback`? Its identity depends on a deps array:
8
+ * - omit deps -> stale; include deps -> new function each render.
9
+ * - This hook decouples identity from freshness.
10
+ *
11
+ * How it works: a single stable "trampoline" function delegates to a mutable ref. The current
12
+ * `callback` is promoted from `next` in an insertion/layout phase effect so abandoned concurrent
13
+ * renders cannot leak outdated handlers.
14
+ *
15
+ * Guarantees: stable identity; latest logic executed; no calls from uncommitted renders; dev
16
+ * error if invoked during render; safe when `callback` is undefined (no-op).
17
+ */
18
+ import React, { useLayoutEffect } from "react";
19
+ import { useRefWithInit } from "./useRefWithInit";
20
+
21
+ /* https://github.com/mui/material-ui/issues/41190#issuecomment-2040873379 */
22
+ const useInsertionEffect = (React as any)[
23
+ `useInsertionEffect${Math.random().toFixed(1)}`.slice(0, -3)
24
+ ];
25
+
26
+ const useSafeInsertionEffect =
27
+ // React 17 doesn't have useInsertionEffect.
28
+ useInsertionEffect &&
29
+ // Preact replaces useInsertionEffect with useLayoutEffect and fires too late.
30
+ useInsertionEffect !== useLayoutEffect
31
+ ? useInsertionEffect
32
+ : (fn: any) => fn();
33
+
34
+ type Callback = (...args: any[]) => any;
35
+
36
+ type Stable<T extends Callback> = {
37
+ /** The next value for callback */
38
+ next: T | undefined;
39
+ /** The function to be called by trampoline. This must fail during the initial render phase. */
40
+ callback: T | undefined;
41
+ trampoline: T;
42
+ effect: () => void;
43
+ };
44
+
45
+ /**
46
+ * TODO: Long term, replace `useCallbackRef` with this hook.
47
+ */
48
+ export function useEventCallback<T extends Callback>(
49
+ callback: T | undefined,
50
+ ): T {
51
+ const stable = useRefWithInit(createStableCallback).current as Stable<T>;
52
+ stable.next = callback;
53
+ useSafeInsertionEffect(stable.effect);
54
+ return stable.trampoline;
55
+ }
56
+
57
+ function createStableCallback() {
58
+ const stable: Stable<Callback> = {
59
+ next: undefined,
60
+ callback: assertNotCalled,
61
+ trampoline: (...args: any[]) => stable.callback?.(...args),
62
+ effect: () => {
63
+ stable.callback = stable.next;
64
+ },
65
+ };
66
+ return stable;
67
+ }
68
+
69
+ function assertNotCalled() {
70
+ if (process.env.NODE_ENV !== "production") {
71
+ throw new Error("Aksel: Cannot call an event handler while rendering.");
72
+ }
73
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { useClientLayoutEffect } from "../../../util";
4
+ import { useRefWithInit } from "./useRefWithInit";
5
+
6
+ export function useLatestRef<T>(value: T) {
7
+ const latest = useRefWithInit(createLatestRef, value).current!;
8
+
9
+ latest.next = value;
10
+
11
+ useClientLayoutEffect(latest.effect);
12
+
13
+ return latest;
14
+ }
15
+
16
+ function createLatestRef<T>(value: T) {
17
+ const latest = {
18
+ current: value,
19
+ next: value,
20
+ effect: () => {
21
+ latest.current = latest.next;
22
+ },
23
+ };
24
+ return latest;
25
+ }
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import React, { useEffect } from "react";
4
+ import { useAnimationsFinished } from "./useAnimationsFinished";
5
+ import { useEventCallback } from "./useEventCallback";
6
+ import { useLatestRef } from "./useLatestRef";
7
+
8
+ interface useOpenChangeCompleteParameters {
9
+ /**
10
+ * Enable / disable the effect. Disabled => no animation tracking / callback.
11
+ * @default true
12
+ */
13
+ enabled?: boolean;
14
+ /**
15
+ * Current open state (e.g. popover open). When this flips we wait for any
16
+ * associated CSS/Web animations on `ref` to finish before firing `onComplete`.
17
+ */
18
+ open?: boolean;
19
+ /**
20
+ * Element whose animations/transition we observe. Should be stable while the
21
+ * open/close animation runs (typically the root animated node).
22
+ */
23
+ ref?: React.RefObject<HTMLElement | null>;
24
+ /**
25
+ * Called exactly once per open-change cycle after animations finish OR
26
+ * immediately if animations are disabled / unsupported.
27
+ */
28
+ onComplete: () => void;
29
+ }
30
+
31
+ /**
32
+ * Waits for the element's current Web Animations / CSS transitions to finish after an
33
+ * `open` state change, then invokes `onComplete`. Guards against race conditions by
34
+ * comparing the `open` value captured at scheduling time with the latest `open` via ref;
35
+ * if they differ (state flipped again mid‑animation) the callback is skipped.
36
+ */
37
+ export function useOpenChangeComplete(
38
+ parameters: useOpenChangeCompleteParameters,
39
+ ) {
40
+ const {
41
+ enabled = true,
42
+ open,
43
+ ref = null,
44
+ onComplete: onCompleteParam,
45
+ } = parameters;
46
+
47
+ const openRef = useLatestRef(open);
48
+ const onComplete = useEventCallback(onCompleteParam);
49
+ const runOnceAnimationsFinish = useAnimationsFinished(ref, open);
50
+
51
+ useEffect(() => {
52
+ if (!enabled) {
53
+ return;
54
+ }
55
+
56
+ /*
57
+ * Schedule completion once the *current* set of animations settle. If during
58
+ * that wait `open` toggles again, skip to avoid firing for an outdated cycle.
59
+ */
60
+ runOnceAnimationsFinish(() => {
61
+ if (open === openRef.current) {
62
+ onComplete();
63
+ }
64
+ });
65
+ }, [enabled, open, onComplete, runOnceAnimationsFinish, openRef]);
66
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import React, { useRef } from "react";
4
+
5
+ const UNINITIALIZED = {};
6
+
7
+ /**
8
+ * useRef initialized with a function on mount.
9
+ */
10
+ function useRefWithInit<T>(init: () => T): React.RefObject<T>;
11
+ function useRefWithInit<T, U>(
12
+ init: (arg: U) => T,
13
+ initArg: U,
14
+ ): React.RefObject<T>;
15
+ function useRefWithInit(init: (arg?: unknown) => unknown, initArg?: unknown) {
16
+ const ref = useRef(UNINITIALIZED as any);
17
+
18
+ if (ref.current === UNINITIALIZED) {
19
+ ref.current = init(initArg);
20
+ }
21
+
22
+ return ref;
23
+ }
24
+
25
+ export { useRefWithInit };
@@ -46,7 +46,7 @@ export const Item: PaginationItemType = forwardRef(
46
46
  return (
47
47
  <Button
48
48
  as={Component}
49
- variant={themeContext ? "tertiary-neutral" : "tertiary"}
49
+ variant={themeContext?.isDarkside ? "tertiary-neutral" : "tertiary"}
50
50
  data-color={color}
51
51
  aria-current={selected}
52
52
  data-pressed={selected}
@@ -138,7 +138,7 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
138
138
  placement,
139
139
  open,
140
140
  middleware: [
141
- flOffset(offset ?? (themeContext ? 8 : arrow ? 16 : 4)),
141
+ flOffset(offset ?? (themeContext?.isDarkside ? 8 : arrow ? 16 : 4)),
142
142
  chosenFlip &&
143
143
  flip({ padding: 5, fallbackPlacements: ["bottom", "top"] }),
144
144
  shift({ padding: 12 }),
@@ -191,7 +191,7 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
191
191
  >
192
192
  {children}
193
193
  {/* Hide arrow in new design, prop will be removed in breaking change update */}
194
- {arrow && !themeContext && (
194
+ {arrow && !themeContext?.isDarkside && (
195
195
  <div
196
196
  ref={(node) => {
197
197
  arrowRef.current = node;
@@ -26,7 +26,7 @@ export const Portal = forwardRef<HTMLDivElement, PortalProps>(
26
26
  * Portal can be mounted outside of theme-classNames.
27
27
  * If a theme is present, we want to make sure that theme cascades to portaled element.
28
28
  */
29
- if (themeContext) {
29
+ if (themeContext?.isDarkside) {
30
30
  return root
31
31
  ? ReactDOM.createPortal(
32
32
  <Theme
@@ -8,8 +8,10 @@ import { useI18n } from "../util/i18n/i18n.hooks";
8
8
  import AnimateHeight from "./AnimateHeight";
9
9
  import DataCell from "./DataCell";
10
10
  import Row, { RowProps } from "./Row";
11
+ import { isElementInteractiveTarget } from "./Table.utils";
11
12
 
12
- export interface ExpandableRowProps extends Omit<RowProps, "content"> {
13
+ export interface ExpandableRowProps
14
+ extends Omit<RowProps, "content" | "onRowClick"> {
13
15
  /**
14
16
  * Content of the expanded row
15
17
  */
@@ -96,7 +98,7 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
96
98
  const handleRowClick = (event: React.MouseEvent<HTMLTableRowElement>) => {
97
99
  expandOnRowClick &&
98
100
  !expansionDisabled &&
99
- !isInteractiveTarget(event.target as HTMLElement) &&
101
+ !isElementInteractiveTarget(event.target as HTMLElement) &&
100
102
  expansionHandler(event);
101
103
  };
102
104
 
@@ -167,19 +169,4 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
167
169
  },
168
170
  );
169
171
 
170
- function isInteractiveTarget(elm: HTMLElement) {
171
- if (elm.nodeName === "TD" || elm.nodeName === "TH" || !elm.parentElement) {
172
- return false;
173
- }
174
- if (
175
- ["BUTTON", "DETAILS", "LABEL", "SELECT", "TEXTAREA", "INPUT", "A"].includes(
176
- elm.nodeName,
177
- )
178
- ) {
179
- return true;
180
- }
181
-
182
- return isInteractiveTarget(elm.parentElement);
183
- }
184
-
185
172
  export default ExpandableRow;
package/src/table/Row.tsx CHANGED
@@ -1,5 +1,7 @@
1
1
  import React, { forwardRef } from "react";
2
2
  import { useRenameCSS } from "../theme/Theme";
3
+ import { composeEventHandlers } from "../util/composeEventHandlers";
4
+ import { isElementInteractiveTarget } from "./Table.utils";
3
5
 
4
6
  export interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
5
7
  /**
@@ -12,6 +14,13 @@ export interface RowProps extends React.HTMLAttributes<HTMLTableRowElement> {
12
14
  * @default true
13
15
  */
14
16
  shadeOnHover?: boolean;
17
+ /**
18
+ * Click handler for row. This differs from onClick by not being called
19
+ * when clicking on interactive elements within the row (buttons, links, inputs etc).
20
+ *
21
+ * **Warning:** This will not be accessible by keyboard! Provide an alternative way to select the row, e.g. a checkbox or a button.
22
+ */
23
+ onRowClick?: (event: React.MouseEvent<HTMLTableRowElement>) => void;
15
24
  }
16
25
 
17
26
  export type RowType = React.ForwardRefExoticComponent<
@@ -19,8 +28,29 @@ export type RowType = React.ForwardRefExoticComponent<
19
28
  >;
20
29
 
21
30
  export const Row: RowType = forwardRef(
22
- ({ className, selected = false, shadeOnHover = true, ...rest }, ref) => {
31
+ (
32
+ {
33
+ className,
34
+ selected = false,
35
+ shadeOnHover = true,
36
+ onClick,
37
+ onRowClick,
38
+ ...rest
39
+ },
40
+ ref,
41
+ ) => {
23
42
  const { cn } = useRenameCSS();
43
+
44
+ const handleRowClick = (event: React.MouseEvent<HTMLTableRowElement>) => {
45
+ if (!onRowClick) {
46
+ return;
47
+ }
48
+ if (isElementInteractiveTarget(event.target as HTMLElement)) {
49
+ return;
50
+ }
51
+ onRowClick(event);
52
+ };
53
+
24
54
  return (
25
55
  <tr
26
56
  {...rest}
@@ -29,6 +59,8 @@ export const Row: RowType = forwardRef(
29
59
  "navds-table__row--selected": selected,
30
60
  "navds-table__row--shade-on-hover": shadeOnHover,
31
61
  })}
62
+ onClick={composeEventHandlers(onClick, handleRowClick)}
63
+ data-interactive={!!onRowClick}
32
64
  />
33
65
  );
34
66
  },