@remit/ui 0.0.56 → 0.0.58
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/package.json +2 -1
- package/src/components/app-shell-types.ts +8 -1
- package/src/components/auto-moved-badge.stories.tsx +3 -21
- package/src/components/auto-moved-badge.tsx +8 -17
- package/src/components/brief-empty.stories.tsx +51 -0
- package/src/components/brief-empty.tsx +60 -0
- package/src/components/dialog.tsx +10 -5
- package/src/components/filter-clause-chip.tsx +39 -1
- package/src/components/filter-rule-editor.stories.tsx +163 -1
- package/src/components/filter-rule-editor.tsx +47 -0
- package/src/components/filter-rule.render.test.ts +33 -0
- package/src/components/filter-rule.ts +141 -0
- package/src/components/input.tsx +7 -0
- package/src/components/mail-header.tsx +10 -0
- package/src/components/message-row.tsx +41 -3
- package/src/components/mobile-search-view.render.test.ts +31 -0
- package/src/components/mobile-search-view.stories.tsx +115 -6
- package/src/components/mobile-search-view.tsx +34 -8
- package/src/components/password-input.render.test.ts +149 -0
- package/src/components/password-input.tsx +45 -0
- package/src/components/primitives.stories.tsx +28 -0
- package/src/components/search-bar.tsx +9 -0
- package/src/components/search-chip-input.tsx +85 -3
- package/src/components/search-result-row.tsx +68 -82
- package/src/components/search-results.stories.tsx +12 -0
- package/src/components/search-results.tsx +20 -8
- package/src/components/suggest-list.render.test.ts +59 -0
- package/src/components/suggest-list.tsx +96 -0
- package/src/index.ts +55 -0
- package/src/lib/property-prefill.test.ts +173 -0
- package/src/lib/property-prefill.ts +151 -0
- package/src/lib/search-rule.test.ts +73 -0
- package/src/lib/search-rule.ts +96 -0
- package/src/lib/sender-derivation.test.ts +106 -0
- package/src/lib/sender-derivation.ts +85 -0
- package/src/lib/suggest-keys.test.ts +101 -0
- package/src/lib/suggest-keys.ts +63 -0
- package/src/lib/use-long-press.ts +70 -37
- package/src/lib/use-suggest-list.test.ts +169 -0
- package/src/lib/use-suggest-list.ts +124 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type KeyboardEvent,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { suggestKeyAction } from "./suggest-keys.js";
|
|
9
|
+
|
|
10
|
+
export interface UseSuggestListInput {
|
|
11
|
+
/** How many suggestions are on offer right now. */
|
|
12
|
+
count: number;
|
|
13
|
+
/** Take the suggestion at this index. */
|
|
14
|
+
onAccept: (index: number) => void;
|
|
15
|
+
/** Extra keys that take the highlighted suggestion — see `suggestKeyAction`. */
|
|
16
|
+
acceptKeys?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The ARIA wiring a combobox input spreads onto itself. */
|
|
20
|
+
export interface ComboboxProps {
|
|
21
|
+
role: "combobox";
|
|
22
|
+
"aria-expanded": boolean;
|
|
23
|
+
"aria-controls": string;
|
|
24
|
+
"aria-autocomplete": "list";
|
|
25
|
+
"aria-activedescendant"?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Marks the field as owning Escape while its list is open, so a surrounding
|
|
28
|
+
* dialog closes on Escape only when the list is not the thing to close.
|
|
29
|
+
*/
|
|
30
|
+
"data-escape-owner"?: "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SuggestListState {
|
|
34
|
+
/** Whether the list should render. */
|
|
35
|
+
open: boolean;
|
|
36
|
+
/** The highlighted option, `-1` when the typed value is what stands. */
|
|
37
|
+
activeIndex: number;
|
|
38
|
+
setActiveIndex: (index: number) => void;
|
|
39
|
+
/** Close the list, keeping whatever is typed. */
|
|
40
|
+
dismiss: () => void;
|
|
41
|
+
/** Offer the list again — what a fresh keystroke does after a dismissal. */
|
|
42
|
+
reopen: () => void;
|
|
43
|
+
/** Returns true when the list consumed the key and the caller should stop. */
|
|
44
|
+
handleKeyDown: (event: KeyboardEvent) => boolean;
|
|
45
|
+
listId: string;
|
|
46
|
+
optionId: (index: number) => string;
|
|
47
|
+
comboboxProps: ComboboxProps;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The open/highlight state, ARIA wiring, and keyboard handling behind a
|
|
52
|
+
* suggestion list, so every typeahead in the app behaves the same way and there
|
|
53
|
+
* is one place the behaviour is defined. The caller owns the data, the markup,
|
|
54
|
+
* and what a picked suggestion means; this owns only which option is highlighted
|
|
55
|
+
* and whether the list is showing.
|
|
56
|
+
*
|
|
57
|
+
* Nothing here writes the field's value — a dismissal, a re-render, or an empty
|
|
58
|
+
* result leaves what the user typed exactly as it is.
|
|
59
|
+
*/
|
|
60
|
+
export function useSuggestList({
|
|
61
|
+
count,
|
|
62
|
+
onAccept,
|
|
63
|
+
acceptKeys,
|
|
64
|
+
}: UseSuggestListInput): SuggestListState {
|
|
65
|
+
const [dismissed, setDismissed] = useState(false);
|
|
66
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
67
|
+
const base = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
68
|
+
const listId = `suggest-${base}`;
|
|
69
|
+
const optionId = useCallback(
|
|
70
|
+
(index: number) => `suggest-${base}-option-${index}`,
|
|
71
|
+
[base],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// A changed result set is a new list: no highlight carries over onto an option
|
|
75
|
+
// the user never saw, and a list dismissed for the previous query is offered
|
|
76
|
+
// again for this one.
|
|
77
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `count` is the trigger, not a value the effect reads — a different result set is what resets the list
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
setActiveIndex(-1);
|
|
80
|
+
setDismissed(false);
|
|
81
|
+
}, [count]);
|
|
82
|
+
|
|
83
|
+
const open = count > 0 && !dismissed;
|
|
84
|
+
|
|
85
|
+
const handleKeyDown = useCallback(
|
|
86
|
+
(event: KeyboardEvent): boolean => {
|
|
87
|
+
const action = suggestKeyAction({
|
|
88
|
+
key: event.key,
|
|
89
|
+
open,
|
|
90
|
+
count,
|
|
91
|
+
activeIndex,
|
|
92
|
+
acceptKeys,
|
|
93
|
+
});
|
|
94
|
+
if (action.type === "none") return false;
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
if (action.type === "move") setActiveIndex(action.index);
|
|
97
|
+
if (action.type === "dismiss") setDismissed(true);
|
|
98
|
+
if (action.type === "accept") onAccept(action.index);
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
[open, count, activeIndex, acceptKeys, onAccept],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
open,
|
|
106
|
+
activeIndex,
|
|
107
|
+
setActiveIndex,
|
|
108
|
+
dismiss: useCallback(() => setDismissed(true), []),
|
|
109
|
+
reopen: useCallback(() => setDismissed(false), []),
|
|
110
|
+
handleKeyDown,
|
|
111
|
+
listId,
|
|
112
|
+
optionId,
|
|
113
|
+
comboboxProps: {
|
|
114
|
+
role: "combobox",
|
|
115
|
+
"aria-expanded": open,
|
|
116
|
+
"aria-controls": listId,
|
|
117
|
+
"aria-autocomplete": "list",
|
|
118
|
+
...(activeIndex >= 0
|
|
119
|
+
? { "aria-activedescendant": optionId(activeIndex) }
|
|
120
|
+
: {}),
|
|
121
|
+
...(open ? { "data-escape-owner": "" as const } : {}),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|