@lotics/ui 11.8.0 → 11.8.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/docs/data_entry.md +6 -3
- package/package.json +1 -1
- package/src/inline_date_picker.tsx +13 -2
- package/src/inline_edit.tsx +15 -2
- package/src/inline_focus.test.ts +11 -1
- package/src/inline_focus.ts +12 -0
package/docs/data_entry.md
CHANGED
|
@@ -39,9 +39,12 @@ input-swap editor — `InlineTextInput`, `InlineNumberInput`, `InlineTimePicker`
|
|
|
39
39
|
`InlineDatePicker` — opens edit mode immediately with the input focused; commit-on-blur then
|
|
40
40
|
makes Tab itself the commit, so the chain is type → Tab → type with the next editor already
|
|
41
41
|
open. Pointer focus never auto-opens (mousedown records "pointer" modality before focus fires
|
|
42
|
-
— `interaction_modality.ts`), so the click path is exactly what it always was. When
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
— `interaction_modality.ts`), so the click path is exactly what it always was. When a
|
|
43
|
+
**keyboard** close (Enter/Escape) leaves focus on `<body>`, the editor returns focus to its
|
|
44
|
+
resting view so the next Tab continues from the field. A **pointer** close (click ✓/✕, or
|
|
45
|
+
click away onto non-focusable space) does NOT restore — a mouse user has no next Tab to
|
|
46
|
+
preserve, and a bare `.focus()` would scroll the target into view, jerking the page (the
|
|
47
|
+
restore is gated on modality via `shouldRestoreFocusOnClose`).
|
|
45
48
|
|
|
46
49
|
**Typed dates.** `InlineDatePicker`'s keyboard mode is the kit's segmented `DateField`: type
|
|
47
50
|
the date in the locale's own field order (dd/MM/yyyy where the locale says so — pass
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ import { ActivityIndicator } from "./activity_indicator";
|
|
|
12
12
|
import { type InlineEditBackground, InlineEditView } from "./inline_edit";
|
|
13
13
|
import { useLoticsLocale } from "./locale";
|
|
14
14
|
import { getInteractionModality } from "./interaction_modality";
|
|
15
|
-
import { shouldOpenOnFocus } from "./inline_focus";
|
|
15
|
+
import { shouldOpenOnFocus, shouldRestoreFocusOnClose } from "./inline_focus";
|
|
16
16
|
|
|
17
17
|
export interface InlineDatePickerProps {
|
|
18
18
|
/** ISO date (`2026-05-22`) or datetime (`2026-05-22T14:30`). */
|
|
@@ -196,8 +196,19 @@ export function InlineDatePicker(props: InlineDatePickerProps) {
|
|
|
196
196
|
// from the top of the page. Return focus to the resting view, arming the
|
|
197
197
|
// suppression window so the restore doesn't re-open the session. A Tab-away
|
|
198
198
|
// blur-commit leaves focus on the next field, so this never steals it back.
|
|
199
|
+
//
|
|
200
|
+
// Gate on KEYBOARD modality (see InlineEditFrame): the restore serves keyboard
|
|
201
|
+
// Tab-order only. A pointer close (click the calendar button, or click away)
|
|
202
|
+
// also lands on <body>, but restoring focus there jumps the scroll (a bare
|
|
203
|
+
// `.focus()` scrolls the target into view, often to the top). Skip for pointer.
|
|
199
204
|
useEffect(() => {
|
|
200
|
-
if (
|
|
205
|
+
if (
|
|
206
|
+
wasEditing.current &&
|
|
207
|
+
!editing &&
|
|
208
|
+
typeof document !== "undefined" &&
|
|
209
|
+
document.activeElement === document.body &&
|
|
210
|
+
shouldRestoreFocusOnClose(getInteractionModality())
|
|
211
|
+
) {
|
|
201
212
|
suppressedAt.current = Date.now();
|
|
202
213
|
anchorRef.current?.focus();
|
|
203
214
|
}
|
package/src/inline_edit.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { colors } from "./colors";
|
|
|
9
9
|
import { FOCUS_RING, CONTROL_RADIUS, HOVER_BORDER, CONTROL_TRANSITION } from "./control_surface";
|
|
10
10
|
import { fontFamilyRegular, getInputTextStyle } from "./text_utils";
|
|
11
11
|
import { getInteractionModality } from "./interaction_modality";
|
|
12
|
-
import { shouldOpenOnFocus } from "./inline_focus";
|
|
12
|
+
import { shouldOpenOnFocus, shouldRestoreFocusOnClose } from "./inline_focus";
|
|
13
13
|
|
|
14
14
|
/** The kit's standard control height (TextInputField, NumberInput, Picker, …).
|
|
15
15
|
* The view box matches it — same height, padding, and a 1px transparent border
|
|
@@ -241,8 +241,21 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
241
241
|
// arming the suppression window so the programmatic focus doesn't re-open the
|
|
242
242
|
// editor it just closed. A Tab-away blur-commit leaves focus on the next field
|
|
243
243
|
// (not <body>), so this never steals focus back.
|
|
244
|
+
//
|
|
245
|
+
// Gate on KEYBOARD modality: this restore exists solely for keyboard Tab-order
|
|
246
|
+
// continuity. A POINTER close (click ✓/✕, or click away onto non-focusable
|
|
247
|
+
// space) also drops focus to <body>, but restoring it there both steals focus
|
|
248
|
+
// from a mouse user AND — because a bare `.focus()` scrolls the target into
|
|
249
|
+
// view — jumps the scroll position (often to the top of the page). A mouse
|
|
250
|
+
// user has no "next Tab" to preserve, so skip the restore for pointer closes.
|
|
244
251
|
useEffect(() => {
|
|
245
|
-
if (
|
|
252
|
+
if (
|
|
253
|
+
wasEditing.current &&
|
|
254
|
+
!editing &&
|
|
255
|
+
typeof document !== "undefined" &&
|
|
256
|
+
document.activeElement === document.body &&
|
|
257
|
+
shouldRestoreFocusOnClose(getInteractionModality())
|
|
258
|
+
) {
|
|
246
259
|
suppressedAt.current = Date.now();
|
|
247
260
|
viewRef.current?.focus();
|
|
248
261
|
}
|
package/src/inline_focus.test.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
3
|
import { ensureModalityListeners, getInteractionModality } from "./interaction_modality";
|
|
4
|
-
import { FOCUS_OPEN_SUPPRESS_MS, shouldOpenOnFocus } from "./inline_focus";
|
|
4
|
+
import { FOCUS_OPEN_SUPPRESS_MS, shouldOpenOnFocus, shouldRestoreFocusOnClose } from "./inline_focus";
|
|
5
|
+
|
|
6
|
+
describe("shouldRestoreFocusOnClose — no scroll jump on a pointer close", () => {
|
|
7
|
+
it("restores focus on a keyboard close (Enter/Escape) — preserves Tab order", () => {
|
|
8
|
+
expect(shouldRestoreFocusOnClose("keyboard")).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("does NOT restore on a pointer close (click ✓/✕ or click away) — a bare focus() would scroll the page", () => {
|
|
12
|
+
expect(shouldRestoreFocusOnClose("pointer")).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
5
15
|
|
|
6
16
|
describe("shouldOpenOnFocus — the inline keyboard-entry gate", () => {
|
|
7
17
|
it("keyboard focus opens the editor", () => {
|
package/src/inline_focus.ts
CHANGED
|
@@ -38,3 +38,15 @@ export function shouldOpenOnFocus(
|
|
|
38
38
|
if (modality !== "keyboard") return false;
|
|
39
39
|
return suppressedAt === null || now - suppressedAt >= FOCUS_OPEN_SUPPRESS_MS;
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* When an inline editor closes and focus has dropped to `<body>`, should it
|
|
44
|
+
* restore focus to its resting view? ONLY for a keyboard close — the restore
|
|
45
|
+
* exists solely to preserve Tab order. A POINTER close (click ✓/✕, or click
|
|
46
|
+
* away onto non-focusable space) must NOT restore: a bare `.focus()` scrolls
|
|
47
|
+
* the target into view, jumping the scroll position (often to the top), and a
|
|
48
|
+
* mouse user has no "next Tab" to preserve.
|
|
49
|
+
*/
|
|
50
|
+
export function shouldRestoreFocusOnClose(modality: InteractionModality): boolean {
|
|
51
|
+
return modality === "keyboard";
|
|
52
|
+
}
|