@astryxdesign/core 0.7.2-canary.411cbed → 0.7.2-canary.4787e3f
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/Heading/Heading.d.ts.map +1 -1
- package/dist/Heading/Heading.js +4 -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/Text/Text.d.ts.map +1 -1
- package/dist/Text/Text.js +4 -2
- 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/astryx.css +5 -0
- package/package.json +3 -3
- package/src/Heading/Heading.test.tsx +57 -0
- package/src/Heading/Heading.tsx +6 -2
- 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/Text/Text.test.tsx +57 -0
- package/src/Text/Text.tsx +6 -2
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Heading.d.ts","sourceRoot":"","sources":["../../src/Heading/Heading.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAE7D,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACX,MAAM,gBAAgB,CAAC;AAkBxB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,UAAU,CAAC;AAG7C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,SAAS,CAAC;AAM5C;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEjD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC;AAQ/C,MAAM,WAAW,YAAa,SAAQ,IAAI,CACxC,SAAS,CAAC,kBAAkB,CAAC,EAC7B,UAAU,CACX;IACC,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACpC;;;OAGG;IACH,KAAK,EAAE,YAAY,CAAC;IAEpB;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IAEpB;;;;;;;;;;;OAWG;IACH,kBAAkB,CAAC,EAAE,YAAY,CAAC;IAElC;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IAE9C;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAEpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAWD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,IAAI,EACJ,MAAM,EACN,kBAAkB,EAClB,KAAiB,EACjB,OAAiB,EACjB,QAAY,EACZ,kBAAyB,EACzB,SAAS,EACT,QAAQ,EACR,OAAiB,EACjB,UAAkB,EAClB,gBAAwB,EACxB,MAAM,EACN,SAAS,EACT,KAAK,EACL,QAAQ,EACR,GAAG,EACH,GAAG,KAAK,EACT,EAAE,YAAY,+
|
|
1
|
+
{"version":3,"file":"Heading.d.ts","sourceRoot":"","sources":["../../src/Heading/Heading.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAE7D,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACR,UAAU,EACX,MAAM,gBAAgB,CAAC;AAkBxB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,UAAU,CAAC;AAG7C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,SAAS,CAAC;AAM5C;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEjD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC;AAQ/C,MAAM,WAAW,YAAa,SAAQ,IAAI,CACxC,SAAS,CAAC,kBAAkB,CAAC,EAC7B,UAAU,CACX;IACC,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IACpC;;;OAGG;IACH,KAAK,EAAE,YAAY,CAAC;IAEpB;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IAEnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IAEpB;;;;;;;;;;;OAWG;IACH,kBAAkB,CAAC,EAAE,YAAY,CAAC;IAElC;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IAE9C;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAEpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;CACrB;AAWD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,IAAI,EACJ,MAAM,EACN,kBAAkB,EAClB,KAAiB,EACjB,OAAiB,EACjB,QAAY,EACZ,kBAAyB,EACzB,SAAS,EACT,QAAQ,EACR,OAAiB,EACjB,UAAkB,EAClB,gBAAwB,EACxB,MAAM,EACN,SAAS,EACT,KAAK,EACL,QAAQ,EACR,GAAG,EACH,GAAG,KAAK,EACT,EAAE,YAAY,+BAsGd;yBA1He,OAAO"}
|
package/dist/Heading/Heading.js
CHANGED
|
@@ -105,9 +105,11 @@ export function Heading({
|
|
|
105
105
|
// the closed built-in StyleX maps with an unknown name.
|
|
106
106
|
const builtinType = type && isBuiltinHeadingType(type) ? type : undefined;
|
|
107
107
|
|
|
108
|
-
// Truncation detection
|
|
108
|
+
// Truncation detection. Only the tooltip reads the measured overflow, so
|
|
109
|
+
// with the tooltip off the text is clipped by CSS alone and nothing is
|
|
110
|
+
// measured or observed.
|
|
109
111
|
const truncation = useTruncation({
|
|
110
|
-
maxLines
|
|
112
|
+
maxLines: hasTruncateTooltip === false ? 0 : maxLines
|
|
111
113
|
});
|
|
112
114
|
|
|
113
115
|
// Tooltip for truncated text
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* `TopNavMenu` use). Hover is a progressive enhancement over the popover's
|
|
14
14
|
* click behavior and is inert on coarse pointers.
|
|
15
15
|
*
|
|
16
|
+
* Nested rows span the full width of the nav list, like top-level rows, so
|
|
17
|
+
* their hover and selected fill lines up with the rows above them. The
|
|
18
|
+
* nesting indent sits inside the row as extra inline-start padding, one step
|
|
19
|
+
* per level, tracked through a private depth context.
|
|
20
|
+
*
|
|
16
21
|
* SYNC: When modified, update these files to stay in sync:
|
|
17
22
|
* - /packages/core/src/SideNav/SideNav.doc.mjs
|
|
18
23
|
* - /packages/core/src/SideNav/SideNav.test.tsx
|
|
@@ -84,7 +89,9 @@ export interface SideNavItemProps extends BaseProps<HTMLElement> {
|
|
|
84
89
|
*/
|
|
85
90
|
actions?: ReactNode;
|
|
86
91
|
/**
|
|
87
|
-
* Sub-items for nesting.
|
|
92
|
+
* Sub-items for nesting. Nested items span the full width of the nav
|
|
93
|
+
* list, like top-level items; their content is indented one step per
|
|
94
|
+
* nesting level.
|
|
88
95
|
*/
|
|
89
96
|
children?: ReactNode;
|
|
90
97
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SideNavItem.d.ts","sourceRoot":"","sources":["../../src/SideNav/SideNavItem.tsx"],"names":[],"mappings":"AAIA
|
|
1
|
+
{"version":3,"file":"SideNavItem.d.ts","sourceRoot":"","sources":["../../src/SideNav/SideNavItem.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAYf,OAAO,EAAuB,KAAK,QAAQ,EAAC,MAAM,SAAS,CAAC;AAE5D,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAC;AAIrD,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAgB,KAAK,WAAW,EAAC,MAAM,iCAAiC,CAAC;AA4ShF,MAAM,WAAW,gBAAiB,SAAQ,SAAS,CAAC,WAAW,CAAC;IAC9D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;;;OAIG;IACH,EAAE,CAAC,EAAE,iBAAiB,CAAC;IACvB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B;;OAEG;IACH,YAAY,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACpC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,UAAU,KAAK,IAAI,CAAC;IACxC;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EACR,OAAO,GACP;QACE,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAC7B,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,KAAK,IAAI,CAAC;KACpD,CAAC;IACN;;;OAGG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,EAAE,EACF,KAAK,EACL,IAAI,EACJ,YAAY,EACZ,UAAkB,EAClB,UAAkB,EAClB,IAAI,EACJ,OAAO,EACP,UAAU,EACV,OAAO,EACP,QAAQ,EACR,WAAW,EAAE,eAAe,EAC5B,IAAW,EACX,aAAa,EAAE,MAAM,EACrB,GAAG,EACH,MAAM,EACN,GAAG,IAAI,EACR,EAAE,gBAAgB,sCAkalB;yBApbe,WAAW"}
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* `TopNavMenu` use). Hover is a progressive enhancement over the popover's
|
|
18
18
|
* click behavior and is inert on coarse pointers.
|
|
19
19
|
*
|
|
20
|
+
* Nested rows span the full width of the nav list, like top-level rows, so
|
|
21
|
+
* their hover and selected fill lines up with the rows above them. The
|
|
22
|
+
* nesting indent sits inside the row as extra inline-start padding, one step
|
|
23
|
+
* per level, tracked through a private depth context.
|
|
24
|
+
*
|
|
20
25
|
* SYNC: When modified, update these files to stay in sync:
|
|
21
26
|
* - /packages/core/src/SideNav/SideNav.doc.mjs
|
|
22
27
|
* - /packages/core/src/SideNav/SideNav.test.tsx
|
|
@@ -24,7 +29,7 @@
|
|
|
24
29
|
* - /apps/storybook/stories/SideNav.stories.tsx
|
|
25
30
|
* - /packages/cli/assets/templates/blocks/components/SideNav/ (showcase blocks)
|
|
26
31
|
*/
|
|
27
|
-
import { useCallback, useId, useMemo, useRef, useState } from 'react';
|
|
32
|
+
import { createContext, use, useCallback, useId, useMemo, useRef, useState } from 'react';
|
|
28
33
|
import * as stylex from '@stylexjs/stylex';
|
|
29
34
|
import "../theme/tokens.stylex.js";
|
|
30
35
|
import { colorVars, spacingVars, sizeVars, fontWeightVars, typeScaleVars, durationVars, easeVars, radiusVars } from "../theme/tokens.stylex.js";
|
|
@@ -133,6 +138,47 @@ const styles = {
|
|
|
133
138
|
}
|
|
134
139
|
};
|
|
135
140
|
|
|
141
|
+
// Nesting indent, drawn inside the row: one `--spacing-6` step per nesting
|
|
142
|
+
// level. The row itself stays full width, so its hover and selected fill
|
|
143
|
+
// matches a top-level row's.
|
|
144
|
+
// - `sm` / `md` — the ordinary row, where the row element is the link or
|
|
145
|
+
// button: its own inline-start padding plus the steps.
|
|
146
|
+
// SYNC: the base padding matches navItemStyles.sm / .md / .lg.
|
|
147
|
+
// - `primary` — the row-wrapper path: the wrapper keeps its base padding and
|
|
148
|
+
// the steps go on the primary link or button, so the whole indent is part
|
|
149
|
+
// of the click target rather than dead space that still shows feedback.
|
|
150
|
+
const _temp = {
|
|
151
|
+
kZCmMZ: "xnvo3vl",
|
|
152
|
+
"$$css": true
|
|
153
|
+
};
|
|
154
|
+
const _temp2 = {
|
|
155
|
+
kZCmMZ: "xnvo3vl",
|
|
156
|
+
"$$css": true
|
|
157
|
+
};
|
|
158
|
+
const _temp3 = {
|
|
159
|
+
kZCmMZ: "xnvo3vl",
|
|
160
|
+
"$$css": true
|
|
161
|
+
};
|
|
162
|
+
const indentStyles = {
|
|
163
|
+
sm: depth => [_temp, {
|
|
164
|
+
"--x-paddingInlineStart": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(`calc(${spacingVars['--spacing-1']} + ${depth} * ${spacingVars['--spacing-6']})`)
|
|
165
|
+
}],
|
|
166
|
+
md: depth => [_temp2, {
|
|
167
|
+
"--x-paddingInlineStart": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(`calc(${spacingVars['--spacing-2']} + ${depth} * ${spacingVars['--spacing-6']})`)
|
|
168
|
+
}],
|
|
169
|
+
primary: depth => [_temp3, {
|
|
170
|
+
"--x-paddingInlineStart": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(`calc(${depth} * ${spacingVars['--spacing-6']})`)
|
|
171
|
+
}]
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* How many SideNavItems deep the current item sits: 0 at the top level,
|
|
176
|
+
* 1 inside one parent, and so on. A collapsed rail's flyout resets it to 0,
|
|
177
|
+
* since the flyout lists a parent's children as its own top level.
|
|
178
|
+
*/
|
|
179
|
+
const SideNavItemDepthContext = /*#__PURE__*/createContext(0);
|
|
180
|
+
SideNavItemDepthContext.displayName = 'SideNavItemDepthContext';
|
|
181
|
+
|
|
136
182
|
/**
|
|
137
183
|
* Cascaded to the `actions` slot through `SizeContext` so a consumer's row
|
|
138
184
|
* controls come out the same height as the built-in expand/collapse toggle,
|
|
@@ -259,6 +305,7 @@ export function SideNavItem({
|
|
|
259
305
|
const id = useId();
|
|
260
306
|
const hasChildren = !!children;
|
|
261
307
|
const itemRef = useRef(null);
|
|
308
|
+
const depth = use(SideNavItemDepthContext);
|
|
262
309
|
|
|
263
310
|
// Popover for collapsed items with children
|
|
264
311
|
const popover = usePopover({
|
|
@@ -391,7 +438,10 @@ export function SideNavItem({
|
|
|
391
438
|
children: label
|
|
392
439
|
}), /*#__PURE__*/_jsx(SideNavCollapseContext, {
|
|
393
440
|
value: EXPANDED_COLLAPSE_STATE,
|
|
394
|
-
children:
|
|
441
|
+
children: /*#__PURE__*/_jsx(SideNavItemDepthContext, {
|
|
442
|
+
value: 0,
|
|
443
|
+
children: children
|
|
444
|
+
})
|
|
395
445
|
})]
|
|
396
446
|
}), {
|
|
397
447
|
placement: 'end',
|
|
@@ -457,6 +507,15 @@ export function SideNavItem({
|
|
|
457
507
|
disabled: isDisabled ? 'disabled' : null
|
|
458
508
|
});
|
|
459
509
|
const itemStyleArgs = [navItemStyles.item, interactionOverlayStyles.backgroundColor, navItemStyles[size], isSelected && navItemStyles.selected, isDisabled && navItemStyles.disabled];
|
|
510
|
+
const rowIndent = depth > 0 && indentStyles[size === 'sm' ? 'sm' : 'md'](depth);
|
|
511
|
+
const primaryIndent = depth > 0 && indentStyles.primary(depth);
|
|
512
|
+
|
|
513
|
+
// The indent is a runtime value, so it reaches the element as an inline
|
|
514
|
+
// style. These props are spread after `rest`, so fold the caller's `style`
|
|
515
|
+
// back in (caller wins) rather than letting the indent replace it.
|
|
516
|
+
const callerStyle = {
|
|
517
|
+
style: rest.style
|
|
518
|
+
};
|
|
460
519
|
|
|
461
520
|
// Three shapes of the same row appearance:
|
|
462
521
|
// - `rowProps` — split-action path (toggle, no actions): presentational
|
|
@@ -471,7 +530,7 @@ export function SideNavItem({
|
|
|
471
530
|
...pressable
|
|
472
531
|
};
|
|
473
532
|
const focusableRowProps = {
|
|
474
|
-
...mergeProps(itemThemeProps, focusOutlineProps.focusVisible(...itemStyleArgs)),
|
|
533
|
+
...mergeProps(itemThemeProps, mergeProps(focusOutlineProps.focusVisible(...itemStyleArgs, rowIndent), callerStyle)),
|
|
475
534
|
...pressable
|
|
476
535
|
};
|
|
477
536
|
const actionsRowProps = {
|
|
@@ -511,10 +570,10 @@ export function SideNavItem({
|
|
|
511
570
|
onClick: handleClick,
|
|
512
571
|
...rest,
|
|
513
572
|
...rowPrimaryAriaProps,
|
|
514
|
-
...(hasActions ?
|
|
573
|
+
...mergeProps(hasActions ?
|
|
515
574
|
// The wrapper rings for this element; suppressing here is what
|
|
516
575
|
// keeps the UA's own ring from painting inside that one.
|
|
517
|
-
focusOutlineProps.suppressed(styles.splitAction) : focusOutlineProps.focusVisible(styles.splitAction)),
|
|
576
|
+
focusOutlineProps.suppressed(styles.splitAction, primaryIndent) : focusOutlineProps.focusVisible(styles.splitAction, primaryIndent), callerStyle),
|
|
518
577
|
children: itemContent
|
|
519
578
|
}), hasIndependentToggle && /*#__PURE__*/_jsx("button", {
|
|
520
579
|
type: "button",
|
|
@@ -575,21 +634,24 @@ export function SideNavItem({
|
|
|
575
634
|
inert: isItemCollapsed ? true : undefined,
|
|
576
635
|
...{
|
|
577
636
|
0: {
|
|
578
|
-
className: "xrvj5dj x1tu4anv
|
|
637
|
+
className: "xrvj5dj x1tu4anv x1rea2x4 x6kj92 xd00j3c x1eizr8u xsr0bmt xostmdb x1k6wr6r xlr8y92"
|
|
579
638
|
},
|
|
580
639
|
1: {
|
|
581
|
-
className: "xrvj5dj
|
|
640
|
+
className: "xrvj5dj x6kj92 xd00j3c x1eizr8u xsr0bmt xlr8y92 xihq33y xb3r6kr xf1rc6"
|
|
582
641
|
}
|
|
583
642
|
}[!!isItemCollapsed << 0],
|
|
584
643
|
children: /*#__PURE__*/_jsxs("div", {
|
|
585
644
|
...{
|
|
586
|
-
className: "
|
|
645
|
+
className: "x2lwn1j"
|
|
587
646
|
},
|
|
588
647
|
children: [/*#__PURE__*/_jsx("span", {
|
|
589
648
|
id: `${id}-label`,
|
|
590
649
|
hidden: true,
|
|
591
650
|
children: label
|
|
592
|
-
}),
|
|
651
|
+
}), /*#__PURE__*/_jsx(SideNavItemDepthContext, {
|
|
652
|
+
value: depth + 1,
|
|
653
|
+
children: children
|
|
654
|
+
})]
|
|
593
655
|
})
|
|
594
656
|
})]
|
|
595
657
|
});
|
package/dist/Text/Text.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Text.d.ts","sourceRoot":"","sources":["../../src/Text/Text.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAE7D,OAAO,KAAK,EACV,QAAQ,EAER,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACT,MAAM,gBAAgB,CAAC;AAkBxB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,UAAU,CAAC;AAG7C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAO5C,YAAY,EAAC,QAAQ,EAAE,QAAQ,EAAC,CAAC;AAEjC,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;IAC5D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAEhB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAEhB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IAEpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IAE9C;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAEpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC1D;AA2BD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,gBAAgB,CAQpE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,IAAI,CAAC,EACnB,IAAa,EACb,IAAI,EACJ,KAAK,EACL,MAAM,EACN,OAAkB,EAClB,QAAY,EACZ,kBAAyB,EACzB,SAAS,EACT,QAAQ,EACR,OAAiB,EACjB,UAAkB,EAClB,gBAAwB,EACxB,iBAAyB,EACzB,MAAM,EACN,SAAS,EACT,KAAK,EACL,EAAE,EAAE,SAAkB,EACtB,QAAQ,EACR,GAAG,EACH,GAAG,KAAK,EACT,EAAE,SAAS,+
|
|
1
|
+
{"version":3,"file":"Text.d.ts","sourceRoot":"","sources":["../../src/Text/Text.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAyB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAE7D,OAAO,KAAK,EACV,QAAQ,EAER,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,WAAW,EACX,SAAS,EACT,QAAQ,EACT,MAAM,gBAAgB,CAAC;AAkBxB,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,UAAU,CAAC;AAG7C,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAO5C,YAAY,EAAC,QAAQ,EAAE,QAAQ,EAAC,CAAC;AAEjC,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC;IAC5D,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IAC7B;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAEhB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAEhB;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAElB;;OAEG;IACH,MAAM,CAAC,EAAE,UAAU,CAAC;IAEpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,GAAG,cAAc,CAAC;IAE9C;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAEpB;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IAEtB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;OAEG;IACH,QAAQ,EAAE,SAAS,CAAC;IAEpB;;;;OAIG;IACH,EAAE,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC1D;AA2BD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,gBAAgB,CAQpE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,IAAI,CAAC,EACnB,IAAa,EACb,IAAI,EACJ,KAAK,EACL,MAAM,EACN,OAAkB,EAClB,QAAY,EACZ,kBAAyB,EACzB,SAAS,EACT,QAAQ,EACR,OAAiB,EACjB,UAAkB,EAClB,gBAAwB,EACxB,iBAAyB,EACzB,MAAM,EACN,SAAS,EACT,KAAK,EACL,EAAE,EAAE,SAAkB,EACtB,QAAQ,EACR,GAAG,EACH,GAAG,KAAK,EACT,EAAE,SAAS,+BA8FX;yBAnHe,IAAI"}
|
package/dist/Text/Text.js
CHANGED
|
@@ -123,9 +123,11 @@ export function Text({
|
|
|
123
123
|
// Resolve display - force block when maxLines > 0 or hasCapsize
|
|
124
124
|
const resolvedDisplay = maxLines > 0 || hasCapsize ? 'block' : display;
|
|
125
125
|
|
|
126
|
-
// Truncation detection
|
|
126
|
+
// Truncation detection. Only the tooltip reads the measured overflow, so
|
|
127
|
+
// with the tooltip off the text is clipped by CSS alone and nothing is
|
|
128
|
+
// measured or observed.
|
|
127
129
|
const truncation = useTruncation({
|
|
128
|
-
maxLines
|
|
130
|
+
maxLines: hasTruncateTooltip === false ? 0 : maxLines
|
|
129
131
|
});
|
|
130
132
|
|
|
131
133
|
// Tooltip for truncated text
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TopNavMegaMenu.d.ts","sourceRoot":"","sources":["../../src/TopNav/TopNavMegaMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAkBf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"TopNavMegaMenu.d.ts","sourceRoot":"","sources":["../../src/TopNav/TopNavMegaMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAkBf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAgN5C,MAAM,WAAW,mBAAoB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACvE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC,kDAAkD;IAClD,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;CAC1C;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,cAAc,CAAC,EAC7B,GAAG,EACH,KAAK,EACL,KAAK,EACL,QAAQ,EACR,KAAW,EACX,SAAe,EACf,YAAY,EACZ,GAAG,IAAI,EACR,EAAE,mBAAmB,4BAuCrB;yBAhDe,cAAc"}
|
|
@@ -411,15 +411,15 @@ function DrawerMegaMenu({
|
|
|
411
411
|
id: `${menuId}-items`,
|
|
412
412
|
...{
|
|
413
413
|
0: {
|
|
414
|
-
className: "xrvj5dj xihq33y
|
|
414
|
+
className: "xrvj5dj xihq33y xb3r6kr x6kj92 xd00j3c x1eizr8u xlr8y92"
|
|
415
415
|
},
|
|
416
416
|
1: {
|
|
417
|
-
className: "xrvj5dj
|
|
417
|
+
className: "xrvj5dj x6kj92 xd00j3c x1eizr8u xlr8y92 x1tu4anv x1rea2x4 xostmdb"
|
|
418
418
|
}
|
|
419
419
|
}[!!isExpanded << 0],
|
|
420
420
|
children: /*#__PURE__*/_jsxs("div", {
|
|
421
421
|
...{
|
|
422
|
-
className: "
|
|
422
|
+
className: "x2lwn1j"
|
|
423
423
|
},
|
|
424
424
|
children: [items, featured != null && /*#__PURE__*/_jsx("div", {
|
|
425
425
|
...{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TopNavMenu.d.ts","sourceRoot":"","sources":["../../src/TopNav/TopNavMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AASf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"TopNavMenu.d.ts","sourceRoot":"","sources":["../../src/TopNav/TopNavMenu.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAKZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AASf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAkN5C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IAEjB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB;AAMD,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,iBAAiB,CAAC;IACnE,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IACnC;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAE5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,KAAK,EACL,KAAK,EACL,KAAW,EACX,SAAe,EACf,MAAM,EACN,SAAS,EACT,KAAK,EACL,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,gBAAgB,EAC9B,YAAY,EAAE,gBAAgB,EAC9B,GAAG,IAAI,EACR,EAAE,eAAe,4BA4QjB;yBAzRe,UAAU"}
|
|
@@ -311,15 +311,15 @@ export function TopNavMenu({
|
|
|
311
311
|
id: `${menuId}-items`,
|
|
312
312
|
...{
|
|
313
313
|
0: {
|
|
314
|
-
className: "xrvj5dj xihq33y
|
|
314
|
+
className: "xrvj5dj xihq33y xb3r6kr x6kj92 xd00j3c x1eizr8u xlr8y92"
|
|
315
315
|
},
|
|
316
316
|
1: {
|
|
317
|
-
className: "xrvj5dj
|
|
317
|
+
className: "xrvj5dj x6kj92 xd00j3c x1eizr8u xlr8y92 x1tu4anv x1rea2x4 xostmdb"
|
|
318
318
|
}
|
|
319
319
|
}[!!drawerExpanded << 0],
|
|
320
320
|
children: /*#__PURE__*/_jsx("div", {
|
|
321
321
|
...{
|
|
322
|
-
className: "
|
|
322
|
+
className: "x2lwn1j"
|
|
323
323
|
},
|
|
324
324
|
children: items.map(item => /*#__PURE__*/_jsxs(LinkComponent, {
|
|
325
325
|
href: item.href,
|
package/dist/astryx.css
CHANGED
|
@@ -1358,6 +1358,8 @@
|
|
|
1358
1358
|
.xsqj5wx:not(#\#):not(#\#):not(#\#){transform:var(--x-transform)}
|
|
1359
1359
|
.xd00j3c:not(#\#):not(#\#):not(#\#){transition-behavior:allow-discrete}
|
|
1360
1360
|
.x5h36tt:not(#\#):not(#\#):not(#\#){transition-delay:.15s}
|
|
1361
|
+
.xf1rc6:not(#\#):not(#\#):not(#\#){transition-delay:0s,0s}
|
|
1362
|
+
.xostmdb:not(#\#):not(#\#):not(#\#){transition-delay:0s,var(--duration-medium)}
|
|
1361
1363
|
.x1ahifba:not(#\#):not(#\#):not(#\#){transition-delay:0s}
|
|
1362
1364
|
.x4avkj2:not(#\#):not(#\#):not(#\#){transition-delay:var(--_fade-delay,0s)}
|
|
1363
1365
|
.x1rq8cwj:not(#\#):not(#\#):not(#\#){transition-delay:var(--_reveal-delay,0s,0s)}
|
|
@@ -1373,6 +1375,7 @@
|
|
|
1373
1375
|
.x1tw3moq:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-fast),0s}
|
|
1374
1376
|
.xuedmi6:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-fast)}
|
|
1375
1377
|
.xgneliz:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-medium-min)}
|
|
1378
|
+
.x1eizr8u:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-medium),0s}
|
|
1376
1379
|
.x80gvsz:not(#\#):not(#\#):not(#\#){transition-duration:var(--duration-medium)}
|
|
1377
1380
|
.x1eaenvl:not(#\#):not(#\#):not(#\#){transition-property:background-color,border-color,color}
|
|
1378
1381
|
.xts7igz:not(#\#):not(#\#):not(#\#){transition-property:background-color,border-color}
|
|
@@ -1395,6 +1398,7 @@
|
|
|
1395
1398
|
.x1mpt4pi:not(#\#):not(#\#):not(#\#){transition-property:color,text-decoration}
|
|
1396
1399
|
.xt3l3uh:not(#\#):not(#\#):not(#\#){transition-property:color}
|
|
1397
1400
|
.xrgi2yo:not(#\#):not(#\#):not(#\#){transition-property:display}
|
|
1401
|
+
.x6kj92:not(#\#):not(#\#):not(#\#){transition-property:grid-template-rows,overflow}
|
|
1398
1402
|
.xjshnde:not(#\#):not(#\#):not(#\#){transition-property:grid-template-rows,padding}
|
|
1399
1403
|
.x1qn9uv2:not(#\#):not(#\#):not(#\#){transition-property:grid-template-rows}
|
|
1400
1404
|
.x157bb6t:not(#\#):not(#\#):not(#\#){transition-property:mask-image}
|
|
@@ -1607,6 +1611,7 @@
|
|
|
1607
1611
|
@media (pointer: coarse){.x1bjav06.x1bjav06:not(#\#):not(#\#):not(#\#){pointer-events:none}}
|
|
1608
1612
|
@media (prefers-reduced-motion: reduce){.xg83hb1.xg83hb1:not(#\#):not(#\#):not(#\#){scroll-behavior:auto}}
|
|
1609
1613
|
@media (forced-colors: active){.xrih18v.xrih18v:not(#\#):not(#\#):not(#\#){scrollbar-color:auto}}
|
|
1614
|
+
@media (prefers-reduced-motion: reduce){.x1k6wr6r.x1k6wr6r:not(#\#):not(#\#):not(#\#){transition-delay:0s,0s}}
|
|
1610
1615
|
@media (prefers-reduced-motion: reduce){.xwgk2z2.xwgk2z2:not(#\#):not(#\#):not(#\#){transition-delay:var(--_reveal-delay-reduced,0s,0s)}}
|
|
1611
1616
|
@media (prefers-reduced-motion: reduce){.xsagj69.xsagj69:not(#\#):not(#\#):not(#\#){transition-duration:.01ms}}
|
|
1612
1617
|
@media (prefers-reduced-motion: reduce){.xzg1mie.xzg1mie:not(#\#):not(#\#):not(#\#){transition-duration:.01s}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/core",
|
|
3
|
-
"version": "0.7.2-canary.
|
|
3
|
+
"version": "0.7.2-canary.4787e3f",
|
|
4
4
|
"displayName": "Astryx Core",
|
|
5
5
|
"description": "The component library. Accessible, themeable React components with built-in spacing, dark mode, and StyleX styling.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -792,8 +792,8 @@
|
|
|
792
792
|
"react-dom": ">=19.0.0"
|
|
793
793
|
},
|
|
794
794
|
"devDependencies": {
|
|
795
|
-
"@astryxdesign/a11y-spec": "0.7.2-canary.
|
|
796
|
-
"@astryxdesign/cli": "0.7.2-canary.
|
|
795
|
+
"@astryxdesign/a11y-spec": "0.7.2-canary.4787e3f",
|
|
796
|
+
"@astryxdesign/cli": "0.7.2-canary.4787e3f",
|
|
797
797
|
"@babel/cli": "^7.29.7",
|
|
798
798
|
"@babel/core": "^7.29.7",
|
|
799
799
|
"@babel/preset-react": "^7.29.7",
|
|
@@ -269,3 +269,60 @@ describe('Heading', () => {
|
|
|
269
269
|
expect(element).not.toHaveAttribute('data-type', 'editorial');
|
|
270
270
|
});
|
|
271
271
|
});
|
|
272
|
+
|
|
273
|
+
// With hasTruncateTooltip={false} nothing reads the measured overflow, so a
|
|
274
|
+
// long table of truncated cells must not pay a layout read per cell.
|
|
275
|
+
function countScrollWidthReads(run: () => void): number {
|
|
276
|
+
const proto = window.HTMLElement.prototype;
|
|
277
|
+
const original = Object.getOwnPropertyDescriptor(proto, 'scrollWidth');
|
|
278
|
+
let reads = 0;
|
|
279
|
+
Object.defineProperty(proto, 'scrollWidth', {
|
|
280
|
+
configurable: true,
|
|
281
|
+
get() {
|
|
282
|
+
reads += 1;
|
|
283
|
+
return 0;
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
run();
|
|
288
|
+
} finally {
|
|
289
|
+
if (original) {
|
|
290
|
+
Object.defineProperty(proto, 'scrollWidth', original);
|
|
291
|
+
} else {
|
|
292
|
+
delete (proto as {scrollWidth?: number}).scrollWidth;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return reads;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
describe('Heading maxLines without a truncation tooltip', () => {
|
|
299
|
+
it('takes no truncation measurement when hasTruncateTooltip is false', () => {
|
|
300
|
+
const reads = countScrollWidthReads(() => {
|
|
301
|
+
render(
|
|
302
|
+
<>
|
|
303
|
+
{Array.from({length: 20}, (_, index) => (
|
|
304
|
+
<Heading
|
|
305
|
+
level={3}
|
|
306
|
+
maxLines={1}
|
|
307
|
+
hasTruncateTooltip={false}
|
|
308
|
+
key={index}>
|
|
309
|
+
Cell {index} with text far wider than its column
|
|
310
|
+
</Heading>
|
|
311
|
+
))}
|
|
312
|
+
</>,
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
expect(reads).toBe(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('still measures when the tooltip is enabled', () => {
|
|
319
|
+
const reads = countScrollWidthReads(() => {
|
|
320
|
+
render(
|
|
321
|
+
<Heading level={3} maxLines={1}>
|
|
322
|
+
Text far wider than its column
|
|
323
|
+
</Heading>,
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
expect(reads).toBeGreaterThan(0);
|
|
327
|
+
});
|
|
328
|
+
});
|
package/src/Heading/Heading.tsx
CHANGED
|
@@ -253,8 +253,12 @@ export function Heading({
|
|
|
253
253
|
// the closed built-in StyleX maps with an unknown name.
|
|
254
254
|
const builtinType = type && isBuiltinHeadingType(type) ? type : undefined;
|
|
255
255
|
|
|
256
|
-
// Truncation detection
|
|
257
|
-
|
|
256
|
+
// Truncation detection. Only the tooltip reads the measured overflow, so
|
|
257
|
+
// with the tooltip off the text is clipped by CSS alone and nothing is
|
|
258
|
+
// measured or observed.
|
|
259
|
+
const truncation = useTruncation({
|
|
260
|
+
maxLines: hasTruncateTooltip === false ? 0 : maxLines,
|
|
261
|
+
});
|
|
258
262
|
|
|
259
263
|
// Tooltip for truncated text
|
|
260
264
|
const tooltipPlacement: LayerPlacement =
|
|
@@ -828,6 +828,169 @@ describe('SideNavItem', () => {
|
|
|
828
828
|
expect(screen.getByText('Security')).toBeInTheDocument();
|
|
829
829
|
});
|
|
830
830
|
|
|
831
|
+
it('keeps nested rows full width and indents their content inside the row', () => {
|
|
832
|
+
render(
|
|
833
|
+
<SideNavItem label="Settings">
|
|
834
|
+
<SideNavItem label="General" href="/settings/general">
|
|
835
|
+
<SideNavItem label="Advanced" href="/settings/general/advanced" />
|
|
836
|
+
</SideNavItem>
|
|
837
|
+
</SideNavItem>,
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
// The children group adds no inset of its own, so a nested row's hover
|
|
841
|
+
// and selected fill spans the nav list like a top-level row's.
|
|
842
|
+
const groups = screen.getAllByRole('group', {hidden: true});
|
|
843
|
+
for (const group of groups) {
|
|
844
|
+
expect(declaredOn(group, 'padding-inline-start')).toEqual([]);
|
|
845
|
+
expect(
|
|
846
|
+
declaredOn(group.firstElementChild!, 'padding-inline-start'),
|
|
847
|
+
).toEqual([]);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// The indent lives inside each row instead, one step per nesting level.
|
|
851
|
+
// An ordinary row is the link itself: its own padding plus the steps.
|
|
852
|
+
const top = screen.getByRole('button', {name: 'Settings'});
|
|
853
|
+
const general = screen.getByRole('link', {name: 'General'});
|
|
854
|
+
const advanced = screen.getByRole('link', {name: 'Advanced'});
|
|
855
|
+
expect(indentOf(top)).toBeNull();
|
|
856
|
+
expect(indentOf(advanced)).toBe(
|
|
857
|
+
'calc(var(--spacing-2) + 2 * var(--spacing-6))',
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
// A split row (own href plus children) keeps the wrapper's base padding
|
|
861
|
+
// and puts the steps on the link, so the whole indent is clickable
|
|
862
|
+
// instead of dead space that still shows hover and press feedback.
|
|
863
|
+
expect(indentOf(general.parentElement!)).toBeNull();
|
|
864
|
+
expect(indentOf(general)).toBe('calc(1 * var(--spacing-6))');
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it('clips the children group only while it is collapsed', async () => {
|
|
868
|
+
// Nested rows sit flush with the group, so a permanent clip would cut off
|
|
869
|
+
// their focus ring. The group clips for the collapse animation only.
|
|
870
|
+
const user = userEvent.setup();
|
|
871
|
+
render(
|
|
872
|
+
<SideNavItem label="Settings" collapsible>
|
|
873
|
+
<SideNavItem label="General" href="/settings/general" />
|
|
874
|
+
</SideNavItem>,
|
|
875
|
+
);
|
|
876
|
+
const group = () => screen.getByRole('group', {hidden: true});
|
|
877
|
+
expect(declaredOn(group(), 'overflow')).toEqual(['visible']);
|
|
878
|
+
expect(declaredOn(group().firstElementChild!, 'overflow')).toEqual([]);
|
|
879
|
+
|
|
880
|
+
await user.click(screen.getByRole('button', {name: 'Settings'}));
|
|
881
|
+
expect(declaredOn(group(), 'overflow')).toEqual(['hidden']);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('puts the indent on the primary link of a nested actions row', () => {
|
|
885
|
+
render(
|
|
886
|
+
<SideNavItem label="Projects">
|
|
887
|
+
<SideNavItem
|
|
888
|
+
label="Alpha"
|
|
889
|
+
href="/projects/alpha"
|
|
890
|
+
actions={
|
|
891
|
+
<button type="button" aria-label="Alpha actions">
|
|
892
|
+
More
|
|
893
|
+
</button>
|
|
894
|
+
}
|
|
895
|
+
/>
|
|
896
|
+
</SideNavItem>,
|
|
897
|
+
);
|
|
898
|
+
const alpha = screen.getByRole('link', {name: 'Alpha'});
|
|
899
|
+
expect(indentOf(alpha.parentElement!)).toBeNull();
|
|
900
|
+
expect(indentOf(alpha)).toBe('calc(1 * var(--spacing-6))');
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it("composes a caller's style with the indent on a nested row", () => {
|
|
904
|
+
render(
|
|
905
|
+
<SideNavItem label="Settings">
|
|
906
|
+
<SideNavItem
|
|
907
|
+
label="General"
|
|
908
|
+
href="/settings/general"
|
|
909
|
+
style={{color: 'rebeccapurple'}}
|
|
910
|
+
/>
|
|
911
|
+
<SideNavItem
|
|
912
|
+
label="Security"
|
|
913
|
+
href="/settings/security"
|
|
914
|
+
style={{color: 'rebeccapurple'}}>
|
|
915
|
+
<SideNavItem label="Keys" href="/settings/security/keys" />
|
|
916
|
+
</SideNavItem>
|
|
917
|
+
</SideNavItem>,
|
|
918
|
+
);
|
|
919
|
+
// Ordinary row: the link is the row.
|
|
920
|
+
const general = screen.getByRole('link', {name: 'General'});
|
|
921
|
+
expect(general.style.color).toBe('rebeccapurple');
|
|
922
|
+
expect(indentOf(general)).toBe(
|
|
923
|
+
'calc(var(--spacing-2) + 1 * var(--spacing-6))',
|
|
924
|
+
);
|
|
925
|
+
// Split row: the caller's style still lands on the primary link.
|
|
926
|
+
const security = screen.getByRole('link', {name: 'Security'});
|
|
927
|
+
expect(security.style.color).toBe('rebeccapurple');
|
|
928
|
+
expect(indentOf(security)).toBe('calc(1 * var(--spacing-6))');
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it("lets a caller's inline-start padding replace the indent", () => {
|
|
932
|
+
// The caller's `style` composes after the indent: a caller-set
|
|
933
|
+
// inline-start padding is an inline declaration, so it wins over the
|
|
934
|
+
// indent's class rule on the element that carries the indent, on both
|
|
935
|
+
// the ordinary and the split row path.
|
|
936
|
+
render(
|
|
937
|
+
<SideNavItem label="Settings">
|
|
938
|
+
<SideNavItem
|
|
939
|
+
label="General"
|
|
940
|
+
href="/settings/general"
|
|
941
|
+
style={{paddingInlineStart: '20px'}}
|
|
942
|
+
/>
|
|
943
|
+
<SideNavItem
|
|
944
|
+
label="Security"
|
|
945
|
+
href="/settings/security"
|
|
946
|
+
style={{paddingInlineStart: '20px'}}>
|
|
947
|
+
<SideNavItem label="Keys" href="/settings/security/keys" />
|
|
948
|
+
</SideNavItem>
|
|
949
|
+
</SideNavItem>,
|
|
950
|
+
);
|
|
951
|
+
const general = screen.getByRole('link', {name: 'General'});
|
|
952
|
+
expect(general.style.paddingInlineStart).toBe('20px');
|
|
953
|
+
const security = screen.getByRole('link', {name: 'Security'});
|
|
954
|
+
expect(security.style.paddingInlineStart).toBe('20px');
|
|
955
|
+
// A row whose caller sets no padding keeps its indent.
|
|
956
|
+
expect(indentOf(screen.getByRole('link', {name: 'Keys'}))).toBe(
|
|
957
|
+
'calc(var(--spacing-2) + 2 * var(--spacing-6))',
|
|
958
|
+
);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('indents a small nested row from the small row padding', () => {
|
|
962
|
+
render(
|
|
963
|
+
<SideNavItem label="Settings">
|
|
964
|
+
<SideNavItem label="General" href="/settings/general" size="sm" />
|
|
965
|
+
</SideNavItem>,
|
|
966
|
+
);
|
|
967
|
+
expect(indentOf(screen.getByRole('link', {name: 'General'}))).toBe(
|
|
968
|
+
'calc(var(--spacing-1) + 1 * var(--spacing-6))',
|
|
969
|
+
);
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it('does not indent the first level of a collapsed rail flyout', async () => {
|
|
973
|
+
const user = userEvent.setup();
|
|
974
|
+
renderCollapsed(
|
|
975
|
+
<SideNavItem label="Settings" icon={StubIcon}>
|
|
976
|
+
<SideNavItem label="General" href="/settings/general">
|
|
977
|
+
<SideNavItem label="Advanced" href="/settings/general/advanced" />
|
|
978
|
+
</SideNavItem>
|
|
979
|
+
</SideNavItem>,
|
|
980
|
+
);
|
|
981
|
+
await user.click(screen.getByRole('button', {name: 'Settings'}));
|
|
982
|
+
|
|
983
|
+
// jsdom leaves the native popover `display: none`, hence `hidden: true`.
|
|
984
|
+
// The flyout lists the parent's children as its own top level.
|
|
985
|
+
const general = screen.getByRole('link', {name: 'General', hidden: true});
|
|
986
|
+
const advanced = screen.getByRole('link', {name: 'Advanced', hidden: true});
|
|
987
|
+
expect(indentOf(general)).toBeNull();
|
|
988
|
+
expect(indentOf(general.parentElement!)).toBeNull();
|
|
989
|
+
expect(indentOf(advanced)).toBe(
|
|
990
|
+
'calc(var(--spacing-2) + 1 * var(--spacing-6))',
|
|
991
|
+
);
|
|
992
|
+
});
|
|
993
|
+
|
|
831
994
|
it('passes data-testid', () => {
|
|
832
995
|
render(<SideNavItem label="Dashboard" data-testid="nav-item" />);
|
|
833
996
|
expect(screen.getByTestId('nav-item')).toBeInTheDocument();
|
|
@@ -2457,6 +2620,22 @@ function declaredOn(el: Element, prop: string): string[] {
|
|
|
2457
2620
|
.filter(value => value != null);
|
|
2458
2621
|
}
|
|
2459
2622
|
|
|
2623
|
+
/**
|
|
2624
|
+
* The nesting indent a row carries, or null for none. The indent is a
|
|
2625
|
+
* runtime StyleX value, so it rides the row's inline style as a custom
|
|
2626
|
+
* property; read the value rather than the generated property name.
|
|
2627
|
+
*/
|
|
2628
|
+
function indentOf(el: HTMLElement): string | null {
|
|
2629
|
+
const {style} = el;
|
|
2630
|
+
for (let i = 0; i < style.length; i++) {
|
|
2631
|
+
const value = style.getPropertyValue(style[i]).trim();
|
|
2632
|
+
if (value.includes('var(--spacing-6)')) {
|
|
2633
|
+
return value;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
return null;
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2460
2639
|
/**
|
|
2461
2640
|
* The selectors that actually paint the shared ring on `el`, read out of the
|
|
2462
2641
|
* live stylesheet with StyleX's `:not(#\#)` specificity padding stripped so
|
|
@@ -90,7 +90,7 @@ export const docs = {
|
|
|
90
90
|
{
|
|
91
91
|
name: 'children',
|
|
92
92
|
type: 'ReactNode',
|
|
93
|
-
description: 'Sub-items for nesting.',
|
|
93
|
+
description: 'Sub-items for nesting. Nested items span the full width of the nav list, like top-level items; their content is indented one step per nesting level.',
|
|
94
94
|
slotElements: [
|
|
95
95
|
{
|
|
96
96
|
__element: 'SideNavItem',
|
|
@@ -213,7 +213,7 @@ export const docsDense = {
|
|
|
213
213
|
'Passive right-side content only (badges, counts). Interactive controls go in actions.',
|
|
214
214
|
actions:
|
|
215
215
|
'Row-level secondary controls (icon buttons, menus) rendered as siblings of the primary element, after the expand/collapse toggle and before nested children in DOM/focus order. Inherit the row control size via SizeContext (explicit size wins). Hidden when rail collapsed. Passive content goes in endContent; interactive content goes here.',
|
|
216
|
-
children: 'Sub-items for nesting.',
|
|
216
|
+
children: 'Sub-items for nesting. Nested rows stay full width; content indents one step per level.',
|
|
217
217
|
collapsible: 'Enables collapse for items w/ children. true=uncontrolled, object=controlled mode.',
|
|
218
218
|
size: "Row size variant: 'sm' | 'md' | 'lg'.",
|
|
219
219
|
},
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* `TopNavMenu` use). Hover is a progressive enhancement over the popover's
|
|
18
18
|
* click behavior and is inert on coarse pointers.
|
|
19
19
|
*
|
|
20
|
+
* Nested rows span the full width of the nav list, like top-level rows, so
|
|
21
|
+
* their hover and selected fill lines up with the rows above them. The
|
|
22
|
+
* nesting indent sits inside the row as extra inline-start padding, one step
|
|
23
|
+
* per level, tracked through a private depth context.
|
|
24
|
+
*
|
|
20
25
|
* SYNC: When modified, update these files to stay in sync:
|
|
21
26
|
* - /packages/core/src/SideNav/SideNav.doc.mjs
|
|
22
27
|
* - /packages/core/src/SideNav/SideNav.test.tsx
|
|
@@ -26,6 +31,8 @@
|
|
|
26
31
|
*/
|
|
27
32
|
|
|
28
33
|
import {
|
|
34
|
+
createContext,
|
|
35
|
+
use,
|
|
29
36
|
useCallback,
|
|
30
37
|
useId,
|
|
31
38
|
useMemo,
|
|
@@ -108,26 +115,45 @@ const styles = stylex.create({
|
|
|
108
115
|
gap: spacingVars['--spacing-1'],
|
|
109
116
|
pointerEvents: 'auto',
|
|
110
117
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
// The group clips its rows only for the 0fr collapse animation. Nested rows
|
|
119
|
+
// sit flush with the group, so a permanent clip would cut off their focus
|
|
120
|
+
// ring, which paints outside the row. The expanded group therefore stops
|
|
121
|
+
// clipping once it has finished opening: `overflow` is discrete, so it rides
|
|
122
|
+
// the same transition with `allow-discrete` and a delay equal to the open
|
|
123
|
+
// duration, holding `hidden` until the rows' height has settled. Collapsing
|
|
124
|
+
// flips back to `hidden` at once (no delay on the collapsed state), so
|
|
125
|
+
// nothing spills while the group closes.
|
|
126
|
+
//
|
|
127
|
+
// Reduced fallback: a browser without `transition-behavior: allow-discrete`
|
|
128
|
+
// (the tier below current Baseline) cannot delay the switch, so the group
|
|
129
|
+
// stops clipping as soon as it starts opening and the opening rows briefly
|
|
130
|
+
// overlap the row below. The group still opens and the ring still shows.
|
|
114
131
|
childrenCollapsible: {
|
|
115
132
|
display: 'grid',
|
|
116
133
|
gridTemplateRows: '1fr',
|
|
117
|
-
|
|
134
|
+
overflow: 'visible',
|
|
135
|
+
transitionProperty: 'grid-template-rows, overflow',
|
|
136
|
+
transitionBehavior: 'allow-discrete',
|
|
118
137
|
transitionDuration: {
|
|
119
|
-
default: durationVars['--duration-medium'],
|
|
120
|
-
'@media (prefers-reduced-motion: reduce)': '0s',
|
|
138
|
+
default: `${durationVars['--duration-medium']}, 0s`,
|
|
139
|
+
'@media (prefers-reduced-motion: reduce)': '0s, 0s',
|
|
140
|
+
},
|
|
141
|
+
transitionDelay: {
|
|
142
|
+
default: `0s, ${durationVars['--duration-medium']}`,
|
|
143
|
+
'@media (prefers-reduced-motion: reduce)': '0s, 0s',
|
|
121
144
|
},
|
|
122
145
|
transitionTimingFunction: easeVars['--ease-standard'],
|
|
123
146
|
},
|
|
124
147
|
childrenCollapsed: {
|
|
125
148
|
gridTemplateRows: '0fr',
|
|
149
|
+
overflow: 'hidden',
|
|
150
|
+
transitionDelay: '0s, 0s',
|
|
126
151
|
},
|
|
152
|
+
// No inline padding: nested rows fill the full width of the nav list, and
|
|
153
|
+
// each row carries its own indent (`indentStyles` below). `minHeight: 0`
|
|
154
|
+
// lets the 0fr track shrink this item to nothing; the group clips it.
|
|
127
155
|
childrenInner: {
|
|
128
|
-
overflow: 'hidden',
|
|
129
156
|
minHeight: 0,
|
|
130
|
-
paddingInlineStart: spacingVars['--spacing-6'],
|
|
131
157
|
},
|
|
132
158
|
expandChevron: {
|
|
133
159
|
display: 'inline-flex',
|
|
@@ -232,6 +258,35 @@ const styles = stylex.create({
|
|
|
232
258
|
},
|
|
233
259
|
});
|
|
234
260
|
|
|
261
|
+
// Nesting indent, drawn inside the row: one `--spacing-6` step per nesting
|
|
262
|
+
// level. The row itself stays full width, so its hover and selected fill
|
|
263
|
+
// matches a top-level row's.
|
|
264
|
+
// - `sm` / `md` — the ordinary row, where the row element is the link or
|
|
265
|
+
// button: its own inline-start padding plus the steps.
|
|
266
|
+
// SYNC: the base padding matches navItemStyles.sm / .md / .lg.
|
|
267
|
+
// - `primary` — the row-wrapper path: the wrapper keeps its base padding and
|
|
268
|
+
// the steps go on the primary link or button, so the whole indent is part
|
|
269
|
+
// of the click target rather than dead space that still shows feedback.
|
|
270
|
+
const indentStyles = stylex.create({
|
|
271
|
+
sm: (depth: number) => ({
|
|
272
|
+
paddingInlineStart: `calc(${spacingVars['--spacing-1']} + ${depth} * ${spacingVars['--spacing-6']})`,
|
|
273
|
+
}),
|
|
274
|
+
md: (depth: number) => ({
|
|
275
|
+
paddingInlineStart: `calc(${spacingVars['--spacing-2']} + ${depth} * ${spacingVars['--spacing-6']})`,
|
|
276
|
+
}),
|
|
277
|
+
primary: (depth: number) => ({
|
|
278
|
+
paddingInlineStart: `calc(${depth} * ${spacingVars['--spacing-6']})`,
|
|
279
|
+
}),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* How many SideNavItems deep the current item sits: 0 at the top level,
|
|
284
|
+
* 1 inside one parent, and so on. A collapsed rail's flyout resets it to 0,
|
|
285
|
+
* since the flyout lists a parent's children as its own top level.
|
|
286
|
+
*/
|
|
287
|
+
const SideNavItemDepthContext = createContext(0);
|
|
288
|
+
SideNavItemDepthContext.displayName = 'SideNavItemDepthContext';
|
|
289
|
+
|
|
235
290
|
/**
|
|
236
291
|
* Cascaded to the `actions` slot through `SizeContext` so a consumer's row
|
|
237
292
|
* controls come out the same height as the built-in expand/collapse toggle,
|
|
@@ -363,7 +418,9 @@ export interface SideNavItemProps extends BaseProps<HTMLElement> {
|
|
|
363
418
|
*/
|
|
364
419
|
actions?: ReactNode;
|
|
365
420
|
/**
|
|
366
|
-
* Sub-items for nesting.
|
|
421
|
+
* Sub-items for nesting. Nested items span the full width of the nav
|
|
422
|
+
* list, like top-level items; their content is indented one step per
|
|
423
|
+
* nesting level.
|
|
367
424
|
*/
|
|
368
425
|
children?: ReactNode;
|
|
369
426
|
/**
|
|
@@ -455,6 +512,7 @@ export function SideNavItem({
|
|
|
455
512
|
const id = useId();
|
|
456
513
|
const hasChildren = !!children;
|
|
457
514
|
const itemRef = useRef<HTMLDivElement>(null);
|
|
515
|
+
const depth = use(SideNavItemDepthContext);
|
|
458
516
|
|
|
459
517
|
// Popover for collapsed items with children
|
|
460
518
|
const popover = usePopover({
|
|
@@ -608,7 +666,9 @@ export function SideNavItem({
|
|
|
608
666
|
onClick={() => popover.hide()}>
|
|
609
667
|
<div {...stylex.props(styles.popoverHeader)}>{label}</div>
|
|
610
668
|
<SideNavCollapseContext value={EXPANDED_COLLAPSE_STATE}>
|
|
611
|
-
{
|
|
669
|
+
<SideNavItemDepthContext value={0}>
|
|
670
|
+
{children}
|
|
671
|
+
</SideNavItemDepthContext>
|
|
612
672
|
</SideNavCollapseContext>
|
|
613
673
|
</div>,
|
|
614
674
|
{placement: 'end', alignment: 'start', xstyle: styles.popoverGap},
|
|
@@ -686,6 +746,14 @@ export function SideNavItem({
|
|
|
686
746
|
isSelected && navItemStyles.selected,
|
|
687
747
|
isDisabled && navItemStyles.disabled,
|
|
688
748
|
] as const;
|
|
749
|
+
const rowIndent =
|
|
750
|
+
depth > 0 && indentStyles[size === 'sm' ? 'sm' : 'md'](depth);
|
|
751
|
+
const primaryIndent = depth > 0 && indentStyles.primary(depth);
|
|
752
|
+
|
|
753
|
+
// The indent is a runtime value, so it reaches the element as an inline
|
|
754
|
+
// style. These props are spread after `rest`, so fold the caller's `style`
|
|
755
|
+
// back in (caller wins) rather than letting the indent replace it.
|
|
756
|
+
const callerStyle = {style: rest.style};
|
|
689
757
|
|
|
690
758
|
// Three shapes of the same row appearance:
|
|
691
759
|
// - `rowProps` — split-action path (toggle, no actions): presentational
|
|
@@ -702,7 +770,10 @@ export function SideNavItem({
|
|
|
702
770
|
const focusableRowProps = {
|
|
703
771
|
...mergeProps(
|
|
704
772
|
itemThemeProps,
|
|
705
|
-
|
|
773
|
+
mergeProps(
|
|
774
|
+
focusOutlineProps.focusVisible(...itemStyleArgs, rowIndent),
|
|
775
|
+
callerStyle,
|
|
776
|
+
),
|
|
706
777
|
),
|
|
707
778
|
...pressable,
|
|
708
779
|
};
|
|
@@ -748,11 +819,17 @@ export function SideNavItem({
|
|
|
748
819
|
onClick={handleClick}
|
|
749
820
|
{...rest}
|
|
750
821
|
{...rowPrimaryAriaProps}
|
|
751
|
-
{...(
|
|
752
|
-
|
|
753
|
-
//
|
|
754
|
-
|
|
755
|
-
|
|
822
|
+
{...mergeProps(
|
|
823
|
+
hasActions
|
|
824
|
+
? // The wrapper rings for this element; suppressing here is what
|
|
825
|
+
// keeps the UA's own ring from painting inside that one.
|
|
826
|
+
focusOutlineProps.suppressed(styles.splitAction, primaryIndent)
|
|
827
|
+
: focusOutlineProps.focusVisible(
|
|
828
|
+
styles.splitAction,
|
|
829
|
+
primaryIndent,
|
|
830
|
+
),
|
|
831
|
+
callerStyle,
|
|
832
|
+
)}>
|
|
756
833
|
{itemContent}
|
|
757
834
|
</NavItemElement>
|
|
758
835
|
{hasIndependentToggle && (
|
|
@@ -831,7 +908,9 @@ export function SideNavItem({
|
|
|
831
908
|
<span id={`${id}-label`} hidden>
|
|
832
909
|
{label}
|
|
833
910
|
</span>
|
|
834
|
-
{
|
|
911
|
+
<SideNavItemDepthContext value={depth + 1}>
|
|
912
|
+
{children}
|
|
913
|
+
</SideNavItemDepthContext>
|
|
835
914
|
</div>
|
|
836
915
|
</div>
|
|
837
916
|
)}
|
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 =
|
|
@@ -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: {
|