flet-box 1.0.9 → 1.1.1
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/bin/commands/checkUi.js +327 -46
- package/bin/commands/create.js +48 -19
- package/bin/commands/createScreen.js +1 -1
- package/bin/commands/runBundle.js +1 -1
- 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/skills.js +59 -0
- package/bin/utils/templates.js +196 -575
- package/docs/README.md +3 -1
- package/docs/cli/README.md +18 -1
- package/docs/guides/README.md +6 -1
- package/docs/guides/accessibility.md +282 -0
- package/docs/guides/clean-ui.md +84 -21
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/mobile-and-platforms.md +51 -0
- 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 +4 -2
- package/skills/fletbox-design/SKILL.md +949 -0
- package/skills/fletbox-memory/SKILL.md +134 -0
- package/skills/fletbox-motion/SKILL.md +266 -0
- package/skills/fletbox-rules/SKILL.md +293 -0
- 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 +31 -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
package/src/widgets/Rating.js
CHANGED
|
@@ -31,10 +31,18 @@ export const Rating = (props) => {
|
|
|
31
31
|
} = props;
|
|
32
32
|
|
|
33
33
|
// Colors from theme
|
|
34
|
+
//
|
|
35
|
+
// Both inks are the readable end of their own hue, for the same reason Radio's
|
|
36
|
+
// off-ring is: `colors.border` measured 1.27:1 on a white card and 1.19:1 on a
|
|
37
|
+
// dark one, so the empty stars were not a dim state but an absent one, and
|
|
38
|
+
// `colors.warning` (#F59E0B) measured 2.15:1 on white, which made a 4-out-of-5
|
|
39
|
+
// score harder to see than the label next to it. `gray500` is the neutral
|
|
40
|
+
// control outline (4.83:1 light, 3.67:1 dark) and `warningStrong` is the amber
|
|
41
|
+
// the palette already declares for words (5.02:1 light, 12.29:1 dark).
|
|
34
42
|
let activeColor =
|
|
35
|
-
propActiveColor !== undefined ? propActiveColor : colors.
|
|
43
|
+
propActiveColor !== undefined ? propActiveColor : colors.warningStrong;
|
|
36
44
|
let inactiveColor =
|
|
37
|
-
propInactiveColor !== undefined ? propInactiveColor : colors.
|
|
45
|
+
propInactiveColor !== undefined ? propInactiveColor : colors.gray500;
|
|
38
46
|
let currentValue = Math.min(Math.max(value, 0), max);
|
|
39
47
|
|
|
40
48
|
let starIcons = []; // stores the SPAN elements of each star
|
|
@@ -43,6 +51,9 @@ export const Rating = (props) => {
|
|
|
43
51
|
let unsubscribeTheme = null;
|
|
44
52
|
let touchActive = false; // to avoid conflicts with mouse
|
|
45
53
|
|
|
54
|
+
/** The score as one sentence, which is what the group is named by. */
|
|
55
|
+
const groupLabel = () => `${currentValue} out of ${max}`;
|
|
56
|
+
|
|
46
57
|
// Update all stars according to currentValue
|
|
47
58
|
const updateStars = () => {
|
|
48
59
|
for (let i = 0; i < max; i++) {
|
|
@@ -77,6 +88,20 @@ export const Rating = (props) => {
|
|
|
77
88
|
if (valueTextRef && showValue) {
|
|
78
89
|
valueTextRef.update({ text: currentValue.toFixed(1) });
|
|
79
90
|
}
|
|
91
|
+
|
|
92
|
+
// The announced state and the roving stop follow the drawn stars, so a score
|
|
93
|
+
// set from the keyboard, from a pointer or by `rating.value = 3` is heard the
|
|
94
|
+
// same way.
|
|
95
|
+
if (!readOnly) {
|
|
96
|
+
const selected = Math.round(currentValue);
|
|
97
|
+
for (let i = 0; i < max; i++) {
|
|
98
|
+
const wrapper = starWrappers[i];
|
|
99
|
+
if (!wrapper) continue;
|
|
100
|
+
wrapper.setAttribute("aria-checked", String(selected === i + 1));
|
|
101
|
+
wrapper.tabIndex = tabIndexFor(i);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (starsContainer) starsContainer.setAttribute("aria-label", groupLabel());
|
|
80
105
|
};
|
|
81
106
|
|
|
82
107
|
// Change the actual value (persistent)
|
|
@@ -111,6 +136,33 @@ export const Rating = (props) => {
|
|
|
111
136
|
};
|
|
112
137
|
|
|
113
138
|
// ========== STAR CREATION ==========
|
|
139
|
+
/**
|
|
140
|
+
* The star a keyboard stop sits on: the one carrying the score, or the first
|
|
141
|
+
* when nothing is chosen yet. A group where every star is a stop makes Tab
|
|
142
|
+
* walk five times through one control.
|
|
143
|
+
*/
|
|
144
|
+
const tabIndexFor = (i) =>
|
|
145
|
+
i === (currentValue > 0 ? Math.min(Math.round(currentValue), max) - 1 : 0)
|
|
146
|
+
? 0
|
|
147
|
+
: -1;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Pick star `i + 1`. With `allowHalf`, pressing the star that is already the
|
|
151
|
+
* whole score turns it into the half between it and the last one.
|
|
152
|
+
*/
|
|
153
|
+
const pickStar = (i) => {
|
|
154
|
+
let newValue = i + 1;
|
|
155
|
+
if (allowHalf && currentValue === newValue) newValue -= 0.5;
|
|
156
|
+
setValue(newValue);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** Move the keyboard and the score to star `index`. */
|
|
160
|
+
const focusStar = (index) => {
|
|
161
|
+
const clamped = Math.min(Math.max(index, 0), max - 1);
|
|
162
|
+
starWrappers[clamped]?.focus();
|
|
163
|
+
setValue(clamped + 1);
|
|
164
|
+
};
|
|
165
|
+
|
|
114
166
|
for (let i = 0; i < max; i++) {
|
|
115
167
|
const icon = Icon({
|
|
116
168
|
name: iconInactive,
|
|
@@ -130,6 +182,45 @@ export const Rating = (props) => {
|
|
|
130
182
|
...(readOnly
|
|
131
183
|
? { padding: 4 }
|
|
132
184
|
: { minWidth: TAP_MINIMUM, minHeight: TAP_MINIMUM }),
|
|
185
|
+
// A score shown as filled stars is the only place that state lives: the
|
|
186
|
+
// glyph is decorative and the number is not rendered when `showValue` is
|
|
187
|
+
// off. Each star is one choice of a group, which is what `radio` means.
|
|
188
|
+
...(readOnly
|
|
189
|
+
? {}
|
|
190
|
+
: {
|
|
191
|
+
role: "radio",
|
|
192
|
+
"aria-label": `Star ${i + 1} of ${max}`,
|
|
193
|
+
"aria-checked": "false",
|
|
194
|
+
tabIndex: tabIndexFor(i),
|
|
195
|
+
// The press is a prop, so the engine turns Enter and Space into this
|
|
196
|
+
// click instead of leaving a star only a finger can light up. The
|
|
197
|
+
// gesture stays the widget's own `scale(1.15)` above: the engine's
|
|
198
|
+
// press scale would overwrite it on the same frame.
|
|
199
|
+
disableTransform: true,
|
|
200
|
+
onclick: () => pickStar(i),
|
|
201
|
+
onKeyDown: (e) => {
|
|
202
|
+
switch (e.key) {
|
|
203
|
+
case "ArrowRight":
|
|
204
|
+
case "ArrowDown":
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
focusStar(i + 1);
|
|
207
|
+
break;
|
|
208
|
+
case "ArrowLeft":
|
|
209
|
+
case "ArrowUp":
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
focusStar(i - 1);
|
|
212
|
+
break;
|
|
213
|
+
case "Home":
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
focusStar(0);
|
|
216
|
+
break;
|
|
217
|
+
case "End":
|
|
218
|
+
e.preventDefault();
|
|
219
|
+
focusStar(max - 1);
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
133
224
|
});
|
|
134
225
|
|
|
135
226
|
starIcons.push(icon);
|
|
@@ -147,17 +238,9 @@ export const Rating = (props) => {
|
|
|
147
238
|
wrapper.style.transform = "scale(1)";
|
|
148
239
|
restoreFromPreview();
|
|
149
240
|
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (allowHalf) {
|
|
154
|
-
if (currentValue === newValue) newValue = newValue - 0.5;
|
|
155
|
-
else if (currentValue === newValue - 0.5) newValue = newValue;
|
|
156
|
-
else newValue = newValue;
|
|
157
|
-
}
|
|
158
|
-
setValue(newValue);
|
|
159
|
-
});
|
|
160
|
-
|
|
241
|
+
// The click itself is the `onclick` prop above — that is what lets the
|
|
242
|
+
// engine reach this star without a pointer, and a listener here would run
|
|
243
|
+
// the same pick twice.
|
|
161
244
|
// ----- TOUCH EVENTS -----
|
|
162
245
|
wrapper.addEventListener("touchstart", (e) => {
|
|
163
246
|
e.preventDefault();
|
|
@@ -207,7 +290,7 @@ export const Rating = (props) => {
|
|
|
207
290
|
}
|
|
208
291
|
touchActive = false;
|
|
209
292
|
});
|
|
210
|
-
wrapper.addEventListener("touchcancel", (
|
|
293
|
+
wrapper.addEventListener("touchcancel", () => {
|
|
211
294
|
wrapper.style.transform = "scale(1)";
|
|
212
295
|
restoreFromPreview();
|
|
213
296
|
touchActive = false;
|
|
@@ -221,6 +304,11 @@ export const Rating = (props) => {
|
|
|
221
304
|
gap: gap,
|
|
222
305
|
flexWrap: "wrap",
|
|
223
306
|
children: starWrappers,
|
|
307
|
+
// One control, not five stops: the group is what a reader lands on and the
|
|
308
|
+
// score is what they hear there. A read-only rating has no choice to make,
|
|
309
|
+
// so it is a picture of a number rather than a group of radios.
|
|
310
|
+
role: readOnly ? "img" : "radiogroup",
|
|
311
|
+
"aria-label": groupLabel(),
|
|
224
312
|
});
|
|
225
313
|
|
|
226
314
|
// Ensure initial state
|
|
@@ -253,8 +341,8 @@ export const Rating = (props) => {
|
|
|
253
341
|
// Dynamic theme
|
|
254
342
|
if (propActiveColor === undefined || propInactiveColor === undefined) {
|
|
255
343
|
unsubscribeTheme = subscribeTheme(() => {
|
|
256
|
-
if (propActiveColor === undefined) activeColor = colors.
|
|
257
|
-
if (propInactiveColor === undefined) inactiveColor = colors.
|
|
344
|
+
if (propActiveColor === undefined) activeColor = colors.warningStrong;
|
|
345
|
+
if (propInactiveColor === undefined) inactiveColor = colors.gray500;
|
|
258
346
|
updateStars();
|
|
259
347
|
});
|
|
260
348
|
}
|
package/src/widgets/Skeleton.js
CHANGED
|
@@ -21,13 +21,11 @@ export const Skeleton = (props) => {
|
|
|
21
21
|
// Color props for consistency
|
|
22
22
|
bgColor = colors.gray200,
|
|
23
23
|
highlightColor = colors.gray100,
|
|
24
|
-
shimmerColor
|
|
24
|
+
shimmerColor,
|
|
25
25
|
|
|
26
26
|
// Animation timing
|
|
27
27
|
pulseDuration = "1.5s",
|
|
28
28
|
waveDuration = "1.5s",
|
|
29
|
-
|
|
30
|
-
...rest
|
|
31
29
|
} = props;
|
|
32
30
|
|
|
33
31
|
// Default dimensions based on variant
|
|
@@ -70,9 +68,15 @@ export const Skeleton = (props) => {
|
|
|
70
68
|
};
|
|
71
69
|
}
|
|
72
70
|
if (animation === "wave") {
|
|
71
|
+
// `shimmerColor` names the band the wave carries; `highlightColor` is the
|
|
72
|
+
// token it falls back to, so either prop alone paints the shimmer.
|
|
73
|
+
const band = shimmerColor ?? highlightColor;
|
|
73
74
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
// `gradient` and `bgSize` are the registered names for `background` and
|
|
76
|
+
// `background-size`. A raw CSS name still paints — the factory falls back
|
|
77
|
+
// to writing it as an inline style — but warns on every placeholder built.
|
|
78
|
+
gradient: `linear-gradient(90deg, ${bgColor} 25%, ${band} 50%, ${bgColor} 75%)`,
|
|
79
|
+
bgSize: "200% 100%",
|
|
76
80
|
animation: `skeleton-wave ${waveDuration} ease-in-out infinite`,
|
|
77
81
|
};
|
|
78
82
|
}
|
package/src/widgets/Slider.js
CHANGED
|
@@ -7,6 +7,7 @@ import { WidgetFactory } from "../widget-factory/index.js";
|
|
|
7
7
|
import { colors } from "../utils/themes.js";
|
|
8
8
|
import { composeUpdate } from "../utils/composeUpdate.js";
|
|
9
9
|
import { injectKeyframes } from "../utils/styleInjector.js";
|
|
10
|
+
import { markMotionLoop } from "../animations/motion.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Props owned by the Slider's closure state. Routed to the widget's `update`
|
|
@@ -24,6 +25,12 @@ const SLIDER_PROP_KEYS = [
|
|
|
24
25
|
"onChanged", "onPress", "onChangeEnd",
|
|
25
26
|
];
|
|
26
27
|
|
|
28
|
+
// The fill eases for a value that changes *by itself* — a key press, `setValue`,
|
|
29
|
+
// an `update({ value })`. During a gesture it is switched off (see `onStart`):
|
|
30
|
+
// an ease that restarts on every pointer move never catches up, so the bar spends
|
|
31
|
+
// the whole drag chasing the thumb instead of moving with it.
|
|
32
|
+
const FILL_EASE = "width 0.1s ease, height 0.1s ease";
|
|
33
|
+
|
|
27
34
|
export const Slider = (props) => {
|
|
28
35
|
let {
|
|
29
36
|
// Basic values
|
|
@@ -77,9 +84,11 @@ export const Slider = (props) => {
|
|
|
77
84
|
let displayPercent = inverted ? 1 - rawPercent : rawPercent;
|
|
78
85
|
|
|
79
86
|
let isHorizontal = orientation === "horizontal";
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
// `height` is the rail's thickness and `width` its length in both orientations:
|
|
88
|
+
// a vertical slider is the same box turned 90°, so its vertical extent comes from
|
|
89
|
+
// `width` (the root below) while the rail stays `height` thin. Reading the
|
|
90
|
+
// thickness from `width` here painted a 160-unit rail 160 units *wide*.
|
|
91
|
+
let trackLength = toREM(height);
|
|
83
92
|
let thumbSizeNum = typeof thumbSize === "number" ? thumbSize : 24;
|
|
84
93
|
|
|
85
94
|
// Positioning retries while the track still measures 0 (hidden parent, not laid
|
|
@@ -102,6 +111,16 @@ export const Slider = (props) => {
|
|
|
102
111
|
const container = WidgetFactory({
|
|
103
112
|
tag: "div",
|
|
104
113
|
widgetName: "Slider",
|
|
114
|
+
// The root is the control: what a reader meets, what Tab stops on and what
|
|
115
|
+
// answers the arrow keys. The thumb is the picture of the value, not a second
|
|
116
|
+
// stop — and one element is where `updateUI` can keep `aria-valuenow` honest.
|
|
117
|
+
role: "slider",
|
|
118
|
+
tabIndex: disabled ? -1 : 0,
|
|
119
|
+
"aria-valuemin": String(currentMin),
|
|
120
|
+
"aria-valuemax": String(currentMax),
|
|
121
|
+
"aria-valuenow": String(currentValue),
|
|
122
|
+
"aria-orientation": isHorizontal ? "horizontal" : "vertical",
|
|
123
|
+
"aria-disabled": String(Boolean(disabled)),
|
|
105
124
|
width: isHorizontal ? width : "auto",
|
|
106
125
|
height: !isHorizontal ? width : "auto",
|
|
107
126
|
opacity: disabled ? 0.5 : 1,
|
|
@@ -170,7 +189,7 @@ export const Slider = (props) => {
|
|
|
170
189
|
position: "absolute",
|
|
171
190
|
backgroundColor: color,
|
|
172
191
|
borderRadius: trackLength,
|
|
173
|
-
transition: disabled ? "none" :
|
|
192
|
+
transition: disabled ? "none" : FILL_EASE,
|
|
174
193
|
...(isHorizontal
|
|
175
194
|
? {
|
|
176
195
|
width: `${displayPercent * 100}%`,
|
|
@@ -189,10 +208,15 @@ export const Slider = (props) => {
|
|
|
189
208
|
if (striped) {
|
|
190
209
|
fill.style.backgroundImage = `repeating-linear-gradient(45deg, ${stripeColor} 0px, ${stripeColor} 8px, transparent 8px, transparent 16px)`;
|
|
191
210
|
fill.style.backgroundSize = "16px 16px";
|
|
192
|
-
if (animatedStripes)
|
|
211
|
+
if (animatedStripes) {
|
|
193
212
|
fill.style.animation = "slider-stripes 0.5s linear infinite";
|
|
213
|
+
markMotionLoop(fill);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (glow) {
|
|
217
|
+
fill.style.animation = "slider-glow 1.5s ease-in-out infinite";
|
|
218
|
+
markMotionLoop(fill);
|
|
194
219
|
}
|
|
195
|
-
if (glow) fill.style.animation = "slider-glow 1.5s ease-in-out infinite";
|
|
196
220
|
track.appendChild(fill);
|
|
197
221
|
|
|
198
222
|
// ---------- Thumb (knob) ----------
|
|
@@ -288,7 +312,7 @@ export const Slider = (props) => {
|
|
|
288
312
|
const label = WidgetFactory({
|
|
289
313
|
tag: "span",
|
|
290
314
|
textContent: mark.label,
|
|
291
|
-
fontSize: "
|
|
315
|
+
fontSize: "12px",
|
|
292
316
|
color: colors.textSecondary,
|
|
293
317
|
position: "absolute",
|
|
294
318
|
...(isHorizontal
|
|
@@ -329,6 +353,13 @@ export const Slider = (props) => {
|
|
|
329
353
|
rawPercent = (currentValue - currentMin) / (currentMax - currentMin);
|
|
330
354
|
displayPercent = inverted ? 1 - rawPercent : rawPercent;
|
|
331
355
|
|
|
356
|
+
// The number a reader hears is the one the thumb is drawn at, so both are
|
|
357
|
+
// written from the same function: a state that only the drag path updates is a
|
|
358
|
+
// state that goes stale the first time `setValue` or `update({ min })` runs.
|
|
359
|
+
container.setAttribute("aria-valuenow", String(currentValue));
|
|
360
|
+
container.setAttribute("aria-valuemin", String(currentMin));
|
|
361
|
+
container.setAttribute("aria-valuemax", String(currentMax));
|
|
362
|
+
|
|
332
363
|
if (isHorizontal) {
|
|
333
364
|
fill.style.width = `${displayPercent * 100}%`;
|
|
334
365
|
} else {
|
|
@@ -433,6 +464,7 @@ export const Slider = (props) => {
|
|
|
433
464
|
document.removeEventListener("touchmove", onMove);
|
|
434
465
|
document.removeEventListener("touchend", onEnd);
|
|
435
466
|
thumb.style.cursor = "grab";
|
|
467
|
+
fill.style.transition = disabled ? "none" : FILL_EASE;
|
|
436
468
|
if (wrapper.style) wrapper.style.touchAction = "";
|
|
437
469
|
};
|
|
438
470
|
|
|
@@ -441,6 +473,9 @@ export const Slider = (props) => {
|
|
|
441
473
|
e.preventDefault();
|
|
442
474
|
dragging = true;
|
|
443
475
|
hasMoved = false;
|
|
476
|
+
// Before the first value write: with the ease still on, the jump to the press
|
|
477
|
+
// point animates and every later move restarts that animation.
|
|
478
|
+
fill.style.transition = "none";
|
|
444
479
|
thumb.style.cursor = "grabbing";
|
|
445
480
|
if (e.pointerId !== undefined) {
|
|
446
481
|
currentPointerId = e.pointerId;
|
|
@@ -471,6 +506,50 @@ export const Slider = (props) => {
|
|
|
471
506
|
thumb.addEventListener("mousedown", onStart);
|
|
472
507
|
thumb.addEventListener("touchstart", onStart);
|
|
473
508
|
|
|
509
|
+
// ---------- Keyboard ----------
|
|
510
|
+
// `role="slider"` is a promise of arrow keys: a handle you can drag but not nudge
|
|
511
|
+
// is announced to a keyboard user as something they cannot move. One step for an
|
|
512
|
+
// arrow, a coarse one for Page keys, the ends for Home and End — the same set a
|
|
513
|
+
// native range input answers.
|
|
514
|
+
const bigStep = () =>
|
|
515
|
+
Math.max(currentStep, (currentMax - currentMin) / 10);
|
|
516
|
+
|
|
517
|
+
const onKeyDown = (e) => {
|
|
518
|
+
if (disabled) return;
|
|
519
|
+
let next = null;
|
|
520
|
+
switch (e.key) {
|
|
521
|
+
case "ArrowRight":
|
|
522
|
+
case "ArrowUp":
|
|
523
|
+
next = currentValue + currentStep;
|
|
524
|
+
break;
|
|
525
|
+
case "ArrowLeft":
|
|
526
|
+
case "ArrowDown":
|
|
527
|
+
next = currentValue - currentStep;
|
|
528
|
+
break;
|
|
529
|
+
case "PageUp":
|
|
530
|
+
next = currentValue + bigStep();
|
|
531
|
+
break;
|
|
532
|
+
case "PageDown":
|
|
533
|
+
next = currentValue - bigStep();
|
|
534
|
+
break;
|
|
535
|
+
case "Home":
|
|
536
|
+
next = currentMin;
|
|
537
|
+
break;
|
|
538
|
+
case "End":
|
|
539
|
+
next = currentMax;
|
|
540
|
+
break;
|
|
541
|
+
default:
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
// Without this, the arrows on a vertical slider scroll the page instead of
|
|
545
|
+
// moving the value.
|
|
546
|
+
e.preventDefault();
|
|
547
|
+
// `triggerEnd` — the same sentence a released drag says, so an app that
|
|
548
|
+
// commits on `onChangeEnd` commits on a key too.
|
|
549
|
+
setValue(next, true, true);
|
|
550
|
+
};
|
|
551
|
+
container.addEventListener("keydown", onKeyDown);
|
|
552
|
+
|
|
474
553
|
// ---------- ResizeObserver to reposition thumb ----------
|
|
475
554
|
let resizeObserver = null;
|
|
476
555
|
if (typeof ResizeObserver !== "undefined") {
|
|
@@ -542,6 +621,7 @@ export const Slider = (props) => {
|
|
|
542
621
|
track.removeEventListener("touchstart", onStart);
|
|
543
622
|
thumb.removeEventListener("mousedown", onStart);
|
|
544
623
|
thumb.removeEventListener("touchstart", onStart);
|
|
624
|
+
container.removeEventListener("keydown", onKeyDown);
|
|
545
625
|
document.removeEventListener("mousemove", onMove);
|
|
546
626
|
document.removeEventListener("mouseup", onEnd);
|
|
547
627
|
document.removeEventListener("touchmove", onMove);
|
package/src/widgets/SnackBar.js
CHANGED
|
@@ -129,9 +129,15 @@ export function SnackBar(options) {
|
|
|
129
129
|
const show = () => {
|
|
130
130
|
if (closed) return;
|
|
131
131
|
if (rafId !== null) cancelAnimationFrame(rafId);
|
|
132
|
+
// Empty it here, fill it on the frame after: that gap is the whole reason the
|
|
133
|
+
// toast is heard. A live region that arrives with its words already inside is
|
|
134
|
+
// a new landmark, not a change, and a reader who is already on the page is
|
|
135
|
+
// told nothing. Clearing first also makes a second `show()` speak again.
|
|
136
|
+
announcer.textContent = "";
|
|
132
137
|
rafId = requestAnimationFrame(() => {
|
|
133
138
|
rafId = null;
|
|
134
139
|
if (closed) return;
|
|
140
|
+
announcer.textContent = message;
|
|
135
141
|
snackbar.style.opacity = "1";
|
|
136
142
|
snackbar.style.transform = "translateY(0)";
|
|
137
143
|
});
|
|
@@ -156,6 +162,7 @@ export function SnackBar(options) {
|
|
|
156
162
|
: `translateY(calc(-100% - ${toREM(margin)}))`;
|
|
157
163
|
setTimeout(() => {
|
|
158
164
|
if (snackbar.parentNode) snackbar.parentNode.removeChild(snackbar);
|
|
165
|
+
if (announcer.parentNode) announcer.parentNode.removeChild(announcer);
|
|
159
166
|
if (onClose) onClose();
|
|
160
167
|
}, animationDuration);
|
|
161
168
|
};
|
|
@@ -164,11 +171,29 @@ export function SnackBar(options) {
|
|
|
164
171
|
const messageEl = WidgetFactory({
|
|
165
172
|
tag: "span",
|
|
166
173
|
flex: 1,
|
|
167
|
-
text: message,
|
|
168
174
|
wordBreak: "break-word",
|
|
175
|
+
text: message,
|
|
169
176
|
});
|
|
170
177
|
snackbar.appendChild(messageEl);
|
|
171
178
|
|
|
179
|
+
// The thing that speaks is not the bar. An error or a warning interrupts the
|
|
180
|
+
// sentence a reader is in the middle of; a confirmation waits for it to end.
|
|
181
|
+
// `opacity: 0` and one unit of size, because a `display: none` region is not
|
|
182
|
+
// read at all, and the bar's own animation is what the eye follows.
|
|
183
|
+
const announcer = WidgetFactory({
|
|
184
|
+
tag: "div",
|
|
185
|
+
role: type === "error" || type === "warning" ? "alert" : "status",
|
|
186
|
+
"aria-live": type === "error" || type === "warning" ? "assertive" : "polite",
|
|
187
|
+
position: "fixed",
|
|
188
|
+
bottom: 0,
|
|
189
|
+
left: 0,
|
|
190
|
+
width: 1,
|
|
191
|
+
height: 1,
|
|
192
|
+
opacity: 0,
|
|
193
|
+
overflow: "hidden",
|
|
194
|
+
pointerEvents: "none",
|
|
195
|
+
});
|
|
196
|
+
|
|
172
197
|
// Action button
|
|
173
198
|
let actionBtn = null;
|
|
174
199
|
if (action) {
|
|
@@ -214,6 +239,9 @@ export function SnackBar(options) {
|
|
|
214
239
|
opacity: 0.7,
|
|
215
240
|
transition: "opacity 0.2s ease",
|
|
216
241
|
textContent: "✕",
|
|
242
|
+
// The glyph is not a name. A reader hears "multiplication sign" and has to
|
|
243
|
+
// guess that this is the thing that dismisses the message.
|
|
244
|
+
"aria-label": "Dismiss notification",
|
|
217
245
|
onclick: close,
|
|
218
246
|
});
|
|
219
247
|
closeBtn.addEventListener("mouseenter", () => {
|
|
@@ -227,6 +255,7 @@ export function SnackBar(options) {
|
|
|
227
255
|
|
|
228
256
|
// Add to DOM
|
|
229
257
|
document.body.appendChild(snackbar);
|
|
258
|
+
document.body.appendChild(announcer);
|
|
230
259
|
// Force reflow
|
|
231
260
|
snackbar.offsetHeight;
|
|
232
261
|
|
package/src/widgets/Stack.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Stack.js
|
|
3
|
-
* @description Positioning container
|
|
4
|
-
*
|
|
5
|
-
* `alignment`
|
|
3
|
+
* @description Positioning container that paints its children in the same box.
|
|
4
|
+
* The first child stays in the flow and gives the stack its size; every child
|
|
5
|
+
* after it floats over that box, anchored by `alignment` or by its own edges.
|
|
6
6
|
*/
|
|
7
7
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
8
|
+
import { ensureStackLayout } from "../utils/stackLayout.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Nine-point grid → [vertical anchor, horizontal anchor].
|
|
11
12
|
* `alignment` turns the stack into a column flex container, so `justifyContent`
|
|
12
13
|
* carries the top/center/bottom anchor and `alignItems` the left/center/right one.
|
|
14
|
+
* Those same two rules are what places a floating child: an absolutely
|
|
15
|
+
* positioned child of a flex container takes its static position from them, so
|
|
16
|
+
* one map anchors both the in-flow child and every layer above it.
|
|
13
17
|
*/
|
|
14
18
|
const ALIGNMENTS = {
|
|
15
19
|
"top-left": ["flex-start", "flex-start"],
|
|
@@ -37,26 +41,34 @@ export const Stack = (props = {}) => {
|
|
|
37
41
|
...rest
|
|
38
42
|
} = props;
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
// The corner a child with no edges of its own lands on. Flet's stack puts the
|
|
45
|
+
// first control in the top-left, and a stack that anchored nothing would place
|
|
46
|
+
// its floating children wherever the flow would have put them — which is the
|
|
47
|
+
// bug this widget is meant to fix, not a default.
|
|
48
|
+
const preset = ALIGNMENTS[alignment] ?? ALIGNMENTS["top-left"];
|
|
41
49
|
|
|
42
50
|
// The preset sits before `...rest`, so an explicit `flexDirection`,
|
|
43
51
|
// `justifyContent` or `alignItems` from the caller always overrides it;
|
|
44
52
|
// `alignment` only fills the axes nobody claimed.
|
|
45
|
-
|
|
53
|
+
const stack = WidgetFactory({
|
|
46
54
|
tag: "div",
|
|
47
55
|
widgetName: "Stack",
|
|
48
56
|
position: position,
|
|
49
|
-
display: display ??
|
|
57
|
+
display: display ?? "flex",
|
|
50
58
|
boxSizing: boxSizing,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
justifyContent: preset[0],
|
|
55
|
-
alignItems: preset[1],
|
|
56
|
-
}
|
|
57
|
-
: null),
|
|
59
|
+
flexDirection: "column",
|
|
60
|
+
justifyContent: preset[0],
|
|
61
|
+
alignItems: preset[1],
|
|
58
62
|
...rest,
|
|
59
63
|
});
|
|
64
|
+
|
|
65
|
+
// The marker is what `stackLayout.js` floats every child but the first against.
|
|
66
|
+
// It goes on the root, not on the children, because `update({ children })`
|
|
67
|
+
// replaces the nodes and would take any per-node flag with it.
|
|
68
|
+
ensureStackLayout();
|
|
69
|
+
stack.setAttribute("data-fb-stack", "");
|
|
70
|
+
|
|
71
|
+
return stack;
|
|
60
72
|
};
|
|
61
73
|
|
|
62
74
|
export default Stack;
|