@blotch/dolan 22.2.4 → 22.2.5

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.
Files changed (124) hide show
  1. package/dist/commands/add.test.js +2 -0
  2. package/dist/commands/add.test.js.map +1 -1
  3. package/dist/commands/doctor.js +78 -21
  4. package/dist/commands/doctor.js.map +1 -1
  5. package/dist/commands/doctor.test.js +167 -26
  6. package/dist/commands/doctor.test.js.map +1 -1
  7. package/dist/commands/init.test.js +4 -7
  8. package/dist/commands/init.test.js.map +1 -1
  9. package/dist/commands/update.d.ts +3 -1
  10. package/dist/commands/update.js +78 -15
  11. package/dist/commands/update.js.map +1 -1
  12. package/dist/commands/update.test.js +257 -2
  13. package/dist/commands/update.test.js.map +1 -1
  14. package/dist/lockfile.d.ts +10 -0
  15. package/dist/lockfile.js +20 -1
  16. package/dist/lockfile.js.map +1 -1
  17. package/dist/lockfile.test.js +43 -3
  18. package/dist/lockfile.test.js.map +1 -1
  19. package/dist/registry.js +73 -11
  20. package/dist/registry.js.map +1 -1
  21. package/dist/registry.test.js +16 -3
  22. package/dist/registry.test.js.map +1 -1
  23. package/dist/test-utils.d.ts +24 -2
  24. package/dist/test-utils.js +41 -2
  25. package/dist/test-utils.js.map +1 -1
  26. package/dist/ui/boolean-inputs.test.ts +76 -0
  27. package/dist/ui/components/action-sheet/action-sheet.test.ts +370 -1
  28. package/dist/ui/components/action-sheet/action-sheet.ts +306 -28
  29. package/dist/ui/components/action-sheet/index.ts +1 -0
  30. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +552 -8
  31. package/dist/ui/components/alert-dialog/alert-dialog.ts +644 -36
  32. package/dist/ui/components/alert-dialog/index.ts +5 -0
  33. package/dist/ui/components/area-chart/area-chart.ts +3 -2
  34. package/dist/ui/components/badge/badge.ts +2 -1
  35. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +968 -3
  36. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +1021 -95
  37. package/dist/ui/components/bottom-sheet/index.ts +7 -1
  38. package/dist/ui/components/button/button.test.ts +36 -1
  39. package/dist/ui/components/button/button.ts +17 -7
  40. package/dist/ui/components/button-group/button-group.test.ts +53 -9
  41. package/dist/ui/components/button-group/button-group.ts +67 -21
  42. package/dist/ui/components/card/card.test.ts +16 -0
  43. package/dist/ui/components/card/card.ts +2 -1
  44. package/dist/ui/components/chart/chart-presets.ts +6 -6
  45. package/dist/ui/components/chart/chart.test.ts +14 -0
  46. package/dist/ui/components/chart/chart.ts +10 -3
  47. package/dist/ui/components/checkbox/checkbox.ts +2 -1
  48. package/dist/ui/components/collapsible/collapsible.ts +2 -1
  49. package/dist/ui/components/dialog/dialog.ts +6 -6
  50. package/dist/ui/components/dock/dock-scroll.ts +9 -0
  51. package/dist/ui/components/dock/dock-search.test.ts +594 -0
  52. package/dist/ui/components/dock/dock.test.ts +4 -1
  53. package/dist/ui/components/dock/dock.ts +529 -51
  54. package/dist/ui/components/dynamic-color/dynamic-color.test.ts +144 -0
  55. package/dist/ui/components/dynamic-color/dynamic-color.ts +146 -0
  56. package/dist/ui/components/dynamic-color/index.ts +5 -0
  57. package/dist/ui/components/glass/glass.test.ts +39 -1
  58. package/dist/ui/components/glass/glass.ts +87 -17
  59. package/dist/ui/components/glass/index.ts +1 -0
  60. package/dist/ui/components/icon/icon-theme.test.ts +28 -0
  61. package/dist/ui/components/icon/icon-theme.ts +38 -7
  62. package/dist/ui/components/input/input.ts +2 -1
  63. package/dist/ui/components/label/label.ts +8 -2
  64. package/dist/ui/components/line-chart/line-chart.ts +3 -2
  65. package/dist/ui/components/list/list.ts +5 -4
  66. package/dist/ui/components/menu/menu-popup.test.ts +18 -14
  67. package/dist/ui/components/menu/menu-popup.ts +24 -3
  68. package/dist/ui/components/menu/menu.test.ts +99 -5
  69. package/dist/ui/components/menu/menu.ts +74 -34
  70. package/dist/ui/components/nav-drawer/nav-drawer.ts +3 -2
  71. package/dist/ui/components/navigation-bar/index.ts +1 -0
  72. package/dist/ui/components/navigation-bar/navigation-bar.test.ts +425 -0
  73. package/dist/ui/components/navigation-bar/navigation-bar.ts +411 -0
  74. package/dist/ui/components/popover/index.ts +9 -0
  75. package/dist/ui/components/popover/popover-panel.test.ts +735 -0
  76. package/dist/ui/components/popover/popover-panel.ts +603 -0
  77. package/dist/ui/components/popover/popover.test.ts +286 -0
  78. package/dist/ui/components/popover/popover.ts +207 -0
  79. package/dist/ui/components/progress/progress.ts +2 -1
  80. package/dist/ui/components/radio-group/radio-group.ts +2 -1
  81. package/dist/ui/components/scroll-area/scroll-area.ts +4 -3
  82. package/dist/ui/components/search-field/index.ts +15 -0
  83. package/dist/ui/components/search-field/keyboard.test.ts +147 -0
  84. package/dist/ui/components/search-field/keyboard.ts +123 -0
  85. package/dist/ui/components/search-field/search-field.test.ts +549 -0
  86. package/dist/ui/components/search-field/search-field.ts +628 -0
  87. package/dist/ui/components/segmented-control/index.ts +8 -0
  88. package/dist/ui/components/segmented-control/segmented-control.test.ts +861 -0
  89. package/dist/ui/components/segmented-control/segmented-control.ts +884 -0
  90. package/dist/ui/components/select/select.ts +2 -1
  91. package/dist/ui/components/sheet/sheet.test.ts +90 -3
  92. package/dist/ui/components/sheet/sheet.ts +59 -4
  93. package/dist/ui/components/slider/index.ts +1 -0
  94. package/dist/ui/components/slider/slider.test.ts +695 -0
  95. package/dist/ui/components/slider/slider.ts +901 -0
  96. package/dist/ui/components/switch/index.ts +1 -1
  97. package/dist/ui/components/switch/switch.test.ts +464 -48
  98. package/dist/ui/components/switch/switch.ts +613 -43
  99. package/dist/ui/components/tabs/index.ts +7 -1
  100. package/dist/ui/components/tabs/tabs.test.ts +278 -2
  101. package/dist/ui/components/tabs/tabs.ts +233 -30
  102. package/dist/ui/components/textarea/textarea.ts +2 -1
  103. package/dist/ui/components/toggle/index.ts +1 -0
  104. package/dist/ui/components/toggle/toggle.test.ts +310 -4
  105. package/dist/ui/components/toggle/toggle.ts +241 -26
  106. package/dist/ui/components/toolbar/bar-scroll.test.ts +310 -0
  107. package/dist/ui/components/toolbar/bar-scroll.ts +372 -0
  108. package/dist/ui/components/toolbar/index.ts +12 -0
  109. package/dist/ui/components/toolbar/toolbar.test.ts +438 -0
  110. package/dist/ui/components/toolbar/toolbar.ts +535 -0
  111. package/dist/utils/analyze.d.ts +10 -5
  112. package/dist/utils/analyze.js +3 -2
  113. package/dist/utils/analyze.js.map +1 -1
  114. package/dist/utils/installed.d.ts +13 -1
  115. package/dist/utils/installed.js +17 -0
  116. package/dist/utils/installed.js.map +1 -1
  117. package/dist/utils/installed.test.js +39 -1
  118. package/dist/utils/installed.test.js.map +1 -1
  119. package/dist/utils/tailwind.d.ts +25 -5
  120. package/dist/utils/tailwind.js +53 -14
  121. package/dist/utils/tailwind.js.map +1 -1
  122. package/dist/utils/tailwind.test.js +85 -22
  123. package/dist/utils/tailwind.test.js.map +1 -1
  124. package/package.json +1 -1
