@adia-ai/web-components 0.6.36 → 0.6.37
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 +28 -1
- 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/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.class.js +2 -0
- package/components/feed/feed.class.js +13 -5
- package/components/feed/feed.css +14 -0
- package/components/index.js +9 -0
- package/components/integration-card/integration-card.class.js +9 -0
- package/components/integration-card/integration-card.test.js +4 -3
- package/components/nav-group/nav-group.css +7 -1
- 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 +5 -1
- package/components/select/select.class.js +4 -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 +16 -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 +28 -6
- 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/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/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 +9 -0
- package/styles/resets.css +10 -0
|
@@ -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
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* Host styles (scoped — host IS a context-menu-ui descendant of itself). */
|
|
2
|
+
@scope (context-menu-ui) {
|
|
3
|
+
:scope {
|
|
4
|
+
/* Element itself does not render — it's just a behavioral wrapper.
|
|
5
|
+
Targets (wrapped child / [for] match) render in flow normally. */
|
|
6
|
+
display: contents;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* Authored items live in light DOM as the source of truth, but render
|
|
10
|
+
ONLY when the surface opens (the surface clones them into the popover).
|
|
11
|
+
Hide them in flow so they don't appear next to the target. */
|
|
12
|
+
:scope > menu-item-ui,
|
|
13
|
+
:scope > menu-divider-ui {
|
|
14
|
+
display: none;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Surface styles are NOT scoped to context-menu-ui — the surface is
|
|
19
|
+
appended to document.body (escapes the host's `display: contents` so
|
|
20
|
+
fixed-position + top-layer work), so any @scope rule pinned to
|
|
21
|
+
context-menu-ui as the scope root would miss.
|
|
22
|
+
|
|
23
|
+
Chrome MUST visually match menu-ui's surface — same vocabulary, same
|
|
24
|
+
tokens, same animation. menu-ui is the canonical menu surface; this
|
|
25
|
+
is just the right-click trigger surface for the same menu shape. */
|
|
26
|
+
[data-context-menu-surface] {
|
|
27
|
+
margin: 0;
|
|
28
|
+
padding: var(--a-space-1);
|
|
29
|
+
border: 1px solid var(--a-border-subtle);
|
|
30
|
+
border-radius: var(--a-radius-lg);
|
|
31
|
+
background: var(--a-bg-subtle);
|
|
32
|
+
box-shadow: var(--a-shadow-lg);
|
|
33
|
+
min-width: 10rem;
|
|
34
|
+
font-family: inherit;
|
|
35
|
+
font-size: var(--a-ui-size);
|
|
36
|
+
color: var(--a-fg);
|
|
37
|
+
opacity: 1;
|
|
38
|
+
translate: 0 0;
|
|
39
|
+
transition: opacity var(--a-duration-fast) var(--a-easing-out),
|
|
40
|
+
translate var(--a-duration-fast) var(--a-easing-out);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
[data-context-menu-surface]:popover-open {
|
|
44
|
+
@starting-style {
|
|
45
|
+
opacity: 0;
|
|
46
|
+
translate: 0 -4px;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[data-context-menu-surface]:not(:popover-open) {
|
|
51
|
+
display: none !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@media (prefers-reduced-motion: reduce) {
|
|
55
|
+
[data-context-menu-surface] { transition: none; }
|
|
56
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<context-menu-ui>` — Right-click activated menu — the OS-native context-menu pattern as a
|
|
3
|
+
web component. Distinct from `menu-ui` (which is button-triggered):
|
|
4
|
+
same item shape (`menu-item-ui` children), different trigger surface
|
|
5
|
+
(`contextmenu` event), and pointer-anchored positioning instead of
|
|
6
|
+
element-anchored. Pattern: WAI-APG Menu.
|
|
7
|
+
|
|
8
|
+
Two binding modes:
|
|
9
|
+
**A. Wrap.** Default-slot child becomes the target:
|
|
10
|
+
`<context-menu-ui><my-table>...</my-table>...items</context-menu-ui>`.
|
|
11
|
+
**B. Selector.** Point at one or more existing elements via [for]:
|
|
12
|
+
`<context-menu-ui for="#my-table">...items</context-menu-ui>`.
|
|
13
|
+
|
|
14
|
+
On `contextmenu` event on a target: `preventDefault()`, position the
|
|
15
|
+
menu at the pointer coords, show via Popover API. Touch long-press
|
|
16
|
+
(configurable via [long-press-ms]) does the same. Shift+F10 / Menu
|
|
17
|
+
key opens at the focused target's center for keyboard users.
|
|
18
|
+
|
|
19
|
+
*
|
|
20
|
+
* @see https://ui-kit.exe.xyz/site/components/context-menu
|
|
21
|
+
*
|
|
22
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
23
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
24
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
25
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
26
|
+
* needed beyond what the yaml `events:` block can express.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { UIElement } from '../../core/element.js';
|
|
30
|
+
|
|
31
|
+
export interface ContextMenuCloseEventDetail {
|
|
32
|
+
/** "select" | "outside" | "escape" */
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type ContextMenuCloseEvent = CustomEvent<ContextMenuCloseEventDetail>;
|
|
37
|
+
export interface ContextMenuOpenEventDetail {
|
|
38
|
+
/** The target element the menu was opened on. */
|
|
39
|
+
target: string;
|
|
40
|
+
/** Pointer x coord (viewport-relative); null for keyboard activation. */
|
|
41
|
+
x: number;
|
|
42
|
+
/** Pointer y coord; null for keyboard activation. */
|
|
43
|
+
y: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ContextMenuOpenEvent = CustomEvent<ContextMenuOpenEventDetail>;
|
|
47
|
+
export interface ContextMenuSelectEventDetail {
|
|
48
|
+
/** Selected item's text. */
|
|
49
|
+
text: string;
|
|
50
|
+
/** Selected item's value. */
|
|
51
|
+
value: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ContextMenuSelectEvent = CustomEvent<ContextMenuSelectEventDetail>;
|
|
55
|
+
|
|
56
|
+
export class UIContextMenu extends UIElement {
|
|
57
|
+
/** CSS selector(s) for target element(s). Empty = use default-slot child. */
|
|
58
|
+
for: string;
|
|
59
|
+
/** Programmatic open state. Set true to open at target center. */
|
|
60
|
+
open: boolean;
|
|
61
|
+
|
|
62
|
+
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
63
|
+
type: K,
|
|
64
|
+
listener: (this: UIContextMenu, ev: HTMLElementEventMap[K]) => unknown,
|
|
65
|
+
options?: boolean | AddEventListenerOptions,
|
|
66
|
+
): void;
|
|
67
|
+
addEventListener(type: 'context-menu-close', listener: (ev: ContextMenuCloseEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
68
|
+
addEventListener(type: 'context-menu-open', listener: (ev: ContextMenuOpenEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
69
|
+
addEventListener(type: 'context-menu-select', listener: (ev: ContextMenuSelectEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
70
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<context-menu-ui>` — auto-registers the tag on import.
|
|
3
|
+
*
|
|
4
|
+
* For non-side-effect class import (test isolation, tag override), use
|
|
5
|
+
* the `class` subpath:
|
|
6
|
+
*
|
|
7
|
+
* import { UIContextMenu } from '@adia-ai/web-components/components/context-menu/class';
|
|
8
|
+
*
|
|
9
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { defineIfFree } from '../../core/register.js';
|
|
13
|
+
import { UIContextMenu } from './context-menu.class.js';
|
|
14
|
+
|
|
15
|
+
defineIfFree('context-menu-ui', UIContextMenu);
|
|
16
|
+
|
|
17
|
+
export { UIContextMenu };
|