@navikt/ds-react 5.0.3 → 5.2.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.
- package/README.md +1 -1
- package/_docs.json +19 -0
- package/cjs/button/Button.js +10 -1
- package/cjs/date/DateInput.js +1 -1
- package/cjs/form/combobox/FilteredOptions/FilteredOptions.js +14 -3
- package/cjs/form/combobox/FilteredOptions/filteredOptionsContext.js +5 -1
- package/cjs/form/combobox/Input/Input.js +3 -1
- package/cjs/form/combobox/Input/inputContext.js +2 -1
- package/cjs/guide-panel/Illustration.js +5 -5
- package/cjs/modal/Modal.js +25 -12
- package/cjs/modal/ModalUtils.js +10 -9
- package/cjs/modal/dialog-polyfill.js +7 -33
- package/esm/button/Button.js +10 -1
- package/esm/button/Button.js.map +1 -1
- package/esm/date/DateInput.js +1 -1
- package/esm/date/DateInput.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js +14 -3
- package/esm/form/combobox/FilteredOptions/FilteredOptions.js.map +1 -1
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.d.ts +3 -1
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js +6 -2
- package/esm/form/combobox/FilteredOptions/filteredOptionsContext.js.map +1 -1
- package/esm/form/combobox/Input/Input.js +3 -1
- package/esm/form/combobox/Input/Input.js.map +1 -1
- package/esm/form/combobox/Input/inputContext.js +3 -2
- package/esm/form/combobox/Input/inputContext.js.map +1 -1
- package/esm/guide-panel/Illustration.js +5 -5
- package/esm/modal/Modal.d.ts +5 -0
- package/esm/modal/Modal.js +25 -12
- package/esm/modal/Modal.js.map +1 -1
- package/esm/modal/ModalUtils.d.ts +2 -1
- package/esm/modal/ModalUtils.js +9 -8
- package/esm/modal/ModalUtils.js.map +1 -1
- package/esm/modal/dialog-polyfill.d.ts +1 -0
- package/esm/modal/dialog-polyfill.js +6 -33
- package/esm/modal/dialog-polyfill.js.map +1 -1
- package/package.json +2 -2
- package/src/button/Button.tsx +13 -1
- package/src/date/DateInput.tsx +1 -1
- package/src/form/combobox/FilteredOptions/FilteredOptions.tsx +16 -0
- package/src/form/combobox/FilteredOptions/filteredOptionsContext.tsx +10 -2
- package/src/form/combobox/Input/Input.tsx +3 -0
- package/src/form/combobox/Input/inputContext.tsx +2 -2
- package/src/form/combobox/combobox.stories.tsx +49 -0
- package/src/guide-panel/Illustration.tsx +5 -5
- package/src/loader/loader.stories.tsx +2 -2
- package/src/modal/Modal.test.tsx +54 -0
- package/src/modal/Modal.tsx +28 -11
- package/src/modal/ModalUtils.ts +9 -7
- package/src/modal/dialog-polyfill.ts +8 -40
- package/src/modal/modal.stories.tsx +2 -2
- package/src/timeline/timeline.stories.tsx +1 -1
|
@@ -6,12 +6,13 @@ import React, {
|
|
|
6
6
|
useContext,
|
|
7
7
|
useCallback,
|
|
8
8
|
useRef,
|
|
9
|
-
|
|
9
|
+
SetStateAction,
|
|
10
10
|
} from "react";
|
|
11
11
|
import cl from "clsx";
|
|
12
12
|
import { useCustomOptionsContext } from "../customOptionsContext";
|
|
13
13
|
import { useInputContext } from "../Input/inputContext";
|
|
14
14
|
import usePrevious from "../../../util/usePrevious";
|
|
15
|
+
import { useClientLayoutEffect } from "../../../util";
|
|
15
16
|
|
|
16
17
|
const normalizeText = (text: string): string =>
|
|
17
18
|
typeof text === "string" ? `${text}`.toLowerCase().trim() : "";
|
|
@@ -35,6 +36,8 @@ type FilteredOptionsContextType = {
|
|
|
35
36
|
isListOpen: boolean;
|
|
36
37
|
isLoading?: boolean;
|
|
37
38
|
filteredOptions: string[];
|
|
39
|
+
isMouseLastUsedInputDevice: boolean;
|
|
40
|
+
setIsMouseLastUsedInputDevice: React.Dispatch<SetStateAction<boolean>>;
|
|
38
41
|
isValueNew: boolean;
|
|
39
42
|
toggleIsListOpen: (newState?: boolean) => void;
|
|
40
43
|
currentOption: string | null;
|
|
@@ -84,7 +87,10 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
84
87
|
|
|
85
88
|
const previousSearchTerm = usePrevious(searchTerm);
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
const [isMouseLastUsedInputDevice, setIsMouseLastUsedInputDevice] =
|
|
91
|
+
useState(false);
|
|
92
|
+
|
|
93
|
+
useClientLayoutEffect(() => {
|
|
88
94
|
if (
|
|
89
95
|
shouldAutocomplete &&
|
|
90
96
|
normalizeText(searchTerm) !== "" &&
|
|
@@ -246,6 +252,8 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
|
|
246
252
|
isListOpen,
|
|
247
253
|
isLoading,
|
|
248
254
|
filteredOptions,
|
|
255
|
+
isMouseLastUsedInputDevice,
|
|
256
|
+
setIsMouseLastUsedInputDevice,
|
|
249
257
|
isValueNew,
|
|
250
258
|
toggleIsListOpen,
|
|
251
259
|
currentOption,
|
|
@@ -42,6 +42,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
42
42
|
moveFocusToInput,
|
|
43
43
|
moveFocusToEnd,
|
|
44
44
|
setFilteredOptionsIndex,
|
|
45
|
+
setIsMouseLastUsedInputDevice,
|
|
45
46
|
shouldAutocomplete,
|
|
46
47
|
} = useFilteredOptionsContext();
|
|
47
48
|
|
|
@@ -102,6 +103,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
102
103
|
|
|
103
104
|
const handleKeyDown = useCallback(
|
|
104
105
|
(e) => {
|
|
106
|
+
setIsMouseLastUsedInputDevice(false);
|
|
105
107
|
if (e.key === "Backspace") {
|
|
106
108
|
if (value === "") {
|
|
107
109
|
const lastSelectedOption =
|
|
@@ -132,6 +134,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
132
134
|
isListOpen,
|
|
133
135
|
filteredOptionsIndex,
|
|
134
136
|
moveFocusUp,
|
|
137
|
+
setIsMouseLastUsedInputDevice,
|
|
135
138
|
]
|
|
136
139
|
);
|
|
137
140
|
|
|
@@ -4,12 +4,12 @@ import React, {
|
|
|
4
4
|
createContext,
|
|
5
5
|
useCallback,
|
|
6
6
|
useContext,
|
|
7
|
-
useLayoutEffect,
|
|
8
7
|
useMemo,
|
|
9
8
|
useRef,
|
|
10
9
|
useState,
|
|
11
10
|
} from "react";
|
|
12
11
|
import { useFormField, FormFieldType } from "../../useFormField";
|
|
12
|
+
import { useClientLayoutEffect } from "../../../util";
|
|
13
13
|
|
|
14
14
|
interface InputContextType extends FormFieldType {
|
|
15
15
|
clearInput: (event: React.PointerEvent | React.KeyboardEvent) => void;
|
|
@@ -92,7 +92,7 @@ export const InputContextProvider = ({ children, value: props }) => {
|
|
|
92
92
|
inputRef.current?.focus?.();
|
|
93
93
|
}, []);
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
useClientLayoutEffect(() => {
|
|
96
96
|
if (shouldAutocomplete && inputRef && value !== searchTerm) {
|
|
97
97
|
inputRef.current?.setSelectionRange?.(searchTerm.length, value.length);
|
|
98
98
|
}
|
|
@@ -509,3 +509,52 @@ export const TestThatCallbacksOnlyFireWhenExpected = {
|
|
|
509
509
|
expect(args.onChange.mock.calls).toHaveLength(searchWord.length + 1);
|
|
510
510
|
},
|
|
511
511
|
};
|
|
512
|
+
|
|
513
|
+
export const TestHoverAndFocusSwitching = {
|
|
514
|
+
render: (props) => {
|
|
515
|
+
return (
|
|
516
|
+
<DemoContainer dataTheme={props.darkMode}>
|
|
517
|
+
<UNSAFE_Combobox
|
|
518
|
+
options={options}
|
|
519
|
+
label="Hva er dine favorittfrukter?"
|
|
520
|
+
{...props}
|
|
521
|
+
/>
|
|
522
|
+
</DemoContainer>
|
|
523
|
+
);
|
|
524
|
+
},
|
|
525
|
+
play: async ({ canvasElement }) => {
|
|
526
|
+
const canvas = within(canvasElement);
|
|
527
|
+
|
|
528
|
+
await sleep(500);
|
|
529
|
+
|
|
530
|
+
const getInput = () =>
|
|
531
|
+
canvas.getByRole("combobox", {
|
|
532
|
+
name: "Hva er dine favorittfrukter?",
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
userEvent.click(getInput());
|
|
536
|
+
expect(getInput().getAttribute("aria-expanded")).toEqual("false");
|
|
537
|
+
expect(getInput().getAttribute("aria-activedescendant")).toBeNull();
|
|
538
|
+
|
|
539
|
+
await sleep(250);
|
|
540
|
+
userEvent.keyboard("{ArrowDown}");
|
|
541
|
+
await sleep(250);
|
|
542
|
+
const bananaOption = canvas.getByRole("option", { name: "banana" });
|
|
543
|
+
expect(getInput().getAttribute("aria-activedescendant")).toBe(
|
|
544
|
+
bananaOption.getAttribute("id")
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
userEvent.keyboard("{ArrowDown}");
|
|
548
|
+
await sleep(250);
|
|
549
|
+
const appleOption = canvas.getByRole("option", { name: "apple" });
|
|
550
|
+
expect(getInput().getAttribute("aria-activedescendant")).toBe(
|
|
551
|
+
appleOption.getAttribute("id")
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
userEvent.hover(bananaOption);
|
|
555
|
+
await sleep(250);
|
|
556
|
+
expect(getInput().getAttribute("aria-activedescendant")).toBe(
|
|
557
|
+
bananaOption.getAttribute("id")
|
|
558
|
+
);
|
|
559
|
+
},
|
|
560
|
+
};
|
|
@@ -54,21 +54,21 @@ export const DefaultIllustration: DefaultIllustrationType = ({
|
|
|
54
54
|
fillRule="evenodd"
|
|
55
55
|
clipRule="evenodd"
|
|
56
56
|
d="M16.584 26.9976C15.7859 27.062 15.5625 25.8029 15.803 24.9807C15.8482 24.8249 16.1124 24.1154 16.5802 24.1154C17.0473 24.1154 17.2536 24.5032 17.2823 24.5699C17.6259 25.3715 17.4571 26.9268 16.584 26.9976"
|
|
57
|
-
fill="#
|
|
57
|
+
fill="#23262a"
|
|
58
58
|
/>
|
|
59
59
|
<path
|
|
60
60
|
fillRule="evenodd"
|
|
61
61
|
clipRule="evenodd"
|
|
62
62
|
d="M25.8405 26.9976C26.6386 27.062 26.862 25.8029 26.6215 24.9807C26.5763 24.8249 26.3121 24.1154 25.8444 24.1154C25.3772 24.1154 25.171 24.5032 25.1423 24.5699C24.7987 25.3715 24.9674 26.9268 25.8405 26.9976"
|
|
63
|
-
fill="#
|
|
63
|
+
fill="#23262a"
|
|
64
64
|
/>
|
|
65
65
|
<path
|
|
66
66
|
d="M21.5081 28.2384C21.9854 28.157 22.3113 28.2081 22.428 28.3669C22.8687 28.9672 22.7277 29.6023 21.9718 30.1237C21.5744 30.3977 21.0273 30.4942 20.7377 30.3521C20.596 30.2826 20.4304 30.3536 20.3677 30.5106C20.3051 30.6676 20.3691 30.8512 20.5107 30.9207C20.9894 31.1555 21.7255 31.0257 22.268 30.6517C23.2953 29.9431 23.5304 28.8837 22.863 27.9743C22.5805 27.59 22.0806 27.5116 21.4228 27.6239C21.2697 27.65 21.1647 27.8088 21.1883 27.9784C21.2118 28.1481 21.355 28.2645 21.5081 28.2384Z"
|
|
67
|
-
fill="#
|
|
67
|
+
fill="#23262a"
|
|
68
68
|
/>
|
|
69
69
|
<path
|
|
70
70
|
d="M24.9595 32.3642C24.9315 32.4234 24.8672 32.5367 24.7639 32.686C24.589 32.9386 24.3694 33.1919 24.1027 33.4281C23.3079 34.1319 22.2735 34.5389 20.9568 34.5017C19.673 34.4654 18.6432 34.0647 17.8358 33.4185C17.5393 33.1813 17.2946 32.9272 17.0989 32.6739C16.9836 32.5246 16.9115 32.4114 16.88 32.3523C16.8043 32.2104 16.618 32.152 16.464 32.2218C16.31 32.2917 16.2466 32.4634 16.3224 32.6053C16.3681 32.6908 16.4569 32.8304 16.5927 33.0062C16.8156 33.2948 17.0928 33.5826 17.4283 33.8511C18.3398 34.5805 19.5046 35.0338 20.9378 35.0743C22.4302 35.1165 23.6233 34.6471 24.5333 33.8412C24.8376 33.5717 25.0877 33.2832 25.2878 32.9941C25.4095 32.8183 25.4887 32.6787 25.5291 32.5934C25.5977 32.4485 25.5259 32.2796 25.3686 32.2163C25.2113 32.153 25.0282 32.2192 24.9595 32.3642Z"
|
|
71
|
-
fill="#
|
|
71
|
+
fill="#23262a"
|
|
72
72
|
/>
|
|
73
73
|
<path
|
|
74
74
|
fillRule="evenodd"
|
|
@@ -80,7 +80,7 @@ export const DefaultIllustration: DefaultIllustrationType = ({
|
|
|
80
80
|
fillRule="evenodd"
|
|
81
81
|
clipRule="evenodd"
|
|
82
82
|
d="M27.6207 51.8307H26.5502C26.4709 51.8307 26.407 51.7671 26.407 51.6882V51.5086C26.407 51.4303 26.4709 51.3661 26.5502 51.3661H27.6207C27.7 51.3661 27.764 51.4303 27.764 51.5086V51.6882C27.764 51.7671 27.7 51.8307 27.6207 51.8307"
|
|
83
|
-
fill="#
|
|
83
|
+
fill="#23262a"
|
|
84
84
|
/>
|
|
85
85
|
<path
|
|
86
86
|
fillRule="evenodd"
|
|
@@ -57,7 +57,7 @@ export const Variant = () => (
|
|
|
57
57
|
<Loader size="3xlarge" variant="inverted" />
|
|
58
58
|
<Loader size="3xlarge" variant="interaction" />
|
|
59
59
|
</div>
|
|
60
|
-
<div style={{ backgroundColor: "#
|
|
60
|
+
<div style={{ backgroundColor: "#23262a" }}>
|
|
61
61
|
<Loader size="3xlarge" variant="neutral" />
|
|
62
62
|
<Loader size="3xlarge" variant="inverted" />
|
|
63
63
|
<Loader size="3xlarge" variant="interaction" />
|
|
@@ -72,7 +72,7 @@ export const Transparent = () => (
|
|
|
72
72
|
<Loader size="3xlarge" transparent variant="inverted" />
|
|
73
73
|
<Loader size="3xlarge" transparent variant="interaction" />
|
|
74
74
|
</div>
|
|
75
|
-
<div style={{ backgroundColor: "#
|
|
75
|
+
<div style={{ backgroundColor: "#23262a" }}>
|
|
76
76
|
<Loader size="3xlarge" transparent variant="neutral" />
|
|
77
77
|
<Loader size="3xlarge" transparent variant="inverted" />
|
|
78
78
|
<Loader size="3xlarge" transparent variant="interaction" />
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { Button, Modal } from "..";
|
|
4
|
+
import { BODY_CLASS } from "./ModalUtils";
|
|
5
|
+
|
|
6
|
+
const Test = () => {
|
|
7
|
+
const [open, setOpen] = useState(true);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Modal open={open}>
|
|
11
|
+
<Modal.Body>
|
|
12
|
+
<p>Foobar</p>
|
|
13
|
+
<Button onClick={() => setOpen(false)}>Close</Button>
|
|
14
|
+
</Modal.Body>
|
|
15
|
+
</Modal>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("Modal", () => {
|
|
20
|
+
test("should be visible", () => {
|
|
21
|
+
render(<Test />);
|
|
22
|
+
expect(screen.getByText("Foobar")).toBeVisible();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("should be hidden after setting 'open' to false", async () => {
|
|
26
|
+
render(<Test />);
|
|
27
|
+
fireEvent.click(screen.getByText("Close"));
|
|
28
|
+
expect(screen.getByText("Foobar")).not.toBeVisible();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("should toggle body class", async () => {
|
|
32
|
+
render(<Test />);
|
|
33
|
+
expect(document.body.classList).toContain(BODY_CLASS);
|
|
34
|
+
|
|
35
|
+
fireEvent.click(screen.getByText("Close"));
|
|
36
|
+
await waitFor(() =>
|
|
37
|
+
expect(document.body.classList).not.toContain(BODY_CLASS)
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("should toggle body class when using portal", async () => {
|
|
42
|
+
render(
|
|
43
|
+
<Modal portal open>
|
|
44
|
+
<Modal.Header />
|
|
45
|
+
</Modal>
|
|
46
|
+
);
|
|
47
|
+
expect(document.body.classList).toContain(BODY_CLASS);
|
|
48
|
+
|
|
49
|
+
fireEvent.click(screen.getByRole("button"));
|
|
50
|
+
await waitFor(() =>
|
|
51
|
+
expect(document.body.classList).not.toContain(BODY_CLASS)
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
package/src/modal/Modal.tsx
CHANGED
|
@@ -5,18 +5,17 @@ import React, {
|
|
|
5
5
|
useMemo,
|
|
6
6
|
useRef,
|
|
7
7
|
} from "react";
|
|
8
|
+
import { createPortal } from "react-dom";
|
|
9
|
+
import { useFloatingPortalNode } from "@floating-ui/react";
|
|
8
10
|
import cl from "clsx";
|
|
9
|
-
import dialogPolyfill from "./dialog-polyfill";
|
|
10
|
-
import { Detail, Heading, mergeRefs, useId } from "..";
|
|
11
|
+
import dialogPolyfill, { needPolyfill } from "./dialog-polyfill";
|
|
12
|
+
import { Detail, Heading, mergeRefs, useId, useProvider } from "..";
|
|
11
13
|
import ModalBody from "./ModalBody";
|
|
12
14
|
import ModalHeader from "./ModalHeader";
|
|
13
15
|
import ModalFooter from "./ModalFooter";
|
|
14
16
|
import { getCloseHandler, useBodyScrollLock } from "./ModalUtils";
|
|
15
17
|
import { ModalContext } from "./ModalContext";
|
|
16
18
|
|
|
17
|
-
const needPolyfill =
|
|
18
|
-
typeof window !== "undefined" && window.HTMLDialogElement === undefined;
|
|
19
|
-
|
|
20
19
|
export interface ModalProps
|
|
21
20
|
extends React.DialogHTMLAttributes<HTMLDialogElement> {
|
|
22
21
|
/**
|
|
@@ -65,6 +64,11 @@ export interface ModalProps
|
|
|
65
64
|
* @default fit-content (up to 700px)
|
|
66
65
|
* */
|
|
67
66
|
width?: "medium" | "small" | number | `${number}${string}`;
|
|
67
|
+
/**
|
|
68
|
+
* Lets you render the modal into a different part of the DOM.
|
|
69
|
+
* Will use `rootElement` from `Provider` if defined, otherwise `document.body`.
|
|
70
|
+
*/
|
|
71
|
+
portal?: boolean;
|
|
68
72
|
/**
|
|
69
73
|
* User defined classname for modal
|
|
70
74
|
*/
|
|
@@ -141,6 +145,7 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
|
|
|
141
145
|
onBeforeClose,
|
|
142
146
|
onCancel,
|
|
143
147
|
width,
|
|
148
|
+
portal,
|
|
144
149
|
className,
|
|
145
150
|
"aria-labelledby": ariaLabelledby,
|
|
146
151
|
style,
|
|
@@ -151,35 +156,41 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
|
|
|
151
156
|
const modalRef = useRef<HTMLDialogElement>(null);
|
|
152
157
|
const mergedRef = useMemo(() => mergeRefs([modalRef, ref]), [ref]);
|
|
153
158
|
const ariaLabelId = useId();
|
|
159
|
+
const rootElement = useProvider()?.rootElement;
|
|
160
|
+
const portalNode = useFloatingPortalNode({ root: rootElement });
|
|
154
161
|
|
|
155
162
|
if (useContext(ModalContext)) {
|
|
156
163
|
console.error("Modals should not be nested");
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
useEffect(() => {
|
|
160
|
-
|
|
167
|
+
// If using portal, modalRef.current will not be set before portalNode is set.
|
|
168
|
+
// If not using portal, modalRef.current is available first.
|
|
169
|
+
// We check both to avoid activating polyfill twice when not using portal.
|
|
170
|
+
if (needPolyfill && modalRef.current && portalNode) {
|
|
161
171
|
dialogPolyfill.registerDialog(modalRef.current);
|
|
162
172
|
}
|
|
163
|
-
}, [modalRef]);
|
|
173
|
+
}, [modalRef, portalNode]);
|
|
164
174
|
|
|
165
175
|
useEffect(() => {
|
|
166
176
|
// We need to have this in a useEffect so that the content renders before the modal is displayed,
|
|
167
177
|
// and in case `open` is true initially.
|
|
168
|
-
|
|
178
|
+
// We need to check both modalRef.current and portalNode to make sure the polyfill has been activated.
|
|
179
|
+
if (modalRef.current && portalNode && open !== undefined) {
|
|
169
180
|
if (open && !modalRef.current.open) {
|
|
170
181
|
modalRef.current.showModal();
|
|
171
182
|
} else if (!open && modalRef.current.open) {
|
|
172
183
|
modalRef.current.close();
|
|
173
184
|
}
|
|
174
185
|
}
|
|
175
|
-
}, [modalRef, open]);
|
|
186
|
+
}, [modalRef, portalNode, open]);
|
|
176
187
|
|
|
177
|
-
useBodyScrollLock(modalRef,
|
|
188
|
+
useBodyScrollLock(modalRef, portalNode);
|
|
178
189
|
|
|
179
190
|
const isWidthPreset =
|
|
180
191
|
typeof width === "string" && ["small", "medium"].includes(width);
|
|
181
192
|
|
|
182
|
-
|
|
193
|
+
const component = (
|
|
183
194
|
<dialog
|
|
184
195
|
ref={mergedRef}
|
|
185
196
|
className={cl("navds-modal", className, {
|
|
@@ -229,6 +240,12 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
|
|
|
229
240
|
</ModalContext.Provider>
|
|
230
241
|
</dialog>
|
|
231
242
|
);
|
|
243
|
+
|
|
244
|
+
if (portal) {
|
|
245
|
+
if (portalNode) return createPortal(component, portalNode);
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return component;
|
|
232
249
|
}
|
|
233
250
|
) as ModalComponent;
|
|
234
251
|
|
package/src/modal/ModalUtils.ts
CHANGED
|
@@ -13,17 +13,19 @@ export function getCloseHandler(
|
|
|
13
13
|
return () => modalRef.current?.close();
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export const BODY_CLASS = "navds-modal__document-body";
|
|
17
|
+
|
|
16
18
|
export function useBodyScrollLock(
|
|
17
19
|
modalRef: React.RefObject<HTMLDialogElement>,
|
|
18
|
-
|
|
20
|
+
portalNode: HTMLElement | null
|
|
19
21
|
) {
|
|
20
22
|
React.useEffect(() => {
|
|
21
|
-
if (!modalRef.current) return;
|
|
22
|
-
if (modalRef.current.open) document.body.classList.add(
|
|
23
|
+
if (!modalRef.current || !portalNode) return; // We check both to avoid running this twice when not using portal
|
|
24
|
+
if (modalRef.current.open) document.body.classList.add(BODY_CLASS); // In case `open` is true initially
|
|
23
25
|
|
|
24
26
|
const observer = new MutationObserver(() => {
|
|
25
|
-
if (modalRef.current?.open) document.body.classList.add(
|
|
26
|
-
else document.body.classList.remove(
|
|
27
|
+
if (modalRef.current?.open) document.body.classList.add(BODY_CLASS);
|
|
28
|
+
else document.body.classList.remove(BODY_CLASS);
|
|
27
29
|
});
|
|
28
30
|
observer.observe(modalRef.current, {
|
|
29
31
|
attributes: true,
|
|
@@ -31,7 +33,7 @@ export function useBodyScrollLock(
|
|
|
31
33
|
});
|
|
32
34
|
return () => {
|
|
33
35
|
observer.disconnect();
|
|
34
|
-
document.body.classList.remove(
|
|
36
|
+
document.body.classList.remove(BODY_CLASS); // In case modal is unmounted before it's closed
|
|
35
37
|
};
|
|
36
|
-
}, [modalRef,
|
|
38
|
+
}, [modalRef, portalNode]);
|
|
37
39
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
|
|
3
|
+
export const needPolyfill =
|
|
4
|
+
typeof window !== "undefined" &&
|
|
5
|
+
(window.HTMLDialogElement === undefined ||
|
|
6
|
+
navigator.userAgent.includes("jsdom"));
|
|
7
|
+
|
|
3
8
|
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
|
4
9
|
//
|
|
5
10
|
// Redistribution and use in source and binary forms, with or without
|
|
@@ -44,35 +49,6 @@ function safeDispatchEvent(target, event) {
|
|
|
44
49
|
return target.dispatchEvent(event);
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
/**
|
|
48
|
-
* @param {Element} el to check for stacking context
|
|
49
|
-
* @return {boolean} whether this el or its parents creates a stacking context
|
|
50
|
-
*/
|
|
51
|
-
function createsStackingContext(el) {
|
|
52
|
-
while (el && el !== document.body) {
|
|
53
|
-
var s = window.getComputedStyle(el);
|
|
54
|
-
var invalid = function (k, ok) {
|
|
55
|
-
return !(s[k] === undefined || s[k] === ok);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
s.opacity < 1 ||
|
|
60
|
-
invalid("zIndex", "auto") ||
|
|
61
|
-
invalid("transform", "none") ||
|
|
62
|
-
invalid("mixBlendMode", "normal") ||
|
|
63
|
-
invalid("filter", "none") ||
|
|
64
|
-
invalid("perspective", "none") ||
|
|
65
|
-
s["isolation"] === "isolate" ||
|
|
66
|
-
s.position === "fixed" ||
|
|
67
|
-
s.webkitOverflowScrolling === "touch"
|
|
68
|
-
) {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
el = el.parentElement;
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
52
|
/**
|
|
77
53
|
* Finds the nearest <dialog> from the passed element.
|
|
78
54
|
*
|
|
@@ -476,14 +452,6 @@ dialogPolyfillInfo.prototype = /** @type {HTMLDialogElement.prototype} */ {
|
|
|
476
452
|
);
|
|
477
453
|
}
|
|
478
454
|
|
|
479
|
-
if (createsStackingContext(this.dialog_.parentElement)) {
|
|
480
|
-
console.warn(
|
|
481
|
-
"A dialog is being shown inside a stacking context. " +
|
|
482
|
-
"This may cause it to be unusable. For more information, see this link: " +
|
|
483
|
-
"https://github.com/GoogleChrome/dialog-polyfill/#stacking-context"
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
455
|
this.setOpen(true);
|
|
488
456
|
this.openAsModal_ = true;
|
|
489
457
|
|
|
@@ -602,7 +570,7 @@ dialogPolyfill.needsCentering = function (dialog) {
|
|
|
602
570
|
* @param {!Element} element to force upgrade
|
|
603
571
|
*/
|
|
604
572
|
dialogPolyfill.forceRegisterDialog = function (element) {
|
|
605
|
-
if (
|
|
573
|
+
if (element.showModal) {
|
|
606
574
|
console.warn(
|
|
607
575
|
"This browser already supports <dialog>, the polyfill " +
|
|
608
576
|
"may not work correctly",
|
|
@@ -847,7 +815,7 @@ dialogPolyfill.DialogManager.prototype.removeDialog = function (dpi) {
|
|
|
847
815
|
this.updateStacking();
|
|
848
816
|
};
|
|
849
817
|
|
|
850
|
-
if (
|
|
818
|
+
if (needPolyfill) {
|
|
851
819
|
dialogPolyfill.dm = new dialogPolyfill.DialogManager();
|
|
852
820
|
dialogPolyfill.formSubmitter = null;
|
|
853
821
|
dialogPolyfill.imagemapUseValue = null;
|
|
@@ -857,7 +825,7 @@ if (typeof window !== "undefined" && window.HTMLDialogElement === undefined) {
|
|
|
857
825
|
* Installs global handlers, such as click listers and native method overrides. These are needed
|
|
858
826
|
* even if a no dialog is registered, as they deal with <form method="dialog">.
|
|
859
827
|
*/
|
|
860
|
-
if (
|
|
828
|
+
if (needPolyfill) {
|
|
861
829
|
/**
|
|
862
830
|
* If HTMLFormElement translates method="DIALOG" into 'get', then replace the descriptor with
|
|
863
831
|
* one that returns the correct value.
|
|
@@ -160,8 +160,8 @@ export const Small = () => (
|
|
|
160
160
|
</Modal>
|
|
161
161
|
);
|
|
162
162
|
|
|
163
|
-
export const
|
|
164
|
-
<Modal open width="medium" header={{ heading: "Simple header" }}>
|
|
163
|
+
export const MediumWithPortal = () => (
|
|
164
|
+
<Modal open portal width="medium" header={{ heading: "Simple header" }}>
|
|
165
165
|
<Modal.Body>Lorem ipsum dolor sit amet.</Modal.Body>
|
|
166
166
|
</Modal>
|
|
167
167
|
);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { CheckmarkCircleFillIcon } from "@navikt/aksel-icons";
|
|
2
|
-
import { useState } from "@storybook/addons";
|
|
3
2
|
import { Meta } from "@storybook/react";
|
|
4
3
|
import * as React from "react";
|
|
5
4
|
import Timeline from "./Timeline";
|
|
5
|
+
import { useState } from "react";
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
title: "ds-react/Timeline",
|