taga11y 1.0.0
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/LICENSE +21 -0
- package/README.md +309 -0
- package/dist/a11y.d.ts +8 -0
- package/dist/a11y.d.ts.map +1 -0
- package/dist/core/dropdown.d.ts +63 -0
- package/dist/core/dropdown.d.ts.map +1 -0
- package/dist/core/input.d.ts +42 -0
- package/dist/core/input.d.ts.map +1 -0
- package/dist/core/keyboard.d.ts +37 -0
- package/dist/core/keyboard.d.ts.map +1 -0
- package/dist/core/selection.d.ts +24 -0
- package/dist/core/selection.d.ts.map +1 -0
- package/dist/dom/chips.d.ts +6 -0
- package/dist/dom/chips.d.ts.map +1 -0
- package/dist/dom/listbox.d.ts +5 -0
- package/dist/dom/listbox.d.ts.map +1 -0
- package/dist/dom/render.d.ts +24 -0
- package/dist/dom/render.d.ts.map +1 -0
- package/dist/i18n.d.ts +44 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/index.d.ts +212 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/locales/ar.json +28 -0
- package/dist/locales/pt.json +21 -0
- package/dist/react/index.d.ts +46 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react.cjs +1 -0
- package/dist/react.mjs +83 -0
- package/dist/taga11y.cjs +1 -0
- package/dist/taga11y.css +2 -0
- package/dist/taga11y.iife.js +1 -0
- package/dist/taga11y.mjs +829 -0
- package/dist/types.d.ts +201 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/debounce.d.ts +5 -0
- package/dist/utils/debounce.d.ts.map +1 -0
- package/dist/utils/filter.d.ts +5 -0
- package/dist/utils/filter.d.ts.map +1 -0
- package/dist/utils/id.d.ts +11 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/vue/index.d.ts +155 -0
- package/dist/vue/index.d.ts.map +1 -0
- package/dist/vue.cjs +1 -0
- package/dist/vue.mjs +127 -0
- package/package.json +111 -0
- package/src/a11y.ts +85 -0
- package/src/aria-notify.d.ts +9 -0
- package/src/core/dropdown.ts +331 -0
- package/src/core/input.ts +172 -0
- package/src/core/keyboard.ts +200 -0
- package/src/core/selection.ts +78 -0
- package/src/dom/chips.ts +80 -0
- package/src/dom/listbox.ts +49 -0
- package/src/dom/render.ts +154 -0
- package/src/i18n.ts +131 -0
- package/src/index.ts +900 -0
- package/src/react/index.tsx +223 -0
- package/src/styles/index.css +367 -0
- package/src/types.ts +224 -0
- package/src/utils/debounce.ts +27 -0
- package/src/utils/filter.ts +14 -0
- package/src/utils/id.ts +30 -0
- package/src/vite-env.d.ts +1 -0
- package/src/vue/index.ts +145 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import type { ComponentPropsWithoutRef, ReactElement, Ref, RefObject } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
Taga11y,
|
|
5
|
+
type Taga11yOptions,
|
|
6
|
+
type Taga11yAddDetail,
|
|
7
|
+
type Taga11yRemoveDetail,
|
|
8
|
+
type Taga11yClearDetail,
|
|
9
|
+
type Taga11yChangeDetail,
|
|
10
|
+
type Taga11yPasteDetail,
|
|
11
|
+
type Taga11yDestroyDetail,
|
|
12
|
+
} from 'taga11y';
|
|
13
|
+
|
|
14
|
+
/** `useLayoutEffect` on the client, `useEffect` on the server — dodges React's SSR warning. */
|
|
15
|
+
const useIsomorphicLayoutEffect =
|
|
16
|
+
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
|
17
|
+
|
|
18
|
+
const OPTION_KEYS = [
|
|
19
|
+
'suggestions',
|
|
20
|
+
'maxTags',
|
|
21
|
+
'delimiter',
|
|
22
|
+
'enforceSuggestions',
|
|
23
|
+
'name',
|
|
24
|
+
'label',
|
|
25
|
+
'disabled',
|
|
26
|
+
'theme',
|
|
27
|
+
'serialize',
|
|
28
|
+
'deserialize',
|
|
29
|
+
'debounceMs',
|
|
30
|
+
'i18n',
|
|
31
|
+
] as const satisfies readonly (keyof Taga11yOptions)[];
|
|
32
|
+
|
|
33
|
+
interface Taga11yHandlers {
|
|
34
|
+
/** Fired when a tag is successfully added. */
|
|
35
|
+
onAdd?: (event: CustomEvent<Taga11yAddDetail>) => void;
|
|
36
|
+
/** Fired when a tag is removed. */
|
|
37
|
+
onRemove?: (event: CustomEvent<Taga11yRemoveDetail>) => void;
|
|
38
|
+
/** Fired when all tags are cleared. */
|
|
39
|
+
onClear?: (event: CustomEvent<Taga11yClearDetail>) => void;
|
|
40
|
+
/** Fired after every mutation, with the current values and raw event. */
|
|
41
|
+
onChange?: (values: string[], event: CustomEvent<Taga11yChangeDetail>) => void;
|
|
42
|
+
/** Fired after a bulk-import paste gesture. */
|
|
43
|
+
onPaste?: (event: CustomEvent<Taga11yPasteDetail>) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Fired when the widget is destroyed (on unmount). The component's own
|
|
46
|
+
* unmount lifecycle is the idiomatic place for teardown; this exists for
|
|
47
|
+
* 1:1 parity with the core event surface.
|
|
48
|
+
*/
|
|
49
|
+
onDestroy?: (event: CustomEvent) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type InputPassthrough = Omit<
|
|
53
|
+
ComponentPropsWithoutRef<'input'>,
|
|
54
|
+
keyof Taga11yOptions | keyof Taga11yHandlers | 'value' | 'defaultValue'
|
|
55
|
+
>;
|
|
56
|
+
|
|
57
|
+
export interface Taga11yInputProps extends Taga11yOptions, Taga11yHandlers, InputPassthrough {
|
|
58
|
+
/** Controlled tag values. When provided, the consumer owns tag state. */
|
|
59
|
+
value?: string[];
|
|
60
|
+
/** Uncontrolled initial tag values. */
|
|
61
|
+
defaultValue?: string[];
|
|
62
|
+
/** Receives the live {@link Taga11y} instance after mount (escape hatch). */
|
|
63
|
+
instanceRef?: RefObject<Taga11y | null>;
|
|
64
|
+
/**
|
|
65
|
+
* Standard React ref, resolving to the underlying `<input>` element (not the
|
|
66
|
+
* `Taga11y` instance — use {@link instanceRef} for that). Enables form
|
|
67
|
+
* libraries (e.g. react-hook-form's `{...field}`) to focus the field.
|
|
68
|
+
*/
|
|
69
|
+
ref?: Ref<HTMLInputElement>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function valuesEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
73
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Accessibility-first tagging input for React 19.
|
|
78
|
+
*
|
|
79
|
+
* Wraps the vanilla {@link Taga11y} class. Supports controlled (`value` +
|
|
80
|
+
* `onChange`) and uncontrolled (`defaultValue`) usage; forwards every core
|
|
81
|
+
* event as a callback; exposes the instance via `instanceRef`. Client-only
|
|
82
|
+
* mount keeps it SSR-safe.
|
|
83
|
+
*/
|
|
84
|
+
export function Taga11yInput(props: Taga11yInputProps): ReactElement {
|
|
85
|
+
const {
|
|
86
|
+
value,
|
|
87
|
+
defaultValue,
|
|
88
|
+
instanceRef,
|
|
89
|
+
ref,
|
|
90
|
+
onAdd,
|
|
91
|
+
onRemove,
|
|
92
|
+
onClear,
|
|
93
|
+
onChange,
|
|
94
|
+
onPaste,
|
|
95
|
+
onDestroy,
|
|
96
|
+
...rest
|
|
97
|
+
} = props;
|
|
98
|
+
|
|
99
|
+
const elRef = useRef<HTMLInputElement>(null);
|
|
100
|
+
const instRef = useRef<Taga11y | null>(null);
|
|
101
|
+
|
|
102
|
+
// Merge the internal element ref with the consumer's `ref`: keeps `elRef`
|
|
103
|
+
// valid for mount (so an incoming `ref` never clobbers it) while forwarding
|
|
104
|
+
// the node to the consumer. Stable across renders unless `ref` changes.
|
|
105
|
+
const setElement = useCallback(
|
|
106
|
+
(node: HTMLInputElement | null): void => {
|
|
107
|
+
elRef.current = node;
|
|
108
|
+
if (typeof ref === 'function') ref(node);
|
|
109
|
+
else if (ref) ref.current = node;
|
|
110
|
+
},
|
|
111
|
+
[ref],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Split remaining props into Taga11y options vs. native <input> attributes.
|
|
115
|
+
const options: Taga11yOptions = {};
|
|
116
|
+
const domProps: Record<string, unknown> = {};
|
|
117
|
+
const optionKeys = new Set<string>(OPTION_KEYS);
|
|
118
|
+
for (const key of Object.keys(rest)) {
|
|
119
|
+
const source = rest as Record<string, unknown>;
|
|
120
|
+
if (optionKeys.has(key)) (options as Record<string, unknown>)[key] = source[key];
|
|
121
|
+
else domProps[key] = source[key];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const optionsRef = useRef(options);
|
|
125
|
+
optionsRef.current = options;
|
|
126
|
+
const onDestroyRef = useRef(onDestroy);
|
|
127
|
+
onDestroyRef.current = onDestroy;
|
|
128
|
+
|
|
129
|
+
// Mount once. Layout timing (not useEffect) so the teardown — which restores
|
|
130
|
+
// the input Taga11y moved into its wrapper — runs in the commit phase before
|
|
131
|
+
// React removes host nodes; otherwise React's removeChild can't find the input.
|
|
132
|
+
// Via the isomorphic shim so it's a no-op useEffect on the server (SSR-safe).
|
|
133
|
+
useIsomorphicLayoutEffect(() => {
|
|
134
|
+
const el = elRef.current;
|
|
135
|
+
if (!el) return;
|
|
136
|
+
const inst = new Taga11y(el, optionsRef.current);
|
|
137
|
+
instRef.current = inst;
|
|
138
|
+
if (instanceRef) instanceRef.current = inst;
|
|
139
|
+
|
|
140
|
+
// Apply initial value before listeners attach → no spurious onChange on mount.
|
|
141
|
+
const initial = value ?? defaultValue;
|
|
142
|
+
if (initial && initial.length > 0) inst.setTags(initial);
|
|
143
|
+
|
|
144
|
+
const destroyHandler = (event: CustomEvent<Taga11yDestroyDetail>): void => {
|
|
145
|
+
onDestroyRef.current?.(event);
|
|
146
|
+
};
|
|
147
|
+
inst.on('taga11y:destroy', destroyHandler);
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
inst.destroy(); // fires taga11y:destroy while destroyHandler is still attached
|
|
151
|
+
inst.off('taga11y:destroy', destroyHandler);
|
|
152
|
+
instRef.current = null;
|
|
153
|
+
if (instanceRef) instanceRef.current = null;
|
|
154
|
+
};
|
|
155
|
+
// Mount-once: later prop changes are handled by the effects below.
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
// Forward changed option props via settings() without reconstructing.
|
|
159
|
+
const prevOptions = useRef<Taga11yOptions>(options);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
const inst = instRef.current;
|
|
162
|
+
if (!inst) return;
|
|
163
|
+
const changed: Partial<Taga11yOptions> = {};
|
|
164
|
+
for (const key of OPTION_KEYS) {
|
|
165
|
+
if (key === 'i18n') continue; // init-only; settings() warns + ignores
|
|
166
|
+
if (optionsRef.current[key] !== prevOptions.current[key]) {
|
|
167
|
+
(changed as Record<string, unknown>)[key] = optionsRef.current[key];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (Object.keys(changed).length > 0) inst.settings(changed);
|
|
171
|
+
prevOptions.current = optionsRef.current;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Controlled value reconciliation, guarded against feedback loops.
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (value === undefined) return; // uncontrolled
|
|
177
|
+
const inst = instRef.current;
|
|
178
|
+
if (!inst) return;
|
|
179
|
+
const current = inst.getTags().map((t) => t.value);
|
|
180
|
+
if (!valuesEqual(current, value)) inst.setTags(value);
|
|
181
|
+
}, [value]);
|
|
182
|
+
|
|
183
|
+
// Per-callback event forwarding — rebinds on identity change, no remount.
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const inst = instRef.current;
|
|
186
|
+
if (!inst || !onChange) return;
|
|
187
|
+
const h = (event: CustomEvent<Taga11yChangeDetail>): void => {
|
|
188
|
+
onChange(event.detail.tags.map((t) => t.value), event);
|
|
189
|
+
};
|
|
190
|
+
inst.on('taga11y:change', h);
|
|
191
|
+
return () => inst.off('taga11y:change', h);
|
|
192
|
+
}, [onChange]);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const inst = instRef.current;
|
|
196
|
+
if (!inst || !onAdd) return;
|
|
197
|
+
inst.on('taga11y:add', onAdd);
|
|
198
|
+
return () => inst.off('taga11y:add', onAdd);
|
|
199
|
+
}, [onAdd]);
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const inst = instRef.current;
|
|
203
|
+
if (!inst || !onRemove) return;
|
|
204
|
+
inst.on('taga11y:remove', onRemove);
|
|
205
|
+
return () => inst.off('taga11y:remove', onRemove);
|
|
206
|
+
}, [onRemove]);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const inst = instRef.current;
|
|
210
|
+
if (!inst || !onClear) return;
|
|
211
|
+
inst.on('taga11y:clear', onClear);
|
|
212
|
+
return () => inst.off('taga11y:clear', onClear);
|
|
213
|
+
}, [onClear]);
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
const inst = instRef.current;
|
|
217
|
+
if (!inst || !onPaste) return;
|
|
218
|
+
inst.on('taga11y:paste', onPaste);
|
|
219
|
+
return () => inst.off('taga11y:paste', onPaste);
|
|
220
|
+
}, [onPaste]);
|
|
221
|
+
|
|
222
|
+
return <input ref={setElement} {...(domProps as InputPassthrough)} />;
|
|
223
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
taga11y — BEM component styles
|
|
3
|
+
============================================================ */
|
|
4
|
+
|
|
5
|
+
/* ── Scoped box-sizing reset (component subtree only) ─────── */
|
|
6
|
+
.taga11y,
|
|
7
|
+
.taga11y *,
|
|
8
|
+
.taga11y *::before,
|
|
9
|
+
.taga11y *::after {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ── 9.2 / 9.3 Light-mode custom properties (defaults) ───── */
|
|
14
|
+
.taga11y {
|
|
15
|
+
/* Base (independent) tokens */
|
|
16
|
+
--taga11y-color-bg: #ffffff;
|
|
17
|
+
--taga11y-color-text: #000000;
|
|
18
|
+
--taga11y-color-border: #767676;
|
|
19
|
+
--taga11y-color-border-focus: #0060df;
|
|
20
|
+
--taga11y-color-tag-bg: #efefef;
|
|
21
|
+
--taga11y-color-error-border: #c62828;
|
|
22
|
+
|
|
23
|
+
/* Derived tokens — track their base via var(); literal is a resilience
|
|
24
|
+
fallback only (base tokens are always defined). Declared once here;
|
|
25
|
+
resolves against the cascaded base value in every theme.
|
|
26
|
+
End user can override these as he pleases */
|
|
27
|
+
--taga11y-color-tag-text: var(--taga11y-color-text, #000000);
|
|
28
|
+
--taga11y-color-tag-border: var(--taga11y-color-tag-text, #000000);
|
|
29
|
+
--taga11y-color-tag-remove-hover: var(--taga11y-color-border-focus, #0060df);
|
|
30
|
+
--taga11y-color-suggestion-bg: var(--taga11y-color-bg, #ffffff);
|
|
31
|
+
--taga11y-color-suggestion-active-bg: var(--taga11y-color-border-focus, #0060df);
|
|
32
|
+
--taga11y-color-suggestion-active-text: var(--taga11y-color-bg, #ffffff);
|
|
33
|
+
--taga11y-color-suggestion-border: var(--taga11y-color-border-focus, #0060df);
|
|
34
|
+
--taga11y-color-error-bg: var(--taga11y-color-bg, #ffffff);
|
|
35
|
+
--taga11y-color-error-text: var(--taga11y-color-text, #000000);
|
|
36
|
+
--taga11y-color-spinner: var(--taga11y-color-border-focus, #0060df);
|
|
37
|
+
|
|
38
|
+
/* Shape / typography tokens */
|
|
39
|
+
--taga11y-radius: 2px;
|
|
40
|
+
--taga11y-border-width: 1px;
|
|
41
|
+
--taga11y-outline-width: 2px;
|
|
42
|
+
--taga11y-shadow: none;
|
|
43
|
+
--taga11y-font-family: inherit;
|
|
44
|
+
--taga11y-font-size: 1em;
|
|
45
|
+
--taga11y-spacing: 1px 2px;
|
|
46
|
+
--taga11y-tag-padding-block: 1px;
|
|
47
|
+
--taga11y-tag-padding-inline: 4px;
|
|
48
|
+
--taga11y-tag-label-gap: 2px;
|
|
49
|
+
|
|
50
|
+
position: relative;
|
|
51
|
+
display: block;
|
|
52
|
+
font-family: var(--taga11y-font-family);
|
|
53
|
+
font-size: var(--taga11y-font-size);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ── 9.4 Dark mode — OS preference ─────────────────────────── */
|
|
57
|
+
@media (prefers-color-scheme: dark) {
|
|
58
|
+
.taga11y {
|
|
59
|
+
/* Only the 6 independent tokens; derived tokens resolve through their
|
|
60
|
+
single .taga11y declaration against these cascaded base values. */
|
|
61
|
+
--taga11y-color-bg: #000000;
|
|
62
|
+
--taga11y-color-text: #ffffff;
|
|
63
|
+
--taga11y-color-border: #898989;
|
|
64
|
+
--taga11y-color-border-focus: #5fb3f5;
|
|
65
|
+
--taga11y-color-tag-bg: #101010;
|
|
66
|
+
--taga11y-color-error-border: #ef9a9a;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* ── 9.4 data-theme="dark" — forced dark regardless of OS ─── */
|
|
71
|
+
.taga11y[data-theme="dark"] {
|
|
72
|
+
/* Only the 6 independent tokens; derived tokens resolve through their
|
|
73
|
+
single .taga11y declaration against these cascaded base values. */
|
|
74
|
+
--taga11y-color-bg: #000000;
|
|
75
|
+
--taga11y-color-text: #ffffff;
|
|
76
|
+
--taga11y-color-border: #898989;
|
|
77
|
+
--taga11y-color-border-focus: #5fb3f5;
|
|
78
|
+
--taga11y-color-tag-bg: #101010;
|
|
79
|
+
--taga11y-color-error-border: #ef9a9a;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── 9.4 data-theme="light" — forced light regardless of OS ── */
|
|
83
|
+
.taga11y[data-theme="light"] {
|
|
84
|
+
/* Only the 6 independent tokens; derived tokens resolve through their
|
|
85
|
+
single .taga11y declaration against these cascaded base values. */
|
|
86
|
+
--taga11y-color-bg: #ffffff;
|
|
87
|
+
--taga11y-color-text: #000000;
|
|
88
|
+
--taga11y-color-border: #767676;
|
|
89
|
+
--taga11y-color-border-focus: #0060df;
|
|
90
|
+
--taga11y-color-tag-bg: #efefef;
|
|
91
|
+
--taga11y-color-error-border: #c62828;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ── 9.1 Label ─────────────────────────────────────────────── */
|
|
95
|
+
.taga11y__label {
|
|
96
|
+
display: block;
|
|
97
|
+
margin-bottom: 4px;
|
|
98
|
+
color: var(--taga11y-color-text);
|
|
99
|
+
font-size: var(--taga11y-font-size);
|
|
100
|
+
font-family: var(--taga11y-font-family);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ── 9.1 / 9.13 Chip + input flex row ─────────────────────── */
|
|
104
|
+
.taga11y__tags {
|
|
105
|
+
/* Anchor for the listbox's viewport-flip enhancement (see @supports below). */
|
|
106
|
+
anchor-name: --taga11y-anchor;
|
|
107
|
+
display: flex;
|
|
108
|
+
flex-wrap: wrap;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: 4px;
|
|
111
|
+
padding: var(--taga11y-spacing);
|
|
112
|
+
background: var(--taga11y-color-bg);
|
|
113
|
+
border: var(--taga11y-border-width) solid var(--taga11y-color-border);
|
|
114
|
+
border-radius: var(--taga11y-radius);
|
|
115
|
+
box-shadow: var(--taga11y-shadow);
|
|
116
|
+
cursor: text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.taga11y__tags:focus-within {
|
|
120
|
+
outline: var(--taga11y-outline-width) solid var(--taga11y-color-border-focus);
|
|
121
|
+
outline-offset: calc(-1 * var(--taga11y-border-width));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* RTL: input moves to the right (reading start), chips grow leftward. */
|
|
125
|
+
.taga11y__tags:dir(rtl) { flex-direction: row-reverse; }
|
|
126
|
+
|
|
127
|
+
.taga11y__tag-list { display: contents; list-style: none; padding: 0; margin: 0; }
|
|
128
|
+
|
|
129
|
+
/* ── 9.1 Combobox input ────────────────────────────────────── */
|
|
130
|
+
.taga11y__input {
|
|
131
|
+
flex: 1 1 4ch;
|
|
132
|
+
min-width: 4ch;
|
|
133
|
+
border: none;
|
|
134
|
+
outline: none;
|
|
135
|
+
background: transparent;
|
|
136
|
+
color: var(--taga11y-color-text);
|
|
137
|
+
font-family: var(--taga11y-font-family);
|
|
138
|
+
font-size: var(--taga11y-font-size);
|
|
139
|
+
padding-block: var(--taga11y-tag-padding-block);
|
|
140
|
+
padding-inline: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ── 9.13 Listbox positioning ──────────────────────────────── */
|
|
144
|
+
.taga11y__listbox {
|
|
145
|
+
position: absolute;
|
|
146
|
+
top: 100%;
|
|
147
|
+
left: 0;
|
|
148
|
+
width: 100%;
|
|
149
|
+
z-index: 100;
|
|
150
|
+
margin: 0;
|
|
151
|
+
padding: 0;
|
|
152
|
+
list-style: none;
|
|
153
|
+
background: var(--taga11y-color-suggestion-bg);
|
|
154
|
+
border-width: var(--taga11y-outline-width);
|
|
155
|
+
border-style: solid;
|
|
156
|
+
border-color: var(--taga11y-color-suggestion-border);
|
|
157
|
+
border-radius: var(--taga11y-radius);
|
|
158
|
+
box-shadow: 0 4px 8px rgb(0 0 0 / 0.12);
|
|
159
|
+
max-height: 240px;
|
|
160
|
+
overflow-y: auto;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* ── 9.13 Listbox viewport-flip (progressive enhancement) ──── */
|
|
164
|
+
/* Flip the listbox above the input when it would be clipped below the viewport;
|
|
165
|
+
unsupported browsers keep the position:absolute rules above. fixed (not
|
|
166
|
+
absolute) so overflow is measured against the viewport, not the relative
|
|
167
|
+
wrapper. Gate needs anchor-scope too, which isolates instances. */
|
|
168
|
+
@supports (anchor-name: --a) and (anchor-scope: --a) {
|
|
169
|
+
.taga11y {
|
|
170
|
+
anchor-scope: --taga11y-anchor;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.taga11y__listbox {
|
|
174
|
+
position: fixed;
|
|
175
|
+
position-anchor: --taga11y-anchor;
|
|
176
|
+
top: anchor(bottom);
|
|
177
|
+
left: anchor(left);
|
|
178
|
+
width: anchor-size(width);
|
|
179
|
+
position-try-fallbacks: flip-block;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ── 9.5 / 9.7 Listbox option ─────────────────────────────── */
|
|
184
|
+
.taga11y__option {
|
|
185
|
+
padding: var(--taga11y-spacing);
|
|
186
|
+
color: var(--taga11y-color-text);
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
user-select: none;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Hover highlight only on hover-capable devices — touch browsers emulate
|
|
192
|
+
:hover on the last-tapped element until something else is hovered,
|
|
193
|
+
which would render alongside the aria-activedescendant highlight. */
|
|
194
|
+
@media (hover: hover) {
|
|
195
|
+
.taga11y__option:hover {
|
|
196
|
+
background: var(--taga11y-color-suggestion-active-bg);
|
|
197
|
+
color: var(--taga11y-color-suggestion-active-text);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Press feedback: fires while a finger/mouse is held, clears on lift. */
|
|
202
|
+
.taga11y__option:active {
|
|
203
|
+
background: var(--taga11y-color-suggestion-active-bg);
|
|
204
|
+
color: var(--taga11y-color-suggestion-active-text);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ── 9.7 Suggestion active state (keyboard highlight) ──────── */
|
|
208
|
+
.taga11y__option[aria-selected="true"] {
|
|
209
|
+
background: var(--taga11y-color-suggestion-active-bg);
|
|
210
|
+
color: var(--taga11y-color-suggestion-active-text);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ── 9.8 Chip (normal) ─────────────────────────────────────── */
|
|
214
|
+
.taga11y__tag {
|
|
215
|
+
display: inline-flex;
|
|
216
|
+
align-items: center;
|
|
217
|
+
gap: var(--taga11y-tag-label-gap);
|
|
218
|
+
padding: var(--taga11y-tag-padding-block) var(--taga11y-tag-padding-inline);
|
|
219
|
+
background: var(--taga11y-color-tag-bg);
|
|
220
|
+
color: var(--taga11y-color-tag-text);
|
|
221
|
+
border-radius: var(--taga11y-radius);
|
|
222
|
+
box-shadow: inset 0 0 0 1px var(--taga11y-color-tag-border);
|
|
223
|
+
font-size: var(--taga11y-font-size);
|
|
224
|
+
max-width: 100%;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* RTL: the remove (x) button renders before the label within each chip. */
|
|
228
|
+
.taga11y__tag:dir(rtl) { flex-direction: row-reverse; }
|
|
229
|
+
|
|
230
|
+
.taga11y__tag-label {
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
text-overflow: ellipsis;
|
|
233
|
+
white-space: nowrap;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* ── 9.8 Chip remove button ────────────────────────────────── */
|
|
237
|
+
.taga11y__tag-remove {
|
|
238
|
+
position: relative;
|
|
239
|
+
display: inline-flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
justify-content: center;
|
|
242
|
+
background: transparent;
|
|
243
|
+
border: none;
|
|
244
|
+
padding: 0 2px;
|
|
245
|
+
cursor: pointer;
|
|
246
|
+
color: inherit;
|
|
247
|
+
font-size: var(--taga11y-font-size);
|
|
248
|
+
border-radius: var(--taga11y-radius);
|
|
249
|
+
line-height: 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.taga11y__tag-remove::before {
|
|
253
|
+
content: '';
|
|
254
|
+
position: absolute;
|
|
255
|
+
top: 50%;
|
|
256
|
+
left: 50%;
|
|
257
|
+
transform: translate(-50%, -50%);
|
|
258
|
+
min-width: 24px;
|
|
259
|
+
min-height: 24px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.taga11y__tag-remove:hover {
|
|
263
|
+
color: var(--taga11y-color-tag-remove-hover);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ── 9.6 Focus ring on remove button ───────────────────────── */
|
|
267
|
+
.taga11y__tag-remove:focus {
|
|
268
|
+
outline: 2px solid var(--taga11y-color-border-focus);
|
|
269
|
+
outline-offset: 1px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* ── 9.8 Error chip ────────────────────────────────────────── */
|
|
273
|
+
.taga11y__tag--error {
|
|
274
|
+
background: var(--taga11y-color-error-bg);
|
|
275
|
+
color: var(--taga11y-color-error-text);
|
|
276
|
+
box-shadow: inset 0 0 0 1px var(--taga11y-color-error-border);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── 9.11 Loading indicator ────────────────────────────────── */
|
|
280
|
+
.taga11y__loading {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: 6px;
|
|
284
|
+
padding: var(--taga11y-spacing);
|
|
285
|
+
color: var(--taga11y-color-text);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.taga11y__loading[hidden] {
|
|
289
|
+
display: none;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.taga11y__loading-spinner {
|
|
293
|
+
display: inline-block;
|
|
294
|
+
width: 1em;
|
|
295
|
+
height: 1em;
|
|
296
|
+
border: 2px solid transparent;
|
|
297
|
+
border-top-color: var(--taga11y-color-spinner);
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
animation: taga11y-spin 0.7s linear infinite;
|
|
300
|
+
flex-shrink: 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
@keyframes taga11y-spin {
|
|
304
|
+
to { transform: rotate(360deg); }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.taga11y__loading-text {
|
|
308
|
+
font-size: var(--taga11y-font-size);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/* ── 9.10 Error region ─────────────────────────────────────── */
|
|
312
|
+
.taga11y__error {
|
|
313
|
+
margin-top: 2px;
|
|
314
|
+
padding: 2px 4px;
|
|
315
|
+
color: var(--taga11y-color-error-text);
|
|
316
|
+
border: var(--taga11y-border-width) solid var(--taga11y-color-error-border);
|
|
317
|
+
border-radius: var(--taga11y-radius);
|
|
318
|
+
background: var(--taga11y-color-error-bg);
|
|
319
|
+
font-size: 0.875em;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* ── 9.12 Visually-hidden announcer (stays in a11y tree) ───── */
|
|
323
|
+
.taga11y__announcer {
|
|
324
|
+
position: absolute;
|
|
325
|
+
width: 1px;
|
|
326
|
+
height: 1px;
|
|
327
|
+
padding: 0;
|
|
328
|
+
margin: -1px;
|
|
329
|
+
overflow: hidden;
|
|
330
|
+
clip: rect(0, 0, 0, 0);
|
|
331
|
+
white-space: nowrap;
|
|
332
|
+
border: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ── 9.1 Hidden form input ─────────────────────────────────── */
|
|
336
|
+
.taga11y__hidden {
|
|
337
|
+
display: none;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* ── 9.9 Disabled state ────────────────────────────────────── */
|
|
341
|
+
.taga11y--disabled {
|
|
342
|
+
opacity: 0.5;
|
|
343
|
+
cursor: not-allowed;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.taga11y--disabled .taga11y__tags,
|
|
347
|
+
.taga11y--disabled .taga11y__input {
|
|
348
|
+
cursor: not-allowed;
|
|
349
|
+
pointer-events: none;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* ── 9.14 Reduced motion ───────────────────────────────────── */
|
|
353
|
+
@media (prefers-reduced-motion: reduce) {
|
|
354
|
+
.taga11y__loading-spinner {
|
|
355
|
+
animation: none;
|
|
356
|
+
border-top-color: transparent;
|
|
357
|
+
border-color: var(--taga11y-color-spinner);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.taga11y__tag,
|
|
361
|
+
.taga11y__error,
|
|
362
|
+
.taga11y__listbox,
|
|
363
|
+
.taga11y__option {
|
|
364
|
+
transition: none;
|
|
365
|
+
animation: none;
|
|
366
|
+
}
|
|
367
|
+
}
|