@astryxdesign/core 0.4.0 → 0.4.1-canary.1ce0b4c
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/CHANGELOG.md +39 -0
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +1 -2
- package/dist/AspectRatio/index.d.ts.map +1 -1
- package/dist/AspectRatio/index.js +1 -2
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +1 -2
- package/dist/Badge/index.d.ts.map +1 -1
- package/dist/Badge/index.js +1 -2
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +17 -3
- package/dist/Blockquote/Blockquote.d.ts.map +1 -1
- package/dist/Blockquote/Blockquote.js +1 -2
- package/dist/Blockquote/index.d.ts.map +1 -1
- package/dist/Blockquote/index.js +1 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -9
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +1 -2
- package/dist/Card/index.d.ts.map +1 -1
- package/dist/Card/index.js +1 -2
- package/dist/Center/Center.d.ts.map +1 -1
- package/dist/Center/Center.js +1 -2
- package/dist/Center/index.d.ts.map +1 -1
- package/dist/Center/index.js +1 -2
- package/dist/Code/Code.d.ts.map +1 -1
- package/dist/Code/Code.js +1 -2
- package/dist/CommandPalette/CommandPalette.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPalette.js +5 -0
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +7 -8
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +23 -10
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +44 -16
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +32 -13
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuRadioGroup.js +10 -3
- package/dist/Field/PanelSearchInput.d.ts +81 -0
- package/dist/Field/PanelSearchInput.d.ts.map +1 -0
- package/dist/Field/PanelSearchInput.js +151 -0
- package/dist/FileInput/FileInput.d.ts.map +1 -1
- package/dist/FileInput/FileInput.js +21 -10
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +1 -2
- package/dist/Grid/index.d.ts.map +1 -1
- package/dist/Grid/index.js +1 -2
- package/dist/HoverCard/HoverCard.d.ts +1 -1
- package/dist/HoverCard/HoverCard.d.ts.map +1 -1
- package/dist/HoverCard/HoverCard.js +5 -13
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +6 -5
- package/dist/Icon/globalIconRegistry.d.ts +5 -4
- package/dist/Icon/globalIconRegistry.d.ts.map +1 -1
- package/dist/Icon/globalIconRegistry.js +5 -4
- package/dist/Indicator/index.d.ts +1 -1
- package/dist/Indicator/index.d.ts.map +1 -1
- package/dist/Indicator/types.d.ts +10 -0
- package/dist/Indicator/types.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.d.ts.map +1 -1
- package/dist/InputGroup/groupStyles.js +7 -2
- package/dist/Layer/layerHost.d.ts +24 -0
- package/dist/Layer/layerHost.d.ts.map +1 -0
- package/dist/Layer/layerHost.js +79 -0
- package/dist/Layer/useLayer.d.ts +14 -6
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +155 -29
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +24 -50
- package/dist/MultiSelector/MultiSelector.d.ts +8 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +134 -102
- package/dist/NavItem/navItemStyles.stylex.d.ts +17 -5
- package/dist/NavItem/navItemStyles.stylex.d.ts.map +1 -1
- package/dist/NavItem/navItemStyles.stylex.js +11 -5
- package/dist/Popover/usePopover.d.ts +15 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +14 -2
- package/dist/ProgressBar/ProgressBar.d.ts.map +1 -1
- package/dist/ProgressBar/ProgressBar.js +2 -3
- package/dist/Section/Section.d.ts.map +1 -1
- package/dist/Section/Section.js +1 -2
- package/dist/Section/index.d.ts.map +1 -1
- package/dist/Section/index.js +1 -2
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +135 -95
- package/dist/SideNav/SideNav.d.ts +7 -9
- package/dist/SideNav/SideNav.d.ts.map +1 -1
- package/dist/SideNav/SideNav.js +32 -5
- package/dist/SideNav/SideNavCollapseButton.d.ts +25 -9
- package/dist/SideNav/SideNavCollapseButton.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseButton.js +39 -15
- package/dist/SideNav/SideNavCollapseContext.d.ts +21 -0
- package/dist/SideNav/SideNavCollapseContext.d.ts.map +1 -1
- package/dist/SideNav/SideNavCollapseContext.js +16 -2
- package/dist/SideNav/SideNavHeading.d.ts.map +1 -1
- package/dist/SideNav/SideNavHeading.js +70 -24
- package/dist/SideNav/SideNavItem.d.ts +7 -2
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +114 -75
- package/dist/SideNav/SideNavSection.d.ts.map +1 -1
- package/dist/SideNav/SideNavSection.js +7 -16
- package/dist/SideNav/index.d.ts +1 -1
- package/dist/SideNav/index.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.d.ts.map +1 -1
- package/dist/Skeleton/Skeleton.js +1 -2
- package/dist/Skeleton/index.d.ts.map +1 -1
- package/dist/Skeleton/index.js +1 -2
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +2 -6
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +3 -3
- package/dist/Table/TableRow.d.ts +1 -1
- package/dist/Table/TableRow.d.ts.map +1 -1
- package/dist/Table/TableRow.js +4 -2
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +7 -2
- package/dist/Timestamp/Timestamp.d.ts.map +1 -1
- package/dist/Timestamp/Timestamp.js +14 -3
- package/dist/Timestamp/formatInstant.d.ts +11 -1
- package/dist/Timestamp/formatInstant.d.ts.map +1 -1
- package/dist/Timestamp/formatInstant.js +3 -1
- package/dist/TopNav/TopNavMegaMenu.js +1 -1
- package/dist/TopNav/TopNavMegaMenuItem.js +1 -1
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +2 -2
- package/dist/VisuallyHidden/VisuallyHidden.d.ts.map +1 -1
- package/dist/VisuallyHidden/VisuallyHidden.js +1 -2
- package/dist/VisuallyHidden/index.d.ts.map +1 -1
- package/dist/VisuallyHidden/index.js +1 -2
- package/dist/astryx.css +23 -24
- package/dist/astryx.umd.js +48 -48
- package/dist/astryx.umd.js.map +4 -4
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +16 -2
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +17 -4
- package/dist/theme/defineTheme.d.ts +2 -2
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/defineTheme.js +2 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/tokens.stylex.d.ts +12 -0
- package/dist/theme/tokens.stylex.d.ts.map +1 -1
- package/dist/theme/tokens.stylex.js +23 -0
- package/dist/utils/focusOutline.stylex.d.ts +34 -19
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +46 -33
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +5 -1
- package/dist/utils/interactionModality.d.ts +34 -0
- package/dist/utils/interactionModality.d.ts.map +1 -0
- package/dist/utils/interactionModality.js +70 -0
- package/locales/en.json +44 -0
- package/locales/pseudo.json +33 -0
- package/package.json +3 -3
- package/src/AppShell/AppShell.doc.mjs +3 -0
- package/src/AspectRatio/AspectRatio.tsx +0 -2
- package/src/AspectRatio/index.ts +0 -2
- package/src/Badge/Badge.tsx +0 -2
- package/src/Badge/index.ts +0 -2
- package/src/Banner/Banner.test.tsx +58 -1
- package/src/Banner/Banner.tsx +24 -5
- package/src/Blockquote/Blockquote.tsx +0 -2
- package/src/Blockquote/index.ts +0 -2
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.tsx +10 -7
- package/src/ButtonGroup/ButtonGroup.test.tsx +7 -4
- package/src/Card/Card.tsx +0 -2
- package/src/Card/index.ts +0 -2
- package/src/Center/Center.tsx +0 -2
- package/src/Center/index.ts +0 -2
- package/src/CheckboxInput/CheckboxInput.test.tsx +8 -3
- package/src/Code/Code.tsx +0 -2
- package/src/CommandPalette/CommandPalette.test.tsx +54 -0
- package/src/CommandPalette/CommandPalette.tsx +5 -0
- package/src/ComplexSelector/ComplexSelector.test.tsx +15 -6
- package/src/ComplexSelector/ComplexSelector.tsx +6 -10
- package/src/DateInput/DateInput.test.tsx +19 -0
- package/src/DateInput/DateInput.tsx +3 -6
- package/src/DateRangeInput/DateRangeInput.tsx +3 -9
- package/src/DateTimeInput/DateTimeInput.test.tsx +79 -1
- package/src/DateTimeInput/DateTimeInput.tsx +23 -9
- package/src/DropdownMenu/DropdownMenuRadioGroup.tsx +2 -1
- package/src/Field/PanelSearchInput.tsx +271 -0
- package/src/FileInput/FileInput.tsx +21 -8
- package/src/Grid/Grid.tsx +0 -2
- package/src/Grid/index.ts +0 -2
- package/src/HoverCard/HoverCard.doc.mjs +3 -3
- package/src/HoverCard/HoverCard.test.tsx +283 -55
- package/src/HoverCard/HoverCard.tsx +5 -13
- package/src/HoverCard/useHoverCard.tsx +8 -11
- package/src/Icon/globalIconRegistry.tsx +5 -4
- package/src/Indicator/Indicator.doc.mjs +1 -1
- package/src/Indicator/index.ts +1 -0
- package/src/Indicator/types.ts +11 -0
- package/src/InputGroup/groupStyles.ts +7 -8
- package/src/Layer/layerHost.test.ts +99 -0
- package/src/Layer/layerHost.ts +141 -0
- package/src/Layer/useLayer.doc.mjs +14 -4
- package/src/Layer/useLayer.test.tsx +318 -7
- package/src/Layer/useLayer.tsx +229 -35
- package/src/Lightbox/Lightbox.tsx +2 -11
- package/src/MultiSelector/MultiSelector.doc.mjs +9 -0
- package/src/MultiSelector/MultiSelector.test.tsx +135 -5
- package/src/MultiSelector/MultiSelector.tsx +170 -127
- package/src/NavItem/navItemStyles.stylex.ts +31 -5
- package/src/Popover/Popover.doc.mjs +2 -0
- package/src/Popover/usePopover.tsx +37 -4
- package/src/ProgressBar/ProgressBar.tsx +2 -8
- package/src/Section/Section.tsx +0 -2
- package/src/Section/index.ts +0 -2
- package/src/Selector/Selector.doc.mjs +10 -0
- package/src/Selector/Selector.test.tsx +386 -2
- package/src/Selector/Selector.tsx +182 -122
- package/src/SideNav/SideNav.doc.mjs +13 -4
- package/src/SideNav/SideNav.test.tsx +771 -2
- package/src/SideNav/SideNav.tsx +37 -16
- package/src/SideNav/SideNavCollapseButton.doc.mjs +28 -6
- package/src/SideNav/SideNavCollapseButton.tsx +67 -15
- package/src/SideNav/SideNavCollapseContext.ts +29 -9
- package/src/SideNav/SideNavHeading.tsx +28 -10
- package/src/SideNav/SideNavItem.doc.mjs +13 -0
- package/src/SideNav/SideNavItem.tsx +92 -84
- package/src/SideNav/SideNavSection.tsx +6 -20
- package/src/SideNav/index.ts +2 -0
- package/src/Skeleton/Skeleton.tsx +0 -2
- package/src/Skeleton/index.ts +0 -2
- package/src/Slider/Slider.tsx +2 -11
- package/src/Switch/Switch.tsx +10 -2
- package/src/Table/Table.test.tsx +31 -0
- package/src/Table/TableRow.tsx +6 -0
- package/src/Text/Text.doc.mjs +1 -1
- package/src/TimeInput/TimeInput.test.tsx +54 -0
- package/src/TimeInput/TimeInput.tsx +17 -2
- package/src/Timestamp/Timestamp.test.tsx +125 -30
- package/src/Timestamp/Timestamp.tsx +14 -2
- package/src/Timestamp/formatInstant.ts +16 -1
- package/src/Toolbar/Toolbar.test.tsx +14 -14
- package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +43 -0
- package/src/TopNav/TopNavMegaMenu.tsx +4 -1
- package/src/TopNav/TopNavMegaMenuItem.tsx +1 -1
- package/src/TopNav/TopNavMenu.test.tsx +41 -0
- package/src/TopNav/TopNavMenu.tsx +8 -2
- package/src/TreeList/TreeList.test.tsx +3 -2
- package/src/VisuallyHidden/VisuallyHidden.tsx +0 -2
- package/src/VisuallyHidden/index.ts +0 -2
- package/src/hooks/index.ts +5 -1
- package/src/hooks/useFocusTrap.test.tsx +16 -0
- package/src/hooks/useFocusTrap.ts +20 -2
- package/src/hooks/useInputStatusIcon.tsx +8 -8
- package/src/serverSafeComponents.test.ts +665 -0
- package/src/theme/defineTheme.ts +3 -0
- package/src/theme/generateThemeRules.test.ts +17 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/themingTargets.test.ts +152 -26
- package/src/theme/tokens.stylex.ts +18 -0
- package/src/theme/tokens.test.ts +37 -0
- package/src/utils/focusOutline.stylex.ts +37 -23
- package/src/utils/index.ts +4 -0
- package/src/utils/interactionModality.ts +74 -0
package/src/Layer/useLayer.tsx
CHANGED
|
@@ -25,7 +25,9 @@ import React, {
|
|
|
25
25
|
} from 'react';
|
|
26
26
|
import * as stylex from '@stylexjs/stylex';
|
|
27
27
|
import type {StyleXStyles} from '@stylexjs/stylex';
|
|
28
|
+
import {createPortal} from 'react-dom';
|
|
28
29
|
import {addAnchorName, removeAnchorName} from './anchorName';
|
|
30
|
+
import {resolveLayerPortalTarget} from './layerHost';
|
|
29
31
|
import {typographyVars} from '../theme/tokens.stylex';
|
|
30
32
|
|
|
31
33
|
const styles = stylex.create({
|
|
@@ -149,12 +151,11 @@ export interface ContextRenderProps {
|
|
|
149
151
|
/**
|
|
150
152
|
* HTML tag to render the popover container as.
|
|
151
153
|
*
|
|
152
|
-
* Defaults to `'div'`.
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* the same on either tag.
|
|
154
|
+
* Defaults to `'div'`. Context layers render an inert `<template>` marker at
|
|
155
|
+
* the JSX position. The marker's parent is checked before the requested
|
|
156
|
+
* container mounts there or portals outside ancestors that cannot safely
|
|
157
|
+
* contain it. The marker remains available to detect a new parent if the
|
|
158
|
+
* render call moves. With `lazyMount`, the first check waits until `show()`.
|
|
158
159
|
*
|
|
159
160
|
* @default 'div'
|
|
160
161
|
*/
|
|
@@ -222,6 +223,15 @@ interface BaseLayerOptions {
|
|
|
222
223
|
*/
|
|
223
224
|
export interface ContextLayerOptions extends BaseLayerOptions {
|
|
224
225
|
mode: 'context';
|
|
226
|
+
/**
|
|
227
|
+
* Defer mounting the final layer and resolving its inline/portal position
|
|
228
|
+
* until `show()` is requested. Hiding unmounts it while the inert marker
|
|
229
|
+
* remains at the JSX position. Use this when rich content must never enter
|
|
230
|
+
* an unsafe ancestor, even briefly, and does not need to exist while closed.
|
|
231
|
+
*
|
|
232
|
+
* @default false
|
|
233
|
+
*/
|
|
234
|
+
lazyMount?: boolean;
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
/**
|
|
@@ -314,16 +324,51 @@ function toCssLength(value: number | string): string {
|
|
|
314
324
|
return typeof value === 'number' ? `${value}px` : value;
|
|
315
325
|
}
|
|
316
326
|
|
|
327
|
+
interface ContextLayerMount {
|
|
328
|
+
/** Null means the marker's parent is safe and the layer stays inline. */
|
|
329
|
+
portalTarget: HTMLElement | null;
|
|
330
|
+
/** Logical writing context lost when moving outside an unsafe ancestor. */
|
|
331
|
+
portalStyle: React.CSSProperties;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function readPortalWritingContext(
|
|
335
|
+
element: HTMLElement,
|
|
336
|
+
portalTarget: HTMLElement,
|
|
337
|
+
): React.CSSProperties {
|
|
338
|
+
const view = element.ownerDocument.defaultView;
|
|
339
|
+
if (!view) {
|
|
340
|
+
return {};
|
|
341
|
+
}
|
|
342
|
+
const sourceStyle = view.getComputedStyle(element);
|
|
343
|
+
const targetStyle = view.getComputedStyle(portalTarget);
|
|
344
|
+
|
|
345
|
+
// Do not snapshot custom properties here. The portal target is the closest
|
|
346
|
+
// safe ancestor, so theme variables continue to inherit and update there.
|
|
347
|
+
// These two properties can be set on the unsafe chain itself and directly
|
|
348
|
+
// affect the logical anchor-positioning keywords used by the layer. Only
|
|
349
|
+
// override values the portal would actually lose; matching values should
|
|
350
|
+
// keep inheriting from the target so later direction changes remain live.
|
|
351
|
+
return {
|
|
352
|
+
...(sourceStyle.direction !== targetStyle.direction && {
|
|
353
|
+
direction: sourceStyle.direction as React.CSSProperties['direction'],
|
|
354
|
+
}),
|
|
355
|
+
...(sourceStyle.writingMode !== targetStyle.writingMode && {
|
|
356
|
+
writingMode:
|
|
357
|
+
sourceStyle.writingMode as React.CSSProperties['writingMode'],
|
|
358
|
+
}),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
317
362
|
/**
|
|
318
363
|
* Map logical placement/alignment to a CSS position-area value.
|
|
319
364
|
*
|
|
320
365
|
* Uses the self-* logical keyword family: the inline axis resolves against
|
|
321
|
-
* the popover's own
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
366
|
+
* the popover's own direction (inherited inline or preserved when portaled),
|
|
367
|
+
* so it mirrors in RTL without placement-specific JS. The block axis is
|
|
368
|
+
* direction-neutral but must come from the same keyword family — mixing
|
|
369
|
+
* physical `top` with `self-inline-*` produces an invalid position-area
|
|
370
|
+
* (computes to `none`, which pins the popover to the viewport corner because
|
|
371
|
+
* styles.base zeroes the UA margins).
|
|
327
372
|
*
|
|
328
373
|
* Note the plain logical family (`inline-start`, no `self-`) is NOT a
|
|
329
374
|
* substitute: it resolves against the containing block — the page root for
|
|
@@ -406,39 +451,122 @@ export function useLayer(
|
|
|
406
451
|
options: ContextLayerOptions | FixedLayerOptions,
|
|
407
452
|
): ContextLayerReturn | FixedLayerReturn {
|
|
408
453
|
const {mode, onShow, onHide, lightDismiss = false} = options;
|
|
454
|
+
const lazyMount = mode === 'context' ? (options.lazyMount ?? false) : false;
|
|
409
455
|
const id = useId();
|
|
410
456
|
const anchorId = `--astryx-layer-${id.replace(/:/g, '')}`;
|
|
411
457
|
|
|
412
458
|
const [isOpen, setIsOpen] = useState(false);
|
|
413
459
|
const popoverRef = useRef<HTMLElement | null>(null);
|
|
460
|
+
// The DOM element on which the current logical open state was applied.
|
|
461
|
+
// A portal target change replaces the popover element; retaining the old
|
|
462
|
+
// reference lets the ref callback recognize and reopen its replacement.
|
|
463
|
+
const openedPopoverRef = useRef<HTMLElement | null>(null);
|
|
414
464
|
const triggerRef = useRef<HTMLElement | null>(null);
|
|
465
|
+
// Context layers place a persistent inert marker at their real JSX position.
|
|
466
|
+
// Its parent tells us whether the final layer can stay inline or needs a
|
|
467
|
+
// corrective portal, including after the render call moves.
|
|
468
|
+
const sentinelRef = useRef<HTMLTemplateElement | null>(null);
|
|
469
|
+
const contextMountRef = useRef<ContextLayerMount | null>(null);
|
|
470
|
+
const [contextMount, setContextMount] = useState<ContextLayerMount | null>(
|
|
471
|
+
null,
|
|
472
|
+
);
|
|
473
|
+
// A show() that arrives before the final layer mounts is replayed when its
|
|
474
|
+
// popover ref attaches.
|
|
475
|
+
const pendingShowRef = useRef(false);
|
|
415
476
|
|
|
416
477
|
// Ref mirrors isOpen for synchronous reads inside show/hide.
|
|
417
478
|
// State drives re-renders; the ref lets the imperative calls avoid
|
|
418
479
|
// stale-closure reads of the previous isOpen value.
|
|
419
480
|
const isOpenRef = useRef(false);
|
|
420
481
|
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
482
|
+
const showPopoverElement = useCallback((popover: HTMLElement) => {
|
|
483
|
+
// Finding infra-4: the Popover API is unsupported on Safari <17 and
|
|
484
|
+
// Firefox <125. On those browsers `showPopover` does not exist, so fall
|
|
485
|
+
// back to plain visibility instead of throwing.
|
|
486
|
+
if (typeof popover.showPopover === 'function') {
|
|
487
|
+
// The trigger is passed as the popover's invoker `source`: a layer
|
|
488
|
+
// hosted away from its trigger then still takes its sequential focus
|
|
489
|
+
// order (and its popover nesting) from the trigger rather than from its
|
|
490
|
+
// own DOM position. Browsers without the option ignore it.
|
|
491
|
+
popover.showPopover({source: triggerRef.current ?? undefined});
|
|
492
|
+
} else {
|
|
493
|
+
popover.style.display = 'block';
|
|
494
|
+
}
|
|
495
|
+
openedPopoverRef.current = popover;
|
|
496
|
+
}, []);
|
|
497
|
+
|
|
498
|
+
const isCurrentContextPopover = useCallback(
|
|
499
|
+
(popover: HTMLElement): boolean => {
|
|
500
|
+
if (mode !== 'context') {
|
|
501
|
+
return true;
|
|
434
502
|
}
|
|
503
|
+
const mount = contextMountRef.current;
|
|
504
|
+
if (mount === null) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
const expectedParent =
|
|
508
|
+
mount.portalTarget ?? sentinelRef.current?.parentElement ?? null;
|
|
509
|
+
return popover.parentElement === expectedParent;
|
|
510
|
+
},
|
|
511
|
+
[mode],
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const requestContextMount = useCallback(() => {
|
|
515
|
+
if (mode !== 'context') {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const sentinel = sentinelRef.current;
|
|
520
|
+
const inlineParent = sentinel?.parentElement ?? null;
|
|
521
|
+
if (!sentinel || !inlineParent) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const portalTarget = resolveLayerPortalTarget(inlineParent);
|
|
526
|
+
const mount: ContextLayerMount = {
|
|
527
|
+
portalTarget,
|
|
528
|
+
portalStyle: portalTarget
|
|
529
|
+
? readPortalWritingContext(sentinel, portalTarget)
|
|
530
|
+
: {},
|
|
531
|
+
};
|
|
532
|
+
contextMountRef.current = mount;
|
|
533
|
+
setContextMount(mount);
|
|
534
|
+
}, [mode]);
|
|
535
|
+
|
|
536
|
+
const clearContextMount = useCallback(() => {
|
|
537
|
+
if (mode !== 'context' || !lazyMount) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
contextMountRef.current = null;
|
|
541
|
+
setContextMount(null);
|
|
542
|
+
}, [mode, lazyMount]);
|
|
543
|
+
|
|
544
|
+
const show = useCallback(() => {
|
|
545
|
+
// A context popover left over until React commits a previous hide must not
|
|
546
|
+
// be reopened. The synchronous mount ref is the source of truth.
|
|
547
|
+
const candidate = popoverRef.current;
|
|
548
|
+
const popover =
|
|
549
|
+
candidate && isCurrentContextPopover(candidate) ? candidate : null;
|
|
550
|
+
if (!popover) {
|
|
551
|
+
pendingShowRef.current = true;
|
|
552
|
+
requestContextMount();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (!isOpenRef.current) {
|
|
556
|
+
showPopoverElement(popover);
|
|
435
557
|
isOpenRef.current = true;
|
|
436
558
|
setIsOpen(true);
|
|
437
559
|
onShow?.();
|
|
438
560
|
}
|
|
439
|
-
}, [
|
|
561
|
+
}, [
|
|
562
|
+
onShow,
|
|
563
|
+
requestContextMount,
|
|
564
|
+
showPopoverElement,
|
|
565
|
+
isCurrentContextPopover,
|
|
566
|
+
]);
|
|
440
567
|
|
|
441
568
|
const hide = useCallback(() => {
|
|
569
|
+
pendingShowRef.current = false;
|
|
442
570
|
if (isOpenRef.current) {
|
|
443
571
|
const el = popoverRef.current;
|
|
444
572
|
// See finding infra-4 note in `show`: mirror the same guard on hide so
|
|
@@ -450,11 +578,13 @@ export function useLayer(
|
|
|
450
578
|
el.style.display = 'none';
|
|
451
579
|
}
|
|
452
580
|
}
|
|
581
|
+
openedPopoverRef.current = null;
|
|
453
582
|
isOpenRef.current = false;
|
|
454
583
|
setIsOpen(false);
|
|
455
584
|
onHide?.();
|
|
456
585
|
}
|
|
457
|
-
|
|
586
|
+
clearContextMount();
|
|
587
|
+
}, [onHide, clearContextMount]);
|
|
458
588
|
|
|
459
589
|
// Ref for trigger element (context mode only)
|
|
460
590
|
const ref: RefCallback<HTMLElement> | undefined =
|
|
@@ -488,12 +618,14 @@ export function useLayer(
|
|
|
488
618
|
(e: Event) => {
|
|
489
619
|
const toggleEvent = e as ToggleEvent;
|
|
490
620
|
if (toggleEvent.newState === 'closed' && isOpenRef.current) {
|
|
621
|
+
openedPopoverRef.current = null;
|
|
491
622
|
isOpenRef.current = false;
|
|
492
623
|
setIsOpen(false);
|
|
493
624
|
onHide?.();
|
|
625
|
+
clearContextMount();
|
|
494
626
|
}
|
|
495
627
|
},
|
|
496
|
-
[onHide],
|
|
628
|
+
[onHide, clearContextMount],
|
|
497
629
|
);
|
|
498
630
|
|
|
499
631
|
// Ref callback for popover element — sets up the `toggle` listener.
|
|
@@ -531,8 +663,40 @@ export function useLayer(
|
|
|
531
663
|
(el: HTMLElement | null) => {
|
|
532
664
|
popoverRef.current = el;
|
|
533
665
|
bindToggleListener(el, handleToggle);
|
|
666
|
+
if (el && pendingShowRef.current) {
|
|
667
|
+
pendingShowRef.current = false;
|
|
668
|
+
show();
|
|
669
|
+
} else if (
|
|
670
|
+
el &&
|
|
671
|
+
isOpenRef.current &&
|
|
672
|
+
openedPopoverRef.current !== el &&
|
|
673
|
+
isCurrentContextPopover(el)
|
|
674
|
+
) {
|
|
675
|
+
// Changing a portal target remounts the popover. Preserve the logical
|
|
676
|
+
// open state without firing onShow again for the replacement element.
|
|
677
|
+
showPopoverElement(el);
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
[
|
|
681
|
+
handleToggle,
|
|
682
|
+
bindToggleListener,
|
|
683
|
+
show,
|
|
684
|
+
showPopoverElement,
|
|
685
|
+
isCurrentContextPopover,
|
|
686
|
+
],
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
const sentinelRefCallback = useCallback(
|
|
690
|
+
(el: HTMLTemplateElement | null) => {
|
|
691
|
+
sentinelRef.current = el;
|
|
692
|
+
if (el && (!lazyMount || pendingShowRef.current || isOpenRef.current)) {
|
|
693
|
+
// The render call may have moved while the hook stayed mounted.
|
|
694
|
+
// Resolve again from the newly attached marker rather than reusing a
|
|
695
|
+
// portal target from its previous JSX position.
|
|
696
|
+
requestContextMount();
|
|
697
|
+
}
|
|
534
698
|
},
|
|
535
|
-
[
|
|
699
|
+
[lazyMount, requestContextMount],
|
|
536
700
|
);
|
|
537
701
|
|
|
538
702
|
// Re-bind when the handler identity changes while the element stays mounted,
|
|
@@ -556,6 +720,15 @@ export function useLayer(
|
|
|
556
720
|
// Render function for context mode
|
|
557
721
|
const renderContext = useCallback(
|
|
558
722
|
(children: ReactNode, props?: ContextRenderProps) => {
|
|
723
|
+
// Keep the marker mounted after resolving the layer. Apart from giving
|
|
724
|
+
// us the real JSX parent on first show, this lets its ref report when a
|
|
725
|
+
// persistent hook's render call moves to a different host.
|
|
726
|
+
const sentinel = <template ref={sentinelRefCallback} />;
|
|
727
|
+
|
|
728
|
+
if (contextMount === null) {
|
|
729
|
+
return <>{sentinel}</>;
|
|
730
|
+
}
|
|
731
|
+
|
|
559
732
|
const {
|
|
560
733
|
placement = 'above',
|
|
561
734
|
alignment = 'center',
|
|
@@ -598,10 +771,10 @@ export function useLayer(
|
|
|
598
771
|
? `${extraClassName} ${stylexResult.className ?? ''}`
|
|
599
772
|
: stylexResult.className;
|
|
600
773
|
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
|
|
774
|
+
// The marker gives us the actual JSX parent without mounting arbitrary
|
|
775
|
+
// children there. Safe positions preserve the existing DOM order and
|
|
776
|
+
// cascade; unsafe positions use the nearest corrective portal target.
|
|
777
|
+
const layer = (
|
|
605
778
|
<Container
|
|
606
779
|
ref={popoverRefCallback}
|
|
607
780
|
id={id}
|
|
@@ -609,14 +782,35 @@ export function useLayer(
|
|
|
609
782
|
aria-label={ariaLabel}
|
|
610
783
|
popover={lightDismiss ? 'auto' : 'manual'}
|
|
611
784
|
className={combinedClassName}
|
|
612
|
-
style={{
|
|
785
|
+
style={{
|
|
786
|
+
...stylexResult.style,
|
|
787
|
+
...anchorStyle,
|
|
788
|
+
...contextMount.portalStyle,
|
|
789
|
+
...extraStyle,
|
|
790
|
+
}}
|
|
613
791
|
onMouseEnter={onMouseEnter}
|
|
614
792
|
onMouseLeave={onMouseLeave}>
|
|
615
793
|
{children}
|
|
616
794
|
</Container>
|
|
617
795
|
);
|
|
796
|
+
|
|
797
|
+
return (
|
|
798
|
+
<>
|
|
799
|
+
{sentinel}
|
|
800
|
+
{contextMount.portalTarget
|
|
801
|
+
? createPortal(layer, contextMount.portalTarget)
|
|
802
|
+
: layer}
|
|
803
|
+
</>
|
|
804
|
+
);
|
|
618
805
|
},
|
|
619
|
-
[
|
|
806
|
+
[
|
|
807
|
+
anchorId,
|
|
808
|
+
contextMount,
|
|
809
|
+
id,
|
|
810
|
+
lightDismiss,
|
|
811
|
+
popoverRefCallback,
|
|
812
|
+
sentinelRefCallback,
|
|
813
|
+
],
|
|
620
814
|
);
|
|
621
815
|
|
|
622
816
|
// Render function for fixed mode
|
|
@@ -35,6 +35,7 @@ import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
|
|
|
35
35
|
import {mergeProps, mergeRefs, rtlStyles} from '../utils';
|
|
36
36
|
import type {BaseProps} from '../BaseProps';
|
|
37
37
|
import {themeProps} from '../utils/themeProps';
|
|
38
|
+
import {focusOutlineStyles} from '../utils/focusOutline.stylex';
|
|
38
39
|
import {useTranslator} from '../i18n';
|
|
39
40
|
|
|
40
41
|
/**
|
|
@@ -158,16 +159,6 @@ const styles = stylex.create({
|
|
|
158
159
|
'@media (hover: hover)': 'zoom-in',
|
|
159
160
|
},
|
|
160
161
|
},
|
|
161
|
-
zoomTarget: {
|
|
162
|
-
outline: {
|
|
163
|
-
default: 'none',
|
|
164
|
-
':focus-visible': `2px solid ${colorVars['--color-accent']}`,
|
|
165
|
-
},
|
|
166
|
-
outlineOffset: {
|
|
167
|
-
default: '0',
|
|
168
|
-
':focus-visible': '2px',
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
162
|
imageWrapperZoomed: {
|
|
172
163
|
cursor: 'grab',
|
|
173
164
|
},
|
|
@@ -641,7 +632,7 @@ export function Lightbox({
|
|
|
641
632
|
aria-label={isZoomTarget ? t('@astryx.lightbox.zoom') : undefined}
|
|
642
633
|
{...stylex.props(
|
|
643
634
|
styles.imageWrapper,
|
|
644
|
-
isZoomTarget &&
|
|
635
|
+
isZoomTarget && focusOutlineStyles.focusVisible,
|
|
645
636
|
!isVideo && hasZoom && !isZoomed && styles.imageWrapperZoomable,
|
|
646
637
|
!isVideo && isZoomed && styles.imageWrapperZoomed,
|
|
647
638
|
!isVideo && isDragging && styles.imageWrapperDragging,
|
|
@@ -27,6 +27,8 @@ export const docs = {
|
|
|
27
27
|
},
|
|
28
28
|
{className: 'astryx-multi-selector-clear-icon', deprecatedFor: 'input-clear-icon'},
|
|
29
29
|
{className: 'astryx-multi-selector-empty-state'},
|
|
30
|
+
{className: 'astryx-multi-selector-search'},
|
|
31
|
+
{className: 'astryx-multi-selector-section-heading'},
|
|
30
32
|
{
|
|
31
33
|
className: 'astryx-multi-selector-indicator-icon',
|
|
32
34
|
states: ['state'],
|
|
@@ -192,6 +194,13 @@ export const docs = {
|
|
|
192
194
|
description:
|
|
193
195
|
'Custom render function for each selectable option in the dropdown. Not called for dividers, sections, or the select-all row.',
|
|
194
196
|
},
|
|
197
|
+
{
|
|
198
|
+
name: 'indicatorPosition',
|
|
199
|
+
type: "'start' | 'end'",
|
|
200
|
+
description:
|
|
201
|
+
'Which edge of the option row carries the checkbox. end pushes it to the far edge of the row, including on the select-all row.',
|
|
202
|
+
default: "'start'",
|
|
203
|
+
},
|
|
195
204
|
{
|
|
196
205
|
name: 'width',
|
|
197
206
|
type: 'SizeValue',
|
|
@@ -1774,7 +1774,134 @@ describe('MultiSelector indicator (chevron) icon theme target', () => {
|
|
|
1774
1774
|
});
|
|
1775
1775
|
});
|
|
1776
1776
|
|
|
1777
|
+
describe('MultiSelector list structure', () => {
|
|
1778
|
+
it('does not draw a divider under select-all', async () => {
|
|
1779
|
+
const user = userEvent.setup();
|
|
1780
|
+
render(
|
|
1781
|
+
<MultiSelector
|
|
1782
|
+
label="Fruit"
|
|
1783
|
+
options={['Apple', 'Banana', 'Orange']}
|
|
1784
|
+
value={[]}
|
|
1785
|
+
onChange={() => {}}
|
|
1786
|
+
hasSelectAll
|
|
1787
|
+
/>,
|
|
1788
|
+
);
|
|
1789
|
+
await user.click(screen.getByRole('combobox'));
|
|
1790
|
+
// Select-all is the first row of the list, not a section of its own. No
|
|
1791
|
+
// option here declares a divider and there is no search row, so the panel
|
|
1792
|
+
// should contain no rule at all.
|
|
1793
|
+
expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
|
|
1794
|
+
const [first] = screen.getAllByRole('option', h);
|
|
1795
|
+
expect(first).toHaveTextContent('Select all');
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
it('renders a section title as a plain heading inside the group, not a divider', async () => {
|
|
1799
|
+
const user = userEvent.setup();
|
|
1800
|
+
render(
|
|
1801
|
+
<MultiSelector
|
|
1802
|
+
label="Fruit"
|
|
1803
|
+
options={[
|
|
1804
|
+
{
|
|
1805
|
+
type: 'section',
|
|
1806
|
+
title: 'Citrus',
|
|
1807
|
+
options: [
|
|
1808
|
+
{value: 'orange', label: 'Orange'},
|
|
1809
|
+
{value: 'lemon', label: 'Lemon'},
|
|
1810
|
+
],
|
|
1811
|
+
},
|
|
1812
|
+
]}
|
|
1813
|
+
value={[]}
|
|
1814
|
+
onChange={() => {}}
|
|
1815
|
+
/>,
|
|
1816
|
+
);
|
|
1817
|
+
await user.click(screen.getByRole('combobox'));
|
|
1818
|
+
|
|
1819
|
+
// A labeled Divider used to stand in for the heading; it rendered a
|
|
1820
|
+
// role="separator" as a direct child of the listbox and stacked a second
|
|
1821
|
+
// rule under the search row's own.
|
|
1822
|
+
expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
|
|
1823
|
+
|
|
1824
|
+
const group = screen.getByRole('group', {name: 'Citrus', ...h});
|
|
1825
|
+
const heading = group.querySelector(
|
|
1826
|
+
'.astryx-multi-selector-section-heading',
|
|
1827
|
+
);
|
|
1828
|
+
expect(heading).toBeTruthy();
|
|
1829
|
+
expect(heading).toHaveTextContent('Citrus');
|
|
1830
|
+
// The group already carries the title as its accessible name, so the
|
|
1831
|
+
// visible heading must not announce it a second time.
|
|
1832
|
+
expect(heading).toHaveAttribute('aria-hidden', 'true');
|
|
1833
|
+
// ...and it precedes the options it heads.
|
|
1834
|
+
const [firstOption] = within(group).getAllByRole('option', h);
|
|
1835
|
+
expect(
|
|
1836
|
+
heading!.compareDocumentPosition(firstOption) &
|
|
1837
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1838
|
+
).toBeTruthy();
|
|
1839
|
+
});
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1777
1842
|
describe('MultiSelector search affordances', () => {
|
|
1843
|
+
it('renders the search row seamlessly — no nested input box, a divider under it', async () => {
|
|
1844
|
+
const user = userEvent.setup();
|
|
1845
|
+
render(
|
|
1846
|
+
<MultiSelector
|
|
1847
|
+
label="Fruit"
|
|
1848
|
+
options={['Apple', 'Banana', 'Orange']}
|
|
1849
|
+
value={[]}
|
|
1850
|
+
onChange={() => {}}
|
|
1851
|
+
hasSearch
|
|
1852
|
+
/>,
|
|
1853
|
+
);
|
|
1854
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1855
|
+
|
|
1856
|
+
const search = screen.getByRole('combobox', h);
|
|
1857
|
+
// The row is the outer gutter; the input sits inside the rounded field.
|
|
1858
|
+
const row = search.closest('.astryx-multi-selector-search');
|
|
1859
|
+
const field = search.parentElement;
|
|
1860
|
+
if (!row || !field) {
|
|
1861
|
+
throw new Error('search row not found');
|
|
1862
|
+
}
|
|
1863
|
+
// The panel is already a bordered surface: the field inside it must not be
|
|
1864
|
+
// a second bordered box (this used to render a TextInput).
|
|
1865
|
+
expect(row).not.toHaveClass('astryx-text-input');
|
|
1866
|
+
expect(search.closest('.astryx-text-input')).toBeNull();
|
|
1867
|
+
// The field is a rounded box inset from the panel edge, shaped like the
|
|
1868
|
+
// option rows under it — not a full-bleed header strip.
|
|
1869
|
+
expect(field).not.toBe(row);
|
|
1870
|
+
// ...and a divider separates it from the options.
|
|
1871
|
+
const separator = document.querySelector('[role="separator"]');
|
|
1872
|
+
if (!separator) {
|
|
1873
|
+
throw new Error('divider not found');
|
|
1874
|
+
}
|
|
1875
|
+
// Order: row, then divider, then the listbox.
|
|
1876
|
+
expect(
|
|
1877
|
+
row.compareDocumentPosition(separator) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1878
|
+
).toBeTruthy();
|
|
1879
|
+
const listbox = screen.getByRole('listbox', h);
|
|
1880
|
+
expect(
|
|
1881
|
+
separator.compareDocumentPosition(listbox) &
|
|
1882
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
1883
|
+
).toBeTruthy();
|
|
1884
|
+
});
|
|
1885
|
+
|
|
1886
|
+
it('keeps the search row outside the scrolling option list', async () => {
|
|
1887
|
+
const user = userEvent.setup();
|
|
1888
|
+
render(
|
|
1889
|
+
<MultiSelector
|
|
1890
|
+
label="Fruit"
|
|
1891
|
+
options={['Apple', 'Banana', 'Orange']}
|
|
1892
|
+
value={[]}
|
|
1893
|
+
onChange={() => {}}
|
|
1894
|
+
hasSearch
|
|
1895
|
+
/>,
|
|
1896
|
+
);
|
|
1897
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1898
|
+
// The options scroll under the header rather than carrying it away, so the
|
|
1899
|
+
// field stays reachable in a long list.
|
|
1900
|
+
const search = screen.getByRole('combobox', h);
|
|
1901
|
+
const listbox = screen.getByRole('listbox', h);
|
|
1902
|
+
expect(listbox.contains(search)).toBe(false);
|
|
1903
|
+
});
|
|
1904
|
+
|
|
1778
1905
|
it('renders a decorative (aria-hidden) magnifier icon whenever hasSearch is on', async () => {
|
|
1779
1906
|
const user = userEvent.setup();
|
|
1780
1907
|
render(
|
|
@@ -1788,8 +1915,7 @@ describe('MultiSelector search affordances', () => {
|
|
|
1788
1915
|
);
|
|
1789
1916
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
1790
1917
|
const search = screen.getByRole('combobox', h);
|
|
1791
|
-
// The
|
|
1792
|
-
// sits inside the input container as a sibling of the <input>.
|
|
1918
|
+
// The magnifier leads the search row, as a sibling of the <input>.
|
|
1793
1919
|
const wrapper = search.parentElement;
|
|
1794
1920
|
const magnifier = wrapper?.querySelector('.astryx-icon');
|
|
1795
1921
|
expect(magnifier).toBeTruthy();
|
|
@@ -2088,7 +2214,7 @@ describe('MultiSelector dropdown option theme target', () => {
|
|
|
2088
2214
|
});
|
|
2089
2215
|
|
|
2090
2216
|
describe('MultiSelector popup theme target', () => {
|
|
2091
|
-
it('puts astryx-multi-selector-popup on the
|
|
2217
|
+
it('puts astryx-multi-selector-popup on the surface that paints, not the list inside it', async () => {
|
|
2092
2218
|
const user = userEvent.setup();
|
|
2093
2219
|
render(
|
|
2094
2220
|
<MultiSelector
|
|
@@ -2100,13 +2226,17 @@ describe('MultiSelector popup theme target', () => {
|
|
|
2100
2226
|
);
|
|
2101
2227
|
await user.click(screen.getByRole('combobox', {name: /Fruit/}));
|
|
2102
2228
|
|
|
2103
|
-
const layer = document.querySelector('[popover]') as HTMLElement;
|
|
2104
2229
|
const popup = document.querySelector(
|
|
2105
2230
|
'.astryx-multi-selector-popup',
|
|
2106
2231
|
) as HTMLElement;
|
|
2107
2232
|
expect(popup).not.toBeNull();
|
|
2233
|
+
expect(popup).toHaveClass('astryx-popover-surface');
|
|
2234
|
+
// The scrolling list is a descendant, not the target itself.
|
|
2235
|
+
expect(popup.querySelector('[role="listbox"]')).not.toBeNull();
|
|
2236
|
+
expect(popup.getAttribute('role')).toBeNull();
|
|
2237
|
+
|
|
2238
|
+
const layer = document.querySelector('[popover]') as HTMLElement;
|
|
2108
2239
|
expect(popup).not.toBe(layer);
|
|
2109
2240
|
expect(layer.contains(popup)).toBe(true);
|
|
2110
|
-
expect(popup.querySelector('[role="listbox"]')).not.toBeNull();
|
|
2111
2241
|
});
|
|
2112
2242
|
});
|