@lumx/react 4.24.0 → 4.24.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +7 -2
- package/index.js +130 -59
- package/index.js.map +1 -1
- package/package.json +4 -3
package/index.d.ts
CHANGED
|
@@ -2487,13 +2487,18 @@ interface DialogProps extends GenericProps$1, HasCloseMode$1, BaseDialogProps {
|
|
|
2487
2487
|
size?: DialogSizes;
|
|
2488
2488
|
/** Z-axis position. */
|
|
2489
2489
|
zIndex?: number;
|
|
2490
|
-
/**
|
|
2490
|
+
/**
|
|
2491
|
+
* Additional props for the dialog container element.
|
|
2492
|
+
* Set `'aria-modal': false` to make the dialog non-modal: the page stays usable (no focus trap, no overlay,
|
|
2493
|
+
* no close on click away, rendered in place instead of in a portal) and escape only closes the dialog when the
|
|
2494
|
+
* focus is inside.
|
|
2495
|
+
*/
|
|
2491
2496
|
dialogProps?: GenericProps$1;
|
|
2492
2497
|
/** On close callback. */
|
|
2493
2498
|
onClose?(): void;
|
|
2494
2499
|
/** Callback called when the open animation starts and the close animation finishes. */
|
|
2495
2500
|
onVisibilityChange?(isVisible: boolean): void;
|
|
2496
|
-
/**
|
|
2501
|
+
/** Whether to disable the scroll on the body or not (ignored on a non-modal dialog: the page stays usable). */
|
|
2497
2502
|
disableBodyScroll?: boolean;
|
|
2498
2503
|
/** Children */
|
|
2499
2504
|
children?: React__default.ReactNode;
|
package/index.js
CHANGED
|
@@ -9260,6 +9260,64 @@ function getFirstAndLastFocusable(parentElement) {
|
|
|
9260
9260
|
return {};
|
|
9261
9261
|
}
|
|
9262
9262
|
|
|
9263
|
+
/**
|
|
9264
|
+
* Focus the zone element itself (last-resort fallback, e.g. an empty dialog).
|
|
9265
|
+
*
|
|
9266
|
+
* Adds a `tabindex="-1"` if needed to make the zone programmatically focusable, removed when the signal aborts.
|
|
9267
|
+
*
|
|
9268
|
+
* @param focusZoneElement The zone element.
|
|
9269
|
+
* @param signal AbortSignal used to remove the added `tabindex`.
|
|
9270
|
+
*/
|
|
9271
|
+
function focusZoneFallback(focusZoneElement, signal) {
|
|
9272
|
+
if (!focusZoneElement.hasAttribute('tabindex')) {
|
|
9273
|
+
focusZoneElement.setAttribute('tabindex', '-1');
|
|
9274
|
+
signal.addEventListener('abort', () => focusZoneElement.removeAttribute('tabindex'), {
|
|
9275
|
+
once: true
|
|
9276
|
+
});
|
|
9277
|
+
}
|
|
9278
|
+
focusZoneElement.focus({
|
|
9279
|
+
preventScroll: true
|
|
9280
|
+
});
|
|
9281
|
+
}
|
|
9282
|
+
|
|
9283
|
+
/**
|
|
9284
|
+
* Move the focus into the `focusZoneElement`:
|
|
9285
|
+
* 1. Focus `focusElement` if provided and contained in the zone.
|
|
9286
|
+
* 2. Otherwise focus the first focusable descendant.
|
|
9287
|
+
* 3. Otherwise focus the zone element itself (falling back to setting `tabindex="-1"` if needed) so that
|
|
9288
|
+
* keyboard users (especially screen reader users) land inside the zone (e.g. an empty dialog).
|
|
9289
|
+
*
|
|
9290
|
+
* @param options Initial focus configuration.
|
|
9291
|
+
* @param signal AbortSignal used to tear down (removes the fallback `tabindex` if it was added).
|
|
9292
|
+
*/
|
|
9293
|
+
function setupInitialFocus(options, signal) {
|
|
9294
|
+
const {
|
|
9295
|
+
focusZoneElement,
|
|
9296
|
+
focusElement
|
|
9297
|
+
} = options;
|
|
9298
|
+
if (!focusZoneElement || signal.aborted) {
|
|
9299
|
+
return;
|
|
9300
|
+
}
|
|
9301
|
+
if (focusElement && focusZoneElement.contains(focusElement)) {
|
|
9302
|
+
// Focus the given element.
|
|
9303
|
+
focusElement.focus({
|
|
9304
|
+
preventScroll: true
|
|
9305
|
+
});
|
|
9306
|
+
return;
|
|
9307
|
+
}
|
|
9308
|
+
const firstFocusable = getFirstAndLastFocusable(focusZoneElement).first;
|
|
9309
|
+
if (firstFocusable) {
|
|
9310
|
+
// Focus the first focusable descendant.
|
|
9311
|
+
firstFocusable.focus({
|
|
9312
|
+
preventScroll: true
|
|
9313
|
+
});
|
|
9314
|
+
return;
|
|
9315
|
+
}
|
|
9316
|
+
|
|
9317
|
+
// No focusable descendant — fall back to the zone itself.
|
|
9318
|
+
focusZoneFallback(focusZoneElement, signal);
|
|
9319
|
+
}
|
|
9320
|
+
|
|
9263
9321
|
/**
|
|
9264
9322
|
* Shared listener tower for focus traps.
|
|
9265
9323
|
*
|
|
@@ -9267,14 +9325,13 @@ function getFirstAndLastFocusable(parentElement) {
|
|
|
9267
9325
|
* registered trap is re-enabled.
|
|
9268
9326
|
*/
|
|
9269
9327
|
const FOCUS_TRAPS = makeListenerTowerContext();
|
|
9328
|
+
|
|
9329
|
+
/** Focus trap options: the zone in which to trap the focus, and the element to focus on activation. */
|
|
9330
|
+
|
|
9270
9331
|
/**
|
|
9271
9332
|
* Trap 'Tab' focus switch inside the `focusZoneElement`.
|
|
9272
9333
|
*
|
|
9273
|
-
* Setup behavior:
|
|
9274
|
-
* 1. Focus `focusElement` if provided and contained in the zone.
|
|
9275
|
-
* 2. Otherwise focus the first focusable descendant.
|
|
9276
|
-
* 3. Otherwise focus the zone element itself (falling back to setting `tabindex="-1"` if needed) so that
|
|
9277
|
-
* keyboard users (especially screen reader users) land inside the trapped region (e.g. an empty dialog).
|
|
9334
|
+
* Setup behavior: move the focus into the zone (see `setupInitialFocus`).
|
|
9278
9335
|
*
|
|
9279
9336
|
* Tab key behavior:
|
|
9280
9337
|
* - With at least one focusable descendant: focus cycles between the first and last focusable in the zone.
|
|
@@ -9287,8 +9344,7 @@ const FOCUS_TRAPS = makeListenerTowerContext();
|
|
|
9287
9344
|
*/
|
|
9288
9345
|
function setupFocusTrap(options, signal) {
|
|
9289
9346
|
const {
|
|
9290
|
-
focusZoneElement
|
|
9291
|
-
focusElement
|
|
9347
|
+
focusZoneElement
|
|
9292
9348
|
} = options;
|
|
9293
9349
|
if (!focusZoneElement || signal.aborted) {
|
|
9294
9350
|
return;
|
|
@@ -9297,25 +9353,6 @@ function setupFocusTrap(options, signal) {
|
|
|
9297
9353
|
// The root node is either the Document (regular DOM) or a ShadowRoot (shadow DOM portal).
|
|
9298
9354
|
const rootNode = focusZoneElement.getRootNode();
|
|
9299
9355
|
|
|
9300
|
-
// Track whether we added a `tabindex="-1"` so we can restore the original state on teardown.
|
|
9301
|
-
let addedTabIndex = false;
|
|
9302
|
-
|
|
9303
|
-
/** Make the zone element programmatically focusable (so we can fall back to it). */
|
|
9304
|
-
const ensureZoneIsFocusable = () => {
|
|
9305
|
-
if (!focusZoneElement.hasAttribute('tabindex')) {
|
|
9306
|
-
focusZoneElement.setAttribute('tabindex', '-1');
|
|
9307
|
-
addedTabIndex = true;
|
|
9308
|
-
}
|
|
9309
|
-
};
|
|
9310
|
-
|
|
9311
|
-
/** Focus the zone element itself as a last-resort fallback. */
|
|
9312
|
-
const focusZoneFallback = () => {
|
|
9313
|
-
ensureZoneIsFocusable();
|
|
9314
|
-
focusZoneElement.focus({
|
|
9315
|
-
preventScroll: true
|
|
9316
|
-
});
|
|
9317
|
-
};
|
|
9318
|
-
|
|
9319
9356
|
// Trap 'Tab' key down focus switch into the focus zone.
|
|
9320
9357
|
const trapTabFocusInFocusZone = evt => {
|
|
9321
9358
|
if (evt.key !== 'Tab') {
|
|
@@ -9326,7 +9363,7 @@ function setupFocusTrap(options, signal) {
|
|
|
9326
9363
|
// Prevent focus switch if no focusable available — pin focus on the zone itself.
|
|
9327
9364
|
if (!focusable.first) {
|
|
9328
9365
|
evt.preventDefault();
|
|
9329
|
-
focusZoneFallback();
|
|
9366
|
+
focusZoneFallback(focusZoneElement, signal);
|
|
9330
9367
|
return;
|
|
9331
9368
|
}
|
|
9332
9369
|
const {
|
|
@@ -9359,32 +9396,11 @@ function setupFocusTrap(options, signal) {
|
|
|
9359
9396
|
};
|
|
9360
9397
|
|
|
9361
9398
|
// SETUP: focus initial element.
|
|
9362
|
-
|
|
9363
|
-
// Focus the given element.
|
|
9364
|
-
focusElement.focus({
|
|
9365
|
-
preventScroll: true
|
|
9366
|
-
});
|
|
9367
|
-
} else {
|
|
9368
|
-
const firstFocusable = getFirstAndLastFocusable(focusZoneElement).first;
|
|
9369
|
-
if (firstFocusable) {
|
|
9370
|
-
// Focus the first focusable descendant.
|
|
9371
|
-
firstFocusable.focus({
|
|
9372
|
-
preventScroll: true
|
|
9373
|
-
});
|
|
9374
|
-
} else {
|
|
9375
|
-
// No focusable descendant — fall back to the zone itself (e.g. an empty dialog).
|
|
9376
|
-
focusZoneFallback();
|
|
9377
|
-
}
|
|
9378
|
-
}
|
|
9399
|
+
setupInitialFocus(options, signal);
|
|
9379
9400
|
FOCUS_TRAPS.register(focusTrap);
|
|
9380
9401
|
|
|
9381
9402
|
// TEARDOWN.
|
|
9382
|
-
signal.addEventListener('abort', () => {
|
|
9383
|
-
FOCUS_TRAPS.unregister(focusTrap);
|
|
9384
|
-
if (addedTabIndex) {
|
|
9385
|
-
focusZoneElement.removeAttribute('tabindex');
|
|
9386
|
-
}
|
|
9387
|
-
}, {
|
|
9403
|
+
signal.addEventListener('abort', () => FOCUS_TRAPS.unregister(focusTrap), {
|
|
9388
9404
|
once: true
|
|
9389
9405
|
});
|
|
9390
9406
|
}
|
|
@@ -12156,6 +12172,23 @@ const useTransitionVisibility = (ref, isComponentVisible, timeout, onVisibilityC
|
|
|
12156
12172
|
return isVisible || isComponentVisible;
|
|
12157
12173
|
};
|
|
12158
12174
|
|
|
12175
|
+
/**
|
|
12176
|
+
* Check if the focus is on the given element or inside it.
|
|
12177
|
+
*
|
|
12178
|
+
* Reads the active element of the element's own root node (Document or ShadowRoot), so it also works when the
|
|
12179
|
+
* element lives in a shadow DOM.
|
|
12180
|
+
*
|
|
12181
|
+
* @param element The element to check.
|
|
12182
|
+
* @return whether the active element is the element itself or one of its descendants.
|
|
12183
|
+
*/
|
|
12184
|
+
function isFocusWithin(element) {
|
|
12185
|
+
if (!element) return false;
|
|
12186
|
+
const {
|
|
12187
|
+
activeElement
|
|
12188
|
+
} = element.getRootNode();
|
|
12189
|
+
return Boolean(activeElement && element.contains(activeElement));
|
|
12190
|
+
}
|
|
12191
|
+
|
|
12159
12192
|
/**
|
|
12160
12193
|
* Dialog label id key in IdsRegistry
|
|
12161
12194
|
*/
|
|
@@ -12202,6 +12235,7 @@ const DialogShell = props => {
|
|
|
12202
12235
|
isVisible,
|
|
12203
12236
|
size = DEFAULT_PROPS$U.size,
|
|
12204
12237
|
zIndex,
|
|
12238
|
+
isModal = true,
|
|
12205
12239
|
children,
|
|
12206
12240
|
Portal,
|
|
12207
12241
|
HeadingLevelProvider,
|
|
@@ -12210,19 +12244,21 @@ const DialogShell = props => {
|
|
|
12210
12244
|
...forwardedProps
|
|
12211
12245
|
} = props;
|
|
12212
12246
|
return /*#__PURE__*/jsx(Portal, {
|
|
12247
|
+
enabled: isModal,
|
|
12213
12248
|
children: /*#__PURE__*/jsxs("div", {
|
|
12214
12249
|
ref: ref,
|
|
12215
12250
|
...forwardedProps,
|
|
12216
12251
|
className: classnames(className, block$N({
|
|
12217
12252
|
'is-hidden': !isOpen,
|
|
12218
12253
|
'is-loading': isLoading,
|
|
12254
|
+
'is-non-modal': !isModal,
|
|
12219
12255
|
'is-shown': isOpen || isVisible,
|
|
12220
12256
|
[`size-${size}`]: Boolean(size)
|
|
12221
12257
|
})),
|
|
12222
12258
|
style: {
|
|
12223
12259
|
zIndex
|
|
12224
12260
|
},
|
|
12225
|
-
children: [/*#__PURE__*/jsx("div", {
|
|
12261
|
+
children: [isModal && /*#__PURE__*/jsx("div", {
|
|
12226
12262
|
className: element$C('overlay')
|
|
12227
12263
|
}), /*#__PURE__*/jsx(HeadingLevelProvider, {
|
|
12228
12264
|
level: 2,
|
|
@@ -12260,6 +12296,14 @@ function resolveAccessibleNameProps(ariaLabel, ariaLabelledBy) {
|
|
|
12260
12296
|
* that has resolved `labelId` from the ids registry.
|
|
12261
12297
|
*/
|
|
12262
12298
|
|
|
12299
|
+
/**
|
|
12300
|
+
* Whether the dialog is modal: it is, unless `dialogProps['aria-modal']` is `false` (or `'false'`).
|
|
12301
|
+
*
|
|
12302
|
+
* @param dialogProps Props of the dialog container element.
|
|
12303
|
+
* @return whether the dialog is modal.
|
|
12304
|
+
*/
|
|
12305
|
+
const isDialogModal = dialogProps => dialogProps?.['aria-modal'] !== false && dialogProps?.['aria-modal'] !== 'false';
|
|
12306
|
+
|
|
12263
12307
|
/**
|
|
12264
12308
|
* Dialog content: the `role="dialog"` element + header/body/footer.
|
|
12265
12309
|
*
|
|
@@ -12301,14 +12345,17 @@ const DialogContent$1 = props => {
|
|
|
12301
12345
|
'aria-labelledby': dialogAriaLabelledBy,
|
|
12302
12346
|
...restDialogProps
|
|
12303
12347
|
} = dialogProps ?? {};
|
|
12348
|
+
const isModal = isDialogModal(restDialogProps);
|
|
12304
12349
|
return /*#__PURE__*/jsx("div", {
|
|
12305
12350
|
className: element$C('container'),
|
|
12306
12351
|
role: "dialog",
|
|
12307
12352
|
"aria-modal": "true",
|
|
12308
12353
|
...restDialogProps,
|
|
12309
12354
|
...resolveAccessibleNameProps(dialogAriaLabel, dialogAriaLabelledBy || labelId),
|
|
12310
|
-
children: /*#__PURE__*/jsx(ClickAwayProvider
|
|
12311
|
-
|
|
12355
|
+
children: /*#__PURE__*/jsx(ClickAwayProvider
|
|
12356
|
+
// A non-modal dialog leaves the page usable: clicking it must not close the dialog.
|
|
12357
|
+
, {
|
|
12358
|
+
callback: isModal && !shouldPreventCloseOnClickAway && handleClose,
|
|
12312
12359
|
childrenRefs: clickAwayRefs,
|
|
12313
12360
|
parentRef: rootRef,
|
|
12314
12361
|
children: /*#__PURE__*/jsxs("section", {
|
|
@@ -12534,28 +12581,51 @@ const DialogBody = forwardRef((props, ref) => {
|
|
|
12534
12581
|
preventCloseOnEscape,
|
|
12535
12582
|
...forwardedProps
|
|
12536
12583
|
} = props;
|
|
12584
|
+
const isModal = isDialogModal(dialogProps);
|
|
12585
|
+
const wrapperRef = useRef(null);
|
|
12537
12586
|
const previousOpen = React__default.useRef(isOpen);
|
|
12538
12587
|
React__default.useEffect(() => {
|
|
12539
12588
|
if (isOpen !== previousOpen.current) {
|
|
12540
12589
|
previousOpen.current = isOpen;
|
|
12541
12590
|
|
|
12542
12591
|
// Focus the parent element on close.
|
|
12543
|
-
|
|
12592
|
+
// Non-modal: only when the focus is inside the dialog (do not steal the focus from the page).
|
|
12593
|
+
if (!isOpen && parentElement?.current && (isModal || isFocusWithin(wrapperRef.current))) {
|
|
12544
12594
|
parentElement.current.focus();
|
|
12545
12595
|
}
|
|
12546
12596
|
}
|
|
12547
|
-
}, [isOpen, parentElement]);
|
|
12597
|
+
}, [isOpen, isModal, parentElement]);
|
|
12548
12598
|
const shouldPreventCloseOnEscape = preventAutoClose || preventCloseOnEscape;
|
|
12549
|
-
|
|
12550
|
-
const wrapperRef = useRef(null);
|
|
12599
|
+
|
|
12551
12600
|
/**
|
|
12552
12601
|
* Since the `contentRef` comes from the parent and is optional,
|
|
12553
12602
|
* we need to create a stable contentRef that will always be available.
|
|
12554
12603
|
*/
|
|
12555
12604
|
const localContentRef = useRef(null);
|
|
12605
|
+
|
|
12606
|
+
// Close on escape (a tooltip or popover opened inside registers after the dialog and so gets the escape first).
|
|
12607
|
+
// Non-modal: the page stays usable, so only close when the focus is inside the dialog.
|
|
12608
|
+
const onEscape = React__default.useCallback(() => {
|
|
12609
|
+
if (isModal || isFocusWithin(wrapperRef.current)) onClose?.();
|
|
12610
|
+
}, [isModal, onClose]);
|
|
12611
|
+
useCallbackOnEscape(onClose && onEscape, isOpen && !shouldPreventCloseOnEscape);
|
|
12612
|
+
|
|
12556
12613
|
// Handle focus trap.
|
|
12557
|
-
useFocusTrap(isOpen && wrapperRef.current, focusElement?.current);
|
|
12558
|
-
|
|
12614
|
+
useFocusTrap(isModal && isOpen && wrapperRef.current, focusElement?.current);
|
|
12615
|
+
|
|
12616
|
+
// Non-modal: without the focus trap, move the focus into the dialog on open ourselves.
|
|
12617
|
+
useEffect(() => {
|
|
12618
|
+
const wrapper = wrapperRef.current;
|
|
12619
|
+
if (isModal || !isOpen || !wrapper) return undefined;
|
|
12620
|
+
const controller = new AbortController();
|
|
12621
|
+
setupInitialFocus({
|
|
12622
|
+
focusZoneElement: wrapper,
|
|
12623
|
+
focusElement: focusElement?.current
|
|
12624
|
+
}, controller.signal);
|
|
12625
|
+
return () => controller.abort();
|
|
12626
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
12627
|
+
}, [isModal, isOpen]);
|
|
12628
|
+
useDisableBodyScroll(isModal && disableBodyScroll && isOpen && localContentRef.current);
|
|
12559
12629
|
const [sentinelTop, setSentinelTop] = useState(null);
|
|
12560
12630
|
const [sentinelBottom, setSentinelBottom] = useState(null);
|
|
12561
12631
|
const intersections = useIntersectionObserver([sentinelTop, sentinelBottom], {
|
|
@@ -12587,6 +12657,7 @@ const DialogBody = forwardRef((props, ref) => {
|
|
|
12587
12657
|
isVisible,
|
|
12588
12658
|
size,
|
|
12589
12659
|
zIndex,
|
|
12660
|
+
isModal,
|
|
12590
12661
|
ref: mergeRefs(rootRef, ref),
|
|
12591
12662
|
...forwardedProps,
|
|
12592
12663
|
children: /*#__PURE__*/jsx(DialogContent, {
|