@navikt/ds-react 7.32.5 → 7.33.1

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 (74) hide show
  1. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +0 -1
  2. package/cjs/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  3. package/cjs/overlays/action-menu/ActionMenu.js +2 -5
  4. package/cjs/overlays/action-menu/ActionMenu.js.map +1 -1
  5. package/cjs/overlays/dismissablelayer/DismissableLayer.d.ts +3 -30
  6. package/cjs/overlays/dismissablelayer/DismissableLayer.js +141 -134
  7. package/cjs/overlays/dismissablelayer/DismissableLayer.js.map +1 -1
  8. package/cjs/overlays/dismissablelayer/util/sort-layers.d.ts +18 -0
  9. package/cjs/overlays/dismissablelayer/util/sort-layers.js +51 -0
  10. package/cjs/overlays/dismissablelayer/util/sort-layers.js.map +1 -0
  11. package/cjs/overlays/floating-menu/Menu.d.ts +5 -7
  12. package/cjs/overlays/floating-menu/Menu.js +7 -15
  13. package/cjs/overlays/floating-menu/Menu.js.map +1 -1
  14. package/cjs/popover/Popover.js +0 -1
  15. package/cjs/popover/Popover.js.map +1 -1
  16. package/cjs/portal/Portal.d.ts +1 -3
  17. package/cjs/portal/Portal.js +49 -17
  18. package/cjs/portal/Portal.js.map +1 -1
  19. package/cjs/process/Process.d.ts +5 -0
  20. package/cjs/process/Process.js +6 -6
  21. package/cjs/process/Process.js.map +1 -1
  22. package/cjs/timeline/Pin.js +5 -4
  23. package/cjs/timeline/Pin.js.map +1 -1
  24. package/cjs/timeline/period/ClickablePeriod.js +3 -2
  25. package/cjs/timeline/period/ClickablePeriod.js.map +1 -1
  26. package/cjs/tooltip/Tooltip.js +23 -22
  27. package/cjs/tooltip/Tooltip.js.map +1 -1
  28. package/cjs/util/focus-boundary/FocusBoundary.d.ts +19 -10
  29. package/cjs/util/focus-boundary/FocusBoundary.js +107 -63
  30. package/cjs/util/focus-boundary/FocusBoundary.js.map +1 -1
  31. package/esm/form/combobox/FilteredOptions/FilteredOptions.js +0 -1
  32. package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
  33. package/esm/overlays/action-menu/ActionMenu.js +2 -5
  34. package/esm/overlays/action-menu/ActionMenu.js.map +1 -1
  35. package/esm/overlays/dismissablelayer/DismissableLayer.d.ts +3 -30
  36. package/esm/overlays/dismissablelayer/DismissableLayer.js +140 -132
  37. package/esm/overlays/dismissablelayer/DismissableLayer.js.map +1 -1
  38. package/esm/overlays/dismissablelayer/util/sort-layers.d.ts +18 -0
  39. package/esm/overlays/dismissablelayer/util/sort-layers.js +49 -0
  40. package/esm/overlays/dismissablelayer/util/sort-layers.js.map +1 -0
  41. package/esm/overlays/floating-menu/Menu.d.ts +5 -7
  42. package/esm/overlays/floating-menu/Menu.js +7 -15
  43. package/esm/overlays/floating-menu/Menu.js.map +1 -1
  44. package/esm/popover/Popover.js +0 -1
  45. package/esm/popover/Popover.js.map +1 -1
  46. package/esm/portal/Portal.d.ts +1 -3
  47. package/esm/portal/Portal.js +50 -18
  48. package/esm/portal/Portal.js.map +1 -1
  49. package/esm/process/Process.d.ts +5 -0
  50. package/esm/process/Process.js +6 -6
  51. package/esm/process/Process.js.map +1 -1
  52. package/esm/timeline/Pin.js +5 -4
  53. package/esm/timeline/Pin.js.map +1 -1
  54. package/esm/timeline/period/ClickablePeriod.js +3 -2
  55. package/esm/timeline/period/ClickablePeriod.js.map +1 -1
  56. package/esm/tooltip/Tooltip.js +23 -22
  57. package/esm/tooltip/Tooltip.js.map +1 -1
  58. package/esm/util/focus-boundary/FocusBoundary.d.ts +19 -10
  59. package/esm/util/focus-boundary/FocusBoundary.js +108 -64
  60. package/esm/util/focus-boundary/FocusBoundary.js.map +1 -1
  61. package/package.json +10 -5
  62. package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +0 -1
  63. package/src/overlays/action-menu/ActionMenu.tsx +2 -4
  64. package/src/overlays/dismissablelayer/DismissableLayer.tsx +219 -194
  65. package/src/overlays/dismissablelayer/util/sort-layers.test.ts +128 -0
  66. package/src/overlays/dismissablelayer/util/sort-layers.ts +61 -0
  67. package/src/overlays/floating-menu/Menu.tsx +11 -21
  68. package/src/popover/Popover.tsx +0 -1
  69. package/src/portal/Portal.tsx +89 -31
  70. package/src/process/Process.tsx +17 -3
  71. package/src/timeline/Pin.tsx +6 -3
  72. package/src/timeline/period/ClickablePeriod.tsx +4 -1
  73. package/src/tooltip/Tooltip.tsx +4 -4
  74. package/src/util/focus-boundary/FocusBoundary.tsx +164 -93
