@astryxdesign/core 0.5.2-canary.c9c8564 → 0.5.2-canary.e4f8e4e

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 (146) hide show
  1. package/dist/BottomSheet/BottomSheet.d.ts +3 -1
  2. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  3. package/dist/BottomSheet/BottomSheet.js +4 -3
  4. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
  5. package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
  6. package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
  7. package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
  9. package/dist/Button/Button.d.ts +2 -1
  10. package/dist/Button/Button.d.ts.map +1 -1
  11. package/dist/Button/Button.js +11 -4
  12. package/dist/Carousel/Carousel.d.ts.map +1 -1
  13. package/dist/Carousel/Carousel.js +2 -2
  14. package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
  15. package/dist/Chat/ChatToolCalls.js +34 -17
  16. package/dist/ContextMenu/ContextMenu.d.ts +14 -3
  17. package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
  18. package/dist/ContextMenu/ContextMenu.js +149 -26
  19. package/dist/ContextMenu/index.d.ts +1 -0
  20. package/dist/ContextMenu/index.d.ts.map +1 -1
  21. package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
  22. package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
  23. package/dist/DropdownMenu/DropdownMenu.js +340 -26
  24. package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
  25. package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
  26. package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
  27. package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
  28. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
  29. package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
  30. package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
  31. package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
  32. package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
  33. package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
  34. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
  35. package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
  36. package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
  37. package/dist/DropdownMenu/index.d.ts +2 -1
  38. package/dist/DropdownMenu/index.d.ts.map +1 -1
  39. package/dist/DropdownMenu/menuWidth.d.ts +14 -0
  40. package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
  41. package/dist/DropdownMenu/menuWidth.js +35 -0
  42. package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
  43. package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
  44. package/dist/DropdownMenu/useMenuOverflow.js +60 -0
  45. package/dist/MoreMenu/MoreMenu.d.ts +8 -2
  46. package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
  47. package/dist/MoreMenu/MoreMenu.js +2 -0
  48. package/dist/MultiSelector/MultiSelector.d.ts +11 -1
  49. package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
  50. package/dist/MultiSelector/MultiSelector.js +97 -68
  51. package/dist/MultiSelector/index.d.ts +1 -1
  52. package/dist/MultiSelector/index.d.ts.map +1 -1
  53. package/dist/Popover/Popover.d.ts +10 -6
  54. package/dist/Popover/Popover.d.ts.map +1 -1
  55. package/dist/Popover/Popover.js +141 -16
  56. package/dist/Popover/usePopover.d.ts +4 -0
  57. package/dist/Popover/usePopover.d.ts.map +1 -1
  58. package/dist/Popover/usePopover.js +52 -5
  59. package/dist/Selector/Selector.d.ts +10 -0
  60. package/dist/Selector/Selector.d.ts.map +1 -1
  61. package/dist/Selector/Selector.js +106 -80
  62. package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
  63. package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
  64. package/dist/Selector/SelectorBottomSheet.js +83 -0
  65. package/dist/Selector/index.d.ts +1 -1
  66. package/dist/Selector/index.d.ts.map +1 -1
  67. package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
  68. package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
  69. package/dist/Selector/selectorPresentation.stylex.js +21 -0
  70. package/dist/Selector/useSelectorPresentation.d.ts +32 -0
  71. package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
  72. package/dist/Selector/useSelectorPresentation.js +90 -0
  73. package/dist/Toast/useToastGesture.d.ts.map +1 -1
  74. package/dist/Toast/useToastGesture.js +46 -9
  75. package/dist/astryx.css +19 -1
  76. package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
  77. package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
  78. package/dist/hooks/useAdaptivePresentation.js +19 -0
  79. package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
  80. package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
  81. package/dist/hooks/useFocusReturnVisibility.js +35 -0
  82. package/dist/hooks/useFocusTrap.d.ts.map +1 -1
  83. package/dist/hooks/useFocusTrap.js +12 -0
  84. package/locales/en.json +20 -0
  85. package/locales/pseudo.json +15 -0
  86. package/package.json +2 -2
  87. package/src/BottomSheet/BottomSheet.doc.mjs +6 -0
  88. package/src/BottomSheet/BottomSheet.test.tsx +29 -0
  89. package/src/BottomSheet/BottomSheet.tsx +7 -4
  90. package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
  91. package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
  92. package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
  93. package/src/Button/Button.doc.mjs +56 -0
  94. package/src/Button/Button.test.tsx +10 -0
  95. package/src/Button/Button.tsx +13 -5
  96. package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
  97. package/src/Carousel/Carousel.test.tsx +83 -0
  98. package/src/Carousel/Carousel.tsx +8 -2
  99. package/src/Chat/ChatToolCalls.test.tsx +44 -0
  100. package/src/Chat/ChatToolCalls.tsx +36 -15
  101. package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
  102. package/src/ContextMenu/ContextMenu.test.tsx +131 -1
  103. package/src/ContextMenu/ContextMenu.tsx +199 -32
  104. package/src/ContextMenu/index.ts +1 -0
  105. package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
  106. package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
  107. package/src/DropdownMenu/DropdownMenu.tsx +459 -28
  108. package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
  109. package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
  110. package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
  111. package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
  112. package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
  113. package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
  114. package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
  115. package/src/DropdownMenu/index.ts +2 -0
  116. package/src/DropdownMenu/menuWidth.ts +51 -0
  117. package/src/DropdownMenu/useMenuOverflow.ts +77 -0
  118. package/src/IconButton/IconButton.doc.mjs +38 -0
  119. package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
  120. package/src/MoreMenu/MoreMenu.test.tsx +47 -0
  121. package/src/MoreMenu/MoreMenu.tsx +13 -1
  122. package/src/MultiSelector/MultiSelector.doc.mjs +16 -0
  123. package/src/MultiSelector/MultiSelector.test.tsx +127 -0
  124. package/src/MultiSelector/MultiSelector.tsx +121 -73
  125. package/src/MultiSelector/index.ts +1 -0
  126. package/src/Popover/Popover.doc.mjs +11 -11
  127. package/src/Popover/Popover.test.tsx +397 -2
  128. package/src/Popover/Popover.tsx +228 -21
  129. package/src/Popover/usePopover.tsx +92 -15
  130. package/src/ProgressBar/ProgressBar.spec.md +208 -0
  131. package/src/SegmentedControl/SegmentedControl.doc.mjs +56 -0
  132. package/src/Selector/Selector.doc.mjs +12 -0
  133. package/src/Selector/Selector.test.tsx +135 -0
  134. package/src/Selector/Selector.tsx +141 -82
  135. package/src/Selector/SelectorBottomSheet.tsx +97 -0
  136. package/src/Selector/index.ts +1 -0
  137. package/src/Selector/selectorPresentation.stylex.ts +20 -0
  138. package/src/Selector/useSelectorPresentation.ts +147 -0
  139. package/src/Toast/ToastViewport.test.tsx +118 -4
  140. package/src/Toast/useToastGesture.ts +61 -8
  141. package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
  142. package/src/hooks/useAdaptivePresentation.ts +35 -0
  143. package/src/hooks/useFocusReturnVisibility.ts +45 -0
  144. package/src/hooks/useFocusTrap.test.tsx +40 -0
  145. package/src/hooks/useFocusTrap.ts +13 -0
  146. package/src/theme/themingTargets.test.ts +91 -25
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * @file Popover.test.tsx
5
- * @input Uses vitest, @testing-library/react, Popover, Dialog,
5
+ * @input Uses vitest, Testing Library, node:fs, Popover, Dialog, and
6
6
  * SegmentedControl
