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
package/src/widgets/Stepper.js
CHANGED
|
@@ -13,6 +13,11 @@ import { colors } from "../utils/themes.js";
|
|
|
13
13
|
|
|
14
14
|
import { toREM } from "../utils/units.js";
|
|
15
15
|
|
|
16
|
+
// Nodes already reported by the shared-content warning below. A screen that
|
|
17
|
+
// reuses one `steps` array steals it once per stepper per render, and a wall of
|
|
18
|
+
// identical warnings is a warning nobody reads.
|
|
19
|
+
const warnedContent = new WeakSet();
|
|
20
|
+
|
|
16
21
|
export const Stepper = (props) => {
|
|
17
22
|
const {
|
|
18
23
|
steps = [],
|
|
@@ -41,6 +46,9 @@ export const Stepper = (props) => {
|
|
|
41
46
|
} = props;
|
|
42
47
|
|
|
43
48
|
let currentStep = Math.min(Math.max(activeStep, 0), steps.length - 1);
|
|
49
|
+
// The two orientations share the marker, the state and the press, and disagree
|
|
50
|
+
// about everything else — so the branch is decided once, here.
|
|
51
|
+
const vertical = orientation !== "horizontal";
|
|
44
52
|
let stepsContainer = null;
|
|
45
53
|
let contentContainer = null;
|
|
46
54
|
let navContainer = null;
|
|
@@ -92,7 +100,7 @@ export const Stepper = (props) => {
|
|
|
92
100
|
justifyContent: "center",
|
|
93
101
|
alignItems: "center",
|
|
94
102
|
child: isCompleted
|
|
95
|
-
? Icon({ name: "check", size: 18, color:
|
|
103
|
+
? Icon({ name: "check", size: 18, color: colors.onSecondary })
|
|
96
104
|
: Text({
|
|
97
105
|
text: String(idx + 1),
|
|
98
106
|
size: 14,
|
|
@@ -117,59 +125,104 @@ export const Stepper = (props) => {
|
|
|
117
125
|
});
|
|
118
126
|
}
|
|
119
127
|
|
|
120
|
-
//
|
|
128
|
+
// The label, in the ink the stage decides, placed where the orientation
|
|
129
|
+
// puts it: under the marker in the horizontal track, beside it in the
|
|
130
|
+
// vertical one. Both branches used to draw their own, so a vertical step
|
|
131
|
+
// read "Account" twice — once under the circle and once stranded at the far
|
|
132
|
+
// edge of a row the marker column had already stretched across.
|
|
133
|
+
const labelColor = isActive ? colors.primary : colors.textSecondary;
|
|
121
134
|
const label = showLabels
|
|
122
|
-
?
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
? vertical
|
|
136
|
+
? Text({ text: step.label, size: 14, color: labelColor })
|
|
137
|
+
: Text({
|
|
138
|
+
text: step.label,
|
|
139
|
+
size: 12,
|
|
140
|
+
color: labelColor,
|
|
141
|
+
marginTop: 4,
|
|
142
|
+
align: "center",
|
|
143
|
+
})
|
|
129
144
|
: null;
|
|
130
145
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
// The number, the check and the accent carry the stage, and all three are
|
|
147
|
+
// drawn: the check is a decorative glyph and the label alone does not say
|
|
148
|
+
// whether the step is behind, ahead or where you are. The state goes in
|
|
149
|
+
// the name, and `aria-current` marks the step you are on. The press lives
|
|
150
|
+
// on this box, so the whole strip answers — not only the circle.
|
|
151
|
+
const press = {
|
|
135
152
|
cursor: "pointer",
|
|
153
|
+
"aria-label": `${step.label || `Step ${idx + 1}`} (${
|
|
154
|
+
isCompleted ? "completed" : isActive ? "current" : "upcoming"
|
|
155
|
+
})`,
|
|
156
|
+
"aria-current": isActive ? "step" : undefined,
|
|
136
157
|
onclick: () => goToStep(idx),
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
stepsContainer.appendChild(
|
|
161
|
+
vertical
|
|
162
|
+
? Row({
|
|
163
|
+
alignItems: "center",
|
|
164
|
+
gap: 12,
|
|
165
|
+
width: "100%",
|
|
166
|
+
...press,
|
|
167
|
+
children: [indicator, label].filter(Boolean),
|
|
168
|
+
})
|
|
169
|
+
: Column({
|
|
170
|
+
alignItems: "center",
|
|
171
|
+
flex: needsWrap ? "auto" : 1,
|
|
172
|
+
minWidth: needsWrap ? "80px" : "0",
|
|
173
|
+
...press,
|
|
174
|
+
children: [indicator, label].filter(Boolean),
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Only the horizontal track links its markers; a vertical list is already
|
|
179
|
+
// read in order, and the Column's own gap is the separation.
|
|
180
|
+
if (!vertical && !isLast) {
|
|
181
|
+
stepsContainer.appendChild(
|
|
182
|
+
needsWrap
|
|
183
|
+
? Container({ width: 8 })
|
|
184
|
+
: Container({
|
|
185
|
+
flex: 1,
|
|
186
|
+
height: "2px",
|
|
187
|
+
minWidth: "10px",
|
|
188
|
+
backgroundColor:
|
|
189
|
+
idx < currentStep ? colors.success : colors.gray300,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
162
192
|
}
|
|
163
193
|
});
|
|
164
194
|
|
|
165
|
-
// Update content
|
|
195
|
+
// Update content. One place owns the slot, and it only rewrites it when the
|
|
196
|
+
// node already there is not this step's: `appendChild` of the node that is
|
|
197
|
+
// already the child is a detach and a re-attach, which throws away the
|
|
198
|
+
// content's layout, the caret of a field being typed in, and any transition
|
|
199
|
+
// it was mid-way through.
|
|
166
200
|
if (contentContainer) {
|
|
167
|
-
while (contentContainer.firstChild) {
|
|
168
|
-
contentContainer.removeChild(contentContainer.firstChild);
|
|
169
|
-
}
|
|
170
201
|
const stepContent = steps[currentStep]?.content;
|
|
171
|
-
if (stepContent) {
|
|
172
|
-
contentContainer.
|
|
202
|
+
if (contentContainer.firstChild !== stepContent) {
|
|
203
|
+
while (contentContainer.firstChild) {
|
|
204
|
+
contentContainer.removeChild(contentContainer.firstChild);
|
|
205
|
+
}
|
|
206
|
+
if (stepContent) {
|
|
207
|
+
// A DOM node has one parent, so the same `content` cannot show in two
|
|
208
|
+
// steppers: the second `appendChild` moves it and leaves the first with
|
|
209
|
+
// an empty slot that reads as "the widget is invisible". Nothing else
|
|
210
|
+
// says so, and the stepper that lost it looks like it never rendered.
|
|
211
|
+
if (
|
|
212
|
+
stepContent.parentElement &&
|
|
213
|
+
stepContent.parentElement !== contentContainer &&
|
|
214
|
+
!warnedContent.has(stepContent)
|
|
215
|
+
) {
|
|
216
|
+
warnedContent.add(stepContent);
|
|
217
|
+
console.warn(
|
|
218
|
+
"Stepper: this step's content is already mounted in another tree, " +
|
|
219
|
+
"so that stepper is now empty — a widget node has one parent. Build " +
|
|
220
|
+
"the steps array where the Stepper is built; do not share one array " +
|
|
221
|
+
"between steppers.",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
contentContainer.appendChild(stepContent);
|
|
225
|
+
}
|
|
173
226
|
}
|
|
174
227
|
}
|
|
175
228
|
|
|
@@ -189,24 +242,17 @@ export const Stepper = (props) => {
|
|
|
189
242
|
}),
|
|
190
243
|
);
|
|
191
244
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
Button({
|
|
204
|
-
text: nextLabel,
|
|
205
|
-
variant: "filled",
|
|
206
|
-
onPress: next,
|
|
207
|
-
}),
|
|
208
|
-
);
|
|
209
|
-
}
|
|
245
|
+
// One filled call, not a filled call per branch: the last step swaps the
|
|
246
|
+
// label, the plate and the handler, and never paints two primaries.
|
|
247
|
+
const isLast = currentStep === steps.length - 1;
|
|
248
|
+
buttons.push(
|
|
249
|
+
Button({
|
|
250
|
+
text: isLast ? finishLabel : nextLabel,
|
|
251
|
+
variant: "filled",
|
|
252
|
+
bgColor: isLast ? colors.successPlate : colors.primary,
|
|
253
|
+
onPress: isLast ? finish : next,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
210
256
|
|
|
211
257
|
const navRow = Row({
|
|
212
258
|
justifyContent: "flex-end",
|
|
@@ -271,9 +317,11 @@ export const Stepper = (props) => {
|
|
|
271
317
|
|
|
272
318
|
stepsWrapper.appendChild(stepsContainer);
|
|
273
319
|
|
|
274
|
-
// Content container
|
|
320
|
+
// Content container — built empty. `updateUI()` below is the only writer of
|
|
321
|
+
// this slot, so the first paint goes through the same code as every
|
|
322
|
+
// navigation, instead of mounting the node here and detaching it a line later
|
|
323
|
+
// to mount the same node again.
|
|
275
324
|
contentContainer = Container({
|
|
276
|
-
child: steps[currentStep]?.content || null,
|
|
277
325
|
width: "100%",
|
|
278
326
|
overflow: "auto",
|
|
279
327
|
});
|
package/src/widgets/Switch.js
CHANGED
|
@@ -43,6 +43,7 @@ export const Switch = (props) => {
|
|
|
43
43
|
let knobRef = null;
|
|
44
44
|
|
|
45
45
|
const updateUI = (newValue) => {
|
|
46
|
+
if (switchContainer) switchContainer.setAttribute("aria-checked", String(Boolean(newValue)));
|
|
46
47
|
if (switchRef) {
|
|
47
48
|
switchRef.style.backgroundColor = newValue ? activeColor : colors.gray300;
|
|
48
49
|
}
|
|
@@ -52,10 +53,14 @@ export const Switch = (props) => {
|
|
|
52
53
|
};
|
|
53
54
|
|
|
54
55
|
// Createste knob
|
|
56
|
+
// The knob is a white plate in every theme: it sits on a track whose colour
|
|
57
|
+
// the caller picks (`activeColor`), so it cannot borrow `onPrimary` — in dark
|
|
58
|
+
// that token is page ink and the knob would vanish into the grey track.
|
|
59
|
+
// `onSecondary` is the palette's own answer for a white surface.
|
|
55
60
|
const knob = WidgetFactory({
|
|
56
61
|
width: sz.knob,
|
|
57
62
|
height: sz.knob,
|
|
58
|
-
backgroundColor:
|
|
63
|
+
backgroundColor: colors.onSecondary,
|
|
59
64
|
borderRadius: "50%",
|
|
60
65
|
transform: internalValue ? `translateX(${travel()})` : "translateX(0)",
|
|
61
66
|
transition: "transform 0.2s ease",
|
|
@@ -89,6 +94,10 @@ export const Switch = (props) => {
|
|
|
89
94
|
|
|
90
95
|
const switchContainer = WidgetFactory({
|
|
91
96
|
widgetName: "Switch",
|
|
97
|
+
// A track and a knob say "toggle" to an eye and nothing at all to a screen
|
|
98
|
+
// reader; the role names the control and `aria-checked` is its state.
|
|
99
|
+
role: "switch",
|
|
100
|
+
"aria-checked": String(Boolean(internalValue)),
|
|
92
101
|
display: "flex",
|
|
93
102
|
flexDirection: "row",
|
|
94
103
|
alignItems: "center",
|
package/src/widgets/Tooltip.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Tooltip.js
|
|
3
3
|
* @description Hover/focus tooltip rendered as a fixed-position portal on `document.body`.
|
|
4
|
+
* Hover only where a pointer can hover, and the bubble is placed inside the
|
|
5
|
+
* device's safe area.
|
|
4
6
|
*/
|
|
5
7
|
import { WidgetFactory } from "../widget-factory/index.js";
|
|
6
8
|
import { resolveReactive } from "../widget-factory/reactiveValue.js";
|
|
@@ -8,6 +10,40 @@ import { colors } from "../utils/themes.js";
|
|
|
8
10
|
|
|
9
11
|
import { toREM } from "../utils/units.js";
|
|
10
12
|
|
|
13
|
+
/** The bubble and the thing it describes have to be able to name each other. */
|
|
14
|
+
let tooltipIdSeed = 0;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The device's own edge insets, in CSS pixels.
|
|
18
|
+
*
|
|
19
|
+
* `env()` has no JavaScript getter — the only way to read it is to measure an
|
|
20
|
+
* element that uses it — and the placement below needs the number, not just the
|
|
21
|
+
* CSS: a bubble that clears the viewport by 10px still sits on the status bar of
|
|
22
|
+
* a notched phone. The probe is a child of the bubble it measures, so showing a
|
|
23
|
+
* tooltip never puts a second node in `document.body`, and it is re-read on every
|
|
24
|
+
* show because rotation moves the inset from the top edge to the sides.
|
|
25
|
+
*/
|
|
26
|
+
const makeInsetProbe = () => {
|
|
27
|
+
const probe = document.createElement("div");
|
|
28
|
+
// Out of the flow, so the bubble keeps the size its text gives it.
|
|
29
|
+
probe.style.cssText =
|
|
30
|
+
"position:absolute;top:0;left:0;width:0;height:0;visibility:hidden;pointer-events:none;" +
|
|
31
|
+
"padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px);" +
|
|
32
|
+
"padding-left:env(safe-area-inset-left,0px);padding-right:env(safe-area-inset-right,0px);";
|
|
33
|
+
return probe;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const readInsets = (probe) => {
|
|
37
|
+
if (!probe) return { top: 0, bottom: 0, left: 0, right: 0 };
|
|
38
|
+
const cs = getComputedStyle(probe);
|
|
39
|
+
return {
|
|
40
|
+
top: parseFloat(cs.paddingTop) || 0,
|
|
41
|
+
bottom: parseFloat(cs.paddingBottom) || 0,
|
|
42
|
+
left: parseFloat(cs.paddingLeft) || 0,
|
|
43
|
+
right: parseFloat(cs.paddingRight) || 0,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
11
47
|
export const Tooltip = (props) => {
|
|
12
48
|
let {
|
|
13
49
|
text,
|
|
@@ -37,8 +73,6 @@ export const Tooltip = (props) => {
|
|
|
37
73
|
borderColor = "transparent",
|
|
38
74
|
borderWidth = 0,
|
|
39
75
|
shadow = "0 2px 8px rgba(0,0,0,0.15)",
|
|
40
|
-
|
|
41
|
-
...rest
|
|
42
76
|
} = props;
|
|
43
77
|
|
|
44
78
|
if (!child) return null;
|
|
@@ -51,6 +85,8 @@ export const Tooltip = (props) => {
|
|
|
51
85
|
let timeoutId = null;
|
|
52
86
|
let isVisible = false;
|
|
53
87
|
|
|
88
|
+
const tooltipId = `fletbox-tooltip-${++tooltipIdSeed}`;
|
|
89
|
+
|
|
54
90
|
let scrollHandler = null;
|
|
55
91
|
let resizeHandler = null;
|
|
56
92
|
|
|
@@ -98,6 +134,11 @@ export const Tooltip = (props) => {
|
|
|
98
134
|
|
|
99
135
|
const tooltip = WidgetFactory({
|
|
100
136
|
tag: "div",
|
|
137
|
+
// The bubble is what `aria-describedby` on the trigger points at, so the id
|
|
138
|
+
// and the role live on the element that is actually shown and hidden — its
|
|
139
|
+
// subtree is the sentence a reader hears.
|
|
140
|
+
id: tooltipId,
|
|
141
|
+
role: "tooltip",
|
|
101
142
|
position: "fixed",
|
|
102
143
|
zIndex: zIndex,
|
|
103
144
|
opacity: 0,
|
|
@@ -106,6 +147,8 @@ export const Tooltip = (props) => {
|
|
|
106
147
|
pointerEvents: "none",
|
|
107
148
|
});
|
|
108
149
|
|
|
150
|
+
const probe = makeInsetProbe();
|
|
151
|
+
tooltip.appendChild(probe);
|
|
109
152
|
tooltip.appendChild(content);
|
|
110
153
|
if (arrow) tooltip.appendChild(arrow);
|
|
111
154
|
|
|
@@ -114,6 +157,7 @@ export const Tooltip = (props) => {
|
|
|
114
157
|
|
|
115
158
|
tooltip._content = content;
|
|
116
159
|
tooltip._arrow = arrow;
|
|
160
|
+
tooltip._probe = probe;
|
|
117
161
|
|
|
118
162
|
return tooltip;
|
|
119
163
|
};
|
|
@@ -121,10 +165,33 @@ export const Tooltip = (props) => {
|
|
|
121
165
|
const positionTooltip = (targetRect, tooltipRect) => {
|
|
122
166
|
if (!tooltipElement) return;
|
|
123
167
|
|
|
168
|
+
// The edge the bubble actually gets: the requested one when it fits, the
|
|
169
|
+
// opposite when it does not. Clamping a `top` bubble into the viewport used
|
|
170
|
+
// to park it at y=10 — over the iOS status bar, above the AppBar it
|
|
171
|
+
// describes — because "there is no room up here" and "put it at the top of
|
|
172
|
+
// the screen" are not the same instruction.
|
|
173
|
+
const MARGIN = 10;
|
|
174
|
+
const inset = readInsets(tooltipElement._probe);
|
|
175
|
+
const boxTop = inset.top + MARGIN;
|
|
176
|
+
const boxBottom = window.innerHeight - inset.bottom - MARGIN;
|
|
177
|
+
const boxLeft = inset.left + MARGIN;
|
|
178
|
+
const boxRight = window.innerWidth - inset.right - MARGIN;
|
|
179
|
+
const needed = tooltipRect.height + offset;
|
|
180
|
+
const wide = tooltipRect.width + offset;
|
|
181
|
+
const room = {
|
|
182
|
+
top: targetRect.top - boxTop,
|
|
183
|
+
bottom: boxBottom - targetRect.bottom,
|
|
184
|
+
left: targetRect.left - boxLeft,
|
|
185
|
+
right: boxRight - targetRect.right,
|
|
186
|
+
};
|
|
187
|
+
const flip = { top: "bottom", bottom: "top", left: "right", right: "left" };
|
|
188
|
+
const fits = { top: room.top >= needed, bottom: room.bottom >= needed, left: room.left >= wide, right: room.right >= wide };
|
|
189
|
+
const side = fits[position] ? position : fits[flip[position]] ? flip[position] : position;
|
|
190
|
+
|
|
124
191
|
let top, left;
|
|
125
192
|
let arrowTop, arrowLeft;
|
|
126
193
|
|
|
127
|
-
switch (
|
|
194
|
+
switch (side) {
|
|
128
195
|
case "top":
|
|
129
196
|
top = targetRect.top - tooltipRect.height - offset;
|
|
130
197
|
left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2;
|
|
@@ -175,11 +242,11 @@ export const Tooltip = (props) => {
|
|
|
175
242
|
break;
|
|
176
243
|
}
|
|
177
244
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
245
|
+
// Keep the bubble inside the safe area on every axis. `Math.max(boxTop, …)`
|
|
246
|
+
// guards the other end for a bubble taller or wider than the screen, where
|
|
247
|
+
// the two bounds cross.
|
|
248
|
+
left = Math.min(Math.max(left, boxLeft), Math.max(boxLeft, boxRight - tooltipRect.width));
|
|
249
|
+
top = Math.min(Math.max(top, boxTop), Math.max(boxTop, boxBottom - tooltipRect.height));
|
|
183
250
|
|
|
184
251
|
tooltipElement.style.top = `${top}px`;
|
|
185
252
|
tooltipElement.style.left = `${left}px`;
|
|
@@ -205,6 +272,10 @@ export const Tooltip = (props) => {
|
|
|
205
272
|
});
|
|
206
273
|
|
|
207
274
|
isVisible = true;
|
|
275
|
+
// The link is made while the bubble is up and broken while it is not: an
|
|
276
|
+
// `aria-describedby` pointing at a hidden node is a control that claims a
|
|
277
|
+
// description it is not being given.
|
|
278
|
+
child.setAttribute("aria-describedby", tooltipId);
|
|
208
279
|
|
|
209
280
|
scrollHandler = hideOnScrollOrResize;
|
|
210
281
|
resizeHandler = hideOnScrollOrResize;
|
|
@@ -223,6 +294,7 @@ export const Tooltip = (props) => {
|
|
|
223
294
|
tooltipElement.style.opacity = "0";
|
|
224
295
|
tooltipElement.style.visibility = "hidden";
|
|
225
296
|
isVisible = false;
|
|
297
|
+
child.removeAttribute("aria-describedby");
|
|
226
298
|
|
|
227
299
|
if (scrollHandler) {
|
|
228
300
|
window.removeEventListener("scroll", scrollHandler, true);
|
|
@@ -237,8 +309,19 @@ export const Tooltip = (props) => {
|
|
|
237
309
|
|
|
238
310
|
const wrappedChild = child;
|
|
239
311
|
|
|
240
|
-
|
|
241
|
-
|
|
312
|
+
// A device that cannot hover has no state a tooltip can live in. iOS WebKit
|
|
313
|
+
// fires a synthetic `mouseenter` for the first tap and never a matching
|
|
314
|
+
// `mouseleave`, so one tap pinned a bubble to the screen and a second tap on
|
|
315
|
+
// another control stacked a second one on top of it — measured: two bubbles
|
|
316
|
+
// visible at once, neither ever hidden. A pointer that does hover keeps the
|
|
317
|
+
// behaviour, and the keyboard path below stays open for everyone.
|
|
318
|
+
const canHover =
|
|
319
|
+
typeof window === "undefined" || !window.matchMedia || window.matchMedia("(hover: hover)").matches;
|
|
320
|
+
|
|
321
|
+
if (canHover) {
|
|
322
|
+
wrappedChild.addEventListener("mouseenter", showTooltip);
|
|
323
|
+
wrappedChild.addEventListener("mouseleave", hideTooltip);
|
|
324
|
+
}
|
|
242
325
|
wrappedChild.addEventListener("focus", showTooltip);
|
|
243
326
|
wrappedChild.addEventListener("blur", hideTooltip);
|
|
244
327
|
|
package/src/widgets/TreeView.js
CHANGED
|
@@ -6,7 +6,7 @@ import { WidgetFactory } from "../widget-factory/index.js";
|
|
|
6
6
|
import { colors } from "../utils/themes.js";
|
|
7
7
|
|
|
8
8
|
import { toREM } from "../utils/units.js";
|
|
9
|
-
import { TAP_MINIMUM } from "../tools/index.js";
|
|
9
|
+
import { PILL, TAP_MINIMUM } from "../tools/index.js";
|
|
10
10
|
|
|
11
11
|
export const TreeView = (props) => {
|
|
12
12
|
const {
|
|
@@ -219,12 +219,12 @@ export const TreeView = (props) => {
|
|
|
219
219
|
// `danger` is the signal tone; the plate is the fill white words can
|
|
220
220
|
// actually sit on (6.47:1 in the light theme, 10.02:1 in the dark one).
|
|
221
221
|
backgroundColor: colors.dangerPlate,
|
|
222
|
-
borderRadius:
|
|
223
|
-
padding: "
|
|
222
|
+
borderRadius: PILL,
|
|
223
|
+
padding: "4px 8px",
|
|
224
224
|
child: WidgetFactory({
|
|
225
225
|
tag: "span",
|
|
226
|
-
fontSize: "
|
|
227
|
-
color:
|
|
226
|
+
fontSize: "12px",
|
|
227
|
+
color: colors.onSecondary,
|
|
228
228
|
fontWeight: "bold",
|
|
229
229
|
textContent: String(node.badge),
|
|
230
230
|
}),
|