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