@@ -0,0 +1,61 @@
1
+ type DismissableLayerElement = HTMLDivElement;
2
+
3
+ /**
4
+ * Returns an array of layers sorted such that parents appear before their children.
5
+ *
6
+ * **Why**:
7
+ * - mount order for parent-child relationships is unstable due to portals
8
+ * - event handling relies on parents being before children in the array
9
+ *
10
+ * This function ensures that for any parent-child relationship, the parent layer
11
+ * will always appear before its child layer in the returned array,
12
+ * resulting in consistent behavior.
13
+ *
14
+ * @param layers - A set of DismissableLayerElements to be sorted.
15
+ * @param branchedLayers - A map where each key is a parent layer and its value is a set of child layers.
16
+ * @returns An array of DismissableLayerElements sorted by parent-child relationships.
17
+ */
18
+ function getSortedLayers(
19
+ layers: Set<DismissableLayerElement>,
20
+ branchedLayers: Map<DismissableLayerElement, Set<DismissableLayerElement>>,
21
+ ): DismissableLayerElement[] {
22
+ const sorted: DismissableLayerElement[] = [];
23
+ const visited = new Set<DismissableLayerElement>();
24
+ const parentMap = new Map<DismissableLayerElement, DismissableLayerElement>();
25
+
26
+ branchedLayers.forEach((children, parent) => {
27
+ children.forEach((child) => {
28
+ if (child !== parent) {
29
+ parentMap.set(child, parent);
30
+ }
31
+ });
32
+ });
33
+
34
+ const walk = (layer: DismissableLayerElement) => {
35
+ if (visited.has(layer)) {
36
+ return;
37
+ }
38
+
39
+ const parent = parentMap.get(layer);
40
+ if (parent && !visited.has(parent)) {
41
+ walk(parent);
42
+ if (visited.has(layer)) {
43
+ return;
44
+ }
45
+ }
46
+
47
+ visited.add(layer);
48
+ sorted.push(layer);
49
+
50
+ const children = branchedLayers.get(layer);
51
+ if (children) {
52
+ children.forEach(walk);
53
+ }
54
+ };
55
+
56
+ layers.forEach(walk);
57
+
58
+ return sorted;
59
+ }
60
+
61
+ export { getSortedLayers };
@@ -243,7 +243,7 @@ type DismissableLayerProps = React.ComponentPropsWithoutRef<
243
243
  >;
244
244
 
245
245
  type MenuContentInternalPrivateProps = {
246
- onOpenAutoFocus?: FocusScopeProps["onMountAutoFocus"];
246
+ initialFocus?: FocusScopeProps["initialFocus"];
247
247
  onDismiss?: DismissableLayerProps["onDismiss"];
248
248
  disableOutsidePointerEvents?: DismissableLayerProps["disableOutsidePointerEvents"];
249
249
  };
@@ -254,11 +254,7 @@ interface MenuContentInternalProps
254
254
  React.ComponentPropsWithoutRef<typeof Floating.Content>,
255
255
  "dir" | "onPlaced"
