@astryxdesign/core 0.4.0 → 0.4.1
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 +2 -2
- 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/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/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/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/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/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 +18 -17
- package/dist/astryx.umd.js +45 -45
- package/dist/astryx.umd.js.map +4 -4
- 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 +40 -0
- package/locales/pseudo.json +30 -0
- package/package.json +2 -2
- 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 +5 -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/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/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/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/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/TimeInput/TimeInput.test.tsx +54 -0
- package/src/TimeInput/TimeInput.tsx +17 -2
- package/src/Timestamp/Timestamp.test.tsx +91 -14
- package/src/Timestamp/Timestamp.tsx +14 -2
- package/src/Timestamp/formatInstant.ts +16 -1
- package/src/TopNav/TopNavMegaMenu.doc.mjs +5 -0
- 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/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/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
|
@@ -25,9 +25,11 @@ import {Icon} from '../Icon';
|
|
|
25
25
|
import {RadioIndicator} from '../Indicator';
|
|
26
26
|
import {InputGroup, InputGroupText} from '../InputGroup';
|
|
27
27
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
28
|
+
import {__resetInteractionModalityForTest} from '../utils/interactionModality';
|
|
28
29
|
import {defineTheme} from '../theme/defineTheme';
|
|
29
30
|
import {Theme} from '../theme/Theme';
|
|
30
31
|
import {generateThemeCSS} from '../theme/generateThemeRules';
|
|
32
|
+
import {spacingVars} from '../theme/tokens.stylex';
|
|
31
33
|
|
|
32
34
|
function generateThemeTestCSS(theme: Parameters<typeof generateThemeCSS>[0]) {
|
|
33
35
|
const {prose, component} = generateThemeCSS(theme);
|
|
@@ -508,6 +510,89 @@ describe('Selector', () => {
|
|
|
508
510
|
}
|
|
509
511
|
});
|
|
510
512
|
|
|
513
|
+
describe('menu clearance', () => {
|
|
514
|
+
it('clears the trigger by the standard menu offset when placement is explicit', async () => {
|
|
515
|
+
const user = userEvent.setup();
|
|
516
|
+
render(
|
|
517
|
+
<Selector
|
|
518
|
+
label="Fruit"
|
|
519
|
+
options={OPTIONS}
|
|
520
|
+
value="Banana"
|
|
521
|
+
onChange={() => {}}
|
|
522
|
+
placement="above"
|
|
523
|
+
/>,
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
await user.click(screen.getByRole('combobox'));
|
|
527
|
+
const popover = screen
|
|
528
|
+
.getByRole('listbox', {hidden: true})
|
|
529
|
+
.closest('[popover]') as HTMLElement;
|
|
530
|
+
// Both block edges, so the gap survives a position-try-fallbacks flip
|
|
531
|
+
// to the opposite side (#4803).
|
|
532
|
+
await waitFor(() => {
|
|
533
|
+
expect(popover.style.getPropertyValue('--x-marginBlockStart')).toBe(
|
|
534
|
+
spacingVars['--spacing-1'],
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
expect(popover.style.getPropertyValue('--x-marginBlockEnd')).toBe(
|
|
538
|
+
spacingVars['--spacing-1'],
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('clears the trigger in search mode', async () => {
|
|
543
|
+
const user = userEvent.setup();
|
|
544
|
+
render(
|
|
545
|
+
<Selector
|
|
546
|
+
label="Fruit"
|
|
547
|
+
options={OPTIONS}
|
|
548
|
+
value="Banana"
|
|
549
|
+
onChange={() => {}}
|
|
550
|
+
hasSearch
|
|
551
|
+
/>,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// In hasSearch mode the trigger is a plain button, not a combobox.
|
|
555
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
556
|
+
const popover = screen
|
|
557
|
+
.getByRole('listbox', {hidden: true})
|
|
558
|
+
.closest('[popover]') as HTMLElement;
|
|
559
|
+
await waitFor(() => {
|
|
560
|
+
expect(popover.style.getPropertyValue('--x-marginBlockStart')).toBe(
|
|
561
|
+
spacingVars['--spacing-1'],
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('stays flush in the default selected-item overlay', async () => {
|
|
567
|
+
const restoreRects = mockSelectorRects();
|
|
568
|
+
const user = userEvent.setup();
|
|
569
|
+
try {
|
|
570
|
+
render(
|
|
571
|
+
<Selector
|
|
572
|
+
label="Fruit"
|
|
573
|
+
options={OPTIONS}
|
|
574
|
+
value="Banana"
|
|
575
|
+
onChange={() => {}}
|
|
576
|
+
/>,
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
await user.click(screen.getByRole('combobox'));
|
|
580
|
+
const popover = screen
|
|
581
|
+
.getByRole('listbox', {hidden: true})
|
|
582
|
+
.closest('[popover]') as HTMLElement;
|
|
583
|
+
await waitFor(() => {
|
|
584
|
+
expect(popover.getAttribute('style')).toContain(
|
|
585
|
+
'margin-block-start: -110px',
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
expect(popover.style.getPropertyValue('--x-marginBlockStart')).toBe('');
|
|
589
|
+
expect(popover.style.getPropertyValue('--x-marginBlockEnd')).toBe('');
|
|
590
|
+
} finally {
|
|
591
|
+
restoreRects();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
511
596
|
describe('hasClear', () => {
|
|
512
597
|
it('shows selected value label when hasClear is enabled', () => {
|
|
513
598
|
render(
|
|
@@ -2302,7 +2387,165 @@ describe('Selector indicator (chevron) icon theme target', () => {
|
|
|
2302
2387
|
});
|
|
2303
2388
|
});
|
|
2304
2389
|
|
|
2390
|
+
describe('Selector section headings', () => {
|
|
2391
|
+
it('renders a section title as a plain heading inside the group, not a divider', async () => {
|
|
2392
|
+
const user = userEvent.setup();
|
|
2393
|
+
render(
|
|
2394
|
+
<Selector
|
|
2395
|
+
label="Fruit"
|
|
2396
|
+
options={[
|
|
2397
|
+
{
|
|
2398
|
+
type: 'section',
|
|
2399
|
+
title: 'Citrus',
|
|
2400
|
+
options: [
|
|
2401
|
+
{value: 'orange', label: 'Orange'},
|
|
2402
|
+
{value: 'lemon', label: 'Lemon'},
|
|
2403
|
+
],
|
|
2404
|
+
},
|
|
2405
|
+
]}
|
|
2406
|
+
value={undefined}
|
|
2407
|
+
onChange={() => {}}
|
|
2408
|
+
/>,
|
|
2409
|
+
);
|
|
2410
|
+
await user.click(screen.getByRole('combobox'));
|
|
2411
|
+
|
|
2412
|
+
// A labeled Divider used to stand in for the heading; it rendered a
|
|
2413
|
+
// role="separator" as a direct child of the listbox and stacked a second
|
|
2414
|
+
// rule under the search row's own.
|
|
2415
|
+
expect(document.querySelectorAll('[role="separator"]')).toHaveLength(0);
|
|
2416
|
+
|
|
2417
|
+
const group = screen.getByRole('group', {name: 'Citrus', hidden: true});
|
|
2418
|
+
const heading = group.querySelector('.astryx-selector-section-heading');
|
|
2419
|
+
expect(heading).toBeTruthy();
|
|
2420
|
+
expect(heading).toHaveTextContent('Citrus');
|
|
2421
|
+
// The group already carries the title as its accessible name, so the
|
|
2422
|
+
// visible heading must not announce it a second time.
|
|
2423
|
+
expect(heading).toHaveAttribute('aria-hidden', 'true');
|
|
2424
|
+
});
|
|
2425
|
+
});
|
|
2426
|
+
|
|
2427
|
+
describe('Selector search focus ring', () => {
|
|
2428
|
+
// The ring is for keyboard focus only. `:focus-visible` cannot express that
|
|
2429
|
+
// on its own: per CSS Selectors 4 a pointer-focused text input matches it
|
|
2430
|
+
// too (verified in Chromium), which is why a modality gate sits alongside
|
|
2431
|
+
// it. jsdom does not implement `:focus-visible`, so these assert the gate;
|
|
2432
|
+
// the painted ring is verified in real Chromium.
|
|
2433
|
+
//
|
|
2434
|
+
// Focus moves into the search input on the frame after the panel opens, and
|
|
2435
|
+
// the gate is read at that moment: every case must wait for the focus to
|
|
2436
|
+
// land before asserting or typing, or a slow frame reads the modality of
|
|
2437
|
+
// whatever the test did next.
|
|
2438
|
+
beforeEach(() => {
|
|
2439
|
+
__resetInteractionModalityForTest();
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
const field = () =>
|
|
2443
|
+
screen.getByRole('combobox', {hidden: true}).parentElement;
|
|
2444
|
+
|
|
2445
|
+
const waitForSearchFocus = async () =>
|
|
2446
|
+
waitFor(() =>
|
|
2447
|
+
expect(screen.getByRole('combobox', {hidden: true})).toHaveFocus(),
|
|
2448
|
+
);
|
|
2449
|
+
|
|
2450
|
+
it('does not ring when the panel is opened by mouse', async () => {
|
|
2451
|
+
const user = userEvent.setup();
|
|
2452
|
+
render(
|
|
2453
|
+
<Selector
|
|
2454
|
+
label="Fruit"
|
|
2455
|
+
options={OPTIONS}
|
|
2456
|
+
value={undefined}
|
|
2457
|
+
onChange={() => {}}
|
|
2458
|
+
hasSearch
|
|
2459
|
+
/>,
|
|
2460
|
+
);
|
|
2461
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2462
|
+
await waitForSearchFocus();
|
|
2463
|
+
expect(field()).not.toHaveAttribute('data-keyboard-focus');
|
|
2464
|
+
});
|
|
2465
|
+
|
|
2466
|
+
it('does not ring when the query is typed after a mouse open', async () => {
|
|
2467
|
+
const user = userEvent.setup();
|
|
2468
|
+
render(
|
|
2469
|
+
<Selector
|
|
2470
|
+
label="Fruit"
|
|
2471
|
+
options={OPTIONS}
|
|
2472
|
+
value={undefined}
|
|
2473
|
+
onChange={() => {}}
|
|
2474
|
+
hasSearch
|
|
2475
|
+
/>,
|
|
2476
|
+
);
|
|
2477
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2478
|
+
await waitForSearchFocus();
|
|
2479
|
+
await user.keyboard('an');
|
|
2480
|
+
// Typing does not retroactively make a pointer focus a keyboard one; the
|
|
2481
|
+
// caret already shows where the text is going.
|
|
2482
|
+
expect(field()).not.toHaveAttribute('data-keyboard-focus');
|
|
2483
|
+
});
|
|
2484
|
+
|
|
2485
|
+
it('rings when the panel is opened from the keyboard', async () => {
|
|
2486
|
+
const user = userEvent.setup();
|
|
2487
|
+
render(
|
|
2488
|
+
<Selector
|
|
2489
|
+
label="Fruit"
|
|
2490
|
+
options={OPTIONS}
|
|
2491
|
+
value={undefined}
|
|
2492
|
+
onChange={() => {}}
|
|
2493
|
+
hasSearch
|
|
2494
|
+
/>,
|
|
2495
|
+
);
|
|
2496
|
+
await user.tab();
|
|
2497
|
+
await user.keyboard('{Enter}');
|
|
2498
|
+
await waitForSearchFocus();
|
|
2499
|
+
expect(field()).toHaveAttribute('data-keyboard-focus', 'true');
|
|
2500
|
+
});
|
|
2501
|
+
});
|
|
2502
|
+
|
|
2305
2503
|
describe('Selector search affordances', () => {
|
|
2504
|
+
it('renders the search row seamlessly — no nested input box, a divider under it', async () => {
|
|
2505
|
+
const user = userEvent.setup();
|
|
2506
|
+
const {container} = render(
|
|
2507
|
+
<Selector
|
|
2508
|
+
label="Fruit"
|
|
2509
|
+
options={OPTIONS}
|
|
2510
|
+
value="Apple"
|
|
2511
|
+
onChange={() => {}}
|
|
2512
|
+
hasSearch
|
|
2513
|
+
/>,
|
|
2514
|
+
);
|
|
2515
|
+
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2516
|
+
|
|
2517
|
+
const search = screen.getByRole('combobox', {hidden: true});
|
|
2518
|
+
// The row is the outer gutter; the input sits inside the rounded field.
|
|
2519
|
+
const row = search.closest('.astryx-selector-search');
|
|
2520
|
+
const field = search.parentElement;
|
|
2521
|
+
if (!row || !field) {
|
|
2522
|
+
throw new Error('search row not found');
|
|
2523
|
+
}
|
|
2524
|
+
// The panel is already a bordered surface: the field inside it must not be
|
|
2525
|
+
// a second bordered box (this used to render a TextInput).
|
|
2526
|
+
expect(row).not.toHaveClass('astryx-text-input');
|
|
2527
|
+
expect(search.closest('.astryx-text-input')).toBeNull();
|
|
2528
|
+
// The field is a rounded box inset from the panel edge, shaped like the
|
|
2529
|
+
// option rows under it — not a full-bleed header strip.
|
|
2530
|
+
expect(field).not.toBe(row);
|
|
2531
|
+
expect(getComputedStyle(field).borderRadius).not.toBe('');
|
|
2532
|
+
// ...and a divider separates it from the options.
|
|
2533
|
+
const separator =
|
|
2534
|
+
container.ownerDocument.querySelector('[role="separator"]');
|
|
2535
|
+
if (!separator) {
|
|
2536
|
+
throw new Error('divider not found');
|
|
2537
|
+
}
|
|
2538
|
+
// Order: row, then divider, then the listbox.
|
|
2539
|
+
expect(
|
|
2540
|
+
row.compareDocumentPosition(separator) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2541
|
+
).toBeTruthy();
|
|
2542
|
+
const listbox = screen.getByRole('listbox', {hidden: true});
|
|
2543
|
+
expect(
|
|
2544
|
+
separator.compareDocumentPosition(listbox) &
|
|
2545
|
+
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2546
|
+
).toBeTruthy();
|
|
2547
|
+
});
|
|
2548
|
+
|
|
2306
2549
|
it('renders a decorative (aria-hidden) magnifier icon whenever hasSearch is on', async () => {
|
|
2307
2550
|
const user = userEvent.setup();
|
|
2308
2551
|
render(
|
|
@@ -2316,8 +2559,7 @@ describe('Selector search affordances', () => {
|
|
|
2316
2559
|
);
|
|
2317
2560
|
await user.click(screen.getByRole('button', {name: 'Fruit'}));
|
|
2318
2561
|
const search = screen.getByRole('combobox', {hidden: true});
|
|
2319
|
-
// The
|
|
2320
|
-
// sits inside the input container as a sibling of the <input>.
|
|
2562
|
+
// The magnifier leads the search row, as a sibling of the <input>.
|
|
2321
2563
|
const container = search.parentElement;
|
|
2322
2564
|
const magnifier = container?.querySelector('.astryx-icon');
|
|
2323
2565
|
expect(magnifier).toBeTruthy();
|
|
@@ -2550,3 +2792,145 @@ describe('Selector disabled state theme target', () => {
|
|
|
2550
2792
|
expect(css).toContain('opacity: 0.4');
|
|
2551
2793
|
});
|
|
2552
2794
|
});
|
|
2795
|
+
|
|
2796
|
+
describe('Selector indicatorPosition', () => {
|
|
2797
|
+
const openRows = (): HTMLElement[] => screen.getAllByRole('option', h);
|
|
2798
|
+
const rowFor = (label: string): HTMLElement =>
|
|
2799
|
+
openRows().find(row => row.textContent?.includes(label))!;
|
|
2800
|
+
|
|
2801
|
+
it('draws the mark after the option content by default', () => {
|
|
2802
|
+
render(
|
|
2803
|
+
<Selector
|
|
2804
|
+
label="Fruit"
|
|
2805
|
+
options={OPTIONS}
|
|
2806
|
+
value="Banana"
|
|
2807
|
+
onChange={() => {}}
|
|
2808
|
+
isDefaultOpen
|
|
2809
|
+
/>,
|
|
2810
|
+
);
|
|
2811
|
+
const row = rowFor('Banana');
|
|
2812
|
+
const mark = row.querySelector('.astryx-selector-check')!;
|
|
2813
|
+
const content = row.querySelector('.astryx-selector-option')!;
|
|
2814
|
+
expect(
|
|
2815
|
+
content.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
2816
|
+
).toBeTruthy();
|
|
2817
|
+
});
|
|
2818
|
+
|
|
2819
|
+
it('draws the mark before the option content when set to start', () => {
|
|
2820
|
+
render(
|
|
2821
|
+
<Selector
|
|
2822
|
+
label="Fruit"
|
|
2823
|
+
options={OPTIONS}
|
|
2824
|
+
value="Banana"
|
|
2825
|
+
onChange={() => {}}
|
|
2826
|
+
indicatorPosition="start"
|
|
2827
|
+
isDefaultOpen
|
|
2828
|
+
/>,
|
|
2829
|
+
);
|
|
2830
|
+
const row = rowFor('Banana');
|
|
2831
|
+
const mark = row.querySelector('.astryx-selector-check')!;
|
|
2832
|
+
const content = row.querySelector('.astryx-selector-option')!;
|
|
2833
|
+
expect(
|
|
2834
|
+
content.compareDocumentPosition(mark) & Node.DOCUMENT_POSITION_PRECEDING,
|
|
2835
|
+
).toBeTruthy();
|
|
2836
|
+
});
|
|
2837
|
+
|
|
2838
|
+
it('reserves the mark column on every row, at either position', () => {
|
|
2839
|
+
// The default check draws nothing when unchecked, so without a reserved
|
|
2840
|
+
// column the chosen row would be laid out differently from the rest —
|
|
2841
|
+
// indented at the start, truncating earlier at the end. Every row is two
|
|
2842
|
+
// children wide either way, so a row's geometry does not depend on whether
|
|
2843
|
+
// it happens to be the chosen one.
|
|
2844
|
+
const {unmount} = render(
|
|
2845
|
+
<Selector
|
|
2846
|
+
label="Fruit"
|
|
2847
|
+
options={OPTIONS}
|
|
2848
|
+
value="Banana"
|
|
2849
|
+
onChange={() => {}}
|
|
2850
|
+
indicatorPosition="start"
|
|
2851
|
+
isDefaultOpen
|
|
2852
|
+
/>,
|
|
2853
|
+
);
|
|
2854
|
+
for (const row of openRows()) {
|
|
2855
|
+
expect(row.children).toHaveLength(2);
|
|
2856
|
+
}
|
|
2857
|
+
unmount();
|
|
2858
|
+
|
|
2859
|
+
render(
|
|
2860
|
+
<Selector
|
|
2861
|
+
label="Fruit"
|
|
2862
|
+
options={OPTIONS}
|
|
2863
|
+
value="Banana"
|
|
2864
|
+
onChange={() => {}}
|
|
2865
|
+
isDefaultOpen
|
|
2866
|
+
/>,
|
|
2867
|
+
);
|
|
2868
|
+
for (const row of openRows()) {
|
|
2869
|
+
expect(row.children).toHaveLength(2);
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
|
|
2873
|
+
it('positions a themed replacement indicator the same way', () => {
|
|
2874
|
+
const theme = defineTheme({
|
|
2875
|
+
name: 'selector-start-radio-mark-test',
|
|
2876
|
+
indicators: {check: RadioIndicator},
|
|
2877
|
+
});
|
|
2878
|
+
render(
|
|
2879
|
+
<Theme theme={theme}>
|
|
2880
|
+
<Selector
|
|
2881
|
+
label="Fruit"
|
|
2882
|
+
options={OPTIONS}
|
|
2883
|
+
value="Banana"
|
|
2884
|
+
onChange={() => {}}
|
|
2885
|
+
indicatorPosition="start"
|
|
2886
|
+
isDefaultOpen
|
|
2887
|
+
/>
|
|
2888
|
+
</Theme>,
|
|
2889
|
+
);
|
|
2890
|
+
for (const row of openRows()) {
|
|
2891
|
+
const radio = row.querySelector('.astryx-radio')!;
|
|
2892
|
+
const content = row.querySelector('.astryx-selector-option')!;
|
|
2893
|
+
expect(
|
|
2894
|
+
content.compareDocumentPosition(radio) &
|
|
2895
|
+
Node.DOCUMENT_POSITION_PRECEDING,
|
|
2896
|
+
).toBeTruthy();
|
|
2897
|
+
}
|
|
2898
|
+
});
|
|
2899
|
+
});
|
|
2900
|
+
|
|
2901
|
+
describe('Selector popup theme target', () => {
|
|
2902
|
+
// The surface is the same element whether or not the popup has a search
|
|
2903
|
+
// field — which is the reason the target lives there. Rendered on the
|
|
2904
|
+
// component's own content, it would land on the listbox in one branch and
|
|
2905
|
+
// on a wrapper in the other, so one theme rule would style two different
|
|
2906
|
+
// boxes.
|
|
2907
|
+
it.each([
|
|
2908
|
+
['without search', false],
|
|
2909
|
+
['with search', true],
|
|
2910
|
+
])(
|
|
2911
|
+
'puts astryx-selector-popup on the painting surface, %s',
|
|
2912
|
+
async (_label, hasSearch) => {
|
|
2913
|
+
const user = userEvent.setup();
|
|
2914
|
+
render(
|
|
2915
|
+
<Selector
|
|
2916
|
+
label="Fruit"
|
|
2917
|
+
options={['Apple', 'Banana']}
|
|
2918
|
+
value="Apple"
|
|
2919
|
+
onChange={() => {}}
|
|
2920
|
+
hasSearch={hasSearch}
|
|
2921
|
+
/>,
|
|
2922
|
+
);
|
|
2923
|
+
// The trigger is a combobox in the plain variant and a listbox-popup
|
|
2924
|
+
// button in the search variant; the surface is the same either way.
|
|
2925
|
+
await user.click(
|
|
2926
|
+
screen.queryByRole('combobox') ??
|
|
2927
|
+
screen.getByRole('button', {name: /Fruit/}),
|
|
2928
|
+
);
|
|
2929
|
+
|
|
2930
|
+
const popup = document.querySelector('.astryx-selector-popup');
|
|
2931
|
+
expect(popup).not.toBeNull();
|
|
2932
|
+
expect(popup).toHaveClass('astryx-popover-surface');
|
|
2933
|
+
expect(popup?.querySelector('[role="listbox"]')).not.toBeNull();
|
|
2934
|
+
},
|
|
2935
|
+
);
|
|
2936
|
+
});
|