@navikt/ds-react 5.7.5 → 5.8.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 (68) hide show
  1. package/_docs.json +31 -10
  2. package/cjs/accordion/AccordionHeader.js +2 -2
  3. package/cjs/chips/Toggle.js +2 -3
  4. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +1 -2
  5. package/cjs/form/combobox/Input/Input.js +13 -5
  6. package/cjs/layout/sidemal-test/Sidebar.js +1 -1
  7. package/cjs/loader/Loader.js +1 -1
  8. package/cjs/modal/Modal.js +35 -14
  9. package/cjs/tooltip/Tooltip.js +14 -3
  10. package/esm/accordion/AccordionHeader.js +2 -2
  11. package/esm/accordion/AccordionHeader.js.map +1 -1
  12. package/esm/chips/Toggle.d.ts +1 -1
  13. package/esm/chips/Toggle.js +2 -3
  14. package/esm/chips/Toggle.js.map +1 -1
  15. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +1 -2
  16. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  17. package/esm/form/combobox/Input/Input.js +13 -5
  18. package/esm/form/combobox/Input/Input.js.map +1 -1
  19. package/esm/layout/bleed/Bleed.d.ts +1 -1
  20. package/esm/layout/bleed/Bleed.js +1 -1
  21. package/esm/layout/bleed/Bleed.js.map +1 -1
  22. package/esm/layout/box/Box.d.ts +1 -2
  23. package/esm/layout/box/Box.js +1 -1
  24. package/esm/layout/box/Box.js.map +1 -1
  25. package/esm/layout/grid/HGrid.d.ts +1 -1
  26. package/esm/layout/grid/HGrid.js +1 -1
  27. package/esm/layout/grid/HGrid.js.map +1 -1
  28. package/esm/layout/responsive/Responsive.d.ts +1 -1
  29. package/esm/layout/sidemal-test/Sidebar.js +1 -1
  30. package/esm/layout/sidemal-test/Sidebar.js.map +1 -1
  31. package/esm/layout/stack/Stack.d.ts +1 -1
  32. package/esm/layout/stack/Stack.js +1 -1
  33. package/esm/layout/stack/Stack.js.map +1 -1
  34. package/esm/layout/utilities/css.d.ts +1 -8
  35. package/esm/layout/utilities/css.js.map +1 -1
  36. package/esm/layout/utilities/types.d.ts +9 -0
  37. package/esm/loader/Loader.d.ts +1 -1
  38. package/esm/loader/Loader.js +1 -1
  39. package/esm/modal/Modal.js +35 -14
  40. package/esm/modal/Modal.js.map +1 -1
  41. package/esm/modal/ModalContext.d.ts +1 -0
  42. package/esm/modal/ModalContext.js.map +1 -1
  43. package/esm/modal/types.d.ts +7 -0
  44. package/esm/tooltip/Tooltip.js +16 -5
  45. package/esm/tooltip/Tooltip.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/accordion/AccordionHeader.tsx +3 -3
  48. package/src/chips/Toggle.tsx +22 -12
  49. package/src/chips/chips.stories.tsx +9 -3
  50. package/src/date/datepicker/datepicker.stories.tsx +0 -1
  51. package/src/date/monthpicker/monthpicker.stories.tsx +28 -1
  52. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +1 -2
  53. package/src/form/combobox/Input/Input.tsx +19 -4
  54. package/src/form/combobox/combobox.stories.tsx +44 -0
  55. package/src/layout/bleed/Bleed.tsx +2 -5
  56. package/src/layout/box/Box.tsx +1 -3
  57. package/src/layout/grid/HGrid.tsx +2 -6
  58. package/src/layout/responsive/Responsive.tsx +1 -1
  59. package/src/layout/sidemal-test/Sidebar.tsx +1 -1
  60. package/src/layout/stack/Stack.tsx +2 -6
  61. package/src/layout/utilities/css.ts +1 -36
  62. package/src/layout/utilities/types.ts +16 -0
  63. package/src/loader/Loader.tsx +1 -1
  64. package/src/modal/Modal.tsx +50 -20
  65. package/src/modal/ModalContext.ts +1 -0
  66. package/src/modal/modal.stories.tsx +30 -2
  67. package/src/modal/types.ts +7 -0
  68. package/src/tooltip/Tooltip.tsx +18 -6
@@ -467,6 +467,50 @@ export const TestThatCallbacksOnlyFireWhenExpected: StoryObj<{
467
467
  },
468
468
  };
469
469
 
