@jxsuite/ui 0.10.4 → 0.12.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-accordion-item.json +5 -1
- package/components/jx-action-button.json +1 -1
- package/components/jx-checkbox.json +35 -2
- package/components/jx-dialog.json +3 -1
- package/components/jx-field.json +18 -1
- package/components/jx-number-field.json +39 -7
- package/components/jx-option.json +8 -0
- package/components/jx-select.json +13 -4
- package/components/jx-slider.json +221 -0
- package/components/jx-switch.json +18 -0
- package/components/jx-textfield.json +50 -8
- package/components/jx-tree-item.json +5 -0
- package/icons/list.json +1 -0
- package/icons/manifest.json +3 -0
- package/package.json +3 -3
- package/project.json +11 -7
- package/src/behaviors/combobox.ts +9 -0
- package/src/behaviors/number-field.ts +3 -1
- package/src/behaviors/select.ts +15 -0
- package/src/behaviors/slider.ts +126 -0
- package/src/behaviors/textfield.ts +38 -0
- package/src/documents.ts +2 -0
- package/src/index.ts +2 -0
- package/src/loaders.ts +1 -0
- package/stylebook/jx-icon.json +1 -0
- package/stylebook/jx-slider.json +151 -0
|
@@ -167,7 +167,11 @@
|
|
|
167
167
|
"overflow": "hidden",
|
|
168
168
|
"textOverflow": "ellipsis",
|
|
169
169
|
"whiteSpace": "nowrap",
|
|
170
|
-
"
|
|
170
|
+
"color": "var(--jx-heading-color)",
|
|
171
|
+
"fontSize": "var(--jx-heading-size)",
|
|
172
|
+
"fontWeight": "var(--jx-heading-weight)",
|
|
173
|
+
"letterSpacing": "var(--jx-heading-tracking)",
|
|
174
|
+
"textTransform": "uppercase"
|
|
171
175
|
},
|
|
172
176
|
"& > [part=\"details\"] > [part=\"summary\"] > [part=\"actions\"]": {
|
|
173
177
|
"display": "inline-flex",
|
|
@@ -432,7 +432,7 @@
|
|
|
432
432
|
"gap": "2px",
|
|
433
433
|
"width": "100%",
|
|
434
434
|
"minWidth": "0",
|
|
435
|
-
"padding": "var(--jx-space-2)
|
|
435
|
+
"padding": "var(--jx-space-2) 0",
|
|
436
436
|
"borderRadius": "0",
|
|
437
437
|
"fontSize": "var(--jx-text-xs)",
|
|
438
438
|
"lineHeight": "var(--jx-leading-xs)"
|
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
"checked",
|
|
7
7
|
"indeterminate",
|
|
8
8
|
"disabled",
|
|
9
|
+
"required",
|
|
10
|
+
"invalid",
|
|
9
11
|
"label",
|
|
10
12
|
"labelledby",
|
|
11
13
|
"describedby",
|
|
12
14
|
"name",
|
|
15
|
+
"value",
|
|
13
16
|
"size"
|
|
14
17
|
],
|
|
15
18
|
"state": {
|
|
@@ -31,6 +34,18 @@
|
|
|
31
34
|
"attribute": "disabled",
|
|
32
35
|
"description": "Cannot be ticked or focused. Disabled, never absent."
|
|
33
36
|
},
|
|
37
|
+
"required": {
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"default": false,
|
|
40
|
+
"attribute": "required",
|
|
41
|
+
"description": "Forwarded to the control: a required box left unticked fails the form's own check."
|
|
42
|
+
},
|
|
43
|
+
"invalid": {
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"default": false,
|
|
46
|
+
"attribute": "invalid",
|
|
47
|
+
"description": "The answer is refused: the control says aria-invalid and the box is ringed in the danger colour."
|
|
48
|
+
},
|
|
34
49
|
"label": {
|
|
35
50
|
"type": "string",
|
|
36
51
|
"default": "",
|
|
@@ -55,6 +70,12 @@
|
|
|
55
70
|
"attribute": "name",
|
|
56
71
|
"description": "The form field name, forwarded to the control."
|
|
57
72
|
},
|
|
73
|
+
"value": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"default": "",
|
|
76
|
+
"attribute": "value",
|
|
77
|
+
"description": "What a ticked box submits under its name. Empty leaves the platform's own \"on\"."
|
|
78
|
+
},
|
|
58
79
|
"size": {
|
|
59
80
|
"type": "string",
|
|
60
81
|
"default": "md",
|
|
@@ -95,7 +116,8 @@
|
|
|
95
116
|
"data-size": "${state.size}",
|
|
96
117
|
"data-checked": "${state.checked ? '' : null}",
|
|
97
118
|
"data-indeterminate": "${state.indeterminate ? '' : null}",
|
|
98
|
-
"data-disabled": "${state.disabled ? '' : null}"
|
|
119
|
+
"data-disabled": "${state.disabled ? '' : null}",
|
|
120
|
+
"data-invalid": "${state.invalid ? '' : null}"
|
|
99
121
|
},
|
|
100
122
|
"style": {
|
|
101
123
|
"display": "inline-flex",
|
|
@@ -131,6 +153,12 @@
|
|
|
131
153
|
"borderRadius": "var(--jx-radius-xs)"
|
|
132
154
|
}
|
|
133
155
|
},
|
|
156
|
+
"&[data-invalid] [part=\"input\"]:not(:focus-visible), & [part=\"input\"]:user-invalid:not(:focus-visible)": {
|
|
157
|
+
"$description": "A refused box is ringed, by the host's word or by the control's own once the reader has been through it. A ring and not a fill, because the box's fill is the platform's accent and says ticked. Focus keeps its own ring, which is the one a keyboard reader is following.",
|
|
158
|
+
"outline": "1px solid var(--jx-danger)",
|
|
159
|
+
"outlineOffset": "1px",
|
|
160
|
+
"borderRadius": "var(--jx-radius-xs)"
|
|
161
|
+
},
|
|
134
162
|
"& [part=\"label\"]": {
|
|
135
163
|
"minWidth": "0",
|
|
136
164
|
"overflow": "hidden",
|
|
@@ -146,7 +174,7 @@
|
|
|
146
174
|
"color": "var(--jx-fg-dim)"
|
|
147
175
|
},
|
|
148
176
|
"&[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
|
|
177
|
+
"$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 §4.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. `contain: layout` keeps the reach LOCAL, as it does on jx-button's and jx-action-button's small controls: an absolutely positioned box still counts toward the SCROLLABLE overflow of the nearest ancestor whose `overflow` isn't `visible`, invisible or not, so a small checkbox touching a scrolling host's edge would add 2px to that host's scrollable region and draw a scrollbar over content that fit. The price is the baseline: a layout-contained label exports none, so a row aligned on `baseline` puts the label's bottom edge on its text baseline, and a row that holds a small checkbox centres it (`align-items: center`) instead.",
|
|
150
178
|
"position": "relative",
|
|
151
179
|
"minHeight": "calc(var(--jx-control-h) - 4px)",
|
|
152
180
|
"fontSize": "var(--jx-text-sm)",
|
|
@@ -187,11 +215,16 @@
|
|
|
187
215
|
"aria-label": "${state.label || null}",
|
|
188
216
|
"aria-labelledby": "${state.labelledby || null}",
|
|
189
217
|
"aria-describedby": "${state.describedby || null}",
|
|
218
|
+
"aria-invalid": "${state.invalid ? 'true' : null}",
|
|
190
219
|
"tabindex": "${state.tabindex || null}",
|
|
191
220
|
"name": "${state.name || null}",
|
|
221
|
+
"value": "${state.value || null}",
|
|
192
222
|
"disabled": {
|
|
193
223
|
"$ref": "#/state/disabled"
|
|
194
224
|
},
|
|
225
|
+
"required": {
|
|
226
|
+
"$ref": "#/state/required"
|
|
227
|
+
},
|
|
195
228
|
"checked": {
|
|
196
229
|
"$ref": "#/state/checked"
|
|
197
230
|
}
|
|
@@ -214,6 +214,7 @@
|
|
|
214
214
|
"fontSize": "var(--jx-text-md)",
|
|
215
215
|
"lineHeight": "var(--jx-leading-md)",
|
|
216
216
|
"overflow": "auto",
|
|
217
|
+
"overscrollBehavior": "contain",
|
|
217
218
|
"&::backdrop": {
|
|
218
219
|
"background": "rgba(0, 0, 0, 0.4)"
|
|
219
220
|
},
|
|
@@ -234,7 +235,8 @@
|
|
|
234
235
|
"$description": "The one region that scrolls. Child combinators, not the descendant ones the parts below use, because a consumer's content is light DOM inside this same box and a surface may name a [part=\"body\"] of its own.",
|
|
235
236
|
"flex": "1 1 auto",
|
|
236
237
|
"minHeight": "0",
|
|
237
|
-
"overflowY": "auto"
|
|
238
|
+
"overflowY": "auto",
|
|
239
|
+
"overscrollBehavior": "contain"
|
|
238
240
|
},
|
|
239
241
|
"&[data-size=\"md\"] > [part=\"dialog\"]": {
|
|
240
242
|
"width": "min(90vw, 32rem)"
|
package/components/jx-field.json
CHANGED
|
@@ -50,6 +50,12 @@
|
|
|
50
50
|
"$src": "jx-ui:/behaviors/textfield.ts",
|
|
51
51
|
"$export": "nameFieldControl",
|
|
52
52
|
"description": "Mints the row's id stem and names the slotted control with it. A document cannot do either: the closed operator set has no counter, and no body can write a property on an element other than the one whose handler is running. It runs once, over what is slotted then: a control APPENDED to the row afterwards lands outside the control part, unplaced and unnamed. Keep the control in the document and switch its state rather than adding it later."
|
|
53
|
+
},
|
|
54
|
+
"onLabelClick": {
|
|
55
|
+
"$prototype": "Function",
|
|
56
|
+
"$src": "jx-ui:/behaviors/textfield.ts",
|
|
57
|
+
"$export": "focusFieldControl",
|
|
58
|
+
"description": "A click on the label moves focus to the control the row holds, as a label's for would. It only focuses: it never ticks a box or flips a switch, because a row's label sits a column away from its control. The control is found at the click, so one a kit element has since replaced is still reached."
|
|
53
59
|
}
|
|
54
60
|
},
|
|
55
61
|
"attributes": {
|
|
@@ -85,6 +91,14 @@
|
|
|
85
91
|
"textOverflow": "ellipsis",
|
|
86
92
|
"whiteSpace": "nowrap"
|
|
87
93
|
},
|
|
94
|
+
"@container (max-width: 288px)": {
|
|
95
|
+
"$description": "A narrow row stacks on its own: when the nearest inline-size container is under 288px, the label goes above the control and reads from the left, exactly as `span` draws it. Under that width the 80px label column leaves the control too little room and cuts long labels off. A host opts its regions in by declaring `container-type: inline-size` (Studio's side panels do); with no container above it a row keeps its two columns, so a dialog's form is unchanged. The width is a px literal because a container condition cannot read a custom property, and not rem because Studio roots its type at 12px, which would make 18rem 216px.",
|
|
96
|
+
"gridTemplateColumns": "minmax(0, 1fr)",
|
|
97
|
+
"& > [part=\"label\"]": {
|
|
98
|
+
"width": "auto",
|
|
99
|
+
"textAlign": "left"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
88
102
|
"&[data-span] > [part=\"label\"]": {
|
|
89
103
|
"width": "auto",
|
|
90
104
|
"textAlign": "left"
|
|
@@ -128,7 +142,10 @@
|
|
|
128
142
|
"part": "label",
|
|
129
143
|
"id": "${state.uid ? state.uid + '-label' : null}"
|
|
130
144
|
},
|
|
131
|
-
"textContent": "${state.label}"
|
|
145
|
+
"textContent": "${state.label}",
|
|
146
|
+
"onclick": {
|
|
147
|
+
"$ref": "#/state/onLabelClick"
|
|
148
|
+
}
|
|
132
149
|
},
|
|
133
150
|
{
|
|
134
151
|
"tagName": "div",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"labelledby",
|
|
18
18
|
"describedby",
|
|
19
19
|
"disabled",
|
|
20
|
+
"readonly",
|
|
21
|
+
"required",
|
|
22
|
+
"invalid",
|
|
20
23
|
"stepper",
|
|
21
24
|
"step-up-label",
|
|
22
25
|
"step-down-label",
|
|
@@ -78,6 +81,24 @@
|
|
|
78
81
|
"attribute": "disabled",
|
|
79
82
|
"description": "Cannot be edited, stepped or focused."
|
|
80
83
|
},
|
|
84
|
+
"readonly": {
|
|
85
|
+
"type": "boolean",
|
|
86
|
+
"default": false,
|
|
87
|
+
"attribute": "readonly",
|
|
88
|
+
"description": "The value can be read, selected and submitted and not changed: forwarded to the control, and the stepper buttons and the shifted arrows stand down with it, because the platform's own stepUp() ignores readonly."
|
|
89
|
+
},
|
|
90
|
+
"required": {
|
|
91
|
+
"type": "boolean",
|
|
92
|
+
"default": false,
|
|
93
|
+
"attribute": "required",
|
|
94
|
+
"description": "Forwarded to the control, which is the element that validates: an empty required field fails the form's own check."
|
|
95
|
+
},
|
|
96
|
+
"invalid": {
|
|
97
|
+
"type": "boolean",
|
|
98
|
+
"default": false,
|
|
99
|
+
"attribute": "invalid",
|
|
100
|
+
"description": "The value is refused: the control says aria-invalid and draws in the danger colour."
|
|
101
|
+
},
|
|
81
102
|
"stepper": {
|
|
82
103
|
"type": "boolean",
|
|
83
104
|
"default": false,
|
|
@@ -205,7 +226,8 @@
|
|
|
205
226
|
"data-size": "${state.size}",
|
|
206
227
|
"data-disabled": "${state.disabled ? '' : null}",
|
|
207
228
|
"data-stepper": "${state.stepper ? '' : null}",
|
|
208
|
-
"data-bad-input": "${state.badInput ? '' : null}"
|
|
229
|
+
"data-bad-input": "${state.badInput ? '' : null}",
|
|
230
|
+
"data-invalid": "${state.invalid ? '' : null}"
|
|
209
231
|
},
|
|
210
232
|
"style": {
|
|
211
233
|
"display": "inline-flex",
|
|
@@ -263,6 +285,13 @@
|
|
|
263
285
|
"cursor": "default"
|
|
264
286
|
}
|
|
265
287
|
},
|
|
288
|
+
"&[data-invalid] [part=\"input\"]": {
|
|
289
|
+
"borderColor": "var(--jx-danger)"
|
|
290
|
+
},
|
|
291
|
+
"& [part=\"input\"]:user-invalid:is(:out-of-range, :required)": {
|
|
292
|
+
"$description": "The control's own verdict, once the reader has finished with it: a number outside min and max, or a required field left empty. Not a bare :user-invalid, which is also true of a step mismatch — and a field with no step attribute has a step of one, so every fraction a reader typed into it would draw as an error.",
|
|
293
|
+
"borderColor": "var(--jx-danger)"
|
|
294
|
+
},
|
|
266
295
|
"& [part=\"stepper-slot\"]": {
|
|
267
296
|
"display": "contents"
|
|
268
297
|
},
|
|
@@ -332,6 +361,7 @@
|
|
|
332
361
|
"aria-label": "${state.label || null}",
|
|
333
362
|
"aria-labelledby": "${state.labelledby || null}",
|
|
334
363
|
"aria-describedby": "${state.describedby || null}",
|
|
364
|
+
"aria-invalid": "${state.invalid ? 'true' : null}",
|
|
335
365
|
"name": "${state.name || null}",
|
|
336
366
|
"min": "${state.min || null}",
|
|
337
367
|
"max": "${state.max || null}",
|
|
@@ -339,6 +369,12 @@
|
|
|
339
369
|
"disabled": {
|
|
340
370
|
"$ref": "#/state/disabled"
|
|
341
371
|
},
|
|
372
|
+
"readonly": {
|
|
373
|
+
"$ref": "#/state/readonly"
|
|
374
|
+
},
|
|
375
|
+
"required": {
|
|
376
|
+
"$ref": "#/state/required"
|
|
377
|
+
},
|
|
342
378
|
"value": "${state.value}"
|
|
343
379
|
},
|
|
344
380
|
"value": {
|
|
@@ -378,9 +414,7 @@
|
|
|
378
414
|
"type": "button",
|
|
379
415
|
"tabindex": "-1",
|
|
380
416
|
"aria-label": "${state.stepUpLabel}",
|
|
381
|
-
"disabled": {
|
|
382
|
-
"$ref": "#/state/disabled"
|
|
383
|
-
}
|
|
417
|
+
"disabled": "${state.disabled || state.readonly}"
|
|
384
418
|
},
|
|
385
419
|
"onclick": {
|
|
386
420
|
"$ref": "#/state/onStepUp"
|
|
@@ -408,9 +442,7 @@
|
|
|
408
442
|
"type": "button",
|
|
409
443
|
"tabindex": "-1",
|
|
410
444
|
"aria-label": "${state.stepDownLabel}",
|
|
411
|
-
"disabled": {
|
|
412
|
-
"$ref": "#/state/disabled"
|
|
413
|
-
}
|
|
445
|
+
"disabled": "${state.disabled || state.readonly}"
|
|
414
446
|
},
|
|
415
447
|
"onclick": {
|
|
416
448
|
"$ref": "#/state/onStepDown"
|
|
@@ -264,6 +264,14 @@
|
|
|
264
264
|
"&[aria-selected=\"true\"] [part=\"description\"], &[aria-selected=\"true\"] [part=\"end\"], &[aria-selected=\"true\"] [part=\"icon\"]": {
|
|
265
265
|
"$description": "A highlighted row draws on the accent, and a muted grey on an accent fails SC 1.4.3. Every channel takes the row's own ink instead of keeping its own.",
|
|
266
266
|
"color": "inherit"
|
|
267
|
+
},
|
|
268
|
+
"@(forced-colors: active)": {
|
|
269
|
+
"&[aria-selected=\"true\"]": {
|
|
270
|
+
"$description": "The highlighted row, redrawn. Forced colours replace the accent fill with the list's own Canvas, so the caret would move through a menu, a listbox or a combobox with nothing on screen following it. The system's selection pair says it instead, as jx-action-group does for its chosen segment; the description, end and icon already inherit the row's ink.",
|
|
271
|
+
"forcedColorAdjust": "none",
|
|
272
|
+
"background": "Highlight",
|
|
273
|
+
"color": "HighlightText"
|
|
274
|
+
}
|
|
267
275
|
}
|
|
268
276
|
},
|
|
269
277
|
"children": [
|
|
@@ -102,6 +102,11 @@
|
|
|
102
102
|
"default": "",
|
|
103
103
|
"description": "This element's own id stem, minted on mount. The error and help sentences take their ids from it, and the control names those ids in aria-describedby."
|
|
104
104
|
},
|
|
105
|
+
"plain": {
|
|
106
|
+
"type": "boolean",
|
|
107
|
+
"default": false,
|
|
108
|
+
"description": "Written at mount, never authored: the engine has no appearance: base-select, so the control is the platform's plain select. A plain select draws an option as its text and nothing else, which would run a row's label and its description together into one word, so the description is left out of the row while this is true. Faces, swatches and group headings are drawing only and simply do not appear."
|
|
109
|
+
},
|
|
105
110
|
"hasError": {
|
|
106
111
|
"$expression": {
|
|
107
112
|
"operator": "!==",
|
|
@@ -266,6 +271,10 @@
|
|
|
266
271
|
"&[data-invalid] select": {
|
|
267
272
|
"borderColor": "var(--jx-danger)"
|
|
268
273
|
},
|
|
274
|
+
"& select:user-invalid": {
|
|
275
|
+
"$description": "The control's own verdict after the reader has been through it, drawn as the host's is: a required select left on its empty row.",
|
|
276
|
+
"borderColor": "var(--jx-danger)"
|
|
277
|
+
},
|
|
269
278
|
"& [part=\"trigger\"]": {
|
|
270
279
|
"display": "flex",
|
|
271
280
|
"alignItems": "center",
|
|
@@ -500,9 +509,9 @@
|
|
|
500
509
|
"tagName": "span",
|
|
501
510
|
"attributes": {
|
|
502
511
|
"part": "description",
|
|
503
|
-
"hidden": "${!$map.item.description}"
|
|
512
|
+
"hidden": "${state.plain || !$map.item.description}"
|
|
504
513
|
},
|
|
505
|
-
"textContent": "${$map.item.description || ''}"
|
|
514
|
+
"textContent": "${state.plain ? '' : $map.item.description || ''}"
|
|
506
515
|
}
|
|
507
516
|
]
|
|
508
517
|
}
|
|
@@ -600,9 +609,9 @@
|
|
|
600
609
|
"tagName": "span",
|
|
601
610
|
"attributes": {
|
|
602
611
|
"part": "description",
|
|
603
|
-
"hidden": "${!$map.item.description}"
|
|
612
|
+
"hidden": "${state.plain || !$map.item.description}"
|
|
604
613
|
},
|
|
605
|
-
"textContent": "${$map.item.description || ''}"
|
|
614
|
+
"textContent": "${state.plain ? '' : $map.item.description || ''}"
|
|
606
615
|
}
|
|
607
616
|
]
|
|
608
617
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://jxsuite.com/schema/v1",
|
|
3
|
+
"$id": "JxSlider",
|
|
4
|
+
"tagName": "jx-slider",
|
|
5
|
+
"observedAttributes": [
|
|
6
|
+
"label",
|
|
7
|
+
"labelledby",
|
|
8
|
+
"describedby",
|
|
9
|
+
"name",
|
|
10
|
+
"value",
|
|
11
|
+
"min",
|
|
12
|
+
"max",
|
|
13
|
+
"step",
|
|
14
|
+
"valuetext",
|
|
15
|
+
"disabled"
|
|
16
|
+
],
|
|
17
|
+
"state": {
|
|
18
|
+
"label": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"default": "",
|
|
21
|
+
"attribute": "label",
|
|
22
|
+
"description": "The accessible name, forwarded to the inner range as aria-label. Required unless labelledby names a visible one: a slider is in the accessibility lint's LABELLED_ROLES, so it may not take its name from anything it contains, and an unnamed track is announced as \"slider\" and a number."
|
|
23
|
+
},
|
|
24
|
+
"labelledby": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"default": "",
|
|
27
|
+
"attribute": "labelledby",
|
|
28
|
+
"description": "Forwarded to the inner range as aria-labelledby, for a visible label the element does not own."
|
|
29
|
+
},
|
|
30
|
+
"describedby": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"default": "",
|
|
33
|
+
"attribute": "describedby",
|
|
34
|
+
"description": "The id of the sentence that describes the control, forwarded as its aria-describedby. A jx-field row writes it for a slider slotted into one."
|
|
35
|
+
},
|
|
36
|
+
"name": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"default": "",
|
|
39
|
+
"attribute": "name",
|
|
40
|
+
"description": "The name the value is submitted under, forwarded to the control."
|
|
41
|
+
},
|
|
42
|
+
"value": {
|
|
43
|
+
"type": "number",
|
|
44
|
+
"default": 0,
|
|
45
|
+
"attribute": "value",
|
|
46
|
+
"description": "Where the thumb is, in the track's own units. It is bound to the inner range BOTH ways: as the value property, which is the live value once the reader's first drag has set the control's dirty flag, and as the value attribute, which is the control's default."
|
|
47
|
+
},
|
|
48
|
+
"min": {
|
|
49
|
+
"type": "number",
|
|
50
|
+
"default": 0,
|
|
51
|
+
"attribute": "min",
|
|
52
|
+
"description": "The bottom of the range."
|
|
53
|
+
},
|
|
54
|
+
"max": {
|
|
55
|
+
"type": "number",
|
|
56
|
+
"default": 100,
|
|
57
|
+
"attribute": "max",
|
|
58
|
+
"description": "The top of the range."
|
|
59
|
+
},
|
|
60
|
+
"step": {
|
|
61
|
+
"type": "number",
|
|
62
|
+
"default": 1,
|
|
63
|
+
"attribute": "step",
|
|
64
|
+
"description": "The grid the value moves on, for the platform's own arrow keys and for the kit's ten-step Shift+Arrow."
|
|
65
|
+
},
|
|
66
|
+
"valuetext": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"default": "",
|
|
69
|
+
"attribute": "valuetext",
|
|
70
|
+
"description": "What the value is announced as, forwarded to aria-valuetext: \"125%\" rather than the bare track position. Empty leaves the number to be read, which is right for a track whose units are its position."
|
|
71
|
+
},
|
|
72
|
+
"disabled": {
|
|
73
|
+
"type": "boolean",
|
|
74
|
+
"default": false,
|
|
75
|
+
"attribute": "disabled",
|
|
76
|
+
"description": "Cannot be moved or focused. Forwarded to the inner range, so the refusal is the platform's rather than a style that only looks like one."
|
|
77
|
+
},
|
|
78
|
+
"fill": "${state.max > state.min ? Math.min(100, Math.max(0, (state.value - state.min) / (state.max - state.min) * 100)) : 0}",
|
|
79
|
+
"onInput": {
|
|
80
|
+
"$prototype": "Function",
|
|
81
|
+
"$src": "jx-ui:/behaviors/slider.ts",
|
|
82
|
+
"$export": "onSliderInput",
|
|
83
|
+
"emits": [
|
|
84
|
+
{
|
|
85
|
+
"name": "input",
|
|
86
|
+
"description": "The value is moving under the reader's hand. It comes from the ELEMENT, so e.target.value is the number in the track's own units."
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"description": "Mirrors the inner range into value and re-says the platform's own input from the host."
|
|
90
|
+
},
|
|
91
|
+
"onChange": {
|
|
92
|
+
"$prototype": "Function",
|
|
93
|
+
"$src": "jx-ui:/behaviors/slider.ts",
|
|
94
|
+
"$export": "onSliderChange",
|
|
95
|
+
"emits": [
|
|
96
|
+
{
|
|
97
|
+
"name": "change",
|
|
98
|
+
"description": "The reader committed the value; e.target is the element."
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"description": "The same mirror and the same re-say, for the commit."
|
|
102
|
+
},
|
|
103
|
+
"onKeydown": {
|
|
104
|
+
"$prototype": "Function",
|
|
105
|
+
"$src": "jx-ui:/behaviors/slider.ts",
|
|
106
|
+
"$export": "onSliderKeydown",
|
|
107
|
+
"description": "Shift+Arrow moves ten steps, which no range has of its own. A plain arrow, Home, End and the Page keys are the platform's and are not touched."
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"oninput": {
|
|
111
|
+
"$ref": "#/state/onInput"
|
|
112
|
+
},
|
|
113
|
+
"onchange": {
|
|
114
|
+
"$ref": "#/state/onChange"
|
|
115
|
+
},
|
|
116
|
+
"onkeydown": {
|
|
117
|
+
"$ref": "#/state/onKeydown"
|
|
118
|
+
},
|
|
119
|
+
"attributes": {
|
|
120
|
+
"data-disabled": "${state.disabled ? '' : null}"
|
|
121
|
+
},
|
|
122
|
+
"style": {
|
|
123
|
+
"display": "block",
|
|
124
|
+
"&[hidden]": {
|
|
125
|
+
"display": "none"
|
|
126
|
+
},
|
|
127
|
+
"boxSizing": "border-box",
|
|
128
|
+
"inlineSize": "100%",
|
|
129
|
+
"minInlineSize": "64px",
|
|
130
|
+
"--jx-slider-track-h": "4px",
|
|
131
|
+
"--jx-slider-thumb": "12px",
|
|
132
|
+
"--jx-slider-fill": "${state.fill}%",
|
|
133
|
+
"&[data-disabled]": {
|
|
134
|
+
"opacity": "0.5"
|
|
135
|
+
},
|
|
136
|
+
"& [part=\"input\"]": {
|
|
137
|
+
"appearance": "none",
|
|
138
|
+
"display": "block",
|
|
139
|
+
"boxSizing": "border-box",
|
|
140
|
+
"inlineSize": "100%",
|
|
141
|
+
"blockSize": "var(--jx-control-h)",
|
|
142
|
+
"margin": "0",
|
|
143
|
+
"padding": "0",
|
|
144
|
+
"border": "0",
|
|
145
|
+
"background": "transparent",
|
|
146
|
+
"font": "inherit",
|
|
147
|
+
"cursor": "pointer",
|
|
148
|
+
"outline": "none",
|
|
149
|
+
":focus-visible": {
|
|
150
|
+
"outline": "var(--jx-focus-ring)",
|
|
151
|
+
"outlineOffset": "1px",
|
|
152
|
+
"borderRadius": "var(--jx-radius-sm)"
|
|
153
|
+
},
|
|
154
|
+
":disabled": {
|
|
155
|
+
"cursor": "default"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"& [part=\"input\"]::-webkit-slider-runnable-track": {
|
|
159
|
+
"blockSize": "var(--jx-slider-track-h)",
|
|
160
|
+
"borderRadius": "var(--jx-radius-sm)",
|
|
161
|
+
"background": "linear-gradient(to right, var(--jx-accent-solid) var(--jx-slider-fill), var(--jx-border-strong) var(--jx-slider-fill))"
|
|
162
|
+
},
|
|
163
|
+
"& [part=\"input\"]::-moz-range-track": {
|
|
164
|
+
"blockSize": "var(--jx-slider-track-h)",
|
|
165
|
+
"borderRadius": "var(--jx-radius-sm)",
|
|
166
|
+
"background": "var(--jx-border-strong)"
|
|
167
|
+
},
|
|
168
|
+
"& [part=\"input\"]::-moz-range-progress": {
|
|
169
|
+
"blockSize": "var(--jx-slider-track-h)",
|
|
170
|
+
"borderRadius": "var(--jx-radius-sm)",
|
|
171
|
+
"background": "var(--jx-accent-solid)"
|
|
172
|
+
},
|
|
173
|
+
"& [part=\"input\"]::-webkit-slider-thumb": {
|
|
174
|
+
"appearance": "none",
|
|
175
|
+
"boxSizing": "border-box",
|
|
176
|
+
"inlineSize": "var(--jx-slider-thumb)",
|
|
177
|
+
"blockSize": "var(--jx-slider-thumb)",
|
|
178
|
+
"marginBlockStart": "calc((var(--jx-slider-track-h) - var(--jx-slider-thumb)) / 2)",
|
|
179
|
+
"borderRadius": "50%",
|
|
180
|
+
"border": "2px solid var(--jx-bg)",
|
|
181
|
+
"boxShadow": "0 0 0 1px var(--jx-fg)",
|
|
182
|
+
"background": "var(--jx-fg)",
|
|
183
|
+
"cursor": "inherit"
|
|
184
|
+
},
|
|
185
|
+
"& [part=\"input\"]::-moz-range-thumb": {
|
|
186
|
+
"appearance": "none",
|
|
187
|
+
"boxSizing": "border-box",
|
|
188
|
+
"inlineSize": "var(--jx-slider-thumb)",
|
|
189
|
+
"blockSize": "var(--jx-slider-thumb)",
|
|
190
|
+
"borderRadius": "50%",
|
|
191
|
+
"border": "2px solid var(--jx-bg)",
|
|
192
|
+
"boxShadow": "0 0 0 1px var(--jx-fg)",
|
|
193
|
+
"background": "var(--jx-fg)",
|
|
194
|
+
"cursor": "inherit"
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"children": [
|
|
198
|
+
{
|
|
199
|
+
"tagName": "input",
|
|
200
|
+
"attributes": {
|
|
201
|
+
"part": "input",
|
|
202
|
+
"type": "range",
|
|
203
|
+
"aria-label": "${state.label || null}",
|
|
204
|
+
"aria-labelledby": "${state.labelledby || null}",
|
|
205
|
+
"aria-describedby": "${state.describedby || null}",
|
|
206
|
+
"name": "${state.name || null}",
|
|
207
|
+
"aria-valuetext": "${state.valuetext || null}",
|
|
208
|
+
"min": "${state.min}",
|
|
209
|
+
"max": "${state.max}",
|
|
210
|
+
"step": "${state.step}",
|
|
211
|
+
"value": "${state.value}",
|
|
212
|
+
"disabled": {
|
|
213
|
+
"$ref": "#/state/disabled"
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"value": {
|
|
217
|
+
"$ref": "#/state/value"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
"observedAttributes": [
|
|
6
6
|
"checked",
|
|
7
7
|
"disabled",
|
|
8
|
+
"required",
|
|
8
9
|
"hint",
|
|
9
10
|
"label",
|
|
10
11
|
"labelledby",
|
|
11
12
|
"describedby",
|
|
12
13
|
"name",
|
|
14
|
+
"value",
|
|
13
15
|
"size"
|
|
14
16
|
],
|
|
15
17
|
"$elements": [
|
|
@@ -30,6 +32,12 @@
|
|
|
30
32
|
"attribute": "disabled",
|
|
31
33
|
"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."
|
|
32
34
|
},
|
|
35
|
+
"required": {
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": false,
|
|
38
|
+
"attribute": "required",
|
|
39
|
+
"description": "Forwarded to the control: a required switch left off fails the form's own check."
|
|
40
|
+
},
|
|
33
41
|
"hint": {
|
|
34
42
|
"type": "string",
|
|
35
43
|
"default": "",
|
|
@@ -60,6 +68,12 @@
|
|
|
60
68
|
"attribute": "name",
|
|
61
69
|
"description": "The form field name, forwarded to the control."
|
|
62
70
|
},
|
|
71
|
+
"value": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"default": "",
|
|
74
|
+
"attribute": "value",
|
|
75
|
+
"description": "What a switch that is on submits under its name. Empty leaves the platform's own \"on\"."
|
|
76
|
+
},
|
|
63
77
|
"size": {
|
|
64
78
|
"type": "string",
|
|
65
79
|
"default": "md",
|
|
@@ -272,9 +286,13 @@
|
|
|
272
286
|
"aria-describedby": "${[state.hasTip ? state.uid + '-tip' : null, state.describedby || null].filter(Boolean).join(' ') || null}",
|
|
273
287
|
"tabindex": "${state.tabindex || null}",
|
|
274
288
|
"name": "${state.name || null}",
|
|
289
|
+
"value": "${state.value || null}",
|
|
275
290
|
"disabled": {
|
|
276
291
|
"$ref": "#/state/disabled"
|
|
277
292
|
},
|
|
293
|
+
"required": {
|
|
294
|
+
"$ref": "#/state/required"
|
|
295
|
+
},
|
|
278
296
|
"checked": {
|
|
279
297
|
"$ref": "#/state/checked"
|
|
280
298
|
}
|
|
@@ -278,7 +278,26 @@
|
|
|
278
278
|
"Escape": [
|
|
279
279
|
{
|
|
280
280
|
"if": {
|
|
281
|
-
"
|
|
281
|
+
"operator": "&&",
|
|
282
|
+
"target": {
|
|
283
|
+
"operator": "&&",
|
|
284
|
+
"target": {
|
|
285
|
+
"operator": "!",
|
|
286
|
+
"target": {
|
|
287
|
+
"$ref": "event#/isComposing"
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"value": {
|
|
291
|
+
"operator": "!==",
|
|
292
|
+
"target": {
|
|
293
|
+
"$ref": "event#/keyCode"
|
|
294
|
+
},
|
|
295
|
+
"value": 229
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"value": {
|
|
299
|
+
"$ref": "#/state/showClear"
|
|
300
|
+
}
|
|
282
301
|
},
|
|
283
302
|
"then": [
|
|
284
303
|
{
|
|
@@ -307,16 +326,35 @@
|
|
|
307
326
|
"if": {
|
|
308
327
|
"operator": "&&",
|
|
309
328
|
"target": {
|
|
310
|
-
"operator": "
|
|
329
|
+
"operator": "&&",
|
|
311
330
|
"target": {
|
|
312
|
-
"
|
|
331
|
+
"operator": "!",
|
|
332
|
+
"target": {
|
|
333
|
+
"$ref": "event#/isComposing"
|
|
334
|
+
}
|
|
313
335
|
},
|
|
314
|
-
"value":
|
|
336
|
+
"value": {
|
|
337
|
+
"operator": "!==",
|
|
338
|
+
"target": {
|
|
339
|
+
"$ref": "event#/keyCode"
|
|
340
|
+
},
|
|
341
|
+
"value": 229
|
|
342
|
+
}
|
|
315
343
|
},
|
|
316
344
|
"value": {
|
|
317
|
-
"operator": "
|
|
345
|
+
"operator": "&&",
|
|
318
346
|
"target": {
|
|
319
|
-
"
|
|
347
|
+
"operator": "===",
|
|
348
|
+
"target": {
|
|
349
|
+
"$ref": "#/state/type"
|
|
350
|
+
},
|
|
351
|
+
"value": "search"
|
|
352
|
+
},
|
|
353
|
+
"value": {
|
|
354
|
+
"operator": "!",
|
|
355
|
+
"target": {
|
|
356
|
+
"$ref": "#/state/multiline"
|
|
357
|
+
}
|
|
320
358
|
}
|
|
321
359
|
}
|
|
322
360
|
},
|
|
@@ -340,7 +378,7 @@
|
|
|
340
378
|
"description": "Escape finished emptying a clearable field; e.target is the field."
|
|
341
379
|
}
|
|
342
380
|
],
|
|
343
|
-
"description": "Two keys, and nothing else. Escape empties a clearable field that has something in it, and cancels so the key does not also close whatever the field sits in. Enter is cancelled on a single-line search field: a search input inside a form submits it, and a search field is the one kind that never means to — the element owns no form, so the guard lives here rather than at every call site. A textarea never submits on Enter, so a multiline field keeps the key and the reader keeps the newline."
|
|
381
|
+
"description": "Two keys, and nothing else. Escape empties a clearable field that has something in it, and cancels so the key does not also close whatever the field sits in. Enter is cancelled on a single-line search field: a search input inside a form submits it, and a search field is the one kind that never means to — the element owns no form, so the guard lives here rather than at every call site. A textarea never submits on Enter, so a multiline field keeps the key and the reader keeps the newline. A key pressed while an input method is composing belongs to the input method: the Enter that accepts a candidate and the Escape that dismisses the candidate list are neither a submit nor a clear, so both cases stand down while event.isComposing is true or the key code is 229, which is how the engines that have already ended the composition still mark its last key."
|
|
344
382
|
}
|
|
345
383
|
},
|
|
346
384
|
"onkeydown": {
|
|
@@ -435,6 +473,10 @@
|
|
|
435
473
|
"&[data-invalid] [part=\"input\"]": {
|
|
436
474
|
"borderColor": "var(--jx-danger)"
|
|
437
475
|
},
|
|
476
|
+
"& [part=\"input\"]:user-invalid": {
|
|
477
|
+
"$description": "The control's OWN verdict, drawn the way the host's is: a required field left empty or an address that is not one draws the danger border once the reader has finished with it. :user-invalid and never :invalid, which is true of an untouched required field before anyone has typed.",
|
|
478
|
+
"borderColor": "var(--jx-danger)"
|
|
479
|
+
},
|
|
438
480
|
"&[data-clearable] [part=\"input\"]": {
|
|
439
481
|
"::-webkit-search-cancel-button": {
|
|
440
482
|
"appearance": "none"
|
|
@@ -447,7 +489,7 @@
|
|
|
447
489
|
"fontSize": "var(--jx-text-lg)"
|
|
448
490
|
},
|
|
449
491
|
"& [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
|
|
492
|
+
"$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 §4.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. `contain: layout` keeps the expansion LOCAL: an absolutely positioned box still counts toward the SCROLLABLE overflow of the nearest ancestor whose `overflow` is not `visible`, invisible or not, so a host that scrolls picks up a couple of phantom pixels from every `::before` like this one between it and the field, and draws a scrollbar over content that already fit.",
|
|
451
493
|
"position": "absolute",
|
|
452
494
|
"insetBlockStart": "1px",
|
|
453
495
|
"insetInlineEnd": "1px",
|
|
@@ -181,9 +181,14 @@
|
|
|
181
181
|
"justifyContent": "center",
|
|
182
182
|
"inlineSize": "14px",
|
|
183
183
|
"flex": "none",
|
|
184
|
+
"alignSelf": "stretch",
|
|
184
185
|
"color": "var(--jx-fg-dim)",
|
|
185
186
|
"--jx-icon-size": "12px"
|
|
186
187
|
},
|
|
188
|
+
"&[aria-expanded] [part=\"twisty\"]": {
|
|
189
|
+
"$description": "A row that can open answers a press on its twisty, so it says so: the cursor is `pointer` rather than whatever the row inherits (a host that makes the whole row a drag handle draws `grab`, and a chevron that looks grabbable is an invitation to start a drag instead of a toggle). The box is the row's whole height (`align-self: stretch` above), because a click target as tall as a 12px glyph loses the press to a pixel of vertical drift. A leaf has no `aria-expanded` and keeps its row's cursor: its dead cell is not a control.",
|
|
190
|
+
"cursor": "pointer"
|
|
191
|
+
},
|
|
187
192
|
"& [part=\"label\"]": {
|
|
188
193
|
"flex": "1",
|
|
189
194
|
"minWidth": "0",
|
package/icons/list.json
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"arrows-clockwise": ["regular"],
|
|
21
21
|
"arrows-horizontal": ["regular"],
|
|
22
22
|
"arrows-in-line-horizontal": ["regular"],
|
|
23
|
+
"arrows-in-line-vertical": ["regular"],
|
|
23
24
|
"arrows-out": ["regular"],
|
|
24
25
|
"arrows-out-line-horizontal": ["regular"],
|
|
25
26
|
"arrows-out-line-vertical": ["regular"],
|
package/icons/manifest.json
CHANGED
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
"arrows-in-line-horizontal": {
|
|
68
68
|
"regular": "M136,40V216a8,8,0,0,1-16,0V40a8,8,0,0,1,16,0ZM69.66,90.34a8,8,0,0,0-11.32,11.32L76.69,120H16a8,8,0,0,0,0,16H76.69L58.34,154.34a8,8,0,0,0,11.32,11.32l32-32a8,8,0,0,0,0-11.32ZM240,120H179.31l18.35-18.34a8,8,0,0,0-11.32-11.32l-32,32a8,8,0,0,0,0,11.32l32,32a8,8,0,0,0,11.32-11.32L179.31,136H240a8,8,0,0,0,0-16Z"
|
|
69
69
|
},
|
|
70
|
+
"arrows-in-line-vertical": {
|
|
71
|
+
"regular": "M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM122.34,101.66a8,8,0,0,0,11.32,0l32-32a8,8,0,0,0-11.32-11.32L136,76.69V16a8,8,0,0,0-16,0V76.69L101.66,58.34A8,8,0,0,0,90.34,69.66Zm11.32,52.68a8,8,0,0,0-11.32,0l-32,32a8,8,0,0,0,11.32,11.32L120,179.31V240a8,8,0,0,0,16,0V179.31l18.34,18.35a8,8,0,0,0,11.32-11.32Z"
|
|
72
|
+
},
|
|
70
73
|
"arrows-out": {
|
|
71
74
|
"regular": "M216,48V96a8,8,0,0,1-16,0V67.31l-42.34,42.35a8,8,0,0,1-11.32-11.32L188.69,56H160a8,8,0,0,1,0-16h48A8,8,0,0,1,216,48ZM98.34,146.34,56,188.69V160a8,8,0,0,0-16,0v48a8,8,0,0,0,8,8H96a8,8,0,0,0,0-16H67.31l42.35-42.34a8,8,0,0,0-11.32-11.32ZM208,152a8,8,0,0,0-8,8v28.69l-42.34-42.35a8,8,0,0,0-11.32,11.32L188.69,200H160a8,8,0,0,0,0,16h48a8,8,0,0,0,8-8V160A8,8,0,0,0,208,152ZM67.31,56H96a8,8,0,0,0,0-16H48a8,8,0,0,0-8,8V96a8,8,0,0,0,16,0V67.31l42.34,42.35a8,8,0,0,0,11.32-11.32Z"
|
|
72
75
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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,8 +58,8 @@
|
|
|
58
58
|
"upgrade": "bun update --latest && bun install"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@jxsuite/runtime": "^
|
|
62
|
-
"@jxsuite/schema": "^
|
|
61
|
+
"@jxsuite/runtime": "^7.0.0",
|
|
62
|
+
"@jxsuite/schema": "^6.0.0"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@happy-dom/global-registrator": "^21.0.4",
|
package/project.json
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
"--jx-gray-200": "#e1e1e7",
|
|
16
16
|
"--jx-gray-300": "#c9c9d2",
|
|
17
17
|
"--jx-gray-400": "#a2a2af",
|
|
18
|
+
"--jx-gray-450": "#8f8f9d",
|
|
18
19
|
"--jx-gray-500": "#7c7c8b",
|
|
20
|
+
"--jx-gray-550": "#6d6d7c",
|
|
19
21
|
"--jx-gray-600": "#5e5e6c",
|
|
20
22
|
"--jx-gray-700": "#45454f",
|
|
21
23
|
"--jx-gray-800": "#2f2f36",
|
|
@@ -76,7 +78,7 @@
|
|
|
76
78
|
"--jx-border-strong": "light-dark(var(--jx-gray-300), var(--jx-gray-700))",
|
|
77
79
|
"--jx-fg": "light-dark(var(--jx-gray-900), var(--jx-gray-100))",
|
|
78
80
|
"--jx-fg-dim": "light-dark(var(--jx-gray-600), var(--jx-gray-400))",
|
|
79
|
-
"--jx-fg-muted": "var(--jx-gray-
|
|
81
|
+
"--jx-fg-muted": "light-dark(var(--jx-gray-550), var(--jx-gray-450))",
|
|
80
82
|
"--jx-accent": "light-dark(var(--jx-blue-600), var(--jx-blue-500))",
|
|
81
83
|
"--jx-accent-solid": "var(--jx-blue-600)",
|
|
82
84
|
"--jx-accent-hover": "light-dark(var(--jx-blue-700), var(--jx-blue-400))",
|
|
@@ -99,7 +101,7 @@
|
|
|
99
101
|
"--jx-switch-thumb": "light-dark(#ffffff, var(--jx-gray-100))",
|
|
100
102
|
"--jx-font-sans": "\"Inter Variable\", \"Inter\", system-ui, -apple-system, \"Segoe UI\", sans-serif",
|
|
101
103
|
"--jx-font-mono": "\"JetBrains Mono\", \"SF Mono\", \"Fira Code\", monospace",
|
|
102
|
-
"--jx-text-xs": "
|
|
104
|
+
"--jx-text-xs": "11px",
|
|
103
105
|
"--jx-text-sm": "11px",
|
|
104
106
|
"--jx-text-md": "12px",
|
|
105
107
|
"--jx-text-lg": "14px",
|
|
@@ -107,6 +109,10 @@
|
|
|
107
109
|
"--jx-leading-sm": "16px",
|
|
108
110
|
"--jx-leading-md": "18px",
|
|
109
111
|
"--jx-leading-lg": "20px",
|
|
112
|
+
"--jx-heading-size": "var(--jx-text-sm)",
|
|
113
|
+
"--jx-heading-weight": "600",
|
|
114
|
+
"--jx-heading-tracking": "0.06em",
|
|
115
|
+
"--jx-heading-color": "var(--jx-fg-dim)",
|
|
110
116
|
"--jx-space-1": "2px",
|
|
111
117
|
"--jx-space-2": "4px",
|
|
112
118
|
"--jx-space-3": "8px",
|
|
@@ -123,17 +129,15 @@
|
|
|
123
129
|
"--jx-radius-md": "6px",
|
|
124
130
|
"--jx-radius-lg": "10px",
|
|
125
131
|
"--jx-focus-ring": "2px solid var(--jx-accent)",
|
|
126
|
-
"--jx-shadow-popover": "0 0 0 1px var(--jx-border-strong), 0 8px 24px -8px rgb(0 0 0 / 0.6), 0 2px 6px rgb(0 0 0 / 0.3)",
|
|
127
|
-
"--jx-shadow-dialog": "0 0 0 1px var(--jx-border-strong), 0 24px 64px -16px rgb(0 0 0 / 0.7)",
|
|
132
|
+
"--jx-shadow-popover": "0 0 0 1px var(--jx-border-strong), 0 8px 24px -8px light-dark(rgb(0 0 0 / 0.18), rgb(0 0 0 / 0.6)), 0 2px 6px light-dark(rgb(0 0 0 / 0.1), rgb(0 0 0 / 0.3))",
|
|
133
|
+
"--jx-shadow-dialog": "0 0 0 1px var(--jx-border-strong), 0 24px 64px -16px light-dark(rgb(0 0 0 / 0.25), rgb(0 0 0 / 0.7))",
|
|
128
134
|
"--jx-dur-1": "150ms",
|
|
129
135
|
"--jx-dur-2": "200ms",
|
|
130
136
|
"--jx-ease-out": "cubic-bezier(0.2, 0, 0, 1)",
|
|
131
137
|
"--jx-ease-in": "cubic-bezier(0.4, 0, 1, 1)",
|
|
132
138
|
"colorScheme": "light dark",
|
|
133
139
|
"&[data-theme=\"light\"]": {
|
|
134
|
-
"colorScheme": "light"
|
|
135
|
-
"--jx-shadow-popover": "0 0 0 1px var(--jx-border-strong), 0 8px 24px -8px rgb(0 0 0 / 0.18), 0 2px 6px rgb(0 0 0 / 0.1)",
|
|
136
|
-
"--jx-shadow-dialog": "0 0 0 1px var(--jx-border-strong), 0 24px 64px -16px rgb(0 0 0 / 0.25)"
|
|
140
|
+
"colorScheme": "light"
|
|
137
141
|
},
|
|
138
142
|
"&[data-theme=\"dark\"]": {
|
|
139
143
|
"colorScheme": "dark"
|
|
@@ -390,6 +390,15 @@ export function onComboboxKeydown(state: ComboboxState, event: KeyboardEvent): v
|
|
|
390
390
|
if (!host || event.ctrlKey || event.metaKey) {
|
|
391
391
|
return;
|
|
392
392
|
}
|
|
393
|
+
/*
|
|
394
|
+
* An input method is composing: the arrows walk ITS candidate list and Enter accepts ITS
|
|
395
|
+
* candidate, so none of them is the reader's answer to this list yet. `keyCode` 229 is the same
|
|
396
|
+
* statement from the engines that fire the keydown before `compositionstart` sets the flag.
|
|
397
|
+
*/
|
|
398
|
+
// oxlint-disable-next-line typescript/no-deprecated -- the only signal Safari gives for the Enter that ends a composition.
|
|
399
|
+
if (event.isComposing || event.keyCode === 229) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
393
402
|
const rows = rowsOf(state);
|
|
394
403
|
const at = Number(state.activeIndex ?? -1);
|
|
395
404
|
switch (event.key) {
|
|
@@ -87,7 +87,9 @@ function announce(host: HTMLElement): void {
|
|
|
87
87
|
export function stepBy(state: NumberFieldState, event: Event, n: number): void {
|
|
88
88
|
const host = hostOf(event);
|
|
89
89
|
const control = host ? controlOf(host) : null;
|
|
90
|
-
|
|
90
|
+
/* `stepUp()` and `stepDown()` are script, and script may write a readonly control: the platform
|
|
91
|
+
only stops the reader's own keys. The element steps on the reader's behalf, so it stops too. */
|
|
92
|
+
if (!host || !control || control.readOnly) {
|
|
91
93
|
return;
|
|
92
94
|
}
|
|
93
95
|
try {
|
package/src/behaviors/select.ts
CHANGED
|
@@ -55,6 +55,20 @@ export interface SelectState {
|
|
|
55
55
|
unlisted?: SelectRow[];
|
|
56
56
|
/** This element's own id stem, minted at mount. */
|
|
57
57
|
uid?: string;
|
|
58
|
+
/** Whether the engine draws a plain select: it has no `appearance: base-select`. */
|
|
59
|
+
plain?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether the engine can draw a select's own list, which is what every row's drawing depends on.
|
|
64
|
+
*
|
|
65
|
+
* Asked of the engine and never guessed from its name. An engine without the feature still gives a
|
|
66
|
+
* working control, with the platform's own list: it ignores the trigger button and the group
|
|
67
|
+
* headings, and draws each row as its text. That is why the one thing the fallback has to change is
|
|
68
|
+
* text: a row's label and its description are two spans, and read as text they are one word.
|
|
69
|
+
*/
|
|
70
|
+
export function drawsOwnList(): boolean {
|
|
71
|
+
return typeof CSS !== "undefined" && CSS.supports("appearance", "base-select");
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
/** How many stems have been minted, so no two instances share one. */
|
|
@@ -136,6 +150,7 @@ export function syncSelect(state: SelectState, sel: HTMLSelectElement): void {
|
|
|
136
150
|
export function mountSelect(state: SelectState, host: HTMLElement): void {
|
|
137
151
|
minted += 1;
|
|
138
152
|
state.uid = `jx-select-${minted}`;
|
|
153
|
+
state.plain = !drawsOwnList();
|
|
139
154
|
const sel = controlOf(host);
|
|
140
155
|
if (!sel) {
|
|
141
156
|
return;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jx-slider`'s behaviour sidecar: ten steps, and one event contract.
|
|
3
|
+
*
|
|
4
|
+
* The element is ONE native `<input type="range">`, so almost nothing is left for this file. The
|
|
5
|
+
* platform owns the thumb, the drag, the along-axis arrows with their clamping and snapping, Home,
|
|
6
|
+
* End, PageUp, PageDown, the focus ring, `:disabled`, and every word of `role="slider"` with its
|
|
7
|
+
* `aria-valuemin`, `aria-valuemax` and `aria-valuenow`. An arrow key with no `Shift` on it returns
|
|
8
|
+
* from {@link onSliderKeydown} untouched so that stays true.
|
|
9
|
+
*
|
|
10
|
+
* Two things are the kit's, and both are `jx-color-slider`'s (its sidecar is the model):
|
|
11
|
+
* `Shift`+Arrow moves ten steps, which no range has of its own, and the inner control's own `input`
|
|
12
|
+
* and `change` are STOPPED and re-dispatched from the host, so a listener reads `e.target.value`
|
|
13
|
+
* off the ELEMENT as a number and never has to ask which node fired. A native `dblclick` is not
|
|
14
|
+
* touched: it bubbles from the range, and the host document that wants "double-click resets"
|
|
15
|
+
* listens for it.
|
|
16
|
+
*
|
|
17
|
+
* @docs extending/ui-kit
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { ARROW_SIGN, snapToStep } from "../color.ts";
|
|
21
|
+
|
|
22
|
+
/** The tag this sidecar belongs to. */
|
|
23
|
+
const HOST = "jx-slider";
|
|
24
|
+
|
|
25
|
+
/** The reactive scope a `jx-slider` document hands its handlers. */
|
|
26
|
+
export interface SliderState {
|
|
27
|
+
/** Where the thumb is, in the track's own units. */
|
|
28
|
+
value: number;
|
|
29
|
+
/** The bottom of the range. */
|
|
30
|
+
min: number;
|
|
31
|
+
/** The top of the range. */
|
|
32
|
+
max: number;
|
|
33
|
+
/** The grid the value moves on. */
|
|
34
|
+
step: number;
|
|
35
|
+
/** Whether the element refuses to move. */
|
|
36
|
+
disabled: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The `jx-slider` an event is being handled on. */
|
|
40
|
+
function hostOf(event: Event): HTMLElement | null {
|
|
41
|
+
const target = event.currentTarget;
|
|
42
|
+
return target instanceof HTMLElement && target.localName === HOST ? target : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The element's own native range. */
|
|
46
|
+
function controlOf(host: HTMLElement): HTMLInputElement | null {
|
|
47
|
+
return host.querySelector<HTMLInputElement>('input[part="input"]');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Say the value moved, from the HOST, in the platform's own names. */
|
|
51
|
+
function announce(host: HTMLElement, ...names: string[]): void {
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
host.dispatchEvent(new Event(name, { bubbles: true }));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The inner range's `input`: mirror it, and re-say it from the host.
|
|
59
|
+
*
|
|
60
|
+
* @param state The element's reactive scope.
|
|
61
|
+
* @param event The range's own input event.
|
|
62
|
+
*/
|
|
63
|
+
export function onSliderInput(state: SliderState, event: Event): void {
|
|
64
|
+
const host = hostOf(event);
|
|
65
|
+
if (!host || event.target === host) {
|
|
66
|
+
/* The listener sits on the ROOT so `currentTarget` is the element, which means it also hears
|
|
67
|
+
the element's own re-dispatch. That one has already been accounted for. */
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
event.stopPropagation();
|
|
71
|
+
/* SAFETY: the element's only form control is its `input[part="input"]` range, so an `input` event
|
|
72
|
+
reaching the host from anything other than the host itself was fired by that range. */
|
|
73
|
+
state.value = Number((event.target as HTMLInputElement).value);
|
|
74
|
+
announce(host, "input");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The inner range's `change`: the same stop, and the commit re-said from the host.
|
|
79
|
+
*
|
|
80
|
+
* @param state The element's reactive scope.
|
|
81
|
+
* @param event The range's own change event.
|
|
82
|
+
*/
|
|
83
|
+
export function onSliderChange(state: SliderState, event: Event): void {
|
|
84
|
+
const host = hostOf(event);
|
|
85
|
+
if (!host || event.target === host) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
/* SAFETY: the element's only form control is its `input[part="input"]` range, so a `change` event
|
|
90
|
+
reaching the host from anything other than the host itself was fired by that range. */
|
|
91
|
+
state.value = Number((event.target as HTMLInputElement).value);
|
|
92
|
+
announce(host, "change");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `Shift`+Arrow: ten steps, cancelled so the platform's own single step does not run on top.
|
|
97
|
+
*
|
|
98
|
+
* @param state The element's reactive scope.
|
|
99
|
+
* @param event The keydown.
|
|
100
|
+
*/
|
|
101
|
+
export function onSliderKeydown(state: SliderState, event: KeyboardEvent): void {
|
|
102
|
+
const host = hostOf(event);
|
|
103
|
+
const sign = ARROW_SIGN.get(event.key);
|
|
104
|
+
if (!host || !sign || !event.shiftKey || state.disabled === true) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
event.preventDefault();
|
|
108
|
+
const min = Number.isFinite(Number(state.min)) ? Number(state.min) : 0;
|
|
109
|
+
const top = Number(state.max);
|
|
110
|
+
const max = Number.isFinite(top) && top > min ? top : min + 100;
|
|
111
|
+
const step =
|
|
112
|
+
Number(state.step) > 0 && Number.isFinite(Number(state.step)) ? Number(state.step) : 1;
|
|
113
|
+
const next = snapToStep(Number(state.value) + sign * step * 10, step, min, max);
|
|
114
|
+
if (next === Number(state.value)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
state.value = next;
|
|
118
|
+
/* The control follows through its own binding, but the binding is what a RENDER does and the
|
|
119
|
+
events go out now, so the control is written here too: a host that reads the range back out of
|
|
120
|
+
`e.target` during the handler must not see the value it had before the key. */
|
|
121
|
+
const control = controlOf(host);
|
|
122
|
+
if (control) {
|
|
123
|
+
control.value = String(next);
|
|
124
|
+
}
|
|
125
|
+
announce(host, "input", "change");
|
|
126
|
+
}
|
|
@@ -109,6 +109,44 @@ export function mintFieldId(state: TextfieldState): void {
|
|
|
109
109
|
|
|
110
110
|
let namedRows = 0;
|
|
111
111
|
|
|
112
|
+
/** The native controls a reader can be IN, which is where a row's label leads. */
|
|
113
|
+
const FOCUSABLE_CONTROLS = "input:not([type=hidden]), select, textarea, button";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A click on a `jx-field`'s label moves focus to the control the row holds, which is what a `<label
|
|
117
|
+
* for>` does and what a reader aiming at the bigger target expects.
|
|
118
|
+
*
|
|
119
|
+
* It is done here rather than with a `for`, because `for` needs an id on the native control and the
|
|
120
|
+
* control a kit element draws is its own to replace: `jx-textfield` swaps an `<input>` for a
|
|
121
|
+
* `<textarea>` when `multiline` changes, and an id minted at mount would not survive that. So the
|
|
122
|
+
* control is looked up at the click: the slotted element itself when it is a native control,
|
|
123
|
+
* otherwise the first one it draws.
|
|
124
|
+
*
|
|
125
|
+
* The click moves focus and nothing else. It does not tick a box or flip a switch: the row's label
|
|
126
|
+
* sits a column away from its control and Studio's rows are dense, so a label that toggled would
|
|
127
|
+
* change a setting on a click that was aimed at reading it. A disabled control takes no focus, and
|
|
128
|
+
* the platform is what says so.
|
|
129
|
+
*
|
|
130
|
+
* @param _state The row's reactive scope, which this never reads.
|
|
131
|
+
* @param event The click on the row's label.
|
|
132
|
+
*/
|
|
133
|
+
export function focusFieldControl(_state: FieldRowState, event: Event): void {
|
|
134
|
+
const label = event.currentTarget;
|
|
135
|
+
if (!(label instanceof Element)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const control = label.nextElementSibling?.firstElementChild;
|
|
139
|
+
if (!control) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const target = control.matches(FOCUSABLE_CONTROLS)
|
|
143
|
+
? control
|
|
144
|
+
: control.querySelector(FOCUSABLE_CONTROLS);
|
|
145
|
+
if (target instanceof HTMLElement) {
|
|
146
|
+
target.focus();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
112
150
|
/** Whether an element's DEFINITION says it can be named — that it observes `labelledby`. */
|
|
113
151
|
function observesNaming(el: Element): boolean {
|
|
114
152
|
const def = customElements.get(el.localName);
|
package/src/documents.ts
CHANGED
|
@@ -27,6 +27,7 @@ import jxCombobox from "../components/jx-combobox.json";
|
|
|
27
27
|
import jxListbox from "../components/jx-listbox.json";
|
|
28
28
|
import jxOption from "../components/jx-option.json";
|
|
29
29
|
import jxSelect from "../components/jx-select.json";
|
|
30
|
+
import jxSlider from "../components/jx-slider.json";
|
|
30
31
|
import jxSpinner from "../components/jx-spinner.json";
|
|
31
32
|
import jxSplit from "../components/jx-split.json";
|
|
32
33
|
import jxTooltip from "../components/jx-tooltip.json";
|
|
@@ -86,6 +87,7 @@ export const documents = {
|
|
|
86
87
|
"jx-switch": kitDocument(jxSwitch),
|
|
87
88
|
"jx-number-field": kitDocument(jxNumberField),
|
|
88
89
|
"jx-select": kitDocument(jxSelect),
|
|
90
|
+
"jx-slider": kitDocument(jxSlider),
|
|
89
91
|
"jx-option": kitDocument(jxOption),
|
|
90
92
|
"jx-listbox": kitDocument(jxListbox),
|
|
91
93
|
"jx-combobox": kitDocument(jxCombobox),
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import * as numberField from "./behaviors/number-field.ts";
|
|
|
23
23
|
import * as actionGroup from "./behaviors/action-group.ts";
|
|
24
24
|
import * as popover from "./behaviors/popover.ts";
|
|
25
25
|
import * as select from "./behaviors/select.ts";
|
|
26
|
+
import * as slider from "./behaviors/slider.ts";
|
|
26
27
|
import * as split from "./behaviors/split.ts";
|
|
27
28
|
import * as swatchGroup from "./behaviors/swatch-group.ts";
|
|
28
29
|
import * as tabs from "./behaviors/tabs.ts";
|
|
@@ -68,6 +69,7 @@ export const KIT_MODULES = {
|
|
|
68
69
|
"jx-ui:/behaviors/action-group.ts": actionGroup,
|
|
69
70
|
"jx-ui:/behaviors/popover.ts": popover,
|
|
70
71
|
"jx-ui:/behaviors/select.ts": select,
|
|
72
|
+
"jx-ui:/behaviors/slider.ts": slider,
|
|
71
73
|
"jx-ui:/behaviors/split.ts": split,
|
|
72
74
|
"jx-ui:/behaviors/swatch-group.ts": swatchGroup,
|
|
73
75
|
"jx-ui:/behaviors/tabs.ts": tabs,
|
package/src/loaders.ts
CHANGED
|
@@ -38,6 +38,7 @@ export const KIT_LOADERS = {
|
|
|
38
38
|
"jx-ui:/behaviors/action-group.ts": () => import("./behaviors/action-group.ts"),
|
|
39
39
|
"jx-ui:/behaviors/popover.ts": () => import("./behaviors/popover.ts"),
|
|
40
40
|
"jx-ui:/behaviors/select.ts": () => import("./behaviors/select.ts"),
|
|
41
|
+
"jx-ui:/behaviors/slider.ts": () => import("./behaviors/slider.ts"),
|
|
41
42
|
"jx-ui:/behaviors/split.ts": () => import("./behaviors/split.ts"),
|
|
42
43
|
"jx-ui:/behaviors/swatch-group.ts": () => import("./behaviors/swatch-group.ts"),
|
|
43
44
|
"jx-ui:/behaviors/tabs.ts": () => import("./behaviors/tabs.ts"),
|
package/stylebook/jx-icon.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://jxsuite.com/schema/v1",
|
|
3
|
+
"$id": "StylebookSlider",
|
|
4
|
+
"tagName": "main",
|
|
5
|
+
"$elements": [
|
|
6
|
+
{
|
|
7
|
+
"$ref": "../components/jx-slider.json"
|
|
8
|
+
}
|
|
9
|
+
],
|
|
10
|
+
"state": {
|
|
11
|
+
"level": 40
|
|
12
|
+
},
|
|
13
|
+
"style": {
|
|
14
|
+
"fontFamily": "var(--jx-font-sans)",
|
|
15
|
+
"fontSize": "var(--jx-text-md)",
|
|
16
|
+
"color": "var(--jx-fg)",
|
|
17
|
+
"background": "var(--jx-bg)",
|
|
18
|
+
"padding": "var(--jx-space-6)",
|
|
19
|
+
"minHeight": "100vh",
|
|
20
|
+
"& > h1": {
|
|
21
|
+
"fontSize": "var(--jx-text-lg)",
|
|
22
|
+
"fontWeight": "600",
|
|
23
|
+
"margin": "0 0 var(--jx-space-5)"
|
|
24
|
+
},
|
|
25
|
+
"& > h2": {
|
|
26
|
+
"fontSize": "var(--jx-text-md)",
|
|
27
|
+
"fontWeight": "600",
|
|
28
|
+
"margin": "var(--jx-space-6) 0 var(--jx-space-3)"
|
|
29
|
+
},
|
|
30
|
+
"& > p": {
|
|
31
|
+
"color": "var(--jx-fg-dim)",
|
|
32
|
+
"margin": "0 0 var(--jx-space-5)"
|
|
33
|
+
},
|
|
34
|
+
"& [role=\"group\"]": {
|
|
35
|
+
"display": "flex",
|
|
36
|
+
"alignItems": "center",
|
|
37
|
+
"gap": "var(--jx-space-4)",
|
|
38
|
+
"margin": "0 0 var(--jx-space-4)",
|
|
39
|
+
"maxWidth": "420px"
|
|
40
|
+
},
|
|
41
|
+
"& [role=\"group\"] > span": {
|
|
42
|
+
"width": "90px",
|
|
43
|
+
"color": "var(--jx-fg-muted)",
|
|
44
|
+
"fontSize": "var(--jx-text-sm)"
|
|
45
|
+
},
|
|
46
|
+
"& [role=\"group\"] > jx-slider": {
|
|
47
|
+
"flex": "1 1 200px"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"children": [
|
|
51
|
+
{
|
|
52
|
+
"tagName": "h1",
|
|
53
|
+
"textContent": "jx-slider"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"tagName": "p",
|
|
57
|
+
"textContent": "One native range. The platform supplies the slider role, the announced value, the thumb, the drag, the arrow keys with their clamping, and Home, End, PageUp and PageDown; the element adds a filled track, a ten-step Shift+Arrow and valuetext, and re-says the platform's own input and change from itself, so a listener always reads e.target.value off the element."
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"tagName": "h2",
|
|
61
|
+
"textContent": "Bound"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"tagName": "div",
|
|
65
|
+
"attributes": {
|
|
66
|
+
"role": "group",
|
|
67
|
+
"aria-label": "Level, bound both ways"
|
|
68
|
+
},
|
|
69
|
+
"children": [
|
|
70
|
+
{
|
|
71
|
+
"tagName": "span",
|
|
72
|
+
"textContent": "Level"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"tagName": "jx-slider",
|
|
76
|
+
"$props": {
|
|
77
|
+
"label": "Level",
|
|
78
|
+
"valuetext": "${Math.round(state.level)} percent",
|
|
79
|
+
"value": {
|
|
80
|
+
"$ref": "#/state/level"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"oninput": {
|
|
84
|
+
"$prototype": "Function",
|
|
85
|
+
"body": [
|
|
86
|
+
{
|
|
87
|
+
"operator": "=",
|
|
88
|
+
"target": {
|
|
89
|
+
"$ref": "#/state/level"
|
|
90
|
+
},
|
|
91
|
+
"value": {
|
|
92
|
+
"$ref": "event#/target/value"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"tagName": "span",
|
|
100
|
+
"textContent": "${Math.round(state.level)}%"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"tagName": "h2",
|
|
106
|
+
"textContent": "States"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"tagName": "div",
|
|
110
|
+
"attributes": {
|
|
111
|
+
"role": "group",
|
|
112
|
+
"aria-label": "A coarse grid and a refusal"
|
|
113
|
+
},
|
|
114
|
+
"children": [
|
|
115
|
+
{
|
|
116
|
+
"tagName": "span",
|
|
117
|
+
"textContent": "Step 25"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"tagName": "jx-slider",
|
|
121
|
+
"$props": {
|
|
122
|
+
"label": "Quarter steps",
|
|
123
|
+
"step": 25,
|
|
124
|
+
"value": 50
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"tagName": "div",
|
|
131
|
+
"attributes": {
|
|
132
|
+
"role": "group",
|
|
133
|
+
"aria-label": "Disabled"
|
|
134
|
+
},
|
|
135
|
+
"children": [
|
|
136
|
+
{
|
|
137
|
+
"tagName": "span",
|
|
138
|
+
"textContent": "Disabled"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"tagName": "jx-slider",
|
|
142
|
+
"$props": {
|
|
143
|
+
"label": "Locked",
|
|
144
|
+
"disabled": true,
|
|
145
|
+
"value": 70
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|