UNPKG

@navikt/ds-react

Version:

React components from the Norwegian Labour and Welfare Administration.

284 lines 13.7 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; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DismissableLayer = void 0; const react_1 = __importStar(require("react")); const Slot_1 = require("../../slot/Slot"); const util_1 = require("../../util"); const composeEventHandlers_1 = require("../../util/composeEventHandlers"); const hooks_1 = require("../../util/hooks"); const owner_1 = require("../../util/owner"); const sort_layers_1 = require("./util/sort-layers"); const useEscapeKeydown_1 = require("./util/useEscapeKeydown"); const useFocusOutside_1 = require("./util/useFocusOutside"); const usePointerDownOutside_1 = require("./util/usePointerDownOutside"); const DismissableLayer = (0, react_1.forwardRef)((_a, forwardedRef) => { var { enabled = true } = _a, restProps = __rest(_a, ["enabled"]); if (!enabled) { const Component = restProps.asChild ? Slot_1.Slot : "div"; return (react_1.default.createElement(Component, Object.assign({}, (0, util_1.omit)(restProps, [ "asChild", "disableOutsidePointerEvents", "onDismiss", "onEscapeKeyDown", "onFocusOutside", "onInteractOutside", "onPointerDownOutside", "safeZone", ]), { ref: forwardedRef }))); } return react_1.default.createElement(DismissableLayerInternal, Object.assign({}, restProps, { ref: forwardedRef })); }); exports.DismissableLayer = DismissableLayer; const BranchedLayerContext = react_1.default.createContext(null); /* ------------------------ DismissableLayerInternal ------------------------ */ const CONTEXT_UPDATE_EVENT = "dismissableLayer.update"; let originalBodyPointerEvents; const DismissableLayerContext = react_1.default.createContext({ layers: new Set(), branchedLayers: new Map(), layersWithOutsidePointerEventsDisabled: new Set(), }); const DismissableLayerInternal = (0, react_1.forwardRef)((_a, forwardedRef) => { var { children, disableOutsidePointerEvents, onDismiss, onInteractOutside, onEscapeKeyDown, onFocusOutside, onPointerDownOutside, safeZone, asChild } = _a, restProps = __rest(_a, ["children", "disableOutsidePointerEvents", "onDismiss", "onInteractOutside", "onEscapeKeyDown", "onFocusOutside", "onPointerDownOutside", "safeZone", "asChild"]); const context = (0, react_1.useContext)(DismissableLayerContext); const [, forceRerender] = (0, react_1.useState)({}); const [node, setNode] = react_1.default.useState(null); const mergedRefs = (0, hooks_1.useMergeRefs)(forwardedRef, setNode); const ownerDoc = (0, owner_1.ownerDocument)(node); /* Layer handling */ const layers = (0, sort_layers_1.getSortedLayers)(context.layers, context.branchedLayers); const highestLayerWithOutsidePointerEventsDisabledIndex = findHighestLayerIndex(layers, context.layersWithOutsidePointerEventsDisabled); const index = node ? layers.indexOf(node) : -1; const isBodyPointerEventsDisabled = context.layersWithOutsidePointerEventsDisabled.size > 0; const shouldEnablePointerEvents = highestLayerWithOutsidePointerEventsDisabledIndex === -1 || index >= highestLayerWithOutsidePointerEventsDisabledIndex; /** * 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. */ function handleOutsideEvent(event) { if (!(safeZone === null || safeZone === void 0 ? void 0 : safeZone.anchor)) { return; } let hasPointerDownOutside = false; if (!event.defaultPrevented) { if (event.detail.originalEvent.type === "pointerdown") { hasPointerDownOutside = true; } } const target = event.target; const targetIsAnchor = safeZone.anchor.contains(target) || target === safeZone.anchor; if (targetIsAnchor) { 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" && hasPointerDownOutside) { event.preventDefault(); } } const pointerDownOutside = (0, usePointerDownOutside_1.usePointerDownOutside)((event) => { if (!shouldEnablePointerEvents) { return; } /** * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault. */ 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); if (!event.defaultPrevented && onDismiss) { onDismiss(); } }, ownerDoc); const focusOutside = (0, useFocusOutside_1.useFocusOutside)((event) => { /** * We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault. */ 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); if (!event.defaultPrevented && onDismiss) { onDismiss(); } }, ownerDoc); (0, useEscapeKeydown_1.useEscapeKeydown)((event) => { /** * The deepest nested element will always be last in the descendants list. * This allows us to only close the highest layer when pressing escape. */ const isHighestLayer = index === context.layers.size - 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(); } }, ownerDoc); /** * Handles registering `layers` and `layersWithOutsidePointerEventsDisabled`. */ (0, react_1.useEffect)(() => { if (!node) { return; } if (disableOutsidePointerEvents) { if (context.layersWithOutsidePointerEventsDisabled.size === 0) { originalBodyPointerEvents = ownerDoc.body.style.pointerEvents; ownerDoc.body.style.pointerEvents = "none"; } context.layersWithOutsidePointerEventsDisabled.add(node); } context.layers.add(node); dispatchUpdate(); return () => { if (disableOutsidePointerEvents && context.layersWithOutsidePointerEventsDisabled.size === 1) { ownerDoc.body.style.pointerEvents = originalBodyPointerEvents; } }; }, [node, disableOutsidePointerEvents, context, ownerDoc]); /** * We purposefully prevent combining this effect with the `disableOutsidePointerEvents` effect * because a change to `disableOutsidePointerEvents` would remove this layer from the stack * and add it to the end again so the layering order wouldn't be creation order. * We only want them to be removed from context stacks when unmounted. */ (0, react_1.useEffect)(() => { return () => { if (!node) { return; } context.layers.delete(node); context.layersWithOutsidePointerEventsDisabled.delete(node); dispatchUpdate(); }; }, [node, context]); const parentBranchedLayer = (0, react_1.useContext)(BranchedLayerContext); /** * Handles registering and unregistering branched (nested) layers. * When this layer has a parent, we register it as a child of the parent. */ (0, react_1.useEffect)(() => { if (!node || !parentBranchedLayer || node === parentBranchedLayer) { return; } if (!context.branchedLayers.has(parentBranchedLayer)) { context.branchedLayers.set(parentBranchedLayer, new Set()); } const branchedChildren = context.branchedLayers.get(parentBranchedLayer); branchedChildren.add(node); dispatchUpdate(); return () => { // Remove this node from the parent's children branchedChildren.delete(node); // If the parent has no more children, remove the parent from branchedLayers if (branchedChildren.size === 0) { context.branchedLayers.delete(parentBranchedLayer); } dispatchUpdate(); }; }, [node, parentBranchedLayer, context]); /** * Synchronizes layer state across all mounted `DismissableLayer` instances. * All layers re-render on every context change to recalculate their position and pointer-events. */ (0, react_1.useEffect)(() => { const handleUpdate = () => forceRerender({}); document.addEventListener(CONTEXT_UPDATE_EVENT, handleUpdate); return () => document.removeEventListener(CONTEXT_UPDATE_EVENT, handleUpdate); }, []); const Comp = asChild ? Slot_1.Slot : "div"; return (react_1.default.createElement(BranchedLayerContext.Provider, { value: node }, react_1.default.createElement(Comp, Object.assign({}, restProps, { ref: mergedRefs, style: Object.assign({ pointerEvents: isBodyPointerEventsDisabled ? shouldEnablePointerEvents ? "auto" : "none" : undefined }, restProps.style), onFocusCapture: (0, composeEventHandlers_1.composeEventHandlers)(restProps.onFocusCapture, focusOutside.onFocusCapture), onBlurCapture: (0, composeEventHandlers_1.composeEventHandlers)(restProps.onBlurCapture, focusOutside.onBlurCapture), onPointerDownCapture: (0, composeEventHandlers_1.composeEventHandlers)(restProps.onPointerDownCapture, pointerDownOutside.onPointerDownCapture) }), children))); }); /** * Dispatches a custom event to inform all `DismissableLayer` components to update. */ function dispatchUpdate() { const event = new CustomEvent(CONTEXT_UPDATE_EVENT); document.dispatchEvent(event); } /** * Returns the index of the last layer that is found in the given subset. * Returns -1 if no layers are found. */ function findHighestLayerIndex(orderedLayers, layersWithOutsidePointerEventsDisabled) { for (let i = orderedLayers.length - 1; i >= 0; i -= 1) { if (layersWithOutsidePointerEventsDisabled.has(orderedLayers[i])) { return i; } } return -1; } //# sourceMappingURL=DismissableLayer.js.map