470
+ export const TestCasingWhenAutoCompleting = {
471
+ args: {
472
+ onChange: jest.fn(),
473
+ onClear: jest.fn(),
474
+ onToggleSelected: jest.fn(),
475
+ },
476
+ render: (props) => {
477
+ return (
478
+ <UNSAFE_Combobox
479
+ options={["Camel Case", "lowercase", "UPPERCASE"]}
480
+ label="Liker du best store eller små bokstaver?"
481
+ shouldAutocomplete
482
+ allowNewValues
483
+ {...props}
484
+ />
485
+ );
486
+ },
487
+ play: async ({ canvasElement }) => {
488
+ const canvas = within(canvasElement);
489
+ const input = canvas.getByRole<HTMLInputElement>("combobox");
490
+
491
+ // With exisiting option
492
+ userEvent.click(input);
493
+ await userEvent.type(input, "cAmEl CaSe", { delay: 250 });
494
+ await sleep(250);
495
+ expect(input.value).toBe("cAmEl CaSe");
496
+ await userEvent.type(input, "{Enter}");
497
+ await sleep(250);
498
+ const chips = canvas.getAllByRole("list")[0];
499
+ const selectedUpperCaseChip = within(chips).getAllByRole("listitem")[0];
500
+ expect(selectedUpperCaseChip).toHaveTextContent("Camel Case"); // A weird issue is preventing the accessible name from being used in the test, even if it works in VoiceOver
501
+
502
+ // With custom option
503
+ userEvent.click(input);
504
+ await userEvent.type(input, "cAmEl{Backspace}", { delay: 250 });
505
+ await sleep(250);
506
+ expect(input.value).toBe("cAmEl");
507
+ await userEvent.type(input, "{Enter}");
508
+ await sleep(250);
509
+ const selectedNewValueChip = within(chips).getAllByRole("listitem")[0];
510
+ expect(selectedNewValueChip).toHaveTextContent("cAmEl"); // A weird issue is preventing the accessible name from being used in the test, even if it works in VoiceOver
511
+ },
512
+ };
513
+
470
514
  export const TestHoverAndFocusSwitching: StoryObject = {
471
515
  render: () => {
472
516
  return (
@@ -1,10 +1,7 @@
1
1
  import cl from "clsx";
2
2
  import React, { forwardRef } from "react";
3
- import {
4
- ResponsiveProp,
5
- SpacingScale,
6
- getResponsiveProps,
7
- } from "../utilities/css";
3
+ import { getResponsiveProps } from "../utilities/css";
4
+ import { ResponsiveProp, SpacingScale } from "../utilities/types";
8
5
  import { Slot } from "../../util/Slot";
9
6
 
10
7
  export type BleedSpacingInline = "0" | "full" | "px" | SpacingScale;
@@ -1,13 +1,11 @@
1
1
  import cl from "clsx";
2
2
  import React, { forwardRef } from "react";
3
3
  import { OverridableComponent } from "../../util/OverridableComponent";
4
+ import { getResponsiveProps } from "../utilities/css";
4
5
  import {
5
6
  SpaceDelimitedAttribute,
6
7
  ResponsiveProp,
7
8
  SpacingScale,
8
- getResponsiveProps,
9
- } from "../utilities/css";
10
- import {
11
9
  BackgroundToken,
12
10
  BorderColorToken,
13
11
  ShadowToken,
@@ -1,11 +1,7 @@
1
1
  import React, { forwardRef, HTMLAttributes } from "react";
2
2
  import cl from "clsx";
3
- import {
4
- getResponsiveProps,
5
- getResponsiveValue,
6
- ResponsiveProp,
7
- SpacingScale,
8
- } from "../utilities/css";
3
+ import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
4
+ import { ResponsiveProp, SpacingScale } from "../utilities/types";
9
5
 
10
6
  export interface HGridProps extends HTMLAttributes<HTMLDivElement> {
11
7
  children: React.ReactNode;
@@ -1,6 +1,6 @@
1
1
  import cl from "clsx";
2
2
  import React, { forwardRef, HTMLAttributes } from "react";
3
- import { BreakpointsAlias } from "../utilities/css";
3
+ import { BreakpointsAlias } from "../utilities/types";
4
4
  import { Slot } from "../../util/Slot";
5
5
 
6
6
  export interface ResponsiveProps extends HTMLAttributes<HTMLDivElement> {
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
+ import { Link } from "../../link";
2
3
  import { Heading, Label } from "../../typography";
3
4
  import { Box } from "../box";
4
5
  import { VStack } from "../stack";
5
- import { Link } from "../../link";
6
6
 
7
7
  const LinkElement = ({ children }) => {
8
8
  return (
@@ -1,12 +1,8 @@
1
1
  import cl from "clsx";
2
2
  import React, { forwardRef, HTMLAttributes } from "react";
3
3
  import { OverridableComponent } from "../../util/OverridableComponent";
4
- import {
5
- getResponsiveProps,
6
- getResponsiveValue,
7
- ResponsiveProp,
8
- SpacingScale,
9
- } from "../utilities/css";
4
+ import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
5
+ import { ResponsiveProp, SpacingScale } from "../utilities/types";
10
6
 
11
7
  export interface StackProps extends HTMLAttributes<HTMLDivElement> {
12
8
  children: React.ReactNode;
@@ -1,39 +1,4 @@
1
- export type BreakpointsAlias = "xs" | "sm" | "md" | "lg" | "xl";
2
-
3
- export type SpacingScale =
4
- | "0"
5
- | "05"
6
- | "1"
7
- | "1-alt"
8
- | "2"
9
- | "3"
10
- | "4"
11
- | "5"
12
- | "6"
13
- | "7"
14
- | "8"
15
- | "9"
16
- | "10"
17
- | "11"
18
- | "12"
19
- | "14"
20
- | "16"
21
- | "18"
22
- | "20"
23
- | "24"
24
- | "32";
25
-
26
- export type SpaceDelimitedAttribute<T extends string> =
27
- | T
28
- | `${T} ${T}`
29
- | `${T} ${T} ${T}`
30
- | `${T} ${T} ${T} ${T}`;
31
-
32
- type FixedResponsiveT<T> = {
33
- [Breakpoint in BreakpointsAlias]?: T;
34
- };
35
-
36
- export type ResponsiveProp<T> = T | FixedResponsiveT<T>;
1
+ import { ResponsiveProp } from "./types";
37
2
 
38
3
  export function getResponsiveValue<T = string>(
39
4
  componentName: string,
@@ -3,6 +3,7 @@ import surfaceColors from "@navikt/ds-tokens/src/colors-surface.json";
3
3
  import borderColors from "@navikt/ds-tokens/src/colors-border.json";
4
4
  import borderRadii from "@navikt/ds-tokens/src/border.json";
5
5
  import shadows from "@navikt/ds-tokens/src/shadow.json";
6
+ import Spacing from "@navikt/ds-tokens/src/spacing.json";
6
7
 
7
8
  export type BackgroundToken =
8
9
  | keyof typeof bgColors.a
@@ -12,3 +13,18 @@ export type BorderRadiiToken =
12
13
  | keyof (typeof borderRadii.a)["border-radius"]
13
14
  | "0";
14
15
  export type ShadowToken = keyof typeof shadows.a.shadow;
16
+
17
+ export type BreakpointsAlias = "xs" | "sm" | "md" | "lg" | "xl";
18
+
19
+ export type SpacingScale = keyof (typeof Spacing)["a"]["spacing"];
20
+
21
+ export type SpaceDelimitedAttribute<T extends string> =
22
+ | T
23
+ | `${T} ${T}`
24
+ | `${T} ${T} ${T}`
25
+ | `${T} ${T} ${T} ${T}`;
26
+ type FixedResponsiveT<T> = {
27
+ [Breakpoint in BreakpointsAlias]?: T;
28
+ };
29
+
30
+ export type ResponsiveProp<T> = T | FixedResponsiveT<T>;
@@ -52,7 +52,7 @@ export type LoaderType = React.ForwardRefExoticComponent<
52
52
  *
53
53
  * @example
54
54
  * ```jsx
55
- * <Loader size="3xlarge" title="venter..." />
55
+ * <Loader size="3xlarge" title="Venter..." />
56
56
  * ```
57
57
  */
58
58
  export const Loader: LoaderType = forwardRef<SVGSVGElement, LoaderProps>(
@@ -82,11 +82,13 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
82
82
  open,
83
83
  onBeforeClose,
84
84
  onCancel,
85
+ closeOnBackdropClick,
85
86
  width,
86
87
  portal,
87
88
  className,
88
89
  "aria-labelledby": ariaLabelledby,
89
90
  style,
91
+ onClick,
90
92
  ...rest
91
93
  }: ModalProps,
92
94
  ref
@@ -108,6 +110,11 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
108
110
  if (needPolyfill && modalRef.current && portalNode) {
109
111
  dialogPolyfill.registerDialog(modalRef.current);
110
112
  }
113
+ // We set autofocus on the dialog element to prevent the default behavior where first focusable element gets focus when modal is opened.
114
+ // This is mainly to fix an edge case where having a Tooltip as the first focusable element would make it activate when you open the modal.
115
+ // We have to use JS because it doesn't work to set it with a prop (React bug?)
116
+ // Currently doesn't seem to work in Chrome. See also Tooltip.tsx
117
+ if (modalRef.current && portalNode) modalRef.current.autofocus = true;
111
118
  }, [modalRef, portalNode]);
112
119
 
113
120
  useEffect(() => {
@@ -128,34 +135,57 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
128
135
  const isWidthPreset =
129
136
  typeof width === "string" && ["small", "medium"].includes(width);
130
137
 
138
+ const mergedClassName = cl("navds-modal", className, {
139
+ "navds-modal--polyfilled": needPolyfill,
140
+ "navds-modal--autowidth": !width,
141
+ [`navds-modal--${width}`]: isWidthPreset,
142
+ });
143
+
144
+ const mergedStyle = {
145
+ ...style,
146
+ ...(!isWidthPreset ? { width } : {}),
147
+ };
148
+
149
+ const mergedOnCancel: React.DialogHTMLAttributes<HTMLDialogElement>["onCancel"] =
150
+ (event) => {
151
+ if (onBeforeClose && onBeforeClose() === false) {
152
+ event.preventDefault();
153
+ } else if (onCancel) onCancel(event);
154
+ };
155
+
156
+ const mergedOnClick =
157
+ closeOnBackdropClick && !needPolyfill // closeOnBackdropClick has issues on polyfill when nesting modals (DatePicker)
158
+ ? (event: React.MouseEvent<HTMLDialogElement>) => {
159
+ onClick && onClick(event);
160
+ if (
161
+ event.target === modalRef.current &&
162
+ (!onBeforeClose || onBeforeClose() !== false)
163
+ ) {
164
+ modalRef.current.close();
165
+ }
166
+ }
167
+ : onClick;
168
+
169
+ const mergedAriaLabelledBy =
170
+ !ariaLabelledby && !rest["aria-label"] && header
171
+ ? ariaLabelId
172
+ : ariaLabelledby;
173
+
131
174
  const component = (
175
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
132
176
  <dialog
133
177
  {...rest}
134
178
  ref={mergedRef}
135
- className={cl("navds-modal", className, {
136
- "navds-modal--polyfilled": needPolyfill,
137
- "navds-modal--autowidth": !width,
138
- [`navds-modal--${width}`]: isWidthPreset,
139
- })}
140
- style={{
141
- ...style,
142
- ...(!isWidthPreset ? { width } : {}),
143
- }}
144
- onCancel={(event) => {
145
- // FYI: onCancel fires when you press Esc
146
- if (onBeforeClose && onBeforeClose() === false) {
147
- event.preventDefault();
148
- } else if (onCancel) onCancel(event);
149
- }}
150
- aria-labelledby={
151
- !ariaLabelledby && !rest["aria-label"] && header
152
- ? ariaLabelId
153
- : ariaLabelledby
154
- }
179
+ className={mergedClassName}
180
+ style={mergedStyle}
181
+ onCancel={mergedOnCancel} // FYI: onCancel fires when you press Esc
182
+ onClick={mergedOnClick}
183
+ aria-labelledby={mergedAriaLabelledBy}
155
184
  >
156
185
  <ModalContext.Provider
157
186
  value={{
158
187
  closeHandler: getCloseHandler(modalRef, header, onBeforeClose),
188
+ ref: modalRef,
159
189
  }}
160
190
  >
161
191
  {header && (
@@ -2,5 +2,6 @@ import React from "react";
2
2
 
3
3
  interface ModalContextProps {
4
4
  closeHandler?: React.MouseEventHandler<HTMLButtonElement>;
5
+ ref: React.RefObject<HTMLDialogElement>;
5
6
  }
6
7
  export const ModalContext = React.createContext<ModalContextProps | null>(null);
@@ -1,6 +1,6 @@
1
- import React, { useRef, useState } from "react";
2
1
  import { FileIcon } from "@navikt/aksel-icons";
3
- import { BodyLong, Button, Heading } from "..";
2
+ import React, { useRef, useState } from "react";
3
+ import { BodyLong, Button, Heading, Tooltip } from "..";
4
4
  import Modal from "./Modal";
5
5
 
6
6
  export default {
@@ -27,6 +27,7 @@ export const WithUseRef = () => {
27
27
  heading: "Title",
28
28
  size: "small",
29
29
  }}
30
+ closeOnBackdropClick
30
31
  >
31
32
  <Modal.Body>
32
33
  <BodyLong spacing>
@@ -45,6 +46,7 @@ export const WithUseRef = () => {
45
46
  onBeforeClose={() =>
46
47
  window.confirm("Are you sure you want to close the modal?")
47
48
  }
49
+ closeOnBackdropClick
48
50
  aria-labelledby="heading123"
49
51
  >
50
52
  <Modal.Header>
@@ -111,6 +113,7 @@ export const WithUseState = () => {
111
113
  e.stopPropagation(); // onClose wil propagate to parent modal if not stopped
112
114
  setOpen2(false);
113
115
  }}
116
+ closeOnBackdropClick
114
117
  aria-label="Nested modal"
115
118
  width={800}
116
119
  >
@@ -165,3 +168,28 @@ export const MediumWithPortal = () => (
165
168
  <Modal.Body>Lorem ipsum dolor sit amet.</Modal.Body>
166
169
  </Modal>
167
170
  );
171
+
172
+ export const WithTooltip = () => {
173
+ const ref = useRef<HTMLDialogElement>(null);
174
+
175
+ return (
176
+ <div>
177
+ <Button onClick={() => ref.current?.showModal()}>Open Modal</Button>
178
+ <Modal
179
+ open={ref.current ? undefined : true /* initially open */}
180
+ ref={ref}
181
+ >
182
+ <Modal.Body>
183
+ <div style={{ marginBottom: "1rem" }}>
184
+ <Tooltip content="This_is_the_first_tooltip">
185
+ <Button>Test 1</Button>
186
+ </Tooltip>
187
+ </div>
188
+ <Tooltip content="This is the second tooltip">
189
+ <Button>Test 2</Button>
190
+ </Tooltip>
191
+ </Modal.Body>
192
+ </Modal>
193
+ </div>
194
+ );
195
+ };
@@ -42,6 +42,13 @@ export interface ModalProps
42
42
  * Called when the user presses the Esc key, unless `onBeforeClose()` returns `false`.
43
43
  */
44
44
  onCancel?: React.ReactEventHandler<HTMLDialogElement>;
45
+ /**
46
+ * Whether to close when clicking on the backdrop.
47
+ *
48
+ * **WARNING:** Users may click outside by accident. Don't use if closing can cause data loss, or the modal contains important info.
49
+ * @default false
50
+ */
51
+ closeOnBackdropClick?: boolean;
45
52
  /**
46
53
  * @default fit-content (up to 700px)
47
54
  * */
@@ -1,8 +1,8 @@
1
1
  import {
2
- arrow as flArrow,
2
+ FloatingPortal,
3
3
  autoUpdate,
4
+ arrow as flArrow,
4
5
  flip,
5
- FloatingPortal,
6
6
  offset,
7
7
  safePolygon,
8
8
  shift,
@@ -14,16 +14,18 @@ import {
14
14
  } from "@floating-ui/react";
15
15
  import cl from "clsx";
16
16
  import React, {
17
+ HTMLAttributes,
17
18
  cloneElement,
18
19
  forwardRef,
19
- HTMLAttributes,
20
+ useContext,
20
21
  useMemo,
21
22
  useRef,
22
23
  useState,
23
24
  } from "react";
25
+ import { ModalContext } from "../modal/ModalContext";
26
+ import { useProvider } from "../provider";
24
27
  import { Detail } from "../typography";
25
28
  import { mergeRefs, useId } from "../util";
26
- import { useProvider } from "../provider";
27
29
 
28
30
  export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
29
31
  /**
@@ -110,7 +112,11 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
110
112
  ) => {
111
113
  const [open, setOpen] = useState(defaultOpen);
112
114
  const arrowRef = useRef<HTMLDivElement | null>(null);
113
- const rootElement = useProvider()?.rootElement;
115
+ const modalContext = useContext(ModalContext);
116
+ const providerRootElement = useProvider()?.rootElement;
117
+ const rootElement = modalContext
118
+ ? modalContext.ref.current
119
+ : providerRootElement;
114
120
 
115
121
  const {
116
122
  x,
@@ -133,7 +139,13 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
133
139
  flip({ padding: 5, fallbackPlacements: ["bottom", "top"] }),
134
140
  flArrow({ element: arrowRef, padding: 5 }),
135
141
  ],
136
- whileElementsMounted: autoUpdate,
142
+ whileElementsMounted: modalContext
143
+ ? (reference, floating, update) =>
144
+ // Reduces jumping in Chrome when used in a Modal and it's the first focusable element.
145
+ // Can be removed when autofocus starts working on <dialog> in Chrome. See also Modal.tsx
146
+ autoUpdate(reference, floating, update, { animationFrame: true })
147
+ : autoUpdate,
148
+ strategy: modalContext ? "fixed" : undefined,
137
149
  });
138
150
 
139
151
  const { getReferenceProps, getFloatingProps } = useInteractions([