7
7
  * @output Unit tests for Popover component behavior
8
8
  * @position Testing; validates Popover.tsx implementation
@@ -11,7 +11,9 @@
11
11
  */
12
12
 
13
13
  import {describe, it, expect, vi, beforeAll, afterAll} from 'vitest';
14
- import {render, screen, fireEvent} from '@testing-library/react';
14
+ import {render, screen, fireEvent, waitFor, act} from '@testing-library/react';
15
+ import userEvent from '@testing-library/user-event';
16
+ import {readFileSync} from 'node:fs';
15
17
  import React, {useRef} from 'react';
16
18
  import {Popover} from './Popover';
17
19
  import type {UsePopoverReturn} from './usePopover';
@@ -64,8 +66,19 @@ describe('usePopover public return type', () => {
64
66
  const hasDismissalGuard: 'wasJustDismissed' extends keyof UsePopoverReturn
65
67
  ? true
66
68
  : false = false;
69
+ const hasInternalToggle: 'toggleWithOptions' extends keyof UsePopoverReturn
70
+ ? true
71
+ : false = false;
72
+ type PublicShowOptions = NonNullable<
73
+ Parameters<UsePopoverReturn['show']>[0]
74
+ >;
75
+ const hasInternalFocusTarget: 'focusTarget' extends keyof PublicShowOptions
76
+ ? true
77
+ : false = false;
67
78
  expect(hasKeepOpenProps).toBe(false);
68
79
  expect(hasDismissalGuard).toBe(false);
80
+ expect(hasInternalToggle).toBe(false);
81
+ expect(hasInternalFocusTarget).toBe(false);
69
82
  });
