@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.
- package/dist/BottomSheet/BottomSheet.d.ts +3 -1
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheet.js +4 -3
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts +2 -2
- package/dist/BottomSheet/BottomSheetEdgeTint.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetEdgeTint.js +7 -10
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -1
- package/dist/BottomSheet/BottomSheetSwitcher.js +1 -1
- package/dist/Button/Button.d.ts +2 -1
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +11 -4
- package/dist/Carousel/Carousel.d.ts.map +1 -1
- package/dist/Carousel/Carousel.js +2 -2
- package/dist/Chat/ChatToolCalls.d.ts.map +1 -1
- package/dist/Chat/ChatToolCalls.js +34 -17
- package/dist/ContextMenu/ContextMenu.d.ts +14 -3
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +149 -26
- package/dist/ContextMenu/index.d.ts +1 -0
- package/dist/ContextMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts +24 -5
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +340 -26
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuItem.js +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts +6 -2
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +43 -14
- package/dist/DropdownMenu/MenuBottomSheet.d.ts +20 -0
- package/dist/DropdownMenu/MenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheet.js +36 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts +10 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.d.ts.map +1 -0
- package/dist/DropdownMenu/MenuBottomSheetActionList.js +111 -0
- package/dist/DropdownMenu/index.d.ts +2 -1
- package/dist/DropdownMenu/index.d.ts.map +1 -1
- package/dist/DropdownMenu/menuWidth.d.ts +14 -0
- package/dist/DropdownMenu/menuWidth.d.ts.map +1 -0
- package/dist/DropdownMenu/menuWidth.js +35 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts +10 -0
- package/dist/DropdownMenu/useMenuOverflow.d.ts.map +1 -0
- package/dist/DropdownMenu/useMenuOverflow.js +60 -0
- package/dist/MoreMenu/MoreMenu.d.ts +8 -2
- package/dist/MoreMenu/MoreMenu.d.ts.map +1 -1
- package/dist/MoreMenu/MoreMenu.js +2 -0
- package/dist/MultiSelector/MultiSelector.d.ts +11 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +97 -68
- package/dist/MultiSelector/index.d.ts +1 -1
- package/dist/MultiSelector/index.d.ts.map +1 -1
- package/dist/Popover/Popover.d.ts +10 -6
- package/dist/Popover/Popover.d.ts.map +1 -1
- package/dist/Popover/Popover.js +141 -16
- package/dist/Popover/usePopover.d.ts +4 -0
- package/dist/Popover/usePopover.d.ts.map +1 -1
- package/dist/Popover/usePopover.js +52 -5
- package/dist/Selector/Selector.d.ts +10 -0
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +106 -80
- package/dist/Selector/SelectorBottomSheet.d.ts +21 -0
- package/dist/Selector/SelectorBottomSheet.d.ts.map +1 -0
- package/dist/Selector/SelectorBottomSheet.js +83 -0
- package/dist/Selector/index.d.ts +1 -1
- package/dist/Selector/index.d.ts.map +1 -1
- package/dist/Selector/selectorPresentation.stylex.d.ts +16 -0
- package/dist/Selector/selectorPresentation.stylex.d.ts.map +1 -0
- package/dist/Selector/selectorPresentation.stylex.js +21 -0
- package/dist/Selector/useSelectorPresentation.d.ts +32 -0
- package/dist/Selector/useSelectorPresentation.d.ts.map +1 -0
- package/dist/Selector/useSelectorPresentation.js +90 -0
- package/dist/Toast/useToastGesture.d.ts.map +1 -1
- package/dist/Toast/useToastGesture.js +46 -9
- package/dist/astryx.css +19 -1
- package/dist/hooks/useAdaptivePresentation.d.ts +5 -0
- package/dist/hooks/useAdaptivePresentation.d.ts.map +1 -0
- package/dist/hooks/useAdaptivePresentation.js +19 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts +7 -0
- package/dist/hooks/useFocusReturnVisibility.d.ts.map +1 -0
- package/dist/hooks/useFocusReturnVisibility.js +35 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +12 -0
- package/locales/en.json +20 -0
- package/locales/pseudo.json +15 -0
- package/package.json +2 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +6 -0
- package/src/BottomSheet/BottomSheet.test.tsx +29 -0
- package/src/BottomSheet/BottomSheet.tsx +7 -4
- package/src/BottomSheet/BottomSheetEdgeTint.test.tsx +4 -6
- package/src/BottomSheet/BottomSheetEdgeTint.tsx +7 -10
- package/src/BottomSheet/BottomSheetSwitcher.tsx +1 -2
- package/src/Button/Button.doc.mjs +56 -0
- package/src/Button/Button.test.tsx +10 -0
- package/src/Button/Button.tsx +13 -5
- package/src/ButtonGroup/ButtonGroup.doc.mjs +47 -0
- package/src/Carousel/Carousel.test.tsx +83 -0
- package/src/Carousel/Carousel.tsx +8 -2
- package/src/Chat/ChatToolCalls.test.tsx +44 -0
- package/src/Chat/ChatToolCalls.tsx +36 -15
- package/src/ContextMenu/ContextMenu.doc.mjs +13 -1
- package/src/ContextMenu/ContextMenu.test.tsx +131 -1
- package/src/ContextMenu/ContextMenu.tsx +199 -32
- package/src/ContextMenu/index.ts +1 -0
- package/src/DropdownMenu/DropdownMenu.doc.mjs +201 -40
- package/src/DropdownMenu/DropdownMenu.test.tsx +518 -7
- package/src/DropdownMenu/DropdownMenu.tsx +459 -28
- package/src/DropdownMenu/DropdownMenuContext.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuItem.tsx +2 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.doc.mjs +4 -2
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +124 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +72 -17
- package/src/DropdownMenu/MenuBottomSheet.tsx +46 -0
- package/src/DropdownMenu/MenuBottomSheetActionList.tsx +141 -0
- package/src/DropdownMenu/index.ts +2 -0
- package/src/DropdownMenu/menuWidth.ts +51 -0
- package/src/DropdownMenu/useMenuOverflow.ts +77 -0
- package/src/IconButton/IconButton.doc.mjs +38 -0
- package/src/MoreMenu/MoreMenu.doc.mjs +17 -0
- package/src/MoreMenu/MoreMenu.test.tsx +47 -0
- package/src/MoreMenu/MoreMenu.tsx +13 -1
- package/src/MultiSelector/MultiSelector.doc.mjs +16 -0
- package/src/MultiSelector/MultiSelector.test.tsx +127 -0
- package/src/MultiSelector/MultiSelector.tsx +121 -73
- package/src/MultiSelector/index.ts +1 -0
- package/src/Popover/Popover.doc.mjs +11 -11
- package/src/Popover/Popover.test.tsx +397 -2
- package/src/Popover/Popover.tsx +228 -21
- package/src/Popover/usePopover.tsx +92 -15
- package/src/ProgressBar/ProgressBar.spec.md +208 -0
- package/src/SegmentedControl/SegmentedControl.doc.mjs +56 -0
- package/src/Selector/Selector.doc.mjs +12 -0
- package/src/Selector/Selector.test.tsx +135 -0
- package/src/Selector/Selector.tsx +141 -82
- package/src/Selector/SelectorBottomSheet.tsx +97 -0
- package/src/Selector/index.ts +1 -0
- package/src/Selector/selectorPresentation.stylex.ts +20 -0
- package/src/Selector/useSelectorPresentation.ts +147 -0
- package/src/Toast/ToastViewport.test.tsx +118 -4
- package/src/Toast/useToastGesture.ts +61 -8
- package/src/ToggleButton/ToggleButton.doc.mjs +56 -0
- package/src/hooks/useAdaptivePresentation.ts +35 -0
- package/src/hooks/useFocusReturnVisibility.ts +45 -0
- package/src/hooks/useFocusTrap.test.tsx +40 -0
- package/src/hooks/useFocusTrap.ts +13 -0
- 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,
|
|
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
|