@antadesign/anta 0.3.9 → 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/anta_helpers.d.ts +18 -0
- package/dist/anta_helpers.js +16 -1
- package/dist/components/Button.d.ts +62 -4
- package/dist/components/Button.js +27 -6
- package/dist/components/ButtonCopy.d.ts +38 -0
- package/dist/components/ButtonCopy.js +10 -0
- package/dist/components/MenuItem.d.ts +9 -2
- package/dist/components/MenuItem.js +17 -2
- package/dist/components/MenuItemCopy.d.ts +34 -0
- package/dist/components/MenuItemCopy.js +10 -0
- 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/components/Title.d.ts +11 -7
- package/dist/elements/a-button.d.ts +7 -0
- package/dist/elements/a-button.js +24 -6
- 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 +11 -9
- package/dist/elements/a-menu.d.ts +6 -2
- package/dist/elements/a-menu.js +34 -3
- package/dist/elements/a-text.css +1 -1
- package/dist/elements/a-title.css +1 -1
- package/dist/elements/copy-behavior.d.ts +53 -0
- package/dist/elements/copy-behavior.js +70 -0
- package/dist/elements/key-activation.d.ts +32 -0
- package/dist/elements/key-activation.js +33 -0
- package/dist/general_types.d.ts +46 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -0
- package/package.json +1 -1
package/dist/anta_helpers.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IconShape } from "./elements/a-icon.shapes";
|
|
1
2
|
export declare function hasChildren(children: React.ReactNode): boolean;
|
|
2
3
|
/**
|
|
3
4
|
* Normalize a wrapper's label content the way `Button` / `Tabs` do: a bare string
|
|
@@ -74,6 +75,23 @@ export declare function roundStyle(round: boolean | number | string | undefined,
|
|
|
74
75
|
* `false` / `undefined` / `''`). Mirrors what `roundStyle` emits a variable for.
|
|
75
76
|
*/
|
|
76
77
|
export declare function roundAttr(round: boolean | number | string | undefined): '' | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* Copy-feedback state for `ButtonCopy` / `MenuItemCopy` (and any future copy
|
|
80
|
+
* preset). The `<a-button>` / `<a-menu-item>` element performs the clipboard
|
|
81
|
+
* write and fires a `copydone` event; this hook holds the transient result and
|
|
82
|
+
* maps it to the icon + tone the wrapper renders: a `check` / `success` on
|
|
83
|
+
* success, an `x` / `critical` on failure, otherwise the caller's own values.
|
|
84
|
+
* Returns the `handleCopied` listener the wrapper wires to `onCopied`.
|
|
85
|
+
*
|
|
86
|
+
* Kept here (not duplicated per preset) so the timing, glyph choice, and timer
|
|
87
|
+
* cleanup live in one place. Hooks come through the `jsx-runtime` indirection, so
|
|
88
|
+
* a custom non-React runtime supplies its own via `configure()`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function useCopyFeedback(icon: IconShape | undefined, tone: string | undefined, onCopied?: (ok: boolean) => void): {
|
|
91
|
+
shownIcon: IconShape | undefined;
|
|
92
|
+
shownTone: string | undefined;
|
|
93
|
+
handleCopied: (ok: boolean) => void;
|
|
94
|
+
};
|
|
77
95
|
/**
|
|
78
96
|
* The rect a floating-element anchor advertises for positioning. Any element may
|
|
79
97
|
* implement `getAnchorRect(): DOMRect` to point positioners (`a-menu`,
|
package/dist/anta_helpers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx } from "./jsx-runtime";
|
|
1
|
+
import { jsx, useState, useMemo } from "./jsx-runtime";
|
|
2
2
|
function hasChildren(children) {
|
|
3
3
|
return Array.isArray(children) ? children.length > 0 : children != null;
|
|
4
4
|
}
|
|
@@ -40,6 +40,20 @@ function roundStyle(round, varName, base) {
|
|
|
40
40
|
function roundAttr(round) {
|
|
41
41
|
return round || round === 0 ? "" : void 0;
|
|
42
42
|
}
|
|
43
|
+
const COPY_FEEDBACK_MS = 2e3;
|
|
44
|
+
function useCopyFeedback(icon, tone, onCopied) {
|
|
45
|
+
const [status, setStatus] = useState("idle");
|
|
46
|
+
const box = useMemo(() => ({ timer: void 0 }), []);
|
|
47
|
+
const handleCopied = (ok) => {
|
|
48
|
+
clearTimeout(box.timer);
|
|
49
|
+
setStatus(ok ? "ok" : "fail");
|
|
50
|
+
box.timer = setTimeout(() => setStatus("idle"), COPY_FEEDBACK_MS);
|
|
51
|
+
onCopied?.(ok);
|
|
52
|
+
};
|
|
53
|
+
const shownIcon = status === "ok" ? "check" : status === "fail" ? "x" : icon;
|
|
54
|
+
const shownTone = status === "ok" ? "success" : status === "fail" ? "critical" : tone;
|
|
55
|
+
return { shownIcon, shownTone, handleCopied };
|
|
56
|
+
}
|
|
43
57
|
function anchorRect(el) {
|
|
44
58
|
const fn = el.getAnchorRect;
|
|
45
59
|
return typeof fn === "function" ? fn.call(el) : el.getBoundingClientRect();
|
|
@@ -127,5 +141,6 @@ export {
|
|
|
127
141
|
roundStyle,
|
|
128
142
|
setMenuPresence,
|
|
129
143
|
toneStyle,
|
|
144
|
+
useCopyFeedback,
|
|
130
145
|
wrapLabel
|
|
131
146
|
};
|
|
@@ -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 = {
|
|
@@ -100,6 +109,55 @@ export type PriorityMode = {
|
|
|
100
109
|
/** Drops outer padding to zero. */
|
|
101
110
|
paddingless?: boolean;
|
|
102
111
|
};
|
|
112
|
+
/** Copy axis — turns the button into a copy control that writes to the clipboard
|
|
113
|
+
* on click and flashes a success / failure state (see `<a-button>`'s copy
|
|
114
|
+
* behavior). **Exactly one** of `copy`, `copyNode`, or `copyUrl` may be set;
|
|
115
|
+
* the union makes the others `never` in each mode. For the batteries-included
|
|
116
|
+
* preset, use `ButtonCopy`. */
|
|
117
|
+
export type CopyMode = {
|
|
118
|
+
copy?: never;
|
|
119
|
+
copyNode?: never;
|
|
120
|
+
copyUrl?: never;
|
|
121
|
+
copyWithUrl?: never;
|
|
122
|
+
onCopied?: never;
|
|
123
|
+
onCopyRequest?: never;
|
|
124
|
+
} | {
|
|
125
|
+
/** Text copied to the clipboard on click. */
|
|
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;
|
|
135
|
+
copyNode?: never;
|
|
136
|
+
copyUrl?: never;
|
|
137
|
+
/** Fires after the copy attempt with whether it succeeded. */
|
|
138
|
+
onCopied?: (ok: boolean) => void;
|
|
139
|
+
} | {
|
|
140
|
+
/** Copy a DOM node as rich text instead of a string. `true` copies the
|
|
141
|
+
* nearest ancestor marked `data-copy-source`; a string is a CSS selector
|
|
142
|
+
* for an ancestor region (`closest`). The copy control is stripped from
|
|
143
|
+
* the copied output. */
|
|
144
|
+
copyNode: boolean | string;
|
|
145
|
+
copy?: never;
|
|
146
|
+
copyUrl?: never;
|
|
147
|
+
copyWithUrl?: never;
|
|
148
|
+
onCopyRequest?: never;
|
|
149
|
+
/** Fires after the copy attempt with whether it succeeded. */
|
|
150
|
+
onCopied?: (ok: boolean) => void;
|
|
151
|
+
} | {
|
|
152
|
+
/** Copy the current page URL (`location.href`). */
|
|
153
|
+
copyUrl: true;
|
|
154
|
+
copy?: never;
|
|
155
|
+
copyNode?: never;
|
|
156
|
+
copyWithUrl?: never;
|
|
157
|
+
onCopyRequest?: never;
|
|
158
|
+
/** Fires after the copy attempt with whether it succeeded. */
|
|
159
|
+
onCopied?: (ok: boolean) => void;
|
|
160
|
+
};
|
|
103
161
|
export type ButtonProps = BaseButtonProps & PriorityMode & ContentMode & SubmitMode & BaseProps;
|
|
104
162
|
/**
|
|
105
163
|
* Action button.
|
|
@@ -121,4 +179,4 @@ export type ButtonProps = BaseButtonProps & PriorityMode & ContentMode & SubmitM
|
|
|
121
179
|
* <Button href="/docs" target="_blank" label="Read the docs" />
|
|
122
180
|
* ```
|
|
123
181
|
*/
|
|
124
|
-
export declare const Button: ({ priority, tone, underline, icon, iconTrailing, paddingless, label, size, loading, disabled, selected, round, href, type, form, className, style, children, ...rest }: ButtonProps) => any;
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "@antadesign/anta/jsx-runtime";
|
|
2
|
-
import { toneStyle, roundStyle, roundAttr, wrapLabel } from "../anta_helpers";
|
|
2
|
+
import { toneStyle, roundStyle, roundAttr, wrapLabel, nativeStateChange } from "../anta_helpers";
|
|
3
3
|
const Button = ({
|
|
4
4
|
priority,
|
|
5
5
|
tone,
|
|
@@ -16,14 +16,22 @@ const Button = ({
|
|
|
16
16
|
href,
|
|
17
17
|
type,
|
|
18
18
|
form,
|
|
19
|
+
copy,
|
|
20
|
+
copyNode,
|
|
21
|
+
copyUrl,
|
|
22
|
+
copyWithUrl,
|
|
23
|
+
onCopied,
|
|
24
|
+
onCopyRequest,
|
|
19
25
|
className,
|
|
20
26
|
style,
|
|
21
27
|
children,
|
|
22
28
|
...rest
|
|
23
29
|
}) => {
|
|
30
|
+
const isCopy = copy != null || copyNode != null || copyUrl === true;
|
|
24
31
|
const toneAttr = tone || void 0;
|
|
25
32
|
const computedStyle = roundStyle(round, "--button-round", toneStyle(toneAttr, "--button-tone-source", style));
|
|
26
|
-
const
|
|
33
|
+
const leadingIcon = icon ?? (isCopy ? "copy" : void 0);
|
|
34
|
+
const isIconOnly = leadingIcon != null && label == null && children == null && iconTrailing == null;
|
|
27
35
|
const sharedAttrs = {
|
|
28
36
|
// `<a-button>` is a custom element with no implicit ARIA role, so AT would
|
|
29
37
|
// announce it as a generic clickable — and `aria-pressed` below is only
|
|
@@ -53,14 +61,27 @@ const Button = ({
|
|
|
53
61
|
"aria-disabled": disabled || loading ? "true" : void 0,
|
|
54
62
|
"aria-busy": loading ? "true" : void 0,
|
|
55
63
|
"aria-pressed": selected ? "true" : void 0,
|
|
56
|
-
// Icon-only buttons get an accessible name
|
|
57
|
-
//
|
|
58
|
-
|
|
64
|
+
// Icon-only buttons get an accessible name: "Copy" for a copy control (its
|
|
65
|
+
// glyph carries no text), else the icon shape. Consumer's own `aria-label`
|
|
66
|
+
// (via ...rest) wins by spread order.
|
|
67
|
+
"aria-label": isIconOnly ? isCopy ? "Copy" : icon : void 0,
|
|
68
|
+
// Copy behavior — the element reads these and performs the write itself.
|
|
69
|
+
copy: copy != null ? copy : void 0,
|
|
70
|
+
"copy-node": copyNode === true ? "" : typeof copyNode === "string" ? copyNode : void 0,
|
|
71
|
+
"copy-url": copyUrl ? "" : void 0,
|
|
72
|
+
"copy-with-url": copyWithUrl ? "" : void 0,
|
|
73
|
+
// Marks the control so `copy-node` serialization strips it from the copied
|
|
74
|
+
// node (a copy button inside the copied region shouldn't paste itself).
|
|
75
|
+
"data-copy-node-button": copyNode != null && copyNode !== false ? "" : void 0,
|
|
76
|
+
oncopydone: onCopied ? (e) => onCopied(nativeStateChange(e).detail?.ok ?? false) : void 0,
|
|
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,
|
|
59
80
|
class: className,
|
|
60
81
|
style: computedStyle
|
|
61
82
|
};
|
|
62
83
|
const inner = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
63
|
-
|
|
84
|
+
leadingIcon && /* @__PURE__ */ jsx("a-icon", { shape: leadingIcon, "aria-hidden": "true" }),
|
|
64
85
|
label != null && /* @__PURE__ */ jsx("a-button-label", { children: label }),
|
|
65
86
|
wrapLabel(children, "a-button-label"),
|
|
66
87
|
iconTrailing && /* @__PURE__ */ jsx("a-icon", { shape: iconTrailing, "aria-hidden": "true" })
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ButtonProps } from './Button';
|
|
2
|
+
/** `ButtonProps` narrowed so a copy target is required — a copy button must copy
|
|
3
|
+
* something. Intersecting with the target union drops the "no copy prop" member
|
|
4
|
+
* of `CopyMode` (its `copy?: never` makes each combination `never`). */
|
|
5
|
+
export type ButtonCopyProps = ButtonProps & ({
|
|
6
|
+
copy: string;
|
|
7
|
+
} | {
|
|
8
|
+
copyNode: boolean | string;
|
|
9
|
+
} | {
|
|
10
|
+
copyUrl: true;
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Copy button — a `Button` preset for copy-to-clipboard. Set `copy` for a literal
|
|
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`).
|
|
20
|
+
*
|
|
21
|
+
* With a `label` it's a labeled button; without one it's an icon-only copy button.
|
|
22
|
+
* Everything else is a normal `Button` prop — `tone`, `priority`, `size`, `icon`
|
|
23
|
+
* (the resting glyph), `onCopied`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <ButtonCopy copy="npm i @antadesign/anta" label="Copy install" />
|
|
28
|
+
* <ButtonCopy copy="https://anta.design" /> // icon-only
|
|
29
|
+
* <ButtonCopy copyNode=".snippet" label="Copy block" priority="tertiary" />
|
|
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" />
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const ButtonCopy: ({ icon, tone, onCopied, ...rest }: ButtonCopyProps) => any;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx } from "@antadesign/anta/jsx-runtime";
|
|
2
|
+
import { Button } from "./Button";
|
|
3
|
+
import { useCopyFeedback } from "../anta_helpers";
|
|
4
|
+
const ButtonCopy = ({ icon, tone, onCopied, ...rest }) => {
|
|
5
|
+
const { shownIcon, shownTone, handleCopied } = useCopyFeedback(icon, tone, onCopied);
|
|
6
|
+
return /* @__PURE__ */ jsx(Button, { icon: shownIcon, tone: shownTone, onCopied: handleCopied, ...rest });
|
|
7
|
+
};
|
|
8
|
+
export {
|
|
9
|
+
ButtonCopy
|
|
10
|
+
};
|
|
@@ -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 { CopyMode } from './Button';
|
|
3
4
|
/** Props shared by every menu item, link or not. */
|
|
4
5
|
export interface MenuItemCommonProps extends BaseProps {
|
|
5
6
|
/** Leading icon shape. */
|
|
@@ -83,6 +84,12 @@ export type MenuItemLinkMode = {
|
|
|
83
84
|
selectionIndicator?: never;
|
|
84
85
|
indeterminate?: never;
|
|
85
86
|
indicator?: never;
|
|
87
|
+
copy?: never;
|
|
88
|
+
copyNode?: never;
|
|
89
|
+
copyUrl?: never;
|
|
90
|
+
copyWithUrl?: never;
|
|
91
|
+
onCopied?: never;
|
|
92
|
+
onCopyRequest?: never;
|
|
86
93
|
};
|
|
87
94
|
export type MenuItemActionMode = {
|
|
88
95
|
href?: never;
|
|
@@ -114,7 +121,7 @@ export type MenuItemActionMode = {
|
|
|
114
121
|
* passive (aria-hidden, no pointer events) so the row owns the click. */
|
|
115
122
|
indicator?: React.ReactNode;
|
|
116
123
|
};
|
|
117
|
-
export type MenuItemProps = MenuItemCommonProps & (MenuItemLinkMode | MenuItemActionMode);
|
|
124
|
+
export type MenuItemProps = MenuItemCommonProps & (MenuItemLinkMode | (MenuItemActionMode & CopyMode));
|
|
118
125
|
/**
|
|
119
126
|
* MenuItem — a single selectable row inside a `Menu`. Composes a leading
|
|
120
127
|
* `icon`, a `label` (or `children`), an optional trailing `kbd` hint, and an
|
|
@@ -135,4 +142,4 @@ export type MenuItemProps = MenuItemCommonProps & (MenuItemLinkMode | MenuItemAc
|
|
|
135
142
|
* </MenuItem>
|
|
136
143
|
* ```
|
|
137
144
|
*/
|
|
138
|
-
export declare const MenuItem: ({ icon, label, hint, kbd, iconTrailing, disabled, selected, selectionIndicator, indeterminate, indicator, tone, toneSelected, submenu, value, onSelect, href, target, rel, download, ping, role: roleOverride, className, style, children, ...rest }: MenuItemProps) => any;
|
|
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from "@antadesign/anta/jsx-runtime";
|
|
2
|
-
import { toneStyle } from "../anta_helpers";
|
|
2
|
+
import { toneStyle, nativeStateChange } from "../anta_helpers";
|
|
3
3
|
const MenuItem = ({
|
|
4
4
|
icon,
|
|
5
5
|
label,
|
|
@@ -21,12 +21,20 @@ const MenuItem = ({
|
|
|
21
21
|
rel,
|
|
22
22
|
download,
|
|
23
23
|
ping,
|
|
24
|
+
copy,
|
|
25
|
+
copyNode,
|
|
26
|
+
copyUrl,
|
|
27
|
+
copyWithUrl,
|
|
28
|
+
onCopied,
|
|
29
|
+
onCopyRequest,
|
|
24
30
|
role: roleOverride,
|
|
25
31
|
className,
|
|
26
32
|
style,
|
|
27
33
|
children,
|
|
28
34
|
...rest
|
|
29
35
|
}) => {
|
|
36
|
+
const isCopy = copy != null || copyNode != null || copyUrl === true;
|
|
37
|
+
const leadingIcon = icon ?? (isCopy ? "copy" : void 0);
|
|
30
38
|
const checkable = selectionIndicator === "checkbox" || selectionIndicator === "radio" || selectionIndicator === "check";
|
|
31
39
|
const role = selectionIndicator === "checkbox" ? "menuitemcheckbox" : selectionIndicator === "radio" || selectionIndicator === "check" ? "menuitemradio" : "menuitem";
|
|
32
40
|
const ariaChecked = !checkable ? void 0 : selectionIndicator === "checkbox" && indeterminate ? "mixed" : selected ? "true" : "false";
|
|
@@ -76,6 +84,13 @@ const MenuItem = ({
|
|
|
76
84
|
"aria-haspopup": submenu ? "menu" : void 0,
|
|
77
85
|
"aria-disabled": disabled ? "true" : void 0,
|
|
78
86
|
onmenuselect: onSelect ? (e) => onSelect(e, { value, label }) : void 0,
|
|
87
|
+
copy: copy != null ? copy : void 0,
|
|
88
|
+
"copy-node": copyNode === true ? "" : typeof copyNode === "string" ? copyNode : void 0,
|
|
89
|
+
"copy-url": copyUrl ? "" : void 0,
|
|
90
|
+
"copy-with-url": copyWithUrl ? "" : void 0,
|
|
91
|
+
"data-copy-node-button": copyNode != null && copyNode !== false ? "" : void 0,
|
|
92
|
+
oncopydone: onCopied ? (e) => onCopied(nativeStateChange(e).detail?.ok ?? false) : void 0,
|
|
93
|
+
oncopyrequest: onCopyRequest ? () => onCopyRequest() : void 0,
|
|
79
94
|
class: className,
|
|
80
95
|
...rest,
|
|
81
96
|
children: [
|
|
@@ -116,7 +131,7 @@ const MenuItem = ({
|
|
|
116
131
|
}
|
|
117
132
|
)
|
|
118
133
|
) : null,
|
|
119
|
-
|
|
134
|
+
leadingIcon && /* @__PURE__ */ jsx("a-icon", { shape: leadingIcon, "aria-hidden": "true" }),
|
|
120
135
|
labelNode,
|
|
121
136
|
children,
|
|
122
137
|
kbd && /* @__PURE__ */ jsx("kbd", { children: kbd }),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type MenuItemCommonProps, type MenuItemActionMode } from './MenuItem';
|
|
2
|
+
import type { CopyMode } from './Button';
|
|
3
|
+
/** The action-mode menu-item props plus a required copy target (a copying row
|
|
4
|
+
* must copy something). The target intersection drops `CopyMode`'s "no copy
|
|
5
|
+
* prop" member. */
|
|
6
|
+
export type MenuItemCopyProps = MenuItemCommonProps & MenuItemActionMode & CopyMode & ({
|
|
7
|
+
copy: string;
|
|
8
|
+
} | {
|
|
9
|
+
copyNode: boolean | string;
|
|
10
|
+
} | {
|
|
11
|
+
copyUrl: true;
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Copying menu item — a `MenuItem` preset for copy-to-clipboard rows inside a
|
|
15
|
+
* `Menu`. Set `copy` for a literal string, `copyNode` to copy a DOM region, or
|
|
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
|
|
18
|
+
* `<a-menu-item>` element performs the write when the row is chosen; this wrapper
|
|
19
|
+
* flips the leading icon to a check / ✕ and retones for ~2s (see `useCopyFeedback`).
|
|
20
|
+
*
|
|
21
|
+
* The menu closes on select by default, so the feedback shows only when the row
|
|
22
|
+
* is kept open (`data-menu-open`). Defaults `label` to "Copy" and the leading
|
|
23
|
+
* `icon` to `copy`; override either. Other props pass through — `tone`, `kbd`,
|
|
24
|
+
* `onCopied`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <Menu>
|
|
29
|
+
* <MenuItemCopy copy={shareUrl} label="Copy link" kbd="⌘C" />
|
|
30
|
+
* <MenuItemCopy copyNode=".invoice" label="Copy invoice" data-menu-open />
|
|
31
|
+
* </Menu>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare const MenuItemCopy: ({ label, icon, tone, onCopied, ...rest }: MenuItemCopyProps) => any;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx } from "@antadesign/anta/jsx-runtime";
|
|
2
|
+
import { MenuItem } from "./MenuItem";
|
|
3
|
+
import { useCopyFeedback } from "../anta_helpers";
|
|
4
|
+
const MenuItemCopy = ({ label = "Copy", icon = "copy", tone, onCopied, ...rest }) => {
|
|
5
|
+
const { shownIcon, shownTone, handleCopied } = useCopyFeedback(icon, tone, onCopied);
|
|
6
|
+
return /* @__PURE__ */ jsx(MenuItem, { label, icon: shownIcon, tone: shownTone, onCopied: handleCopied, ...rest });
|
|
7
|
+
};
|
|
8
|
+
export {
|
|
9
|
+
MenuItemCopy
|
|
10
|
+
};
|
|
@@ -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);
|
|
@@ -24,18 +24,22 @@ export interface TitleProps extends BaseProps {
|
|
|
24
24
|
* — so there are no `icon` / `iconTrailing` props; just compose
|
|
25
25
|
* inside.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* `<Title>` carries no vertical margins: it's a spacing-neutral atom
|
|
28
|
+
* that drops into any container, and the parent owns spacing (a `gap`
|
|
29
|
+
* or padding). Raw `<h1>`-`<h6>` do keep per-level top/bottom margins
|
|
30
|
+
* (via `src/reset.css`) for document prose rhythm — so use a real
|
|
31
|
+
* heading tag if SEO matters or you want that built-in rhythm and don't
|
|
32
|
+
* need the `tone` / `priority` props.
|
|
30
33
|
*
|
|
31
34
|
* Styling notes (`a-title.css` ships comment-free): `<a-title>` is
|
|
32
35
|
* intentionally CSS-only — no JS, no shadow DOM, not even
|
|
33
36
|
* `customElements.define`; the browser treats it as a generic unknown
|
|
34
37
|
* element and the CSS gives it block layout, the demi-bold variable-font
|
|
35
|
-
* weight, and the per-level type scale
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* weight, and the per-level type scale. The matching `h1`–`h6` rules in
|
|
39
|
+
* `src/reset.css` share that type scale and weight, so raw markup reads
|
|
40
|
+
* the same as `<Title level={n}>` — they diverge only on margins: raw
|
|
41
|
+
* headings carry the document vertical rhythm, `<a-title>` is margin-free.
|
|
42
|
+
* The tone × priority color matrix has the same shape as `a-text`'s.
|
|
39
43
|
*
|
|
40
44
|
* Requires `@antadesign/anta/elements` to be imported (client-side only)
|
|
41
45
|
* so the CSS ships with the page.
|
|
@@ -63,5 +63,12 @@ declare global {
|
|
|
63
63
|
*/
|
|
64
64
|
export declare class AButtonElement extends HTMLElementBase {
|
|
65
65
|
connectedCallback(): void;
|
|
66
|
+
/** Perform a copy if this button is a copy control (`copy` / `copy-node` /
|
|
67
|
+
* `copy-url`). Called from the delegated click handler on activation. The
|
|
68
|
+
* outcome is announced via a `copydone` event the wrapper reflects. */
|
|
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;
|
|
66
73
|
}
|
|
67
74
|
export declare function register_a_button(): void;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
+
import { emitCopyRequest, runCopy } from "./copy-behavior";
|
|
3
|
+
import { installKeyActivation } from "./key-activation";
|
|
2
4
|
import "./a-button.css";
|
|
3
5
|
function installDocumentHandlers(doc) {
|
|
4
6
|
if (!doc || doc.hasKeyListenerForAButton) return;
|
|
5
|
-
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);
|
|
6
15
|
doc.addEventListener("click", handleClick, true);
|
|
7
16
|
doc.hasKeyListenerForAButton = true;
|
|
8
17
|
}
|
|
@@ -10,6 +19,17 @@ class AButtonElement extends HTMLElementBase {
|
|
|
10
19
|
connectedCallback() {
|
|
11
20
|
installDocumentHandlers(this.doc);
|
|
12
21
|
}
|
|
22
|
+
/** Perform a copy if this button is a copy control (`copy` / `copy-node` /
|
|
23
|
+
* `copy-url`). Called from the delegated click handler on activation. The
|
|
24
|
+
* outcome is announced via a `copydone` event the wrapper reflects. */
|
|
25
|
+
performCopy() {
|
|
26
|
+
return runCopy(this);
|
|
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
|
+
}
|
|
13
33
|
}
|
|
14
34
|
function findForm(el) {
|
|
15
35
|
const formId = el.getAttribute("form");
|
|
@@ -18,16 +38,13 @@ function findForm(el) {
|
|
|
18
38
|
}
|
|
19
39
|
return el.closest("form");
|
|
20
40
|
}
|
|
21
|
-
function
|
|
22
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
23
|
-
if (e.repeat) return;
|
|
41
|
+
function handlePointerDown(e) {
|
|
24
42
|
const el = e.target.closest(
|
|
25
43
|
"a-button"
|
|
26
44
|
);
|
|
27
45
|
if (!el) return;
|
|
28
46
|
if (el.hasAttribute("disabled") || el.hasAttribute("loading")) return;
|
|
29
|
-
|
|
30
|
-
el.click();
|
|
47
|
+
el.requestCopy();
|
|
31
48
|
}
|
|
32
49
|
function handleClick(e) {
|
|
33
50
|
const el = e.target.closest(
|
|
@@ -41,6 +58,7 @@ function handleClick(e) {
|
|
|
41
58
|
new CustomEvent(customEvent, { bubbles: true, cancelable: true })
|
|
42
59
|
);
|
|
43
60
|
}
|
|
61
|
+
el.performCopy();
|
|
44
62
|
const type = el.getAttribute("type") || "button";
|
|
45
63
|
const form = findForm(el);
|
|
46
64
|
if (!form) return;
|
|
@@ -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,10 +1,14 @@
|
|
|
1
1
|
import { HTMLElementBase } from "../anta_helpers";
|
|
2
|
+
import { emitCopyRequest, runCopy } from "./copy-behavior";
|
|
3
|
+
import { installKeyActivation } from "./key-activation";
|
|
2
4
|
import "./a-menu-item.css";
|
|
3
5
|
class AMenuItemElement extends HTMLElementBase {
|
|
4
6
|
internals;
|
|
5
7
|
constructor() {
|
|
6
8
|
super();
|
|
7
9
|
this.internals = this.attachInternals?.();
|
|
10
|
+
this.addEventListener("menuselect", () => runCopy(this));
|
|
11
|
+
this.addEventListener("pointerdown", () => emitCopyRequest(this));
|
|
8
12
|
}
|
|
9
13
|
/** The active (combobox) cursor. `a-menu` sets this **property** (never an
|
|
10
14
|
* attribute — no DOM mutation, same rule as `a-radio.selected`) as ArrowUp/Down
|
|
@@ -28,17 +32,15 @@ function isMenuItemEl(el) {
|
|
|
28
32
|
}
|
|
29
33
|
function ensureMenuItemKeyListener(doc) {
|
|
30
34
|
if (doc.hasKeyListenerForAMenuItem) return;
|
|
31
|
-
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
|
+
});
|
|
32
42
|
doc.hasKeyListenerForAMenuItem = true;
|
|
33
43
|
}
|
|
34
|
-
function handleKeyDown(e) {
|
|
35
|
-
if (e.key !== "Enter" && e.key !== " ") return;
|
|
36
|
-
const el = e.target?.closest?.(MENU_ITEM_SELECTOR);
|
|
37
|
-
if (!el) return;
|
|
38
|
-
e.preventDefault();
|
|
39
|
-
if (el.hasAttribute("disabled")) return;
|
|
40
|
-
el.click();
|
|
41
|
-
}
|
|
42
44
|
function register_a_menu_item() {
|
|
43
45
|
if (typeof customElements === "undefined") return;
|
|
44
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
|
};
|
package/dist/elements/a-text.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer anta{a-text{--text-color: var(--text-2);--text-link-color: var(--link-color);--text-link-hover: var(--link-color-hover);display:block;color:var(--text-color);font-size:15px;line-height:20px;text-wrap:pretty;font-feature-settings:"ss02","ss05","tnum"}a-text[size=small]{font-size:13px;line-height:16px}a-text[size=medium]{font-size:15px;line-height:20px}a-text[size=large]{font-size:17px;line-height:24px}a-text[inline]{display:inline-block}a-text[truncate]{min-width:0}a-text[priority=primary]{--text-color: var(--text-1)}a-text[priority=tertiary]{--text-color: var(--text-3);--text-link-color: currentColor;--text-link-hover: var(--text-2)}a-text[priority=quaternary]{--text-color: var(--text-4);--text-link-color: currentColor;--text-link-hover: var(--text-3)}a-text[priority=quinary]{--text-color: var(--text-5);--text-link-color: currentColor;--text-link-hover: var(--text-4)}a-text[tone=brand]{--text-color: var(--text-2-brand);--text-link-color: currentColor;--text-link-hover: var(--text-1-brand)}a-text[tone=brand][priority=primary]{--text-color: var(--text-1-brand)}a-text[tone=brand][priority=tertiary]{--text-color: var(--text-3-brand);--text-link-hover: var(--text-2-brand)}a-text[tone=brand][priority=quaternary]{--text-color: var(--text-4-brand);--text-link-hover: var(--text-3-brand)}a-text[tone=brand][priority=quinary]{--text-color: var(--text-5-brand);--text-link-hover: var(--text-4-brand)}a-text[tone=success]{--text-color: var(--text-2-success);--text-link-color: currentColor;--text-link-hover: var(--text-1-success)}a-text[tone=success][priority=primary]{--text-color: var(--text-1-success)}a-text[tone=success][priority=tertiary]{--text-color: var(--text-3-success);--text-link-hover: var(--text-2-success)}a-text[tone=success][priority=quaternary]{--text-color: var(--text-4-success);--text-link-hover: var(--text-3-success)}a-text[tone=success][priority=quinary]{--text-color: var(--text-5-success);--text-link-hover: var(--text-4-success)}a-text[tone=critical]{--text-color: var(--text-2-critical);--text-link-color: currentColor;--text-link-hover: var(--text-1-critical)}a-text[tone=critical][priority=primary]{--text-color: var(--text-1-critical)}a-text[tone=critical][priority=tertiary]{--text-color: var(--text-3-critical);--text-link-hover: var(--text-2-critical)}a-text[tone=critical][priority=quaternary]{--text-color: var(--text-4-critical);--text-link-hover: var(--text-3-critical)}a-text[tone=critical][priority=quinary]{--text-color: var(--text-5-critical);--text-link-hover: var(--text-4-critical)}a-text[tone=warning]{--text-color: var(--text-2-warning);--text-link-color: currentColor;--text-link-hover: var(--text-1-warning)}a-text[tone=warning][priority=primary]{--text-color: var(--text-1-warning)}a-text[tone=warning][priority=tertiary]{--text-color: var(--text-3-warning);--text-link-hover: var(--text-2-warning)}a-text[tone=warning][priority=quaternary]{--text-color: var(--text-4-warning);--text-link-hover: var(--text-3-warning)}a-text[tone=warning][priority=quinary]{--text-color: var(--text-5-warning);--text-link-hover: var(--text-4-warning)}a-text[tone=info]{--text-color: var(--text-2-info);--text-link-color: currentColor;--text-link-hover: var(--text-1-info)}a-text[tone=info][priority=primary]{--text-color: var(--text-1-info)}a-text[tone=info][priority=tertiary]{--text-color: var(--text-3-info);--text-link-hover: var(--text-2-info)}a-text[tone=info][priority=quaternary]{--text-color: var(--text-4-info);--text-link-hover: var(--text-3-info)}a-text[tone=info][priority=quinary]{--text-color: var(--text-5-info);--text-link-hover: var(--text-4-info)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--text-tone-source: attr(tone type(<color>), currentColor);--_text-tone: oklch(from var(--text-tone-source) .41 .15 h);--text-color: var(--_text-tone);--text-link-color: currentColor;--text-link-hover: oklch(from var(--text-tone-source) .33 .15 h)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=primary]{--text-color: oklch(from var(--text-tone-source) .33 .15 h)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=tertiary]{--text-color: color-mix(in oklch, var(--_text-tone) 80%, transparent);--text-link-hover: var(--_text-tone)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=quaternary]{--text-color: color-mix(in oklch, var(--_text-tone) 60%, transparent)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=quinary]{--text-color: color-mix(in oklch, var(--_text-tone) 40%, transparent)}.dark a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--_text-tone: oklch(from var(--text-tone-source) .75 .11 h);--text-link-hover: oklch(from var(--text-tone-source) .82 .1 h)}.dark a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=primary]{--text-color: oklch(from var(--text-tone-source) .82 .1 h)}a-text a,a-text a:link,a-text a:visited{color:var(--text-link-color)}@media(hover:hover)and (pointer:fine){a-text a:hover{color:var(--text-link-hover);text-decoration-color:var(--text-link-hover)}}}
|
|
1
|
+
@layer anta{a-text{--text-color: var(--text-2);--text-link-color: var(--link-color);--text-link-hover: var(--link-color-hover);display:block;font-family:var(--sans-serif);color:var(--text-color);font-size:15px;line-height:20px;text-wrap:pretty;font-feature-settings:"ss02","ss05","tnum"}a-text[size=small]{font-size:13px;line-height:16px}a-text[size=medium]{font-size:15px;line-height:20px}a-text[size=large]{font-size:17px;line-height:24px}a-text[inline]{display:inline-block}a-text[truncate]{min-width:0}a-text[priority=primary]{--text-color: var(--text-1)}a-text[priority=tertiary]{--text-color: var(--text-3);--text-link-color: currentColor;--text-link-hover: var(--text-2)}a-text[priority=quaternary]{--text-color: var(--text-4);--text-link-color: currentColor;--text-link-hover: var(--text-3)}a-text[priority=quinary]{--text-color: var(--text-5);--text-link-color: currentColor;--text-link-hover: var(--text-4)}a-text[tone=brand]{--text-color: var(--text-2-brand);--text-link-color: currentColor;--text-link-hover: var(--text-1-brand)}a-text[tone=brand][priority=primary]{--text-color: var(--text-1-brand)}a-text[tone=brand][priority=tertiary]{--text-color: var(--text-3-brand);--text-link-hover: var(--text-2-brand)}a-text[tone=brand][priority=quaternary]{--text-color: var(--text-4-brand);--text-link-hover: var(--text-3-brand)}a-text[tone=brand][priority=quinary]{--text-color: var(--text-5-brand);--text-link-hover: var(--text-4-brand)}a-text[tone=success]{--text-color: var(--text-2-success);--text-link-color: currentColor;--text-link-hover: var(--text-1-success)}a-text[tone=success][priority=primary]{--text-color: var(--text-1-success)}a-text[tone=success][priority=tertiary]{--text-color: var(--text-3-success);--text-link-hover: var(--text-2-success)}a-text[tone=success][priority=quaternary]{--text-color: var(--text-4-success);--text-link-hover: var(--text-3-success)}a-text[tone=success][priority=quinary]{--text-color: var(--text-5-success);--text-link-hover: var(--text-4-success)}a-text[tone=critical]{--text-color: var(--text-2-critical);--text-link-color: currentColor;--text-link-hover: var(--text-1-critical)}a-text[tone=critical][priority=primary]{--text-color: var(--text-1-critical)}a-text[tone=critical][priority=tertiary]{--text-color: var(--text-3-critical);--text-link-hover: var(--text-2-critical)}a-text[tone=critical][priority=quaternary]{--text-color: var(--text-4-critical);--text-link-hover: var(--text-3-critical)}a-text[tone=critical][priority=quinary]{--text-color: var(--text-5-critical);--text-link-hover: var(--text-4-critical)}a-text[tone=warning]{--text-color: var(--text-2-warning);--text-link-color: currentColor;--text-link-hover: var(--text-1-warning)}a-text[tone=warning][priority=primary]{--text-color: var(--text-1-warning)}a-text[tone=warning][priority=tertiary]{--text-color: var(--text-3-warning);--text-link-hover: var(--text-2-warning)}a-text[tone=warning][priority=quaternary]{--text-color: var(--text-4-warning);--text-link-hover: var(--text-3-warning)}a-text[tone=warning][priority=quinary]{--text-color: var(--text-5-warning);--text-link-hover: var(--text-4-warning)}a-text[tone=info]{--text-color: var(--text-2-info);--text-link-color: currentColor;--text-link-hover: var(--text-1-info)}a-text[tone=info][priority=primary]{--text-color: var(--text-1-info)}a-text[tone=info][priority=tertiary]{--text-color: var(--text-3-info);--text-link-hover: var(--text-2-info)}a-text[tone=info][priority=quaternary]{--text-color: var(--text-4-info);--text-link-hover: var(--text-3-info)}a-text[tone=info][priority=quinary]{--text-color: var(--text-5-info);--text-link-hover: var(--text-4-info)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--text-tone-source: attr(tone type(<color>), currentColor);--_text-tone: oklch(from var(--text-tone-source) .41 .15 h);--text-color: var(--_text-tone);--text-link-color: currentColor;--text-link-hover: oklch(from var(--text-tone-source) .33 .15 h)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=primary]{--text-color: oklch(from var(--text-tone-source) .33 .15 h)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=tertiary]{--text-color: color-mix(in oklch, var(--_text-tone) 80%, transparent);--text-link-hover: var(--_text-tone)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=quaternary]{--text-color: color-mix(in oklch, var(--_text-tone) 60%, transparent)}a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=quinary]{--text-color: color-mix(in oklch, var(--_text-tone) 40%, transparent)}.dark a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--_text-tone: oklch(from var(--text-tone-source) .75 .11 h);--text-link-hover: oklch(from var(--text-tone-source) .82 .1 h)}.dark a-text[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=primary]{--text-color: oklch(from var(--text-tone-source) .82 .1 h)}a-text a,a-text a:link,a-text a:visited{color:var(--text-link-color)}@media(hover:hover)and (pointer:fine){a-text a:hover{color:var(--text-link-hover);text-decoration-color:var(--text-link-hover)}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer anta{a-title{display:block;color:var(--text-1);font-weight:584.62;letter-spacing:0;text-wrap:balance;font-feature-settings:"ss02","ss05","tnum"}a-title a-icon{transform:translateY(-.05em)}a-title[level="1"]{font-size:28px;line-height:32px
|
|
1
|
+
@layer anta{a-title{display:block;font-family:var(--sans-serif);color:var(--text-1);font-weight:584.62;letter-spacing:0;text-wrap:balance;font-feature-settings:"ss02","ss05","tnum"}a-title a-icon{transform:translateY(-.05em)}a-title[level="1"]{font-size:28px;line-height:32px}a-title[level="2"]{font-size:24px;line-height:28px}a-title[level="3"]{font-size:20px;line-height:24px}a-title[level="4"]{font-size:17px;line-height:20px}a-title[level="5"]{font-size:15px;line-height:20px}a-title[level="6"]{font-size:13px;line-height:16px}a-title[priority=secondary]{color:var(--text-2)}a-title[priority=tertiary]{color:var(--text-3)}a-title[priority=quaternary]{color:var(--text-4)}a-title[priority=quinary]{color:var(--text-5)}a-title[tone=brand]{color:var(--text-1-brand)}a-title[tone=brand][priority=secondary]{color:var(--text-2-brand)}a-title[tone=brand][priority=tertiary]{color:var(--text-3-brand)}a-title[tone=brand][priority=quaternary]{color:var(--text-4-brand)}a-title[tone=brand][priority=quinary]{color:var(--text-5-brand)}a-title[tone=success]{color:var(--text-1-success)}a-title[tone=success][priority=secondary]{color:var(--text-2-success)}a-title[tone=success][priority=tertiary]{color:var(--text-3-success)}a-title[tone=success][priority=quaternary]{color:var(--text-4-success)}a-title[tone=success][priority=quinary]{color:var(--text-5-success)}a-title[tone=critical]{color:var(--text-1-critical)}a-title[tone=critical][priority=secondary]{color:var(--text-2-critical)}a-title[tone=critical][priority=tertiary]{color:var(--text-3-critical)}a-title[tone=critical][priority=quaternary]{color:var(--text-4-critical)}a-title[tone=critical][priority=quinary]{color:var(--text-5-critical)}a-title[tone=warning]{color:var(--text-1-warning)}a-title[tone=warning][priority=secondary]{color:var(--text-2-warning)}a-title[tone=warning][priority=tertiary]{color:var(--text-3-warning)}a-title[tone=warning][priority=quaternary]{color:var(--text-4-warning)}a-title[tone=warning][priority=quinary]{color:var(--text-5-warning)}a-title[tone=info]{color:var(--text-1-info)}a-title[tone=info][priority=secondary]{color:var(--text-2-info)}a-title[tone=info][priority=tertiary]{color:var(--text-3-info)}a-title[tone=info][priority=quaternary]{color:var(--text-4-info)}a-title[tone=info][priority=quinary]{color:var(--text-5-info)}a-title[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--title-tone-source: attr(tone type(<color>), currentColor);--_title-tone: oklch(from var(--title-tone-source) .41 .15 h);color:oklch(from var(--title-tone-source) .33 .15 h)}a-title[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=secondary]{color:var(--_title-tone)}a-title[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=tertiary]{color:color-mix(in oklch,var(--_title-tone) 80%,transparent)}a-title[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=quaternary]{color:color-mix(in oklch,var(--_title-tone) 60%,transparent)}a-title[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical])[priority=quinary]{color:color-mix(in oklch,var(--_title-tone) 40%,transparent)}.dark a-title[tone]:not([tone=neutral],[tone=brand],[tone=info],[tone=success],[tone=warning],[tone=critical]){--_title-tone: oklch(from var(--title-tone-source) .75 .11 h);color:oklch(from var(--title-tone-source) .82 .1 h)}}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared copy-to-clipboard behavior for `<a-button>` and `<a-menu-item>` — the
|
|
3
|
+
* write half of the productized "copy button" / "copying menu item". The element
|
|
4
|
+
* performs the clipboard write itself (rather than a UI-thread callback) and
|
|
5
|
+
* reports the outcome with a `copydone` event; the JSX wrapper listens and
|
|
6
|
+
* orchestrates the visual feedback (icon + tone swap). Nothing here touches the
|
|
7
|
+
* DOM — no light-DOM mutation, no host attributes, no ElementInternals.
|
|
8
|
+
*
|
|
9
|
+
* Events are dispatched non-bubbling (`bubbles: false`): the wrapper binds on the
|
|
10
|
+
* host itself, so it catches them in the target phase, and not bubbling keeps a
|
|
11
|
+
* nested copy row's `copydone` from flipping an ancestor copy row's feedback
|
|
12
|
+
* (the same point-to-point rule Anta's `statechange` follows).
|
|
13
|
+
*
|
|
14
|
+
* Modes, chosen by attribute:
|
|
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>`.
|
|
18
|
+
* - `copy-node` / `copy-node="<selector>"` — copy a DOM node as rich text
|
|
19
|
+
* (`text/html`) + plain text. Bare `copy-node` copies the nearest ancestor
|
|
20
|
+
* marked `[data-copy-source]`; a value is a selector resolved with `closest()`
|
|
21
|
+
* (an ancestor region). The copy control is stripped from the serialized
|
|
22
|
+
* output (`[data-copy-node-button]`).
|
|
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.
|
|
35
|
+
*
|
|
36
|
+
* Precedence when several are set (the discriminated-union prop types make this
|
|
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.
|
|
46
|
+
*/
|
|
47
|
+
export declare function emitCopyRequest(el: HTMLElement): void;
|
|
48
|
+
/**
|
|
49
|
+
* Perform a copy on activation (click / Enter / Space / menu select). Returns
|
|
50
|
+
* `true` if the element is a copy control (so the caller knows the copy path
|
|
51
|
+
* ran); `false` for a plain, non-copy element.
|
|
52
|
+
*/
|
|
53
|
+
export declare function runCopy(el: HTMLElement): boolean;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
async function writeText(text) {
|
|
2
|
+
try {
|
|
3
|
+
await navigator.clipboard.writeText(text);
|
|
4
|
+
return true;
|
|
5
|
+
} catch {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
async function writeNode(node) {
|
|
10
|
+
const clone = node.cloneNode(true);
|
|
11
|
+
clone.querySelectorAll("[data-copy-node-button]").forEach((n) => n.remove());
|
|
12
|
+
const html = clone.outerHTML;
|
|
13
|
+
const text = clone.textContent ?? "";
|
|
14
|
+
try {
|
|
15
|
+
if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
|
|
16
|
+
await navigator.clipboard.write([
|
|
17
|
+
new ClipboardItem({
|
|
18
|
+
"text/html": new Blob([html], { type: "text/html" }),
|
|
19
|
+
"text/plain": new Blob([text], { type: "text/plain" })
|
|
20
|
+
})
|
|
21
|
+
]);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
await navigator.clipboard.writeText(text);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function resolveNodeTarget(el) {
|
|
31
|
+
const sel = el.getAttribute("copy-node");
|
|
32
|
+
if (sel == null) return null;
|
|
33
|
+
return el.closest(sel === "" ? "[data-copy-source]" : sel);
|
|
34
|
+
}
|
|
35
|
+
function run(el, work) {
|
|
36
|
+
void work.then((ok) => {
|
|
37
|
+
el.dispatchEvent(new CustomEvent("copydone", { bubbles: false, detail: { ok } }));
|
|
38
|
+
});
|
|
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
|
+
}
|
|
57
|
+
function runCopy(el) {
|
|
58
|
+
if (el.hasAttribute("copy-node")) {
|
|
59
|
+
const target = resolveNodeTarget(el);
|
|
60
|
+
run(el, target ? writeNode(target) : Promise.resolve(false));
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (!isTextCopy(el)) return false;
|
|
64
|
+
run(el, writeText(resolveText(el)));
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
emitCopyRequest,
|
|
69
|
+
runCopy
|
|
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
|
@@ -727,6 +727,28 @@ export interface AMenuItemAttributes extends BaseAttributes {
|
|
|
727
727
|
* a `MouseEvent`, carrying the modifier keys (e.g. `altKey`). All-lowercase so
|
|
728
728
|
* React/Preact bind it to the CustomEvent. */
|
|
729
729
|
onmenuselect?: (e: MouseEvent) => void;
|
|
730
|
+
/** Text copied to the clipboard when the row is chosen (a "copying menu item").
|
|
731
|
+
* The element performs the write itself on `menuselect`. */
|
|
732
|
+
copy?: string;
|
|
733
|
+
/** Copy a DOM node instead of a string (rich `text/html` + plain text). Bare
|
|
734
|
+
* `copy-node` copies the nearest ancestor marked `[data-copy-source]`; a value
|
|
735
|
+
* is a CSS selector resolved with `closest()`. Presence-based for the boolean
|
|
736
|
+
* form. */
|
|
737
|
+
'copy-node'?: boolean | string;
|
|
738
|
+
/** Copy the current page URL (`location.href`) instead of a `copy` string.
|
|
739
|
+
* Presence-based. */
|
|
740
|
+
'copy-url'?: boolean | '';
|
|
741
|
+
/** Prefix the copied `copy` string with `// URL: <href>`. Presence-based. */
|
|
742
|
+
'copy-with-url'?: boolean | '';
|
|
743
|
+
/** Fired after a copy attempt (bubbling `copydone`, `detail: { ok }`).
|
|
744
|
+
* All-lowercase so it binds in React *and* Preact. */
|
|
745
|
+
oncopydone?: (e: CustomEvent<{
|
|
746
|
+
ok: boolean;
|
|
747
|
+
}>) => void;
|
|
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. */
|
|
751
|
+
oncopyrequest?: (e: CustomEvent) => void;
|
|
730
752
|
}
|
|
731
753
|
/**
|
|
732
754
|
* Attributes for the `<a-menu-group>` styled element. For the typed JSX
|
|
@@ -770,6 +792,30 @@ export interface AButtonAttributes extends BaseAttributes {
|
|
|
770
792
|
form?: string;
|
|
771
793
|
/** Custom event name dispatched (bubbling) on click. */
|
|
772
794
|
'data-custom-event'?: string;
|
|
795
|
+
/** Text copied to the clipboard on activation (the "copy button"). The element
|
|
796
|
+
* performs the write itself and flashes a success / failure state. */
|
|
797
|
+
copy?: string;
|
|
798
|
+
/** Copy a DOM node instead of a string (rich `text/html` + plain text). Bare
|
|
799
|
+
* `copy-node` copies the nearest ancestor marked `[data-copy-source]`; a value
|
|
800
|
+
* is a CSS selector resolved with `closest()` (an ancestor region). Presence-
|
|
801
|
+
* based for the boolean form. */
|
|
802
|
+
'copy-node'?: boolean | string;
|
|
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 | '';
|
|
808
|
+
/** Fired after a copy attempt (`copydone`, `detail: { ok }`, non-bubbling so a
|
|
809
|
+
* nested copy row can't flip an ancestor's feedback). All-lowercase so it
|
|
810
|
+
* binds in React *and* Preact. */
|
|
811
|
+
oncopydone?: (e: CustomEvent<{
|
|
812
|
+
ok: boolean;
|
|
813
|
+
}>) => 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;
|
|
773
819
|
'aria-disabled'?: 'true' | 'false' | boolean;
|
|
774
820
|
'aria-busy'?: 'true' | 'false' | boolean;
|
|
775
821
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,9 @@ export type { TagProps } from './components/Tag';
|
|
|
27
27
|
export { Icon } from './components/Icon';
|
|
28
28
|
export type { IconProps } from './components/Icon';
|
|
29
29
|
export { Button } from './components/Button';
|
|
30
|
-
export type { ButtonProps, BaseButtonProps, ContentMode, SubmitMode, PriorityMode, } from './components/Button';
|
|
30
|
+
export type { ButtonProps, BaseButtonProps, ContentMode, SubmitMode, PriorityMode, CopyMode, } from './components/Button';
|
|
31
|
+
export { ButtonCopy } from './components/ButtonCopy';
|
|
32
|
+
export type { ButtonCopyProps } from './components/ButtonCopy';
|
|
31
33
|
export { ICON_SHAPES, ICON_SYNONYMS } from './elements/a-icon.shapes';
|
|
32
34
|
export { Tooltip } from './components/Tooltip';
|
|
33
35
|
export type { TooltipProps } from './components/Tooltip';
|
|
@@ -37,6 +39,8 @@ export { Menu } from './components/Menu';
|
|
|
37
39
|
export type { MenuProps } from './components/Menu';
|
|
38
40
|
export { MenuItem } from './components/MenuItem';
|
|
39
41
|
export type { MenuItemProps } from './components/MenuItem';
|
|
42
|
+
export { MenuItemCopy } from './components/MenuItemCopy';
|
|
43
|
+
export type { MenuItemCopyProps } from './components/MenuItemCopy';
|
|
40
44
|
export { MenuSeparator } from './components/MenuSeparator';
|
|
41
45
|
export type { MenuSeparatorProps } from './components/MenuSeparator';
|
|
42
46
|
export { MenuGroup } from './components/MenuGroup';
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,13 @@ import { Title } from "./components/Title";
|
|
|
4
4
|
import { Tag } from "./components/Tag";
|
|
5
5
|
import { Icon } from "./components/Icon";
|
|
6
6
|
import { Button } from "./components/Button";
|
|
7
|
+
import { ButtonCopy } from "./components/ButtonCopy";
|
|
7
8
|
import { ICON_SHAPES, ICON_SYNONYMS } from "./elements/a-icon.shapes";
|
|
8
9
|
import { Tooltip } from "./components/Tooltip";
|
|
9
10
|
import { Checkbox } from "./components/Checkbox";
|
|
10
11
|
import { Menu } from "./components/Menu";
|
|
11
12
|
import { MenuItem } from "./components/MenuItem";
|
|
13
|
+
import { MenuItemCopy } from "./components/MenuItemCopy";
|
|
12
14
|
import { MenuSeparator } from "./components/MenuSeparator";
|
|
13
15
|
import { MenuGroup } from "./components/MenuGroup";
|
|
14
16
|
import { Expander } from "./components/Expander";
|
|
@@ -43,6 +45,7 @@ import { Card } from "./components/Card";
|
|
|
43
45
|
import { configure } from "./jsx-runtime";
|
|
44
46
|
export {
|
|
45
47
|
Button,
|
|
48
|
+
ButtonCopy,
|
|
46
49
|
Calendar,
|
|
47
50
|
Card,
|
|
48
51
|
Checkbox,
|
|
@@ -58,6 +61,7 @@ export {
|
|
|
58
61
|
Menu,
|
|
59
62
|
MenuGroup,
|
|
60
63
|
MenuItem,
|
|
64
|
+
MenuItemCopy,
|
|
61
65
|
MenuSeparator,
|
|
62
66
|
Progress,
|
|
63
67
|
RadioGroup,
|