@microbit/ui 0.1.0-alpha.30 → 0.1.0-alpha.31
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 +36 -13
- package/lang/ui.ar.json +62 -0
- package/package.json +1 -1
- package/src/Avatar.recipe.ts +29 -4
- package/src/Breadcrumb.recipe.ts +6 -0
- package/src/Button.tsx +8 -8
- package/src/Drawer.recipe.ts +17 -5
- package/src/ExternalLink.tsx +1 -1
- package/src/IconButton.tsx +1 -1
- package/src/InputGroup.tsx +6 -5
- package/src/LinkButton.tsx +8 -8
- package/src/Modal.recipe.ts +3 -0
- package/src/NativeSelect.tsx +2 -2
- package/src/SharedUIProvider.tsx +20 -8
- package/src/Slider.recipe.ts +3 -1
- package/src/Slider.tsx +16 -4
- package/src/Switch.recipe.ts +18 -17
- package/src/Toast.recipe.ts +3 -3
- package/src/Tooltip.tsx +17 -0
- package/src/button-icon.ts +2 -2
package/README.md
CHANGED
|
@@ -91,20 +91,31 @@ node_modules/@microbit/ui/lang/ui.fr.json --ast --out-file ...` (multiple
|
|
|
91
91
|
their English catalog for those, and should then pass `en`, keeping the
|
|
92
92
|
chosen language in their own settings for the embed. Everything downstream
|
|
93
93
|
reads this locale as a statement about the rendered page — `<html lang>`
|
|
94
|
-
via `SharedUIProvider`, react-aria's text direction and
|
|
95
|
-
`Intl` number/date formatting and plural rules. A catalog
|
|
96
|
-
has per-message gaps (an incomplete translation) is still
|
|
97
|
-
only a wholesale fallback to the English catalog should
|
|
94
|
+
and `<html dir>` via `SharedUIProvider`, react-aria's text direction and
|
|
95
|
+
built-in strings, `Intl` number/date formatting and plural rules. A catalog
|
|
96
|
+
that loads but has per-message gaps (an incomplete translation) is still
|
|
97
|
+
that language; only a wholesale fallback to the English catalog should
|
|
98
|
+
claim `en`.
|
|
98
99
|
|
|
99
100
|
6. **`SharedUIProvider`** inside the `IntlProvider`, wrapping the app. It
|
|
100
101
|
passes the locale on to react-aria, which translates its own built-in
|
|
101
102
|
strings (see [Strings](#strings)); without it those follow the browser
|
|
102
|
-
rather than the app's language setting. It also keeps `<html lang>`
|
|
103
|
-
step with the locale so assistive tech announces the page
|
|
104
|
-
language
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
the tree (e.g.
|
|
103
|
+
rather than the app's language setting. It also keeps `<html lang>` and
|
|
104
|
+
`<html dir>` in step with the locale, so assistive tech announces the page
|
|
105
|
+
in the app's language and an RTL language lays out the right way round —
|
|
106
|
+
pass `setDocumentLang={false}` where the app doesn't own the document it's
|
|
107
|
+
mounted in (an embedded widget). Also takes an optional overlay-close
|
|
108
|
+
registrar, so the app can dismiss open menus from outside the tree (e.g.
|
|
109
|
+
the Android hardware back button).
|
|
110
|
+
|
|
111
|
+
An app that passes `setDocumentLang={false}` **must set `dir` itself**,
|
|
112
|
+
on whatever element it mounts into, if it offers an RTL language. The two
|
|
113
|
+
halves of mirroring read the direction from different places: react-aria
|
|
114
|
+
takes it from the provider locale in JS and mirrors regardless of the DOM,
|
|
115
|
+
while logical properties and Panda's `_rtl` rules match on `dir`. With one
|
|
116
|
+
side mirrored and the other not, components that combine both come apart —
|
|
117
|
+
`Slider`'s thumb moves to the mirrored end while its filled track stays.
|
|
118
|
+
|
|
108
119
|
7. **`ToastProvider`** once near the root, inside the two providers above.
|
|
109
120
|
|
|
110
121
|
## Upgrading in an app
|
|
@@ -176,6 +187,18 @@ To drop it all: raise `BUILD_TARGETS`/`browserslist` past the affected
|
|
|
176
187
|
browsers, then remove the two PostCSS plugins (and this package's
|
|
177
188
|
`postcss-legacy-safari` export).
|
|
178
189
|
|
|
190
|
+
RTL is fine at this floor, with one thing to know. Panda's `_rtl` condition
|
|
191
|
+
emits `:where([dir=rtl], :dir(rtl))`, and `:dir()` is Safari 16.4. At the
|
|
192
|
+
pinned targets lightningcss rewrites that arm into a `:lang()` list, leaving
|
|
193
|
+
the `[dir=rtl]` arm — which is the one `SharedUIProvider` sets — intact. So
|
|
194
|
+
the legacy build matches on `dir` as intended, and additionally on
|
|
195
|
+
`lang="ar"` and friends, which modern builds don't. Everything else the
|
|
196
|
+
mirroring uses (logical longhands, `text-align: start`, custom properties in
|
|
197
|
+
`calc()`) passes through untouched; only logical _shorthands_ need the shim,
|
|
198
|
+
as before. `:where()` contributes no specificity, so the RTL rules win on
|
|
199
|
+
source order — de-layering preserves it, since it pads later layers rather
|
|
200
|
+
than reordering within one.
|
|
201
|
+
|
|
179
202
|
## The CSS-variable contract
|
|
180
203
|
|
|
181
204
|
Panda emits every token as a CSS custom property with its default naming —
|
|
@@ -287,9 +310,9 @@ blank.
|
|
|
287
310
|
Until this package's Crowdin project is wired up, the non-English catalogs are
|
|
288
311
|
pre-Crowdin seeds — some strings AI-drafted, all of them for Crowdin to review
|
|
289
312
|
— credited as follows. Translations of the same strings elsewhere in the
|
|
290
|
-
micro:bit translation programme: ml-trainer
|
|
291
|
-
MakeCode's editor strings, which are not in the pxt repo but can be fetched
|
|
292
|
-
locale from
|
|
313
|
+
micro:bit translation programme: ml-trainer, python-editor-v3 and classroom,
|
|
314
|
+
and MakeCode's editor strings, which are not in the pxt repo but can be fetched
|
|
315
|
+
per locale from
|
|
293
316
|
`https://makecode.microbit.org/api/translations?lang=<locale>&filename=strings.json&approved=true`.
|
|
294
317
|
The `ui.toast-status-*` words come from Spectrum 2's InlineAlert catalogs
|
|
295
318
|
(`@react-spectrum/s2/intl/*.json`, keys `inlinealert.informative` / `notice` /
|
package/lang/ui.ar.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui.breadcrumb": {
|
|
3
|
+
"defaultMessage": "مسار التنقل",
|
|
4
|
+
"description": "Accessible label for the breadcrumb navigation trail (the WAI-ARIA APG's conventional name)"
|
|
5
|
+
},
|
|
6
|
+
"ui.close-action": {
|
|
7
|
+
"defaultMessage": "إغلاق",
|
|
8
|
+
"description": "Close button text or label"
|
|
9
|
+
},
|
|
10
|
+
"ui.combobox-listbox": {
|
|
11
|
+
"defaultMessage": "مقترحات",
|
|
12
|
+
"description": "Accessible label for a ComboBox's dropdown list of suggestions"
|
|
13
|
+
},
|
|
14
|
+
"ui.combobox-trigger": {
|
|
15
|
+
"defaultMessage": "عرض المقترحات",
|
|
16
|
+
"description": "Accessible label for the button that opens a ComboBox's suggestions dropdown"
|
|
17
|
+
},
|
|
18
|
+
"ui.loading": {
|
|
19
|
+
"defaultMessage": "جاري التحميل",
|
|
20
|
+
"description": "Announced by screen readers for a button showing a loading spinner"
|
|
21
|
+
},
|
|
22
|
+
"ui.new-tab-notice": {
|
|
23
|
+
"defaultMessage": "يفتح في علامة تبويب جديدة",
|
|
24
|
+
"description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
|
|
25
|
+
},
|
|
26
|
+
"ui.numberfield-decrease": {
|
|
27
|
+
"defaultMessage": "خفض {fieldLabel}",
|
|
28
|
+
"description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
|
|
29
|
+
},
|
|
30
|
+
"ui.numberfield-increase": {
|
|
31
|
+
"defaultMessage": "زيادة {fieldLabel}",
|
|
32
|
+
"description": "Accessible label for a number field's increment stepper; fieldLabel is the field's label and may be empty"
|
|
33
|
+
},
|
|
34
|
+
"ui.select-placeholder": {
|
|
35
|
+
"defaultMessage": "حدد عنصرًا",
|
|
36
|
+
"description": "Shown in a Select's trigger while nothing is chosen and the app supplies no placeholder"
|
|
37
|
+
},
|
|
38
|
+
"ui.select-row-action": {
|
|
39
|
+
"defaultMessage": "تحديد",
|
|
40
|
+
"description": "Accessible label for a row-selection checkbox in a list or table; screen readers read it together with the row's content"
|
|
41
|
+
},
|
|
42
|
+
"ui.toast-region": {
|
|
43
|
+
"defaultMessage": "{count, plural, zero {لا إشعارات} one {إشعار واحد} two {إشعاران} few {# إشعارات} many {# إشعارًا} other {# إشعار}}.",
|
|
44
|
+
"description": "Accessible label for the notifications area; count is the number of notifications showing"
|
|
45
|
+
},
|
|
46
|
+
"ui.toast-status-error": {
|
|
47
|
+
"defaultMessage": "خطأ",
|
|
48
|
+
"description": "Announced by screen readers before an error notification"
|
|
49
|
+
},
|
|
50
|
+
"ui.toast-status-info": {
|
|
51
|
+
"defaultMessage": "معلومات",
|
|
52
|
+
"description": "Announced by screen readers before an informational notification"
|
|
53
|
+
},
|
|
54
|
+
"ui.toast-status-success": {
|
|
55
|
+
"defaultMessage": "تم بنجاح",
|
|
56
|
+
"description": "Announced by screen readers before a success notification"
|
|
57
|
+
},
|
|
58
|
+
"ui.toast-status-warning": {
|
|
59
|
+
"defaultMessage": "تحذير",
|
|
60
|
+
"description": "Announced by screen readers before a warning notification"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbit/ui",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.31",
|
|
4
4
|
"description": "micro:bit design-system primitives: react-aria-components + Panda CSS with a design language ported from Chakra UI v2. Ships as source; see README for the consumption setup.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/Avatar.recipe.ts
CHANGED
|
@@ -65,6 +65,19 @@ export const avatar = defineSlotRecipe({
|
|
|
65
65
|
borderWidth: "0.2em",
|
|
66
66
|
borderStyle: "solid",
|
|
67
67
|
borderColor: "white",
|
|
68
|
+
// The corner variants pin the badge with logical insets but nudge it
|
|
69
|
+
// out over the edge with a transform, which is physical — so the
|
|
70
|
+
// outward direction is a variable the RTL rule negates. Without it a
|
|
71
|
+
// `start` badge would sit at the right corner and nudge left, i.e.
|
|
72
|
+
// inwards over the avatar.
|
|
73
|
+
// Fallbacks carry a unit: `calc(-1 * 0)` is a number, which is invalid
|
|
74
|
+
// where a length is wanted, and would drop the RTL transform whole.
|
|
75
|
+
transform:
|
|
76
|
+
"translate(var(--avatar-badge-x, 0px), var(--avatar-badge-y, 0px))",
|
|
77
|
+
_rtl: {
|
|
78
|
+
transform:
|
|
79
|
+
"translate(calc(-1 * var(--avatar-badge-x, 0px)), var(--avatar-badge-y, 0px))",
|
|
80
|
+
},
|
|
68
81
|
},
|
|
69
82
|
},
|
|
70
83
|
variants: {
|
|
@@ -141,21 +154,33 @@ export const avatar = defineSlotRecipe({
|
|
|
141
154
|
badge: {
|
|
142
155
|
top: "0",
|
|
143
156
|
insetStart: "0",
|
|
144
|
-
|
|
157
|
+
"--avatar-badge-x": "-25%",
|
|
158
|
+
"--avatar-badge-y": "-25%",
|
|
145
159
|
},
|
|
146
160
|
},
|
|
147
161
|
"top-end": {
|
|
148
|
-
badge: {
|
|
162
|
+
badge: {
|
|
163
|
+
top: "0",
|
|
164
|
+
insetEnd: "0",
|
|
165
|
+
"--avatar-badge-x": "25%",
|
|
166
|
+
"--avatar-badge-y": "-25%",
|
|
167
|
+
},
|
|
149
168
|
},
|
|
150
169
|
"bottom-start": {
|
|
151
170
|
badge: {
|
|
152
171
|
bottom: "0",
|
|
153
172
|
insetStart: "0",
|
|
154
|
-
|
|
173
|
+
"--avatar-badge-x": "-25%",
|
|
174
|
+
"--avatar-badge-y": "25%",
|
|
155
175
|
},
|
|
156
176
|
},
|
|
157
177
|
"bottom-end": {
|
|
158
|
-
badge: {
|
|
178
|
+
badge: {
|
|
179
|
+
bottom: "0",
|
|
180
|
+
insetEnd: "0",
|
|
181
|
+
"--avatar-badge-x": "25%",
|
|
182
|
+
"--avatar-badge-y": "25%",
|
|
183
|
+
},
|
|
159
184
|
},
|
|
160
185
|
},
|
|
161
186
|
},
|
package/src/Breadcrumb.recipe.ts
CHANGED
|
@@ -34,6 +34,12 @@ export const breadcrumb = defineSlotRecipe({
|
|
|
34
34
|
},
|
|
35
35
|
separator: {
|
|
36
36
|
mx: "2",
|
|
37
|
+
// A chevron separator points along the trail, so it has to turn round
|
|
38
|
+
// in RTL. Scoped to an svg because the default separator is the text
|
|
39
|
+
// "/", which would become "\": a caller passing a glyph owns its
|
|
40
|
+
// direction, one passing an icon gets it handled. Override via the
|
|
41
|
+
// separator slot's css for an icon that shouldn't turn.
|
|
42
|
+
_rtl: { "& svg": { transform: "scaleX(-1)" } },
|
|
37
43
|
},
|
|
38
44
|
},
|
|
39
45
|
});
|
package/src/Button.tsx
CHANGED
|
@@ -48,9 +48,9 @@ export interface ButtonProps
|
|
|
48
48
|
css?: SystemStyleObject;
|
|
49
49
|
className?: string;
|
|
50
50
|
/** Icon rendered before the label. */
|
|
51
|
-
|
|
51
|
+
startIcon?: ReactNode;
|
|
52
52
|
/** Icon rendered after the label. */
|
|
53
|
-
|
|
53
|
+
endIcon?: ReactNode;
|
|
54
54
|
/**
|
|
55
55
|
* Show a spinner in place of the label and disable interaction: the label
|
|
56
56
|
* stays in the layout but invisible, so the button keeps its size, and the
|
|
@@ -74,8 +74,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
74
74
|
tone,
|
|
75
75
|
css: cssProp,
|
|
76
76
|
className,
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
startIcon,
|
|
78
|
+
endIcon,
|
|
79
79
|
isLoading,
|
|
80
80
|
children,
|
|
81
81
|
...rest
|
|
@@ -84,12 +84,12 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
|
84
84
|
) {
|
|
85
85
|
const label = (
|
|
86
86
|
<>
|
|
87
|
-
{
|
|
88
|
-
<span className={buttonIcon({ side: "
|
|
87
|
+
{startIcon ? (
|
|
88
|
+
<span className={buttonIcon({ side: "start" })}>{startIcon}</span>
|
|
89
89
|
) : null}
|
|
90
90
|
{children}
|
|
91
|
-
{
|
|
92
|
-
<span className={buttonIcon({ side: "
|
|
91
|
+
{endIcon ? (
|
|
92
|
+
<span className={buttonIcon({ side: "end" })}>{endIcon}</span>
|
|
93
93
|
) : null}
|
|
94
94
|
</>
|
|
95
95
|
);
|
package/src/Drawer.recipe.ts
CHANGED
|
@@ -76,22 +76,34 @@ export const drawer = defineSlotRecipe({
|
|
|
76
76
|
},
|
|
77
77
|
},
|
|
78
78
|
variants: {
|
|
79
|
+
// Reading-order sides, so a drawer keeps its relationship to the content
|
|
80
|
+
// in an RTL locale rather than its screen position. The slide-out
|
|
81
|
+
// transforms are physical, so each needs mirroring under `dir=rtl` to
|
|
82
|
+
// travel towards its own edge.
|
|
79
83
|
placement: {
|
|
80
|
-
|
|
84
|
+
start: {
|
|
81
85
|
content: {
|
|
82
|
-
|
|
86
|
+
insetStart: 0,
|
|
83
87
|
"&[data-entering]": { transform: "translateX(-100%)" },
|
|
84
88
|
"&[data-exiting]": { transform: "translateX(-100%)" },
|
|
89
|
+
_rtl: {
|
|
90
|
+
"&[data-entering]": { transform: "translateX(100%)" },
|
|
91
|
+
"&[data-exiting]": { transform: "translateX(100%)" },
|
|
92
|
+
},
|
|
85
93
|
},
|
|
86
94
|
},
|
|
87
|
-
|
|
95
|
+
end: {
|
|
88
96
|
content: {
|
|
89
|
-
|
|
97
|
+
insetEnd: 0,
|
|
90
98
|
"&[data-entering]": { transform: "translateX(100%)" },
|
|
91
99
|
"&[data-exiting]": { transform: "translateX(100%)" },
|
|
100
|
+
_rtl: {
|
|
101
|
+
"&[data-entering]": { transform: "translateX(-100%)" },
|
|
102
|
+
"&[data-exiting]": { transform: "translateX(-100%)" },
|
|
103
|
+
},
|
|
92
104
|
},
|
|
93
105
|
},
|
|
94
106
|
},
|
|
95
107
|
},
|
|
96
|
-
defaultVariants: { placement: "
|
|
108
|
+
defaultVariants: { placement: "start" },
|
|
97
109
|
});
|
package/src/ExternalLink.tsx
CHANGED
|
@@ -38,6 +38,6 @@ export const ExternalLink = ({ children, ...props }: ExternalLinkProps) => (
|
|
|
38
38
|
{", "}
|
|
39
39
|
<FormattedMessage {...uiMessage("ui.new-tab-notice")} />
|
|
40
40
|
</VisuallyHidden>
|
|
41
|
-
<Icon as={RiExternalLinkLine} aria-hidden css={{
|
|
41
|
+
<Icon as={RiExternalLinkLine} aria-hidden css={{ ms: 1 }} />
|
|
42
42
|
</Link>
|
|
43
43
|
);
|
package/src/IconButton.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { forwardRef } from "react";
|
|
|
7
7
|
import { Button, ButtonProps } from "./Button";
|
|
8
8
|
|
|
9
9
|
export interface IconButtonProps
|
|
10
|
-
extends Omit<ButtonProps, "
|
|
10
|
+
extends Omit<ButtonProps, "startIcon" | "endIcon"> {
|
|
11
11
|
/** Icon-only buttons have no visible label, so this is required. */
|
|
12
12
|
"aria-label": string;
|
|
13
13
|
}
|
package/src/InputGroup.tsx
CHANGED
|
@@ -7,7 +7,8 @@ import { styled } from "styled-system/jsx";
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* InputGroup — relative wrapper for an Input with addon elements. Pad the
|
|
10
|
-
* input (`
|
|
10
|
+
* input (`ps`/`pe`) to make room for the elements; the elements sit on the
|
|
11
|
+
* reading-order sides, so physical padding lands opposite them in RTL.
|
|
11
12
|
*/
|
|
12
13
|
export const InputGroup = styled("div", {
|
|
13
14
|
base: { position: "relative", width: "100%", display: "flex" },
|
|
@@ -19,11 +20,11 @@ export const InputGroup = styled("div", {
|
|
|
19
20
|
// size; pass the same `size` as the grouped Input.
|
|
20
21
|
|
|
21
22
|
/** Element overlaying the start of an InputGroup. */
|
|
22
|
-
export const
|
|
23
|
+
export const InputStartElement = styled("div", {
|
|
23
24
|
base: {
|
|
24
25
|
position: "absolute",
|
|
25
26
|
top: 0,
|
|
26
|
-
|
|
27
|
+
insetStart: 0,
|
|
27
28
|
display: "flex",
|
|
28
29
|
alignItems: "center",
|
|
29
30
|
justifyContent: "center",
|
|
@@ -40,11 +41,11 @@ export const InputLeftElement = styled("div", {
|
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
/** Element overlaying the end of an InputGroup. */
|
|
43
|
-
export const
|
|
44
|
+
export const InputEndElement = styled("div", {
|
|
44
45
|
base: {
|
|
45
46
|
position: "absolute",
|
|
46
47
|
top: 0,
|
|
47
|
-
|
|
48
|
+
insetEnd: 0,
|
|
48
49
|
display: "flex",
|
|
49
50
|
alignItems: "center",
|
|
50
51
|
justifyContent: "center",
|
package/src/LinkButton.tsx
CHANGED
|
@@ -20,9 +20,9 @@ export interface LinkButtonProps
|
|
|
20
20
|
css?: SystemStyleObject;
|
|
21
21
|
className?: string;
|
|
22
22
|
/** Icon rendered before the label. */
|
|
23
|
-
|
|
23
|
+
startIcon?: ReactNode;
|
|
24
24
|
/** Icon rendered after the label. */
|
|
25
|
-
|
|
25
|
+
endIcon?: ReactNode;
|
|
26
26
|
children?: ReactNode;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -51,8 +51,8 @@ export const LinkButton = forwardRef<HTMLAnchorElement, LinkButtonProps>(
|
|
|
51
51
|
tone,
|
|
52
52
|
css: cssProp,
|
|
53
53
|
className,
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
startIcon,
|
|
55
|
+
endIcon,
|
|
56
56
|
children,
|
|
57
57
|
...rest
|
|
58
58
|
},
|
|
@@ -68,12 +68,12 @@ export const LinkButton = forwardRef<HTMLAnchorElement, LinkButtonProps>(
|
|
|
68
68
|
)}
|
|
69
69
|
{...rest}
|
|
70
70
|
>
|
|
71
|
-
{
|
|
72
|
-
<span className={buttonIcon({ side: "
|
|
71
|
+
{startIcon ? (
|
|
72
|
+
<span className={buttonIcon({ side: "start" })}>{startIcon}</span>
|
|
73
73
|
) : null}
|
|
74
74
|
{children}
|
|
75
|
-
{
|
|
76
|
-
<span className={buttonIcon({ side: "
|
|
75
|
+
{endIcon ? (
|
|
76
|
+
<span className={buttonIcon({ side: "end" })}>{endIcon}</span>
|
|
77
77
|
) : null}
|
|
78
78
|
</RACLink>
|
|
79
79
|
);
|
package/src/Modal.recipe.ts
CHANGED
|
@@ -33,6 +33,9 @@ const dialogBox = {
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
// The `full` variant also styles these slots; same symmetry requirement.
|
|
36
|
+
// The header's padding stays physical to pair with that variant's
|
|
37
|
+
// window-controls inset, which is a screen position rather than a
|
|
38
|
+
// reading-order one; the base `px` covers the start side either way.
|
|
36
39
|
const dialogSlots = {
|
|
37
40
|
header: { pl: "6" },
|
|
38
41
|
body: { overflowY: "visible" },
|
package/src/NativeSelect.tsx
CHANGED
|
@@ -54,7 +54,7 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
|
54
54
|
_disabled: { cursor: "not-allowed" },
|
|
55
55
|
},
|
|
56
56
|
// Room for the chevron overlay (constant across sizes).
|
|
57
|
-
hideChevron ? undefined : {
|
|
57
|
+
hideChevron ? undefined : { paddingEnd: "8" },
|
|
58
58
|
cssProp,
|
|
59
59
|
),
|
|
60
60
|
className,
|
|
@@ -83,7 +83,7 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
|
83
83
|
aria-hidden
|
|
84
84
|
className={css({
|
|
85
85
|
position: "absolute",
|
|
86
|
-
|
|
86
|
+
insetEnd: "2",
|
|
87
87
|
top: "50%",
|
|
88
88
|
transform: "translateY(-50%)",
|
|
89
89
|
width: "5",
|
package/src/SharedUIProvider.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
useEffect,
|
|
11
11
|
useMemo,
|
|
12
12
|
} from "react";
|
|
13
|
+
import { isRTL } from "react-aria";
|
|
13
14
|
import { I18nProvider } from "react-aria-components";
|
|
14
15
|
import { IntlContext } from "react-intl";
|
|
15
16
|
import { racLocale } from "./rac-locale";
|
|
@@ -37,11 +38,17 @@ export interface SharedUIProviderProps {
|
|
|
37
38
|
*/
|
|
38
39
|
locale?: string;
|
|
39
40
|
/**
|
|
40
|
-
* Keeps `<html lang>` in step with the locale, so assistive
|
|
41
|
-
* the page in the app's language
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* index.html remains the
|
|
41
|
+
* Keeps `<html lang>` and `<html dir>` in step with the locale, so assistive
|
|
42
|
+
* tech announces the page in the app's language and the layout runs the
|
|
43
|
+
* right way round. On by default; set false where the app doesn't own the
|
|
44
|
+
* document it's mounted in (an embedded widget), or when mounting more than
|
|
45
|
+
* one provider per page. The static `lang="en"` in index.html remains the
|
|
46
|
+
* pre-hydration default.
|
|
47
|
+
*
|
|
48
|
+
* Turning it off in an RTL locale means the app must set `dir` itself above
|
|
49
|
+
* the components, or the UI half-mirrors: react-aria mirrors from the locale
|
|
50
|
+
* in JS whatever the DOM says, while logical properties and `_rtl` rules
|
|
51
|
+
* read `dir` (see the README's RTL note).
|
|
45
52
|
*
|
|
46
53
|
* Correct only when the IntlProvider locale is the language the app
|
|
47
54
|
* actually renders in — an app falling back wholesale to its English
|
|
@@ -74,18 +81,23 @@ export const SharedUIProvider = ({
|
|
|
74
81
|
// no IntlProvider: react-aria falls back to the browser locale, as before.
|
|
75
82
|
const intlLocale = useContext(IntlContext)?.locale;
|
|
76
83
|
const appLocale = locale ?? intlLocale;
|
|
84
|
+
// The tag react-aria gets, so the document's direction and the components'
|
|
85
|
+
// cannot disagree; also the only form isRTL can be handed safely, as it
|
|
86
|
+
// throws on a malformed tag.
|
|
87
|
+
const sanitizedLocale = racLocale(appLocale);
|
|
77
88
|
useEffect(() => {
|
|
78
|
-
if (setDocumentLang && appLocale) {
|
|
89
|
+
if (setDocumentLang && appLocale && sanitizedLocale) {
|
|
79
90
|
document.documentElement.lang = appLocale;
|
|
91
|
+
document.documentElement.dir = isRTL(sanitizedLocale) ? "rtl" : "ltr";
|
|
80
92
|
}
|
|
81
|
-
}, [setDocumentLang, appLocale]);
|
|
93
|
+
}, [setDocumentLang, appLocale, sanitizedLocale]);
|
|
82
94
|
const value = useMemo(
|
|
83
95
|
() => ({ overlayCloseRegistrar }),
|
|
84
96
|
[overlayCloseRegistrar],
|
|
85
97
|
);
|
|
86
98
|
return (
|
|
87
99
|
<SharedUIContext.Provider value={value}>
|
|
88
|
-
<I18nProvider locale={
|
|
100
|
+
<I18nProvider locale={sanitizedLocale}>{children}</I18nProvider>
|
|
89
101
|
</SharedUIContext.Provider>
|
|
90
102
|
);
|
|
91
103
|
};
|
package/src/Slider.recipe.ts
CHANGED
|
@@ -60,7 +60,9 @@ export const slider = defineSlotRecipe({
|
|
|
60
60
|
filledTrack: {
|
|
61
61
|
position: "absolute",
|
|
62
62
|
top: 0,
|
|
63
|
-
|
|
63
|
+
// Logical, unlike the thumb: the fill grows from the track's start, so
|
|
64
|
+
// in RTL it fills from the right and the two still meet.
|
|
65
|
+
insetStart: 0,
|
|
64
66
|
height: "100%",
|
|
65
67
|
// Fill colour is set per call site via `filledTrackCss`.
|
|
66
68
|
bg: "brand.500",
|
package/src/Slider.tsx
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: MIT
|
|
5
5
|
*/
|
|
6
6
|
import { ReactNode } from "react";
|
|
7
|
+
import { useLocale } from "react-aria";
|
|
7
8
|
import {
|
|
8
9
|
Slider as RACSlider,
|
|
9
10
|
SliderThumb,
|
|
@@ -40,8 +41,13 @@ export interface SliderProps {
|
|
|
40
41
|
/**
|
|
41
42
|
* Additional positioned overlays rendered inside the slider root
|
|
42
43
|
* (always-visible value labels, threshold markers, ...). The root is
|
|
43
|
-
* position: relative; position children absolutely
|
|
44
|
-
*
|
|
44
|
+
* position: relative; position children absolutely.
|
|
45
|
+
*
|
|
46
|
+
* Anything sitting at a *value* along the track takes a percentage `left`,
|
|
47
|
+
* physical and mirrored by hand in RTL as the thumb is (see `offset`
|
|
48
|
+
* below), never `insetInlineStart`. Anything pinned to the track's *ends*
|
|
49
|
+
* — min/max labels and the like — is ordinary layout and should be
|
|
50
|
+
* logical, so `insetStart`/`insetEnd`.
|
|
45
51
|
*/
|
|
46
52
|
children?: ReactNode;
|
|
47
53
|
/**
|
|
@@ -76,7 +82,13 @@ export const Slider = ({
|
|
|
76
82
|
onThumbFocusChange,
|
|
77
83
|
}: SliderProps) => {
|
|
78
84
|
const slots = slider();
|
|
85
|
+
const { direction } = useLocale();
|
|
79
86
|
const percent = ((value - minValue) / (maxValue - minValue)) * 100;
|
|
87
|
+
// react-aria positions the thumb with a physical `left` that it mirrors
|
|
88
|
+
// itself in RTL (useSliderThumb), so anything tracking the thumb has to
|
|
89
|
+
// mirror the same way rather than going logical — `insetInlineStart` would
|
|
90
|
+
// put these on the opposite side of the track from the thumb.
|
|
91
|
+
const offset = direction === "rtl" ? 100 - percent : percent;
|
|
80
92
|
return (
|
|
81
93
|
<RACSlider
|
|
82
94
|
value={value}
|
|
@@ -92,7 +104,7 @@ export const Slider = ({
|
|
|
92
104
|
<div
|
|
93
105
|
data-part="mark"
|
|
94
106
|
className={cx(slots.mark, markCss ? css(markCss) : undefined)}
|
|
95
|
-
style={{ left: `${
|
|
107
|
+
style={{ left: `${offset}%` }}
|
|
96
108
|
>
|
|
97
109
|
{mark}
|
|
98
110
|
</div>
|
|
@@ -144,7 +156,7 @@ export const Slider = ({
|
|
|
144
156
|
borderTopColor: "gray.700",
|
|
145
157
|
},
|
|
146
158
|
})}
|
|
147
|
-
style={{ left: `${
|
|
159
|
+
style={{ left: `${offset}%` }}
|
|
148
160
|
>
|
|
149
161
|
{thumbTooltip}
|
|
150
162
|
</div>
|
package/src/Switch.recipe.ts
CHANGED
|
@@ -59,6 +59,19 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
59
59
|
transitionProperty: "transform",
|
|
60
60
|
transitionDuration: "normal",
|
|
61
61
|
borderRadius: "inherit",
|
|
62
|
+
// The thumb rests at the track's start and slides to its end, so the
|
|
63
|
+
// travel has to reverse in RTL — a transform is physical however the
|
|
64
|
+
// rest of the switch is laid out. Distance is per size, below.
|
|
65
|
+
// Fallbacks carry a unit: `calc(-1 * 0)` is a number, which is invalid
|
|
66
|
+
// where a length is wanted, and would drop the RTL transform whole.
|
|
67
|
+
"&[data-selected]": {
|
|
68
|
+
transform: "translateX(var(--switch-travel, 0px))",
|
|
69
|
+
},
|
|
70
|
+
_rtl: {
|
|
71
|
+
"&[data-selected]": {
|
|
72
|
+
transform: "translateX(calc(-1 * var(--switch-travel, 0px)))",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
62
75
|
_forcedColors: {
|
|
63
76
|
bg: "ButtonText",
|
|
64
77
|
"&[data-selected]": { bg: "SelectedItemText" },
|
|
@@ -89,32 +102,20 @@ export const switchRecipe = defineSlotRecipe({
|
|
|
89
102
|
label: { marginStart: "0", marginEnd: "3" },
|
|
90
103
|
},
|
|
91
104
|
},
|
|
92
|
-
// Track width x height per size; the
|
|
93
|
-
//
|
|
105
|
+
// Track width x height per size; the thumb's travel is the difference
|
|
106
|
+
// between them.
|
|
94
107
|
size: {
|
|
95
108
|
sm: {
|
|
96
109
|
track: { width: "1.375rem", height: "3" },
|
|
97
|
-
thumb: {
|
|
98
|
-
width: "3",
|
|
99
|
-
height: "3",
|
|
100
|
-
"&[data-selected]": { transform: "translateX(0.625rem)" },
|
|
101
|
-
},
|
|
110
|
+
thumb: { width: "3", height: "3", "--switch-travel": "0.625rem" },
|
|
102
111
|
},
|
|
103
112
|
md: {
|
|
104
113
|
track: { width: "1.875rem", height: "4" },
|
|
105
|
-
thumb: {
|
|
106
|
-
width: "4",
|
|
107
|
-
height: "4",
|
|
108
|
-
"&[data-selected]": { transform: "translateX(0.875rem)" },
|
|
109
|
-
},
|
|
114
|
+
thumb: { width: "4", height: "4", "--switch-travel": "0.875rem" },
|
|
110
115
|
},
|
|
111
116
|
lg: {
|
|
112
117
|
track: { width: "2.875rem", height: "6" },
|
|
113
|
-
thumb: {
|
|
114
|
-
width: "6",
|
|
115
|
-
height: "6",
|
|
116
|
-
"&[data-selected]": { transform: "translateX(1.375rem)" },
|
|
117
|
-
},
|
|
118
|
+
thumb: { width: "6", height: "6", "--switch-travel": "1.375rem" },
|
|
118
119
|
},
|
|
119
120
|
},
|
|
120
121
|
},
|
package/src/Toast.recipe.ts
CHANGED
|
@@ -59,9 +59,9 @@ export const toast = defineSlotRecipe({
|
|
|
59
59
|
p: "4",
|
|
60
60
|
// Leave room for the absolutely-positioned close button plus breathing
|
|
61
61
|
// space so its hover area never overlaps the title text (24px button at
|
|
62
|
-
// 4px inset spans ~4-28px from the
|
|
63
|
-
// ~12px gap).
|
|
64
|
-
|
|
62
|
+
// 4px inset spans ~4-28px from the end edge; content stops at 40px for
|
|
63
|
+
// a ~12px gap). Logical, like the button's own inset.
|
|
64
|
+
paddingEnd: "10",
|
|
65
65
|
borderRadius: "md",
|
|
66
66
|
boxShadow: "lg",
|
|
67
67
|
color: "white",
|
package/src/Tooltip.tsx
CHANGED
|
@@ -19,15 +19,32 @@ export interface TooltipProps {
|
|
|
19
19
|
label: ReactNode;
|
|
20
20
|
/** A single focusable trigger element (e.g. a Button). */
|
|
21
21
|
children: ReactElement;
|
|
22
|
+
/**
|
|
23
|
+
* Which side of the trigger the tooltip prefers. A preference, not a
|
|
24
|
+
* promise: react-aria flips it at runtime when the tooltip would leave the
|
|
25
|
+
* viewport, so pick the side that reads right rather than one that dodges
|
|
26
|
+
* an edge.
|
|
27
|
+
*
|
|
28
|
+
* `top`/`bottom` are direction-neutral and suit most tooltips.
|
|
29
|
+
* `start`/`end` follow reading order and mirror in RTL. `left`/`right` are
|
|
30
|
+
* screen sides and never mirror — reach for them only when the trigger is
|
|
31
|
+
* pinned to a physical edge.
|
|
32
|
+
*/
|
|
22
33
|
placement?:
|
|
23
34
|
| "top"
|
|
24
35
|
| "bottom"
|
|
36
|
+
| "start"
|
|
37
|
+
| "end"
|
|
25
38
|
| "left"
|
|
26
39
|
| "right"
|
|
27
40
|
| "top start"
|
|
28
41
|
| "top end"
|
|
29
42
|
| "bottom start"
|
|
30
43
|
| "bottom end"
|
|
44
|
+
| "start top"
|
|
45
|
+
| "start bottom"
|
|
46
|
+
| "end top"
|
|
47
|
+
| "end bottom"
|
|
31
48
|
| "left top"
|
|
32
49
|
| "left bottom"
|
|
33
50
|
| "right top"
|