flet-box 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/bin/commands/checkUi.js +276 -45
- package/bin/commands/create.js +8 -1
- package/bin/commands/createBundle.js +18 -6
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- package/bin/utils/bundleConfig.js +47 -3
- package/bin/utils/colors.js +0 -8
- package/bin/utils/helpers.js +0 -5
- package/bin/utils/images/jpeg.js +2 -3
- package/bin/utils/templates.js +101 -85
- package/docs/README.md +24 -1
- package/docs/cli/README.md +51 -10
- package/docs/guides/README.md +5 -2
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +53 -8
- package/docs/guides/first-app.md +6 -7
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- package/docs/guides/online-updates.md +18 -6
- package/docs/guides/utilities.md +116 -4
- package/docs/guides/widget-structure.md +30 -0
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +134 -11
- package/docs/tools/device.md +75 -3
- package/docs/tools/text-and-time.md +116 -6
- package/docs/widget/Accordion.md +2 -0
- package/docs/widget/AnimatedBox.md +46 -20
- package/docs/widget/AnimatedText.md +13 -7
- package/docs/widget/Badge.md +1 -0
- package/docs/widget/COMMON_PROPS.md +26 -3
- package/docs/widget/Chart.md +1 -0
- package/docs/widget/Checkbox.md +7 -1
- package/docs/widget/CircularChart.md +2 -0
- package/docs/widget/Drawer.md +1 -0
- package/docs/widget/GridView.md +4 -1
- package/docs/widget/Input.md +2 -0
- package/docs/widget/InstallButton.md +1 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/Rating.md +2 -0
- package/docs/widget/Slider.md +2 -0
- package/docs/widget/Stack.md +6 -5
- package/docs/widget/Stepper.md +2 -0
- package/docs/widget/Tooltip.md +3 -1
- package/package.json +3 -2
- package/skills/fletbox-design/SKILL.md +390 -14
- package/skills/fletbox-motion/SKILL.md +145 -47
- package/skills/fletbox-rules/SKILL.md +140 -1
- package/src/animations/AnimatedBox.js +321 -226
- package/src/animations/MatrixRain.js +7 -1
- package/src/animations/ParallaxBox.js +13 -10
- package/src/animations/animate.js +269 -106
- package/src/animations/color.js +91 -0
- package/src/animations/easings.js +275 -0
- package/src/animations/index.js +21 -0
- package/src/animations/motion.js +204 -0
- package/src/animations/transitions.js +327 -0
- package/src/core/runApp.js +0 -6
- package/src/helpers/boxEdges.js +69 -0
- package/src/helpers/html.js +39 -0
- package/src/index.d.ts +377 -25
- package/src/index.js +77 -0
- package/src/navigations/AdaptiveScaffold.js +6 -2
- package/src/navigations/AppBar.js +16 -7
- package/src/navigations/BottomNavigation.js +24 -1
- package/src/navigations/CollapsibleSideBar.js +0 -1
- package/src/navigations/Drawer.js +32 -1
- package/src/navigations/DrawerItem.js +1 -1
- package/src/navigations/Router.js +0 -1
- package/src/navigations/Scaffold.js +9 -3
- package/src/navigations/Tabs.js +104 -31
- package/src/services/RamStore.js +1 -2
- package/src/tools/breakpoints.js +113 -0
- package/src/tools/createList.js +0 -2
- package/src/tools/fonts.js +99 -0
- package/src/tools/index.js +74 -1
- package/src/tools/margin.js +5 -63
- package/src/tools/padding.js +5 -63
- package/src/tools/print.js +0 -8
- package/src/tools/scale.js +9 -2
- package/src/tools/time.js +56 -15
- package/src/tools/useState.js +1 -54
- package/src/utils/fieldFont.js +77 -0
- package/src/utils/focusRing.js +73 -0
- package/src/utils/focusScope.js +113 -0
- package/src/utils/markdownParser.js +7 -9
- package/src/utils/readableInk.js +7 -1
- package/src/utils/stackLayout.js +55 -0
- package/src/utils/styleInjector.js +25 -0
- package/src/utils/syntaxHighlight.js +1 -17
- package/src/utils/themes.js +15 -0
- package/src/utils/visualEffects.js +49 -47
- package/src/widget-factory/createWidget.js +16 -3
- package/src/widget-factory/effects.js +76 -3
- package/src/widget-factory/reactiveValue.js +0 -7
- package/src/widget-factory/tools.js +26 -143
- package/src/widget-factory/translateProps.js +5 -0
- package/src/widget-factory/widgetFactory.js +1 -1
- package/src/widgets/Accordion.js +70 -12
- package/src/widgets/AdMob.js +1 -1
- package/src/widgets/AdSense.js +1 -1
- package/src/widgets/AlertDialog.js +11 -9
- package/src/widgets/Avatar.js +10 -1
- package/src/widgets/Badge.js +7 -1
- package/src/widgets/BottomSheet.js +39 -5
- package/src/widgets/Chart.js +41 -9
- package/src/widgets/Checkbox.js +59 -9
- package/src/widgets/Chip.js +13 -1
- package/src/widgets/CircularBar.js +17 -5
- package/src/widgets/CircularChart.js +24 -2
- package/src/widgets/CodeViewer.js +5 -3
- package/src/widgets/Dropdown.js +223 -20
- package/src/widgets/GridView.js +17 -1
- package/src/widgets/Icon.js +21 -0
- package/src/widgets/Input.js +75 -44
- package/src/widgets/InstallButton.js +8 -2
- package/src/widgets/ListTile.js +6 -5
- package/src/widgets/ListView.js +21 -7
- package/src/widgets/Markdown.js +25 -44
- package/src/widgets/Modal.js +43 -6
- package/src/widgets/Pagination.js +35 -3
- package/src/widgets/ProgressBar.js +18 -0
- package/src/widgets/Radio.js +7 -0
- package/src/widgets/Rating.js +104 -16
- package/src/widgets/Skeleton.js +9 -5
- package/src/widgets/Slider.js +87 -7
- package/src/widgets/SnackBar.js +30 -1
- package/src/widgets/Stack.js +25 -13
- package/src/widgets/Stepper.js +112 -64
- package/src/widgets/Switch.js +10 -1
- package/src/widgets/Tooltip.js +93 -10
- package/src/widgets/TreeView.js +5 -5
- package/src/create-index.sh +0 -237
- package/src/widget-factory/pixTool.js +0 -40
- package/src/widget-factory/stringTool.js +0 -26
|
@@ -10,6 +10,8 @@ import { Text } from "./Text.js";
|
|
|
10
10
|
import { Icon } from "./Icon.js";
|
|
11
11
|
import { Container } from "./Container.js";
|
|
12
12
|
import { toREM } from "../utils/units.js";
|
|
13
|
+
import { PILL } from "../tools/index.js";
|
|
14
|
+
import { trapFocus } from "../utils/focusScope.js";
|
|
13
15
|
|
|
14
16
|
export const BottomSheet = (props) => {
|
|
15
17
|
const {
|
|
@@ -55,8 +57,6 @@ export const BottomSheet = (props) => {
|
|
|
55
57
|
// Callbacks
|
|
56
58
|
onOpen,
|
|
57
59
|
onClose,
|
|
58
|
-
|
|
59
|
-
...rest
|
|
60
60
|
} = props;
|
|
61
61
|
|
|
62
62
|
let isOpen = false;
|
|
@@ -64,6 +64,11 @@ export const BottomSheet = (props) => {
|
|
|
64
64
|
// Pending "hide after the close animation" timer, so it can be cancelled.
|
|
65
65
|
let closeTimer = null;
|
|
66
66
|
let sheetContainer = null;
|
|
67
|
+
// What `trapFocus` hands back while the sheet is open (see Modal.js for the
|
|
68
|
+
// whole argument): Tab stays inside the panel and focus goes home on close.
|
|
69
|
+
// The release is idempotent, so close and unmount can both call it.
|
|
70
|
+
let releaseFocusScope = () => {};
|
|
71
|
+
|
|
67
72
|
let startY = 0;
|
|
68
73
|
let currentY = 0;
|
|
69
74
|
|
|
@@ -76,6 +81,15 @@ export const BottomSheet = (props) => {
|
|
|
76
81
|
height: "100%",
|
|
77
82
|
backgroundColor: overlayColor,
|
|
78
83
|
zIndex: overlayZIndex,
|
|
84
|
+
// The scrim closes the sheet for a pointer and is nothing for a keyboard —
|
|
85
|
+
// left to the engine, its `click` would become a full-viewport tab stop.
|
|
86
|
+
role: "presentation",
|
|
87
|
+
tabIndex: -1,
|
|
88
|
+
// ...but it is also the panel's parent, and the engine's press feedback writes
|
|
89
|
+
// `scale: 0.98` on whatever carries the handler: pressing the backdrop shrank the
|
|
90
|
+
// whole layer, so the sheet dipped and grew a shadow-less frame for the length of
|
|
91
|
+
// the press. A scrim is not a control — it presses nothing, it only dismisses.
|
|
92
|
+
disableTransform: true,
|
|
79
93
|
opacity: 0,
|
|
80
94
|
visibility: "hidden",
|
|
81
95
|
transition: `opacity ${animationDuration}ms ease, visibility ${animationDuration}ms ease`,
|
|
@@ -97,7 +111,7 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
97
111
|
width: dragHandleWidth,
|
|
98
112
|
height: dragHandleHeight,
|
|
99
113
|
backgroundColor: dragHandleColor,
|
|
100
|
-
borderRadius:
|
|
114
|
+
borderRadius: PILL,
|
|
101
115
|
}),
|
|
102
116
|
})
|
|
103
117
|
: null;
|
|
@@ -116,13 +130,23 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
116
130
|
);
|
|
117
131
|
}
|
|
118
132
|
if (showCloseButton) {
|
|
133
|
+
// Both the name and the handler are props, not post-hoc assignments: `Icon`
|
|
134
|
+
// decides whether it is decoration from the props it is handed, and the engine
|
|
135
|
+
// only makes a control keyboard-operable when it can see the press while the
|
|
136
|
+
// widget is built. Set after the fact, this close button was a mouse-only "×"
|
|
137
|
+
// that a reader could not even find.
|
|
119
138
|
const closeBtn = Icon({
|
|
120
139
|
name: "close",
|
|
121
|
-
size:
|
|
140
|
+
size: 24,
|
|
141
|
+
// 24 + 2 x 8 = 40 framework units: the same target floor every other
|
|
142
|
+
// pressable control answers to, and it scales with the reader's font.
|
|
143
|
+
paddingVertical: 8,
|
|
144
|
+
paddingHorizontal: 8,
|
|
122
145
|
color: colors.textSecondary,
|
|
123
146
|
cursor: "pointer",
|
|
147
|
+
onclick: () => close(),
|
|
148
|
+
"aria-label": "Close sheet",
|
|
124
149
|
});
|
|
125
|
-
closeBtn.onclick = () => close();
|
|
126
150
|
headerChildren.push(closeBtn);
|
|
127
151
|
}
|
|
128
152
|
|
|
@@ -189,6 +213,12 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
189
213
|
),
|
|
190
214
|
});
|
|
191
215
|
|
|
216
|
+
sheetContainer.setAttribute("role", "dialog");
|
|
217
|
+
sheetContainer.setAttribute("aria-modal", "true");
|
|
218
|
+
if (typeof title === "string" && title) {
|
|
219
|
+
sheetContainer.setAttribute("aria-label", title);
|
|
220
|
+
}
|
|
221
|
+
|
|
192
222
|
sheetContainer.appendChild(sheetBody);
|
|
193
223
|
overlay.appendChild(sheetContainer);
|
|
194
224
|
document.body.appendChild(overlay);
|
|
@@ -203,6 +233,7 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
203
233
|
closeTimer = null;
|
|
204
234
|
}
|
|
205
235
|
document.removeEventListener("keydown", handleKeyDown);
|
|
236
|
+
releaseFocusScope();
|
|
206
237
|
});
|
|
207
238
|
|
|
208
239
|
// Drag handlers
|
|
@@ -254,6 +285,7 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
254
285
|
sheetContainer.style.transform = "translateY(0)";
|
|
255
286
|
if (onOpen) onOpen();
|
|
256
287
|
document.addEventListener("keydown", handleKeyDown);
|
|
288
|
+
releaseFocusScope = trapFocus(sheetContainer);
|
|
257
289
|
|
|
258
290
|
if (closeOnDragDown && dragHandle) {
|
|
259
291
|
dragHandle.addEventListener("mousedown", onTouchStart);
|
|
@@ -277,6 +309,7 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
277
309
|
closeTimer = null;
|
|
278
310
|
overlay.style.visibility = "hidden";
|
|
279
311
|
}, animationDuration);
|
|
312
|
+
releaseFocusScope();
|
|
280
313
|
if (onClose) onClose();
|
|
281
314
|
document.removeEventListener("keydown", handleKeyDown);
|
|
282
315
|
|
|
@@ -297,6 +330,7 @@ onclick: (e) => e.stopPropagation(), // ✅ Prevents the click from reaching the
|
|
|
297
330
|
|
|
298
331
|
const destroy = () => {
|
|
299
332
|
if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
333
|
+
releaseFocusScope();
|
|
300
334
|
document.removeEventListener("keydown", handleKeyDown);
|
|
301
335
|
};
|
|
302
336
|
|
package/src/widgets/Chart.js
CHANGED
|
@@ -10,6 +10,36 @@
|
|
|
10
10
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
11
11
|
import { colors } from "../utils/themes.js";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The step a Y axis labels in: a round number whose mantissa is 1, 2 or 5.
|
|
15
|
+
*
|
|
16
|
+
* Reading the axis off the padded range divided into five — what this did before —
|
|
17
|
+
* puts the labels wherever the data happens to end: a `[6, 21]` series wrote
|
|
18
|
+
* `0, 4, 8, 13, 17, 21`, whose steps are 4, 4, 5, 4, 4. Equal gridline spacing with
|
|
19
|
+
* unequal labelled steps reads as a rounding bug even when nothing is rounding, and
|
|
20
|
+
* it makes the top label disagree with the line it sits on.
|
|
21
|
+
*
|
|
22
|
+
* @param {number} rough - The step the desired number of lines implies.
|
|
23
|
+
* @returns {number} The nearest round step at or above it.
|
|
24
|
+
*/
|
|
25
|
+
const niceStep = (rough) => {
|
|
26
|
+
if (!(rough > 0)) return 1;
|
|
27
|
+
const exponent = Math.pow(10, Math.floor(Math.log10(rough)));
|
|
28
|
+
const mantissa = rough / exponent;
|
|
29
|
+
const nice = mantissa <= 1 ? 1 : mantissa <= 2 ? 2 : mantissa <= 5 ? 5 : 10;
|
|
30
|
+
return nice * exponent;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How many decimals a step needs to be written honestly: a 0.5 step labelled as
|
|
35
|
+
* `0` and `1` describes neither of its lines.
|
|
36
|
+
*
|
|
37
|
+
* @param {number} step
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
const stepDecimals = (step) =>
|
|
41
|
+
Math.max(0, Math.min(6, -Math.floor(Math.log10(step))));
|
|
42
|
+
|
|
13
43
|
/**
|
|
14
44
|
* Creates a Chart widget rendered on an HTML5 `<canvas>` element.
|
|
15
45
|
*
|
|
@@ -364,7 +394,6 @@ export const Chart = (props) => {
|
|
|
364
394
|
* Draws a candlestick chart onto the canvas context.
|
|
365
395
|
*
|
|
366
396
|
* @param {CanvasRenderingContext2D} ctx
|
|
367
|
-
* @param {number} canvasWidth
|
|
368
397
|
* @param {number} chartHeight
|
|
369
398
|
* @param {number} maxValue
|
|
370
399
|
* @param {number} minValue
|
|
@@ -373,7 +402,6 @@ export const Chart = (props) => {
|
|
|
373
402
|
*/
|
|
374
403
|
const drawCandle = (
|
|
375
404
|
ctx,
|
|
376
|
-
canvasWidth,
|
|
377
405
|
chartHeight,
|
|
378
406
|
maxValue,
|
|
379
407
|
minValue,
|
|
@@ -434,23 +462,28 @@ export const Chart = (props) => {
|
|
|
434
462
|
ctx.strokeStyle = axisColor;
|
|
435
463
|
ctx.lineWidth = 0.5;
|
|
436
464
|
|
|
437
|
-
|
|
465
|
+
// Lines sit on round values inside the plotted range, not on fifths of it.
|
|
438
466
|
const range = maxValue - minValue;
|
|
467
|
+
const step = niceStep(range / 5);
|
|
468
|
+
const decimals = stepDecimals(step);
|
|
469
|
+
const first = Math.ceil(minValue / step) * step;
|
|
470
|
+
const lines = range > 0 ? Math.floor((maxValue - first) / step) : 0;
|
|
439
471
|
|
|
440
|
-
for (let i = 0; i <=
|
|
441
|
-
const value =
|
|
472
|
+
for (let i = 0; i <= lines; i++) {
|
|
473
|
+
const value = first + i * step;
|
|
442
474
|
const y = getYPosition(value, minValue, maxValue, chartHeight);
|
|
443
475
|
ctx.beginPath();
|
|
444
476
|
ctx.moveTo(pad.left, y);
|
|
445
477
|
ctx.lineTo(canvasWidth - pad.right, y);
|
|
446
478
|
ctx.stroke();
|
|
447
479
|
|
|
448
|
-
//
|
|
449
|
-
|
|
480
|
+
// A step of 0.5 needs one decimal; a step of 5 needs none. Writing `-0` for
|
|
481
|
+
// a value that is zero by rounding reads as a negative axis.
|
|
482
|
+
const label = Math.abs(value) < step / 100 ? 0 : value.toFixed(decimals);
|
|
450
483
|
ctx.fillStyle = yAxisColor;
|
|
451
484
|
ctx.font = "9px sans-serif";
|
|
452
485
|
ctx.textAlign = "right";
|
|
453
|
-
ctx.fillText(
|
|
486
|
+
ctx.fillText(label, pad.left - 5, y + 3);
|
|
454
487
|
}
|
|
455
488
|
};
|
|
456
489
|
|
|
@@ -534,7 +567,6 @@ export const Chart = (props) => {
|
|
|
534
567
|
} else if (type === "candle") {
|
|
535
568
|
drawCandle(
|
|
536
569
|
context,
|
|
537
|
-
canvasWidth,
|
|
538
570
|
chartHeight,
|
|
539
571
|
maxValue,
|
|
540
572
|
minValue,
|
package/src/widgets/Checkbox.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
6
|
import { Text } from "./Text.js";
|
|
7
7
|
import { colors } from "../utils/themes.js";
|
|
8
|
+
import { readableInk } from "../utils/readableInk.js";
|
|
8
9
|
import { TAP_MINIMUM, LABEL_TEXT } from "../tools/index.js";
|
|
9
10
|
|
|
10
11
|
export const Checkbox = (props) => {
|
|
@@ -16,6 +17,8 @@ export const Checkbox = (props) => {
|
|
|
16
17
|
size = 20,
|
|
17
18
|
activeColor = colors.primary,
|
|
18
19
|
label = null,
|
|
20
|
+
borderRadius = null,
|
|
21
|
+
variant = "outlined",
|
|
19
22
|
...rest
|
|
20
23
|
} = props;
|
|
21
24
|
|
|
@@ -33,22 +36,55 @@ export const Checkbox = (props) => {
|
|
|
33
36
|
// clears the 3:1 non-text minimum on each surface.
|
|
34
37
|
const offColor = disabled ? colors.border : colors.gray500;
|
|
35
38
|
|
|
39
|
+
// `variant: "filled"` is the same word the rest of the API already uses for a solid
|
|
40
|
+
// plate (`Button`, `Chip`, `Input`, `Dropdown`), and it changes the *checked* state
|
|
41
|
+
// only: a filled box that means "off" is a button, not a checkbox. The plate is the
|
|
42
|
+
// accent the control already carries, so one brand color per screen still holds, and
|
|
43
|
+
// the tick takes whichever of black or white reads on it — the same decision `Button`
|
|
44
|
+
// makes, and it falls back to the theme's own on-plate ink for a fill this code
|
|
45
|
+
// cannot measure (a gradient, a CSS variable).
|
|
46
|
+
const isFilled = variant === "filled";
|
|
47
|
+
const plateInk = readableInk(activeColor, colors.onPrimary);
|
|
48
|
+
|
|
49
|
+
// The corner is a decision, and this control shipped none: `borderRadius: 0` put a
|
|
50
|
+
// raw square plate inside the rounded cards, fields and buttons of the same screen,
|
|
51
|
+
// which is rule 02's "two products on one page". It is snapped to two steps of the
|
|
52
|
+
// radius scale rather than scaled continuously — a radius between the steps is the
|
|
53
|
+
// drift rule 03 calls `OFF_SCALE_RADIUS`, and two checkboxes on one form that differ
|
|
54
|
+
// by 1px of corner read as a mistake nobody can name. `4` on the default 20-unit box,
|
|
55
|
+
// `8` once the box is big enough to carry it.
|
|
56
|
+
const boxRadius = borderRadius ?? (size >= 28 ? 8 : 4);
|
|
57
|
+
|
|
58
|
+
// The three things a state decides, in one place, so the box built below and every
|
|
59
|
+
// repaint on press cannot disagree. A checked box always wears the accent: the rim
|
|
60
|
+
// used to fall to `colors.border` while disabled, which measured 1.2:1 on the dark
|
|
61
|
+
// card — a locked-on checkbox that looked unchecked.
|
|
62
|
+
const rimFor = (on) => (on ? activeColor : offColor);
|
|
63
|
+
const fillFor = (on) => (on && isFilled ? activeColor : "transparent");
|
|
64
|
+
const tickFor = (on) =>
|
|
65
|
+
on && isFilled ? plateInk : disabled ? colors.textDisabled : activeColor;
|
|
66
|
+
|
|
36
67
|
const updateUI = (newChecked) => {
|
|
68
|
+
// The state an assistive technology reads is this attribute, not the tick it
|
|
69
|
+
// cannot see. `updateUI` is the one place the visual state is painted, so the
|
|
70
|
+
// attribute and the box cannot disagree.
|
|
71
|
+
if (checkbox) checkbox.setAttribute("aria-checked", String(Boolean(newChecked)));
|
|
37
72
|
if (checkboxRef) {
|
|
38
|
-
checkboxRef.style.border = `2px solid ${newChecked
|
|
73
|
+
checkboxRef.style.border = `2px solid ${rimFor(newChecked)}`;
|
|
74
|
+
checkboxRef.style.backgroundColor = fillFor(newChecked);
|
|
39
75
|
}
|
|
40
76
|
if (checkMarkRef) {
|
|
41
77
|
checkMarkRef.style.display = newChecked ? "inline-block" : "none";
|
|
78
|
+
checkMarkRef.style.color = tickFor(newChecked);
|
|
42
79
|
}
|
|
43
80
|
};
|
|
44
81
|
|
|
45
|
-
// The tick
|
|
46
|
-
//
|
|
47
|
-
// to carry (white in light, near-black in dark) only read on that plate.
|
|
82
|
+
// The tick lives inside the frame, in the accent — or in the plate's readable ink
|
|
83
|
+
// once `variant: "filled"` turns the box into a solid plate.
|
|
48
84
|
const checkMark = WidgetFactory({
|
|
49
85
|
tag: "span",
|
|
50
86
|
textContent: "✓",
|
|
51
|
-
color:
|
|
87
|
+
color: tickFor(internalChecked),
|
|
52
88
|
fontSize: Math.max(12, Math.round(size * 0.65)),
|
|
53
89
|
fontWeight: "bold",
|
|
54
90
|
lineHeight: 1,
|
|
@@ -56,14 +92,14 @@ export const Checkbox = (props) => {
|
|
|
56
92
|
});
|
|
57
93
|
checkMarkRef = checkMark;
|
|
58
94
|
|
|
59
|
-
// The visual
|
|
95
|
+
// The visual box. Everything drawn lives here, so the root below can be the
|
|
60
96
|
// finger's target without becoming a 40-unit checkbox.
|
|
61
97
|
const box = WidgetFactory({
|
|
62
98
|
width: size,
|
|
63
99
|
height: size,
|
|
64
|
-
border: `2px solid ${
|
|
65
|
-
borderRadius:
|
|
66
|
-
backgroundColor:
|
|
100
|
+
border: `2px solid ${rimFor(internalChecked)}`,
|
|
101
|
+
borderRadius: boxRadius,
|
|
102
|
+
backgroundColor: fillFor(internalChecked),
|
|
67
103
|
display: "flex",
|
|
68
104
|
alignItems: "center",
|
|
69
105
|
justifyContent: "center",
|
|
@@ -89,6 +125,12 @@ export const Checkbox = (props) => {
|
|
|
89
125
|
|
|
90
126
|
const rootProps = {
|
|
91
127
|
widgetName: "Checkbox",
|
|
128
|
+
// The drawn box is a `span` and the root is a row, so nothing here is a
|
|
129
|
+
// checkbox to an assistive technology unless the widget says so. `role`
|
|
130
|
+
// reaches the factory before `applyEffects`, which is what keeps the engine
|
|
131
|
+
// from labelling the control `button`.
|
|
132
|
+
role: "checkbox",
|
|
133
|
+
"aria-checked": String(Boolean(internalChecked)),
|
|
92
134
|
display: "inline-flex",
|
|
93
135
|
alignItems: "center",
|
|
94
136
|
justifyContent: isLabelled ? "flex-start" : "center",
|
|
@@ -116,11 +158,19 @@ export const Checkbox = (props) => {
|
|
|
116
158
|
// default press feedback, because there the root *is* the control.
|
|
117
159
|
if (isLabelled) rootProps.disableTransform = true;
|
|
118
160
|
|
|
161
|
+
// A caller's radius is the control's, so it rounds the drawn box above. It is
|
|
162
|
+
// re-applied to the root only when it was actually given: that row is where a
|
|
163
|
+
// `bgColor` lands on a labelled checkbox, and it must keep the corner such an app
|
|
164
|
+
// already saw. Left alone otherwise, because the root is invisible and its own
|
|
165
|
+
// corner is nobody's business.
|
|
166
|
+
if (borderRadius !== null) rootProps.borderRadius = borderRadius;
|
|
167
|
+
|
|
119
168
|
const checkbox = WidgetFactory({ ...rootProps, ...rest });
|
|
120
169
|
|
|
121
170
|
// Inline usage (a checkbox dropped mid-paragraph) would otherwise
|
|
122
171
|
// baseline-align the box's bottom edge and lift the square above the text.
|
|
123
172
|
checkbox.style.verticalAlign = "middle";
|
|
173
|
+
if (disabled) checkbox.setAttribute("aria-disabled", "true");
|
|
124
174
|
|
|
125
175
|
// Handle click
|
|
126
176
|
if (!disabled) {
|
package/src/widgets/Chip.js
CHANGED
|
@@ -57,7 +57,12 @@ export const Chip = (props) => {
|
|
|
57
57
|
*/
|
|
58
58
|
const chipPadding =
|
|
59
59
|
padding === undefined
|
|
60
|
-
? {
|
|
60
|
+
? {
|
|
61
|
+
// The delete glyph carries the 40-unit target itself, so the row does
|
|
62
|
+
// not need a second gutter above and below it.
|
|
63
|
+
paddingVertical: onDelete ? 0 : 4,
|
|
64
|
+
paddingHorizontal: 12,
|
|
65
|
+
}
|
|
61
66
|
: typeof padding === "number"
|
|
62
67
|
? { paddingVertical: padding, paddingHorizontal: padding * 2 }
|
|
63
68
|
: { padding };
|
|
@@ -104,7 +109,14 @@ export const Chip = (props) => {
|
|
|
104
109
|
if (onDelete) {
|
|
105
110
|
const closeIcon = Icon({
|
|
106
111
|
name: "close",
|
|
112
|
+
// The glyph is decoration and the ligature is not a name: "close" is what a
|
|
113
|
+
// reader would say, on a control whose whole job is to take this label away.
|
|
114
|
+
"aria-label": label ? `Remove ${label}` : "Remove",
|
|
107
115
|
size: Math.max(12, size - 2),
|
|
116
|
+
// The glyph stays small; the box is the 40-unit target, in framework
|
|
117
|
+
// units so it grows with the reader's font like every other length.
|
|
118
|
+
width: TAP_MINIMUM,
|
|
119
|
+
height: TAP_MINIMUM,
|
|
108
120
|
color: txtColor,
|
|
109
121
|
cursor: "pointer",
|
|
110
122
|
onclick: (e) => {
|
|
@@ -78,7 +78,6 @@ export const CircularBar = (props) => {
|
|
|
78
78
|
let resizeObserver = null;
|
|
79
79
|
let container = null;
|
|
80
80
|
let currentWidth = 0;
|
|
81
|
-
let isHovering = false;
|
|
82
81
|
|
|
83
82
|
const isFixedSize = size !== null && size !== undefined;
|
|
84
83
|
const fixedSize = isFixedSize ? size : 200;
|
|
@@ -297,7 +296,7 @@ export const CircularBar = (props) => {
|
|
|
297
296
|
const setupEvents = () => {
|
|
298
297
|
if (!canvas) return;
|
|
299
298
|
if (onClick)
|
|
300
|
-
canvas.addEventListener("click", (
|
|
299
|
+
canvas.addEventListener("click", () =>
|
|
301
300
|
onClick({
|
|
302
301
|
value: currentValue,
|
|
303
302
|
max,
|
|
@@ -306,11 +305,9 @@ export const CircularBar = (props) => {
|
|
|
306
305
|
);
|
|
307
306
|
if (onHover) {
|
|
308
307
|
canvas.addEventListener("mouseenter", () => {
|
|
309
|
-
isHovering = true;
|
|
310
308
|
onHover(true, { value: currentValue, max });
|
|
311
309
|
});
|
|
312
310
|
canvas.addEventListener("mouseleave", () => {
|
|
313
|
-
isHovering = false;
|
|
314
311
|
onHover(false);
|
|
315
312
|
});
|
|
316
313
|
}
|
|
@@ -321,6 +318,14 @@ export const CircularBar = (props) => {
|
|
|
321
318
|
container = WidgetFactory({
|
|
322
319
|
tag: "div",
|
|
323
320
|
widgetName: "CircularBar",
|
|
321
|
+
// The arc is painted into a canvas, which is one anonymous rectangle to a
|
|
322
|
+
// reader. The role and the three numbers are the whole of what the ring
|
|
323
|
+
// means, and they are cheap to keep true.
|
|
324
|
+
role: "progressbar",
|
|
325
|
+
"aria-valuemin": "0",
|
|
326
|
+
"aria-valuemax": String(max),
|
|
327
|
+
"aria-valuenow": String(Math.min(value, max)),
|
|
328
|
+
...(label ? { "aria-label": label } : null),
|
|
324
329
|
display: isFixedSize ? "inline-block" : "block",
|
|
325
330
|
width: isFixedSize ? `${fixedSize}px` : "100%",
|
|
326
331
|
height: isFixedSize ? `${fixedSize}px` : "auto",
|
|
@@ -352,8 +357,15 @@ export const CircularBar = (props) => {
|
|
|
352
357
|
});
|
|
353
358
|
|
|
354
359
|
container.updateValue = (newValue, newMax = null) => {
|
|
355
|
-
if (newMax !== null)
|
|
360
|
+
if (newMax !== null) {
|
|
361
|
+
max = newMax;
|
|
362
|
+
container.setAttribute("aria-valuemax", String(max));
|
|
363
|
+
}
|
|
356
364
|
value = Math.min(newValue, max);
|
|
365
|
+
// The announced value is the target of the animation, not the frame it is
|
|
366
|
+
// drawing: a reader who hears the sweep would hear a different number every
|
|
367
|
+
// time they asked.
|
|
368
|
+
container.setAttribute("aria-valuenow", String(value));
|
|
357
369
|
startAnimation();
|
|
358
370
|
};
|
|
359
371
|
|
|
@@ -303,7 +303,11 @@ export const CircularChart = (props) => {
|
|
|
303
303
|
const cy = getCenterY();
|
|
304
304
|
ctx.beginPath();
|
|
305
305
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
306
|
-
|
|
306
|
+
// The disc behind a donut is the hole's filler, not a shape of its own:
|
|
307
|
+
// `gray200` painted a pale plate inside the ring, so the widget read as a
|
|
308
|
+
// pie with a grey centre instead of a ring with nothing in it. The slices'
|
|
309
|
+
// separators already use `colors.card` for the same reason.
|
|
310
|
+
ctx.fillStyle = colors.card;
|
|
307
311
|
ctx.fill();
|
|
308
312
|
}
|
|
309
313
|
};
|
|
@@ -390,7 +394,7 @@ export const CircularChart = (props) => {
|
|
|
390
394
|
if (animationId) cancelAnimationFrame(animationId);
|
|
391
395
|
if (!ctx || !canvas) return;
|
|
392
396
|
if (data && data.length > 0) {
|
|
393
|
-
currentData = data.map((
|
|
397
|
+
currentData = data.map(() => 0);
|
|
394
398
|
} else {
|
|
395
399
|
currentData = [];
|
|
396
400
|
}
|
|
@@ -471,6 +475,20 @@ export const CircularChart = (props) => {
|
|
|
471
475
|
}
|
|
472
476
|
};
|
|
473
477
|
|
|
478
|
+
/**
|
|
479
|
+
* The slices as a sentence. To a reader a canvas is one empty rectangle, so
|
|
480
|
+
* the numbers painted inside it have to be said beside it.
|
|
481
|
+
*/
|
|
482
|
+
const chartSummary = () =>
|
|
483
|
+
data.length === 0
|
|
484
|
+
? "Chart with no data"
|
|
485
|
+
: `Chart: ${data
|
|
486
|
+
.map(
|
|
487
|
+
(slice, i) =>
|
|
488
|
+
`${slice?.label || `Slice ${i + 1}`} ${slice?.value ?? 0}`,
|
|
489
|
+
)
|
|
490
|
+
.join(", ")}`;
|
|
491
|
+
|
|
474
492
|
const initWidget = () => {
|
|
475
493
|
container = WidgetFactory({
|
|
476
494
|
tag: "div",
|
|
@@ -497,6 +515,8 @@ export const CircularChart = (props) => {
|
|
|
497
515
|
canvas.style.display = "block";
|
|
498
516
|
canvas.style.width = "100%";
|
|
499
517
|
canvas.style.height = "100%";
|
|
518
|
+
canvas.setAttribute("role", "img");
|
|
519
|
+
canvas.setAttribute("aria-label", chartSummary());
|
|
500
520
|
canvas.addEventListener("click", handleCanvasClick);
|
|
501
521
|
canvas.addEventListener("mousemove", handleCanvasMousemove);
|
|
502
522
|
canvas.addEventListener("mouseleave", () => {
|
|
@@ -527,6 +547,8 @@ export const CircularChart = (props) => {
|
|
|
527
547
|
const updateData = (newData, animateUpdate = true) => {
|
|
528
548
|
if (!Array.isArray(newData)) return;
|
|
529
549
|
data = newData;
|
|
550
|
+
// New numbers on the drawing, and new numbers in the sentence that says them.
|
|
551
|
+
if (canvas) canvas.setAttribute("aria-label", chartSummary());
|
|
530
552
|
animate = animateUpdate;
|
|
531
553
|
startAnimation();
|
|
532
554
|
};
|
|
@@ -7,6 +7,7 @@ import { colors } from "../utils/themes.js";
|
|
|
7
7
|
import { composeUpdate } from "../utils/composeUpdate.js";
|
|
8
8
|
import { generateHighlightedHtml, syntaxPaletteFor } from "../utils/syntaxHighlight.js";
|
|
9
9
|
import { clipboard } from "../tools/clipboard.js";
|
|
10
|
+
import { TAP_MINIMUM } from "../tools/index.js";
|
|
10
11
|
|
|
11
12
|
import { toREM } from "../utils/units.js";
|
|
12
13
|
|
|
@@ -105,10 +106,11 @@ code = "",
|
|
|
105
106
|
backgroundColor: colors.card,
|
|
106
107
|
color: colors.text,
|
|
107
108
|
border: `1px solid ${colors.border}`,
|
|
108
|
-
borderRadius:
|
|
109
|
-
padding: "
|
|
109
|
+
borderRadius: 8,
|
|
110
|
+
padding: "8px 12px",
|
|
111
|
+
minHeight: TAP_MINIMUM,
|
|
110
112
|
fontFamily: "monospace",
|
|
111
|
-
fontSize:
|
|
113
|
+
fontSize: 12,
|
|
112
114
|
lineHeight: "1.6",
|
|
113
115
|
flexShrink: 0,
|
|
114
116
|
cursor: "pointer",
|