diff --git a/.changeset/slimy-cameras-reflect.md b/.changeset/slimy-cameras-reflect.md new file mode 100644 index 0000000000..a99b48f3e8 --- /dev/null +++ b/.changeset/slimy-cameras-reflect.md @@ -0,0 +1,5 @@ +--- +"@navikt/ds-react": minor +--- + +:sparkles: Modal: Støtte for å lukke ved klikk utenfor diff --git a/@navikt/core/react/src/modal/Modal.tsx b/@navikt/core/react/src/modal/Modal.tsx index 0df1542a68..e9bf338f25 100644 --- a/@navikt/core/react/src/modal/Modal.tsx +++ b/@navikt/core/react/src/modal/Modal.tsx @@ -82,11 +82,13 @@ export const Modal = forwardRef( open, onBeforeClose, onCancel, + closeOnBackdropClick, width, portal, className, "aria-labelledby": ariaLabelledby, style, + onClick, ...rest }: ModalProps, ref @@ -133,30 +135,52 @@ export const Modal = forwardRef( const isWidthPreset = typeof width === "string" && ["small", "medium"].includes(width); + const mergedClassName = cl("navds-modal", className, { + "navds-modal--polyfilled": needPolyfill, + "navds-modal--autowidth": !width, + [`navds-modal--${width}`]: isWidthPreset, + }); + + const mergedStyle = { + ...style, + ...(!isWidthPreset ? { width } : {}), + }; + + const mergedOnCancel: React.DialogHTMLAttributes["onCancel"] = + (event) => { + if (onBeforeClose && onBeforeClose() === false) { + event.preventDefault(); + } else if (onCancel) onCancel(event); + }; + + const mergedOnClick = + closeOnBackdropClick && !needPolyfill // closeOnBackdropClick has issues on polyfill when nesting modals (DatePicker) + ? (event: React.MouseEvent) => { + onClick && onClick(event); + if ( + event.target === modalRef.current && + (!onBeforeClose || onBeforeClose() !== false) + ) { + modalRef.current.close(); + } + } + : onClick; + + const mergedAriaLabelledBy = + !ariaLabelledby && !rest["aria-label"] && header + ? ariaLabelId + : ariaLabelledby; + const component = ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions { - // FYI: onCancel fires when you press Esc - if (onBeforeClose && onBeforeClose() === false) { - event.preventDefault(); - } else if (onCancel) onCancel(event); - }} - aria-labelledby={ - !ariaLabelledby && !rest["aria-label"] && header - ? ariaLabelId - : ariaLabelledby - } + className={mergedClassName} + style={mergedStyle} + onCancel={mergedOnCancel} // FYI: onCancel fires when you press Esc + onClick={mergedOnClick} + aria-labelledby={mergedAriaLabelledBy} > { heading: "Title", size: "small", }} + closeOnBackdropClick > @@ -45,6 +46,7 @@ export const WithUseRef = () => { onBeforeClose={() => window.confirm("Are you sure you want to close the modal?") } + closeOnBackdropClick aria-labelledby="heading123" > @@ -111,6 +113,7 @@ export const WithUseState = () => { e.stopPropagation(); // onClose wil propagate to parent modal if not stopped setOpen2(false); }} + closeOnBackdropClick aria-label="Nested modal" width={800} > diff --git a/@navikt/core/react/src/modal/types.ts b/@navikt/core/react/src/modal/types.ts index 4e84c52ad1..3ad4e5e026 100644 --- a/@navikt/core/react/src/modal/types.ts +++ b/@navikt/core/react/src/modal/types.ts @@ -42,6 +42,13 @@ export interface ModalProps * Called when the user presses the Esc key, unless `onBeforeClose()` returns `false`. */ onCancel?: React.ReactEventHandler; + /** + * Whether to close when clicking on the backdrop. + * + * **WARNING:** Users may click outside by accident. Don't use if closing can cause data loss, or the modal contains important info. + * @default false + */ + closeOnBackdropClick?: boolean; /** * @default fit-content (up to 700px) * */ diff --git a/aksel.nav.no/website/pages/eksempler/modal/close-on-backdrop-click.tsx b/aksel.nav.no/website/pages/eksempler/modal/close-on-backdrop-click.tsx new file mode 100644 index 0000000000..aefadfc2fc --- /dev/null +++ b/aksel.nav.no/website/pages/eksempler/modal/close-on-backdrop-click.tsx @@ -0,0 +1,38 @@ +import { BodyLong, Button, Modal } from "@navikt/ds-react"; +import { withDsExample } from "components/website-modules/examples/withDsExample"; +import { useRef } from "react"; + +const Example = () => { + const ref = useRef(null); + + return ( +
+ + + + + + Culpa aliquip ut cupidatat laborum minim quis ex in aliqua. Qui + incididunt dolor do ad ut. Incididunt eiusmod nostrud deserunt duis + laborum. Proident aute culpa qui nostrud velit adipisicing minim. + Consequat aliqua aute dolor do sit Lorem nisi mollit velit. Aliqua + exercitation non minim minim pariatur sunt laborum ipsum. + Exercitation nostrud est laborum magna non non aliqua qui esse. + + + +
+ ); +}; + +export default withDsExample(Example); + +/* Storybook story */ +export const Demo = { + render: Example, +}; + +export const args = { + index: 5, + desc: "Husk at det er lett å klikke utenfor ved et uhell. Ikke bruk 'closeOnBackdropClick' hvis det kan føre til at brukeren mister data eller går glipp av viktig informasjon.", +};