@astryxdesign/core 0.7.1 → 0.7.2-canary.01eab6a
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/AspectRatio/AspectRatio.d.ts +6 -8
- package/dist/AspectRatio/AspectRatio.d.ts.map +1 -1
- package/dist/AspectRatio/AspectRatio.js +44 -3
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
- package/dist/Button/Button.d.ts.map +1 -1
- package/dist/Button/Button.js +38 -14
- package/dist/CheckboxInput/CheckboxInput.d.ts +1 -1
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +3 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts +9 -1
- package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxListItem.js +4 -2
- package/dist/CommandPalette/CommandPaletteInput.d.ts +1 -1
- package/dist/CommandPalette/CommandPaletteInput.d.ts.map +1 -1
- package/dist/CommandPalette/CommandPaletteInput.js +37 -5
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +5 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +20 -8
- package/dist/Drawer/Drawer.d.ts +191 -0
- package/dist/Drawer/Drawer.d.ts.map +1 -0
- package/dist/Drawer/Drawer.js +382 -0
- package/dist/Drawer/DrawerHeader.d.ts +85 -0
- package/dist/Drawer/DrawerHeader.d.ts.map +1 -0
- package/dist/Drawer/DrawerHeader.js +134 -0
- package/dist/Drawer/index.d.ts +11 -0
- package/dist/Drawer/index.d.ts.map +1 -0
- package/dist/Drawer/index.js +11 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts +42 -0
- package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -0
- package/dist/Drawer/useDrawerDialogPresence.js +236 -0
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +47 -4
- package/dist/DropdownMenu/DropdownMenuContext.d.ts +17 -0
- package/dist/DropdownMenu/DropdownMenuContext.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuContext.js +21 -0
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts +5 -3
- package/dist/DropdownMenu/DropdownMenuDivider.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuDivider.js +7 -3
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +19 -8
- package/dist/EmptyState/EmptyState.d.ts.map +1 -1
- package/dist/EmptyState/EmptyState.js +4 -10
- package/dist/Field/Field.d.ts +1 -1
- package/dist/Field/Field.d.ts.map +1 -1
- package/dist/Field/Field.js +8 -4
- package/dist/Grid/GridSpan.d.ts.map +1 -1
- package/dist/Grid/GridSpan.js +1 -0
- package/dist/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +4 -2
- package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
- package/dist/HoverCard/useHoverCard.js +10 -2
- package/dist/Item/Item.d.ts +6 -2
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +19 -4
- package/dist/Layer/layerViewportInset.stylex.d.ts +6 -2
- package/dist/Layer/layerViewportInset.stylex.d.ts.map +1 -1
- package/dist/Layer/layerViewportInset.stylex.js +6 -2
- package/dist/Layer/useLayer.d.ts.map +1 -1
- package/dist/Layer/useLayer.js +13 -7
- package/dist/Layer/useLayerDismissal.d.ts +1 -1
- package/dist/Layer/useLayerDismissal.js +1 -1
- package/dist/Layout/container.stylex.d.ts +5 -5
- package/dist/Layout/container.stylex.js +4 -4
- package/dist/List/List.d.ts +46 -19
- package/dist/List/List.d.ts.map +1 -1
- package/dist/List/List.js +28 -10
- package/dist/List/ListContext.d.ts +8 -3
- package/dist/List/ListContext.d.ts.map +1 -1
- package/dist/List/ListContext.js +7 -2
- package/dist/List/ListItem.d.ts +13 -5
- package/dist/List/ListItem.d.ts.map +1 -1
- package/dist/List/ListItem.js +21 -8
- package/dist/List/index.d.ts +1 -1
- package/dist/List/index.d.ts.map +1 -1
- package/dist/Markdown/Markdown.d.ts.map +1 -1
- package/dist/Markdown/Markdown.js +8 -1
- package/dist/Markdown/plugins/frontmatter.d.ts.map +1 -1
- package/dist/Markdown/plugins/frontmatter.js +7 -3
- package/dist/MetadataList/MetadataList.d.ts.map +1 -1
- package/dist/MetadataList/MetadataList.js +5 -1
- package/dist/MetadataList/MetadataListContext.d.ts +1 -1
- package/dist/MetadataList/MetadataListContext.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +22 -8
- package/dist/MultiSelector/OptionActions.a11y.chromium.spec.js +100 -2
- package/dist/MultiSelector/hooks.d.ts.map +1 -1
- package/dist/MultiSelector/hooks.js +3 -1
- package/dist/Pagination/Pagination.d.ts +8 -5
- package/dist/Pagination/Pagination.d.ts.map +1 -1
- package/dist/Pagination/Pagination.js +21 -8
- package/dist/Popover/Popover.js +2 -2
- package/dist/PowerSearch/PowerSearch.js +1 -1
- package/dist/RadioList/RadioList.d.ts +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +6 -2
- package/dist/RadioList/RadioListItem.d.ts +9 -1
- package/dist/RadioList/RadioListItem.d.ts.map +1 -1
- package/dist/RadioList/RadioListItem.js +2 -0
- package/dist/Selector/hooks.d.ts.map +1 -1
- package/dist/Selector/hooks.js +6 -2
- package/dist/SideNav/SideNavItem.d.ts +8 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +71 -9
- package/dist/Switch/Switch.d.ts +1 -1
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +13 -1
- package/dist/TabList/TabMenu.d.ts +2 -2
- package/dist/TabList/TabMenu.d.ts.map +1 -1
- package/dist/TabList/TabMenu.js +6 -3
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -2
- package/dist/Text/useTruncation.d.ts +7 -5
- package/dist/Text/useTruncation.d.ts.map +1 -1
- package/dist/Text/useTruncation.js +16 -5
- package/dist/ToggleButton/ToggleButton.d.ts +3 -2
- package/dist/ToggleButton/ToggleButton.d.ts.map +1 -1
- package/dist/ToggleButton/ToggleButton.js +12 -1
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +11 -0
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +3 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +3 -3
- package/dist/TreeList/TreeList.d.ts.map +1 -1
- package/dist/TreeList/TreeList.js +1 -0
- package/dist/TreeList/TreeListBranches.d.ts.map +1 -1
- package/dist/TreeList/TreeListBranches.js +2 -2
- package/dist/TreeList/TreeListItem.d.ts.map +1 -1
- package/dist/TreeList/TreeListItem.js +57 -9
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +26 -0
- package/dist/astryx.css +47 -13
- package/dist/hooks/useEntryAnimation.d.ts +2 -1
- package/dist/hooks/useEntryAnimation.d.ts.map +1 -1
- package/dist/hooks/useEntryAnimation.js +14 -12
- package/dist/hooks/useIsHydrating.d.ts +6 -0
- package/dist/hooks/useIsHydrating.d.ts.map +1 -0
- package/dist/hooks/useIsHydrating.js +30 -0
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +18 -13
- package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/en.generated.js +112 -1
- package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
- package/dist/i18n/generated-locales/pseudo.generated.js +112 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/derivedVarRegistry.d.ts.map +1 -1
- package/dist/theme/derivedVarRegistry.js +0 -3
- package/dist/utils/focusOutline.stylex.d.ts +4 -0
- package/dist/utils/focusOutline.stylex.d.ts.map +1 -1
- package/dist/utils/focusOutline.stylex.js +4 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +15 -9
- package/dist/utils/sharedMutationObserver.d.ts +21 -0
- package/dist/utils/sharedMutationObserver.d.ts.map +1 -0
- package/dist/utils/sharedMutationObserver.js +69 -0
- package/locales/en.json +444 -0
- package/locales/pseudo.json +333 -0
- package/package.json +9 -4
- package/src/AspectRatio/AspectRatio.test.tsx +44 -43
- package/src/AspectRatio/AspectRatio.tsx +38 -18
- package/src/AspectRatio/__tests__/AspectRatioFitLayers.a11y.browser.spec.ts +157 -0
- package/src/Avatar/Avatar.doc.mjs +5 -3
- package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
- package/src/Button/Button.doc.mjs +2 -2
- package/src/Button/Button.test.tsx +155 -9
- package/src/Button/Button.tsx +39 -16
- package/src/Button/__tests__/Button.a11y.known-failures.ts +1 -62
- package/src/Button/__tests__/Button.a11y.states.ts +0 -10
- package/src/Chat/__tests__/ChatMessageMetadata.a11y.chromium.spec.ts +3 -1
- package/src/CheckboxInput/CheckboxInput.test.tsx +34 -0
- package/src/CheckboxInput/CheckboxInput.tsx +3 -1
- package/src/CheckboxList/CheckboxList.test.tsx +22 -0
- package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
- package/src/CheckboxList/CheckboxListItem.tsx +10 -0
- package/src/CommandPalette/CommandPaletteInput.test.tsx +53 -4
- package/src/CommandPalette/CommandPaletteInput.tsx +33 -3
- package/src/CommandPalette/__tests__/CommandPaletteInputAudit.a11y.chromium.spec.ts +335 -0
- package/src/CommandPalette/__tests__/CommandPaletteItemAudit.a11y.chromium.spec.ts +292 -0
- package/src/ContextMenu/ContextMenu.doc.mjs +2 -3
- package/src/ContextMenu/ContextMenu.tsx +4 -1
- package/src/ContextMenu/__tests__/ContextMenuAudit.a11y.chromium.spec.ts +438 -0
- package/src/Dialog/Dialog.test.tsx +46 -0
- package/src/Dialog/Dialog.tsx +20 -7
- package/src/Dialog/__tests__/Dialog.a11y.known-failures.ts +0 -14
- package/src/Drawer/Drawer.doc.mjs +472 -0
- package/src/Drawer/Drawer.spec.md +357 -0
- package/src/Drawer/Drawer.test.tsx +1300 -0
- package/src/Drawer/Drawer.tsx +613 -0
- package/src/Drawer/DrawerDirection.test.tsx +127 -0
- package/src/Drawer/DrawerHeader.doc.mjs +293 -0
- package/src/Drawer/DrawerHeader.test.tsx +102 -0
- package/src/Drawer/DrawerHeader.tsx +235 -0
- package/src/Drawer/__tests__/DrawerAudit.a11y.chromium.spec.ts +401 -0
- package/src/Drawer/__tests__/DrawerHeaderAudit.a11y.chromium.spec.ts +297 -0
- package/src/Drawer/index.ts +13 -0
- package/src/Drawer/useDrawerDialogPresence.ts +284 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +62 -13
- package/src/DropdownMenu/DropdownMenu.tsx +47 -4
- package/src/DropdownMenu/DropdownMenuContext.tsx +24 -0
- package/src/DropdownMenu/DropdownMenuDivider.doc.mjs +2 -2
- package/src/DropdownMenu/DropdownMenuDivider.spec.md +192 -0
- package/src/DropdownMenu/DropdownMenuDivider.tsx +9 -5
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +19 -5
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.chromium.spec.ts +145 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.known-failures.ts +26 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.renders.tsx +65 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.states.ts +138 -0
- package/src/DropdownMenu/__tests__/Menu.a11y.test.tsx +136 -0
- package/src/EmptyState/EmptyState.doc.mjs +3 -2
- package/src/EmptyState/EmptyState.spec.md +189 -0
- package/src/EmptyState/EmptyState.test.tsx +74 -3
- package/src/EmptyState/EmptyState.tsx +4 -8
- package/src/Field/Field.doc.mjs +14 -8
- package/src/Field/Field.spec.md +217 -0
- package/src/Field/Field.test.tsx +60 -2
- package/src/Field/Field.tsx +17 -8
- package/src/Grid/GridSpan.tsx +3 -0
- package/src/Heading/Heading.test.tsx +57 -0
- package/src/Heading/Heading.tsx +6 -2
- package/src/HoverCard/HoverCard.test.tsx +58 -0
- package/src/HoverCard/useHoverCard.tsx +10 -2
- package/src/IconButton/IconButton.doc.mjs +2 -2
- package/src/IconButton/IconButton.test.tsx +3 -1
- package/src/Item/Item.doc.mjs +12 -8
- package/src/Item/Item.spec.md +226 -0
- package/src/Item/Item.test.tsx +61 -0
- package/src/Item/Item.tsx +40 -4
- package/src/Layer/layerViewportInset.source-build.test.mjs +90 -0
- package/src/Layer/layerViewportInset.stylex.ts +6 -2
- package/src/Layer/layerViewportInset.test.ts +1 -0
- package/src/Layer/useLayer.tsx +24 -13
- package/src/Layer/useLayerDismissal.ts +1 -1
- package/src/Layout/container.stylex.ts +6 -6
- package/src/List/List.doc.mjs +14 -1
- package/src/List/List.spec.md +129 -77
- package/src/List/List.test.tsx +163 -94
- package/src/List/List.tsx +74 -18
- package/src/List/ListContext.tsx +9 -3
- package/src/List/ListItem.doc.mjs +24 -5
- package/src/List/ListItem.tsx +56 -27
- package/src/List/index.ts +1 -1
- package/src/Markdown/Markdown.tsx +14 -1
- package/src/Markdown/plugins/frontmatter.test.tsx +25 -0
- package/src/Markdown/plugins/frontmatter.ts +7 -3
- package/src/MetadataList/MetadataList.doc.mjs +2 -2
- package/src/MetadataList/MetadataList.test.tsx +38 -0
- package/src/MetadataList/MetadataList.tsx +8 -1
- package/src/MetadataList/MetadataListContext.tsx +1 -1
- package/src/MultiSelector/MultiSelector.test.tsx +159 -0
- package/src/MultiSelector/MultiSelector.tsx +40 -7
- package/src/MultiSelector/OptionActions.a11y.chromium.spec.ts +112 -2
- package/src/MultiSelector/hooks.ts +3 -1
- package/src/MultiSelector/useMultiCombobox.doc.mjs +267 -0
- package/src/Pagination/Pagination.doc.mjs +7 -7
- package/src/Pagination/Pagination.spec.md +9 -9
- package/src/Pagination/Pagination.test.tsx +242 -0
- package/src/Pagination/Pagination.tsx +27 -11
- package/src/Popover/Popover.test.tsx +2 -2
- package/src/Popover/Popover.tsx +4 -4
- package/src/PowerSearch/PowerSearch.test.tsx +85 -0
- package/src/PowerSearch/PowerSearch.tsx +2 -2
- package/src/RadioList/RadioList.fr10.test.tsx +45 -2
- package/src/RadioList/RadioList.test.tsx +22 -0
- package/src/RadioList/RadioList.tsx +4 -1
- package/src/RadioList/RadioListItem.doc.mjs +16 -0
- package/src/RadioList/RadioListItem.tsx +10 -1
- package/src/Selector/hooks.ts +6 -2
- package/src/Selector/useCombobox.doc.mjs +237 -0
- package/src/SideNav/SideNav.test.tsx +179 -0
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +96 -17
- package/src/Slider/Slider.spec.md +39 -18
- package/src/Switch/Switch.test.tsx +49 -0
- package/src/Switch/Switch.tsx +11 -0
- package/src/TabList/TabList.spec.md +57 -44
- package/src/TabList/TabList.test.tsx +67 -0
- package/src/TabList/TabMenu.doc.mjs +4 -3
- package/src/TabList/TabMenu.tsx +7 -4
- package/src/Text/Text.test.tsx +57 -0
- package/src/Text/Text.tsx +6 -2
- package/src/Text/useTruncation.test.tsx +69 -0
- package/src/Text/useTruncation.ts +16 -5
- package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
- package/src/ToggleButton/ToggleButton.test.tsx +95 -0
- package/src/ToggleButton/ToggleButton.tsx +12 -1
- package/src/Tokenizer/Tokenizer.test.tsx +54 -1
- package/src/Tooltip/Tooltip.test.tsx +31 -0
- package/src/Tooltip/useTooltip.test.tsx +13 -0
- package/src/Tooltip/useTooltip.tsx +11 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +22 -0
- package/src/TopNav/TopNavMegaMenu.tsx +7 -3
- package/src/TopNav/TopNavMenu.test.tsx +19 -0
- package/src/TopNav/TopNavMenu.tsx +11 -3
- package/src/TreeList/TreeList.doc.mjs +1 -0
- package/src/TreeList/TreeList.spec.md +29 -17
- package/src/TreeList/TreeList.test.tsx +327 -6
- package/src/TreeList/TreeList.tsx +5 -1
- package/src/TreeList/TreeListBranches.tsx +6 -0
- package/src/TreeList/TreeListItem.tsx +88 -13
- package/src/Typeahead/BaseTypeahead.tsx +26 -0
- package/src/Typeahead/Typeahead.test.tsx +260 -2
- package/src/docPropLiterals.test.ts +36 -10
- package/src/hooks/useEntryAnimation.doc.mjs +4 -4
- package/src/hooks/useEntryAnimation.ssr.test.tsx +143 -0
- package/src/hooks/useEntryAnimation.ts +15 -16
- package/src/hooks/useIsHydrating.test.tsx +70 -0
- package/src/hooks/useIsHydrating.ts +31 -0
- package/src/hooks/useMenuHover.ts +18 -13
- package/src/i18n/InternationalizationProvider.doc.mjs +8 -0
- package/src/i18n/InternationalizationProvider.spec.md +252 -0
- package/src/i18n/__tests__/InternationalizationProvider.test.tsx +128 -0
- package/src/i18n/generated-locales/en.generated.ts +121 -0
- package/src/i18n/generated-locales/pseudo.generated.ts +121 -0
- package/src/i18n/useTranslator.test.tsx +37 -0
- package/src/index.ts +1 -0
- package/src/reset.css +0 -29
- package/src/theme/derivedVarRegistry.test.ts +7 -3
- package/src/theme/derivedVarRegistry.ts +0 -3
- package/src/utils/focusOutline.stylex.ts +4 -0
- package/src/utils/plainDate.test.ts +37 -1
- package/src/utils/plainDate.ts +15 -5
- package/src/utils/sharedMutationObserver.test.ts +53 -0
- package/src/utils/sharedMutationObserver.ts +78 -0
package/src/Text/Text.test.tsx
CHANGED
|
@@ -346,3 +346,60 @@ describe('truncated text shows one tooltip, not two', () => {
|
|
|
346
346
|
}
|
|
347
347
|
});
|
|
348
348
|
});
|
|
349
|
+
|
|
350
|
+
// With hasTruncateTooltip={false} nothing reads the measured overflow, so a
|
|
351
|
+
// long table of truncated cells must not pay a layout read per cell.
|
|
352
|
+
function countScrollWidthReads(run: () => void): number {
|
|
353
|
+
const proto = window.HTMLElement.prototype;
|
|
354
|
+
const original = Object.getOwnPropertyDescriptor(proto, 'scrollWidth');
|
|
355
|
+
let reads = 0;
|
|
356
|
+
Object.defineProperty(proto, 'scrollWidth', {
|
|
357
|
+
configurable: true,
|
|
358
|
+
get() {
|
|
359
|
+
reads += 1;
|
|
360
|
+
return 0;
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
run();
|
|
365
|
+
} finally {
|
|
366
|
+
if (original) {
|
|
367
|
+
Object.defineProperty(proto, 'scrollWidth', original);
|
|
368
|
+
} else {
|
|
369
|
+
delete (proto as {scrollWidth?: number}).scrollWidth;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return reads;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
describe('Text maxLines without a truncation tooltip', () => {
|
|
376
|
+
it('takes no truncation measurement when hasTruncateTooltip is false', () => {
|
|
377
|
+
const reads = countScrollWidthReads(() => {
|
|
378
|
+
render(
|
|
379
|
+
<>
|
|
380
|
+
{Array.from({length: 20}, (_, index) => (
|
|
381
|
+
<Text
|
|
382
|
+
type="body"
|
|
383
|
+
maxLines={1}
|
|
384
|
+
hasTruncateTooltip={false}
|
|
385
|
+
key={index}>
|
|
386
|
+
Cell {index} with text far wider than its column
|
|
387
|
+
</Text>
|
|
388
|
+
))}
|
|
389
|
+
</>,
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
expect(reads).toBe(0);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('still measures when the tooltip is enabled', () => {
|
|
396
|
+
const reads = countScrollWidthReads(() => {
|
|
397
|
+
render(
|
|
398
|
+
<Text type="body" maxLines={1}>
|
|
399
|
+
Text far wider than its column
|
|
400
|
+
</Text>,
|
|
401
|
+
);
|
|
402
|
+
});
|
|
403
|
+
expect(reads).toBeGreaterThan(0);
|
|
404
|
+
});
|
|
405
|
+
});
|
package/src/Text/Text.tsx
CHANGED
|
@@ -260,8 +260,12 @@ export function Text({
|
|
|
260
260
|
// Resolve display - force block when maxLines > 0 or hasCapsize
|
|
261
261
|
const resolvedDisplay = maxLines > 0 || hasCapsize ? 'block' : display;
|
|
262
262
|
|
|
263
|
-
// Truncation detection
|
|
264
|
-
|
|
263
|
+
// Truncation detection. Only the tooltip reads the measured overflow, so
|
|
264
|
+
// with the tooltip off the text is clipped by CSS alone and nothing is
|
|
265
|
+
// measured or observed.
|
|
266
|
+
const truncation = useTruncation({
|
|
267
|
+
maxLines: hasTruncateTooltip === false ? 0 : maxLines,
|
|
268
|
+
});
|
|
265
269
|
|
|
266
270
|
// Tooltip for truncated text
|
|
267
271
|
const tooltipPlacement: LayerPlacement =
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file useTruncation.test.tsx
|
|
5
|
+
* Tests for useTruncation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {render, screen, waitFor} from '@testing-library/react';
|
|
9
|
+
import {afterEach, describe, it, expect, vi} from 'vitest';
|
|
10
|
+
import {useTruncation} from './useTruncation';
|
|
11
|
+
|
|
12
|
+
function Probe({text}: {text: string}) {
|
|
13
|
+
const truncation = useTruncation({maxLines: 1});
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<span ref={truncation.ref}>{text}</span>
|
|
17
|
+
<output data-testid="full-text">{truncation.fullText}</output>
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A status line at a fixed width never resizes when its message changes, so
|
|
23
|
+
// the tooltip text must follow the content, not only the element's size.
|
|
24
|
+
describe('useTruncation', () => {
|
|
25
|
+
it('re-measures when the text changes without a resize', async () => {
|
|
26
|
+
const {rerender} = render(<Probe text="planning the first step" />);
|
|
27
|
+
expect(screen.getByTestId('full-text')).toHaveTextContent(
|
|
28
|
+
'planning the first step',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
rerender(<Probe text="writing the second step" />);
|
|
32
|
+
|
|
33
|
+
await waitFor(() =>
|
|
34
|
+
expect(screen.getByTestId('full-text')).toHaveTextContent(
|
|
35
|
+
'writing the second step',
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('content observation', () => {
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
vi.unstubAllGlobals();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('shares one observer across every truncating instance', () => {
|
|
46
|
+
const NativeMutationObserver = globalThis.MutationObserver;
|
|
47
|
+
let created = 0;
|
|
48
|
+
vi.stubGlobal(
|
|
49
|
+
'MutationObserver',
|
|
50
|
+
class extends NativeMutationObserver {
|
|
51
|
+
constructor(callback: MutationCallback) {
|
|
52
|
+
super(callback);
|
|
53
|
+
created += 1;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
render(
|
|
59
|
+
<>
|
|
60
|
+
{Array.from({length: 50}, (_, index) => (
|
|
61
|
+
<Probe key={index} text={`cell ${index}`} />
|
|
62
|
+
))}
|
|
63
|
+
</>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(created).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file useTruncation.ts
|
|
7
|
-
* @input Uses React hooks, sharedResizeObserver
|
|
7
|
+
* @input Uses React hooks, sharedResizeObserver, sharedMutationObserver
|
|
8
8
|
* @output Exports useTruncation hook for detecting text overflow
|
|
9
9
|
* @position Hook; consumed by Text.tsx, Heading.tsx
|
|
10
10
|
*
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import {useCallback, useRef, useState, type RefCallback} from 'react';
|
|
16
16
|
import {observeResize} from '../utils/sharedResizeObserver';
|
|
17
|
+
import {observeContent} from '../utils/sharedMutationObserver';
|
|
17
18
|
|
|
18
19
|
export interface UseTruncationOptions {
|
|
19
20
|
/**
|
|
@@ -43,10 +44,12 @@ export interface UseTruncationReturn {
|
|
|
43
44
|
/**
|
|
44
45
|
* Hook for detecting text overflow/truncation.
|
|
45
46
|
*
|
|
46
|
-
* Uses a shared ResizeObserver singleton (via observeResize)
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* Uses a shared ResizeObserver singleton (via observeResize) for size
|
|
48
|
+
* changes and a shared MutationObserver singleton (via observeContent) for
|
|
49
|
+
* content changes, so even hundreds of table cells create one observer of
|
|
50
|
+
* each kind. Observing content matters because an element whose text changes
|
|
51
|
+
* without resizing (a fixed-width status line) must still be re-measured for
|
|
52
|
+
* the tooltip text and the truncated state to follow the current content.
|
|
50
53
|
*
|
|
51
54
|
* - Single-line: compares scrollWidth > offsetWidth
|
|
52
55
|
* - Multi-line: uses Range.getBoundingClientRect() to measure actual content
|
|
@@ -71,6 +74,7 @@ export function useTruncation(
|
|
|
71
74
|
const [fullText, setFullText] = useState('');
|
|
72
75
|
const elementRef = useRef<HTMLElement | null>(null);
|
|
73
76
|
const unobserveRef = useRef<(() => void) | null>(null);
|
|
77
|
+
const unobserveContentRef = useRef<(() => void) | null>(null);
|
|
74
78
|
|
|
75
79
|
const checkTruncation = useCallback(
|
|
76
80
|
(element: HTMLElement) => {
|
|
@@ -113,6 +117,8 @@ export function useTruncation(
|
|
|
113
117
|
// by element leaves any other observer of the same node untouched.
|
|
114
118
|
unobserveRef.current?.();
|
|
115
119
|
unobserveRef.current = null;
|
|
120
|
+
unobserveContentRef.current?.();
|
|
121
|
+
unobserveContentRef.current = null;
|
|
116
122
|
|
|
117
123
|
elementRef.current = element;
|
|
118
124
|
|
|
@@ -127,6 +133,11 @@ export function useTruncation(
|
|
|
127
133
|
} else {
|
|
128
134
|
checkTruncation(element);
|
|
129
135
|
}
|
|
136
|
+
// New text in a box that keeps its size fires no resize, so the
|
|
137
|
+
// captured text and truncated state would otherwise stay stale.
|
|
138
|
+
unobserveContentRef.current = observeContent(element, () => {
|
|
139
|
+
checkTruncation(element);
|
|
140
|
+
});
|
|
130
141
|
} else {
|
|
131
142
|
setIsTruncated(false);
|
|
132
143
|
setFullText('');
|
|
@@ -191,6 +191,21 @@ for (const [surface, presentation, selector] of SURFACES) {
|
|
|
191
191
|
expect(own?.textContent).toContain('Start');
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
it('puts hidden, inert, and dir on the control', () => {
|
|
195
|
+
render(
|
|
196
|
+
<TimeInput
|
|
197
|
+
label="Start"
|
|
198
|
+
presentation={presentation}
|
|
199
|
+
hidden
|
|
200
|
+
inert
|
|
201
|
+
dir="rtl"
|
|
202
|
+
/>,
|
|
203
|
+
);
|
|
204
|
+
expect(control(selector)).toHaveAttribute('hidden');
|
|
205
|
+
expect(control(selector)).toHaveAttribute('inert');
|
|
206
|
+
expect(control(selector)).toHaveAttribute('dir', 'rtl');
|
|
207
|
+
});
|
|
208
|
+
|
|
194
209
|
it('keeps control* on the control box in a horizontal-labels FormLayout', () => {
|
|
195
210
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
196
211
|
const {container} = render(
|
|
@@ -497,6 +497,78 @@ describe('ToggleButton', () => {
|
|
|
497
497
|
expect(button).not.toHaveAttribute('aria-busy', 'true');
|
|
498
498
|
await act(async () => {});
|
|
499
499
|
});
|
|
500
|
+
|
|
501
|
+
// family:buttons FR5: a caller onClick on a standalone ToggleButton runs
|
|
502
|
+
// before onPressedChange and pressedChangeAction, never replaces the toggle
|
|
503
|
+
// request, and cancels it only by calling preventDefault() itself.
|
|
504
|
+
describe('with a caller onClick', () => {
|
|
505
|
+
it('runs the caller onClick first, then requests the next state and runs the Action', async () => {
|
|
506
|
+
const calls: string[] = [];
|
|
507
|
+
render(
|
|
508
|
+
<ToggleButton
|
|
509
|
+
label="Compact"
|
|
510
|
+
isPressed={false}
|
|
511
|
+
onClick={() => calls.push('onClick')}
|
|
512
|
+
onPressedChange={next => calls.push(`onPressedChange:${next}`)}
|
|
513
|
+
pressedChangeAction={next => {
|
|
514
|
+
calls.push(`pressedChangeAction:${next}`);
|
|
515
|
+
}}
|
|
516
|
+
/>,
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
fireEvent.click(screen.getByRole('button', {name: 'Compact'}));
|
|
520
|
+
await act(async () => {});
|
|
521
|
+
|
|
522
|
+
expect(calls).toEqual([
|
|
523
|
+
'onClick',
|
|
524
|
+
'onPressedChange:true',
|
|
525
|
+
'pressedChangeAction:true',
|
|
526
|
+
]);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it('requests no next state when the caller onClick calls preventDefault', async () => {
|
|
530
|
+
const onPressedChange = vi.fn();
|
|
531
|
+
const pressedChangeAction = vi.fn();
|
|
532
|
+
render(
|
|
533
|
+
<ToggleButton
|
|
534
|
+
label="Compact"
|
|
535
|
+
isPressed={false}
|
|
536
|
+
onClick={event => event.preventDefault()}
|
|
537
|
+
onPressedChange={onPressedChange}
|
|
538
|
+
pressedChangeAction={pressedChangeAction}
|
|
539
|
+
/>,
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const button = screen.getByRole('button', {name: 'Compact'});
|
|
543
|
+
fireEvent.click(button);
|
|
544
|
+
await act(async () => {});
|
|
545
|
+
|
|
546
|
+
expect(onPressedChange).not.toHaveBeenCalled();
|
|
547
|
+
expect(pressedChangeAction).not.toHaveBeenCalled();
|
|
548
|
+
expect(button).toHaveAttribute('aria-pressed', 'false');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('still requests the next state when the click was already prevented before the caller onClick ran', () => {
|
|
552
|
+
const onPressedChange = vi.fn();
|
|
553
|
+
render(
|
|
554
|
+
<div onClickCapture={event => event.preventDefault()}>
|
|
555
|
+
<ToggleButton
|
|
556
|
+
label="Compact"
|
|
557
|
+
isPressed={false}
|
|
558
|
+
onClick={() => {}}
|
|
559
|
+
onPressedChange={onPressedChange}
|
|
560
|
+
/>
|
|
561
|
+
</div>,
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
fireEvent.click(screen.getByRole('button', {name: 'Compact'}));
|
|
565
|
+
|
|
566
|
+
expect(onPressedChange).toHaveBeenCalledExactlyOnceWith(
|
|
567
|
+
true,
|
|
568
|
+
expect.anything(),
|
|
569
|
+
);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
500
572
|
});
|
|
501
573
|
|
|
502
574
|
// =============================================================================
|
|
@@ -721,6 +793,29 @@ describe.each(['single', 'multiple'] as const)(
|
|
|
721
793
|
expect(onPressedChange).not.toHaveBeenCalled();
|
|
722
794
|
expect(pressedChangeAction).not.toHaveBeenCalled();
|
|
723
795
|
});
|
|
796
|
+
|
|
797
|
+
// family:buttons FR11: a member's selection request comes from the group,
|
|
798
|
+
// and a caller onClick on the member must not replace it.
|
|
799
|
+
it('still asks the group to toggle a member that receives an onClick', async () => {
|
|
800
|
+
const onChange = vi.fn();
|
|
801
|
+
const onClick = vi.fn();
|
|
802
|
+
const groupProps =
|
|
803
|
+
type === 'single'
|
|
804
|
+
? {type, value: null, onChange}
|
|
805
|
+
: {type, value: [], onChange};
|
|
806
|
+
render(
|
|
807
|
+
<ToggleButtonGroup {...groupProps} label="View">
|
|
808
|
+
<ToggleButton value="grid" label="Grid" onClick={onClick} />
|
|
809
|
+
</ToggleButtonGroup>,
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
await userEvent.setup().click(screen.getByRole('button', {name: 'Grid'}));
|
|
813
|
+
|
|
814
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
815
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith(
|
|
816
|
+
type === 'single' ? 'grid' : ['grid'],
|
|
817
|
+
);
|
|
818
|
+
});
|
|
724
819
|
},
|
|
725
820
|
);
|
|
726
821
|
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* @position Thin wrapper over Button; adds controlled toggle pattern
|
|
10
10
|
*
|
|
11
11
|
* ToggleButton wraps Button with `isPressed` and adds:
|
|
12
|
-
* - `onPressedChange` from internal `onClick
|
|
12
|
+
* - `onPressedChange` from internal `onClick`, after any caller `onClick`;
|
|
13
|
+
* `pressedChangeAction` via Button
|
|
13
14
|
* - `pressedIcon` for outline-to-filled icon swap
|
|
14
15
|
* - Font weight shift on press with width reservation to prevent layout shift
|
|
15
16
|
* - Group integration via ToggleButtonGroupContext
|
|
@@ -258,6 +259,7 @@ export function ToggleButton({
|
|
|
258
259
|
xstyle,
|
|
259
260
|
className,
|
|
260
261
|
style,
|
|
262
|
+
onClick: onClickProp,
|
|
261
263
|
...props
|
|
262
264
|
}: ToggleButtonProps): ReactNode {
|
|
263
265
|
const group = useToggleButtonGroup();
|
|
@@ -287,11 +289,20 @@ export function ToggleButton({
|
|
|
287
289
|
if (isDisabled) {
|
|
288
290
|
return;
|
|
289
291
|
}
|
|
292
|
+
// A caller onClick (a trigger wrapper often supplies one) runs first and
|
|
293
|
+
// never replaces the toggle's own request (family:buttons FR5, FR11).
|
|
294
|
+
const wasPrevented = event.defaultPrevented;
|
|
295
|
+
onClickProp?.(event);
|
|
290
296
|
if (group && value != null) {
|
|
291
297
|
group.toggle(value);
|
|
292
298
|
event.preventDefault();
|
|
293
299
|
return;
|
|
294
300
|
}
|
|
301
|
+
// A standalone toggle is cancelled only by the caller's own
|
|
302
|
+
// preventDefault(); Button then skips pressedChangeAction too.
|
|
303
|
+
if (!wasPrevented && event.defaultPrevented) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
295
306
|
|
|
296
307
|
onPressedChangeProp?.(nextPressed, event);
|
|
297
308
|
};
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
afterEach,
|
|
21
21
|
} from 'vitest';
|
|
22
22
|
import {render, screen, fireEvent, act, waitFor} from '@testing-library/react';
|
|
23
|
-
import {createRef, Profiler} from 'react';
|
|
23
|
+
import {createRef, Profiler, useState} from 'react';
|
|
24
24
|
import userEvent from '@testing-library/user-event';
|
|
25
25
|
import {Tokenizer} from './Tokenizer';
|
|
26
26
|
import {__resetLiveRegionsForTest} from '../hooks/useAnnounce';
|
|
@@ -1790,6 +1790,59 @@ describe('Tokenizer end-lane reserve', () => {
|
|
|
1790
1790
|
expect(renderedTokens).toEqual([]);
|
|
1791
1791
|
});
|
|
1792
1792
|
|
|
1793
|
+
it('clears the busy spinner when a token is added while a bootstrap load is in flight', async () => {
|
|
1794
|
+
let settle: (items: SearchableItem[]) => void = () => {};
|
|
1795
|
+
const bootstrap = vi.fn(
|
|
1796
|
+
async () =>
|
|
1797
|
+
new Promise<SearchableItem[]>(resolve => {
|
|
1798
|
+
settle = resolve;
|
|
1799
|
+
}),
|
|
1800
|
+
);
|
|
1801
|
+
const source: SearchSource = {search: () => [], bootstrap};
|
|
1802
|
+
|
|
1803
|
+
function Harness() {
|
|
1804
|
+
const [value, setValue] = useState<SearchableItem[]>([]);
|
|
1805
|
+
return (
|
|
1806
|
+
<>
|
|
1807
|
+
<Tokenizer
|
|
1808
|
+
label="Items"
|
|
1809
|
+
searchSource={source}
|
|
1810
|
+
value={value}
|
|
1811
|
+
onChange={setValue}
|
|
1812
|
+
hasEntriesOnFocus
|
|
1813
|
+
/>
|
|
1814
|
+
<button type="button" onClick={() => setValue([users[0]])}>
|
|
1815
|
+
Add Token
|
|
1816
|
+
</button>
|
|
1817
|
+
</>
|
|
1818
|
+
);
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
render(<Harness />);
|
|
1822
|
+
const input = screen.getByRole('combobox');
|
|
1823
|
+
|
|
1824
|
+
fireEvent.focus(input);
|
|
1825
|
+
await waitFor(() => {
|
|
1826
|
+
expect(screen.getByRole('status', {name: 'Loading'})).toBeInTheDocument();
|
|
1827
|
+
});
|
|
1828
|
+
|
|
1829
|
+
fireEvent.click(screen.getByRole('button', {name: 'Add Token'}));
|
|
1830
|
+
|
|
1831
|
+
await waitFor(() => {
|
|
1832
|
+
expect(
|
|
1833
|
+
screen.queryByRole('status', {name: 'Loading'}),
|
|
1834
|
+
).not.toBeInTheDocument();
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
await act(async () => {
|
|
1838
|
+
settle([]);
|
|
1839
|
+
await Promise.resolve();
|
|
1840
|
+
});
|
|
1841
|
+
expect(
|
|
1842
|
+
screen.queryByRole('status', {name: 'Loading'}),
|
|
1843
|
+
).not.toBeInTheDocument();
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1793
1846
|
it('shares one observer across every field on the page', () => {
|
|
1794
1847
|
// Browsers batch per observer instance, so one observer per lane meant N
|
|
1795
1848
|
// callback dispatches a frame for N fields.
|
|
@@ -183,6 +183,37 @@ describe('Tooltip', () => {
|
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
+
describe('icon-only trigger', () => {
|
|
187
|
+
// A rendered icon is an SVGElement, and ShowPopoverOptions.source is
|
|
188
|
+
// declared as an HTMLElement. Handing the icon to showPopover throws
|
|
189
|
+
// during dictionary conversion, before the popover opens, so the hint
|
|
190
|
+
// never appeared. (#7226)
|
|
191
|
+
it('opens an icon trigger without passing an SVGElement as the source', async () => {
|
|
192
|
+
const showSpy = vi.mocked(HTMLElement.prototype.showPopover);
|
|
193
|
+
showSpy.mockClear();
|
|
194
|
+
|
|
195
|
+
render(
|
|
196
|
+
<Tooltip content="Shared memory" isDefaultOpen>
|
|
197
|
+
<svg data-testid="icon" aria-hidden="true" />
|
|
198
|
+
</Tooltip>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
await waitFor(() => {
|
|
202
|
+
expect(showSpy).toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const [options] = showSpy.mock.calls[0];
|
|
206
|
+
// Compared as a boolean on purpose: a plain `toBeUndefined` failure makes
|
|
207
|
+
// the diff printer walk the SVGElement, which it cannot inspect.
|
|
208
|
+
expect(options?.source === undefined).toBe(true);
|
|
209
|
+
|
|
210
|
+
// The icon is still the trigger the ARIA tooltip pattern is wired to.
|
|
211
|
+
expect(screen.getByTestId('icon').getAttribute('aria-describedby')).toBe(
|
|
212
|
+
screen.getByRole('tooltip', {hidden: true}).id,
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
186
217
|
describe('WCAG 1.4.13 — content on hover or focus', () => {
|
|
187
218
|
it('dismisses on Escape while visible (dismissible)', async () => {
|
|
188
219
|
const onOpenChange = vi.fn();
|
|
@@ -147,6 +147,19 @@ describe('useTooltip — show delay', () => {
|
|
|
147
147
|
|
|
148
148
|
expect(onShow).not.toHaveBeenCalled();
|
|
149
149
|
});
|
|
150
|
+
|
|
151
|
+
it('drops a pending show when isEnabled turns false before the delay elapses', () => {
|
|
152
|
+
const onShow = vi.fn();
|
|
153
|
+
const {rerender} = render(<Harness onShow={onShow} />);
|
|
154
|
+
|
|
155
|
+
fireEvent.mouseEnter(trigger());
|
|
156
|
+
tick(100);
|
|
157
|
+
rerender(<Harness isEnabled={false} onShow={onShow} />);
|
|
158
|
+
tick(1000);
|
|
159
|
+
|
|
160
|
+
expect(onShow).not.toHaveBeenCalled();
|
|
161
|
+
expect(tooltipElement().matches(':popover-open')).toBe(false);
|
|
162
|
+
});
|
|
150
163
|
});
|
|
151
164
|
|
|
152
165
|
describe('useTooltip — touch suppression', () => {
|
|
@@ -352,6 +352,17 @@ export function useTooltip(options: TooltipOptions = {}): TooltipReturn {
|
|
|
352
352
|
}, delay);
|
|
353
353
|
}, [isEnabled, isOpen, clearTimeouts, showLayer, delay]);
|
|
354
354
|
|
|
355
|
+
// A show scheduled while the triggers were enabled must not outlive them:
|
|
356
|
+
// disabling also detaches the leave listener that would have cancelled it,
|
|
357
|
+
// so the timer would open a tooltip that nothing ever closes. Only the
|
|
358
|
+
// pending show is dropped; a pending hide still runs.
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
if (!isEnabled && showTimeoutRef.current) {
|
|
361
|
+
clearTimeout(showTimeoutRef.current);
|
|
362
|
+
showTimeoutRef.current = null;
|
|
363
|
+
}
|
|
364
|
+
}, [isEnabled]);
|
|
365
|
+
|
|
355
366
|
// Schedule hide with delay (suppressed when isOpen is true).
|
|
356
367
|
// A small hover bridge (when hideDelay is 0) lets the pointer travel from the
|
|
357
368
|
// trigger onto the tooltip surface without the tooltip vanishing — required
|
|
@@ -769,6 +769,28 @@ describe('TopNavMegaMenu — drawer mode', () => {
|
|
|
769
769
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
770
770
|
});
|
|
771
771
|
|
|
772
|
+
it('clips the drawer section only while it is collapsed', async () => {
|
|
773
|
+
// The ring paints outside the item, so a permanent clip would cut it off.
|
|
774
|
+
const user = userEvent.setup();
|
|
775
|
+
render(
|
|
776
|
+
<TopNavRenderContext value="drawer">
|
|
777
|
+
<TopNavMegaMenu
|
|
778
|
+
label="Products"
|
|
779
|
+
items={<TopNavMegaMenuItem title="Analytics" href="/analytics" />}
|
|
780
|
+
/>
|
|
781
|
+
</TopNavRenderContext>,
|
|
782
|
+
);
|
|
783
|
+
const trigger = screen.getByRole('button', {name: 'Products'});
|
|
784
|
+
const section = document.getElementById(
|
|
785
|
+
trigger.getAttribute('aria-controls') ?? '',
|
|
786
|
+
)!;
|
|
787
|
+
expect(getComputedStyle(section).overflow).toBe('hidden');
|
|
788
|
+
expect(getComputedStyle(section.firstElementChild!).overflow).toBe('');
|
|
789
|
+
|
|
790
|
+
await user.click(trigger);
|
|
791
|
+
expect(getComputedStyle(section).overflow).toBe('visible');
|
|
792
|
+
});
|
|
793
|
+
|
|
772
794
|
it('renders featured content when expanded', async () => {
|
|
773
795
|
const user = userEvent.setup();
|
|
774
796
|
|
|
@@ -229,18 +229,22 @@ const styles = stylex.create({
|
|
|
229
229
|
drawerChevronExpanded: {
|
|
230
230
|
transform: 'rotate(180deg)',
|
|
231
231
|
},
|
|
232
|
+
// Clips only while collapsed or animating; see TopNavMenu drawerStyles.items.
|
|
232
233
|
drawerItems: {
|
|
233
234
|
display: 'grid',
|
|
234
235
|
gridTemplateRows: '0fr',
|
|
235
|
-
|
|
236
|
-
|
|
236
|
+
overflow: 'hidden',
|
|
237
|
+
transitionProperty: 'grid-template-rows, overflow',
|
|
238
|
+
transitionBehavior: 'allow-discrete',
|
|
239
|
+
transitionDuration: `${durationVars['--duration-medium']}, 0s`,
|
|
237
240
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
238
241
|
},
|
|
239
242
|
drawerItemsExpanded: {
|
|
240
243
|
gridTemplateRows: '1fr',
|
|
244
|
+
overflow: 'visible',
|
|
245
|
+
transitionDelay: `0s, ${durationVars['--duration-medium']}`,
|
|
241
246
|
},
|
|
242
247
|
drawerItemsInner: {
|
|
243
|
-
overflow: 'hidden',
|
|
244
248
|
minHeight: 0,
|
|
245
249
|
},
|
|
246
250
|
|
|
@@ -241,6 +241,25 @@ describe('TopNavMenu — drawer focus ring', () => {
|
|
|
241
241
|
);
|
|
242
242
|
expectSharedFocusRing(screen.getByRole('link', {name: /Analytics/}));
|
|
243
243
|
});
|
|
244
|
+
|
|
245
|
+
it('clips the drawer section only while it is collapsed', async () => {
|
|
246
|
+
// The ring paints outside the item, so a permanent clip would cut it off.
|
|
247
|
+
const user = userEvent.setup();
|
|
248
|
+
render(
|
|
249
|
+
<TopNavRenderContext value="drawer">
|
|
250
|
+
<TopNavMenu label="Products" items={mockItems} />
|
|
251
|
+
</TopNavRenderContext>,
|
|
252
|
+
);
|
|
253
|
+
const trigger = screen.getByRole('button', {name: /Products/});
|
|
254
|
+
const section = document.getElementById(
|
|
255
|
+
trigger.getAttribute('aria-controls') ?? '',
|
|
256
|
+
)!;
|
|
257
|
+
expect(getComputedStyle(section).overflow).toBe('hidden');
|
|
258
|
+
expect(getComputedStyle(section.firstElementChild!).overflow).toBe('');
|
|
259
|
+
|
|
260
|
+
await user.click(trigger);
|
|
261
|
+
expect(getComputedStyle(section).overflow).toBe('visible');
|
|
262
|
+
});
|
|
244
263
|
});
|
|
245
264
|
|
|
246
265
|
describe('TopNavMenu pass-through props', () => {
|
|
@@ -194,18 +194,26 @@ const drawerStyles = stylex.create({
|
|
|
194
194
|
chevronExpanded: {
|
|
195
195
|
transform: 'rotate(180deg)',
|
|
196
196
|
},
|
|
197
|
+
// The section clips its items only for the 0fr collapse animation: a
|
|
198
|
+
// permanent clip cuts off the focus ring, which paints outside the item.
|
|
199
|
+
// `overflow` is discrete, so it rides the transition with `allow-discrete`
|
|
200
|
+
// and switches to `visible` only after the open has finished. Collapsing
|
|
201
|
+
// clips again at once.
|
|
197
202
|
items: {
|
|
198
203
|
display: 'grid',
|
|
199
204
|
gridTemplateRows: '0fr',
|
|
200
|
-
|
|
201
|
-
|
|
205
|
+
overflow: 'hidden',
|
|
206
|
+
transitionProperty: 'grid-template-rows, overflow',
|
|
207
|
+
transitionBehavior: 'allow-discrete',
|
|
208
|
+
transitionDuration: `${durationVars['--duration-medium']}, 0s`,
|
|
202
209
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
203
210
|
},
|
|
204
211
|
itemsExpanded: {
|
|
205
212
|
gridTemplateRows: '1fr',
|
|
213
|
+
overflow: 'visible',
|
|
214
|
+
transitionDelay: `0s, ${durationVars['--duration-medium']}`,
|
|
206
215
|
},
|
|
207
216
|
itemsInner: {
|
|
208
|
-
overflow: 'hidden',
|
|
209
217
|
minHeight: 0,
|
|
210
218
|
},
|
|
211
219
|
item: {
|
|
@@ -89,6 +89,7 @@ export const docs = {
|
|
|
89
89
|
{name: '--tree-list-indent', description: 'Per-level indentation step. Each nesting level indents its rows by this distance, and the guide lines follow it so they stay aligned. Set it on the `tree-list` target to retune the metric (e.g. `var(--spacing-5)` for a wider indent).', default: 'var(--spacing-4)'},
|
|
90
90
|
{name: '--tree-list-row-gap', description: 'Vertical gap between adjacent rows. Default `2px` (var(--spacing-0-5)) gives a subtle separation; set it on the `tree-list` target to widen or close the gap. The connector guides span the gap automatically (the line stays continuous) and do not overhang the last row, so no guide-height tuning is needed.', default: 'var(--spacing-0-5)'},
|
|
91
91
|
{name: '--_tree-indent', description: 'Distance one row is indented, computed per row from --tree-list-indent and the row depth. Set --tree-list-indent to retune indentation; this is the resolved value.', default: '0px', private: true},
|
|
92
|
+
{name: '--_tree-focus-z', description: 'Stacking order of a row while its treeitem has keyboard focus, published by each treeitem so its focused row paints above the guide lines. Not a theming lever.', default: 'auto', private: true},
|
|
92
93
|
],
|
|
93
94
|
},
|
|
94
95
|
components: [
|