@@ -4,7 +4,7 @@ import {
4
4
  render,
5
5
  waitForUpdate,
6
6
  } from '@blotch/angular-lynx-testing-library';
7
- import { LYNX_ELEMENTS } from '@blotch/angular-lynx';
7
+ import { LYNX_ELEMENTS, LynxAccessibilitySettings } from '@blotch/angular-lynx';
8
8
  import {
9
9
  type MockInstance,
10
10
  afterEach,
@@ -16,12 +16,14 @@ import {
16
16
  } from 'vitest';
17
17
  import { DURATION } from '../../utils/animate';
18
18
  import { fireLynxEvent } from '../../../test-utils/fire-lynx-event';
19
+ import { UiGlass } from '../glass/glass';
19
20
  import {
20
21
  type LynxMainThread,
21
22
  lynxMainThread,
22
23
  } from '../../../test-utils/lynx-main-thread';
23
24
  import {
24
25
  UiActionSheet,
26
+ UiActionSheetAnchor,
25
27
  UiActionSheetCancel,
26
28
  UiActionSheetItem,
27
29
  UiActionSheetTitle,
@@ -70,6 +72,68 @@ class ActionSheetHost {
70
72
  })
71
73
  class CancelLabelHost {}
72
74
 
75
+ /**
76
+ * An action sheet opened from the button that presents it.
77
+ */
78
+ @Component({
79
+ imports: [
80
+ UiActionSheet,
81
+ UiActionSheetAnchor,
82
+ UiActionSheetTitle,
83
+ UiActionSheetItem,
84
+ UiActionSheetCancel,
85
+ UiGlass,
86
+ LYNX_ELEMENTS,
87
+ ],
88
+ template: `
89
+ @if (glassAnchor()) {
90
+ <blur-view
91
+ ui-glass
92
+ class="share"
93
+ [uiActionSheetAnchor]="sheet"
94
+ (bindtap)="open.set(true)"
95
+ >
96
+ <text>Share</text>
97
+ </blur-view>
98
+ } @else if (anchored()) {
99
+ <view
100
+ class="share"
101
+ [uiActionSheetAnchor]="sheet"
102
+ (bindtap)="open.set(true)"
103
+ >
104
+ <text>Share</text>
105
+ </view>
106
+ }
107
+ <ui-action-sheet #sheet [(open)]="open" (closed)="closed()">
108
+ <ui-action-sheet-title>Delete this draft?</ui-action-sheet-title>
109
+ <ui-action-sheet-item (pressed)="saved()"
110
+ >Save Draft</ui-action-sheet-item
111
+ >
112
+ <ui-action-sheet-item variant="destructive" (pressed)="deleted()">
113
+ Delete Draft
114
+ </ui-action-sheet-item>
115
+ <ui-action-sheet-cancel />
116
+ </ui-action-sheet>
117
+ `,
118
+ })
119
+ class AnchoredHost {
120
+ readonly open = signal(false);
121
+ readonly anchored = signal(true);
122
+ readonly glassAnchor = signal(false);
123
+ readonly closed = vi.fn();
124
+ readonly saved = vi.fn();
125
+ readonly deleted = vi.fn();
126
+ }
127
+
128
+ /**
129
+ * A title on its own, outside any action sheet.
130
+ */
131
+ @Component({
132
+ imports: [UiActionSheetTitle],
133
+ template: `<ui-action-sheet-title>Alone</ui-action-sheet-title>`,
134
+ })
135
+ class LoneTitleHost {}
136
+
73
137
  /**
74
138
  * The Lynx function an element's `invoke()` calls to run one of its UI
75
139
  * methods, such as moving a screen reader's focus. The testing library
@@ -248,6 +312,14 @@ describe('UiActionSheet', () => {
248
312
  expect(host.closed).toHaveBeenCalledOnce();
249
313
  });
250
314
 
315
+ it("closes on Android's back button", async () => {
316
+ const { host, overlay } = await renderSheet();
317
+ await openSheet(host);
318
+
319
+ fireLynxEvent(overlay, 'requestclose');
320
+ expect(host.open()).toBe(false);
321
+ });
322
+
251
323
  it('stays open when the panel is tapped', async () => {
252
324
  const { host, panel } = await renderSheet();
253
325
  await openSheet(host);
@@ -341,3 +413,300 @@ describe('UiActionSheetCancel', () => {
341
413
  expectPressFeedback(button('Cancel'));
342
414
  });
343
415
  });
416
+
417
+ /**
418
+ * The Lynx function that runs an element's UI methods, here measuring the
419
+ * anchor and the popover (`boundingClientRect`) as on a phone.
420
+ */
421
+ type MeasureUIMethod = {
422
+ __InvokeUIMethod: (
423
+ element: Element,
424
+ method: string,
425
+ params: Record<string, unknown>,
426
+ callback: (result: { code: number; data?: unknown }) => void,
427
+ ) => void;
428
+ };
429
+
430
+ /** Accessibility settings a test can change, as the host app would. */
431
+ const settings = {
432
+ reduceMotion: signal(false),
433
+ reduceTransparency: signal(false),
434
+ increaseContrast: signal(false),
435
+ };
436
+
437
+ /**
438
+ * Renders the anchored sheet on a platform (none for Material), with the
439
+ * safe area's markers a phone's screen, the popover's content 270 by 150,
440
+ * and the anchor a button near the top.
441
+ */
442
+ const renderAnchored = async (
443
+ platform: { platform: string; osVersion: string } | null = {
444
+ platform: 'iOS',
445
+ osVersion: '26.0',
446
+ },
447
+ ) => {
448
+ vi.stubGlobal('SystemInfo', platform ?? undefined);
449
+ vi.spyOn(
450
+ globalThis as unknown as MeasureUIMethod,
451
+ '__InvokeUIMethod',
452
+ ).mockImplementation((element, method, _params, callback) => {
453
+ if (method !== 'boundingClientRect') {
454
+ callback({ code: 0 });
455
+ return;
456
+ }
457
+ const style = element.getAttribute('style') ?? '';
458
+ const data = style.includes('safe-area-inset')
459
+ ? { left: 0, top: 0, width: 402, height: 840 }
460
+ : element.classList.contains('shrink-0')
461
+ ? { left: 0, top: 0, width: 270, height: 150 }
462
+ : { left: 16, top: 80, width: 80, height: 44 };
463
+ callback({ code: 0, data });
464
+ });
465
+ const result = await render(AnchoredHost, {
466
+ providers: [{ provide: LynxAccessibilitySettings, useValue: settings }],
467
+ });
468
+ const query = (selector: string) =>
469
+ result.container.querySelector(selector) as HTMLElement | null;
470
+ return {
471
+ ...result,
472
+ host: result.componentRef.instance as AnchoredHost,
473
+ overlay: () => query('overlay')!,
474
+ anchor: () => query('.share')!,
475
+ popover: () => query('blur-view[ui-glass]:not(.share)'),
476
+ content: () => query('.shrink-0')!,
477
+ // A button's pressable row: the view around its label.
478
+ button: (label: string) => result.getByText(label).parentElement!,
479
+ };
480
+ };
481
+
482
+ type Anchored = Awaited<ReturnType<typeof renderAnchored>>;
483
+
484
+ /**
485
+ * Opens the anchored sheet and waits until its popover has grown.
486
+ */
487
+ const openAnchored = async (rendered: Anchored) => {
488
+ fireEvent.tap(rendered.anchor());
489
+ await vi.waitFor(() => expect(rendered.content().style.opacity).toBe('1'));
490
+ };
491
+
492
+ /**
493
+ * Lets Lynx's timers run: the touch check waits for the next one.
494
+ */
495
+ const nextTimer = () => new Promise((resolve) => setTimeout(resolve, 5));
496
+
497
+ describe('UiActionSheet opened from its control', () => {
498
+ beforeEach(() => {
499
+ // The renderer warns that the components aren't Lynx elements.
500
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
501
+ });
502
+ afterEach(() => {
503
+ vi.restoreAllMocks();
504
+ vi.unstubAllGlobals();
505
+ });
506
+
507
+ it('grows out of its control as Liquid Glass on iOS 26, leaving the screen undimmed', async () => {
508
+ const rendered = await renderAnchored();
509
+ await openAnchored(rendered);
510
+
511
+ const popover = rendered.popover()!;
512
+ expect(popover.getAttribute('blur-effect')).toBe('glass');
513
+ expect(popover.style.width).toBe('270px');
514
+ // The bottom sheet and its backdrop stay hidden.
515
+ expect(rendered.overlay().getAttribute('visible')).toBe('false');
516
+ // The control hides under the glass that grows out of it.
517
+ expect(rendered.anchor().style.visibility).toBe('hidden');
518
+ // Its title and actions, as iOS 26 lays them out, and no Cancel: a
519
+ // touch outside cancels.
520
+ expect(popover.contains(rendered.getByText('Delete this draft?'))).toBe(
521
+ true,
522
+ );
523
+ expect(rendered.getByText('Delete this draft?').classList).toContain(
524
+ 'text-glass-muted-foreground',
525
+ );
526
+ expect(rendered.getByText('Save Draft').classList).toContain(
527
+ 'text-glass-foreground',
528
+ );
529
+ expect(rendered.getByText('Delete Draft').classList).toContain(
530
+ 'text-destructive',
531
+ );
532
+ expect(rendered.button('Save Draft').classList).toContain('min-h-[44px]');
533
+ expect(rendered.button('Save Draft').classList).toContain('justify-center');
534
+ expect(popover.contains(rendered.getByText('Cancel'))).toBe(false);
535
+ });
536
+
537
+ it("takes a glass control's glass over, shown as the control hides", async () => {
538
+ const rendered = await renderAnchored();
539
+ expect(rendered.anchor().hasAttribute('glass-effect-id')).toBe(false);
540
+ rendered.host.glassAnchor.set(true);
541
+ await waitForUpdate();
542
+ const id = rendered.anchor().getAttribute('glass-effect-id');
543
+ expect(id).toMatch(/^ui-action-sheet-\d+$/);
544
+
545
+ await openAnchored(rendered);
546
+ expect(rendered.popover()!.getAttribute('glass-effect-id')).toBe(id);
547
+ expect(rendered.anchor().style.visibility).toBe('hidden');
548
+
549
+ // A control that goes takes its glass with it.
550
+ rendered.host.open.set(false);
551
+ await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
552
+ rendered.host.glassAnchor.set(false);
553
+ await waitForUpdate();
554
+ expect(rendered.anchor().tagName.toLowerCase()).toBe('view');
555
+ });
556
+
557
+ it('chooses an action, and closes', async () => {
558
+ const rendered = await renderAnchored();
559
+ await openAnchored(rendered);
560
+
561
+ fireEvent.tap(rendered.button('Delete Draft'));
562
+ expect(rendered.host.deleted).toHaveBeenCalledTimes(1);
563
+ expect(rendered.host.open()).toBe(false);
564
+ await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
565
+ await waitForUpdate();
566
+ expect(rendered.popover()).toBeNull();
567
+ expect(rendered.anchor().style.visibility).toBe('');
568
+ expect(rendered.host.closed).toHaveBeenCalledTimes(1);
569
+ });
570
+
571
+ it('lights a row up while it is pressed, rather than shrinking it', async () => {
572
+ const rendered = await renderAnchored();
573
+ await openAnchored(rendered);
574
+ const row = rendered.button('Save Draft');
575
+ const highlight = row.querySelector('.bg-glass-foreground') as HTMLElement;
576
+ // iOS 26's menu rows light up in a capsule, inset from the sides.
577
+ expect(highlight.className.split(' ')).toEqual(
578
+ expect.arrayContaining(['left-[10px]', 'right-[10px]', 'rounded-full']),
579
+ );
580
+
581
+ fireEvent.touchstart(row);
582
+ await waitForUpdate();
583
+ expect(highlight.style.opacity).toBe('0.08');
584
+ expect(row.style.transform).toBe('');
585
+ fireEvent.touchend(row);
586
+ await waitForUpdate();
587
+ expect(highlight.style.opacity).toBe('0');
588
+ fireEvent.mousedown(row);
589
+ await waitForUpdate();
590
+ expect(highlight.style.opacity).toBe('0.08');
591
+ fireLynxEvent(row, 'mouseleave');
592
+ await waitForUpdate();
593
+ expect(highlight.style.opacity).toBe('0');
594
+ });
595
+
596
+ it('lights a Material row up edge to edge', async () => {
597
+ const rendered = await renderAnchored(null);
598
+ await openAnchored(rendered);
599
+ const row = rendered.button('Save Draft');
600
+ const highlight = row.querySelector('.bg-glass-foreground') as HTMLElement;
601
+ expect(highlight.className.split(' ')).toEqual(
602
+ expect.arrayContaining(['left-0', 'right-0']),
603
+ );
604
+ fireEvent.touchstart(row);
605
+ await waitForUpdate();
606
+ expect(highlight.style.opacity).toBe('0.1');
607
+ });
608
+
609
+ it('closes on a touch outside it, without counting one on its control', async () => {
610
+ const rendered = await renderAnchored();
611
+ await openAnchored(rendered);
612
+
613
+ // A touch on the control that presented it isn't outside it.
614
+ fireLynxEvent(rendered.anchor(), 'touchstart', {}, 'bindEvent');
615
+ fireLynxEvent(rendered.popover()!, 'touchstart', {}, 'global-bindEvent');
616
+ await nextTimer();
617
+ fireLynxEvent(rendered.anchor(), 'mousedown', {}, 'bindEvent');
618
+ fireLynxEvent(rendered.popover()!, 'mousedown', {}, 'global-bindEvent');
619
+ await nextTimer();
620
+ expect(rendered.host.open()).toBe(true);
621
+
622
+ fireLynxEvent(rendered.popover()!, 'touchstart', {}, 'global-bindEvent');
623
+ await nextTimer();
624
+ expect(rendered.host.open()).toBe(false);
625
+ });
626
+
627
+ it('is a Material 3 menu beside its control on Android', async () => {
628
+ const rendered = await renderAnchored(null);
629
+ await openAnchored(rendered);
630
+
631
+ const popover = rendered.popover()!;
632
+ expect(popover.style.width).toBe('280px');
633
+ expect(popover.classList).toContain('bg-secondary');
634
+ // Beside the control, which stays in view.
635
+ expect(rendered.anchor().style.visibility).toBe('');
636
+ expect(rendered.button('Save Draft').classList).toContain('min-h-[48px]');
637
+ expect(rendered.button('Save Draft').classList).not.toContain(
638
+ 'justify-center',
639
+ );
640
+ expect(rendered.getByText('Save Draft').classList).toContain('text-[16px]');
641
+ expect(
642
+ rendered.getByText('Delete this draft?').parentElement?.classList,
643
+ ).toContain('px-3');
644
+ });
645
+
646
+ it('rises from the bottom before iOS 26, as iPhone action sheets did', async () => {
647
+ const rendered = await renderAnchored({
648
+ platform: 'iOS',
649
+ osVersion: '18.5',
650
+ });
651
+ fireEvent.tap(rendered.anchor());
652
+ for (let i = 0; i < 3; i++) await waitForUpdate();
653
+
654
+ expect(rendered.overlay().getAttribute('visible')).toBe('true');
655
+ expect(rendered.popover()).toBeNull();
656
+ expect(rendered.getByText('Save Draft').classList).toContain(
657
+ 'text-primary',
658
+ );
659
+ expect(rendered.anchor().style.visibility).toBe('');
660
+ });
661
+
662
+ it('rises from the bottom again once its control is gone', async () => {
663
+ const rendered = await renderAnchored();
664
+ rendered.host.anchored.set(false);
665
+ await waitForUpdate();
666
+
667
+ rendered.host.open.set(true);
668
+ for (let i = 0; i < 3; i++) await waitForUpdate();
669
+ expect(rendered.overlay().getAttribute('visible')).toBe('true');
670
+ expect(rendered.popover()).toBeNull();
671
+ // Its actions are back in the sheet's group, with Cancel below.
672
+ expect(rendered.button('Save Draft').classList).toContain('border-t');
673
+ expect(rendered.getByText('Cancel')).toBeTruthy();
674
+ });
675
+
676
+ it("doesn't report a close when its control comes or goes while it's closed", async () => {
677
+ const rendered = await renderAnchored();
678
+ // Opened and closed from the bottom once...
679
+ rendered.host.anchored.set(false);
680
+ await waitForUpdate();
681
+ rendered.host.open.set(true);
682
+ for (let i = 0; i < 3; i++) await waitForUpdate();
683
+ rendered.host.open.set(false);
684
+ await vi.waitFor(() => expect(rendered.host.closed).toHaveBeenCalled());
685
+
686
+ // ...then given its control back, closed.
687
+ rendered.host.anchored.set(true);
688
+ await waitForUpdate();
689
+ await new Promise((resolve) => setTimeout(resolve, 300));
690
+ expect(rendered.host.closed).toHaveBeenCalledTimes(1);
691
+ });
692
+ });
693
+
694
+ describe('UiActionSheet accessibility', () => {
695
+ it('reads its title as a heading and its actions as buttons', async () => {
696
+ const { getByText } = await renderSheet();
697
+ expect(getByText('Pick').getAttribute('accessibility-traits')).toBe(
698
+ 'header',
699
+ );
700
+ expect(getByText('Share').getAttribute('accessibility-traits')).toBe(
701
+ 'button',
702
+ );
703
+ expect(getByText('Cancel').getAttribute('accessibility-traits')).toBe(
704
+ 'button',
705
+ );
706
+ });
707
+
708
+ it('draws a title outside an action sheet as in one', async () => {
709
+ const { getByText } = await render(LoneTitleHost);
710
+ expect(getByText('Alone').classList).toContain('text-muted-foreground');
711
+ });
712
+ });