@astryxdesign/core 0.7.2-canary.fe2c655 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +156 -2
- package/README.md +4 -4
- package/dist/AppShell/AppShell.d.ts.map +1 -1
- package/dist/AppShell/AppShell.js +40 -1
- 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/Avatar/Avatar.d.ts.map +1 -1
- package/dist/Avatar/Avatar.js +8 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.d.ts.map +1 -1
- package/dist/AvatarGroup/AvatarGroupOverflow.js +4 -1
- package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
- package/dist/Breadcrumbs/BreadcrumbItem.js +5 -2
- package/dist/Chat/ChatLayoutScrollButton.d.ts.map +1 -1
- package/dist/Chat/ChatLayoutScrollButton.js +1 -0
- 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/CheckboxList.d.ts +1 -1
- package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
- package/dist/CheckboxList/CheckboxList.js +6 -2
- 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/DropdownMenu/DropdownMenu.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenu.js +84 -113
- package/dist/DropdownMenu/DropdownMenuCheckboxItem.d.ts +1 -1
- 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/DropdownMenuGroup.d.ts +6 -4
- package/dist/DropdownMenu/DropdownMenuGroup.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuGroup.js +4 -3
- package/dist/DropdownMenu/DropdownMenuRadioItem.d.ts +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.d.ts.map +1 -1
- package/dist/DropdownMenu/DropdownMenuSubMenu.js +15 -4
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts +24 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.d.ts.map +1 -0
- package/dist/DropdownMenu/lazyMenuBottomSheet.js +46 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts +38 -0
- package/dist/DropdownMenu/menuRegionStops.d.ts.map +1 -0
- package/dist/DropdownMenu/menuRegionStops.js +131 -0
- 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/FormLayout/FormLayout.d.ts +5 -4
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +1 -1
- package/dist/FormLayout/FormLayoutContext.d.ts +3 -3
- package/dist/FormLayout/FormLayoutContext.js +3 -3
- package/dist/Grid/Grid.d.ts +8 -7
- package/dist/Grid/Grid.d.ts.map +1 -1
- package/dist/Grid/Grid.js +5 -4
- package/dist/Grid/GridSpan.d.ts +1 -1
- 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/Item/Item.d.ts +6 -2
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/Item.js +19 -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/parser.d.ts.map +1 -1
- package/dist/Markdown/parser.js +8 -2
- package/dist/Markdown/plugins/headingLinks.d.ts.map +1 -1
- package/dist/Markdown/plugins/headingLinks.js +9 -1
- 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/MobileNav/MobileNav.d.ts +9 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +30 -3
- package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearch.js +5 -0
- package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
- package/dist/PowerSearch/PowerSearchValueEditor.js +45 -11
- 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/SideNav/SideNavItem.d.ts +8 -1
- package/dist/SideNav/SideNavItem.d.ts.map +1 -1
- package/dist/SideNav/SideNavItem.js +76 -10
- package/dist/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -2
- 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/Token/Token.js +1 -1
- package/dist/TopNav/TopNavMegaMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMegaMenu.js +5 -3
- package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
- package/dist/TopNav/TopNavMenu.js +5 -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/astryx.css +27 -4
- package/dist/hooks/useMenuHover.d.ts.map +1 -1
- package/dist/hooks/useMenuHover.js +18 -13
- package/dist/reset.a11y.chromium.spec.js +50 -1
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +15 -9
- package/docs.mjs +4 -4
- package/package.json +4 -4
- package/scripts/agent-doc-state.mjs +66 -6
- package/scripts/postinstall.mjs +44 -3
- package/src/AppShell/AppShell.tsx +45 -1
- 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/Avatar/Avatar.tsx +11 -2
- package/src/AvatarGroup/AvatarGroup.doc.mjs +3 -2
- package/src/AvatarGroup/AvatarGroupOverflow.test.tsx +24 -1
- package/src/AvatarGroup/AvatarGroupOverflow.tsx +2 -0
- package/src/Breadcrumbs/BreadcrumbItem.tsx +4 -1
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +30 -1
- package/src/Card/Card.doc.mjs +1 -1
- package/src/Chat/ChatLayoutScrollButton.test.tsx +13 -0
- package/src/Chat/ChatLayoutScrollButton.tsx +4 -2
- package/src/Chat/__tests__/ChatLayoutScrollButton.a11y.chromium.spec.ts +64 -19
- 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.fr10.test.tsx +29 -2
- package/src/CheckboxList/CheckboxList.test.tsx +22 -0
- package/src/CheckboxList/CheckboxList.tsx +4 -1
- package/src/CheckboxList/CheckboxListItem.doc.mjs +16 -0
- package/src/CheckboxList/CheckboxListItem.tsx +10 -0
- package/src/Code/Code.doc.mjs +1 -0
- package/src/CommandPalette/CommandPalette.doc.mjs +2 -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.test.tsx +12 -2
- 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 +1 -0
- package/src/DropdownMenu/DropdownMenu.ssr.test.tsx +57 -0
- package/src/DropdownMenu/DropdownMenu.test.tsx +79 -6
- package/src/DropdownMenu/DropdownMenu.tsx +88 -144
- package/src/DropdownMenu/DropdownMenuCheckboxItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuCheckboxItem.tsx +1 -1
- 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/DropdownMenuGroup.doc.mjs +169 -10
- package/src/DropdownMenu/DropdownMenuGroup.tsx +6 -4
- package/src/DropdownMenu/DropdownMenuLazySheet.test.tsx +126 -0
- package/src/DropdownMenu/DropdownMenuRadioItem.doc.mjs +1 -1
- package/src/DropdownMenu/DropdownMenuRadioItem.tsx +1 -1
- package/src/DropdownMenu/DropdownMenuSubMenu.test.tsx +185 -0
- package/src/DropdownMenu/DropdownMenuSubMenu.tsx +17 -3
- package/src/DropdownMenu/__tests__/DropdownMenuDividerAudit.a11y.chromium.spec.ts +280 -0
- package/src/DropdownMenu/__tests__/DropdownMenuGroupAudit.a11y.chromium.spec.ts +334 -0
- package/src/DropdownMenu/__tests__/warmMenuBottomSheet.tsx +43 -0
- package/src/DropdownMenu/lazyMenuBottomSheet.ts +71 -0
- package/src/DropdownMenu/menuRegionStops.ts +167 -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/Field/FieldLabel.doc.mjs +133 -26
- package/src/Field/FieldLabel.spec.md +172 -0
- package/src/Field/__tests__/FieldLabelAudit.a11y.chromium.spec.ts +296 -0
- package/src/FormLayout/FormLayout.doc.mjs +6 -3
- package/src/FormLayout/FormLayout.spec.md +192 -0
- package/src/FormLayout/FormLayout.test.tsx +15 -0
- package/src/FormLayout/FormLayout.tsx +7 -6
- package/src/FormLayout/FormLayoutContext.ts +3 -3
- package/src/Grid/Grid.doc.mjs +7 -1
- package/src/Grid/Grid.spec.md +93 -113
- package/src/Grid/Grid.test.tsx +22 -0
- package/src/Grid/Grid.tsx +10 -9
- package/src/Grid/GridSpan.doc.mjs +3 -3
- package/src/Grid/GridSpan.tsx +4 -1
- package/src/Heading/Heading.test.tsx +57 -0
- package/src/Heading/Heading.tsx +6 -2
- package/src/Icon/Icon.doc.mjs +36 -0
- package/src/Indicator/Indicator.doc.mjs +3 -3
- package/src/Item/Item.doc.mjs +12 -8
- package/src/Item/Item.test.tsx +61 -0
- package/src/Item/Item.tsx +40 -4
- package/src/Layout/LayoutHeader.doc.mjs +5 -0
- 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 +16 -0
- package/src/List/ListItem.tsx +56 -27
- package/src/List/index.ts +1 -1
- package/src/Markdown/Markdown.tsx +14 -1
- package/src/Markdown/parser.ts +8 -2
- package/src/Markdown/plugins/headingLinks.ts +9 -1
- package/src/MetadataList/MetadataList.doc.mjs +3 -3
- package/src/MetadataList/MetadataList.test.tsx +38 -0
- package/src/MetadataList/MetadataList.tsx +8 -1
- package/src/MetadataList/MetadataListContext.tsx +1 -1
- package/src/MobileNav/MobileNav.tsx +30 -3
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +173 -15
- package/src/MoreMenu/MoreMenu.test.tsx +5 -1
- package/src/PowerSearch/PowerSearch.test.tsx +62 -0
- package/src/PowerSearch/PowerSearch.tsx +9 -0
- package/src/PowerSearch/PowerSearchValueEditor.test.tsx +71 -0
- package/src/PowerSearch/PowerSearchValueEditor.tsx +45 -0
- package/src/ProgressBar/ProgressBar.doc.mjs +1 -0
- 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/Resizable/useResizable.doc.mjs +2 -0
- package/src/ScrollableArea/ScrollableArea.doc.mjs +1 -1
- package/src/SideNav/SideNav.spec.md +227 -0
- package/src/SideNav/SideNav.test.tsx +220 -0
- package/src/SideNav/SideNavItem.doc.mjs +2 -2
- package/src/SideNav/SideNavItem.tsx +99 -17
- package/src/Slider/Slider.spec.md +39 -18
- package/src/Spinner/Spinner.doc.mjs +1 -1
- package/src/TabList/TabList.spec.md +57 -44
- package/src/Table/Table.spec.md +6 -1
- package/src/Table/plugins/activation/useTableActivation.spec.md +254 -0
- package/src/Text/Text.test.tsx +57 -0
- package/src/Text/Text.tsx +6 -2
- package/src/TimeInput/TimeInput.fr10.test.tsx +15 -0
- package/src/Timestamp/Timestamp.doc.mjs +1 -1
- package/src/ToggleButton/ToggleButton.test.tsx +95 -0
- package/src/ToggleButton/ToggleButton.tsx +12 -1
- package/src/ToggleButton/ToggleButtonGroup.doc.mjs +4 -0
- package/src/Token/Token.tsx +1 -1
- package/src/Token/__tests__/TokenRemoveTarget.a11y.chromium.spec.ts +86 -0
- package/src/Toolbar/Toolbar.test.tsx +0 -8
- package/src/Toolbar/__tests__/Toolbar.a11y.chromium.spec.ts +163 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.renders.tsx +63 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.states.ts +142 -0
- package/src/Toolbar/__tests__/Toolbar.a11y.test.tsx +126 -0
- package/src/TopNav/TopNavMegaMenu.test.tsx +47 -0
- package/src/TopNav/TopNavMegaMenu.tsx +9 -3
- package/src/TopNav/TopNavMenu.test.tsx +46 -1
- package/src/TopNav/TopNavMenu.tsx +13 -3
- package/src/TreeList/TreeList.doc.mjs +1 -0
- 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/docPropLiterals.test.ts +36 -10
- package/src/hooks/useMenuHover.ts +18 -13
- package/src/postinstall.test.mjs +79 -0
- package/src/reset.a11y.chromium.spec.ts +52 -1
- package/src/reset.css +9 -29
- package/src/reset.test.ts +11 -0
- package/src/theme/derivedVarRegistry.test.ts +7 -0
- package/src/unicodePropertyEscapes.source-build.test.mjs +69 -0
- package/src/utils/plainDate.test.ts +37 -1
- package/src/utils/plainDate.ts +15 -5
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - /packages/cli/assets/templates/blocks/components/TreeList/ (showcase blocks)
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {useId, useMemo, type ReactNode} from 'react';
|
|
17
|
+
import {useId, useMemo, useRef, type ReactNode} from 'react';
|
|
18
18
|
import * as stylex from '@stylexjs/stylex';
|
|
19
19
|
import {
|
|
20
20
|
colorVars,
|
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
import {focusOutlineProps} from '../utils/focusOutline.stylex';
|
|
28
28
|
import {interactionOverlayStyles} from '../utils/interactionOverlay.stylex';
|
|
29
29
|
import {usePressFeedback} from '../hooks/usePressFeedback';
|
|
30
|
+
import {
|
|
31
|
+
hasInteractiveAncestor,
|
|
32
|
+
useClickableContainer,
|
|
33
|
+
} from '../hooks/useClickableContainer';
|
|
30
34
|
import {Icon} from '../Icon';
|
|
31
35
|
import {mergeProps} from '../utils';
|
|
32
36
|
import {useLinkComponent} from '../Link/useLinkComponent';
|
|
@@ -46,6 +50,13 @@ const styles = stylex.create({
|
|
|
46
50
|
padding: 0,
|
|
47
51
|
position: 'relative',
|
|
48
52
|
width: '100%',
|
|
53
|
+
// Publish whether THIS treeitem has keyboard focus, for its row's
|
|
54
|
+
// `zIndex` (see `contentWrapper`). Every nested <li> redeclares the
|
|
55
|
+
// default, so a focused ancestor never lifts its descendants' rows.
|
|
56
|
+
'--_tree-focus-z': {
|
|
57
|
+
default: 'auto',
|
|
58
|
+
':focus-visible': '2',
|
|
59
|
+
},
|
|
49
60
|
// The treeitem row is the roving-tabindex focus owner; suppress the
|
|
50
61
|
// native focus ring in favor of the row's :focus-visible outline below.
|
|
51
62
|
outline: 'none',
|
|
@@ -82,6 +93,9 @@ const styles = stylex.create({
|
|
|
82
93
|
display: 'flex',
|
|
83
94
|
alignItems: 'center',
|
|
84
95
|
gap: spacingVars['--spacing-2'],
|
|
96
|
+
// The row's own inline padding only. The per-level indent lives on
|
|
97
|
+
// `indentedContent` inside it, so a theme or caller padding here composes
|
|
98
|
+
// with the indent instead of replacing it.
|
|
85
99
|
paddingInline: spacingVars['--spacing-2'],
|
|
86
100
|
// No `outline: 'none'` here: this element receives the shared focus ring,
|
|
87
101
|
// which already defaults to none, and the shorthand would erase it.
|
|
@@ -89,12 +103,38 @@ const styles = stylex.create({
|
|
|
89
103
|
position: 'relative',
|
|
90
104
|
boxSizing: 'border-box',
|
|
91
105
|
textAlign: 'start',
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
},
|
|
107
|
+
// Guide lines draw above the rows (TreeListBranches) so a row's fill does
|
|
108
|
+
// not break them. Lift the row whose focus ring is showing above the guides
|
|
109
|
+
// too, so no guide cuts through the ring: when focus is inside the row, or
|
|
110
|
+
// when its own treeitem publishes focus (`--_tree-focus-z` on `wrapper`).
|
|
111
|
+
// The ring is the row's outline, so it cannot be lifted on its own: a
|
|
112
|
+
// focused row that is also filled (selected or hovered) covers the guides at
|
|
113
|
+
// that row. That is the accepted trade-off (DD3). Applied only with
|
|
114
|
+
// `focusWithinOrPublished`, the rows that draw the ring: a plain row shows
|
|
115
|
+
// no ring, so its fill stays under the guides.
|
|
116
|
+
focusLift: {
|
|
117
|
+
zIndex: {
|
|
118
|
+
default: 'var(--_tree-focus-z, auto)',
|
|
119
|
+
':has(:focus-visible)': 2,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
// Per-level indent, drawn INSIDE the row box: the row (`contentWrapper`)
|
|
123
|
+
// stays full width, so a nested row's hover and selected fill spans the
|
|
124
|
+
// list like a root row's, while its content sits exactly where a margin
|
|
125
|
+
// indent would put it. A child of the row rather than the row's own padding,
|
|
126
|
+
// so a theme or caller that sets the row's padding keeps the indent. It
|
|
127
|
+
// inherits the row's flex alignment and gap, so a theme that retunes those
|
|
128
|
+
// on `tree-list-item` still lays the content out. The row publishes only the
|
|
129
|
+
// computed distance as `--_tree-indent`; the per-level step is the public,
|
|
130
|
+
// themeable `--tree-list-indent` lever (see TreeList `root`).
|
|
131
|
+
indentedContent: {
|
|
132
|
+
display: 'flex',
|
|
133
|
+
flex: 1,
|
|
134
|
+
minWidth: 0,
|
|
135
|
+
alignItems: 'inherit',
|
|
136
|
+
gap: 'inherit',
|
|
137
|
+
paddingInlineStart: 'var(--_tree-indent, 0px)',
|
|
98
138
|
},
|
|
99
139
|
interactive: {
|
|
100
140
|
cursor: {
|
|
@@ -369,6 +409,7 @@ export function TreeListItem({
|
|
|
369
409
|
const labelId = useId();
|
|
370
410
|
const descriptionId = useId();
|
|
371
411
|
const LinkComponent = useLinkComponent();
|
|
412
|
+
const linkRef = useRef<HTMLAnchorElement>(null);
|
|
372
413
|
const isInteractive = onClick != null || href != null;
|
|
373
414
|
|
|
374
415
|
const handleToggle = useMemo(
|
|
@@ -388,8 +429,10 @@ export function TreeListItem({
|
|
|
388
429
|
if (isDisabled) {
|
|
389
430
|
return;
|
|
390
431
|
}
|
|
391
|
-
|
|
392
|
-
|
|
432
|
+
// Controls inside the row, native or role-based, keep their own
|
|
433
|
+
// activation (FR7): the same rule link rows get from the shared
|
|
434
|
+
// enlarged-surface hook below.
|
|
435
|
+
if (hasInteractiveAncestor(e.target as Element, e.currentTarget)) {
|
|
393
436
|
return;
|
|
394
437
|
}
|
|
395
438
|
if (onClick != null) {
|
|
@@ -402,6 +445,34 @@ export function TreeListItem({
|
|
|
402
445
|
return undefined;
|
|
403
446
|
}, [onClick, hasChildren, onToggle, id, isDisabled]);
|
|
404
447
|
|
|
448
|
+
// A link row (an `href` and no other row action) is an enlarged surface for
|
|
449
|
+
// its link: the indent is part of the row, so a click anywhere in it must
|
|
450
|
+
// follow the link, as Enter already does. Built on the shared
|
|
451
|
+
// enlarged-surface hook (family:navigation-destinations), which skips
|
|
452
|
+
// clicks on nested controls, native or role-based, and text selections.
|
|
453
|
+
// - Clicks are delegated: the hook re-dispatches the click on the anchor
|
|
454
|
+
// with its modifier keys, so the link component and the browser handle
|
|
455
|
+
// same-tab, new-tab and new-window activation, and stops the original, so
|
|
456
|
+
// ancestors see one click per press.
|
|
457
|
+
// - Middle-click has no synthetic equivalent, so it takes the hook's href
|
|
458
|
+
// path, which opens the destination in a new tab after the shared
|
|
459
|
+
// navigation rule.
|
|
460
|
+
// The container ref is attached to link rows only, so every other row stays
|
|
461
|
+
// a plain element and both hook instances stay inert on it.
|
|
462
|
+
const forwardsToLink = href != null && handleClick == null;
|
|
463
|
+
const linkRowRef = useRef<HTMLDivElement>(null);
|
|
464
|
+
const {onClick: delegateToLink} = useClickableContainer({
|
|
465
|
+
containerRef: linkRowRef,
|
|
466
|
+
interactiveRef: linkRef,
|
|
467
|
+
disabled: isDisabled || !forwardsToLink,
|
|
468
|
+
});
|
|
469
|
+
const {onMouseUp: openLinkOnMiddleClick} = useClickableContainer({
|
|
470
|
+
containerRef: linkRowRef,
|
|
471
|
+
href,
|
|
472
|
+
target,
|
|
473
|
+
disabled: isDisabled || !forwardsToLink,
|
|
474
|
+
});
|
|
475
|
+
|
|
405
476
|
// Per-level indent distance. The per-level step is the public, themeable
|
|
406
477
|
// `--tree-list-indent` lever (default `--spacing-4`, set on the tree-list
|
|
407
478
|
// root). A leaf adds a fixed chevron-column offset (chevron width + gap) so
|
|
@@ -411,8 +482,8 @@ export function TreeListItem({
|
|
|
411
482
|
// every row sits flush, like a parent at that level. That offset is tied to
|
|
412
483
|
// the chevron's own dimensions, not the indent step, so it does not scale
|
|
413
484
|
// with the lever. Published as the private `--_tree-indent` and consumed by
|
|
414
|
-
// `
|
|
415
|
-
//
|
|
485
|
+
// `indentedContent`'s stylesheet `padding-inline-start`; the step stays
|
|
486
|
+
// themeable through `--tree-list-indent` (see #4308).
|
|
416
487
|
const reservesChevronColumn = !hasChildren && hasExpandableItems;
|
|
417
488
|
const indentDistance = reservesChevronColumn
|
|
418
489
|
? `calc(${nestedLevel} * var(--tree-list-indent) + ${spacingVars['--spacing-4']} + ${spacingVars['--spacing-2']})`
|
|
@@ -521,6 +592,7 @@ export function TreeListItem({
|
|
|
521
592
|
)}
|
|
522
593
|
{href != null ? (
|
|
523
594
|
<LinkComponent
|
|
595
|
+
ref={linkRef}
|
|
524
596
|
href={href}
|
|
525
597
|
target={target}
|
|
526
598
|
aria-disabled={isDisabled || undefined}
|
|
@@ -599,6 +671,7 @@ export function TreeListItem({
|
|
|
599
671
|
isInteractive || (hasChildren && onClick == null)
|
|
600
672
|
? focusOutlineProps.focusWithinOrPublished(
|
|
601
673
|
styles.contentWrapper,
|
|
674
|
+
styles.focusLift,
|
|
602
675
|
densityStyles[density],
|
|
603
676
|
styles.interactive,
|
|
604
677
|
interactionOverlayStyles.backgroundImage,
|
|
@@ -620,8 +693,10 @@ export function TreeListItem({
|
|
|
620
693
|
className,
|
|
621
694
|
{...indentStyle, ...style},
|
|
622
695
|
)}
|
|
623
|
-
|
|
624
|
-
{
|
|
696
|
+
ref={forwardsToLink ? linkRowRef : undefined}
|
|
697
|
+
onClick={forwardsToLink ? delegateToLink : handleClick}
|
|
698
|
+
onMouseUp={forwardsToLink ? openLinkOnMiddleClick : undefined}>
|
|
699
|
+
<div {...stylex.props(styles.indentedContent)}>{innerContent}</div>
|
|
625
700
|
</div>
|
|
626
701
|
</div>
|
|
627
702
|
{isExpanded && renderedChildren != null && (
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
4
4
|
/**
|
|
5
5
|
* @file Guards doc prop types against opaque named unions (#1645).
|
|
6
|
-
* @input Every {Name}.doc.mjs plus the TypeScript sources they describe
|
|
6
|
+
* @input Every {Name}.doc.mjs plus the TypeScript sources they describe,
|
|
7
|
+
* including same-file private aliases used by exported literal unions.
|
|
7
8
|
* @output Fails when a documented prop type hides its legal literal values.
|
|
8
9
|
* @position Repo-wide doc-shape guard, sibling of docPropReferences.test.ts.
|
|
9
10
|
*
|
|
@@ -188,18 +189,34 @@ for (const file of findFiles(SRC_DIR, ['.ts', '.tsx'])) {
|
|
|
188
189
|
// markers the comment-stripped `src` has already removed.
|
|
189
190
|
const rawTypeRhs = new Map<string, string>();
|
|
190
191
|
for (const m of raw.matchAll(
|
|
191
|
-
|
|
192
|
+
/\btype ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
|
|
192
193
|
)) {
|
|
193
194
|
rawTypeRhs.set(m[1], m[2]);
|
|
194
195
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
const declarations = [
|
|
197
|
+
...src.matchAll(
|
|
198
|
+
/\b(export )?type ([A-Z][A-Za-z0-9]*)(?:<[^>]*>)?\s*=\s*([^;]+);/g,
|
|
199
|
+
),
|
|
200
|
+
];
|
|
201
|
+
// Resolve same-file literal aliases before registering exported types.
|
|
202
|
+
// Repeat until no new literal unions are found.
|
|
203
|
+
const fileLiterals = new Map<string, string>();
|
|
204
|
+
let previousSize: number;
|
|
205
|
+
do {
|
|
206
|
+
previousSize = fileLiterals.size;
|
|
207
|
+
for (const [, , name, rhsRaw] of declarations) {
|
|
208
|
+
const rhs = rhsRaw
|
|
209
|
+
.replace(/\s+/g, ' ')
|
|
210
|
+
.trim()
|
|
211
|
+
.split('|')
|
|
212
|
+
.map(part => fileLiterals.get(part.trim()) ?? part.trim())
|
|
213
|
+
.join(' | ');
|
|
214
|
+
|
|
215
|
+
if (!LITERAL_UNION.test(rhs)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
201
218
|
const deprecated = deprecatedLiteralMembers(rawTypeRhs.get(name) ?? '');
|
|
202
|
-
|
|
219
|
+
fileLiterals.set(
|
|
203
220
|
name,
|
|
204
221
|
rhs
|
|
205
222
|
.replace(/^\|\s*/, '')
|
|
@@ -209,8 +226,17 @@ for (const file of findFiles(SRC_DIR, ['.ts', '.tsx'])) {
|
|
|
209
226
|
.filter(member => !deprecated.has(member))
|
|
210
227
|
.join(' | '),
|
|
211
228
|
);
|
|
229
|
+
}
|
|
230
|
+
} while (fileLiterals.size !== previousSize);
|
|
231
|
+
for (const [, exported, name, rhsRaw] of declarations) {
|
|
232
|
+
if (!exported) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const literals = fileLiterals.get(name);
|
|
236
|
+
if (literals !== undefined) {
|
|
237
|
+
literalUnions.set(name, literals);
|
|
212
238
|
} else {
|
|
213
|
-
namedBodies.set(name,
|
|
239
|
+
namedBodies.set(name, rhsRaw.replace(/\s+/g, ' ').trim());
|
|
214
240
|
}
|
|
215
241
|
}
|
|
216
242
|
for (const m of src.matchAll(
|
|
@@ -230,19 +230,6 @@ export function useMenuHover<T extends HTMLElement = HTMLElement>(
|
|
|
230
230
|
const hoverOpenedAtRef = useRef(0);
|
|
231
231
|
const prevIsOpenRef = useRef(isOpen);
|
|
232
232
|
|
|
233
|
-
// Catches every close, whatever caused it. A layout effect, not render: it
|
|
234
|
-
// must land before the browser hit-tests the vanished panel and fires the
|
|
235
|
-
// mouseenter REOPEN_SUPPRESS_MS exists to swallow.
|
|
236
|
-
useIsomorphicLayoutEffect(() => {
|
|
237
|
-
const wasOpen = prevIsOpenRef.current;
|
|
238
|
-
prevIsOpenRef.current = isOpen;
|
|
239
|
-
if (wasOpen && !isOpen) {
|
|
240
|
-
hoverModeRef.current = false;
|
|
241
|
-
hoverOpenedAtRef.current = 0;
|
|
242
|
-
closedAtRef.current = Date.now();
|
|
243
|
-
}
|
|
244
|
-
}, [isOpen]);
|
|
245
|
-
|
|
246
233
|
const clearTimeouts = useCallback(() => {
|
|
247
234
|
if (showTimerRef.current) {
|
|
248
235
|
clearTimeout(showTimerRef.current);
|
|
@@ -256,6 +243,24 @@ export function useMenuHover<T extends HTMLElement = HTMLElement>(
|
|
|
256
243
|
safeTriangleCleanupRef.current = null;
|
|
257
244
|
}, []);
|
|
258
245
|
|
|
246
|
+
// Catches every close, whatever caused it. A layout effect, not render: it
|
|
247
|
+
// must land before the browser hit-tests the vanished panel and fires the
|
|
248
|
+
// mouseenter REOPEN_SUPPRESS_MS exists to swallow.
|
|
249
|
+
useIsomorphicLayoutEffect(() => {
|
|
250
|
+
const wasOpen = prevIsOpenRef.current;
|
|
251
|
+
prevIsOpenRef.current = isOpen;
|
|
252
|
+
if (wasOpen && !isOpen) {
|
|
253
|
+
// A hover-open scheduled before the close must not fire afterwards and
|
|
254
|
+
// reopen what just closed from elsewhere — e.g. the root menu's light
|
|
255
|
+
// dismiss closing a submenu flyout mid hover-intent (#6893). A fresh
|
|
256
|
+
// mouseenter still opens; only the stale timer is cancelled.
|
|
257
|
+
clearTimeouts();
|
|
258
|
+
hoverModeRef.current = false;
|
|
259
|
+
hoverOpenedAtRef.current = 0;
|
|
260
|
+
closedAtRef.current = Date.now();
|
|
261
|
+
}
|
|
262
|
+
}, [isOpen, clearTimeouts]);
|
|
263
|
+
|
|
259
264
|
// useListFocus needs an onEscape that is defined in terms of its own listRef;
|
|
260
265
|
// the indirection avoids the use-before-declare.
|
|
261
266
|
const escapeHandlerRef = useRef<() => void>(() => {});
|
package/src/postinstall.test.mjs
CHANGED
|
@@ -11,17 +11,22 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
14
|
+
import {spawnSync} from 'node:child_process';
|
|
14
15
|
import * as fs from 'node:fs';
|
|
15
16
|
import * as os from 'node:os';
|
|
16
17
|
import * as path from 'node:path';
|
|
18
|
+
import {fileURLToPath} from 'node:url';
|
|
17
19
|
// Core's own package-local contract copy — what actually runs at install time.
|
|
18
20
|
import {
|
|
19
21
|
isAstryxInitialized,
|
|
22
|
+
isAstryxPromptInReach,
|
|
20
23
|
shouldNudge,
|
|
21
24
|
AGENT_DOC_PATHS,
|
|
22
25
|
INIT_MARKERS,
|
|
23
26
|
SETUP_NUDGE,
|
|
27
|
+
setupNudge,
|
|
24
28
|
} from '../scripts/agent-doc-state.mjs';
|
|
29
|
+
import {cliInvocation} from '../scripts/postinstall.mjs';
|
|
25
30
|
// Dev-only, monorepo-relative: core must not depend on the CLI at runtime (the
|
|
26
31
|
// CLI peer-depends on core), but the test can read the source and the generator.
|
|
27
32
|
import * as cliLeaf from '../../cli/foundation/agent-docs/agent-doc-state.mjs';
|
|
@@ -92,12 +97,58 @@ describe('core postinstall — one authoritative source, no drift', () => {
|
|
|
92
97
|
expect(AGENT_DOC_PATHS).toEqual(cliLeaf.AGENT_DOC_PATHS);
|
|
93
98
|
expect(INIT_MARKERS).toEqual(cliLeaf.INIT_MARKERS);
|
|
94
99
|
expect(SETUP_NUDGE).toBe(cliLeaf.SETUP_NUDGE);
|
|
100
|
+
expect(setupNudge('pnpm exec astryx')).toBe(
|
|
101
|
+
cliLeaf.setupNudge('pnpm exec astryx'),
|
|
102
|
+
);
|
|
95
103
|
});
|
|
96
104
|
|
|
97
105
|
it('agrees with the CLI leaf on a real project', () => {
|
|
98
106
|
write('AGENTS.md', `# doc\n${MARKER}\nbody`);
|
|
99
107
|
expect(isAstryxInitialized(tmp)).toBe(cliLeaf.isAstryxInitialized(tmp));
|
|
100
108
|
});
|
|
109
|
+
|
|
110
|
+
it('agrees with the CLI leaf on a monorepo package', () => {
|
|
111
|
+
fs.mkdirSync(path.join(tmp, '.git'));
|
|
112
|
+
write('AGENTS.md', MARKER);
|
|
113
|
+
write('packages/app/package.json', '{"name":"app"}');
|
|
114
|
+
const app = path.join(tmp, 'packages/app');
|
|
115
|
+
expect(isAstryxPromptInReach(app)).toBe(true);
|
|
116
|
+
expect(isAstryxPromptInReach(app)).toBe(cliLeaf.isAstryxPromptInReach(app));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Install time, end to end: core's real script and its contract copy, installed
|
|
121
|
+
// under a monorepo's node_modules, run the way npm runs them for a package.
|
|
122
|
+
describe('core postinstall — inside a monorepo package', () => {
|
|
123
|
+
const CORE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
124
|
+
const install = () => {
|
|
125
|
+
const installed = path.join(tmp, 'node_modules/@astryxdesign/core/scripts');
|
|
126
|
+
fs.mkdirSync(installed, {recursive: true});
|
|
127
|
+
for (const file of ['postinstall.mjs', 'agent-doc-state.mjs']) {
|
|
128
|
+
fs.copyFileSync(path.join(CORE, 'scripts', file), path.join(installed, file));
|
|
129
|
+
}
|
|
130
|
+
return spawnSync(process.execPath, [path.join(installed, 'postinstall.mjs')], {
|
|
131
|
+
env: {...process.env, INIT_CWD: path.join(tmp, 'packages/app'), npm_command: 'install'},
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
fs.mkdirSync(path.join(tmp, '.git'));
|
|
137
|
+
write('packages/app/package.json', '{"name":"app"}');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('stays quiet when init ran at the repository root', () => {
|
|
141
|
+
write('AGENTS.md', MARKER);
|
|
142
|
+
const r = install();
|
|
143
|
+
expect(r.status).toBe(0);
|
|
144
|
+
expect(r.stdout).toBe('');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('still nudges when no folder up to the repository root ran init', () => {
|
|
148
|
+
const r = install();
|
|
149
|
+
expect(r.status).toBe(0);
|
|
150
|
+
expect(r.stdout).toMatch(/finish setup and install the Astryx agent prompt/);
|
|
151
|
+
});
|
|
101
152
|
});
|
|
102
153
|
|
|
103
154
|
describe('core postinstall — shouldNudge', () => {
|
|
@@ -135,3 +186,31 @@ describe('core postinstall — shouldNudge', () => {
|
|
|
135
186
|
}
|
|
136
187
|
});
|
|
137
188
|
});
|
|
189
|
+
|
|
190
|
+
describe('core postinstall — the nudge names the command the CLI would print', () => {
|
|
191
|
+
it('words the nudge around the stem it is given', () => {
|
|
192
|
+
expect(setupNudge('pnpm exec astryx')).toContain(
|
|
193
|
+
'run `pnpm exec astryx init` to finish setup',
|
|
194
|
+
);
|
|
195
|
+
expect(SETUP_NUDGE).toContain('run `npx @astryxdesign/cli init`');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("asks the project's installed CLI for its stem", async () => {
|
|
199
|
+
write('package.json', '{"name":"app"}');
|
|
200
|
+
write('pnpm-lock.yaml', '');
|
|
201
|
+
write('node_modules/.bin/astryx', '');
|
|
202
|
+
fs.mkdirSync(path.join(tmp, 'node_modules/@astryxdesign'), {
|
|
203
|
+
recursive: true,
|
|
204
|
+
});
|
|
205
|
+
fs.symlinkSync(
|
|
206
|
+
path.resolve(import.meta.dirname, '../../cli'),
|
|
207
|
+
path.join(tmp, 'node_modules/@astryxdesign/cli'),
|
|
208
|
+
);
|
|
209
|
+
expect(await cliInvocation(tmp)).toBe('pnpm exec astryx');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('has no stem to offer when the CLI is not installed', async () => {
|
|
213
|
+
write('package.json', '{"name":"app"}');
|
|
214
|
+
expect(await cliInvocation(tmp)).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @file reset.a11y.chromium.spec.ts
|
|
5
5
|
* @input packages/core/src/reset.css, alone on a blank Chromium page
|
|
6
6
|
* @output Browser evidence that the reset reaches ::before, ::after,
|
|
7
|
-
* ::placeholder
|
|
7
|
+
* ::placeholder, the search field's cancel button and the dropdown marker
|
|
8
|
+
* of a field with a list
|
|
8
9
|
* @position reset.css integration proof. A pseudo-element inside :where() is
|
|
9
10
|
* invalid, and the forgiving list drops it without an error, so only a
|
|
10
11
|
* shipping engine shows which of the reset's rules still match.
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
import * as fs from 'node:fs';
|
|
14
15
|
import {fileURLToPath} from 'node:url';
|
|
15
16
|
import {expect, test, type Page} from '@playwright/test';
|
|
17
|
+
// @ts-expect-error -- pngjs ships no declarations; runtime support is pinned.
|
|
18
|
+
import {PNG} from 'pngjs';
|
|
16
19
|
|
|
17
20
|
const RESET_CSS = fs.readFileSync(
|
|
18
21
|
fileURLToPath(new URL('./reset.css', import.meta.url)),
|
|
@@ -154,6 +157,54 @@ test('a search field has no cancel button to clear it', async ({page}) => {
|
|
|
154
157
|
await expect(query).toHaveValue('query');
|
|
155
158
|
});
|
|
156
159
|
|
|
160
|
+
test('the dropdown marker of a field with a list sits in its middle', async ({
|
|
161
|
+
page,
|
|
162
|
+
}) => {
|
|
163
|
+
// Chromium shows the marker while the pointer is over the field and draws
|
|
164
|
+
// it as a glyph set on the field's line-height, the 1.5 the reset inherits.
|
|
165
|
+
await page.evaluate(() => {
|
|
166
|
+
const field = document.createElement('input');
|
|
167
|
+
field.id = 'fruit';
|
|
168
|
+
field.setAttribute('list', 'fruits');
|
|
169
|
+
const options = document.createElement('datalist');
|
|
170
|
+
options.id = 'fruits';
|
|
171
|
+
options.append(new Option('Apple'));
|
|
172
|
+
document.body.append(field, options);
|
|
173
|
+
});
|
|
174
|
+
await page.addStyleTag({
|
|
175
|
+
content: `
|
|
176
|
+
#fruit {
|
|
177
|
+
display: block;
|
|
178
|
+
width: 240px;
|
|
179
|
+
padding: 7px 11px;
|
|
180
|
+
border: 1px solid;
|
|
181
|
+
line-height: 1.5;
|
|
182
|
+
}
|
|
183
|
+
`,
|
|
184
|
+
});
|
|
185
|
+
const field = page.locator('#fruit');
|
|
186
|
+
await field.hover();
|
|
187
|
+
const png = PNG.sync.read(await field.screenshot({caret: 'hide'}));
|
|
188
|
+
// The marker is the only ink in the field's last 32px, inside the border.
|
|
189
|
+
const inkRows: number[] = [];
|
|
190
|
+
for (let y = 2; y < png.height - 2; y++) {
|
|
191
|
+
for (let x = png.width - 32; x < png.width - 2; x++) {
|
|
192
|
+
const offset = (y * png.width + x) * 4;
|
|
193
|
+
const luminance =
|
|
194
|
+
(png.data[offset] + png.data[offset + 1] + png.data[offset + 2]) / 3;
|
|
195
|
+
if (luminance < 160) {
|
|
196
|
+
inkRows.push(y);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
expect(inkRows.length, 'the hovered field shows its marker').toBeGreaterThan(
|
|
202
|
+
0,
|
|
203
|
+
);
|
|
204
|
+
const markerMiddle = (inkRows[0] + inkRows[inkRows.length - 1] + 1) / 2;
|
|
205
|
+
expect(Math.abs(markerMiddle - png.height / 2)).toBeLessThanOrEqual(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
157
208
|
test('a later layer and an unlayered rule still override the reset', async ({
|
|
158
209
|
page,
|
|
159
210
|
}) => {
|
package/src/reset.css
CHANGED
|
@@ -203,35 +203,6 @@
|
|
|
203
203
|
height: auto;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
/* ===========================================================================
|
|
207
|
-
AspectRatio Fit Baseline
|
|
208
|
-
=========================================================================== */
|
|
209
|
-
|
|
210
|
-
/*
|
|
211
|
-
* Child sizing for AspectRatio's `fit` prop — the component marks the
|
|
212
|
-
* child's direct parent with `data-astryx-aspect-ratio-override` (carrying
|
|
213
|
-
* the fit value), and these direct-child rules size the child, so
|
|
214
|
-
* consumers don't hand-write width/height/object-fit on every media child.
|
|
215
|
-
* The marker sits on the parent itself, so the rules don't depend on
|
|
216
|
-
* AspectRatio's internal element structure or its theming surface. As with
|
|
217
|
-
* the [data-astryx-media] baseline below, the rules are zero-specificity
|
|
218
|
-
* and any styling the child sets itself still wins. `fit="center"` needs
|
|
219
|
-
* no rules here — the component centers the child from its own wrapper.
|
|
220
|
-
*/
|
|
221
|
-
:where([data-astryx-aspect-ratio-override="cover"], [data-astryx-aspect-ratio-override="contain"])
|
|
222
|
-
> :where(*) {
|
|
223
|
-
width: 100%;
|
|
224
|
-
height: 100%;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
:where([data-astryx-aspect-ratio-override="cover"]) > :where(img, video) {
|
|
228
|
-
object-fit: cover;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
:where([data-astryx-aspect-ratio-override="contain"]) > :where(img, video) {
|
|
232
|
-
object-fit: contain;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
206
|
/* ===========================================================================
|
|
236
207
|
Forms
|
|
237
208
|
=========================================================================== */
|
|
@@ -294,6 +265,15 @@
|
|
|
294
265
|
color: var(--color-text-secondary, light-dark(#4E606F, #AAAFB5));
|
|
295
266
|
}
|
|
296
267
|
|
|
268
|
+
/*
|
|
269
|
+
* Chromium draws the dropdown marker of a field with a `list` as a glyph
|
|
270
|
+
* set on the line-height the field inherits, so with any line-height above
|
|
271
|
+
* 1 the marker sits below the middle of the field.
|
|
272
|
+
*/
|
|
273
|
+
:where([list])::-webkit-calendar-picker-indicator {
|
|
274
|
+
line-height: 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
297
277
|
/*
|
|
298
278
|
* Remove the inner padding and cancel buttons in search inputs
|
|
299
279
|
* for Chrome and Safari.
|
package/src/reset.test.ts
CHANGED
|
@@ -172,6 +172,17 @@ describe('reset.css pseudo-element rules', () => {
|
|
|
172
172
|
).toBe(true);
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
it('sets the dropdown marker of a field with a list on a single line', () => {
|
|
176
|
+
const marker = ruleBlocks.filter(({selector}) =>
|
|
177
|
+
topLevelSelectors(selector).some(entry =>
|
|
178
|
+
entry.endsWith('::-webkit-calendar-picker-indicator'),
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
expect(marker.map(({body}) => body).join('\n')).toMatch(
|
|
182
|
+
/line-height:\s*1;/,
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
175
186
|
it('falls back to the --color-text-secondary default for placeholders', () => {
|
|
176
187
|
// A page that loads only the reset never defines the token. The fallback
|
|
177
188
|
// repeats the token's own default, so that page shows the placeholder an
|
|
@@ -261,6 +261,9 @@ const CROSS_COMPONENT_VARS: Record<string, string[]> = {
|
|
|
261
261
|
// The destructive item variant recolors the Item it renders; Item owns,
|
|
262
262
|
// documents and reads both slots.
|
|
263
263
|
DropdownMenu: ['--_item-label-color', '--_item-description-color'],
|
|
264
|
+
// List's edge="flush" zeroes the inline inset of the Item each ListItem
|
|
265
|
+
// renders; Item owns, documents and reads it.
|
|
266
|
+
List: ['--_item-inset-inline'],
|
|
264
267
|
// A layer content root stops an outer Stepper's connector gap; Stepper owns,
|
|
265
268
|
// documents and reads it.
|
|
266
269
|
Layer: ['--step-connector-gap'],
|
|
@@ -316,6 +319,10 @@ const VARS_WITHOUT_DERIVED_MAPPING = new Set([
|
|
|
316
319
|
'--tree-list-indent',
|
|
317
320
|
'--_tree-indent',
|
|
318
321
|
'--tree-list-row-gap',
|
|
322
|
+
// Published by each treeitem while it has keyboard focus and read as its
|
|
323
|
+
// row's z-index, so the focused row paints above the guide lines. Focus
|
|
324
|
+
// state, not a value a theme author sets.
|
|
325
|
+
'--_tree-focus-z',
|
|
319
326
|
// Clipped on the ONE side each on-track connector segment faces the
|
|
320
327
|
// indicator from — trailing above the node, leading below it, mirrored
|
|
321
328
|
// again per axis. No standard property maps onto this purpose-based seam:
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file unicodePropertyEscapes.source-build.test.mjs
|
|
5
|
+
* @input Every Core source module a bundler compiles (tests and stories
|
|
6
|
+
* excluded)
|
|
7
|
+
* @output Fails when a module writes a Unicode property escape (`\p{…}` or
|
|
8
|
+
* `\P{…}`) as a regular expression literal
|
|
9
|
+
* @position Guard for source builds: for targets without property escapes, or
|
|
10
|
+
* with no browserslist, Babel rewrites such a literal, and the copy bundled
|
|
11
|
+
* with Next.js lacks the Unicode data to do it, so the build fails with
|
|
12
|
+
* "Unknown property". Modules build these expressions at runtime instead,
|
|
13
|
+
* as NumberInput/numberParser.ts does.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import {fileURLToPath} from 'node:url';
|
|
19
|
+
import {parseSync} from '@babel/core';
|
|
20
|
+
import {describe, expect, it} from 'vitest';
|
|
21
|
+
|
|
22
|
+
const SRC = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const SKIPPED = /\.(test|stories)\.tsx?$|\.d\.ts$/;
|
|
24
|
+
|
|
25
|
+
function sourceFiles(dir) {
|
|
26
|
+
return fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
|
|
27
|
+
const file = path.join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
return entry.name === '__tests__' ? [] : sourceFiles(file);
|
|
30
|
+
}
|
|
31
|
+
return /\.tsx?$/.test(entry.name) && !SKIPPED.test(entry.name)
|
|
32
|
+
? [file]
|
|
33
|
+
: [];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Every regular expression literal in a parsed tree. */
|
|
38
|
+
function regexLiterals(node, found = []) {
|
|
39
|
+
if (node == null || typeof node !== 'object') return found;
|
|
40
|
+
if (node.type === 'RegExpLiteral') found.push(node);
|
|
41
|
+
for (const [key, value] of Object.entries(node)) {
|
|
42
|
+
if (key !== 'loc' && key !== 'extra') regexLiterals(value, found);
|
|
43
|
+
}
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('Core regular expressions in source builds', () => {
|
|
48
|
+
it('writes no Unicode property escape as a regex literal', () => {
|
|
49
|
+
const offenders = [];
|
|
50
|
+
for (const file of sourceFiles(SRC)) {
|
|
51
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
52
|
+
if (!/\\[pP]\{/.test(source)) continue;
|
|
53
|
+
const ast = parseSync(source, {
|
|
54
|
+
filename: file,
|
|
55
|
+
babelrc: false,
|
|
56
|
+
configFile: false,
|
|
57
|
+
parserOpts: {plugins: ['typescript', 'jsx']},
|
|
58
|
+
});
|
|
59
|
+
for (const literal of regexLiterals(ast)) {
|
|
60
|
+
if (/\\[pP]\{/.test(literal.pattern)) {
|
|
61
|
+
offenders.push(
|
|
62
|
+
`${path.relative(SRC, file)}:${literal.loc.start.line} /${literal.pattern}/${literal.flags}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
expect(offenders).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|