@adia-ai/web-components 0.6.36 → 0.6.38
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/CHANGELOG.md +48 -1
- package/components/accordion/accordion-item.a2ui.json +3 -0
- package/components/accordion/accordion-item.yaml +5 -0
- package/components/action-list/action-item.a2ui.json +5 -1
- package/components/action-list/action-item.yaml +7 -0
- package/components/badge/badge.a2ui.json +10 -0
- package/components/badge/badge.css +70 -0
- package/components/badge/badge.yaml +20 -0
- package/components/blockquote/blockquote.a2ui.json +121 -0
- package/components/blockquote/blockquote.class.js +68 -0
- package/components/blockquote/blockquote.css +46 -0
- package/components/blockquote/blockquote.d.ts +31 -0
- package/components/blockquote/blockquote.js +17 -0
- package/components/blockquote/blockquote.yaml +124 -0
- package/components/button/button.css +11 -3
- package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
- package/components/calendar-picker/calendar-picker.class.js +7 -1
- package/components/calendar-picker/calendar-picker.yaml +14 -0
- package/components/card/card.a2ui.json +17 -1
- package/components/card/card.yaml +24 -1
- package/components/color-input/color-input.a2ui.json +2 -2
- package/components/color-input/color-input.class.js +9 -2
- package/components/color-input/color-input.yaml +2 -2
- package/components/combobox/combobox.class.js +4 -0
- package/components/context-menu/context-menu.a2ui.json +159 -0
- package/components/context-menu/context-menu.class.js +275 -0
- package/components/context-menu/context-menu.css +56 -0
- package/components/context-menu/context-menu.d.ts +70 -0
- package/components/context-menu/context-menu.js +17 -0
- package/components/context-menu/context-menu.yaml +136 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
- package/components/date-range-picker/date-range-picker.class.js +2 -0
- package/components/date-range-picker/date-range-picker.yaml +14 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
- package/components/datetime-picker/datetime-picker.class.js +3 -1
- package/components/datetime-picker/datetime-picker.d.ts +2 -0
- package/components/datetime-picker/datetime-picker.yaml +14 -0
- package/components/empty-state/empty-state.a2ui.json +9 -0
- package/components/empty-state/empty-state.class.js +2 -0
- package/components/empty-state/empty-state.yaml +15 -0
- package/components/feed/feed-item.a2ui.json +5 -0
- package/components/feed/feed-item.yaml +10 -0
- package/components/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/field/field.a2ui.json +6 -0
- package/components/field/field.yaml +10 -0
- package/components/index.js +11 -0
- package/components/inline-edit/inline-edit.a2ui.json +159 -0
- package/components/inline-edit/inline-edit.class.js +184 -0
- package/components/inline-edit/inline-edit.css +62 -0
- package/components/inline-edit/inline-edit.d.ts +52 -0
- package/components/inline-edit/inline-edit.js +12 -0
- package/components/inline-edit/inline-edit.yaml +125 -0
- package/components/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- package/components/list/list-item.a2ui.json +8 -1
- package/components/list/list-item.yaml +12 -0
- package/components/list/list.css +36 -6
- package/components/mark/mark.a2ui.json +109 -0
- package/components/mark/mark.class.js +22 -0
- package/components/mark/mark.css +39 -0
- package/components/mark/mark.d.ts +27 -0
- package/components/mark/mark.js +12 -0
- package/components/mark/mark.yaml +87 -0
- package/components/modal/modal.a2ui.json +9 -0
- package/components/modal/modal.yaml +14 -0
- package/components/nav-group/nav-group.a2ui.json +3 -0
- package/components/nav-group/nav-group.css +7 -1
- package/components/nav-group/nav-group.yaml +5 -0
- package/components/nav-item/nav-item.a2ui.json +3 -0
- package/components/nav-item/nav-item.yaml +5 -0
- package/components/number-format/number-format.a2ui.json +180 -0
- package/components/number-format/number-format.class.js +96 -0
- package/components/number-format/number-format.css +18 -0
- package/components/number-format/number-format.d.ts +68 -0
- package/components/number-format/number-format.js +17 -0
- package/components/number-format/number-format.yaml +204 -0
- package/components/pagination/pagination.a2ui.json +19 -2
- package/components/pagination/pagination.class.js +90 -37
- package/components/pagination/pagination.css +32 -127
- package/components/pagination/pagination.d.ts +8 -2
- package/components/pagination/pagination.test.js +195 -0
- package/components/pagination/pagination.yaml +22 -1
- package/components/password-strength/password-strength.a2ui.json +152 -0
- package/components/password-strength/password-strength.class.js +157 -0
- package/components/password-strength/password-strength.css +80 -0
- package/components/password-strength/password-strength.d.ts +59 -0
- package/components/password-strength/password-strength.js +17 -0
- package/components/password-strength/password-strength.yaml +153 -0
- package/components/popover/popover.css +43 -23
- package/components/popover/popover.yaml +8 -4
- package/components/qr-code/QR-TEST.svg +4 -0
- package/components/qr-code/qr-code.a2ui.json +154 -0
- package/components/qr-code/qr-code.class.js +129 -0
- package/components/qr-code/qr-code.css +41 -0
- package/components/qr-code/qr-code.d.ts +83 -0
- package/components/qr-code/qr-code.js +17 -0
- package/components/qr-code/qr-code.yaml +203 -0
- package/components/qr-code/qr-encoder.js +633 -0
- package/components/relative-time/relative-time.a2ui.json +120 -0
- package/components/relative-time/relative-time.class.js +136 -0
- package/components/relative-time/relative-time.css +22 -0
- package/components/relative-time/relative-time.d.ts +51 -0
- package/components/relative-time/relative-time.js +17 -0
- package/components/relative-time/relative-time.yaml +133 -0
- package/components/segmented/segmented.class.js +15 -3
- package/components/select/select.a2ui.json +3 -0
- package/components/select/select.class.js +4 -0
- package/components/select/select.yaml +5 -0
- package/components/skip-nav/skip-nav.a2ui.json +92 -0
- package/components/skip-nav/skip-nav.class.js +45 -0
- package/components/skip-nav/skip-nav.css +54 -0
- package/components/skip-nav/skip-nav.d.ts +27 -0
- package/components/skip-nav/skip-nav.js +12 -0
- package/components/skip-nav/skip-nav.yaml +68 -0
- package/components/slider/slider.a2ui.json +22 -1
- package/components/slider/slider.class.js +264 -122
- package/components/slider/slider.css +82 -2
- package/components/slider/slider.d.ts +19 -3
- package/components/slider/slider.test.js +55 -0
- package/components/slider/slider.yaml +38 -6
- package/components/stat/stat.css +18 -14
- package/components/stepper/stepper-item.a2ui.json +3 -0
- package/components/stepper/stepper-item.yaml +5 -0
- package/components/table/table.class.js +29 -6
- package/components/table/table.css +31 -4
- package/components/table-toolbar/table-toolbar.class.js +3 -1
- package/components/tag/tag.a2ui.json +3 -2
- package/components/tag/tag.css +35 -11
- package/components/tag/tag.d.ts +14 -0
- package/components/tag/tag.test.js +35 -11
- package/components/tag/tag.yaml +13 -7
- package/components/timeline/timeline-item.a2ui.json +8 -1
- package/components/timeline/timeline-item.yaml +12 -0
- package/components/toast/toast.class.js +12 -4
- package/components/toc/toc.a2ui.json +159 -0
- package/components/toc/toc.class.js +222 -0
- package/components/toc/toc.css +92 -0
- package/components/toc/toc.d.ts +61 -0
- package/components/toc/toc.js +17 -0
- package/components/toc/toc.yaml +180 -0
- package/components/toolbar/toolbar.class.js +3 -0
- package/components/tree/tree-item.a2ui.json +5 -1
- package/components/tree/tree-item.yaml +7 -0
- package/components/tree/tree.a2ui.json +3 -0
- package/components/tree/tree.yaml +5 -0
- package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
- package/components/visually-hidden/visually-hidden.class.js +14 -0
- package/components/visually-hidden/visually-hidden.css +25 -0
- package/components/visually-hidden/visually-hidden.d.ts +26 -0
- package/components/visually-hidden/visually-hidden.js +12 -0
- package/components/visually-hidden/visually-hidden.yaml +54 -0
- package/core/anchor.js +19 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +100 -89
- package/package.json +1 -1
- package/styles/colors/semantics.css +11 -2
- package/styles/components.css +11 -0
- package/styles/resets.css +10 -0
|
@@ -161,15 +161,23 @@ button-ui[color="danger"]:not([disabled]):hover {
|
|
|
161
161
|
--button-fg-default: var(--button-fg-danger, var(--button-fg-danger-default));
|
|
162
162
|
}
|
|
163
163
|
:scope[color="success"] {
|
|
164
|
-
|
|
164
|
+
/* Consume the L3 `-bg` alias, not the L2 `-strong` step. Currently
|
|
165
|
+
resolves identically (both = `-50`), but consuming `-bg` future-
|
|
166
|
+
proofs if the surface-step ever redirects (as -warning-bg did
|
|
167
|
+
in v0.6.36 to fix muddy contrast). */
|
|
168
|
+
--button-bg-default: var(--a-success-bg);
|
|
165
169
|
--button-fg-default: var(--a-success-fg);
|
|
166
170
|
}
|
|
167
171
|
:scope[color="info"] {
|
|
168
|
-
--button-bg-default: var(--a-info-
|
|
172
|
+
--button-bg-default: var(--a-info-bg);
|
|
169
173
|
--button-fg-default: var(--a-info-fg);
|
|
170
174
|
}
|
|
171
175
|
:scope[color="warning"] {
|
|
172
|
-
|
|
176
|
+
/* `--a-warning-bg` (bright amber, scheme-independent) not
|
|
177
|
+
`-strong` (mid-tone). The pair `-strong` + `-fg` gives muddy
|
|
178
|
+
brown-on-brown — the L3 `-bg` is the canonical solid-warning
|
|
179
|
+
surface paired with `-fg` (dark text). */
|
|
180
|
+
--button-bg-default: var(--a-warning-bg);
|
|
173
181
|
--button-fg-default: var(--a-warning-fg);
|
|
174
182
|
}
|
|
175
183
|
|
|
@@ -66,6 +66,21 @@
|
|
|
66
66
|
"type": "string",
|
|
67
67
|
"default": "Select date..."
|
|
68
68
|
},
|
|
69
|
+
"placement": {
|
|
70
|
+
"description": "Popover placement relative to the trigger. Default `bottom` centers the calendar panel under the trigger (ADR-0034 Rule 2 — calendar panel wider than trigger).",
|
|
71
|
+
"type": "string",
|
|
72
|
+
"enum": [
|
|
73
|
+
"top",
|
|
74
|
+
"bottom",
|
|
75
|
+
"left",
|
|
76
|
+
"right",
|
|
77
|
+
"top-start",
|
|
78
|
+
"top-end",
|
|
79
|
+
"bottom-start",
|
|
80
|
+
"bottom-end"
|
|
81
|
+
],
|
|
82
|
+
"default": "bottom"
|
|
83
|
+
},
|
|
69
84
|
"value": {
|
|
70
85
|
"description": "Selected date (ISO string)",
|
|
71
86
|
"type": "string",
|
|
@@ -68,6 +68,10 @@ export class UICalendarPicker extends UIFormElement {
|
|
|
68
68
|
min: { type: String, default: '', reflect: true },
|
|
69
69
|
max: { type: String, default: '', reflect: true },
|
|
70
70
|
open: { type: Boolean, default: false, reflect: true },
|
|
71
|
+
// Popover placement — yaml documented this as reflect:true since v1;
|
|
72
|
+
// the JS reads it via getAttribute('placement') in the anchorPopover
|
|
73
|
+
// call site. Declaring here so el.placement = 'top' also updates.
|
|
74
|
+
placement: { type: String, default: 'bottom', reflect: true },
|
|
71
75
|
};
|
|
72
76
|
|
|
73
77
|
static template = () => null;
|
|
@@ -139,8 +143,10 @@ export class UICalendarPicker extends UIFormElement {
|
|
|
139
143
|
if (this.open) {
|
|
140
144
|
this.#renderCalendar();
|
|
141
145
|
this.#popover?.showPopover?.();
|
|
146
|
+
// ADR-0034 Rule 2: calendar panel (~330px) >> trigger button (~200px) → center.
|
|
147
|
+
// Consumer can override via placement="bottom-start|top|…" attribute.
|
|
142
148
|
this.#anchorCleanup = anchorPopover(this.#trigger, this.#popover, {
|
|
143
|
-
placement: 'bottom
|
|
149
|
+
placement: this.getAttribute('placement') || 'bottom', gap: 4,
|
|
144
150
|
});
|
|
145
151
|
this.#openRaf = requestAnimationFrame(() => {
|
|
146
152
|
this.#openRaf = null;
|
|
@@ -59,6 +59,20 @@ props:
|
|
|
59
59
|
description: Selected date (ISO string)
|
|
60
60
|
type: string
|
|
61
61
|
default: ''
|
|
62
|
+
placement:
|
|
63
|
+
description: Popover placement relative to the trigger. Default `bottom` centers the calendar panel under the trigger (ADR-0034 Rule 2 — calendar panel wider than trigger).
|
|
64
|
+
type: string
|
|
65
|
+
default: bottom
|
|
66
|
+
reflect: true
|
|
67
|
+
enum:
|
|
68
|
+
- top
|
|
69
|
+
- bottom
|
|
70
|
+
- left
|
|
71
|
+
- right
|
|
72
|
+
- top-start
|
|
73
|
+
- top-end
|
|
74
|
+
- bottom-start
|
|
75
|
+
- bottom-end
|
|
62
76
|
events:
|
|
63
77
|
change:
|
|
64
78
|
description: Fired when a date is selected
|
|
@@ -113,7 +113,23 @@
|
|
|
113
113
|
"alert",
|
|
114
114
|
"skeleton"
|
|
115
115
|
],
|
|
116
|
-
"slots": {
|
|
116
|
+
"slots": {
|
|
117
|
+
"description": {
|
|
118
|
+
"description": "Optional descriptive text beneath the heading. Renders in the header slot at body-subtle typography. Use for short metadata lines (timestamp, author, status sentence)."
|
|
119
|
+
},
|
|
120
|
+
"action": {
|
|
121
|
+
"description": "Trailing action cluster in the header (e.g. icon-buttons, menu trigger, more-options). Aligns to the header's flex-end edge."
|
|
122
|
+
},
|
|
123
|
+
"action-leading": {
|
|
124
|
+
"description": "Leading action cluster in the header (e.g. back button, switcher, breadcrumb-context). Aligns to the header's flex-start edge, before the icon/heading column."
|
|
125
|
+
},
|
|
126
|
+
"heading": {
|
|
127
|
+
"description": "Card title. Renders in the header slot with title typography. Typically a short noun phrase or document/object name."
|
|
128
|
+
},
|
|
129
|
+
"icon": {
|
|
130
|
+
"description": "Optional leading icon for the card header (status / brand / type marker). Renders next to the heading. Use `<icon-ui name=\"…\">` or any inline icon element."
|
|
131
|
+
}
|
|
132
|
+
},
|
|
117
133
|
"states": [
|
|
118
134
|
{
|
|
119
135
|
"description": "Default, ready for interaction.",
|
|
@@ -58,7 +58,30 @@ props:
|
|
|
58
58
|
- soft
|
|
59
59
|
- primary
|
|
60
60
|
events: {}
|
|
61
|
-
slots:
|
|
61
|
+
slots:
|
|
62
|
+
icon:
|
|
63
|
+
description: >-
|
|
64
|
+
Optional leading icon for the card header (status / brand / type
|
|
65
|
+
marker). Renders next to the heading. Use `<icon-ui name="…">` or
|
|
66
|
+
any inline icon element.
|
|
67
|
+
heading:
|
|
68
|
+
description: >-
|
|
69
|
+
Card title. Renders in the header slot with title typography.
|
|
70
|
+
Typically a short noun phrase or document/object name.
|
|
71
|
+
description:
|
|
72
|
+
description: >-
|
|
73
|
+
Optional descriptive text beneath the heading. Renders in the
|
|
74
|
+
header slot at body-subtle typography. Use for short metadata
|
|
75
|
+
lines (timestamp, author, status sentence).
|
|
76
|
+
action:
|
|
77
|
+
description: >-
|
|
78
|
+
Trailing action cluster in the header (e.g. icon-buttons, menu
|
|
79
|
+
trigger, more-options). Aligns to the header's flex-end edge.
|
|
80
|
+
action-leading:
|
|
81
|
+
description: >-
|
|
82
|
+
Leading action cluster in the header (e.g. back button, switcher,
|
|
83
|
+
breadcrumb-context). Aligns to the header's flex-start edge,
|
|
84
|
+
before the icon/heading column.
|
|
62
85
|
states:
|
|
63
86
|
- name: idle
|
|
64
87
|
description: Default, ready for interaction.
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"default": false
|
|
67
67
|
},
|
|
68
68
|
"placement": {
|
|
69
|
-
"description": "Popover placement relative to the trigger.",
|
|
69
|
+
"description": "Popover placement relative to the trigger. Default `bottom` centers the color-picker panel under the swatch button (ADR-0034 Rule 2 — panel wider than trigger).",
|
|
70
70
|
"type": "string",
|
|
71
71
|
"enum": [
|
|
72
72
|
"top",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"bottom-start",
|
|
79
79
|
"bottom-end"
|
|
80
80
|
],
|
|
81
|
-
"default": "bottom
|
|
81
|
+
"default": "bottom"
|
|
82
82
|
},
|
|
83
83
|
"value": {
|
|
84
84
|
"description": "Current color as a string in the active [format].",
|
|
@@ -42,7 +42,7 @@ export class UIColorInput extends UIFormElement {
|
|
|
42
42
|
...UIFormElement.properties,
|
|
43
43
|
value: { type: String, default: '#3b82f6', reflect: true },
|
|
44
44
|
format: { type: String, default: 'hex', reflect: true },
|
|
45
|
-
placement: { type: String, default: 'bottom
|
|
45
|
+
placement: { type: String, default: 'bottom', reflect: true }, // ADR-0034 Rule 2: color picker panel >> swatch trigger
|
|
46
46
|
open: { type: Boolean, default: false, reflect: true },
|
|
47
47
|
// Generation-constraint props forwarded to the inner <color-picker-ui>.
|
|
48
48
|
// v0.5.13 §-TBD (FB-33 §1) — full parity with color-picker's 5 constraint
|
|
@@ -63,6 +63,7 @@ export class UIColorInput extends UIFormElement {
|
|
|
63
63
|
#swatch = null;
|
|
64
64
|
#valueLabel = null;
|
|
65
65
|
#wired = false;
|
|
66
|
+
#popoverObserver = null;
|
|
66
67
|
|
|
67
68
|
connected() {
|
|
68
69
|
this.#mount();
|
|
@@ -165,7 +166,11 @@ export class UIColorInput extends UIFormElement {
|
|
|
165
166
|
if (this.open !== open) this.open = open;
|
|
166
167
|
};
|
|
167
168
|
this.#popover.addEventListener('toggle', sync);
|
|
168
|
-
|
|
169
|
+
// Store the observer in a #field so disconnected() can clean it up
|
|
170
|
+
// — without the explicit reference, the observer survives the host
|
|
171
|
+
// element across mount/unmount cycles (audit-lifecycle-leak).
|
|
172
|
+
this.#popoverObserver = new MutationObserver(sync);
|
|
173
|
+
this.#popoverObserver.observe(this.#popover, {
|
|
169
174
|
attributes: true,
|
|
170
175
|
attributeFilter: ['open'],
|
|
171
176
|
});
|
|
@@ -216,6 +221,8 @@ export class UIColorInput extends UIFormElement {
|
|
|
216
221
|
disconnected() {
|
|
217
222
|
this.#picker?.removeEventListener('change', this.#onPickerChange);
|
|
218
223
|
this.#picker?.removeEventListener('input', this.#onPickerInput);
|
|
224
|
+
this.#popoverObserver?.disconnect();
|
|
225
|
+
this.#popoverObserver = null;
|
|
219
226
|
this.#wired = false;
|
|
220
227
|
}
|
|
221
228
|
}
|
|
@@ -44,9 +44,9 @@ props:
|
|
|
44
44
|
default: false
|
|
45
45
|
reflect: true
|
|
46
46
|
placement:
|
|
47
|
-
description: Popover placement relative to the trigger.
|
|
47
|
+
description: Popover placement relative to the trigger. Default `bottom` centers the color-picker panel under the swatch button (ADR-0034 Rule 2 — panel wider than trigger).
|
|
48
48
|
type: string
|
|
49
|
-
default: bottom
|
|
49
|
+
default: bottom
|
|
50
50
|
reflect: true
|
|
51
51
|
enum:
|
|
52
52
|
- top
|
|
@@ -63,6 +63,10 @@ export class UICombobox extends UIFormElement {
|
|
|
63
63
|
...UIFormElement.properties,
|
|
64
64
|
placeholder: { type: String, default: 'Select...', reflect: true },
|
|
65
65
|
label: { type: String, default: '', reflect: true },
|
|
66
|
+
// Universal [size] system — sm/md/lg → 24/30/36 px (with density).
|
|
67
|
+
// yaml documented this as reflect:true since v1; closing the
|
|
68
|
+
// static-properties gap so el.size = 'lg' actually updates.
|
|
69
|
+
size: { type: String, default: 'md', reflect: true },
|
|
66
70
|
open: { type: Boolean, default: false, reflect: true },
|
|
67
71
|
freeText: { type: Boolean, default: false, reflect: true, attribute: 'free-text' },
|
|
68
72
|
creatable: { type: Boolean, default: false, reflect: true },
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/ContextMenu.json",
|
|
4
|
+
"title": "ContextMenu",
|
|
5
|
+
"description": "Right-click activated menu — the OS-native context-menu pattern as a\nweb component. Distinct from `menu-ui` (which is button-triggered):\nsame item shape (`menu-item-ui` children), different trigger surface\n(`contextmenu` event), and pointer-anchored positioning instead of\nelement-anchored. Pattern: WAI-APG Menu.\n\nTwo binding modes:\n **A. Wrap.** Default-slot child becomes the target:\n `<context-menu-ui><my-table>...</my-table>...items</context-menu-ui>`.\n **B. Selector.** Point at one or more existing elements via [for]:\n `<context-menu-ui for=\"#my-table\">...items</context-menu-ui>`.\n\nOn `contextmenu` event on a target: `preventDefault()`, position the\nmenu at the pointer coords, show via Popover API. Touch long-press\n(configurable via [long-press-ms]) does the same. Shift+F10 / Menu\nkey opens at the focused target's center for keyboard users.\n",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"component": {
|
|
17
|
+
"const": "ContextMenu"
|
|
18
|
+
},
|
|
19
|
+
"for": {
|
|
20
|
+
"description": "CSS selector(s) for target element(s). Empty = use default-slot child.",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"default": ""
|
|
23
|
+
},
|
|
24
|
+
"long-press-ms": {
|
|
25
|
+
"description": "Long-press duration (ms) on touch devices to open the menu.",
|
|
26
|
+
"type": "number",
|
|
27
|
+
"default": 500
|
|
28
|
+
},
|
|
29
|
+
"open": {
|
|
30
|
+
"description": "Programmatic open state. Set true to open at target center.",
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"default": false
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": [
|
|
36
|
+
"component"
|
|
37
|
+
],
|
|
38
|
+
"unevaluatedProperties": false,
|
|
39
|
+
"x-adiaui": {
|
|
40
|
+
"anti_patterns": [
|
|
41
|
+
{
|
|
42
|
+
"fix": "Wrap a target: `<context-menu-ui><my-target></my-target>...items</context-menu-ui>` OR point at one: `<context-menu-ui for=\"#my-target\">...items</context-menu-ui>`.",
|
|
43
|
+
"why": "No target binding — the menu never opens.",
|
|
44
|
+
"wrong": "<context-menu-ui>...just items...</context-menu-ui>"
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"category": "container",
|
|
48
|
+
"composes": [
|
|
49
|
+
"menu-item-ui"
|
|
50
|
+
],
|
|
51
|
+
"events": {
|
|
52
|
+
"context-menu-close": {
|
|
53
|
+
"description": "Fired when the menu closes (item-select / outside-click / Escape).",
|
|
54
|
+
"detail": {
|
|
55
|
+
"reason": {
|
|
56
|
+
"description": "\"select\" | \"outside\" | \"escape\"",
|
|
57
|
+
"type": "string"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"context-menu-open": {
|
|
62
|
+
"description": "Fired when the menu opens (right-click / long-press / keyboard).",
|
|
63
|
+
"detail": {
|
|
64
|
+
"target": {
|
|
65
|
+
"description": "The target element the menu was opened on.",
|
|
66
|
+
"type": "Element"
|
|
67
|
+
},
|
|
68
|
+
"x": {
|
|
69
|
+
"description": "Pointer x coord (viewport-relative); null for keyboard activation.",
|
|
70
|
+
"type": "number"
|
|
71
|
+
},
|
|
72
|
+
"y": {
|
|
73
|
+
"description": "Pointer y coord; null for keyboard activation.",
|
|
74
|
+
"type": "number"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"context-menu-select": {
|
|
79
|
+
"description": "Fired when an item is activated. Same shape as menu-ui's `action` event.",
|
|
80
|
+
"detail": {
|
|
81
|
+
"text": {
|
|
82
|
+
"description": "Selected item's text.",
|
|
83
|
+
"type": "string"
|
|
84
|
+
},
|
|
85
|
+
"value": {
|
|
86
|
+
"description": "Selected item's value.",
|
|
87
|
+
"type": "string"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"examples": [
|
|
93
|
+
{
|
|
94
|
+
"description": "Right-click a file row for Open / Rename / Delete.",
|
|
95
|
+
"a2ui": "[\n { \"id\": \"root\", \"component\": \"ContextMenu\", \"children\": [\"target\", \"item-open\", \"item-rename\", \"div\", \"item-delete\"] },\n { \"id\": \"target\", \"component\": \"Text\", \"textContent\": \"Right-click me\" },\n { \"id\": \"item-open\", \"component\": \"MenuItem\", \"value\": \"open\", \"text\": \"Open\" },\n { \"id\": \"item-rename\", \"component\": \"MenuItem\", \"value\": \"rename\", \"text\": \"Rename\" },\n { \"id\": \"div\", \"component\": \"MenuDivider\" },\n { \"id\": \"item-delete\", \"component\": \"MenuItem\", \"value\": \"delete\", \"text\": \"Delete\", \"variant\": \"danger\" }\n]\n",
|
|
96
|
+
"name": "file-actions"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"keywords": [
|
|
100
|
+
"context-menu",
|
|
101
|
+
"right-click",
|
|
102
|
+
"menu",
|
|
103
|
+
"popup-menu"
|
|
104
|
+
],
|
|
105
|
+
"name": "UIContextMenu",
|
|
106
|
+
"related": [
|
|
107
|
+
"menu",
|
|
108
|
+
"menu-item",
|
|
109
|
+
"popover"
|
|
110
|
+
],
|
|
111
|
+
"slots": {
|
|
112
|
+
"default": {
|
|
113
|
+
"description": "Two-purpose slot: the wrapped target element (mode A — first\nnon-menu-item-ui child) AND the menu-item-ui items. Items are\npromoted to the popover surface on open.\n"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"states": [
|
|
117
|
+
{
|
|
118
|
+
"description": "Default. Menu closed; trigger listeners attached.",
|
|
119
|
+
"name": "idle"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"description": "Menu visible at pointer position; focus inside.",
|
|
123
|
+
"attribute": "open",
|
|
124
|
+
"name": "open"
|
|
125
|
+
}
|
|
126
|
+
],
|
|
127
|
+
"status": "stable",
|
|
128
|
+
"synonyms": {
|
|
129
|
+
"popup-menu": [
|
|
130
|
+
"menu",
|
|
131
|
+
"context-menu"
|
|
132
|
+
],
|
|
133
|
+
"right-click": [
|
|
134
|
+
"context-menu"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
"tag": "context-menu-ui",
|
|
138
|
+
"tokens": {
|
|
139
|
+
"--context-menu-bg": {
|
|
140
|
+
"description": "Menu surface background color.",
|
|
141
|
+
"default": "var(--a-bg-subtle)"
|
|
142
|
+
},
|
|
143
|
+
"--context-menu-border": {
|
|
144
|
+
"description": "Menu surface border color.",
|
|
145
|
+
"default": "var(--a-border-subtle)"
|
|
146
|
+
},
|
|
147
|
+
"--context-menu-radius": {
|
|
148
|
+
"description": "Menu surface border radius.",
|
|
149
|
+
"default": "var(--a-radius-md)"
|
|
150
|
+
},
|
|
151
|
+
"--context-menu-shadow": {
|
|
152
|
+
"description": "Menu surface shadow.",
|
|
153
|
+
"default": "var(--a-shadow-lg)"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"traits": [],
|
|
157
|
+
"version": 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<context-menu-ui>` — right-click activated menu (OS context-menu pattern).
|
|
3
|
+
*
|
|
4
|
+
* Two binding shapes:
|
|
5
|
+
* A. Wrap a target — first non-menu-item-ui default-slot child becomes
|
|
6
|
+
* the contextmenu host.
|
|
7
|
+
* B. Point at targets via [for="<selector>"] — useful for whole-table
|
|
8
|
+
* / whole-canvas menus where wrapping isn't practical.
|
|
9
|
+
*
|
|
10
|
+
* Architecture: reuses the existing anchor pattern (core/anchor.js) by
|
|
11
|
+
* creating a virtual 1×1 anchor element at the pointer coords on each
|
|
12
|
+
* open. anchorPopover() does the rest — handles native CSS anchor-
|
|
13
|
+
* positioning + JS fallback + viewport clamping + scroll/resize updates.
|
|
14
|
+
* This keeps coord-anchored popovers using the same code path as every
|
|
15
|
+
* other popover surface in the system (menu-ui, popover-ui, etc.).
|
|
16
|
+
*
|
|
17
|
+
* @see ../../core/anchor.js
|
|
18
|
+
* @see ../menu/menu.class.js — peer button-triggered menu primitive.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { UIElement } from '../../core/element.js';
|
|
22
|
+
import { anchorPopover } from '../../core/anchor.js';
|
|
23
|
+
|
|
24
|
+
const LONG_PRESS_DEFAULT = 500;
|
|
25
|
+
|
|
26
|
+
export class UIContextMenu extends UIElement {
|
|
27
|
+
static properties = {
|
|
28
|
+
for: { type: String, default: '', reflect: true },
|
|
29
|
+
open: { type: Boolean, default: false, reflect: true },
|
|
30
|
+
longPressMs: { type: Number, default: LONG_PRESS_DEFAULT, reflect: false, attribute: 'long-press-ms' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
static template = () => null;
|
|
34
|
+
|
|
35
|
+
#surface = null;
|
|
36
|
+
#virtualAnchor = null;
|
|
37
|
+
#anchorCleanup = null;
|
|
38
|
+
#targets = [];
|
|
39
|
+
#touchTimer = null;
|
|
40
|
+
#lastTarget = null;
|
|
41
|
+
#outsideHandler = null;
|
|
42
|
+
#keyHandler = null;
|
|
43
|
+
|
|
44
|
+
connected() {
|
|
45
|
+
super.connected();
|
|
46
|
+
this.#bindTargets();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
disconnected() {
|
|
50
|
+
super.disconnected();
|
|
51
|
+
this.#unbindTargets();
|
|
52
|
+
this.#teardown();
|
|
53
|
+
this.#surface?.remove();
|
|
54
|
+
this.#surface = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* ── Public API ──────────────────────────────────────────────────── */
|
|
58
|
+
|
|
59
|
+
openAt(x, y, target = null) {
|
|
60
|
+
target ??= this.#targets[0] ?? null;
|
|
61
|
+
if (!target) return;
|
|
62
|
+
this.#lastTarget = target;
|
|
63
|
+
if (x == null || y == null) {
|
|
64
|
+
const r = target.getBoundingClientRect();
|
|
65
|
+
x = r.left + r.width / 2;
|
|
66
|
+
y = r.top + r.height / 2;
|
|
67
|
+
}
|
|
68
|
+
this.#show(x, y);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
close(reason = 'manual') {
|
|
72
|
+
this.#hide(reason);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ── Target binding ──────────────────────────────────────────────── */
|
|
76
|
+
|
|
77
|
+
#bindTargets() {
|
|
78
|
+
this.#targets = this.#resolveTargets();
|
|
79
|
+
for (const t of this.#targets) {
|
|
80
|
+
t.addEventListener('contextmenu', this.#onContextMenu);
|
|
81
|
+
t.addEventListener('touchstart', this.#onTouchStart, { passive: true });
|
|
82
|
+
t.addEventListener('touchend', this.#onTouchEnd);
|
|
83
|
+
t.addEventListener('touchcancel', this.#onTouchEnd);
|
|
84
|
+
t.addEventListener('keydown', this.#onTargetKeydown);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
#unbindTargets() {
|
|
89
|
+
for (const t of this.#targets) {
|
|
90
|
+
t.removeEventListener('contextmenu', this.#onContextMenu);
|
|
91
|
+
t.removeEventListener('touchstart', this.#onTouchStart);
|
|
92
|
+
t.removeEventListener('touchend', this.#onTouchEnd);
|
|
93
|
+
t.removeEventListener('touchcancel', this.#onTouchEnd);
|
|
94
|
+
t.removeEventListener('keydown', this.#onTargetKeydown);
|
|
95
|
+
}
|
|
96
|
+
this.#targets = [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#resolveTargets() {
|
|
100
|
+
if (this.for) {
|
|
101
|
+
try { return [...document.querySelectorAll(this.for)]; }
|
|
102
|
+
catch (e) {
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.warn(`[context-menu-ui] invalid [for] selector: ${this.for}`, e);
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const child of this.children) {
|
|
109
|
+
const tag = child.tagName.toLowerCase();
|
|
110
|
+
if (tag !== 'menu-item-ui' && tag !== 'menu-divider-ui') return [child];
|
|
111
|
+
}
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* ── Trigger event handlers ──────────────────────────────────────── */
|
|
116
|
+
|
|
117
|
+
#onContextMenu = (e) => {
|
|
118
|
+
if (e.defaultPrevented) return;
|
|
119
|
+
if (!this.#hasItems()) return;
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
this.#lastTarget = e.currentTarget;
|
|
122
|
+
this.#show(e.clientX, e.clientY);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
#onTouchStart = (e) => {
|
|
126
|
+
if (!this.#hasItems()) return;
|
|
127
|
+
const touch = e.touches[0];
|
|
128
|
+
if (!touch) return;
|
|
129
|
+
this.#touchTimer = setTimeout(() => {
|
|
130
|
+
this.#lastTarget = e.currentTarget;
|
|
131
|
+
this.#show(touch.clientX, touch.clientY);
|
|
132
|
+
}, this.longPressMs ?? LONG_PRESS_DEFAULT);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
#onTouchEnd = () => {
|
|
136
|
+
if (this.#touchTimer) clearTimeout(this.#touchTimer);
|
|
137
|
+
this.#touchTimer = null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
#onTargetKeydown = (e) => {
|
|
141
|
+
if ((e.shiftKey && e.key === 'F10') || e.key === 'ContextMenu') {
|
|
142
|
+
if (!this.#hasItems()) return;
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
this.#lastTarget = e.currentTarget;
|
|
145
|
+
this.openAt(null, null, e.currentTarget);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/* ── Show / hide ─────────────────────────────────────────────────── */
|
|
150
|
+
|
|
151
|
+
#hasItems() {
|
|
152
|
+
return [...this.children].some((c) => c.tagName.toLowerCase() === 'menu-item-ui');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#ensureSurface() {
|
|
156
|
+
if (this.#surface) return this.#surface;
|
|
157
|
+
const surface = document.createElement('div');
|
|
158
|
+
surface.setAttribute('data-context-menu-surface', '');
|
|
159
|
+
surface.setAttribute('popover', 'manual');
|
|
160
|
+
surface.setAttribute('role', 'menu');
|
|
161
|
+
surface.setAttribute('aria-label', this.getAttribute('aria-label') || 'Context menu');
|
|
162
|
+
surface.tabIndex = -1;
|
|
163
|
+
// Inline outline reset — the surface is appended to document.body
|
|
164
|
+
// (outside the @scope(context-menu-ui) boundary), so a scoped CSS
|
|
165
|
+
// rule wouldn't match. UA paints a 3-px default focus ring on the
|
|
166
|
+
// tabindex=-1 surface when it receives programmatic focus on open.
|
|
167
|
+
surface.style.outline = 'none';
|
|
168
|
+
surface.addEventListener('click', this.#onSurfaceClick);
|
|
169
|
+
document.body.appendChild(surface);
|
|
170
|
+
this.#surface = surface;
|
|
171
|
+
return surface;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#ensureVirtualAnchor(x, y) {
|
|
175
|
+
if (!this.#virtualAnchor) {
|
|
176
|
+
const anchor = document.createElement('div');
|
|
177
|
+
anchor.setAttribute('data-context-menu-anchor', '');
|
|
178
|
+
anchor.style.cssText = 'position:fixed;width:1px;height:1px;pointer-events:none;opacity:0;';
|
|
179
|
+
document.body.appendChild(anchor);
|
|
180
|
+
this.#virtualAnchor = anchor;
|
|
181
|
+
}
|
|
182
|
+
this.#virtualAnchor.style.left = `${x}px`;
|
|
183
|
+
this.#virtualAnchor.style.top = `${y}px`;
|
|
184
|
+
return this.#virtualAnchor;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#show(x, y) {
|
|
188
|
+
const surface = this.#ensureSurface();
|
|
189
|
+
// Hoist items into the surface.
|
|
190
|
+
surface.replaceChildren();
|
|
191
|
+
for (const child of [...this.children]) {
|
|
192
|
+
const tag = child.tagName.toLowerCase();
|
|
193
|
+
if (tag === 'menu-item-ui' || tag === 'menu-divider-ui') {
|
|
194
|
+
surface.appendChild(child.cloneNode(true));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Promote to top layer FIRST so anchorPopover can measure its size.
|
|
198
|
+
try { surface.showPopover(); } catch { /* popover API unavailable */ }
|
|
199
|
+
|
|
200
|
+
// Reuse the canonical anchor pattern — virtual 1×1 element at pointer
|
|
201
|
+
// coords, then anchorPopover() handles native CSS anchor-positioning
|
|
202
|
+
// (with position-try-fallbacks for viewport flips) + JS fallback.
|
|
203
|
+
const anchor = this.#ensureVirtualAnchor(x, y);
|
|
204
|
+
this.#anchorCleanup?.();
|
|
205
|
+
this.#anchorCleanup = anchorPopover(anchor, surface, { placement: 'bottom-start', gap: 0 });
|
|
206
|
+
|
|
207
|
+
this.open = true;
|
|
208
|
+
this.#setupOpenHandlers();
|
|
209
|
+
const first = surface.querySelector('menu-item-ui:not([disabled])');
|
|
210
|
+
queueMicrotask(() => first?.focus?.());
|
|
211
|
+
|
|
212
|
+
this.dispatchEvent(new CustomEvent('context-menu-open', {
|
|
213
|
+
bubbles: true,
|
|
214
|
+
detail: { target: this.#lastTarget, x, y },
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#hide(reason = 'manual') {
|
|
219
|
+
if (!this.open) return;
|
|
220
|
+
this.open = false;
|
|
221
|
+
this.#teardown();
|
|
222
|
+
this.dispatchEvent(new CustomEvent('context-menu-close', {
|
|
223
|
+
bubbles: true,
|
|
224
|
+
detail: { reason },
|
|
225
|
+
}));
|
|
226
|
+
try { this.#lastTarget?.focus?.(); } catch { /* noop */ }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#teardown() {
|
|
230
|
+
this.#teardownOpenHandlers();
|
|
231
|
+
this.#anchorCleanup?.();
|
|
232
|
+
this.#anchorCleanup = null;
|
|
233
|
+
try { this.#surface?.hidePopover(); } catch { /* noop */ }
|
|
234
|
+
if (this.#virtualAnchor) {
|
|
235
|
+
this.#virtualAnchor.remove();
|
|
236
|
+
this.#virtualAnchor = null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* ── While-open document handlers ────────────────────────────────── */
|
|
241
|
+
|
|
242
|
+
#setupOpenHandlers() {
|
|
243
|
+
this.#outsideHandler = (e) => {
|
|
244
|
+
if (!this.#surface?.contains(e.target)) this.#hide('outside');
|
|
245
|
+
};
|
|
246
|
+
this.#keyHandler = (e) => {
|
|
247
|
+
if (e.key === 'Escape') { e.stopPropagation(); this.#hide('escape'); }
|
|
248
|
+
};
|
|
249
|
+
requestAnimationFrame(() => {
|
|
250
|
+
document.addEventListener('pointerdown', this.#outsideHandler);
|
|
251
|
+
document.addEventListener('keydown', this.#keyHandler);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#teardownOpenHandlers() {
|
|
256
|
+
if (this.#outsideHandler) document.removeEventListener('pointerdown', this.#outsideHandler);
|
|
257
|
+
if (this.#keyHandler) document.removeEventListener('keydown', this.#keyHandler);
|
|
258
|
+
this.#outsideHandler = null;
|
|
259
|
+
this.#keyHandler = null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* ── Item activation ─────────────────────────────────────────────── */
|
|
263
|
+
|
|
264
|
+
#onSurfaceClick = (e) => {
|
|
265
|
+
const item = e.target.closest('menu-item-ui');
|
|
266
|
+
if (!item || item.hasAttribute('disabled')) return;
|
|
267
|
+
const value = item.getAttribute('value') || '';
|
|
268
|
+
const text = item.getAttribute('text') || item.textContent.trim();
|
|
269
|
+
this.dispatchEvent(new CustomEvent('context-menu-select', {
|
|
270
|
+
bubbles: true,
|
|
271
|
+
detail: { value, text },
|
|
272
|
+
}));
|
|
273
|
+
this.#hide('select');
|
|
274
|
+
};
|
|
275
|
+
}
|