256
256
  > {
257
- /**
258
- * Event handler called when auto-focusing after close.
259
- * Can be prevented.
260
- */
261
- onCloseAutoFocus?: FocusScopeProps["onUnmountAutoFocus"];
257
+ returnFocus?: FocusScopeProps["returnFocus"];
262
258
  onEntryFocus?: RovingFocusProps["onEntryFocus"];
263
259
  onEscapeKeyDown?: DismissableLayerProps["onEscapeKeyDown"];
264
260
  onPointerDownOutside?: DismissableLayerProps["onPointerDownOutside"];
@@ -273,8 +269,8 @@ const MenuContentInternal = forwardRef<
273
269
  >(
274
270
  (
275
271
  {
276
- onOpenAutoFocus,
277
- onCloseAutoFocus,
272
+ initialFocus,
273
+ returnFocus,
278
274
  disableOutsidePointerEvents,
279
275
  onEntryFocus,
280
276
  onEscapeKeyDown,
@@ -301,13 +297,8 @@ const MenuContentInternal = forwardRef<
301
297
 
302
298
  return (
303
299
  <FocusBoundary
304
- onMountAutoFocus={composeEventHandlers(onOpenAutoFocus, (event) => {
305
- // when opening, explicitly focus the content area only and leave
306
- // `onEntryFocus` in control of focusing first item
307
- event.preventDefault();
308
- contentRef.current?.focus({ preventScroll: true });
309
- })}
310
- onUnmountAutoFocus={onCloseAutoFocus}
300
+ initialFocus={initialFocus ?? contentRef}
301
+ returnFocus={returnFocus}
311
302
  /* Focus trapping is handled in `Floating.Content: onKeyDown */
312
303
  trapped={false}
313
304
  loop={false}
@@ -588,7 +579,7 @@ const MenuPortal = forwardRef<MenuPortalElement, MenuPortalProps>(
588
579
  }
589
580
 
590
581
  return (
591
- <Portal asChild rootElement={rootElement} ref={ref}>
582
+ <Portal rootElement={rootElement} ref={ref}>
592
583
  {children}
593
584
  </Portal>
594
585
  );
@@ -917,15 +908,14 @@ const MenuSubContent = forwardRef<
917
908
  align="start"
918
909
  side="right"
919
910
  disableOutsidePointerEvents={false}
920
- onOpenAutoFocus={(event) => {
921
- // when opening a submenu, focus content for keyboard users only
911
+ initialFocus={() => {
922
912
  if (rootContext.isUsingKeyboardRef.current) {
923
- ref.current?.focus();
913
+ return ref.current;
924
914
  }
925
- event.preventDefault();
915
+ return false;
926
916
  }}
927
917
  /* Since we manually focus Subtrigger, we prevent use of auto-focus */
928
- onCloseAutoFocus={(event) => event.preventDefault()}
918
+ returnFocus={false}
929
919
  onEscapeKeyDown={composeEventHandlers(
930
920
  props.onEscapeKeyDown,
931
921
  (event) => {
@@ -174,7 +174,6 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
174
174
  asChild
175
175
  safeZone={{
176
176
  anchor: anchorEl,
177
- dismissable: refs.floating.current,
178
177
  }}
179
178
  onDismiss={() => open && onClose?.()}
180
179
  enabled={open}
@@ -1,53 +1,111 @@
1
- import React, { HTMLAttributes, forwardRef } from "react";
1
+ import React, {
2
+ HTMLAttributes,
3
+ createContext,
4
+ forwardRef,
5
+ useContext,
6
+ } from "react";
2
7
  import ReactDOM from "react-dom";
3
8
  import { useProvider } from "../provider/Provider";
4
- import { Slot } from "../slot/Slot";
5
9
  import { Theme, useThemeInternal } from "../theme/Theme";
6
- import { AsChildProps } from "../util/types";
10
+ import { useClientLayoutEffect, useMergeRefs } from "../util/hooks";
7
11
 
8
- interface PortalBaseProps extends HTMLAttributes<HTMLDivElement> {
12
+ export interface PortalProps extends HTMLAttributes<HTMLDivElement> {
9
13
  /**
10
14
  * An optional container where the portaled content should be appended.
11
15
  */
12
16
  rootElement?: HTMLElement | null;
13
17
  }
14
18
 
15
- export type PortalProps = PortalBaseProps & AsChildProps;
19
+ const PortalContext = createContext<HTMLElement | null>(null);
16
20
 
17
21
  export const Portal = forwardRef<HTMLDivElement, PortalProps>(
18
- ({ rootElement, asChild, ...rest }, ref) => {
19
- const themeContext = useThemeInternal(false);
20
- const contextRoot = useProvider()?.rootElement;
21
- const root = rootElement ?? contextRoot ?? globalThis?.document?.body;
22
+ ({ rootElement, children, ...restProps }, forwardedRef) => {
23
+ const providerRootElement = useProvider()?.rootElement;
24
+
25
+ const parentPortalNode = useContext(PortalContext);
26
+
27
+ const [containerElement, setContainerElement] = React.useState<
28
+ HTMLElement | ShadowRoot | null
29
+ >(null);
30
+ const [portalNode, setPortalNode] = React.useState<HTMLElement | null>(
31
+ null,
32
+ );
33
+
34
+ const containerRef = React.useRef<HTMLElement | ShadowRoot | null>(null);
22
35
 
23
- const Component = asChild ? Slot : "div";
36
+ const mergedRefs = useMergeRefs(forwardedRef, setPortalNode);
24
37
 
25
38
  /**
26
- * Portal can be mounted outside of theme-classNames.
27
- * If a theme is present, we want to make sure that theme cascades to portaled element.
39
+ * We update container in effect to avoid SSR mismatches.
28
40
  */
41
+ useClientLayoutEffect(() => {
42
+ /* Wait for the container to be resolved if explicitly `null`. */
43
+ if ((rootElement ?? providerRootElement) === null) {
44
+ if (containerRef.current) {
45
+ containerRef.current = null;
46
+ setPortalNode(null);
47
+ setContainerElement(null);
48
+ }
49
+ return;
50
+ }
51
+
52
+ const resolvedContainer =
53
+ rootElement ??
54
+ parentPortalNode ??
55
+ providerRootElement ??
56
+ globalThis?.document?.body;
57
+
58
+ if (resolvedContainer === null) {
59
+ if (containerRef.current) {
60
+ containerRef.current = null;
61
+ setPortalNode(null);
62
+ setContainerElement(null);
63
+ }
64
+ return;
65
+ }
66
+
67
+ if (containerRef.current !== resolvedContainer) {
68
+ containerRef.current = resolvedContainer;
69
+ setPortalNode(null);
70
+ setContainerElement(resolvedContainer);
71
+ }
72
+ }, [parentPortalNode, providerRootElement, rootElement]);
73
+
74
+ if (!containerElement) {
75
+ return null;
76
+ }
77
+
78
+ return ReactDOM.createPortal(
79
+ <PortalDiv ref={mergedRefs} {...restProps} data-aksel-portal="">
80
+ <PortalContext.Provider value={portalNode}>
81
+ {children}
82
+ </PortalContext.Provider>
83
+ </PortalDiv>,
84
+ containerElement,
85
+ );
86
+ },
87
+ );
88
+
89
+ type PortalDivProps = React.HTMLAttributes<HTMLDivElement>;
90
+
91
+ const PortalDiv = forwardRef<HTMLDivElement, PortalDivProps>(
92
+ (props: PortalDivProps, forwardedRef) => {
93
+ const themeContext = useThemeInternal(false);
94
+
29
95
  if (themeContext?.isDarkside) {
30
- return root
31
- ? ReactDOM.createPortal(
32
- <Theme
33
- theme={themeContext.theme}
34
- asChild
35
- hasBackground={false}
36
- data-color={themeContext.color}
37
- >
38
- <Component ref={ref} data-aksel-portal="" {...rest} />
39
- </Theme>,
40
- root,
41
- )
42
- : null;
96
+ return (
97
+ <Theme
98
+ theme={themeContext?.theme}
99
+ asChild
100
+ hasBackground={false}
101
+ data-color={themeContext?.color}
102
+ >
103
+ <div ref={forwardedRef} {...props} />
104
+ </Theme>
105
+ );
43
106
  }
44
107
 
45
- return root
46
- ? ReactDOM.createPortal(
47
- <Component ref={ref} data-aksel-portal="" {...rest} />,
48
- root,
49
- )
50
- : null;
108
+ return <div ref={forwardedRef} {...props} />;
51
109
  },
52
110
  );
53
111
 
@@ -22,6 +22,11 @@ interface ProcessProps extends React.HTMLAttributes<HTMLOListElement> {
22
22
  * @default false
23
23
  */
24
24
  hideStatusText?: boolean;
25
+ /**
26
+ * Indicates that the process is truncated and that there are more Events
27
+ * not shown either before, after or on both sides of the current list.
28
+ */
29
+ isTruncated?: "start" | "end" | "both";
25
30
  }
26
31
 
27
32
  type ProcessContextProps = Pick<ProcessProps, "hideStatusText"> & {
@@ -100,6 +105,7 @@ export const Process: ProcessComponent = forwardRef<
100
105
  className,
101
106
  hideStatusText = false,
102
107
  id,
108
+ isTruncated,
103
109
  ...restProps
104
110
  }: ProcessProps,
105
111
  forwardedRef,
@@ -157,6 +163,7 @@ export const Process: ProcessComponent = forwardRef<
157
163
  className={cn("navds-process", className)}
158
164
  id={rootId}
159
165
  aria-controls={activeChildId}
166
+ data-truncated={isTruncated}
160
167
  >
161
168
  <ProcessContextProvider
162
169
  hideStatusText={hideStatusText}
@@ -237,6 +244,7 @@ export const ProcessEvent = forwardRef<HTMLLIElement, ProcessEventProps>(
237
244
  data-process-event=""
238
245
  data-status={status}
239
246
  >
247
+ <ProcessLine position="start" />
240
248
  <div className={cn("navds-process__item")}>
241
249
  <ProcessBullet>{bullet}</ProcessBullet>
242
250
 
@@ -256,7 +264,7 @@ export const ProcessEvent = forwardRef<HTMLLIElement, ProcessEventProps>(
256
264
  )}
257
265
  </div>
258
266
  </div>
259
- <ProcessLine />
267
+ <ProcessLine position="end" />
260
268
  </li>
261
269
  );
262
270
  },
@@ -346,10 +354,16 @@ const ProcessBullet = ({ children }: ProcessBulletProps) => {
346
354
  };
347
355
 
348
356
  /* ------------------------------ Process Line ------------------------------ */
349
- const ProcessLine = () => {
357
+
358
+ type ProcessLineProps = {
359
+ position?: "start" | "end";
360
+ };
361
+ const ProcessLine = ({ position }: ProcessLineProps) => {
350
362
  const { cn } = useRenameCSS();
351
363
 
352
- return <span className={cn("navds-process__line")} />;
364
+ return (
365
+ <span className={cn("navds-process__line")} data-position={position} />
366
+ );
353
367
  };
354
368
 
355
369
  /* -------------------------- Process exports ------------------------- */
@@ -94,6 +94,10 @@ export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
94
94
  left: "right",
95
95
  }[placement.split("-")[0]];
96
96
 
97
+ const label = translate("Pin.pin", {
98
+ date: format(date, translate("dateFormat")),
99
+ });
100
+
97
101
  return (
98
102
  <>
99
103
  <div
@@ -104,9 +108,7 @@ export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
104
108
  {...rest}
105
109
  ref={mergedRef}
106
110
  className={cn("navds-timeline__pin-button")}
107
- aria-label={translate("Pin.pin", {
108
- date: format(date, translate("dateFormat")),
109
- })}
111
+ aria-label={label}
110
112
  type="button"
111
113
  aria-expanded={children ? open : undefined}
112
114
  {...getReferenceProps({
@@ -133,6 +135,7 @@ export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
133
135
  data-placement={placement}
134
136
  ref={refs.setFloating}
135
137
  role="dialog"
138
+ aria-label={label}
136
139
  {...getFloatingProps()}
137
140
  style={floatingStyles}
138
141
  >
@@ -106,6 +106,8 @@ const ClickablePeriod = React.memo(
106
106
  left: "right",
107
107
  }[placement.split("-")[0]];
108
108
 
109
+ const label = ariaLabel(start, end, status, statusLabel, translate);
110
+
109
111
  return (
110
112
  <>
111
113
  <button
@@ -116,7 +118,7 @@ const ClickablePeriod = React.memo(
116
118
  firstFocus && addFocusable(r, index);
117
119
  mergedRef(r);
118
120
  }}
119
- aria-label={ariaLabel(start, end, status, statusLabel, translate)}
121
+ aria-label={label}
120
122
  className={cn(
121
123
  "navds-timeline__period--clickable",
122
124
  getConditionalClasses(cropped, direction, status),
@@ -168,6 +170,7 @@ const ClickablePeriod = React.memo(
168
170
  data-placement={placement}
169
171
  ref={refs.setFloating}
170
172
  role="dialog"
173
+ aria-label={label}
171
174
  {...getFloatingProps()}
172
175
  style={floatingStyles}
173
176
  >
@@ -209,8 +209,8 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
209
209
  >
210
210
  {children}
211
211
  </Slot>
212
- <Portal rootElement={rootElement} asChild>
213
- {_open && (
212
+ {_open && (
213
+ <Portal rootElement={rootElement}>
214
214
  <div
215
215
  {...getFloatingProps({
216
216
  ...rest,
@@ -267,8 +267,8 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
267
267
  />
268
268
  )}
269
269
  </div>
270
- )}
271
- </Portal>
270
+ </Portal>
271
+ )}
272
272
  </>
273
273
  );
274
274
  },