@raylang/elements 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/base.css +1 -1
- package/components/accordion/accordion.js +7 -4
- package/components/alert/alert.js +2 -1
- package/components/attachment/attachment.js +4 -5
- package/components/blockquote/blockquote.js +2 -2
- package/components/bottom-nav/bottom-nav.js +3 -3
- package/components/carousel/carousel.js +22 -10
- package/components/checkbox/checkbox.js +5 -2
- package/components/checkbox-group/checkbox-group.js +1 -1
- package/components/code/code.js +16 -11
- package/components/color-picker/color-picker.js +5 -5
- package/components/combobox/combobox.js +2 -1
- package/components/command-palette/command-palette.js +17 -2
- package/components/date-picker/date-picker.js +5 -3
- package/components/dialog/dialog.js +1 -1
- package/components/doc-tabs/doc-tabs.js +255 -0
- package/components/drawer/drawer.js +27 -9
- package/components/icon/icon.js +4 -1
- package/components/icon-button/icon-button.js +4 -0
- package/components/indicator/indicator.js +2 -1
- package/components/input/input.js +13 -1
- package/components/menu/menu.js +43 -8
- package/components/mode-input/mode-input.js +221 -0
- package/components/nav-link/nav-link.js +5 -2
- package/components/notification/notification.js +15 -12
- package/components/progress/progress.js +2 -2
- package/components/radio/radio.js +4 -2
- package/components/scroll-area/scroll-area.js +14 -11
- package/components/search/search.js +3 -3
- package/components/segmented-control/segmented-control.js +5 -3
- package/components/select/select.js +3 -3
- package/components/separator/separator.js +8 -3
- package/components/sidebar/sidebar.js +154 -11
- package/components/slider/slider.js +5 -2
- package/components/split/split.js +25 -7
- package/components/stepper/stepper.js +4 -4
- package/components/switch/switch.js +3 -3
- package/components/table/table.js +5 -5
- package/components/tabs/tabs.js +55 -14
- package/components/textarea/textarea.js +13 -1
- package/components/timeline/timeline.js +13 -8
- package/components/toggle-group/toggle-group.js +6 -4
- package/core/complete.js +252 -0
- package/core/core.js +6 -1
- package/core/direction.js +19 -0
- package/core/icons.js +12 -0
- package/core/position.js +11 -3
- package/core/scroll.js +85 -1
- package/core/styles.js +1 -1
- package/core/textlayer.js +214 -0
- package/core.js +1 -0
- package/custom-elements.json +1487 -143
- package/index.js +2 -0
- package/llms-full.txt +224 -28
- package/llms.txt +26 -4
- package/package.json +9 -1
- package/ray-elements.d.ts +190 -31
- package/vscode.html-custom-data.json +477 -22
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// <ray-doc-tabs> + <ray-doc-tab>: the tabs of open documents (an editor's,
|
|
2
|
+
// a browser's), each closable and with an unsaved mark, all over one shared
|
|
3
|
+
// panel (WAI-ARIA tabs: `panel` is that element's id).
|
|
4
|
+
//
|
|
5
|
+
// Closing is the app's: the × (for the pointer only: a tab holds no
|
|
6
|
+
// focusable control), a middle click, or Delete/Backspace on the focused tab
|
|
7
|
+
// fire `ray-request-close` ({ value, tab, source }); the app asks if it must
|
|
8
|
+
// and removes the tab. When the selected tab goes, the one that takes its
|
|
9
|
+
// place is selected (`change`), and focused if the closed tab had the focus.
|
|
10
|
+
//
|
|
11
|
+
// Too many tabs for the width: the bar scrolls sideways like ray-tabs.
|
|
12
|
+
|
|
13
|
+
import { RayElement, css, define, uid } from "../../core/core.js";
|
|
14
|
+
import { inlineKey } from "../../core/direction.js";
|
|
15
|
+
import { SideScroller, sideScrollerStyles } from "../../core/scroll.js";
|
|
16
|
+
|
|
17
|
+
const barStyles = css`
|
|
18
|
+
:host { display: flex; align-items: center; gap: var(--ray-space-1); min-width: 0; }
|
|
19
|
+
.list { display: flex; flex: 0 1 auto; align-items: center; gap: 2px; min-width: 0; overflow-x: auto; scrollbar-width: none; }
|
|
20
|
+
.list::-webkit-scrollbar { display: none; }
|
|
21
|
+
.list ::slotted(ray-doc-tab) { flex: none; }
|
|
22
|
+
.end { display: inline-flex; flex: none; align-items: center; }
|
|
23
|
+
.end:not([data-filled]) { display: none; }
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export class RayDocTabs extends RayElement {
|
|
27
|
+
static properties = {
|
|
28
|
+
value: { type: String },
|
|
29
|
+
panel: { type: String },
|
|
30
|
+
label: { type: String },
|
|
31
|
+
closeLabel: { type: String, default: "Close" },
|
|
32
|
+
dirtyLabel: { type: String, default: "Unsaved changes" },
|
|
33
|
+
};
|
|
34
|
+
static styles = [sideScrollerStyles, barStyles];
|
|
35
|
+
static template = `<div class="list" part="list" role="tablist"><slot></slot></div><span class="end" part="end"><slot name="end"></slot></span>`;
|
|
36
|
+
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
this._order = [];
|
|
40
|
+
this._focused = null;
|
|
41
|
+
this.addEventListener("click", (e) => {
|
|
42
|
+
const tab = this._tabOf(e);
|
|
43
|
+
if (!tab) return;
|
|
44
|
+
if (e.composedPath().some((n) => n.part?.contains?.("close"))) this._requestClose(tab, "button");
|
|
45
|
+
else this._select(tab, true);
|
|
46
|
+
});
|
|
47
|
+
// The middle button closes, without the browser's autoscroll.
|
|
48
|
+
this.addEventListener("mousedown", (e) => {
|
|
49
|
+
if (e.button === 1 && this._tabOf(e)) e.preventDefault();
|
|
50
|
+
});
|
|
51
|
+
this.addEventListener("auxclick", (e) => {
|
|
52
|
+
const tab = this._tabOf(e);
|
|
53
|
+
if (tab && e.button === 1) this._requestClose(tab, "middle");
|
|
54
|
+
});
|
|
55
|
+
this.addEventListener("keydown", (e) => this._key(e));
|
|
56
|
+
// Which tab has the focus, kept while it is removed (no focusout then).
|
|
57
|
+
this.addEventListener("focusin", (e) => {
|
|
58
|
+
this._focused = e.target.closest?.("ray-doc-tab") ?? null;
|
|
59
|
+
});
|
|
60
|
+
this.addEventListener("focusout", (e) => {
|
|
61
|
+
if (e.relatedTarget) this._focused = null;
|
|
62
|
+
});
|
|
63
|
+
const end = this.$('slot[name="end"]');
|
|
64
|
+
end.addEventListener("slotchange", () => end.parentElement.toggleAttribute("data-filled", end.assignedNodes().length > 0));
|
|
65
|
+
this.$(".list slot").addEventListener("slotchange", () => this.requestUpdate());
|
|
66
|
+
this._scroller = new SideScroller(this, this.$(".list"), { onResize: () => this.requestUpdate() });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
connectedCallback() {
|
|
70
|
+
super.connectedCallback();
|
|
71
|
+
this._scroller.connect();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
disconnectedCallback() {
|
|
75
|
+
super.disconnectedCallback();
|
|
76
|
+
this._scroller.disconnect();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get tabs() {
|
|
80
|
+
return [...this.querySelectorAll("ray-doc-tab")].filter((t) => t.closest("ray-doc-tabs") === this);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The shared panel (`panel` is its id), or null. */
|
|
84
|
+
get panelElement() {
|
|
85
|
+
return this.panel ? this.getRootNode().getElementById?.(this.panel) ?? null : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
update() {
|
|
89
|
+
const tabs = this.tabs;
|
|
90
|
+
if (!tabs.some((t) => t.value === this.value)) this._replaceSelected(tabs);
|
|
91
|
+
const list = this.$(".list");
|
|
92
|
+
if (this.label) list.setAttribute("aria-label", this.label);
|
|
93
|
+
else list.removeAttribute("aria-label");
|
|
94
|
+
const panel = this.panelElement;
|
|
95
|
+
for (const tab of tabs) {
|
|
96
|
+
const active = tab.value === this.value;
|
|
97
|
+
tab.id ||= uid("doc-tab");
|
|
98
|
+
tab.setAttribute("role", "tab");
|
|
99
|
+
tab.setAttribute("aria-selected", String(active));
|
|
100
|
+
tab.tabIndex = active ? 0 : -1;
|
|
101
|
+
tab.toggleAttribute("data-active", active);
|
|
102
|
+
if (panel) tab.setAttribute("aria-controls", panel.id);
|
|
103
|
+
else tab.removeAttribute("aria-controls");
|
|
104
|
+
tab.requestUpdate();
|
|
105
|
+
}
|
|
106
|
+
if (panel) {
|
|
107
|
+
panel.setAttribute("role", "tabpanel");
|
|
108
|
+
const active = tabs.find((t) => t.value === this.value);
|
|
109
|
+
if (active) panel.setAttribute("aria-labelledby", active.id);
|
|
110
|
+
else panel.removeAttribute("aria-labelledby");
|
|
111
|
+
}
|
|
112
|
+
this._order = tabs;
|
|
113
|
+
if (this.value !== this._revealed && this._scroller.reveal(tabs.find((t) => t.value === this.value))) {
|
|
114
|
+
this._revealed = this.value;
|
|
115
|
+
}
|
|
116
|
+
this._scroller.update();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The selected tab is gone (or none was chosen yet): the one at its place
|
|
120
|
+
// takes over, else the one before it; the first when nothing was selected.
|
|
121
|
+
_replaceSelected(tabs) {
|
|
122
|
+
const before = this._order.findIndex((t) => t.value === this.value);
|
|
123
|
+
const next = before < 0 ? tabs[0] : tabs[Math.min(before, tabs.length - 1)];
|
|
124
|
+
const closed = before >= 0 ? this._order[before] : null;
|
|
125
|
+
if (!next) {
|
|
126
|
+
if (this.value !== undefined && before >= 0) {
|
|
127
|
+
this.commitProperty("value", undefined);
|
|
128
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (this.value === undefined || before < 0) {
|
|
133
|
+
this.commitProperty("value", next.value);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.commitProperty("value", next.value);
|
|
137
|
+
this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
138
|
+
if (closed && closed === this._focused) {
|
|
139
|
+
this._focused = next;
|
|
140
|
+
queueMicrotask(() => next.focus());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_tabOf(e) {
|
|
145
|
+
const tab = e.target.closest?.("ray-doc-tab");
|
|
146
|
+
return tab && tab.closest("ray-doc-tabs") === this ? tab : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_select(tab, user) {
|
|
150
|
+
const changed = tab.value !== this.value;
|
|
151
|
+
this.value = tab.value;
|
|
152
|
+
if (user && changed) this.dispatchEvent(new Event("change", { bubbles: true }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_requestClose(tab, source) {
|
|
156
|
+
if (tab.hideClose) return;
|
|
157
|
+
this.emit("ray-request-close", { value: tab.value, tab, source });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_key(e) {
|
|
161
|
+
const tab = this._tabOf(e);
|
|
162
|
+
if (!tab || e.target !== tab) return;
|
|
163
|
+
const tabs = this.tabs;
|
|
164
|
+
const i = tabs.indexOf(tab);
|
|
165
|
+
// Right to left, ArrowLeft is the next tab.
|
|
166
|
+
const key = inlineKey(e.key, this);
|
|
167
|
+
let target = null;
|
|
168
|
+
if (key === "ArrowRight") target = tabs[(i + 1) % tabs.length];
|
|
169
|
+
else if (key === "ArrowLeft") target = tabs[(i - 1 + tabs.length) % tabs.length];
|
|
170
|
+
else if (e.key === "Home") target = tabs[0];
|
|
171
|
+
else if (e.key === "End") target = tabs[tabs.length - 1];
|
|
172
|
+
else if (e.key === "Delete" || e.key === "Backspace") {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
this._requestClose(tab, "key");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!target) return;
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
target.focus();
|
|
180
|
+
this._select(target, true);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const tabStyles = css`
|
|
185
|
+
:host {
|
|
186
|
+
display: inline-flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: var(--ray-space-1);
|
|
189
|
+
box-sizing: border-box;
|
|
190
|
+
max-width: 14rem;
|
|
191
|
+
height: var(--ray-control-height-xs);
|
|
192
|
+
padding-inline: var(--ray-space-2) var(--ray-space-1);
|
|
193
|
+
font-size: var(--ray-font-size-sm);
|
|
194
|
+
color: var(--ray-muted-foreground);
|
|
195
|
+
white-space: nowrap;
|
|
196
|
+
border: 1px solid transparent;
|
|
197
|
+
border-radius: var(--ray-radius-sm);
|
|
198
|
+
cursor: default;
|
|
199
|
+
user-select: none;
|
|
200
|
+
outline: none;
|
|
201
|
+
transition: color var(--ray-duration-fast) var(--ray-easing-standard), background-color var(--ray-duration-fast) var(--ray-easing-standard);
|
|
202
|
+
}
|
|
203
|
+
:host(:hover) { color: var(--ray-foreground); background: var(--ray-muted); }
|
|
204
|
+
:host([data-active]) { color: var(--ray-foreground); background: var(--ray-card); border-color: var(--ray-border); }
|
|
205
|
+
:host(:focus-visible) { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: -2px; }
|
|
206
|
+
.section { display: inline-flex; flex: none; align-items: center; }
|
|
207
|
+
.section:not([data-filled]) { display: none; }
|
|
208
|
+
::slotted(svg) { width: 1rem; height: 1rem; }
|
|
209
|
+
.label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
210
|
+
|
|
211
|
+
/* The end of the tab: the × (on hover and on the selected tab), or the
|
|
212
|
+
unsaved dot, which turns into the × under the pointer. */
|
|
213
|
+
.mark { position: relative; display: inline-flex; flex: none; align-items: center; justify-content: center; width: 1.125rem; height: 1.125rem; }
|
|
214
|
+
.close { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: var(--ray-muted-foreground); border-radius: var(--ray-radius-xs); visibility: hidden; }
|
|
215
|
+
.close svg { width: 0.75rem; height: 0.75rem; }
|
|
216
|
+
.close:hover { color: var(--ray-foreground); background: var(--ray-accent); }
|
|
217
|
+
:host(:hover) .close, :host([data-active]) .close { visibility: visible; }
|
|
218
|
+
.dot { position: absolute; display: none; width: 0.4375rem; height: 0.4375rem; background: var(--ray-primary); border-radius: 50%; }
|
|
219
|
+
:host([dirty]) .dot { display: block; }
|
|
220
|
+
:host([dirty]) .close { visibility: hidden; }
|
|
221
|
+
:host([dirty]) .mark:hover .close { visibility: visible; }
|
|
222
|
+
:host([dirty]) .mark:hover .dot { display: none; }
|
|
223
|
+
:host([hide-close]) .close { display: none; }
|
|
224
|
+
:host([hide-close]:not([dirty])) .mark { display: none; }
|
|
225
|
+
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
export class RayDocTab extends RayElement {
|
|
229
|
+
static properties = {
|
|
230
|
+
value: { type: String, reflect: true },
|
|
231
|
+
dirty: { type: Boolean, reflect: true },
|
|
232
|
+
hideClose: { type: Boolean, reflect: true },
|
|
233
|
+
};
|
|
234
|
+
static styles = [tabStyles];
|
|
235
|
+
static template = `<span class="section" part="left-section"><slot name="left"></slot></span><span class="label" part="label"><slot></slot></span><span class="mark" aria-hidden="true"><span class="dot" part="dot"></span><span class="close" part="close" data-ray-icon="ui.close"></span></span><span class="sr"></span>`;
|
|
236
|
+
|
|
237
|
+
constructor() {
|
|
238
|
+
super();
|
|
239
|
+
const slot = this.$('slot[name="left"]');
|
|
240
|
+
slot.addEventListener("slotchange", () => slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
update(changed) {
|
|
244
|
+
const bar = this.closest("ray-doc-tabs");
|
|
245
|
+
this.$(".close").title = bar?.closeLabel ?? "Close";
|
|
246
|
+
// The unsaved mark is part of the tab's name for assistive technology.
|
|
247
|
+
this.$(".sr").textContent = this.dirty ? `, ${bar?.dirtyLabel ?? "Unsaved changes"}` : "";
|
|
248
|
+
if (this.hideClose) this.removeAttribute("aria-keyshortcuts");
|
|
249
|
+
else this.setAttribute("aria-keyshortcuts", "Delete");
|
|
250
|
+
if (changed.has("value")) bar?.requestUpdate();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
define("ray-doc-tabs", RayDocTabs);
|
|
255
|
+
define("ray-doc-tab", RayDocTab);
|
|
@@ -32,24 +32,41 @@ const styles = css`
|
|
|
32
32
|
dialog[open] { display: flex; flex-direction: column; }
|
|
33
33
|
dialog::backdrop { animation: ray-fade-in var(--ray-duration-base) var(--ray-easing-out); }
|
|
34
34
|
|
|
35
|
-
:
|
|
36
|
-
|
|
35
|
+
/* position: start/end follow the writing direction, left/right are sides
|
|
36
|
+
of the screen. */
|
|
37
|
+
dialog {
|
|
38
|
+
inset-block: var(--_inset);
|
|
39
|
+
inset-inline: auto var(--_inset);
|
|
37
40
|
width: min(var(--_size), calc(100vw - 2 * var(--_inset)));
|
|
38
41
|
height: calc(100dvh - 2 * var(--_inset));
|
|
39
|
-
border-
|
|
42
|
+
border-inline-start-width: 1px;
|
|
43
|
+
--_from: translateX(100%);
|
|
44
|
+
}
|
|
45
|
+
dialog:dir(rtl) { --_from: translateX(-100%); }
|
|
46
|
+
:host([position="start"]) dialog {
|
|
47
|
+
inset-inline: var(--_inset) auto;
|
|
48
|
+
border-inline-start-width: 0;
|
|
49
|
+
border-inline-end-width: 1px;
|
|
50
|
+
--_from: translateX(-100%);
|
|
51
|
+
}
|
|
52
|
+
:host([position="start"]) dialog:dir(rtl) { --_from: translateX(100%); }
|
|
53
|
+
:host([position="right"]) dialog {
|
|
54
|
+
inset: var(--_inset) var(--_inset) var(--_inset) auto; /* physical */
|
|
55
|
+
border-width: 0;
|
|
56
|
+
border-left-width: 1px; /* physical */
|
|
40
57
|
--_from: translateX(100%);
|
|
41
58
|
}
|
|
42
59
|
:host([position="left"]) dialog {
|
|
43
|
-
inset: var(--_inset) auto var(--_inset) var(--_inset);
|
|
44
|
-
width:
|
|
45
|
-
|
|
46
|
-
border-right-width: 1px;
|
|
60
|
+
inset: var(--_inset) auto var(--_inset) var(--_inset); /* physical */
|
|
61
|
+
border-width: 0;
|
|
62
|
+
border-right-width: 1px; /* physical */
|
|
47
63
|
--_from: translateX(-100%);
|
|
48
64
|
}
|
|
49
65
|
:host([position="top"]) dialog {
|
|
50
66
|
inset: var(--_inset) var(--_inset) auto var(--_inset);
|
|
51
67
|
width: calc(100vw - 2 * var(--_inset));
|
|
52
68
|
height: min(var(--_size), calc(100dvh - 2 * var(--_inset)));
|
|
69
|
+
border-width: 0;
|
|
53
70
|
border-bottom-width: 1px;
|
|
54
71
|
--_from: translateY(-100%);
|
|
55
72
|
}
|
|
@@ -57,6 +74,7 @@ const styles = css`
|
|
|
57
74
|
inset: auto var(--_inset) var(--_inset) var(--_inset);
|
|
58
75
|
width: calc(100vw - 2 * var(--_inset));
|
|
59
76
|
height: min(var(--_size), calc(100dvh - 2 * var(--_inset)));
|
|
77
|
+
border-width: 0;
|
|
60
78
|
border-top-width: 1px;
|
|
61
79
|
--_from: translateY(100%);
|
|
62
80
|
}
|
|
@@ -83,7 +101,7 @@ const styles = css`
|
|
|
83
101
|
justify-content: center;
|
|
84
102
|
width: var(--ray-control-height-xs);
|
|
85
103
|
height: var(--ray-control-height-xs);
|
|
86
|
-
margin-
|
|
104
|
+
margin-inline-start: auto;
|
|
87
105
|
padding: 0;
|
|
88
106
|
color: var(--ray-muted-foreground);
|
|
89
107
|
background: transparent;
|
|
@@ -101,7 +119,7 @@ const styles = css`
|
|
|
101
119
|
export class RayDrawer extends RayDialog {
|
|
102
120
|
static properties = {
|
|
103
121
|
...RayDialog.properties,
|
|
104
|
-
position: { type: String, reflect: true, default: "
|
|
122
|
+
position: { type: String, reflect: true, default: "end" },
|
|
105
123
|
size: { type: String, reflect: true, default: "md" },
|
|
106
124
|
modeless: { type: Boolean, reflect: true },
|
|
107
125
|
floating: { type: Boolean, reflect: true },
|
package/components/icon/icon.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// <ray-icon name="…">: an icon of the registry (setIcons), so the app's own
|
|
2
2
|
// markup uses the same set as the components. Decorative unless `label`.
|
|
3
3
|
// `filled` draws the icon's filled version (a state: active, selected,
|
|
4
|
-
// favorite), or the outline one when it has none.
|
|
4
|
+
// favorite), or the outline one when it has none. `mirror` mirrors it right
|
|
5
|
+
// to left (a back arrow), as the roles that point along the text (ui.next…) are.
|
|
5
6
|
|
|
6
7
|
import { RayElement, css, define } from "../../core/core.js";
|
|
7
8
|
|
|
@@ -13,6 +14,7 @@ const styles = css`
|
|
|
13
14
|
size) must not reach the icon. size= overrides it inline. */
|
|
14
15
|
:host { --_size: 1em; display: inline-flex; vertical-align: -0.125em; line-height: 1; }
|
|
15
16
|
.icon svg { width: var(--_size); height: var(--_size); }
|
|
17
|
+
:host([mirror]) .icon:dir(rtl) > svg { transform: scaleX(-1); }
|
|
16
18
|
`;
|
|
17
19
|
|
|
18
20
|
export class RayIcon extends RayElement {
|
|
@@ -23,6 +25,7 @@ export class RayIcon extends RayElement {
|
|
|
23
25
|
color: { type: String, reflect: true },
|
|
24
26
|
stroke: { type: Number },
|
|
25
27
|
filled: { type: Boolean, reflect: true },
|
|
28
|
+
mirror: { type: Boolean, reflect: true },
|
|
26
29
|
};
|
|
27
30
|
static styles = [styles];
|
|
28
31
|
static template = `<span class="icon" part="icon" data-ray-icon=""></span>`;
|
|
@@ -5,6 +5,10 @@ import { css, define } from "../../core/core.js";
|
|
|
5
5
|
import { RayButton } from "../button/button.js";
|
|
6
6
|
|
|
7
7
|
const styles = css`
|
|
8
|
+
/* The square on the host too: the inner button's flex: 1 (a 0 basis) gives
|
|
9
|
+
it no width of its own to an auto grid track or a shrink-to-fit box. A
|
|
10
|
+
width set on the element from outside still wins. */
|
|
11
|
+
:host { width: var(--_h); }
|
|
8
12
|
.button { width: var(--_h); padding: 0; }
|
|
9
13
|
.label { font-size: calc(var(--_fs) * 1.15); }
|
|
10
14
|
::slotted(svg) { width: 1.15em; height: 1.15em; }
|
|
@@ -21,7 +21,7 @@ const styles = css`
|
|
|
21
21
|
position: absolute;
|
|
22
22
|
z-index: 1;
|
|
23
23
|
top: var(--_top);
|
|
24
|
-
|
|
24
|
+
inset-inline-start: var(--_left);
|
|
25
25
|
display: inline-flex;
|
|
26
26
|
align-items: center;
|
|
27
27
|
justify-content: center;
|
|
@@ -34,6 +34,7 @@ const styles = css`
|
|
|
34
34
|
transform: translate(-50%, -50%);
|
|
35
35
|
pointer-events: none;
|
|
36
36
|
}
|
|
37
|
+
.indicator:dir(rtl) { transform: translate(50%, -50%); }
|
|
37
38
|
.indicator[data-label] {
|
|
38
39
|
height: auto;
|
|
39
40
|
min-height: var(--_size);
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import { css, define } from "../../core/core.js";
|
|
5
5
|
import { sizeStyles, radiusStyles } from "../../core/styles.js";
|
|
6
6
|
import { FieldControl, fieldStyles, boxStyles, fieldTemplate } from "../../core/form.js";
|
|
7
|
+
import { syncHighlight, textLayerStyles } from "../../core/textlayer.js";
|
|
8
|
+
import { syncComplete, completeStyles } from "../../core/complete.js";
|
|
7
9
|
|
|
8
10
|
const styles = css`
|
|
9
11
|
.control[type="search"]::-webkit-search-cancel-button { cursor: pointer; }
|
|
@@ -23,8 +25,11 @@ export class RayInput extends FieldControl {
|
|
|
23
25
|
placeholder: { type: String },
|
|
24
26
|
readonly: { type: Boolean, reflect: true },
|
|
25
27
|
autofocus: { type: Boolean },
|
|
28
|
+
highlight: { attribute: false },
|
|
29
|
+
complete: { attribute: false },
|
|
30
|
+
suggestionsLabel: { type: String, default: "Suggestions" },
|
|
26
31
|
};
|
|
27
|
-
static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
|
|
32
|
+
static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, textLayerStyles, completeStyles, styles];
|
|
28
33
|
static template = fieldTemplate(`<input id="control" class="control" part="input">`);
|
|
29
34
|
|
|
30
35
|
static get observedAttributes() {
|
|
@@ -64,6 +69,13 @@ export class RayInput extends FieldControl {
|
|
|
64
69
|
c.readOnly = Boolean(this.readonly);
|
|
65
70
|
this.syncField();
|
|
66
71
|
this._commit();
|
|
72
|
+
syncHighlight(this);
|
|
73
|
+
syncComplete(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Runs `highlight` again (what it marks changed, not the text). */
|
|
77
|
+
refreshHighlight() {
|
|
78
|
+
syncHighlight(this);
|
|
67
79
|
}
|
|
68
80
|
|
|
69
81
|
_commit() {
|
package/components/menu/menu.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
// context menu opened at the pointer over the trigger area.
|
|
4
4
|
//
|
|
5
5
|
// Choosing an item fires `ray-select` ({ value, item }) on the menu and
|
|
6
|
-
// closes it (unless keep-open).
|
|
6
|
+
// closes it (unless keep-open). An item of type="checkbox" toggles its
|
|
7
|
+
// `checked` first; one of type="radio" checks itself and unchecks the radio
|
|
8
|
+
// items of the menu with the same `name`. Before its trigger opens it, the
|
|
9
|
+
// menu fires a cancelable `ray-request-open` ({ event }): the place to fill
|
|
10
|
+
// items that depend on what was clicked, read only after it.
|
|
7
11
|
//
|
|
8
12
|
// With `native` and a native host that offers "popup_menu" (core/host.js; a
|
|
9
13
|
// std/ui window with core/raylang.js), it opens as the system's context menu
|
|
@@ -64,6 +68,7 @@ export class RayMenu extends Floating {
|
|
|
64
68
|
if (this.context) {
|
|
65
69
|
t.addEventListener("contextmenu", (e) => {
|
|
66
70
|
e.preventDefault();
|
|
71
|
+
if (!this._requestOpen(e)) return;
|
|
67
72
|
if (this._native()) {
|
|
68
73
|
this.popupNative();
|
|
69
74
|
return;
|
|
@@ -84,12 +89,12 @@ export class RayMenu extends Floating {
|
|
|
84
89
|
});
|
|
85
90
|
return;
|
|
86
91
|
}
|
|
87
|
-
t.addEventListener("click", () => {
|
|
92
|
+
t.addEventListener("click", (e) => {
|
|
88
93
|
if (this._native()) {
|
|
89
|
-
this.popupNative();
|
|
94
|
+
if (this._requestOpen(e)) this.popupNative();
|
|
90
95
|
return;
|
|
91
96
|
}
|
|
92
|
-
if (!this.open && this.justClosed()) return;
|
|
97
|
+
if (!this.open && (this.justClosed() || !this._requestOpen(e))) return;
|
|
93
98
|
this._focusOnOpen = "first";
|
|
94
99
|
this.toggle();
|
|
95
100
|
});
|
|
@@ -98,11 +103,20 @@ export class RayMenu extends Floating {
|
|
|
98
103
|
e.preventDefault();
|
|
99
104
|
this._focusOnOpen = e.key === "ArrowDown" ? "first" : "last";
|
|
100
105
|
if (this.open) this._focus(this._focusOnOpen);
|
|
101
|
-
else this.open = true;
|
|
106
|
+
else if (this._requestOpen(e)) this.open = true;
|
|
102
107
|
}
|
|
103
108
|
});
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Fires ray-request-open before the trigger opens the menu; false when a
|
|
113
|
+
* listener cancelled it. It comes before the items are read: an app's own
|
|
114
|
+
* listener on the trigger may run after the menu's.
|
|
115
|
+
*/
|
|
116
|
+
_requestOpen(event) {
|
|
117
|
+
return this.emit("ray-request-open", { event }, { cancelable: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
_native() {
|
|
107
121
|
return Boolean(this.native) && hostCan("popup_menu");
|
|
108
122
|
}
|
|
@@ -116,7 +130,7 @@ export class RayMenu extends Floating {
|
|
|
116
130
|
const entries = [...this.querySelectorAll("ray-menu-item, ray-menu-divider")].filter((el) => el.closest("ray-menu") === this);
|
|
117
131
|
const items = entries.map((el) => (el.localName === "ray-menu-divider"
|
|
118
132
|
? { separator: true }
|
|
119
|
-
: { title: itemLabel(el), enabled: !el.disabled }));
|
|
133
|
+
: { title: itemLabel(el), enabled: !el.disabled, ...(el.checkable ? { checked: Boolean(el.checked) } : {}) }));
|
|
120
134
|
const index = await getHost().popupMenu(items);
|
|
121
135
|
const item = index === null || index === undefined ? null : entries[index];
|
|
122
136
|
if (!item || item.localName !== "ray-menu-item") return null;
|
|
@@ -193,6 +207,13 @@ export class RayMenu extends Floating {
|
|
|
193
207
|
|
|
194
208
|
_choose(item) {
|
|
195
209
|
if (item.disabled) return;
|
|
210
|
+
if (item.type === "checkbox") item.checked = !item.checked;
|
|
211
|
+
else if (item.type === "radio") {
|
|
212
|
+
for (const other of this.items) {
|
|
213
|
+
if (other !== item && other.type === "radio" && (other.name ?? "") === (item.name ?? "")) other.checked = false;
|
|
214
|
+
}
|
|
215
|
+
item.checked = true;
|
|
216
|
+
}
|
|
196
217
|
this.emit("ray-select", { value: item.value ?? item.textContent.trim(), item });
|
|
197
218
|
if (!this.keepOpen) this.open = false;
|
|
198
219
|
}
|
|
@@ -227,10 +248,14 @@ const itemStyles = css`
|
|
|
227
248
|
:host([color]:hover), :host([color]:focus) { background: var(--_light); color: var(--_light-color); }
|
|
228
249
|
:host([disabled]) { color: var(--ray-muted-foreground); background: transparent; cursor: not-allowed; opacity: 0.6; }
|
|
229
250
|
.label { flex: 1; white-space: nowrap; }
|
|
251
|
+
/* A checkbox or radio item keeps a column for its mark, checked or not. */
|
|
252
|
+
.mark { display: none; flex: none; width: 1rem; height: 1rem; visibility: hidden; }
|
|
253
|
+
:host([type="checkbox"]) .mark, :host([type="radio"]) .mark { display: inline-flex; }
|
|
254
|
+
:host([checked]) .mark { visibility: visible; }
|
|
230
255
|
.section { display: inline-flex; align-items: center; color: var(--ray-muted-foreground); }
|
|
231
256
|
:host([color]) .section { color: inherit; }
|
|
232
257
|
.section:not([data-filled]) { display: none; }
|
|
233
|
-
.right { margin-
|
|
258
|
+
.right { margin-inline-start: var(--ray-space-6); font-size: var(--ray-font-size-xs); }
|
|
234
259
|
::slotted(svg) { width: 1rem; height: 1rem; }
|
|
235
260
|
`;
|
|
236
261
|
|
|
@@ -239,9 +264,12 @@ export class RayMenuItem extends RayElement {
|
|
|
239
264
|
value: { type: String, reflect: true },
|
|
240
265
|
disabled: { type: Boolean, reflect: true },
|
|
241
266
|
color: { type: String, reflect: true },
|
|
267
|
+
type: { type: String, reflect: true, default: "item" },
|
|
268
|
+
checked: { type: Boolean, reflect: true },
|
|
269
|
+
name: { type: String, reflect: true },
|
|
242
270
|
};
|
|
243
271
|
static styles = [itemStyles];
|
|
244
|
-
static template = `<span class="section left" part="left-section"><slot name="left"></slot></span><span class="label" part="label"><slot></slot></span><span class="section right" part="right-section"><slot name="right"></slot></span>`;
|
|
272
|
+
static template = `<span class="mark" part="mark" aria-hidden="true" data-ray-icon="ui.checked"></span><span class="section left" part="left-section"><slot name="left"></slot></span><span class="label" part="label"><slot></slot></span><span class="section right" part="right-section"><slot name="right"></slot></span>`;
|
|
245
273
|
|
|
246
274
|
constructor() {
|
|
247
275
|
super();
|
|
@@ -252,8 +280,15 @@ export class RayMenuItem extends RayElement {
|
|
|
252
280
|
}
|
|
253
281
|
}
|
|
254
282
|
|
|
283
|
+
/** Whether it is a checkbox or radio item. */
|
|
284
|
+
get checkable() {
|
|
285
|
+
return this.type === "checkbox" || this.type === "radio";
|
|
286
|
+
}
|
|
287
|
+
|
|
255
288
|
update() {
|
|
256
289
|
this.tabIndex = -1;
|
|
290
|
+
this.internals.role = this.type === "checkbox" ? "menuitemcheckbox" : this.type === "radio" ? "menuitemradio" : "menuitem";
|
|
291
|
+
this.internals.ariaChecked = this.checkable ? String(Boolean(this.checked)) : null;
|
|
257
292
|
this.internals.ariaDisabled = this.disabled ? "true" : "false";
|
|
258
293
|
if (this.color) applyColor(this, this.color);
|
|
259
294
|
}
|