70
83
  });
71
84
 
@@ -205,6 +218,288 @@ describe('Popover', () => {
205
218
  expect(screen.getByTestId('my-popover')).toBeInTheDocument();
206
219
  });
207
220
 
221
+ it('constrains the layer without clipping content that already fits', () => {
222
+ render(
223
+ <Popover
224
+ content={<span>Content</span>}
225
+ label="Test"
226
+ width={640}
227
+ data-testid="popover-content">
228
+ <button type="button">Open</button>
229
+ </Popover>,
230
+ );
231
+
232
+ fireEvent.click(screen.getByRole('button', {name: 'Open'}));
233
+
234
+ const layer = document.querySelector('[popover]');
235
+ expect(layer).toHaveStyle({boxSizing: 'border-box'});
236
+ expect(layer?.className).toContain('Popover__styles.viewportFit');
237
+ expect(layer?.className).toContain('Popover__styles.viewportAligned');
238
+ expect(layer?.className).toContain('Popover__styles.viewportStart');
239
+ const surface = screen.getByTestId('popover-content').parentElement;
240
+ expect(surface?.className).toContain('Popover__styles.surfaceViewportFit');
241
+ const popoverSource = readFileSync(
242
+ 'packages/core/src/Popover/Popover.tsx',
243
+ 'utf8',
244
+ );
245
+ expect(popoverSource).toMatch(
246
+ /surfaceViewportFit:[\s\S]*?maxInlineSize: stylex\.firstThatWorks\(\s*POPOVER_MAX_INLINE_SIZE/,
247
+ );
248
+ expect(popoverSource).toMatch(
249
+ /surfaceViewportFit:[\s\S]*?maxBlockSize: stylex\.firstThatWorks\(\s*POPOVER_MAX_BLOCK_SIZE/,
250
+ );
251
+ expect(surface?.className).not.toContain(
252
+ 'Popover__styles.surfaceScrollable',
253
+ );
254
+ });
255
+
256
+ it('enables internal scrolling only when content exceeds the available space', () => {
257
+ const clientHeight = vi
258
+ .spyOn(HTMLElement.prototype, 'clientHeight', 'get')
259
+ .mockReturnValue(100);
260
+ const scrollHeight = vi
261
+ .spyOn(HTMLElement.prototype, 'scrollHeight', 'get')
262
+ .mockReturnValue(200);
263
+ const clientWidth = vi
264
+ .spyOn(HTMLElement.prototype, 'clientWidth', 'get')
265
+ .mockReturnValue(200);
266
+ const scrollWidth = vi
267
+ .spyOn(HTMLElement.prototype, 'scrollWidth', 'get')
268
+ .mockReturnValue(200);
269
+
270
+ try {
271
+ render(
272
+ <Popover
273
+ content={<span>Long content</span>}
274
+ label="Test"
275
+ data-testid="popover-content">
276
+ <button type="button">Open</button>
277
+ </Popover>,
278
+ );
279
+ fireEvent.click(screen.getByRole('button', {name: 'Open'}));
280
+
281
+ expect(
282
+ screen.getByTestId('popover-content').parentElement?.className,
283
+ ).toContain('Popover__styles.surfaceScrollable');
284
+ } finally {
285
+ clientHeight.mockRestore();
286
+ scrollHeight.mockRestore();
287
+ clientWidth.mockRestore();
288
+ scrollWidth.mockRestore();
289
+ }
290
+ });
291
+
292
+ it('coalesces repeated overflow signals into one animation frame', () => {
293
+ let resizeCallback: ResizeObserverCallback | undefined;
294
+ let mutationCallback: MutationCallback | undefined;
295
+ const frameCallbacks: FrameRequestCallback[] = [];
296
+
297
+ class ResizeObserverMock {
298
+ constructor(callback: ResizeObserverCallback) {
299
+ resizeCallback = callback;
300
+ }
301
+ observe = vi.fn();
302
+ unobserve = vi.fn();
303
+ disconnect = vi.fn();
304
+ }
305
+
306
+ class MutationObserverMock {
307
+ constructor(callback: MutationCallback) {
308
+ mutationCallback = callback;
309
+ }
310
+ observe = vi.fn();
311
+ disconnect = vi.fn();
312
+ takeRecords = vi.fn(() => []);
313
+ }
314
+
315
+ const requestFrame = vi.fn((callback: FrameRequestCallback) => {
316
+ frameCallbacks.push(callback);
317
+ return frameCallbacks.length;
318
+ });
319
+ const cancelFrame = vi.fn();
320
+ vi.stubGlobal('ResizeObserver', ResizeObserverMock);
321
+ vi.stubGlobal('MutationObserver', MutationObserverMock);
322
+ vi.stubGlobal('requestAnimationFrame', requestFrame);
323
+ vi.stubGlobal('cancelAnimationFrame', cancelFrame);
324
+
325
+ const {unmount} = render(
326
+ <Popover content={<span>Content</span>} label="Test">
327
+ <button type="button">Open</button>
328
+ </Popover>,
329
+ );
330
+
331
+ try {
332
+ fireEvent.click(screen.getByRole('button', {name: 'Open'}));
333
+ while (frameCallbacks.length > 0) {
334
+ const pendingFrames = frameCallbacks.splice(0);
335
+ act(() => pendingFrames.forEach(callback => callback(0)));
336
+ }
337
+ requestFrame.mockClear();
338
+
339
+ act(() => {
340
+ resizeCallback?.([], {} as ResizeObserver);
341
+ mutationCallback?.([], {} as MutationObserver);
342
+ window.dispatchEvent(new Event('resize'));
343
+ });
344
+
345
+ expect(requestFrame).toHaveBeenCalledTimes(1);
346
+ expect(frameCallbacks).toHaveLength(1);
347
+ } finally {
348
+ unmount();
349
+ vi.unstubAllGlobals();
350
+ }
351
+ });
352
+
353
+ it('observes overflow only while the popover is open', () => {
354
+ const resizeConstructed = vi.fn();
355
+ const resizeDisconnected = vi.fn();
356
+ const mutationConstructed = vi.fn();
357
+ const mutationDisconnected = vi.fn();
358
+
359
+ class ResizeObserverMock {
360
+ constructor() {
361
+ resizeConstructed();
362
+ }
363
+ observe = vi.fn();
364
+ unobserve = vi.fn();
365
+ disconnect = resizeDisconnected;
366
+ }
367
+
368
+ class MutationObserverMock {
369
+ constructor() {
370
+ mutationConstructed();
371
+ }
372
+ observe = vi.fn();
373
+ disconnect = mutationDisconnected;
374
+ takeRecords = vi.fn(() => []);
375
+ }
376
+
377
+ vi.stubGlobal('ResizeObserver', ResizeObserverMock);
378
+ vi.stubGlobal('MutationObserver', MutationObserverMock);
379
+
380
+ const {unmount} = render(
381
+ <Popover content={<span>Content</span>} label="Test">
382
+ <button type="button">Open</button>
383
+ </Popover>,
384
+ );
385
+
386
+ try {
387
+ const trigger = screen.getByRole('button', {name: 'Open'});
388
+ expect(resizeConstructed).not.toHaveBeenCalled();
389
+ expect(mutationConstructed).not.toHaveBeenCalled();
390
+
391
+ fireEvent.click(trigger);
392
+ expect(resizeConstructed).toHaveBeenCalledTimes(1);
393
+ expect(mutationConstructed).toHaveBeenCalledTimes(1);
394
+
395
+ fireEvent.click(trigger);
396
+ expect(resizeDisconnected).toHaveBeenCalledTimes(1);
397
+ expect(mutationDisconnected).toHaveBeenCalledTimes(1);
398
+ expect(resizeConstructed).toHaveBeenCalledTimes(1);
399
+ expect(mutationConstructed).toHaveBeenCalledTimes(1);
400
+ } finally {
401
+ unmount();
402
+ vi.unstubAllGlobals();
403
+ }
404
+ });
405
+
406
+ it('keeps aligned popovers anchored while applying viewport gutters', () => {
407
+ render(
408
+ <Popover content={<span>Content</span>} label="Test">
409
+ <button type="button">Open</button>
410
+ </Popover>,
411
+ );
412
+
413
+ fireEvent.click(screen.getByRole('button', {name: 'Open'}));
414
+
415
+ const layer = document.querySelector('[popover]');
416
+ expect(layer?.className).toContain('Popover__styles.viewportAligned');
417
+ expect(layer?.className).toContain('Popover__styles.viewportStart');
418
+ expect(layer).toHaveStyle(
419
+ 'min-width: min(anchor-size(width),calc(100% - max(var(--spacing-4),env(safe-area-inset-left,0px),env(safe-area-inset-right,0px))))',
420
+ );
421
+ });
422
+
423
+ it('mirrors the far-edge gutter for end-aligned popovers', () => {
424
+ render(
425
+ <Popover content={<span>Content</span>} label="Test" alignment="end">
426
+ <button type="button">Open</button>
427
+ </Popover>,
428
+ );
429
+
430
+ fireEvent.click(screen.getByRole('button', {name: 'Open'}));
431
+
432
+ const layer = document.querySelector('[popover]');
433
+ expect(layer?.className).toContain('Popover__styles.viewportAligned');
434
+ expect(layer?.className).toContain('Popover__styles.viewportEnd');
435
+ expect(layer?.className).not.toContain('Popover__styles.viewportStart');
436
+ });
437
+
438
+ it('reserves both inline gutters only for centered popovers', () => {
439
+ render(
440
+ <Popover content={<span>Content</span>} label="Test" alignment="center">
441
+ <button type="button">Open</button>
442
+ </Popover>,
443
+ );
444
+
445
+ fireEvent.click(screen.getByRole('button', {name: 'Open'}));
446
+
447
+ const layer = document.querySelector('[popover]');
448
+ expect(layer?.className).toContain('Popover__styles.viewportCentered');
449
+ expect(layer?.className).not.toContain('Popover__styles.viewportAligned');
450
+ expect(layer).toHaveStyle(
451
+ 'min-width: min(anchor-size(width),calc(100vi - max(var(--spacing-4),env(safe-area-inset-left,0px)) - max(var(--spacing-4),env(safe-area-inset-right,0px))))',
452
+ );
453
+ });
454
+
455
+ it('uses block-axis gutters for side placement', () => {
456
+ render(
457
+ <Popover
458
+ content={<span>Content</span>}
459
+ label="Test"
460
+ placement="end"
461
+ alignment="start">
462
+ <button type="button">Open</button>
463
+ </Popover>,
464
+ );
465
+
466
+ fireEvent.click(screen.getByRole('button', {name: 'Open'}));
467
+
468
+ const layer = document.querySelector('[popover]');
469
+ expect(layer?.className).toContain('Popover__styles.viewportBlockStart');
470
+ expect(layer?.className).not.toContain('Popover__styles.viewportStart');
471
+ });
472
+
473
+ it('preserves the dialog aria-haspopup contract for render-prop triggers', () => {
474
+ render(
475
+ <Popover
476
+ role="none"
477
+ content={
478
+ <div role="menu" aria-label="Actions">
479
+ Menu content
480
+ </div>
481
+ }
482
+ label="Actions">
483
+ {triggerProps => (
484
+ <button
485
+ type="button"
486
+ ref={triggerProps.ref}
487
+ onClick={triggerProps.onClick}
488
+ aria-haspopup={triggerProps['aria-haspopup']}
489
+ aria-expanded={triggerProps['aria-expanded']}
490
+ aria-controls={triggerProps['aria-controls']}>
491
+ Open menu
492
+ </button>
493
+ )}
494
+ </Popover>,
495
+ );
496
+
497
+ expect(screen.getByRole('button', {name: 'Open menu'})).toHaveAttribute(
498
+ 'aria-haspopup',
499
+ 'dialog',
500
+ );
501
+ });
502
+
208
503
  it('supports anchorRef sibling mode', () => {
209
504
  function AnchorRefTest() {
210
505
  const ref = useRef<HTMLButtonElement>(null);
@@ -437,6 +732,106 @@ describe('Popover', () => {
437
732
  });
438
733
 
439
734
  describe('focus restoration', () => {
735
+ it('focuses the dialog container without outlining an action after pointer activation', async () => {
736
+ render(
737
+ <Popover
738
+ content={<button type="button">Delete</button>}
739
+ label="Confirm deletion">
740
+ <button type="button">Open confirmation</button>
741
+ </Popover>,
742
+ );
743
+
744
+ fireEvent.click(screen.getByRole('button', {name: 'Open confirmation'}), {
745
+ detail: 1,
746
+ });
747
+
748
+ const dialog = screen.getByRole('dialog', {
749
+ name: 'Confirm deletion',
750
+ hidden: true,
751
+ });
752
+ await waitFor(() => expect(dialog).toHaveFocus());
753
+ expect(dialog).toHaveStyle({outline: 'none'});
754
+ expect(
755
+ screen.getByRole('button', {name: 'Delete', hidden: true}),
756
+ ).not.toHaveFocus();
757
+ });
758
+
759
+ it('focuses the first content control after keyboard activation', async () => {
760
+ render(
761
+ <Popover
762
+ content={<button type="button">Delete</button>}
763
+ label="Confirm deletion">
764
+ <button type="button">Open confirmation</button>
765
+ </Popover>,
766
+ );
767
+
768
+ fireEvent.click(screen.getByRole('button', {name: 'Open confirmation'}), {
769
+ detail: 0,
770
+ });
771
+
772
+ await waitFor(() =>
773
+ expect(
774
+ screen.getByRole('button', {name: 'Delete', hidden: true}),
775
+ ).toHaveFocus(),
776
+ );
777
+ });
778
+
779
+ it('focuses the dialog container when content has no controls', async () => {
780
+ render(
781
+ <Popover content={<span>Read-only content</span>} label="Read only">
782
+ <button type="button">Open read only</button>
783
+ </Popover>,
784
+ );
785
+ fireEvent.click(screen.getByRole('button', {name: 'Open read only'}));
786
+
787
+ const dialog = screen.getByRole('dialog', {
788
+ name: 'Read only',
789
+ hidden: true,
790
+ });
791
+ await waitFor(() => expect(dialog).toHaveFocus());
792
+ expect(
793
+ screen.getByRole('button', {name: 'Close popover', hidden: true}),
794
+ ).not.toHaveFocus();
795
+ });
796
+
797
+ it('prefers a genuine content control for initial focus', async () => {
798
+ render(
799
+ <Popover
800
+ content={<button type="button">Content action</button>}
801
+ label="Action popover">
802
+ <button type="button">Open action</button>
803
+ </Popover>,
804
+ );
805
+ fireEvent.click(screen.getByRole('button', {name: 'Open action'}));
806
+
807
+ await waitFor(() =>
808
+ expect(
809
+ screen.getByRole('button', {name: 'Content action', hidden: true}),
810
+ ).toHaveFocus(),
811
+ );
812
+ });
813
+
814
+ it('keeps the fallback close button reachable by Tab', async () => {
815
+ const user = userEvent.setup();
816
+ render(
817
+ <Popover content={<span>Read-only content</span>} label="Read only">
818
+ <button type="button">Open read only</button>
819
+ </Popover>,
820
+ );
821
+ fireEvent.click(screen.getByRole('button', {name: 'Open read only'}));
822
+ await waitFor(() =>
823
+ expect(
824
+ screen.getByRole('dialog', {name: 'Read only', hidden: true}),
825
+ ).toHaveFocus(),
826
+ );
827
+
828
+ await user.tab();
829
+
830
+ expect(
831
+ screen.getByRole('button', {name: 'Close popover', hidden: true}),
832
+ ).toHaveFocus();
833
+ });
834
+
440
835
  it('returns focus to the trigger when closed via Escape', () => {
441
836
  render(
442
837
  <Popover