@navikt/ds-react 5.7.6 → 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 (60) hide show
  1. package/_docs.json +30 -9
  2. package/cjs/accordion/AccordionHeader.js +2 -2
  3. package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +1 -2
  4. package/cjs/form/combobox/Input/Input.js +13 -5
  5. package/cjs/layout/sidemal-test/Sidebar.js +1 -1
  6. package/cjs/loader/Loader.js +1 -1
  7. package/cjs/modal/Modal.js +35 -14
  8. package/cjs/tooltip/Tooltip.js +14 -3
  9. package/esm/accordion/AccordionHeader.js +2 -2
  10. package/esm/accordion/AccordionHeader.js.map +1 -1
  11. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +1 -2
  12. package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
  13. package/esm/form/combobox/Input/Input.js +13 -5
  14. package/esm/form/combobox/Input/Input.js.map +1 -1
  15. package/esm/layout/bleed/Bleed.d.ts +1 -1
  16. package/esm/layout/bleed/Bleed.js +1 -1
  17. package/esm/layout/bleed/Bleed.js.map +1 -1
  18. package/esm/layout/box/Box.d.ts +1 -2
  19. package/esm/layout/box/Box.js +1 -1
  20. package/esm/layout/box/Box.js.map +1 -1
  21. package/esm/layout/grid/HGrid.d.ts +1 -1
  22. package/esm/layout/grid/HGrid.js +1 -1
  23. package/esm/layout/grid/HGrid.js.map +1 -1
  24. package/esm/layout/responsive/Responsive.d.ts +1 -1
  25. package/esm/layout/sidemal-test/Sidebar.js +1 -1
  26. package/esm/layout/sidemal-test/Sidebar.js.map +1 -1
  27. package/esm/layout/stack/Stack.d.ts +1 -1
  28. package/esm/layout/stack/Stack.js +1 -1
  29. package/esm/layout/stack/Stack.js.map +1 -1
  30. package/esm/layout/utilities/css.d.ts +1 -8
  31. package/esm/layout/utilities/css.js.map +1 -1
  32. package/esm/layout/utilities/types.d.ts +9 -0
  33. package/esm/loader/Loader.d.ts +1 -1
  34. package/esm/loader/Loader.js +1 -1
  35. package/esm/modal/Modal.js +35 -14
  36. package/esm/modal/Modal.js.map +1 -1
  37. package/esm/modal/ModalContext.d.ts +1 -0
  38. package/esm/modal/ModalContext.js.map +1 -1
  39. package/esm/modal/types.d.ts +7 -0
  40. package/esm/tooltip/Tooltip.js +16 -5
  41. package/esm/tooltip/Tooltip.js.map +1 -1
  42. package/package.json +3 -3
  43. package/src/accordion/AccordionHeader.tsx +3 -3
  44. package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +1 -2
  45. package/src/form/combobox/Input/Input.tsx +19 -4
  46. package/src/form/combobox/combobox.stories.tsx +44 -0
  47. package/src/layout/bleed/Bleed.tsx +2 -5
  48. package/src/layout/box/Box.tsx +1 -3
  49. package/src/layout/grid/HGrid.tsx +2 -6
  50. package/src/layout/responsive/Responsive.tsx +1 -1
  51. package/src/layout/sidemal-test/Sidebar.tsx +1 -1
  52. package/src/layout/stack/Stack.tsx +2 -6
  53. package/src/layout/utilities/css.ts +1 -36
  54. package/src/layout/utilities/types.ts +16 -0
  55. package/src/loader/Loader.tsx +1 -1
  56. package/src/modal/Modal.tsx +50 -20
  57. package/src/modal/ModalContext.ts +1 -0
  58. package/src/modal/modal.stories.tsx +30 -2
  59. package/src/modal/types.ts +7 -0
  60. package/src/tooltip/Tooltip.tsx +18 -6
@@ -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([