@navikt/ds-react
Version:
React components from the Norwegian Labour and Welfare Administration.
284 lines • 13.7 kB
JavaScript
;
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