@antadesign/anta 0.3.10 → 0.3.11
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/dist/components/Button.d.ts +32 -17
- package/dist/components/Button.js +8 -6
- package/dist/components/ButtonCopy.d.ts +13 -7
- package/dist/components/MenuItem.d.ts +3 -2
- package/dist/components/MenuItem.js +6 -4
- package/dist/components/MenuItemCopy.d.ts +3 -2
- package/dist/components/Select.d.ts +8 -0
- package/dist/components/Select.js +4 -0
- package/dist/components/SelectFaceted.d.ts +8 -0
- package/dist/components/SelectFaceted.js +4 -0
- package/dist/elements/a-button.d.ts +4 -1
- package/dist/elements/a-button.js +18 -8
- package/dist/elements/a-expander.js +20 -1
- package/dist/elements/a-menu-item.d.ts +8 -1
- package/dist/elements/a-menu-item.js +10 -10
- package/dist/elements/a-menu.d.ts +6 -2
- package/dist/elements/a-menu.js +34 -3
- package/dist/elements/copy-behavior.d.ts +25 -8
- package/dist/elements/copy-behavior.js +20 -18
- package/dist/elements/key-activation.d.ts +32 -0
- package/dist/elements/key-activation.js +33 -0
- package/dist/general_types.d.ts +17 -15
- package/package.json +1 -1
|
@@ -55,7 +55,10 @@ export type ContentMode = {
|
|
|
55
55
|
iconTrailing?: IconShape;
|
|
56
56
|
};
|
|
57
57
|
/** Submit axis — anchors (href) don't carry form-submission props; buttons
|
|
58
|
-
* don't carry anchor props.
|
|
58
|
+
* don't carry anchor props. A link (`href`) also can't be a copy control, so the
|
|
59
|
+
* copy props live only on the non-link branch (via `CopyMode`) and are `never`
|
|
60
|
+
* on the link branch — a `<ButtonCopy href=…>` or `<Button href=… copy=…>` is a
|
|
61
|
+
* type error, not a silently-ignored attribute. */
|
|
59
62
|
export type SubmitMode = {
|
|
60
63
|
/** Renders as `<a role="button">` instead of `<a-button>`. */
|
|
61
64
|
href: string;
|
|
@@ -69,7 +72,13 @@ export type SubmitMode = {
|
|
|
69
72
|
ping?: string;
|
|
70
73
|
type?: never;
|
|
71
74
|
form?: never;
|
|
72
|
-
|
|
75
|
+
copy?: never;
|
|
76
|
+
copyNode?: never;
|
|
77
|
+
copyUrl?: never;
|
|
78
|
+
copyWithUrl?: never;
|
|
79
|
+
onCopyRequest?: never;
|
|
80
|
+
onCopied?: never;
|
|
81
|
+
} | ({
|
|
73
82
|
href?: never;
|
|
74
83
|
target?: never;
|
|
75
84
|
rel?: never;
|
|
@@ -79,7 +88,7 @@ export type SubmitMode = {
|
|
|
79
88
|
type?: 'button' | 'submit' | 'reset';
|
|
80
89
|
/** Form id when the button isn't a descendant of its form. */
|
|
81
90
|
form?: string;
|
|
82
|
-
};
|
|
91
|
+
} & CopyMode);
|
|
83
92
|
/** Priority axis — `underline` only on `tertiary` / `quaternary`,
|
|
84
93
|
* `paddingless` only on `quaternary`. */
|
|
85
94
|
export type PriorityMode = {
|
|
@@ -102,21 +111,29 @@ export type PriorityMode = {
|
|
|
102
111
|
};
|
|
103
112
|
/** Copy axis — turns the button into a copy control that writes to the clipboard
|
|
104
113
|
* on click and flashes a success / failure state (see `<a-button>`'s copy
|
|
105
|
-
* behavior). **Exactly one** of `copy`, `copyNode`, or `
|
|
114
|
+
* behavior). **Exactly one** of `copy`, `copyNode`, or `copyUrl` may be set;
|
|
106
115
|
* the union makes the others `never` in each mode. For the batteries-included
|
|
107
116
|
* preset, use `ButtonCopy`. */
|
|
108
117
|
export type CopyMode = {
|
|
109
118
|
copy?: never;
|
|
110
119
|
copyNode?: never;
|
|
111
|
-
|
|
120
|
+
copyUrl?: never;
|
|
121
|
+
copyWithUrl?: never;
|
|
112
122
|
onCopied?: never;
|
|
113
123
|
onCopyRequest?: never;
|
|
114
124
|
} | {
|
|
115
125
|
/** Text copied to the clipboard on click. */
|
|
116
126
|
copy: string;
|
|
127
|
+
/** Prefix the copied text with `// URL: <current page URL>`. */
|
|
128
|
+
copyWithUrl?: boolean;
|
|
129
|
+
/** Compute the copy content lazily. Fires on **pointerdown**; update `copy`
|
|
130
|
+
* (a state change) here and the click copies the latest value. The
|
|
131
|
+
* pointerdown→click gap lets the update land even when this handler runs
|
|
132
|
+
* off the UI thread (a worker-rendered app) — only the serializable `copy`
|
|
133
|
+
* string crosses, via the normal re-render. */
|
|
134
|
+
onCopyRequest?: () => void;
|
|
117
135
|
copyNode?: never;
|
|
118
|
-
|
|
119
|
-
onCopyRequest?: never;
|
|
136
|
+
copyUrl?: never;
|
|
120
137
|
/** Fires after the copy attempt with whether it succeeded. */
|
|
121
138
|
onCopied?: (ok: boolean) => void;
|
|
122
139
|
} | {
|
|
@@ -126,24 +143,22 @@ export type CopyMode = {
|
|
|
126
143
|
* the copied output. */
|
|
127
144
|
copyNode: boolean | string;
|
|
128
145
|
copy?: never;
|
|
129
|
-
|
|
146
|
+
copyUrl?: never;
|
|
147
|
+
copyWithUrl?: never;
|
|
130
148
|
onCopyRequest?: never;
|
|
131
149
|
/** Fires after the copy attempt with whether it succeeded. */
|
|
132
150
|
onCopied?: (ok: boolean) => void;
|
|
133
151
|
} | {
|
|
134
|
-
/**
|
|
135
|
-
|
|
136
|
-
* with the value — synchronously, or after an `await` (the browser's
|
|
137
|
-
* transient-activation window still covers the write). */
|
|
138
|
-
copyLazy: true;
|
|
152
|
+
/** Copy the current page URL (`location.href`). */
|
|
153
|
+
copyUrl: true;
|
|
139
154
|
copy?: never;
|
|
140
155
|
copyNode?: never;
|
|
141
|
-
|
|
142
|
-
onCopyRequest
|
|
156
|
+
copyWithUrl?: never;
|
|
157
|
+
onCopyRequest?: never;
|
|
143
158
|
/** Fires after the copy attempt with whether it succeeded. */
|
|
144
159
|
onCopied?: (ok: boolean) => void;
|
|
145
160
|
};
|
|
146
|
-
export type ButtonProps = BaseButtonProps & PriorityMode & ContentMode & SubmitMode &
|
|
161
|
+
export type ButtonProps = BaseButtonProps & PriorityMode & ContentMode & SubmitMode & BaseProps;
|
|
147
162
|
/**
|
|
148
163
|
* Action button.
|
|
149
164
|
*
|
|
@@ -164,4 +179,4 @@ export type ButtonProps = BaseButtonProps & PriorityMode & ContentMode & SubmitM
|
|
|
164
179
|
* <Button href="/docs" target="_blank" label="Read the docs" />
|
|
165
180
|
* ```
|
|
166
181
|
*/
|
|
167
|
-
export declare const Button: ({ priority, tone, underline, icon, iconTrailing, paddingless, label, size, loading, disabled, selected, round, href, type, form, copy, copyNode,
|
|
182
|
+
export declare const Button: ({ priority, tone, underline, icon, iconTrailing, paddingless, label, size, loading, disabled, selected, round, href, type, form, copy, copyNode, copyUrl, copyWithUrl, onCopied, onCopyRequest, className, style, children, ...rest }: ButtonProps) => any;
|
|
@@ -18,7 +18,8 @@ const Button = ({
|
|
|
18
18
|
form,
|
|
19
19
|
copy,
|
|
20
20
|
copyNode,
|
|
21
|
-
|
|
21
|
+
copyUrl,
|
|
22
|
+
copyWithUrl,
|
|
22
23
|
onCopied,
|
|
23
24
|
onCopyRequest,
|
|
24
25
|
className,
|
|
@@ -26,7 +27,7 @@ const Button = ({
|
|
|
26
27
|
children,
|
|
27
28
|
...rest
|
|
28
29
|
}) => {
|
|
29
|
-
const isCopy = copy != null || copyNode != null ||
|
|
30
|
+
const isCopy = copy != null || copyNode != null || copyUrl === true;
|
|
30
31
|
const toneAttr = tone || void 0;
|
|
31
32
|
const computedStyle = roundStyle(round, "--button-round", toneStyle(toneAttr, "--button-tone-source", style));
|
|
32
33
|
const leadingIcon = icon ?? (isCopy ? "copy" : void 0);
|
|
@@ -67,14 +68,15 @@ const Button = ({
|
|
|
67
68
|
// Copy behavior — the element reads these and performs the write itself.
|
|
68
69
|
copy: copy != null ? copy : void 0,
|
|
69
70
|
"copy-node": copyNode === true ? "" : typeof copyNode === "string" ? copyNode : void 0,
|
|
70
|
-
"copy-
|
|
71
|
+
"copy-url": copyUrl ? "" : void 0,
|
|
72
|
+
"copy-with-url": copyWithUrl ? "" : void 0,
|
|
71
73
|
// Marks the control so `copy-node` serialization strips it from the copied
|
|
72
74
|
// node (a copy button inside the copied region shouldn't paste itself).
|
|
73
75
|
"data-copy-node-button": copyNode != null && copyNode !== false ? "" : void 0,
|
|
74
76
|
oncopydone: onCopied ? (e) => onCopied(nativeStateChange(e).detail?.ok ?? false) : void 0,
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
oncopyrequest: onCopyRequest ? (
|
|
77
|
+
// Fired on pointerdown for a lazy copy — the consumer updates `copy` here
|
|
78
|
+
// (a state change) and the click copies the latest value. No payload crosses.
|
|
79
|
+
oncopyrequest: onCopyRequest ? () => onCopyRequest() : void 0,
|
|
78
80
|
class: className,
|
|
79
81
|
style: computedStyle
|
|
80
82
|
};
|
|
@@ -7,15 +7,16 @@ export type ButtonCopyProps = ButtonProps & ({
|
|
|
7
7
|
} | {
|
|
8
8
|
copyNode: boolean | string;
|
|
9
9
|
} | {
|
|
10
|
-
|
|
10
|
+
copyUrl: true;
|
|
11
11
|
});
|
|
12
12
|
/**
|
|
13
13
|
* Copy button — a `Button` preset for copy-to-clipboard. Set `copy` for a literal
|
|
14
|
-
* string, `copyNode` to copy a DOM region, or `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* string, `copyNode` to copy a DOM region, or `copyUrl` to copy the current page
|
|
15
|
+
* URL. For content computed on demand, keep `copy` reactive and refresh it in
|
|
16
|
+
* `onCopyRequest` (fired on pointerdown). The `<a-button>` element performs the
|
|
17
|
+
* write and reports the result; this wrapper flips the leading icon to a check
|
|
18
|
+
* (success) or ✕ (failure) and retones to `success` / `critical` for ~2s — a JSX
|
|
19
|
+
* re-render, no element state (see `useCopyFeedback`).
|
|
19
20
|
*
|
|
20
21
|
* With a `label` it's a labeled button; without one it's an icon-only copy button.
|
|
21
22
|
* Everything else is a normal `Button` prop — `tone`, `priority`, `size`, `icon`
|
|
@@ -26,7 +27,12 @@ export type ButtonCopyProps = ButtonProps & ({
|
|
|
26
27
|
* <ButtonCopy copy="npm i @antadesign/anta" label="Copy install" />
|
|
27
28
|
* <ButtonCopy copy="https://anta.design" /> // icon-only
|
|
28
29
|
* <ButtonCopy copyNode=".snippet" label="Copy block" priority="tertiary" />
|
|
29
|
-
* <ButtonCopy
|
|
30
|
+
* <ButtonCopy copyUrl label="Copy link" /> // copies location.href
|
|
31
|
+
* <ButtonCopy copy={code} copyWithUrl label="Copy snippet" /> // code + source URL
|
|
32
|
+
*
|
|
33
|
+
* // Lazy: compute on pointerdown, copied on the click that follows.
|
|
34
|
+
* const [report, setReport] = useState('')
|
|
35
|
+
* <ButtonCopy copy={report} onCopyRequest={() => setReport(buildReport())} label="Copy report" />
|
|
30
36
|
* ```
|
|
31
37
|
*/
|
|
32
38
|
export declare const ButtonCopy: ({ icon, tone, onCopied, ...rest }: ButtonCopyProps) => any;
|
|
@@ -86,7 +86,8 @@ export type MenuItemLinkMode = {
|
|
|
86
86
|
indicator?: never;
|
|
87
87
|
copy?: never;
|
|
88
88
|
copyNode?: never;
|
|
89
|
-
|
|
89
|
+
copyUrl?: never;
|
|
90
|
+
copyWithUrl?: never;
|
|
90
91
|
onCopied?: never;
|
|
91
92
|
onCopyRequest?: never;
|
|
92
93
|
};
|
|
@@ -141,4 +142,4 @@ export type MenuItemProps = MenuItemCommonProps & (MenuItemLinkMode | (MenuItemA
|
|
|
141
142
|
* </MenuItem>
|
|
142
143
|
* ```
|
|
143
144
|
*/
|
|
144
|
-
export declare const MenuItem: ({ icon, label, hint, kbd, iconTrailing, disabled, selected, selectionIndicator, indeterminate, indicator, tone, toneSelected, submenu, value, onSelect, href, target, rel, download, ping, copy, copyNode,
|
|
145
|
+
export declare const MenuItem: ({ icon, label, hint, kbd, iconTrailing, disabled, selected, selectionIndicator, indeterminate, indicator, tone, toneSelected, submenu, value, onSelect, href, target, rel, download, ping, copy, copyNode, copyUrl, copyWithUrl, onCopied, onCopyRequest, role: roleOverride, className, style, children, ...rest }: MenuItemProps) => any;
|
|
@@ -23,7 +23,8 @@ const MenuItem = ({
|
|
|
23
23
|
ping,
|
|
24
24
|
copy,
|
|
25
25
|
copyNode,
|
|
26
|
-
|
|
26
|
+
copyUrl,
|
|
27
|
+
copyWithUrl,
|
|
27
28
|
onCopied,
|
|
28
29
|
onCopyRequest,
|
|
29
30
|
role: roleOverride,
|
|
@@ -32,7 +33,7 @@ const MenuItem = ({
|
|
|
32
33
|
children,
|
|
33
34
|
...rest
|
|
34
35
|
}) => {
|
|
35
|
-
const isCopy = copy != null || copyNode != null ||
|
|
36
|
+
const isCopy = copy != null || copyNode != null || copyUrl === true;
|
|
36
37
|
const leadingIcon = icon ?? (isCopy ? "copy" : void 0);
|
|
37
38
|
const checkable = selectionIndicator === "checkbox" || selectionIndicator === "radio" || selectionIndicator === "check";
|
|
38
39
|
const role = selectionIndicator === "checkbox" ? "menuitemcheckbox" : selectionIndicator === "radio" || selectionIndicator === "check" ? "menuitemradio" : "menuitem";
|
|
@@ -85,10 +86,11 @@ const MenuItem = ({
|
|
|
85
86
|
onmenuselect: onSelect ? (e) => onSelect(e, { value, label }) : void 0,
|
|
86
87
|
copy: copy != null ? copy : void 0,
|
|
87
88
|
"copy-node": copyNode === true ? "" : typeof copyNode === "string" ? copyNode : void 0,
|
|
88
|
-
"copy-
|
|
89
|
+
"copy-url": copyUrl ? "" : void 0,
|
|
90
|
+
"copy-with-url": copyWithUrl ? "" : void 0,
|
|
89
91
|
"data-copy-node-button": copyNode != null && copyNode !== false ? "" : void 0,
|
|
90
92
|
oncopydone: onCopied ? (e) => onCopied(nativeStateChange(e).detail?.ok ?? false) : void 0,
|
|
91
|
-
oncopyrequest: onCopyRequest ? (
|
|
93
|
+
oncopyrequest: onCopyRequest ? () => onCopyRequest() : void 0,
|
|
92
94
|
class: className,
|
|
93
95
|
...rest,
|
|
94
96
|
children: [
|
|
@@ -8,12 +8,13 @@ export type MenuItemCopyProps = MenuItemCommonProps & MenuItemActionMode & CopyM
|
|
|
8
8
|
} | {
|
|
9
9
|
copyNode: boolean | string;
|
|
10
10
|
} | {
|
|
11
|
-
|
|
11
|
+
copyUrl: true;
|
|
12
12
|
});
|
|
13
13
|
/**
|
|
14
14
|
* Copying menu item — a `MenuItem` preset for copy-to-clipboard rows inside a
|
|
15
15
|
* `Menu`. Set `copy` for a literal string, `copyNode` to copy a DOM region, or
|
|
16
|
-
* `
|
|
16
|
+
* `copyUrl` to copy the current page URL. For content computed on demand, keep
|
|
17
|
+
* `copy` reactive and refresh it in `onCopyRequest` (fired on pointerdown). The
|
|
17
18
|
* `<a-menu-item>` element performs the write when the row is chosen; this wrapper
|
|
18
19
|
* flips the leading icon to a check / ✕ and retones for ~2s (see `useCopyFeedback`).
|
|
19
20
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BaseProps } from '../general_types';
|
|
2
2
|
import type { IconShape } from '../elements/a-icon.shapes';
|
|
3
|
+
import { type MenuProps } from './Menu';
|
|
3
4
|
/** One option in a `<Select>`. Pass a bare string as shorthand for
|
|
4
5
|
* `{ value: s, label: s }`. Carries an index signature so you can attach
|
|
5
6
|
* arbitrary fields (a `ranAt` date, a `status`, …) and read them back in
|
|
@@ -131,6 +132,13 @@ export interface SelectCommonProps extends Omit<BaseProps, 'children'> {
|
|
|
131
132
|
* logical pick (both rows toggle together; the trigger resolves to the last). Dev
|
|
132
133
|
* builds `console.warn` on a duplicate. */
|
|
133
134
|
options: SelectItem[];
|
|
135
|
+
/** Preferred placement of the options menu relative to the trigger. The menu
|
|
136
|
+
* auto-flips vertically and clamps horizontally when needed.
|
|
137
|
+
* @defaultValue bottom-start */
|
|
138
|
+
placement?: MenuProps['placement'];
|
|
139
|
+
/** Gap in pixels between the trigger and the options menu.
|
|
140
|
+
* @defaultValue 4 */
|
|
141
|
+
offset?: number;
|
|
134
142
|
/** The per-row mark for **single**-select: `'none'` (a tint-only highlight),
|
|
135
143
|
* `'check'` (a trailing checkmark on the selected row, keeping the tint — the
|
|
136
144
|
* canonical Select look), or `'radio'` (a leading radio on every row).
|
|
@@ -37,6 +37,8 @@ function optionsWithSelection(options, values) {
|
|
|
37
37
|
const Select = (props) => {
|
|
38
38
|
const {
|
|
39
39
|
options,
|
|
40
|
+
placement,
|
|
41
|
+
offset,
|
|
40
42
|
selection,
|
|
41
43
|
indicator,
|
|
42
44
|
value,
|
|
@@ -274,6 +276,8 @@ const Select = (props) => {
|
|
|
274
276
|
/* @__PURE__ */ jsxs(
|
|
275
277
|
Menu,
|
|
276
278
|
{
|
|
279
|
+
placement,
|
|
280
|
+
offset,
|
|
277
281
|
onStateChange: (_e, { next }) => {
|
|
278
282
|
setOpen(next);
|
|
279
283
|
if (!next) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BaseProps } from '../general_types';
|
|
2
2
|
import type { IconShape } from '../elements/a-icon.shapes';
|
|
3
3
|
import type { SelectItem, SelectOption } from './Select';
|
|
4
|
+
import { type MenuProps } from './Menu';
|
|
4
5
|
interface FacetBase {
|
|
5
6
|
/** The facet's namespace — the key its value is stored under in the value
|
|
6
7
|
* record, and what `onValueChange`'s `attrs.facet` reports. Unique across
|
|
@@ -108,6 +109,13 @@ export interface SelectFacetedTriggerState {
|
|
|
108
109
|
export interface SelectFacetedProps extends Omit<BaseProps, 'children'> {
|
|
109
110
|
/** The facets (dimensions) to filter across, each with its own editor kind. */
|
|
110
111
|
facets: SelectFacet[];
|
|
112
|
+
/** Preferred placement of the root filter menu relative to its trigger. The
|
|
113
|
+
* menu auto-flips vertically and clamps horizontally when needed.
|
|
114
|
+
* @defaultValue bottom-start */
|
|
115
|
+
placement?: MenuProps['placement'];
|
|
116
|
+
/** Gap in pixels between the trigger and the filter menu.
|
|
117
|
+
* @defaultValue 4 */
|
|
118
|
+
offset?: number;
|
|
111
119
|
/** Controlled value — the facet-keyed record. When provided, the consumer
|
|
112
120
|
* owns state: a pick only *requests* a change via `onValueChange` (reject by
|
|
113
121
|
* not updating). Leave undefined for uncontrolled. */
|
|
@@ -27,6 +27,8 @@ const defaultMatch = (o, query) => matchesQuery(o, matchQueryRegex(query));
|
|
|
27
27
|
const SelectFaceted = (props) => {
|
|
28
28
|
const {
|
|
29
29
|
facets,
|
|
30
|
+
placement,
|
|
31
|
+
offset,
|
|
30
32
|
value,
|
|
31
33
|
defaultValue,
|
|
32
34
|
onValueChange,
|
|
@@ -284,6 +286,8 @@ const SelectFaceted = (props) => {
|
|
|
284
286
|
/* @__PURE__ */ jsxs(
|
|
285
287
|
Menu,
|
|
286
288
|
{
|
|
289
|
+
placement,
|
|
290
|
+
offset,
|
|
287
291
|
open,
|
|
288
292
|
onStateChange: (_e, { next }) => {
|
|
289
293
|
setOpen(next);
|
|
@@ -64,8 +64,11 @@ declare global {
|
|
|
64
64
|
export declare class AButtonElement extends HTMLElementBase {
|
|
65
65
|
connectedCallback(): void;
|
|
66
66
|
/** Perform a copy if this button is a copy control (`copy` / `copy-node` /
|
|
67
|
-
* `copy-
|
|
67
|
+
* `copy-url`). Called from the delegated click handler on activation. The
|
|
68
68
|
* outcome is announced via a `copydone` event the wrapper reflects. */
|
|
69
69
|
performCopy(): boolean;
|
|
70
|
+
/** Ask a lazy consumer to refresh the `copy` value on pointerdown (fires a
|
|
71
|
+
* `copyrequest` event); the click then copies whatever `copy` holds. */
|
|
72
|
+
requestCopy(): void;
|
|
70
73
|
}
|
|
71
74
|
export declare function register_a_button(): void;
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
-
import { runCopy } from "./copy-behavior";
|
|
2
|
+
import { emitCopyRequest, runCopy } from "./copy-behavior";
|
|
3
|
+
import { installKeyActivation } from "./key-activation";
|
|
3
4
|
import "./a-button.css";
|
|
4
5
|
function installDocumentHandlers(doc) {
|
|
5
6
|
if (!doc || doc.hasKeyListenerForAButton) return;
|
|
6
|
-
doc
|
|
7
|
+
installKeyActivation(doc, {
|
|
8
|
+
keys: ["Enter", " "],
|
|
9
|
+
resolve: (t) => t?.closest?.("a-button"),
|
|
10
|
+
blocked: (el) => el.hasAttribute("disabled") || el.hasAttribute("loading"),
|
|
11
|
+
preflight: (el) => el.requestCopy(),
|
|
12
|
+
activate: (el) => el.click()
|
|
13
|
+
});
|
|
14
|
+
doc.addEventListener("pointerdown", handlePointerDown, true);
|
|
7
15
|
doc.addEventListener("click", handleClick, true);
|
|
8
16
|
doc.hasKeyListenerForAButton = true;
|
|
9
17
|
}
|
|
@@ -12,11 +20,16 @@ class AButtonElement extends HTMLElementBase {
|
|
|
12
20
|
installDocumentHandlers(this.doc);
|
|
13
21
|
}
|
|
14
22
|
/** Perform a copy if this button is a copy control (`copy` / `copy-node` /
|
|
15
|
-
* `copy-
|
|
23
|
+
* `copy-url`). Called from the delegated click handler on activation. The
|
|
16
24
|
* outcome is announced via a `copydone` event the wrapper reflects. */
|
|
17
25
|
performCopy() {
|
|
18
26
|
return runCopy(this);
|
|
19
27
|
}
|
|
28
|
+
/** Ask a lazy consumer to refresh the `copy` value on pointerdown (fires a
|
|
29
|
+
* `copyrequest` event); the click then copies whatever `copy` holds. */
|
|
30
|
+
requestCopy() {
|
|
31
|
+
emitCopyRequest(this);
|
|
32
|
+
}
|
|
20
33
|
}
|
|
21
34
|
function findForm(el) {
|
|
22
35
|
const formId = el.getAttribute("form");
|
|
@@ -25,16 +38,13 @@ function findForm(el) {
|
|
|
25
38
|
}
|
|
26
39
|
return el.closest("form");
|
|
27
40
|
}
|
|
28
|
-
function
|
|
29
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
30
|
-
if (e.repeat) return;
|
|
41
|
+
function handlePointerDown(e) {
|
|
31
42
|
const el = e.target.closest(
|
|
32
43
|
"a-button"
|
|
33
44
|
);
|
|
34
45
|
if (!el) return;
|
|
35
46
|
if (el.hasAttribute("disabled") || el.hasAttribute("loading")) return;
|
|
36
|
-
|
|
37
|
-
el.click();
|
|
47
|
+
el.requestCopy();
|
|
38
48
|
}
|
|
39
49
|
function handleClick(e) {
|
|
40
50
|
const el = e.target.closest(
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { HTMLElementBase, parseOpenState } from "../anta_helpers";
|
|
2
2
|
import "./a-expander.css";
|
|
3
3
|
const ANIM_MS = 200;
|
|
4
|
+
const INTERACTIVE_SELECTOR = 'a-button,a-checkbox,a-radio,a-radio-group,a-input,a-input-time,a-calendar,a-menu-item,a-tab,[data-anta][role="button"],button,a[href],input,select,textarea,label,[contenteditable=""],[contenteditable="true"]';
|
|
5
|
+
const DISABLED_SELECTOR = ':disabled,[disabled],[aria-disabled="true"]';
|
|
6
|
+
function ignoresToggle(el) {
|
|
7
|
+
if (el.matches("[data-expander-ignore]")) return true;
|
|
8
|
+
return el.matches(INTERACTIVE_SELECTOR) && !el.matches(DISABLED_SELECTOR);
|
|
9
|
+
}
|
|
4
10
|
const SUMMARY_TYPE_SCALE = {
|
|
5
11
|
"1": [28, 32],
|
|
6
12
|
"2": [24, 28],
|
|
@@ -266,11 +272,24 @@ class AExpanderElement extends HTMLElementBase {
|
|
|
266
272
|
} catch {
|
|
267
273
|
}
|
|
268
274
|
}
|
|
275
|
+
/** Refuse to toggle when the click landed on a control inside the projected
|
|
276
|
+
* title — walk the composed path up to (not including) the summary button and
|
|
277
|
+
* test each element with `ignoresToggle` (enabled interactive control or the
|
|
278
|
+
* opt-out marker). Synchronous and read here in the element, so it stands in
|
|
279
|
+
* off-UI-thread runtimes. Suppressing before the dispatch also keeps a
|
|
280
|
+
* *controlled* consumer from toggling (no `statechange` fires). */
|
|
281
|
+
#clickIgnoresToggle(e) {
|
|
282
|
+
const path = e.composedPath();
|
|
283
|
+
const end = path.indexOf(this.summary);
|
|
284
|
+
const scope = end === -1 ? path : path.slice(0, end);
|
|
285
|
+
return scope.some((n) => n instanceof Element && ignoresToggle(n));
|
|
286
|
+
}
|
|
269
287
|
/** Compute the requested next state, announce it (cancelable, *before*
|
|
270
288
|
* applying), then — uncontrolled only, and only if not vetoed — apply it.
|
|
271
289
|
* Controlled: never self-apply; the consumer answers via the `state`
|
|
272
290
|
* attribute. See STATEFUL-COMPONENTS.md. */
|
|
273
|
-
onSummaryClick = () => {
|
|
291
|
+
onSummaryClick = (e) => {
|
|
292
|
+
if (this.#clickIgnoresToggle(e)) return;
|
|
274
293
|
const prev = this.#current;
|
|
275
294
|
const next = prev === "open" ? "closed" : "open";
|
|
276
295
|
const ok = this.dispatchEvent(
|
|
@@ -67,6 +67,13 @@ export declare function isMenuItemEl(el: EventTarget | null | undefined): el is
|
|
|
67
67
|
/** Install the one-per-document Enter/Space activation listener. Called from
|
|
68
68
|
* both `AMenuItemElement` and `AMenuElement` on connect, so a menu built only
|
|
69
69
|
* from link items (no `<a-menu-item>` ever upgrades) still gets keyboard
|
|
70
|
-
* activation. Idempotent per document.
|
|
70
|
+
* activation. Idempotent per document.
|
|
71
|
+
*
|
|
72
|
+
* Shares `installKeyActivation` with `<a-button>`: activation is on keyup, with
|
|
73
|
+
* a keydown pre-request (`emitCopyRequest`) so a lazy copy row can refresh its
|
|
74
|
+
* `copy` during the hold. `.click()` on a link item navigates natively (honoring
|
|
75
|
+
* download / target); on the custom element it routes through the menu's click
|
|
76
|
+
* delegation — one path for both. (The combobox-filter Enter is a separate path
|
|
77
|
+
* handled by `a-menu`, which uses the same keyup pre-request.) */
|
|
71
78
|
export declare function ensureMenuItemKeyListener(doc: Document): void;
|
|
72
79
|
export declare function register_a_menu_item(): void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
-
import { runCopy } from "./copy-behavior";
|
|
2
|
+
import { emitCopyRequest, runCopy } from "./copy-behavior";
|
|
3
|
+
import { installKeyActivation } from "./key-activation";
|
|
3
4
|
import "./a-menu-item.css";
|
|
4
5
|
class AMenuItemElement extends HTMLElementBase {
|
|
5
6
|
internals;
|
|
@@ -7,6 +8,7 @@ class AMenuItemElement extends HTMLElementBase {
|
|
|
7
8
|
super();
|
|
8
9
|
this.internals = this.attachInternals?.();
|
|
9
10
|
this.addEventListener("menuselect", () => runCopy(this));
|
|
11
|
+
this.addEventListener("pointerdown", () => emitCopyRequest(this));
|
|
10
12
|
}
|
|
11
13
|
/** The active (combobox) cursor. `a-menu` sets this **property** (never an
|
|
12
14
|
* attribute — no DOM mutation, same rule as `a-radio.selected`) as ArrowUp/Down
|
|
@@ -30,17 +32,15 @@ function isMenuItemEl(el) {
|
|
|
30
32
|
}
|
|
31
33
|
function ensureMenuItemKeyListener(doc) {
|
|
32
34
|
if (doc.hasKeyListenerForAMenuItem) return;
|
|
33
|
-
doc
|
|
35
|
+
installKeyActivation(doc, {
|
|
36
|
+
keys: ["Enter", " "],
|
|
37
|
+
resolve: (t) => t?.closest?.(MENU_ITEM_SELECTOR),
|
|
38
|
+
blocked: (el) => el.hasAttribute("disabled"),
|
|
39
|
+
preflight: (el) => emitCopyRequest(el),
|
|
40
|
+
activate: (el) => el.click()
|
|
41
|
+
});
|
|
34
42
|
doc.hasKeyListenerForAMenuItem = true;
|
|
35
43
|
}
|
|
36
|
-
function handleKeyDown(e) {
|
|
37
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
38
|
-
const el = e.target?.closest?.(MENU_ITEM_SELECTOR);
|
|
39
|
-
if (!el) return;
|
|
40
|
-
e.preventDefault();
|
|
41
|
-
if (el.hasAttribute("disabled")) return;
|
|
42
|
-
el.click();
|
|
43
|
-
}
|
|
44
44
|
function register_a_menu_item() {
|
|
45
45
|
if (typeof customElements === "undefined") return;
|
|
46
46
|
if (!customElements.get("a-menu-item")) {
|
|
@@ -122,6 +122,9 @@ export declare class AMenuElement extends HTMLElementBase {
|
|
|
122
122
|
* (line start/end); PageUp / PageDown jump the option list to first / last
|
|
123
123
|
* instead. Any other key falls through to the input (typing). */
|
|
124
124
|
private handleComboKey;
|
|
125
|
+
/** Combobox keyup: activate the item a filter-field Enter armed. Paired with the
|
|
126
|
+
* keydown pre-request in `handleComboKey` so lazy content resolves in the gap. */
|
|
127
|
+
handleKeyUp(e: KeyboardEvent): void;
|
|
125
128
|
/** Public imperative API. Routes through the same intent path as the
|
|
126
129
|
* triggers, so it emits `statechange` and respects controlled mode. */
|
|
127
130
|
open(opts?: {
|
|
@@ -212,8 +215,9 @@ export declare class AMenuElement extends HTMLElementBase {
|
|
|
212
215
|
/** Close the whole open menu system from the root down. */
|
|
213
216
|
private closeSystem;
|
|
214
217
|
/** Called by the coordinator on the topmost open menu. Handles navigation;
|
|
215
|
-
* Enter / Space activation is
|
|
216
|
-
* (
|
|
218
|
+
* roving Enter / Space activation is `<a-menu-item>`'s own delegated keyup
|
|
219
|
+
* (synthesizes a click → routed through onSurfaceClick). Combobox Enter (focus
|
|
220
|
+
* in the filter field) is handled here via handleComboKey / handleKeyUp. */
|
|
217
221
|
handleKey(e: KeyboardEvent): void;
|
|
218
222
|
private submenuOf;
|
|
219
223
|
private typeahead;
|
package/dist/elements/a-menu.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HTMLElementBase, anchorRect, setMenuPresence } from "../anta_helpers";
|
|
2
2
|
import { AMenuItemElement, isMenuItemEl, ensureMenuItemKeyListener } from "./a-menu-item";
|
|
3
|
+
import { emitCopyRequest } from "./copy-behavior";
|
|
3
4
|
import "./a-menu.css";
|
|
4
5
|
const MARGIN = 4;
|
|
5
6
|
const MIN_HEIGHT = 96;
|
|
@@ -18,6 +19,7 @@ function bindDocListeners(doc, view) {
|
|
|
18
19
|
if (docBound) return;
|
|
19
20
|
doc.addEventListener("pointerdown", onDocPointerDown, true);
|
|
20
21
|
doc.addEventListener("keydown", onDocKeyDown, true);
|
|
22
|
+
doc.addEventListener("keyup", onDocKeyUp, true);
|
|
21
23
|
doc.addEventListener("contextmenu", onDocContextMenu, true);
|
|
22
24
|
view.addEventListener("resize", onResize);
|
|
23
25
|
boundDoc = doc;
|
|
@@ -28,6 +30,7 @@ function unbindDocListeners() {
|
|
|
28
30
|
if (!docBound) return;
|
|
29
31
|
boundDoc?.removeEventListener("pointerdown", onDocPointerDown, true);
|
|
30
32
|
boundDoc?.removeEventListener("keydown", onDocKeyDown, true);
|
|
33
|
+
boundDoc?.removeEventListener("keyup", onDocKeyUp, true);
|
|
31
34
|
boundDoc?.removeEventListener("contextmenu", onDocContextMenu, true);
|
|
32
35
|
boundView?.removeEventListener("resize", onResize);
|
|
33
36
|
removePosTracker?.();
|
|
@@ -66,6 +69,14 @@ function pathHitsMenus(e, primaryClick = false) {
|
|
|
66
69
|
}
|
|
67
70
|
return false;
|
|
68
71
|
}
|
|
72
|
+
function pathCrossesTopLayerBeforeAnchor(e, anchor) {
|
|
73
|
+
for (const node of e.composedPath()) {
|
|
74
|
+
if (node === anchor) return false;
|
|
75
|
+
if (node instanceof HTMLDialogElement && node.matches(":modal") || node instanceof HTMLElement && node.matches(":popover-open"))
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
69
80
|
function nodeHitsMenus(node) {
|
|
70
81
|
for (const m of openStack) {
|
|
71
82
|
if (m.contains(node) || m.surface.contains(node)) return true;
|
|
@@ -95,6 +106,11 @@ function onDocKeyDown(e) {
|
|
|
95
106
|
if (!menu) return;
|
|
96
107
|
menu.handleKey(e);
|
|
97
108
|
}
|
|
109
|
+
function onDocKeyUp(e) {
|
|
110
|
+
const menu = openStack[openStack.length - 1];
|
|
111
|
+
if (!menu) return;
|
|
112
|
+
menu.handleKeyUp(e);
|
|
113
|
+
}
|
|
98
114
|
function dismiss(originEvent) {
|
|
99
115
|
openStack[0]?.requestClose(originEvent);
|
|
100
116
|
}
|
|
@@ -535,13 +551,26 @@ class AMenuElement extends HTMLElementBase {
|
|
|
535
551
|
case "Enter":
|
|
536
552
|
if (this.activeItem) {
|
|
537
553
|
e.preventDefault();
|
|
538
|
-
this.activeItem
|
|
554
|
+
emitCopyRequest(this.activeItem);
|
|
555
|
+
this.#pendingComboActivate = this.activeItem;
|
|
539
556
|
}
|
|
540
557
|
return true;
|
|
541
558
|
default:
|
|
542
559
|
return false;
|
|
543
560
|
}
|
|
544
561
|
}
|
|
562
|
+
/** The combobox cursor armed by an Enter keydown in the filter field, consumed
|
|
563
|
+
* on the matching keyup. Only the combobox path arms this — the roving Enter/
|
|
564
|
+
* Space activation is `<a-menu-item>`'s (also keyup, via key-activation). */
|
|
565
|
+
#pendingComboActivate = null;
|
|
566
|
+
/** Combobox keyup: activate the item a filter-field Enter armed. Paired with the
|
|
567
|
+
* keydown pre-request in `handleComboKey` so lazy content resolves in the gap. */
|
|
568
|
+
handleKeyUp(e) {
|
|
569
|
+
if (e.key !== "Enter") return;
|
|
570
|
+
const el = this.#pendingComboActivate;
|
|
571
|
+
this.#pendingComboActivate = null;
|
|
572
|
+
el?.click();
|
|
573
|
+
}
|
|
545
574
|
/* ============================ open / close ============================ */
|
|
546
575
|
/** Public imperative API. Routes through the same intent path as the
|
|
547
576
|
* triggers, so it emits `statechange` and respects controlled mode. */
|
|
@@ -863,8 +892,9 @@ class AMenuElement extends HTMLElementBase {
|
|
|
863
892
|
}
|
|
864
893
|
/* ============================ keyboard ============================ */
|
|
865
894
|
/** Called by the coordinator on the topmost open menu. Handles navigation;
|
|
866
|
-
* Enter / Space activation is
|
|
867
|
-
* (
|
|
895
|
+
* roving Enter / Space activation is `<a-menu-item>`'s own delegated keyup
|
|
896
|
+
* (synthesizes a click → routed through onSurfaceClick). Combobox Enter (focus
|
|
897
|
+
* in the filter field) is handled here via handleComboKey / handleKeyUp. */
|
|
868
898
|
handleKey(e) {
|
|
869
899
|
const active = this.doc.activeElement;
|
|
870
900
|
if (e.key === "Escape") {
|
|
@@ -998,6 +1028,7 @@ class AMenuElement extends HTMLElementBase {
|
|
|
998
1028
|
};
|
|
999
1029
|
} else if (this.#isContext) {
|
|
1000
1030
|
const onContext = (e) => {
|
|
1031
|
+
if (pathCrossesTopLayerBeforeAnchor(e, anchor)) return;
|
|
1001
1032
|
e.preventDefault();
|
|
1002
1033
|
this.requestOpen({ coord: [e.clientX, e.clientY], originEvent: e });
|
|
1003
1034
|
};
|
|
@@ -11,23 +11,40 @@
|
|
|
11
11
|
* nested copy row's `copydone` from flipping an ancestor copy row's feedback
|
|
12
12
|
* (the same point-to-point rule Anta's `statechange` follows).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Modes, chosen by attribute:
|
|
15
15
|
* - `copy="<text>"` — copy the literal string on activation.
|
|
16
|
+
* - `copy-url` — copy the current page URL (`location.href`); no `copy` needed.
|
|
17
|
+
* - `copy-with-url` — prefix the copied `copy` string with `// URL: <href>`.
|
|
16
18
|
* - `copy-node` / `copy-node="<selector>"` — copy a DOM node as rich text
|
|
17
19
|
* (`text/html`) + plain text. Bare `copy-node` copies the nearest ancestor
|
|
18
20
|
* marked `[data-copy-source]`; a value is a selector resolved with `closest()`
|
|
19
21
|
* (an ancestor region). The copy control is stripped from the serialized
|
|
20
22
|
* output (`[data-copy-node-button]`).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
*
|
|
24
|
+
* ## Lazy content (off-UI-thread safe)
|
|
25
|
+
*
|
|
26
|
+
* When the content isn't known ahead of time, the element emits a `copyrequest`
|
|
27
|
+
* event on **pointerdown** (`emitCopyRequest`, fired whenever a `copy` attribute
|
|
28
|
+
* is present). The consumer answers by setting the `copy` attribute to the
|
|
29
|
+
* freshly-computed value; the **click** then reads that value and writes it. The
|
|
30
|
+
* pointerdown→click gap absorbs the round-trip, so this works even when the
|
|
31
|
+
* consumer's handler runs off the UI thread (e.g. a worker-rendered app): only a
|
|
32
|
+
* serializable string crosses the boundary via the normal reconcile — no
|
|
33
|
+
* callback, and the write stays inside the click's transient activation. This is
|
|
34
|
+
* why there is no `provide`-function handoff and no `copy-lazy` marker.
|
|
26
35
|
*
|
|
27
36
|
* Precedence when several are set (the discriminated-union prop types make this
|
|
28
|
-
* unreachable from the wrappers, but hand-authored markup can): node →
|
|
29
|
-
* literal `copy
|
|
37
|
+
* unreachable from the wrappers, but hand-authored markup can): node → url →
|
|
38
|
+
* literal `copy` (with the optional URL prefix).
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Ask a lazy consumer to refresh the copy content, on pointerdown — dispatch a
|
|
42
|
+
* non-bubbling, payload-free `copyrequest`. The consumer answers by setting the
|
|
43
|
+
* `copy` attribute to the freshly-computed value before the click reads it (see
|
|
44
|
+
* the "Lazy content" note above). Fires only for string-copy controls; node/url
|
|
45
|
+
* modes resolve themselves at click time and need no request. No-op otherwise.
|
|
30
46
|
*/
|
|
47
|
+
export declare function emitCopyRequest(el: HTMLElement): void;
|
|
31
48
|
/**
|
|
32
49
|
* Perform a copy on activation (click / Enter / Space / menu select). Returns
|
|
33
50
|
* `true` if the element is a copy control (so the caller knows the copy path
|
|
@@ -37,32 +37,34 @@ function run(el, work) {
|
|
|
37
37
|
el.dispatchEvent(new CustomEvent("copydone", { bubbles: false, detail: { ok } }));
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
+
function currentUrl() {
|
|
41
|
+
return typeof location !== "undefined" ? location.href : "";
|
|
42
|
+
}
|
|
43
|
+
function resolveText(el) {
|
|
44
|
+
if (el.hasAttribute("copy-url")) return currentUrl();
|
|
45
|
+
const base = el.getAttribute("copy") ?? "";
|
|
46
|
+
if (el.hasAttribute("copy-with-url")) return `// URL: ${currentUrl()}
|
|
47
|
+
${base}`;
|
|
48
|
+
return base;
|
|
49
|
+
}
|
|
50
|
+
function isTextCopy(el) {
|
|
51
|
+
return el.hasAttribute("copy") || el.hasAttribute("copy-url");
|
|
52
|
+
}
|
|
53
|
+
function emitCopyRequest(el) {
|
|
54
|
+
if (!el.hasAttribute("copy")) return;
|
|
55
|
+
el.dispatchEvent(new CustomEvent("copyrequest", { bubbles: false }));
|
|
56
|
+
}
|
|
40
57
|
function runCopy(el) {
|
|
41
58
|
if (el.hasAttribute("copy-node")) {
|
|
42
59
|
const target = resolveNodeTarget(el);
|
|
43
60
|
run(el, target ? writeNode(target) : Promise.resolve(false));
|
|
44
61
|
return true;
|
|
45
62
|
}
|
|
46
|
-
if (el
|
|
47
|
-
|
|
48
|
-
el.dispatchEvent(
|
|
49
|
-
new CustomEvent("copyrequest", {
|
|
50
|
-
bubbles: false,
|
|
51
|
-
detail: {
|
|
52
|
-
provide(text) {
|
|
53
|
-
if (done) return;
|
|
54
|
-
done = true;
|
|
55
|
-
run(el, writeText(String(text ?? "")));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
);
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
if (!el.hasAttribute("copy")) return false;
|
|
63
|
-
run(el, writeText(el.getAttribute("copy") ?? ""));
|
|
63
|
+
if (!isTextCopy(el)) return false;
|
|
64
|
+
run(el, writeText(resolveText(el)));
|
|
64
65
|
return true;
|
|
65
66
|
}
|
|
66
67
|
export {
|
|
68
|
+
emitCopyRequest,
|
|
67
69
|
runCopy
|
|
68
70
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared "activate on key release" for Enter / Space, used by `<a-button>` and
|
|
3
|
+
* `<a-menu-item>` (via one delegated keydown/keyup pair per document).
|
|
4
|
+
*
|
|
5
|
+
* Activation fires on **keyup**, not keydown. This matches the native `<button>`
|
|
6
|
+
* (Space already activates on release), makes activation **cancelable** (move
|
|
7
|
+
* focus between press and release → no fire), and — the reason it's uniform
|
|
8
|
+
* across Enter and Space here — opens a **keydown→keyup gap** so a lazy copy
|
|
9
|
+
* control can refresh its `copy` value between press and release before the write
|
|
10
|
+
* (the keyboard analog of the pointerdown→click gap; see copy-behavior's "Lazy
|
|
11
|
+
* content" note).
|
|
12
|
+
*
|
|
13
|
+
* keydown: `preventDefault()` (stop Space scrolling the page and a link's own
|
|
14
|
+
* native Enter→click), run `preflight(el)` — the copy pre-request — and arm the
|
|
15
|
+
* release. keyup: `activate(el)` only if the same element is still armed.
|
|
16
|
+
*
|
|
17
|
+
* Idempotency is the caller's job (each guards with its own per-document flag);
|
|
18
|
+
* this just wires one keydown/keyup pair.
|
|
19
|
+
*/
|
|
20
|
+
export interface KeyActivation {
|
|
21
|
+
/** Keys that activate — `['Enter', ' ']` for buttons/menu items. */
|
|
22
|
+
keys: readonly string[];
|
|
23
|
+
/** Resolve the activatable element from the event target (e.g. `closest`). */
|
|
24
|
+
resolve(target: EventTarget | null): HTMLElement | null;
|
|
25
|
+
/** Blocked (disabled / loading) — swallow the key without activating. */
|
|
26
|
+
blocked?(el: HTMLElement): boolean;
|
|
27
|
+
/** Runs on keydown, before the release: the lazy copy pre-request. */
|
|
28
|
+
preflight?(el: HTMLElement): void;
|
|
29
|
+
/** Runs on keyup: the actual activation (a synthesized `click()`). */
|
|
30
|
+
activate(el: HTMLElement): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function installKeyActivation(doc: Document, config: KeyActivation): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function installKeyActivation(doc, config) {
|
|
2
|
+
let armed = null;
|
|
3
|
+
doc.addEventListener(
|
|
4
|
+
"keydown",
|
|
5
|
+
(e) => {
|
|
6
|
+
if (e.repeat || !config.keys.includes(e.key)) return;
|
|
7
|
+
const el = config.resolve(e.target);
|
|
8
|
+
if (!el) return;
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
armed = config.blocked?.(el) ? null : el;
|
|
11
|
+
if (armed) config.preflight?.(armed);
|
|
12
|
+
},
|
|
13
|
+
true
|
|
14
|
+
);
|
|
15
|
+
doc.addEventListener(
|
|
16
|
+
"keyup",
|
|
17
|
+
(e) => {
|
|
18
|
+
if (!config.keys.includes(e.key)) return;
|
|
19
|
+
const el = config.resolve(e.target);
|
|
20
|
+
if (!el || el !== armed) {
|
|
21
|
+
armed = null;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
armed = null;
|
|
25
|
+
if (config.blocked?.(el)) return;
|
|
26
|
+
config.activate(el);
|
|
27
|
+
},
|
|
28
|
+
true
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
installKeyActivation
|
|
33
|
+
};
|
package/dist/general_types.d.ts
CHANGED
|
@@ -735,17 +735,19 @@ export interface AMenuItemAttributes extends BaseAttributes {
|
|
|
735
735
|
* is a CSS selector resolved with `closest()`. Presence-based for the boolean
|
|
736
736
|
* form. */
|
|
737
737
|
'copy-node'?: boolean | string;
|
|
738
|
-
/**
|
|
739
|
-
* write completes once `copy` is set back (within the activation window).
|
|
738
|
+
/** Copy the current page URL (`location.href`) instead of a `copy` string.
|
|
740
739
|
* Presence-based. */
|
|
741
|
-
'copy-
|
|
740
|
+
'copy-url'?: boolean | '';
|
|
741
|
+
/** Prefix the copied `copy` string with `// URL: <href>`. Presence-based. */
|
|
742
|
+
'copy-with-url'?: boolean | '';
|
|
742
743
|
/** Fired after a copy attempt (bubbling `copydone`, `detail: { ok }`).
|
|
743
744
|
* All-lowercase so it binds in React *and* Preact. */
|
|
744
745
|
oncopydone?: (e: CustomEvent<{
|
|
745
746
|
ok: boolean;
|
|
746
747
|
}>) => void;
|
|
747
|
-
/** Fired on a
|
|
748
|
-
*
|
|
748
|
+
/** Fired on **pointerdown** for a string-copy row (`copyrequest`, non-bubbling).
|
|
749
|
+
* Answer by setting the `copy` attribute to the freshly-computed value; the
|
|
750
|
+
* chosen row copies it. All-lowercase. */
|
|
749
751
|
oncopyrequest?: (e: CustomEvent) => void;
|
|
750
752
|
}
|
|
751
753
|
/**
|
|
@@ -798,22 +800,22 @@ export interface AButtonAttributes extends BaseAttributes {
|
|
|
798
800
|
* is a CSS selector resolved with `closest()` (an ancestor region). Presence-
|
|
799
801
|
* based for the boolean form. */
|
|
800
802
|
'copy-node'?: boolean | string;
|
|
801
|
-
/**
|
|
802
|
-
* `
|
|
803
|
-
|
|
804
|
-
|
|
803
|
+
/** Copy the current page URL (`location.href`) instead of a `copy` string; no
|
|
804
|
+
* `copy` needed. Presence-based. */
|
|
805
|
+
'copy-url'?: boolean | '';
|
|
806
|
+
/** Prefix the copied `copy` string with `// URL: <href>`. Presence-based. */
|
|
807
|
+
'copy-with-url'?: boolean | '';
|
|
805
808
|
/** Fired after a copy attempt (`copydone`, `detail: { ok }`, non-bubbling so a
|
|
806
809
|
* nested copy row can't flip an ancestor's feedback). All-lowercase so it
|
|
807
810
|
* binds in React *and* Preact. */
|
|
808
811
|
oncopydone?: (e: CustomEvent<{
|
|
809
812
|
ok: boolean;
|
|
810
813
|
}>) => void;
|
|
811
|
-
/** Fired on a
|
|
812
|
-
*
|
|
813
|
-
* click
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}>) => void;
|
|
814
|
+
/** Fired on **pointerdown** for a string-copy button (`copyrequest`,
|
|
815
|
+
* non-bubbling). Answer by setting the `copy` attribute to the freshly-computed
|
|
816
|
+
* value; the click then copies it. The pointerdown→click gap lets an
|
|
817
|
+
* off-UI-thread handler set `copy` in time. All-lowercase. */
|
|
818
|
+
oncopyrequest?: (e: CustomEvent) => void;
|
|
817
819
|
'aria-disabled'?: 'true' | 'false' | boolean;
|
|
818
820
|
'aria-busy'?: 'true' | 'false' | boolean;
|
|
819
821
|
}
|