@microbit/ui 0.2.0 → 0.2.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/lang/ui.ar.json +1 -1
- package/lang/ui.ga-ie.json +9 -9
- package/lang/ui.lol.json +14 -14
- package/package.json +1 -1
- package/src/Modal.tsx +91 -0
package/lang/ui.ar.json
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"description": "Shown in a Select's trigger while nothing is chosen and the app supplies no placeholder"
|
|
37
37
|
},
|
|
38
38
|
"ui.select-row-action": {
|
|
39
|
-
"defaultMessage": "
|
|
39
|
+
"defaultMessage": "حدّد",
|
|
40
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
41
|
},
|
|
42
42
|
"ui.toast-region": {
|
package/lang/ui.ga-ie.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"ui.breadcrumb": {
|
|
3
|
-
"defaultMessage": "
|
|
3
|
+
"defaultMessage": "Grabhróg aráin",
|
|
4
4
|
"description": "Accessible label for the breadcrumb navigation trail (the WAI-ARIA APG's conventional name)"
|
|
5
5
|
},
|
|
6
6
|
"ui.close-action": {
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"description": "Close button text or label"
|
|
9
9
|
},
|
|
10
10
|
"ui.combobox-listbox": {
|
|
11
|
-
"defaultMessage": "
|
|
11
|
+
"defaultMessage": "Moltaí",
|
|
12
12
|
"description": "Accessible label for a ComboBox's dropdown list of suggestions"
|
|
13
13
|
},
|
|
14
14
|
"ui.combobox-trigger": {
|
|
15
|
-
"defaultMessage": "
|
|
15
|
+
"defaultMessage": "Taispeáin moltaí",
|
|
16
16
|
"description": "Accessible label for the button that opens a ComboBox's suggestions dropdown"
|
|
17
17
|
},
|
|
18
18
|
"ui.loading": {
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
"description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
|
|
25
25
|
},
|
|
26
26
|
"ui.numberfield-decrease": {
|
|
27
|
-
"defaultMessage": "
|
|
27
|
+
"defaultMessage": "Laghdaigh {fieldLabel}",
|
|
28
28
|
"description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
|
|
29
29
|
},
|
|
30
30
|
"ui.numberfield-increase": {
|
|
31
|
-
"defaultMessage": "
|
|
31
|
+
"defaultMessage": "Méadaigh {fieldLabel}",
|
|
32
32
|
"description": "Accessible label for a number field's increment stepper; fieldLabel is the field's label and may be empty"
|
|
33
33
|
},
|
|
34
34
|
"ui.select-placeholder": {
|
|
35
|
-
"defaultMessage": "
|
|
35
|
+
"defaultMessage": "Roghnaigh mír",
|
|
36
36
|
"description": "Shown in a Select's trigger while nothing is chosen and the app supplies no placeholder"
|
|
37
37
|
},
|
|
38
38
|
"ui.select-row-action": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
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
41
|
},
|
|
42
42
|
"ui.toast-region": {
|
|
43
|
-
"defaultMessage": "{count, plural, one {#
|
|
43
|
+
"defaultMessage": "{count, plural, one {# fógra} two {# fhógra} few {# fógraí} many {# notifications} other {# fógraí}}.",
|
|
44
44
|
"description": "Accessible label for the notifications area; count is the number of notifications showing"
|
|
45
45
|
},
|
|
46
46
|
"ui.toast-status-error": {
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"description": "Announced by screen readers before an error notification"
|
|
49
49
|
},
|
|
50
50
|
"ui.toast-status-info": {
|
|
51
|
-
"defaultMessage": "
|
|
51
|
+
"defaultMessage": "Eolas",
|
|
52
52
|
"description": "Announced by screen readers before an informational notification"
|
|
53
53
|
},
|
|
54
54
|
"ui.toast-status-success": {
|
|
55
|
-
"defaultMessage": "
|
|
55
|
+
"defaultMessage": "Rath",
|
|
56
56
|
"description": "Announced by screen readers before a success notification"
|
|
57
57
|
},
|
|
58
58
|
"ui.toast-status-warning": {
|
package/lang/ui.lol.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"ui.breadcrumb": {
|
|
3
|
-
"defaultMessage": "
|
|
3
|
+
"defaultMessage": "crwdns370036:0crwdne370036:0",
|
|
4
4
|
"description": "Accessible label for the breadcrumb navigation trail (the WAI-ARIA APG's conventional name)"
|
|
5
5
|
},
|
|
6
6
|
"ui.close-action": {
|
|
7
|
-
"defaultMessage": "
|
|
7
|
+
"defaultMessage": "crwdns370038:0crwdne370038:0",
|
|
8
8
|
"description": "Close button text or label"
|
|
9
9
|
},
|
|
10
10
|
"ui.combobox-listbox": {
|
|
11
|
-
"defaultMessage": "
|
|
11
|
+
"defaultMessage": "crwdns370040:0crwdne370040:0",
|
|
12
12
|
"description": "Accessible label for a ComboBox's dropdown list of suggestions"
|
|
13
13
|
},
|
|
14
14
|
"ui.combobox-trigger": {
|
|
15
|
-
"defaultMessage": "
|
|
15
|
+
"defaultMessage": "crwdns370042:0crwdne370042:0",
|
|
16
16
|
"description": "Accessible label for the button that opens a ComboBox's suggestions dropdown"
|
|
17
17
|
},
|
|
18
18
|
"ui.loading": {
|
|
19
|
-
"defaultMessage": "
|
|
19
|
+
"defaultMessage": "crwdns370044:0crwdne370044:0",
|
|
20
20
|
"description": "Announced by screen readers for a button showing a loading spinner"
|
|
21
21
|
},
|
|
22
22
|
"ui.new-tab-notice": {
|
|
@@ -24,39 +24,39 @@
|
|
|
24
24
|
"description": "Visually hidden suffix on links that open a new tab, announced by screen readers"
|
|
25
25
|
},
|
|
26
26
|
"ui.numberfield-decrease": {
|
|
27
|
-
"defaultMessage": "
|
|
27
|
+
"defaultMessage": "crwdns370046:0{fieldLabel}crwdne370046:0",
|
|
28
28
|
"description": "Accessible label for a number field's decrement stepper; fieldLabel is the field's label and may be empty"
|
|
29
29
|
},
|
|
30
30
|
"ui.numberfield-increase": {
|
|
31
|
-
"defaultMessage": "
|
|
31
|
+
"defaultMessage": "crwdns370048:0{fieldLabel}crwdne370048:0",
|
|
32
32
|
"description": "Accessible label for a number field's increment stepper; fieldLabel is the field's label and may be empty"
|
|
33
33
|
},
|
|
34
34
|
"ui.select-placeholder": {
|
|
35
|
-
"defaultMessage": "
|
|
35
|
+
"defaultMessage": "crwdns370050:0crwdne370050:0",
|
|
36
36
|
"description": "Shown in a Select's trigger while nothing is chosen and the app supplies no placeholder"
|
|
37
37
|
},
|
|
38
38
|
"ui.select-row-action": {
|
|
39
|
-
"defaultMessage": "
|
|
39
|
+
"defaultMessage": "crwdns370052:0crwdne370052:0",
|
|
40
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
41
|
},
|
|
42
42
|
"ui.toast-region": {
|
|
43
|
-
"defaultMessage": "{count
|
|
43
|
+
"defaultMessage": "crwdns370054:0count={count}crwdne370054:0",
|
|
44
44
|
"description": "Accessible label for the notifications area; count is the number of notifications showing"
|
|
45
45
|
},
|
|
46
46
|
"ui.toast-status-error": {
|
|
47
|
-
"defaultMessage": "
|
|
47
|
+
"defaultMessage": "crwdns370056:0crwdne370056:0",
|
|
48
48
|
"description": "Announced by screen readers before an error notification"
|
|
49
49
|
},
|
|
50
50
|
"ui.toast-status-info": {
|
|
51
|
-
"defaultMessage": "
|
|
51
|
+
"defaultMessage": "crwdns370058:0crwdne370058:0",
|
|
52
52
|
"description": "Announced by screen readers before an informational notification"
|
|
53
53
|
},
|
|
54
54
|
"ui.toast-status-success": {
|
|
55
|
-
"defaultMessage": "
|
|
55
|
+
"defaultMessage": "crwdns370060:0crwdne370060:0",
|
|
56
56
|
"description": "Announced by screen readers before a success notification"
|
|
57
57
|
},
|
|
58
58
|
"ui.toast-status-warning": {
|
|
59
|
-
"defaultMessage": "
|
|
59
|
+
"defaultMessage": "crwdns370062:0crwdne370062:0",
|
|
60
60
|
"description": "Announced by screen readers before a warning notification"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microbit/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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/Modal.tsx
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
ReactNode,
|
|
10
10
|
RefObject,
|
|
11
11
|
useContext,
|
|
12
|
+
useEffect,
|
|
13
|
+
useRef,
|
|
12
14
|
} from "react";
|
|
13
15
|
import {
|
|
14
16
|
Button as RACButton,
|
|
@@ -28,6 +30,92 @@ import { dataAttrs } from "./data-attrs";
|
|
|
28
30
|
import { uiMessage } from "./messages";
|
|
29
31
|
import { UnmountCallback } from "./UnmountCallback";
|
|
30
32
|
|
|
33
|
+
// react-aria's tabbable selector, which it doesn't export. It special-cases
|
|
34
|
+
// radio groups (only the checked radio is tabbable) where this doesn't — an
|
|
35
|
+
// acceptable divergence for the shim below, whose worst case on a miss is
|
|
36
|
+
// standing aside and leaving the keystroke to FocusScope, as without it.
|
|
37
|
+
const TABBABLE_SELECTOR = [
|
|
38
|
+
"input:not([disabled]):not([type=hidden])",
|
|
39
|
+
"select:not([disabled])",
|
|
40
|
+
"textarea:not([disabled])",
|
|
41
|
+
"button:not([disabled])",
|
|
42
|
+
"a[href]",
|
|
43
|
+
"area[href]",
|
|
44
|
+
"summary",
|
|
45
|
+
"iframe",
|
|
46
|
+
"object",
|
|
47
|
+
"embed",
|
|
48
|
+
"audio[controls]",
|
|
49
|
+
"video[controls]",
|
|
50
|
+
"[contenteditable]:not([contenteditable^='false'])",
|
|
51
|
+
"[tabindex]:not([disabled])",
|
|
52
|
+
]
|
|
53
|
+
.map((s) => `${s}:not([hidden]):not([tabindex='-1'])`)
|
|
54
|
+
.join(",");
|
|
55
|
+
|
|
56
|
+
const isTabbable = (element: Element) =>
|
|
57
|
+
!element.closest("[inert]") &&
|
|
58
|
+
(element.checkVisibility?.({ visibilityProperty: true }) ?? true);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Tab handling for iframes in the dialog, which react-aria's focus
|
|
62
|
+
* containment gets wrong in Firefox: FocusScope re-implements Tab as a
|
|
63
|
+
* synchronous programmatic focus of the next tabbable element, and Firefox
|
|
64
|
+
* silently drops a synchronous focus() of a cross-origin iframe made during
|
|
65
|
+
* key event dispatch — the keystroke appears to do nothing. The same call
|
|
66
|
+
* deferred to the next frame works, so this capture-phase listener (which
|
|
67
|
+
* runs before FocusScope's bubble-phase one) claims the keystroke whenever
|
|
68
|
+
* the element Tab would move to is an iframe, and performs the move
|
|
69
|
+
* deferred. Every other Tab is left to FocusScope. Remove if react-aria
|
|
70
|
+
* defers iframe focus itself.
|
|
71
|
+
*/
|
|
72
|
+
const IframeTabShim = ({
|
|
73
|
+
containerRef,
|
|
74
|
+
}: {
|
|
75
|
+
containerRef: RefObject<HTMLElement | null>;
|
|
76
|
+
}) => {
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
79
|
+
if (
|
|
80
|
+
e.key !== "Tab" ||
|
|
81
|
+
e.altKey ||
|
|
82
|
+
e.ctrlKey ||
|
|
83
|
+
e.metaKey ||
|
|
84
|
+
e.isComposing
|
|
85
|
+
) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const container = containerRef.current;
|
|
89
|
+
const active = container?.ownerDocument.activeElement;
|
|
90
|
+
if (!container || !active || !container.contains(active)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const tabbables = Array.from(
|
|
94
|
+
container.querySelectorAll<HTMLElement>(TABBABLE_SELECTOR),
|
|
95
|
+
).filter(isTabbable);
|
|
96
|
+
const index = tabbables.indexOf(active as HTMLElement);
|
|
97
|
+
if (index === -1) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const target =
|
|
101
|
+
tabbables[
|
|
102
|
+
(index + (e.shiftKey ? -1 : 1) + tabbables.length) % tabbables.length
|
|
103
|
+
];
|
|
104
|
+
if (!(target instanceof HTMLIFrameElement)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
// Keep FocusScope's own document-level handler from also acting.
|
|
109
|
+
e.stopImmediatePropagation();
|
|
110
|
+
requestAnimationFrame(() => target.focus());
|
|
111
|
+
};
|
|
112
|
+
const doc = containerRef.current?.ownerDocument ?? document;
|
|
113
|
+
doc.addEventListener("keydown", onKeyDown, true);
|
|
114
|
+
return () => doc.removeEventListener("keydown", onKeyDown, true);
|
|
115
|
+
}, [containerRef]);
|
|
116
|
+
return null;
|
|
117
|
+
};
|
|
118
|
+
|
|
31
119
|
type DialogSlots = ReturnType<typeof dialog>;
|
|
32
120
|
|
|
33
121
|
const SlotContext = createContext<{ slots: DialogSlots; onClose: () => void }>({
|
|
@@ -163,6 +251,7 @@ export const Modal = ({
|
|
|
163
251
|
// prop over the context, and so do we for the close function.
|
|
164
252
|
const triggerState = useContext(OverlayTriggerStateContext);
|
|
165
253
|
const close = onClose ?? (() => triggerState?.close());
|
|
254
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
166
255
|
const dataProps = dataAttrs(rest);
|
|
167
256
|
const slots = dialog({ size, centered: isCentered });
|
|
168
257
|
const motionlessClass = motionless
|
|
@@ -205,6 +294,7 @@ export const Modal = ({
|
|
|
205
294
|
<UnmountCallback callback={handleUnmount} />
|
|
206
295
|
<RACModal
|
|
207
296
|
{...dataProps}
|
|
297
|
+
ref={contentRef}
|
|
208
298
|
style={contentStyle}
|
|
209
299
|
className={cx(
|
|
210
300
|
slots.content,
|
|
@@ -213,6 +303,7 @@ export const Modal = ({
|
|
|
213
303
|
)}
|
|
214
304
|
>
|
|
215
305
|
<Dialog role={role} aria-label={ariaLabel} className={slots.inner}>
|
|
306
|
+
<IframeTabShim containerRef={contentRef} />
|
|
216
307
|
<SlotContext.Provider value={{ slots, onClose: close }}>
|
|
217
308
|
{children}
|
|
218
309
|
</SlotContext.Provider>
|