@eifi1/ui-kit 0.33.0 → 0.33.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/README.md +3 -3
- package/dist/account/delete-account-setting.d.ts +2 -1
- package/dist/admin/admin-action-confirm.d.ts +2 -1
- package/dist/admin/invitations-panel.d.ts +2 -1
- package/dist/admin/plan-change.d.ts +2 -1
- package/dist/admin/plan-change.js +3 -1
- package/dist/admin/plan-change.js.map +1 -1
- package/dist/admin/reviewer-scope-editor.d.ts +2 -1
- package/dist/admin/role-select.d.ts +2 -1
- package/dist/admin/transfer-ownership-dialog.d.ts +2 -1
- package/dist/admin/user-roster.d.ts +2 -1
- package/dist/auth/register-form.d.ts +2 -1
- package/dist/billing/billing-banner.d.ts +2 -1
- package/dist/billing/billing-labels.d.ts +2 -1
- package/dist/billing/billing-lock.d.ts +2 -1
- package/dist/billing/billing-overview.d.ts +2 -1
- package/dist/billing/billing-standing.d.ts +2 -1
- package/dist/billing/checkout-processing.d.ts +2 -1
- package/dist/billing/pay-page.d.ts +11 -6
- package/dist/billing/pay-page.js.map +1 -1
- package/dist/billing/plan-card.d.ts +2 -1
- package/dist/billing/plan-limit-notice.d.ts +2 -1
- package/dist/billing/plan-limit-toast.d.ts +2 -1
- package/dist/billing/subscription-actions.d.ts +2 -1
- package/dist/billing/subscription-status-chip.d.ts +2 -1
- package/dist/chart.d.ts +2 -1
- package/dist/components/amount-input.d.ts +2 -1
- package/dist/components/autocomplete.js +2 -2
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/button-group.d.ts +2 -1
- package/dist/components/calculator.d.ts +2 -1
- package/dist/components/combobox-core.js +2 -1
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js +2 -2
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +2 -1
- package/dist/components/copy-button.d.ts +2 -1
- package/dist/components/danger-confirm.d.ts +2 -1
- package/dist/components/dropdown.js +9 -1
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -1
- package/dist/components/file-button.d.ts +2 -1
- package/dist/components/file-dropzone.d.ts +2 -1
- package/dist/components/form-actions.d.ts +2 -1
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/iban-input.d.ts +2 -1
- package/dist/components/language-select.d.ts +2 -1
- package/dist/components/legal-page.d.ts +2 -1
- package/dist/components/legal.d.ts +2 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/money-field.d.ts +2 -1
- package/dist/components/number-field.d.ts +9 -1
- package/dist/components/number-field.js.map +1 -1
- package/dist/components/number-input.d.ts +29 -2
- package/dist/components/number-input.js +27 -12
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +2 -1
- package/dist/components/phone-input.d.ts +2 -1
- package/dist/components/popover.js +6 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/row-actions.d.ts +2 -1
- package/dist/components/series-chart.d.ts +2 -1
- package/dist/components/settings-fields.d.ts +2 -1
- package/dist/components/share-card.d.ts +2 -1
- package/dist/components/skeleton.d.ts +10 -2
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/text-link.d.ts +2 -1
- package/dist/components/time-input.d.ts +2 -1
- package/dist/components/ui.d.ts +2 -1
- package/dist/{delete-account-setting-DiPX4Vue.d.ts → delete-account-setting-DAgGBrbn.d.ts} +44 -7
- package/dist/feedback/feedback-attachment.d.ts +2 -1
- package/dist/feedback/feedback-context.d.ts +2 -1
- package/dist/feedback/feedback-crash.d.ts +2 -1
- package/dist/feedback/feedback-dialog.d.ts +2 -1
- package/dist/feedback/feedback-inbox.d.ts +2 -1
- package/dist/feedback/feedback-labels.d.ts +2 -1
- package/dist/feedback/feedback-menu.d.ts +2 -1
- package/dist/feedback/feedback-record.d.ts +2 -1
- package/dist/feedback/feedback-row-detail.d.ts +2 -1
- package/dist/feedback/feedback-status-undo.d.ts +2 -1
- package/dist/feedback/feedback-submit.d.ts +2 -1
- package/dist/feedback/feedback-swipe.d.ts +2 -1
- package/dist/feedback/feedback-table.d.ts +2 -1
- package/dist/feedback/feedback-thread.d.ts +3 -2
- package/dist/feedback/feedback-thread.js +5 -1
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +2 -1
- package/dist/hooks/use-anchored-panel.d.ts +37 -7
- package/dist/hooks/use-anchored-panel.js +6 -7
- package/dist/hooks/use-anchored-panel.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +2 -1
- package/dist/hooks/use-focus-trap.d.ts +8 -0
- package/dist/hooks/use-focus-trap.js +17 -11
- package/dist/hooks/use-focus-trap.js.map +1 -1
- package/dist/i18n/defaults.d.ts +2 -1
- package/dist/i18n/german.d.ts +2 -1
- package/dist/i18n/german.js +4 -1
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +2 -1
- package/dist/i18n/languages.d.ts +2 -1
- package/dist/i18n/locales/de-CH.d.ts +2 -1
- package/dist/i18n/locales/en.d.ts +2 -1
- package/dist/i18n/locales/es.d.ts +2 -1
- package/dist/i18n/locales/es.js +2 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +2 -1
- package/dist/i18n/locales/fr.js +3 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +2 -1
- package/dist/i18n/locales/hu.js +4 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +2 -1
- package/dist/i18n/locales/it.js +2 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +2 -1
- package/dist/i18n/locales/zh.js +1 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/plurals.d.ts +20 -1
- package/dist/i18n/plurals.js +11 -0
- package/dist/i18n/plurals.js.map +1 -1
- package/dist/i18n/review.d.ts +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/landing/landing-actions.d.ts +2 -1
- package/dist/landing/landing-sections.d.ts +2 -1
- package/dist/landing/public-header.d.ts +2 -1
- package/dist/landing/routing.d.ts +2 -1
- package/dist/rhf/fields.d.ts +2 -1
- package/dist/rhf/form.d.ts +2 -1
- package/dist/rhf.d.ts +2 -1
- package/dist/settings/settings-section.d.ts +2 -1
- package/dist/shell/app-shell.d.ts +2 -1
- package/dist/shell/top-bar-brand.d.ts +2 -1
- package/dist/shell.d.ts +2 -1
- package/dist/wizard/stepper-nav.d.ts +2 -1
- package/dist/wizard.d.ts +2 -1
- package/package.json +1 -1
- package/src/admin/plan-change.tsx +35 -7
- package/src/billing/pay-page.ts +9 -5
- package/src/components/autocomplete.tsx +5 -2
- package/src/components/combobox-core.tsx +5 -2
- package/src/components/combobox.tsx +4 -2
- package/src/components/dropdown.tsx +12 -2
- package/src/components/full-bleed-dialog.tsx +6 -0
- package/src/components/modal.tsx +5 -0
- package/src/components/number-field.tsx +7 -0
- package/src/components/number-input.tsx +71 -11
- package/src/components/popover.tsx +6 -2
- package/src/components/skeleton.tsx +10 -2
- package/src/feedback/feedback-thread.tsx +20 -1
- package/src/hooks/use-anchored-panel.ts +60 -14
- package/src/hooks/use-focus-trap.ts +55 -12
- package/src/i18n/german.ts +4 -1
- package/src/i18n/locales/es.ts +2 -1
- package/src/i18n/locales/fr.ts +3 -1
- package/src/i18n/locales/hu.ts +4 -2
- package/src/i18n/locales/it.ts +2 -1
- package/src/i18n/locales/zh.ts +1 -0
- package/src/i18n/plurals.ts +29 -0
- package/src/index.ts +3 -1
- package/tokens.css +18 -0
|
@@ -21,6 +21,13 @@ import { useTextSize } from "../theme/text-size";
|
|
|
21
21
|
export interface ViewportBox {
|
|
22
22
|
top: number;
|
|
23
23
|
height: number;
|
|
24
|
+
/**
|
|
25
|
+
* The height of the LAYOUT viewport itself, which `position: fixed`'s `bottom` is
|
|
26
|
+
* measured from. Only {@link anchoredPanelPlacement} reads it, for a flipped panel's
|
|
27
|
+
* `bottom`. Defaults to `top + height`: the two are the same box whenever no keyboard
|
|
28
|
+
* and no zoom shrink the visible region.
|
|
29
|
+
*/
|
|
30
|
+
layoutHeight?: number;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/**
|
|
@@ -71,9 +78,21 @@ export function readKeyboardInset({ minimum = MIN_KEYBOARD_PX }: KeyboardInsetOp
|
|
|
71
78
|
function readViewport(): ViewportBox {
|
|
72
79
|
if (typeof window === "undefined") return { top: 0, height: 0 };
|
|
73
80
|
const vv = window.visualViewport;
|
|
81
|
+
// The layout viewport's height is `innerHeight`, the reference `readKeyboardInset`
|
|
82
|
+
// measures the keyboard against. The keyboard leaves it alone (it shrinks only the
|
|
83
|
+
// VISUAL viewport; under `interactive-widget=resizes-content` it shrinks both, the
|
|
84
|
+
// fixed box with them), and it follows the mobile URL bar as it slides away, as a
|
|
85
|
+
// fixed box's `bottom` does. Not `documentElement.clientHeight`: on mobile Chrome and
|
|
86
|
+
// Safari that stays at the small, URL-bar-shown height, so with the bar scrolled away
|
|
87
|
+
// a flipped panel would sit that much too low, over its own field. `innerHeight`'s one
|
|
88
|
+
// error is a desktop page with a horizontal scrollbar, which it counts and the fixed
|
|
89
|
+
// box does not: the gap widens by the bar's thickness, and never closes.
|
|
90
|
+
const layoutHeight = window.innerHeight;
|
|
74
91
|
// offsetTop, not 0: with the keyboard up the user can also pan the visual viewport
|
|
75
92
|
// within the layout viewport, which shifts where "visible" starts.
|
|
76
|
-
return vv
|
|
93
|
+
return vv
|
|
94
|
+
? { top: vv.offsetTop, height: vv.height, layoutHeight }
|
|
95
|
+
: { top: 0, height: window.innerHeight, layoutHeight };
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
export interface AnchoredPanelOptions {
|
|
@@ -90,11 +109,24 @@ export interface AnchoredPanelOptions {
|
|
|
90
109
|
minHeight?: number;
|
|
91
110
|
}
|
|
92
111
|
|
|
112
|
+
/**
|
|
113
|
+
* `top` and `bottom` are the two edges of a panel exactly `maxHeight` tall. A panel is
|
|
114
|
+
* sized by its content and `maxHeight` only caps it, so apply the edge on the trigger's
|
|
115
|
+
* side, the one that holds whatever the height: `top` while the panel is below,
|
|
116
|
+
* `bottom` (and no `top`) once it is flipped `above`.
|
|
117
|
+
*/
|
|
93
118
|
export interface AnchoredPanel {
|
|
94
119
|
/** The trigger's rect, or null while closed/unmeasured. */
|
|
95
120
|
rect: AnchorRect | null;
|
|
96
|
-
/** Viewport-space `top` for the panel.
|
|
121
|
+
/** Viewport-space `top` for the panel — the one to apply while it is below. Flipped,
|
|
122
|
+
* it is where a FULL-height panel's top would be, and a three-row list placed by it
|
|
123
|
+
* floated far above its field (keksdose K1, live #401); place that one by `bottom`. */
|
|
97
124
|
top: number;
|
|
125
|
+
/** CSS `bottom` for a `position: fixed` panel: the distance from the layout
|
|
126
|
+
* viewport's bottom edge up to the panel's. The one to apply once it is flipped
|
|
127
|
+
* `above`: it keeps the panel's foot `gap` above the trigger however short its
|
|
128
|
+
* content, and while a filtered list shrinks or grows under the user's typing. */
|
|
129
|
+
bottom: number;
|
|
98
130
|
/** Cap that keeps the panel inside the VISIBLE viewport — apply as `maxHeight`. */
|
|
99
131
|
maxHeight: number;
|
|
100
132
|
/** Whether the panel was flipped above the trigger. */
|
|
@@ -122,6 +154,14 @@ const FLIP_GAIN = 48;
|
|
|
122
154
|
* returned `maxHeight` is what makes the flip sufficient rather than merely different:
|
|
123
155
|
* without it a tall panel placed above just runs off the top instead of the bottom.
|
|
124
156
|
*
|
|
157
|
+
* A flipped panel hangs from its BOTTOM edge, `gap` above the trigger, and is placed by
|
|
158
|
+
* `bottom` (keksdose K1, live #401). It used to be placed by a `top` worked out as
|
|
159
|
+
* though the panel were `maxHeight` tall, and a panel is only as tall as its content:
|
|
160
|
+
* a three-row address list under a 256 px cap sat ~176 px above its field, on a phone
|
|
161
|
+
* with the keyboard up. Only when even the {@link FLOOR} does not fit above does that
|
|
162
|
+
* edge give way, as `top` always did, so the panel's head (a search box, often) stays
|
|
163
|
+
* on screen rather than above it.
|
|
164
|
+
*
|
|
125
165
|
* Split out so the geometry is testable without a DOM and a fake keyboard — @hb/ui has
|
|
126
166
|
* no test runner of its own, so this is exercised from the consuming app.
|
|
127
167
|
*/
|
|
@@ -140,19 +180,17 @@ export function anchoredPanelPlacement(
|
|
|
140
180
|
(spaceBelow < preferredHeight && (spaceAbove >= preferredHeight || spaceAbove - spaceBelow >= FLIP_GAIN)));
|
|
141
181
|
const space = Math.max(FLOOR, above ? spaceAbove : spaceBelow);
|
|
142
182
|
const maxHeight = Math.min(preferredHeight, space);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
183
|
+
// Unchanged, for the apps that call the hook and apply it: the edge to use below, and
|
|
184
|
+
// a full-height panel's top above. In layout-viewport space, like the rect.
|
|
185
|
+
const top = above ? Math.max(viewTop, rect.top - gap - maxHeight) : rect.bottom + gap;
|
|
186
|
+
// `bottom` counts up from the LAYOUT viewport's bottom edge, not the visible region's:
|
|
187
|
+
// `position: fixed` is laid out in the layout viewport, which runs on behind the
|
|
188
|
+
// keyboard, exactly as the rect is measured in it. The keyboard, the pan and the zoom
|
|
189
|
+
// are already in `maxHeight`, which keeps the panel's top inside what is on screen.
|
|
190
|
+
const layoutHeight = viewport.layoutHeight ?? viewport.top + viewport.height;
|
|
191
|
+
return { top, bottom: layoutHeight - (top + maxHeight), maxHeight, above };
|
|
148
192
|
}
|
|
149
193
|
|
|
150
|
-
/**
|
|
151
|
-
* Where to put a panel anchored under `ref`, tracking the visible viewport.
|
|
152
|
-
*
|
|
153
|
-
* Callers own horizontal placement — the pickers align to the trigger's left edge, the
|
|
154
|
-
* popovers to its right — and must apply `maxHeight` with an internal `overflow-y`.
|
|
155
|
-
*/
|
|
156
194
|
/**
|
|
157
195
|
* The region actually on screen, tracked while `active` (Keksdose live #328).
|
|
158
196
|
*
|
|
@@ -204,6 +242,14 @@ export function useVisualViewport(active: boolean): ViewportBox | null {
|
|
|
204
242
|
return box;
|
|
205
243
|
}
|
|
206
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Where to put a panel anchored under `ref`, tracking the visible viewport.
|
|
247
|
+
*
|
|
248
|
+
* Callers own horizontal placement — the pickers align to the trigger's left edge, the
|
|
249
|
+
* popovers to its right — and must apply `maxHeight` with an internal `overflow-y`.
|
|
250
|
+
* Vertically, apply `top` while the panel is below and `bottom` (with no `top`) once it
|
|
251
|
+
* is `above`; see {@link AnchoredPanel}.
|
|
252
|
+
*/
|
|
207
253
|
export function useAnchoredPanel<T extends HTMLElement>(
|
|
208
254
|
ref: RefObject<T | null>,
|
|
209
255
|
open: boolean,
|
|
@@ -236,7 +282,7 @@ export function useAnchoredPanel<T extends HTMLElement>(
|
|
|
236
282
|
}, [open]);
|
|
237
283
|
|
|
238
284
|
if (!rect) {
|
|
239
|
-
return { rect: null, top: 0, maxHeight: preferredHeight, above: false };
|
|
285
|
+
return { rect: null, top: 0, bottom: 0, maxHeight: preferredHeight, above: false };
|
|
240
286
|
}
|
|
241
287
|
return { rect, ...anchoredPanelPlacement(rect, viewport, { ...options, preferredHeight, minHeight }) };
|
|
242
288
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Keep keyboard focus inside an open overlay, and give it back when it closes.
|
|
@@ -23,6 +23,10 @@ import { useEffect, useRef } from "react";
|
|
|
23
23
|
* `<body>`, which for a keyboard user means losing their place in the page
|
|
24
24
|
* entirely. When the target is gone, focus falls back to the nearest thing that
|
|
25
25
|
* still exists rather than to nothing.
|
|
26
|
+
* - **Leaves an app's own autofocus alone** (0.33.1). React honours a child's
|
|
27
|
+
* `autoFocus` while it commits, and this hook's effect runs AFTER that commit, so
|
|
28
|
+
* focusing the container there took focus straight back off the field the app had
|
|
29
|
+
* asked for. See `initialFocus`.
|
|
26
30
|
* - **Nests.** An overlay can open another — a picker inside a dialog. Only the
|
|
27
31
|
* innermost trap handles a keystroke, and closing it hands control back to the one
|
|
28
32
|
* underneath. Innermost is decided by DOM containment rather than by mount order;
|
|
@@ -66,29 +70,61 @@ export interface FocusTrapOptions {
|
|
|
66
70
|
* what `Modal` does and why: focusing the first FIELD pops the software keyboard on
|
|
67
71
|
* a phone the moment a dialog opens, before the user has asked to type.
|
|
68
72
|
* The container needs `tabIndex={-1}` for this to work.
|
|
73
|
+
*
|
|
74
|
+
* `"container"` and `"first"` both step aside when focus is ALREADY inside the
|
|
75
|
+
* container, on something other than the container itself. That is a child the app
|
|
76
|
+
* focused on purpose — an `autoFocus`, which React honours before this hook's effect
|
|
77
|
+
* runs — and the keyboard it brings up is the one the user was promised. Moving focus
|
|
78
|
+
* off it lost keksdose's create card its amount field the instant it got it, and on a
|
|
79
|
+
* phone the numpad sheet with it, which shows only while the field is focused (live
|
|
80
|
+
* #402). A function is the caller's explicit choice and is always followed.
|
|
69
81
|
*/
|
|
70
82
|
initialFocus?: "container" | "first" | (() => HTMLElement | null);
|
|
71
83
|
}
|
|
72
84
|
|
|
85
|
+
/** Whatever has focus right now; nothing on the server, where there is no document. */
|
|
86
|
+
function focusedElement(): Element | null {
|
|
87
|
+
return typeof document === "undefined" ? null : document.activeElement;
|
|
88
|
+
}
|
|
89
|
+
|
|
73
90
|
export function useFocusTrap(
|
|
74
91
|
containerRef: React.RefObject<HTMLElement | null>,
|
|
75
92
|
{ active = true, restoreFocus = true, initialFocus = "container" }: FocusTrapOptions = {},
|
|
76
93
|
): void {
|
|
77
94
|
// Latest-ref so a caller passing an inline `initialFocus` thunk — which every caller
|
|
78
95
|
// will — does not tear the trap down and re-engage it on each render, stealing focus
|
|
79
|
-
// back to the top of the panel mid-interaction.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
// back to the top of the panel mid-interaction. Written in a LAYOUT effect so that
|
|
97
|
+
// the trap's cleanup, which runs after the layout effects of the same commit, reads
|
|
98
|
+
// this render's `active` rather than the last one's — see the cleanup for why.
|
|
99
|
+
const optsRef = useRef({ active, restoreFocus, initialFocus });
|
|
100
|
+
useLayoutEffect(() => {
|
|
101
|
+
optsRef.current = { active, restoreFocus, initialFocus };
|
|
83
102
|
});
|
|
84
103
|
|
|
104
|
+
// Where focus was BEFORE the trap engaged: the element it is given back to on close.
|
|
105
|
+
//
|
|
106
|
+
// Read DURING the render that turns the trap on, and that timing is the point. React
|
|
107
|
+
// honours a child's `autoFocus` while it commits that render, before any effect runs,
|
|
108
|
+
// so the effect below asking `document.activeElement` would get the child, inside the
|
|
109
|
+
// overlay. Restoring to that on close aimed focus at the dialog that was closing —
|
|
110
|
+
// already out of the DOM, so focus fell to <body> rather than going back to the
|
|
111
|
+
// button that opened it.
|
|
112
|
+
//
|
|
113
|
+
// State adjusted during render (the pattern `Modal` uses for `open`) rather than a
|
|
114
|
+
// ref written there: set once per opening and cleared on release, so a re-render
|
|
115
|
+
// while open cannot replace it with something inside, and StrictMode's second render
|
|
116
|
+
// and second effect both see the same element. `undefined` means "not captured";
|
|
117
|
+
// `null` is a capture that found nothing.
|
|
118
|
+
const [opener, setOpener] = useState<Element | null | undefined>(undefined);
|
|
119
|
+
if (active && opener === undefined) setOpener(focusedElement());
|
|
120
|
+
else if (!active && opener !== undefined) setOpener(undefined);
|
|
121
|
+
|
|
85
122
|
useEffect(() => {
|
|
86
123
|
if (!active) return;
|
|
87
124
|
const container = containerRef.current;
|
|
88
125
|
if (!container) return;
|
|
89
126
|
|
|
90
127
|
container.setAttribute(TRAP_ATTR, "");
|
|
91
|
-
const previouslyFocused = document.activeElement as HTMLElement | null;
|
|
92
128
|
|
|
93
129
|
/** Is `node` inside a trap nested within this one? */
|
|
94
130
|
const inNestedTrap = (node: Element | null) => {
|
|
@@ -106,12 +142,13 @@ export function useFocusTrap(
|
|
|
106
142
|
);
|
|
107
143
|
|
|
108
144
|
const { initialFocus: how } = optsRef.current;
|
|
145
|
+
const focused = document.activeElement;
|
|
146
|
+
// A child that already has focus was given it on purpose — see `initialFocus`.
|
|
147
|
+
const childHasFocus = focused !== container && container.contains(focused);
|
|
109
148
|
if (typeof how === "function") {
|
|
110
149
|
(how() ?? container).focus();
|
|
111
|
-
} else if (
|
|
112
|
-
(tabbables()[0] ?? container).focus();
|
|
113
|
-
} else {
|
|
114
|
-
container.focus();
|
|
150
|
+
} else if (!childHasFocus) {
|
|
151
|
+
(how === "first" ? (tabbables()[0] ?? container) : container).focus();
|
|
115
152
|
}
|
|
116
153
|
|
|
117
154
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
@@ -149,12 +186,18 @@ export function useFocusTrap(
|
|
|
149
186
|
container.removeEventListener("keydown", onKeyDown);
|
|
150
187
|
container.removeAttribute(TRAP_ATTR);
|
|
151
188
|
|
|
189
|
+
// Only when the trap is really being released: the container has left the page,
|
|
190
|
+
// or `active` has turned false. StrictMode runs this cleanup once more right after
|
|
191
|
+
// mounting, with the overlay still up, and then the effect again — and React does
|
|
192
|
+
// not re-apply `autoFocus` for that second run. Restoring here would pull focus
|
|
193
|
+
// out of the autofocused field for good: live #402 again, in development.
|
|
194
|
+
if (container.isConnected && optsRef.current.active) return;
|
|
152
195
|
if (!optsRef.current.restoreFocus) return;
|
|
153
196
|
// `isConnected` is the whole point: restoring to a node that has been removed
|
|
154
197
|
// sends focus to <body> and the user loses their place entirely. When the
|
|
155
198
|
// original is gone, leave focus where the browser put it rather than moving it
|
|
156
199
|
// somewhere arbitrary — a wrong guess is worse than no move.
|
|
157
|
-
if (
|
|
200
|
+
if (opener?.isConnected) (opener as HTMLElement).focus?.();
|
|
158
201
|
};
|
|
159
|
-
}, [active, containerRef]);
|
|
202
|
+
}, [active, containerRef, opener]);
|
|
160
203
|
}
|
package/src/i18n/german.ts
CHANGED
|
@@ -1573,7 +1573,10 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1573
1573
|
keptBeta: "Die Beta behält ihr Ende; nur der Tarif wurde geändert.",
|
|
1574
1574
|
untilDone: (date) => `Kostenlos bis ${date}.`,
|
|
1575
1575
|
overLimit: "Über dem Limit des neuen Tarifs: Nichts wird entfernt, neue Einträge sind gesperrt.",
|
|
1576
|
-
|
|
1576
|
+
// 0.33.1: the app's word heads the line. After "von" it would want the dative
|
|
1577
|
+
// plural ("von 5 Objekten"), which no app's `one`/`other` form carries; as a
|
|
1578
|
+
// heading it is the nominative, in the form for the limit ("Objekt: 1 von 1").
|
|
1579
|
+
usageOf: (used, limit, dimension) => `${dimension}: ${used} von ${limit}`,
|
|
1577
1580
|
},
|
|
1578
1581
|
// 0.32.0 — docs/text-size-harmonization.md §6: the text-size and contrast settings in
|
|
1579
1582
|
// the "Darstellung" group.
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -1524,7 +1524,8 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
1524
1524
|
keptBeta: "La beta conserva su fin; solo ha cambiado el plan.",
|
|
1525
1525
|
untilDone: (date) => `Gratis hasta el ${date}.`,
|
|
1526
1526
|
overLimit: "Por encima del límite del nuevo plan: no se elimina nada, los elementos nuevos quedan bloqueados.",
|
|
1527
|
-
// Figures formatted; "de", as `billing.usage`.
|
|
1527
|
+
// Figures formatted; "de", as `billing.usage`. The app's word follows the limit,
|
|
1528
|
+
// in its form (0.33.1): "1 de 1 presupuesto", "1 de 5 presupuestos".
|
|
1528
1529
|
usageOf: (used, limit, dimension) => `${used} de ${limit} ${dimension}`,
|
|
1529
1530
|
},
|
|
1530
1531
|
// 0.32.0 — docs/text-size-harmonization.md §6: the text-size and contrast settings in
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -1561,7 +1561,9 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
1561
1561
|
untilDone: (date) => `Gratuit jusqu’au ${date}.`,
|
|
1562
1562
|
overLimit:
|
|
1563
1563
|
"Au-delà de la limite de la nouvelle formule\u00a0: rien n’est supprimé, les nouveaux éléments sont bloqués.",
|
|
1564
|
-
// Figures formatted; "sur", as `billing.usage`.
|
|
1564
|
+
// Figures formatted; "sur", as `billing.usage`. The app's word follows the limit,
|
|
1565
|
+
// in its form (0.33.1): "1 sur 1 budget", "1 sur 5 budgets" — and French reads 0
|
|
1566
|
+
// as singular, which the forms' `one` covers.
|
|
1565
1567
|
usageOf: (used, limit, dimension) => `${used} sur ${limit} ${dimension}`,
|
|
1566
1568
|
},
|
|
1567
1569
|
// 0.32.0 — docs/text-size-harmonization.md §6: the text-size and contrast settings in
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -1531,8 +1531,10 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
1531
1531
|
keptBeta: "A béta megtartja a lejáratát; csak a csomag változott.",
|
|
1532
1532
|
untilDone: (date) => `Ingyenes eddig: ${date}.`,
|
|
1533
1533
|
overLimit: "Az új csomag korlátja felett: semmi sem törlődik, de újak nem hozhatók létre.",
|
|
1534
|
-
// The figures bare, as `billing.usage` (convention 3)
|
|
1535
|
-
|
|
1534
|
+
// The figures bare, as `billing.usage` (convention 3), and the app's word after
|
|
1535
|
+
// them, as `dataTable.filterResults` ("1 / 5 költségvetés"): since 0.33.1 it is the
|
|
1536
|
+
// word as it reads after a number, which stays singular here (convention 2).
|
|
1537
|
+
usageOf: (used, limit, dimension) => `${used} / ${limit} ${dimension}`,
|
|
1536
1538
|
},
|
|
1537
1539
|
// 0.32.0 — docs/text-size-harmonization.md §6: the text-size and contrast settings in
|
|
1538
1540
|
// the "Megjelenés" group.
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -1541,7 +1541,8 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
1541
1541
|
keptBeta: "La beta mantiene la sua scadenza; è cambiato solo il piano.",
|
|
1542
1542
|
untilDone: (date) => `Gratuito fino al ${date}.`,
|
|
1543
1543
|
overLimit: "Oltre il limite del nuovo piano: non viene rimosso nulla, i nuovi elementi sono bloccati.",
|
|
1544
|
-
// Figures formatted; "di", as `billing.usage`.
|
|
1544
|
+
// Figures formatted; "di", as `billing.usage`. The app's word follows the limit,
|
|
1545
|
+
// in its form (0.33.1): "1 di 1 budget", "1 di 5 budget".
|
|
1545
1546
|
usageOf: (used, limit, dimension) => `${used} di ${limit} ${dimension}`,
|
|
1546
1547
|
},
|
|
1547
1548
|
// 0.32.0 — docs/text-size-harmonization.md §6: the text-size and contrast settings in
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -1464,6 +1464,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
1464
1464
|
keptBeta: "测试版保留其结束日期;只更改了套餐。",
|
|
1465
1465
|
untilDone: (date) => `免费至 ${date}。`,
|
|
1466
1466
|
overLimit: "超出新套餐的上限:不会删除任何内容,但无法新建。",
|
|
1467
|
+
// No plural: the app's word reads the same after any number (0.33.1).
|
|
1467
1468
|
usageOf: (used, limit, dimension) => `${used}/${limit} ${dimension}`,
|
|
1468
1469
|
},
|
|
1469
1470
|
// 0.32.0 — docs/text-size-harmonization.md §6: the text-size and contrast settings in
|
package/src/i18n/plurals.ts
CHANGED
|
@@ -52,3 +52,32 @@ function pluralCategories(locale: string, type: Intl.PluralRuleType): readonly s
|
|
|
52
52
|
return [];
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A word in the plural forms a count can ask for, `other` required: an app's
|
|
58
|
+
* `key_one` / `key_other` pair, or any of CLDR's categories (`zero`, `one`, `two`, `few`,
|
|
59
|
+
* `many`). A category left out reads `other`, the stand-in {@link withAllPlurals} fills
|
|
60
|
+
* a catalogue with — so English's two forms serve every language, as in the apps'
|
|
61
|
+
* catalogues.
|
|
62
|
+
*
|
|
63
|
+
* { one: t("planUsage.budget_one"), other: t("planUsage.budget_other") }
|
|
64
|
+
*/
|
|
65
|
+
export type PluralForms = { other: string } & Partial<Record<Intl.LDMLPluralRule, string>>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The form of `forms` that `count` takes in `locale` — CLDR's cardinal category for it,
|
|
69
|
+
* through `Intl.PluralRules`, else `other`. A locale `Intl` cannot read reads as the
|
|
70
|
+
* runtime's, as everywhere the kit formats (and `undefined` is the runtime's). For the
|
|
71
|
+
* kit's own parts that put an app's word after a count: `planColumn`'s usage.
|
|
72
|
+
*/
|
|
73
|
+
export function pluralForm(count: number, forms: PluralForms, locale?: string): string {
|
|
74
|
+
return forms[pluralCategory(count, locale)] ?? forms.other;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pluralCategory(count: number, locale: string | undefined): Intl.LDMLPluralRule {
|
|
78
|
+
try {
|
|
79
|
+
return new Intl.PluralRules(locale).select(count);
|
|
80
|
+
} catch {
|
|
81
|
+
return new Intl.PluralRules().select(count);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -402,8 +402,10 @@ export { kitLabelStrings } from "./i18n/review";
|
|
|
402
402
|
export type { KitLabelStringsOptions } from "./i18n/review";
|
|
403
403
|
// The seven languages, how an app resolves one, and the kit catalogue loader for it.
|
|
404
404
|
export * from "./i18n/languages";
|
|
405
|
-
// CLDR plural categories for an app's i18next catalogue, filled from `_other
|
|
405
|
+
// CLDR plural categories for an app's i18next catalogue, filled from `_other`; and the
|
|
406
|
+
// forms of an app's word a kit part picks from by count (0.33.1, `PlanColumnUsage.label`).
|
|
406
407
|
export { withAllPlurals } from "./i18n/plurals";
|
|
408
|
+
export type { PluralForms } from "./i18n/plurals";
|
|
407
409
|
// Translation review (0.19): one contract for kastlan's and keksdose's review pages —
|
|
408
410
|
// the pure rows/status/placeholder logic and the presentational page parts.
|
|
409
411
|
export {
|
package/tokens.css
CHANGED
|
@@ -192,6 +192,24 @@ html[data-text-size="xlarge"] {
|
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/* The same for FIGURES — an amount, a date, a count with its unit (0.33.1, keksdose):
|
|
196
|
+
`truncate` at Normal, and from Large up a wrap at spaces only (`overflow-wrap: normal`).
|
|
197
|
+
`truncate-until-large` breaks anywhere, so at Large it split an amount mid-number
|
|
198
|
+
("CHF 1’2" over "34.50"), which reads as two numbers. Here "CHF" and "1’234.50" may
|
|
199
|
+
take two lines, but a number never splits: one wider than its box is cut at its end
|
|
200
|
+
instead, so give a figure's cell the room for it (`shrink-0`). Prose — a name, a
|
|
201
|
+
title, a sentence — keeps `truncate-until-large`, where breaking a long word is what
|
|
202
|
+
keeps the line on the screen. */
|
|
203
|
+
@utility truncate-until-large-words {
|
|
204
|
+
overflow: hidden;
|
|
205
|
+
text-overflow: ellipsis;
|
|
206
|
+
white-space: nowrap;
|
|
207
|
+
@variant large {
|
|
208
|
+
white-space: normal;
|
|
209
|
+
overflow-wrap: normal;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
195
213
|
:root {
|
|
196
214
|
--bg-page: #ede3cf; /* warm dim sand — low-glare light page */
|
|
197
215
|
--bg-surface: #f6ecd9; /* warm off-white card/panel (not pure white) */
|