@jxsuite/ui 0.1.0 → 0.3.0
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/components/jx-action-button.json +7 -1
- package/components/jx-action-group.json +8 -0
- package/components/jx-button.json +8 -1
- package/components/jx-checkbox.json +8 -1
- package/components/jx-combobox.json +7 -0
- package/components/jx-menu-item.json +1 -1
- package/components/jx-switch.json +82 -4
- package/components/jx-textfield.json +6 -0
- package/package.json +2 -2
- package/src/behaviors/listbox.ts +23 -1
- package/src/behaviors/split.ts +28 -1
- package/src/behaviors/tooltip.ts +16 -12
|
@@ -455,9 +455,15 @@
|
|
|
455
455
|
"color": "var(--jx-accent-fg)"
|
|
456
456
|
},
|
|
457
457
|
"&[data-size=\"sm\"] > [part=\"control\"]": {
|
|
458
|
+
"$description": "The small action button is the kit's most packed control — Studio's toolbars, block action bar and inspector rows declare it some four hundred times — and it draws 4px under the kit's control height, which is the WCAG 2.2 SC 2.5.8 floor (ui.md §4.3). The `::before` is the hit area, jx-tab's close-button technique: an empty pseudo-element that reaches 2px past the drawn box on every side, so the region that accepts the pointer is exactly `--jx-control-h` square while the picture stays 20px. The inset is -3px and not -2px because an absolutely positioned box is offset from its containing block's PADDING edge, and this control draws a 1px border: the border width comes first, then half of the 4px the box is under (jx-tab's close button is borderless, so -4px was exact there). The control is already `position: relative` for the badge, so the pseudo-element needs nothing more. A 4px gap (jx-toolbar's) lets two extended neighbours abut; a compact jx-action-group packs its members touching, and its sheet zeroes the extension on that axis, because an extension over a neighbour's box would take the neighbour's clicks, and the criterion gives an overlapped strip to the target on top rather than to both; a default group's 2px gap leaves every member but the last at 22px on that axis, which ui.md §11 names as a non-claim. Held by target-size.test.ts and the conformance gate, both of which read the border and subtract it.",
|
|
458
459
|
"minWidth": "calc(var(--jx-control-h) - 4px)",
|
|
459
460
|
"minHeight": "calc(var(--jx-control-h) - 4px)",
|
|
460
|
-
"padding": "0 var(--jx-space-1)"
|
|
461
|
+
"padding": "0 var(--jx-space-1)",
|
|
462
|
+
"::before": {
|
|
463
|
+
"content": "\"\"",
|
|
464
|
+
"position": "absolute",
|
|
465
|
+
"inset": "-3px"
|
|
466
|
+
}
|
|
461
467
|
},
|
|
462
468
|
"&[data-size=\"lg\"] > [part=\"control\"]": {
|
|
463
469
|
"minWidth": "calc(var(--jx-control-h) + 8px)",
|
|
@@ -113,6 +113,14 @@
|
|
|
113
113
|
},
|
|
114
114
|
"&[data-compact] > jx-action-button > [part=\"control\"]:focus-visible": {
|
|
115
115
|
"zIndex": "1"
|
|
116
|
+
},
|
|
117
|
+
"&[data-compact] > jx-action-button[data-size=\"sm\"] > [part=\"control\"]::before": {
|
|
118
|
+
"$description": "A small action button carries a `::before` that extends its hit area 2px past its drawn box on every side (an inset of -3px: the button's 1px border, then half the 4px it is under, because an absolutely positioned box is offset from the padding edge), so that a 20px picture accepts the pointer over the kit's 24px control height (WCAG 2.2 SC 2.5.8, ui.md §4.3). In a compact group the boxes TOUCH on the packed axis — no gap, and each border drawn over the last — so an extension there could reach nothing but the neighbour's own box, and the later sibling paints above the earlier one: the last 2px of every button but the last would activate the button after it. The criterion gives an overlapped area to the target on TOP, so the extension would buy the earlier button nothing and cost it a stolen strip. The default 2px gap is the other case: two extensions overlap by 2px there, the later member keeps the strip, and every small member but the last counts 22px on the packed axis, which is under the floor and which ui.md §11 names beside this one. The packed axis is therefore zeroed here — `0` is the padding edge, 1px inside the drawn border, and the button's own box accepts the pointer over that pixel — and the cross axis keeps its 2px past the drawn box, which reaches only the gap around the group: a compact group's small members are 24px tall and as wide as they draw, which ui.md §11 records as the one thing the technique cannot reach. The vertical variant swaps the axes, and its inline value is the button's own -3px for the same reason.",
|
|
119
|
+
"insetInline": "0"
|
|
120
|
+
},
|
|
121
|
+
"&[data-compact][data-orientation=\"vertical\"] > jx-action-button[data-size=\"sm\"] > [part=\"control\"]::before": {
|
|
122
|
+
"insetInline": "-3px",
|
|
123
|
+
"insetBlock": "0"
|
|
116
124
|
}
|
|
117
125
|
},
|
|
118
126
|
"children": [
|
|
@@ -297,9 +297,16 @@
|
|
|
297
297
|
}
|
|
298
298
|
},
|
|
299
299
|
"&[data-size=\"sm\"] > [part=\"control\"]": {
|
|
300
|
+
"$description": "A small button draws 4px under the kit's control height, and the control height is the WCAG 2.2 SC 2.5.8 floor (ui.md §4.3), so the drawn box alone would be a 20px pointer target at the default density. The `::before` is the hit area: an empty pseudo-element that reaches 2px past the drawn box on every side, so what accepts the pointer is exactly `--jx-control-h` square while the picture stays small — jx-tab's close button's technique, and the criterion measures the region that accepts the pointer, not the paint. The inset is -3px and not -2px because an absolutely positioned box is offset from its containing block's PADDING edge, and this control draws a 1px border: the border width comes first, then half of the 4px the box is under. jx-tab's close button is borderless, which is why -4px was exact there, and why the number did not carry over unchanged. In a jx-toolbar (a 4px gap) two extended neighbours abut and neither loses a pixel; jx-action-group's own sheet zeroes the packed axis in a compact group, where the boxes touch and an extension could only steal a strip of the neighbour. Held by target-size.test.ts and the conformance gate, both of which read the border and subtract it.",
|
|
301
|
+
"position": "relative",
|
|
300
302
|
"minHeight": "calc(var(--jx-control-h) - 4px)",
|
|
301
303
|
"padding": "0 var(--jx-space-3)",
|
|
302
|
-
"fontSize": "var(--jx-text-sm)"
|
|
304
|
+
"fontSize": "var(--jx-text-sm)",
|
|
305
|
+
"::before": {
|
|
306
|
+
"content": "\"\"",
|
|
307
|
+
"position": "absolute",
|
|
308
|
+
"inset": "-3px"
|
|
309
|
+
}
|
|
303
310
|
},
|
|
304
311
|
"&[data-size=\"lg\"] > [part=\"control\"]": {
|
|
305
312
|
"minHeight": "calc(var(--jx-control-h) + 8px)",
|
|
@@ -146,9 +146,16 @@
|
|
|
146
146
|
"color": "var(--jx-fg-dim)"
|
|
147
147
|
},
|
|
148
148
|
"&[data-size=\"sm\"] > [part=\"control\"]": {
|
|
149
|
+
"$description": "The control is the `<label>` wrapping the input, and the label is the pointer target: a click anywhere on it toggles the box. At `sm` it draws 4px under the kit's control height, the WCAG 2.2 SC 2.5.8 floor (ui.md \u00a74.3), so the `::before` extends the hit area 2px beyond the box on every side — jx-tab's close-button technique, the same shape jx-button carries — and the region that accepts the pointer is `--jx-control-h` tall while the row stays 20px. -2px is exact here where jx-button needs -3px: an absolutely positioned box is offset from the PADDING edge, and the label draws no border, so its padding edge is its drawn edge. The label needs `position: relative` here for the pseudo-element to measure against; the input inside is 14px and needs nothing of its own, because the label around it is the target. Held by target-size.test.ts and the conformance gate.",
|
|
150
|
+
"position": "relative",
|
|
149
151
|
"minHeight": "calc(var(--jx-control-h) - 4px)",
|
|
150
152
|
"fontSize": "var(--jx-text-sm)",
|
|
151
|
-
"lineHeight": "var(--jx-leading-sm)"
|
|
153
|
+
"lineHeight": "var(--jx-leading-sm)",
|
|
154
|
+
"::before": {
|
|
155
|
+
"content": "\"\"",
|
|
156
|
+
"position": "absolute",
|
|
157
|
+
"inset": "-2px"
|
|
158
|
+
}
|
|
152
159
|
},
|
|
153
160
|
"&[data-size=\"sm\"] [part=\"input\"]": {
|
|
154
161
|
"width": "calc(var(--jx-icon-size) - 2px)",
|
|
@@ -321,6 +321,8 @@
|
|
|
321
321
|
"fontSize": "var(--jx-text-lg)"
|
|
322
322
|
},
|
|
323
323
|
"& [part=\"toggle\"]": {
|
|
324
|
+
"$description": "The toggle sits inside the field's border, so it draws 2px under the field's height: 22px in a 24px field, 18px in an `sm` one. The `::before` is its hit area (WCAG 2.2 SC 2.5.8, ui.md \u00a74.3), the same shape as jx-textfield's clear button: an empty pseudo-element inset by half the difference between the drawn box and the kit's control height, -1px on a default field and -3px on a small one, so the region that accepts the pointer is `--jx-control-h` square at every size while the caret and its box stay where they were; on a large field the inset is positive and the pseudo-element is an invisible box inside a button already over the floor. `position: relative` is what the pseudo-element measures against, and it is new here: the toggle is a flex item of the field, not an absolutely placed one. The rotation the open state applies turns the pseudo-element with the button, which a square does not mind.",
|
|
325
|
+
"position": "relative",
|
|
324
326
|
"display": "inline-flex",
|
|
325
327
|
"alignItems": "center",
|
|
326
328
|
"justifyContent": "center",
|
|
@@ -338,6 +340,11 @@
|
|
|
338
340
|
"cursor": "pointer",
|
|
339
341
|
"outline": "none",
|
|
340
342
|
"transition": "rotate var(--jx-dur-1) var(--jx-ease-out)",
|
|
343
|
+
"::before": {
|
|
344
|
+
"content": "\"\"",
|
|
345
|
+
"position": "absolute",
|
|
346
|
+
"inset": "calc((var(--jx-combobox-h) - 2px - var(--jx-control-h)) / 2)"
|
|
347
|
+
},
|
|
341
348
|
":hover": {
|
|
342
349
|
"color": "var(--jx-fg)"
|
|
343
350
|
},
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"type": "string",
|
|
32
32
|
"default": "",
|
|
33
33
|
"attribute": "requires",
|
|
34
|
-
"description": "The one-sentence reason a
|
|
34
|
+
"description": "The one-sentence reason a DISABLED row cannot act, drawn as the row's native title while it is disabled and nowhere at all while it is not. The title is the whole contract here, and deliberately not the tooltip hybrid jx-button, jx-action-button and jx-switch ship: a disabled row can open no tip, so the hybrid collapses to its disabled half, and an ENABLED row never carries a requires — every Studio projection fills it from the registry's disabledReason, which is undefined for a command that can run, and a reason on a row that can act is a contradiction rather than a hint. Studio's menu surface also prints the same sentence as the row's visible description (`Needs …`), so a keyboard reader hears it without the title."
|
|
35
35
|
},
|
|
36
36
|
"checked": {
|
|
37
37
|
"type": "string",
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
"name",
|
|
13
13
|
"size"
|
|
14
14
|
],
|
|
15
|
+
"$elements": [
|
|
16
|
+
{
|
|
17
|
+
"$ref": "./jx-tooltip.json"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
15
20
|
"state": {
|
|
16
21
|
"checked": {
|
|
17
22
|
"type": "boolean",
|
|
@@ -23,13 +28,13 @@
|
|
|
23
28
|
"type": "boolean",
|
|
24
29
|
"default": false,
|
|
25
30
|
"attribute": "disabled",
|
|
26
|
-
"description": "Cannot be flipped or focused. Disabled, never absent."
|
|
31
|
+
"description": "Cannot be flipped or focused. Disabled, never absent, and the hint moves from the tip to the row's title while it is: a disabled control cannot open a tip, and why the setting cannot be changed is exactly what the reader hovers to find out."
|
|
27
32
|
},
|
|
28
33
|
"hint": {
|
|
29
34
|
"type": "string",
|
|
30
35
|
"default": "",
|
|
31
36
|
"attribute": "hint",
|
|
32
|
-
"description": "The tooltip on the row:
|
|
37
|
+
"description": "The tooltip on the row: what turning the setting on does, or why it cannot be changed. The same hybrid jx-button and jx-action-button ship. While the switch is ENABLED it is drawn as a jx-tooltip child of this element — shown on keyboard focus at once and on hover after the tip's delay, hoverable and dismissible with Escape (SC 1.4.13) — and the control names it in aria-describedby, so it is a description beside the name and never the name itself. While the switch is DISABLED it is the native title of the wrapping label instead, because a disabled control receives no focus and no pointer event and could never open a tip, while the platform still shows a title on hover. Never both at once; empty draws neither. ONE difference from the buttons, and it is the platform's rather than the kit's: the tip is wired by `for` alone and the row declares NO `interestfor`. Interest invokers are supported on `a`, `area` and `button` only — never on a label or an input — so the attribute here would show nothing on Chrome 152 while telling the tooltip sidecar the platform had the pair, which is the one combination that draws no tip on any engine. The sidecar binds the label on every engine instead, and that is what the switch pays for a tooltip: six listeners on the row and two on the tip, always, where a button pays them only where the platform lacks the attribute."
|
|
33
38
|
},
|
|
34
39
|
"label": {
|
|
35
40
|
"type": "string",
|
|
@@ -66,6 +71,47 @@
|
|
|
66
71
|
"default": "",
|
|
67
72
|
"description": "Forwarded to the inner control as its tabindex, which is what makes a roving caret possible: the focusable node is the input inside, not the host and not the label that wraps it, so a jx-toolbar moving focus writes this rather than reaching into a child's internals. It is a PROPERTY ONLY (el.tabindex = \"0\") and deliberately not an observed attribute: written as an attribute it would land on the host as well, and a host carrying tabindex is itself focusable, so the one control would become two tab stops."
|
|
68
73
|
},
|
|
74
|
+
"uid": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"default": "",
|
|
77
|
+
"description": "This row's own id stem, minted on mount by the tooltip sidecar. The label's id, the tip's id and the two attributes that wire one to the other are all built from it, so a consumer writes none of them and two hinted switches on one panel cannot collide. Empty until the mount microtask, which is why every binding that reads it guards on it."
|
|
78
|
+
},
|
|
79
|
+
"hasTip": {
|
|
80
|
+
"$expression": {
|
|
81
|
+
"operator": "&&",
|
|
82
|
+
"target": {
|
|
83
|
+
"operator": "!",
|
|
84
|
+
"target": {
|
|
85
|
+
"operator": "!",
|
|
86
|
+
"target": {
|
|
87
|
+
"$ref": "#/state/hint"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"value": {
|
|
92
|
+
"operator": "&&",
|
|
93
|
+
"target": {
|
|
94
|
+
"operator": "!",
|
|
95
|
+
"target": {
|
|
96
|
+
"$ref": "#/state/disabled"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"value": {
|
|
100
|
+
"operator": "!==",
|
|
101
|
+
"target": {
|
|
102
|
+
"$ref": "#/state/uid"
|
|
103
|
+
},
|
|
104
|
+
"value": ""
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"onMount": {
|
|
110
|
+
"$prototype": "Function",
|
|
111
|
+
"$src": "jx-ui:/behaviors/tooltip.ts",
|
|
112
|
+
"$export": "mintHintId",
|
|
113
|
+
"description": "Mints the id stem the hint's tooltip is wired by. A document cannot: the closed operator set has no counter and no identity."
|
|
114
|
+
},
|
|
69
115
|
"onChange": {
|
|
70
116
|
"$prototype": "Function",
|
|
71
117
|
"body": [
|
|
@@ -178,6 +224,9 @@
|
|
|
178
224
|
"& [part=\"label\"]:empty": {
|
|
179
225
|
"display": "none"
|
|
180
226
|
},
|
|
227
|
+
"& > [part=\"tooltip\"]": {
|
|
228
|
+
"display": "contents"
|
|
229
|
+
},
|
|
181
230
|
"@(forced-colors: active)": {
|
|
182
231
|
"& [part=\"input\"]": {
|
|
183
232
|
"forcedColorAdjust": "none",
|
|
@@ -208,7 +257,8 @@
|
|
|
208
257
|
"tagName": "label",
|
|
209
258
|
"attributes": {
|
|
210
259
|
"part": "control",
|
|
211
|
-
"
|
|
260
|
+
"id": "${state.uid || null}",
|
|
261
|
+
"title": "${state.disabled && state.hint ? state.hint : null}"
|
|
212
262
|
},
|
|
213
263
|
"children": [
|
|
214
264
|
{
|
|
@@ -219,7 +269,7 @@
|
|
|
219
269
|
"role": "switch",
|
|
220
270
|
"aria-label": "${state.label || null}",
|
|
221
271
|
"aria-labelledby": "${state.labelledby || null}",
|
|
222
|
-
"aria-describedby": "${state.describedby || null}",
|
|
272
|
+
"aria-describedby": "${[state.hasTip ? state.uid + '-tip' : null, state.describedby || null].filter(Boolean).join(' ') || null}",
|
|
223
273
|
"tabindex": "${state.tabindex || null}",
|
|
224
274
|
"name": "${state.name || null}",
|
|
225
275
|
"disabled": {
|
|
@@ -248,6 +298,34 @@
|
|
|
248
298
|
]
|
|
249
299
|
}
|
|
250
300
|
]
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"tagName": "span",
|
|
304
|
+
"attributes": {
|
|
305
|
+
"part": "tooltip"
|
|
306
|
+
},
|
|
307
|
+
"$switch": {
|
|
308
|
+
"$ref": "#/state/hasTip"
|
|
309
|
+
},
|
|
310
|
+
"cases": {
|
|
311
|
+
"true": {
|
|
312
|
+
"tagName": "jx-tooltip",
|
|
313
|
+
"attributes": {
|
|
314
|
+
"part": "tip",
|
|
315
|
+
"id": "${state.uid}-tip",
|
|
316
|
+
"for": "${state.uid}"
|
|
317
|
+
},
|
|
318
|
+
"children": [
|
|
319
|
+
{
|
|
320
|
+
"tagName": "span",
|
|
321
|
+
"attributes": {
|
|
322
|
+
"part": "tip-text"
|
|
323
|
+
},
|
|
324
|
+
"textContent": "${state.hint}"
|
|
325
|
+
}
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
}
|
|
251
329
|
}
|
|
252
330
|
]
|
|
253
331
|
}
|
|
@@ -447,9 +447,15 @@
|
|
|
447
447
|
"fontSize": "var(--jx-text-lg)"
|
|
448
448
|
},
|
|
449
449
|
"& [part=\"clear\"]": {
|
|
450
|
+
"$description": "The clear button sits inside the field's border, so it draws 2px under the field's height: 22px in a 24px field, and 18px in an `sm` one. The `::before` is its hit area (WCAG 2.2 SC 2.5.8, ui.md \u00a74.3): an empty pseudo-element inset by half the difference between the drawn box and the kit's control height, which is -1px on a default field and -3px on a small one, so the region that accepts the pointer is `--jx-control-h` square at every size while the glyph and its hover box stay where they were. On a large field the difference is positive and the pseudo-element is an invisible box INSIDE a button that is already over the floor, which changes nothing. jx-tab's close button is the technique; the button is absolutely positioned already, so it is its own containing block. The extension lies over the input it clears, which was true of the drawn button too, and 2px past the field's edge at `sm`, where a neighbour at the kit's 4px gap extends 2px back and the two abut.",
|
|
450
451
|
"position": "absolute",
|
|
451
452
|
"insetBlockStart": "1px",
|
|
452
453
|
"insetInlineEnd": "1px",
|
|
454
|
+
"::before": {
|
|
455
|
+
"content": "\"\"",
|
|
456
|
+
"position": "absolute",
|
|
457
|
+
"inset": "calc((var(--jx-textfield-h) - 2px - var(--jx-control-h)) / 2)"
|
|
458
|
+
},
|
|
453
459
|
"display": "inline-flex",
|
|
454
460
|
"alignItems": "center",
|
|
455
461
|
"justifyContent": "center",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The Jx UI kit: interface elements authored as Jx documents, with their theme and icons",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"upgrade": "bun update --latest && bun install"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@jxsuite/runtime": "^4.0.
|
|
61
|
+
"@jxsuite/runtime": "^4.0.1",
|
|
62
62
|
"@jxsuite/schema": "^2.2.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
package/src/behaviors/listbox.ts
CHANGED
|
@@ -106,8 +106,30 @@ export function syncListbox(host: HTMLElement, active: string): OptionElement |
|
|
|
106
106
|
*/
|
|
107
107
|
export function mountListbox(state: ListboxState, host: HTMLElement): void {
|
|
108
108
|
syncListbox(host, String(state.active ?? ""));
|
|
109
|
+
/* One sync per frame, not one per mutation record. The palette opens by constructing its row
|
|
110
|
+
set: dozens of `childList` records in one task, and a sync per record is a querySelectorAll
|
|
111
|
+
walk plus a flush-baiting read each — the DevTools timeline billed every one of them. The
|
|
112
|
+
LAST record of a task settles the same active id the task started with (a filter's rows land
|
|
113
|
+
whole, and a caret move never changes rows), so reading `active` at frame time is the same
|
|
114
|
+
answer once, with one style pass for the burst instead of one per record. */
|
|
115
|
+
let frameId: number | null = null;
|
|
116
|
+
const scheduleSync = (): void => {
|
|
117
|
+
if (frameId !== null) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
frameId = requestAnimationFrame(() => {
|
|
121
|
+
frameId = null;
|
|
122
|
+
if (host.isConnected) {
|
|
123
|
+
syncListbox(host, String(state.active ?? ""));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
};
|
|
109
127
|
const observer = new MutationObserver(() => {
|
|
110
|
-
|
|
128
|
+
if (typeof requestAnimationFrame === "function") {
|
|
129
|
+
scheduleSync();
|
|
130
|
+
} else {
|
|
131
|
+
syncListbox(host, String(state.active ?? ""));
|
|
132
|
+
}
|
|
111
133
|
});
|
|
112
134
|
observer.observe(host, {
|
|
113
135
|
attributeFilter: ["data-active"],
|
package/src/behaviors/split.ts
CHANGED
|
@@ -202,11 +202,37 @@ function coordOf(vertical: boolean, event: { clientX: number; clientY: number })
|
|
|
202
202
|
* realm cannot see through — walking past it costs a frame's work once per gesture and is the only
|
|
203
203
|
* thing that makes the pane grid's two nested `display: contents` rows measurable at all.
|
|
204
204
|
*
|
|
205
|
+
* The ancestor that resolved as the track is MEMOIZED per host and re-read per call. Only its
|
|
206
|
+
* identity is cached; the length is measured fresh every time, so a window resize answers the new
|
|
207
|
+
* track width with no listener anywhere. Re-measuring one element's box instead of walking with a
|
|
208
|
+
* computed-style test per ancestor is what profiling pointed at: every gesture during a shell rep
|
|
209
|
+
* forced the dirty-tree flush and the DevTools timeline charged the recalc to the read, five times
|
|
210
|
+
* per boot with a document open. The cache re-walks when the remembered track no longer contains
|
|
211
|
+
* the host (reparenting, teardown) — containment and connectivity are node walks, not layout — and
|
|
212
|
+
* when the remembered track measures zero, because an unlaid-out track resolving later (the first
|
|
213
|
+
* frame after a pane opens) is the case the walk exists for.
|
|
214
|
+
*
|
|
205
215
|
* @param host The element.
|
|
206
216
|
* @param vertical Whether the drag is along x.
|
|
207
217
|
* @returns The length in pixels, or 0 when nothing above the splitter has one.
|
|
208
218
|
*/
|
|
209
|
-
|
|
219
|
+
const trackCache = new WeakMap<HTMLElement, { element: HTMLElement; vertical: boolean }>();
|
|
220
|
+
|
|
221
|
+
export function trackOf(host: HTMLElement, vertical: boolean): number {
|
|
222
|
+
const cached = trackCache.get(host);
|
|
223
|
+
if (
|
|
224
|
+
cached &&
|
|
225
|
+
cached.vertical === vertical &&
|
|
226
|
+
cached.element.isConnected &&
|
|
227
|
+
host.isConnected &&
|
|
228
|
+
cached.element.contains(host)
|
|
229
|
+
) {
|
|
230
|
+
const box = cached.element.getBoundingClientRect();
|
|
231
|
+
const length = vertical ? box.width : box.height;
|
|
232
|
+
if (length > 0) {
|
|
233
|
+
return length;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
210
236
|
for (let node = host.parentElement; node; node = node.parentElement) {
|
|
211
237
|
if (globalThis.getComputedStyle(node).display === "contents") {
|
|
212
238
|
continue;
|
|
@@ -214,6 +240,7 @@ function trackOf(host: HTMLElement, vertical: boolean): number {
|
|
|
214
240
|
const box = node.getBoundingClientRect();
|
|
215
241
|
const length = vertical ? box.width : box.height;
|
|
216
242
|
if (length > 0) {
|
|
243
|
+
trackCache.set(host, { element: node, vertical });
|
|
217
244
|
return length;
|
|
218
245
|
}
|
|
219
246
|
}
|
package/src/behaviors/tooltip.ts
CHANGED
|
@@ -689,17 +689,21 @@ let mintedHints = 0;
|
|
|
689
689
|
* Mint the id stem a control that draws its `hint` as a `jx-tooltip` needs, and write it into
|
|
690
690
|
* `uid`.
|
|
691
691
|
*
|
|
692
|
-
* `jx-action-button` and `jx-
|
|
693
|
-
* is wired by id in both directions: `interestfor` on the control naming the tip,
|
|
694
|
-
* shows it with nothing bound on an engine that has interest invokers (Chrome 152 —
|
|
695
|
-
* desktop app's Chromium 147, which is on the fallback path until the CEF bump reaches
|
|
696
|
-
* on the tip naming the control, which is what {@link onTooltipReady} resolves by id
|
|
697
|
-
* an attribute scan of the document; and `aria-describedby` on the control naming
|
|
698
|
-
* which is what makes the hint a DESCRIPTION beside the name rather than the name
|
|
699
|
-
* consumer writes none of the three, and two hinted
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
692
|
+
* `jx-action-button`, `jx-button` and `jx-switch` render their tip as a child of their own element,
|
|
693
|
+
* and the pair is wired by id in both directions: `interestfor` on the control naming the tip,
|
|
694
|
+
* which is what shows it with nothing bound on an engine that has interest invokers (Chrome 152 —
|
|
695
|
+
* and NOT the desktop app's Chromium 147, which is on the fallback path until the CEF bump reaches
|
|
696
|
+
* it); `for` on the tip naming the control, which is what {@link onTooltipReady} resolves by id
|
|
697
|
+
* rather than by an attribute scan of the document; and `aria-describedby` on the control naming
|
|
698
|
+
* the tip again, which is what makes the hint a DESCRIPTION beside the name rather than the name
|
|
699
|
+
* itself. So a consumer writes none of the three, and two hinted controls in one toolbar cannot
|
|
700
|
+
* collide. The switch writes only the last two: its trigger is the `<label>` wrapping the input,
|
|
701
|
+
* and interest invokers are supported on `a`, `area` and `button` alone, so an `interestfor` there
|
|
702
|
+
* would show nothing on Chrome 152 while telling {@link bindTooltip} the platform had the pair — it
|
|
703
|
+
* is wired by `for` and bound on every engine instead. A document cannot mint the stem: the closed
|
|
704
|
+
* operator set has no counter and no identity, which is the sidecar case specs/ui.md §3.2
|
|
705
|
+
* sanctions. The stem carries the host's own tag, so an id read off the inspector says what it
|
|
706
|
+
* belongs to.
|
|
703
707
|
*
|
|
704
708
|
* The write lands after the first render, because `onMount` runs a microtask after it — so the tip
|
|
705
709
|
* and the three attributes appear on the second pass, and every binding that reads the stem guards
|
|
@@ -707,7 +711,7 @@ let mintedHints = 0;
|
|
|
707
711
|
* `title` until it is enabled.
|
|
708
712
|
*
|
|
709
713
|
* @param state The control's reactive scope, whose `uid` this writes.
|
|
710
|
-
* @param host The `jx-action-button` or `jx-
|
|
714
|
+
* @param host The `jx-action-button`, `jx-button` or `jx-switch` element.
|
|
711
715
|
*/
|
|
712
716
|
export function mintHintId(state: Record<string, unknown>, host: HTMLElement): void {
|
|
713
717
|
mintedHints += 1;
|