UNPKG

@navikt/ds-react

Version:

React components from the Norwegian Labour and Welfare Administration.

287 lines 14.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.DismissableLayer = exports.useDismissableDescendant = exports.useDismissableDescendants = exports.useDismissableDescendantsContext = exports.DismissableDescendantsProvider = void 0; const react_1 = __importStar(require("react")); const Slot_1 = require("../../slot/Slot"); const hooks_1 = require("../../util/hooks"); const useDescendant_1 = require("../../util/hooks/descendants/useDescendant"); const useEscapeKeydown_1 = require("./util/useEscapeKeydown"); const useFocusOutside_1 = require("./util/useFocusOutside"); const usePointerDownOutside_1 = require("./util/usePointerDownOutside"); _a = (0, useDescendant_1.createDescendantContext)(), exports.DismissableDescendantsProvider = _a[0], exports.useDismissableDescendantsContext = _a[1], exports.useDismissableDescendants = _a[2], exports.useDismissableDescendant = _a[3]; /** * Number of layers with `disableOutsidePointerEvents` set to `true` currently enabled. */ let bodyLockCount = 0; let originalBodyPointerEvents; const DismissableLayer = (0, react_1.forwardRef)((props, ref) => { const context = (0, exports.useDismissableDescendantsContext)(false); /** * To correctly handle nested DismissableLayer, * we only initialize the `Descendants`-API for the root layer to aboid resetting context */ return context ? (react_1.default.createElement(DismissableLayerNode, Object.assign({ ref: ref }, props))) : (react_1.default.createElement(DismissableRoot, null, react_1.default.createElement(DismissableLayerNode, Object.assign({ ref: ref }, props)))); }); exports.DismissableLayer = DismissableLayer; /** * DismissableRoot * * Used to initialize the `Descendants`-API at the root layer. * All subsequent layers will use the same context. */ const DismissableRoot = ({ children }) => { const descendants = (0, exports.useDismissableDescendants)(); return (react_1.default.createElement(exports.DismissableDescendantsProvider, { value: descendants }, children)); }; const DismissableLayerNode = (0, react_1.forwardRef)((_a, ref) => { var _b; var { children, asChild, onEscapeKeyDown, onPointerDownOutside, onFocusOutside, onInteractOutside, onDismiss, safeZone, disableOutsidePointerEvents = false, enabled = true } = _a, rest = __rest(_a, ["children", "asChild", "onEscapeKeyDown", "onPointerDownOutside", "onFocusOutside", "onInteractOutside", "onDismiss", "safeZone", "disableOutsidePointerEvents", "enabled"]); const [, setForce] = (0, react_1.useState)({}); const { register, index, descendants } = (0, exports.useDismissableDescendant)({ disableOutsidePointerEvents, disabled: !enabled, forceUpdate: () => setForce({}), }); /** * `node` will be set to the ref of the component or nested component * Ex: If * ``` * <DismissableLayer asChild> * <Popover /> * </DismissableLayer> * ``` * `node` will in this case be the Popover-element. * We use State her and not ref since we want to trigger a rerender when the node changes. */ const [node, setNode] = (0, react_1.useState)(null); const mergedRefs = (0, hooks_1.useMergeRefs)(setNode, register, ref); /** * In some cases the `node.ownerDocument` can differ from global document. * This can happend when portaling elements or using web-components */ const ownerDocument = (_b = node === null || node === void 0 ? void 0 : node.ownerDocument) !== null && _b !== void 0 ? _b : globalThis === null || globalThis === void 0 ? void 0 : globalThis.document; const hasInteractedOutsideRef = (0, react_1.useRef)(false); const hasPointerDownOutsideRef = (0, react_1.useRef)(false); const pointerState = (() => { let lastIndex = -1; const descendantNodes = descendants.enabledValues(); descendantNodes.forEach((obj, _index) => { if (obj.disableOutsidePointerEvents) { lastIndex = _index; } }); return { /** * Makes sure we stop events at the highest layer with pointer events disabled. * If not checked, we risk closing every layer when clicking outside the layer. */ isPointerEventsEnabled: index >= lastIndex, /** * If we find a node with `disableOutsidePointerEvents` we want to disable pointer events on the body. */ isBodyPointerEventsDisabled: bodyLockCount > 0, pointerStyle: (index >= lastIndex && bodyLockCount > 0 ? "auto" : undefined), }; })(); /** * We want to prevent the Layer from closing when the trigger, anchor element, or its child elements are interacted with. * * To achieve this, we check if the event target is the trigger, anchor or a child. If it is, we prevent default event behavior. * * The `pointerDownOutside` and `focusOutside` handlers already check if the event target is within the DismissableLayer (`node`). * However, since we don't add a `tabIndex` to the Popover/Tooltip, the `focusOutside` handler doesn't correctly handle focus events. * Therefore, we also need to check that neither the trigger (`anchor`) nor the DismissableLayer (`dismissable`) are the event targets. */ function handleOutsideEvent(event) { var _a, _b; if ((!(safeZone === null || safeZone === void 0 ? void 0 : safeZone.anchor) && !(safeZone === null || safeZone === void 0 ? void 0 : safeZone.dismissable)) || !enabled) { return; } if (!event.defaultPrevented) { hasInteractedOutsideRef.current = true; if (event.detail.originalEvent.type === "pointerdown") { hasPointerDownOutsideRef.current = true; } } const target = event.target; /** * pointerdown-events works as expected, but focus-events does not. * For focus-event we need to also check `safeZone.dismissable` (the Popover/Tooltip itself) since it does not have a tabIndex. */ if (event.detail.originalEvent.type === "pointerdown") { const targetIsTrigger = ((_a = safeZone === null || safeZone === void 0 ? void 0 : safeZone.anchor) === null || _a === void 0 ? void 0 : _a.contains(target)) || target === (safeZone === null || safeZone === void 0 ? void 0 : safeZone.anchor); targetIsTrigger && event.preventDefault(); } else { const targetIsNotTrigger = target instanceof HTMLElement && ![safeZone === null || safeZone === void 0 ? void 0 : safeZone.anchor, safeZone === null || safeZone === void 0 ? void 0 : safeZone.dismissable].some((element) => element === null || element === void 0 ? void 0 : element.contains(target)) && !target.contains((_b = safeZone === null || safeZone === void 0 ? void 0 : safeZone.dismissable) !== null && _b !== void 0 ? _b : null); !targetIsNotTrigger && event.preventDefault(); } /** * In Safari, if the trigger element is inside a container with tabIndex={0}, a click on the trigger * will first fire a 'pointerdownoutside' event on the trigger itself. However, it will then fire a * 'focusoutside' event on the container. * * To handle this, we ignore any 'focusoutside' events if a 'pointerdownoutside' event has already occurred. * 'pointerdownoutside' event is sufficient to indicate interaction outside the DismissableLayer. */ if (event.detail.originalEvent.type === "focusin" && hasPointerDownOutsideRef.current) { event.preventDefault(); } hasPointerDownOutsideRef.current = false; hasInteractedOutsideRef.current = false; } const pointerDownOutside = (0, usePointerDownOutside_1.usePointerDownOutside)((event) => { if (!pointerState.isPointerEventsEnabled || !enabled) { return; } /** * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases. */ onPointerDownOutside === null || onPointerDownOutside === void 0 ? void 0 : onPointerDownOutside(event); onInteractOutside === null || onInteractOutside === void 0 ? void 0 : onInteractOutside(event); /** * Add safeZone to prevent closing when interacting with trigger/anchor or its children. */ safeZone && handleOutsideEvent(event); /** * Both `onPointerDownOutside` and `onInteractOutside` are able to preventDefault the event, thus stopping call for `onDismiss`. */ if (!event.defaultPrevented && onDismiss) { onDismiss(); } }, ownerDocument); const focusOutside = (0, useFocusOutside_1.useFocusOutside)((event) => { if (!enabled) { return; } /** * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases. */ onFocusOutside === null || onFocusOutside === void 0 ? void 0 : onFocusOutside(event); onInteractOutside === null || onInteractOutside === void 0 ? void 0 : onInteractOutside(event); /** * Add safeZone to prevent closing when interacting with trigger/anchor or its children. */ safeZone && handleOutsideEvent(event); /** * Both `onFocusOutside` and `onInteractOutside` are able to preventDefault the event, thus stopping call for `onDismiss`. */ if (!event.defaultPrevented && onDismiss) { onDismiss(); } }, ownerDocument); (0, useEscapeKeydown_1.useEscapeKeydown)((event) => { if (!enabled) { return; } /** * The deepest nested element will always be last in the descendants list. * This allows us to only close the highest layer when pressing escape. * * In some cases a layer might still exist, but be disabled. We want to ignore these layers. */ const isHighestLayer = index === descendants.enabledCount() - 1; if (!isHighestLayer) { return; } /** * We call this before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases. */ onEscapeKeyDown === null || onEscapeKeyDown === void 0 ? void 0 : onEscapeKeyDown(event); /** * `onEscapeKeyDown` is able to preventDefault the event, thus stopping call for `onDismiss`. * We want to `preventDefault` the escape-event to avoid sideeffect from other elements on screen */ if (!event.defaultPrevented && onDismiss) { event.preventDefault(); onDismiss(); } }, ownerDocument); /** * If `disableOutsidePointerEvents` is true, * we want to disable pointer events on the body when the first layer is opened. */ // biome-ignore lint/correctness/useExhaustiveDependencies: Every time the descendants change, we want to update the body pointer events since we might have added or removed a layer. (0, react_1.useEffect)(() => { if (!node || !enabled || !disableOutsidePointerEvents) return; if (bodyLockCount === 0) { originalBodyPointerEvents = ownerDocument.body.style.pointerEvents; ownerDocument.body.style.pointerEvents = "none"; } bodyLockCount++; return () => { if (bodyLockCount === 1) { ownerDocument.body.style.pointerEvents = originalBodyPointerEvents; } bodyLockCount--; }; }, [ node, ownerDocument, disableOutsidePointerEvents, descendants, enabled, ]); /** * To make sure pointerEvents are enabled for all parents and siblings when the layer is removed from the DOM */ // biome-ignore lint/correctness/useExhaustiveDependencies: We explicitly want to run this on unmount, including every time the node updates to make sure we don't lock the application behind pointer-events: none. (0, react_1.useEffect)(() => { return () => descendants.values().forEach((x) => x.forceUpdate()); }, [descendants, node]); const Comp = asChild ? Slot_1.Slot : "div"; return (react_1.default.createElement(Comp, Object.assign({ ref: mergedRefs }, rest, { onFocusCapture: focusOutside.onFocusCapture, onBlurCapture: focusOutside.onBlurCapture, onPointerDownCapture: pointerDownOutside.onPointerDownCapture, style: Object.assign({ pointerEvents: pointerState.pointerStyle }, rest.style) }), children)); }); //# sourceMappingURL=DismissableLayer.js.map