@raylang/elements 0.0.0-stage → 0.4.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/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +12 -2
- package/base.css +111 -0
- package/components/accordion/accordion.js +197 -0
- package/components/alert/alert.js +112 -0
- package/components/app-bar/app-bar.js +168 -0
- package/components/app-shell/app-shell.js +198 -0
- package/components/aspect-ratio/aspect-ratio.js +59 -0
- package/components/attachment/attachment.js +333 -0
- package/components/avatar/avatar.js +96 -0
- package/components/badge/badge.js +63 -0
- package/components/blockquote/blockquote.js +128 -0
- package/components/bottom-nav/bottom-nav.js +309 -0
- package/components/breadcrumbs/breadcrumbs.js +175 -0
- package/components/button/button.js +283 -0
- package/components/button-group/button-group.js +72 -0
- package/components/card/card.js +73 -0
- package/components/carousel/carousel.js +435 -0
- package/components/center/center.js +26 -0
- package/components/checkbox/checkbox.js +152 -0
- package/components/checkbox-group/checkbox-group.js +152 -0
- package/components/code/code.js +574 -0
- package/components/color-picker/color-picker.js +775 -0
- package/components/combobox/combobox.js +563 -0
- package/components/command-palette/command-palette.js +510 -0
- package/components/container/container.js +41 -0
- package/components/date-picker/date-picker.js +594 -0
- package/components/dialog/dialog.js +329 -0
- package/components/drawer/drawer.js +139 -0
- package/components/empty-state/empty-state.js +66 -0
- package/components/file-upload/file-upload.js +589 -0
- package/components/grid/grid.js +96 -0
- package/components/group/group.js +40 -0
- package/components/icon/icon.js +58 -0
- package/components/icon-button/icon-button.js +30 -0
- package/components/indicator/indicator.js +112 -0
- package/components/input/input.js +85 -0
- package/components/kbd/kbd.js +60 -0
- package/components/menu/menu.js +296 -0
- package/components/nav-link/nav-link.js +159 -0
- package/components/notification/notification.js +591 -0
- package/components/number-input/number-input.js +241 -0
- package/components/pagination/pagination.js +194 -0
- package/components/password-input/password-input.js +85 -0
- package/components/path-input/path-input.js +137 -0
- package/components/popover/popover.js +85 -0
- package/components/progress/progress.js +109 -0
- package/components/radio/radio.js +253 -0
- package/components/scroll-area/scroll-area.js +158 -0
- package/components/search/search.js +157 -0
- package/components/segmented-control/segmented-control.js +190 -0
- package/components/select/select.js +506 -0
- package/components/separator/separator.js +58 -0
- package/components/sidebar/sidebar.js +252 -0
- package/components/skeleton/skeleton.js +90 -0
- package/components/slider/slider.js +155 -0
- package/components/spinner/spinner.js +39 -0
- package/components/split/split.js +221 -0
- package/components/stack/stack.js +46 -0
- package/components/stat/stat.js +79 -0
- package/components/statusbar/statusbar.js +110 -0
- package/components/stepper/stepper.js +239 -0
- package/components/switch/switch.js +45 -0
- package/components/table/table.js +328 -0
- package/components/tabs/tabs.js +224 -0
- package/components/textarea/textarea.js +105 -0
- package/components/theme-toggle/theme-toggle.js +78 -0
- package/components/timeline/timeline.js +150 -0
- package/components/toggle/toggle.js +130 -0
- package/components/toggle-group/toggle-group.js +216 -0
- package/components/tooltip/tooltip.js +101 -0
- package/core/charts.js +107 -0
- package/core/color.js +110 -0
- package/core/core.js +556 -0
- package/core/dismiss.js +69 -0
- package/core/floating.js +195 -0
- package/core/focus.js +70 -0
- package/core/form.js +235 -0
- package/core/host.js +200 -0
- package/core/hotkey.js +40 -0
- package/core/icons.js +104 -0
- package/core/motion.js +73 -0
- package/core/position.js +91 -0
- package/core/raylang.js +167 -0
- package/core/scroll.js +58 -0
- package/core/styles.js +211 -0
- package/core.js +16 -0
- package/custom-elements.json +18545 -0
- package/index.js +74 -0
- package/package.json +312 -4
- package/ray-elements.d.ts +2570 -0
- package/tokens.css +796 -0
- package/tokens.json +2083 -0
- package/vscode.html-custom-data.json +5499 -0
package/core/floating.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Base class of trigger + floating panel components (popover, menu, tooltip).
|
|
2
|
+
//
|
|
3
|
+
// The panel is a [popover] element in the shadow root, so it renders in the
|
|
4
|
+
// top layer (above dialogs, never clipped by overflow). It is a manual
|
|
5
|
+
// popover with light dismiss done in core/dismiss.js (Escape, outside
|
|
6
|
+
// clicks, one open at a time): closing is always ours, so it can animate
|
|
7
|
+
// out. The trigger is the element slotted into slot="trigger".
|
|
8
|
+
|
|
9
|
+
import { RayElement, css } from "./core.js";
|
|
10
|
+
import { place, autoUpdate } from "./position.js";
|
|
11
|
+
import { dismissable } from "./dismiss.js";
|
|
12
|
+
import { play, POP_OUT } from "./motion.js";
|
|
13
|
+
|
|
14
|
+
export const floatingStyles = css`
|
|
15
|
+
:host { display: inline-block; }
|
|
16
|
+
.panel {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
max-width: calc(100vw - 16px);
|
|
19
|
+
max-height: calc(100vh - 16px);
|
|
20
|
+
overflow: auto;
|
|
21
|
+
padding: 0;
|
|
22
|
+
margin: 0;
|
|
23
|
+
color: var(--ray-popover-foreground);
|
|
24
|
+
background: var(--ray-popover);
|
|
25
|
+
border: 1px solid var(--ray-border);
|
|
26
|
+
border-radius: var(--ray-radius-default);
|
|
27
|
+
box-shadow: var(--ray-shadow-md);
|
|
28
|
+
}
|
|
29
|
+
.panel:popover-open { animation: ray-pop-in var(--ray-duration-fast) var(--ray-easing-out); }
|
|
30
|
+
@keyframes ray-pop-in { from { opacity: 0; transform: scale(0.97); } }
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export class Floating extends RayElement {
|
|
34
|
+
static properties = {
|
|
35
|
+
open: { type: Boolean, reflect: true },
|
|
36
|
+
placement: { type: String, reflect: true, default: "bottom" },
|
|
37
|
+
offset: { type: Number, default: 8 },
|
|
38
|
+
};
|
|
39
|
+
/** popover attribute of the panel ("manual": closing is handled here). */
|
|
40
|
+
static popoverMode = "manual";
|
|
41
|
+
/** Close on Escape and outside clicks (tooltips: false). */
|
|
42
|
+
static lightDismiss = true;
|
|
43
|
+
/** aria-haspopup set on the trigger (null = none). */
|
|
44
|
+
static haspopup = "dialog";
|
|
45
|
+
/** The slot holding the trigger. */
|
|
46
|
+
static triggerSlot = 'slot[name="trigger"]';
|
|
47
|
+
|
|
48
|
+
#wired = false;
|
|
49
|
+
#synced = false;
|
|
50
|
+
#trigger = null;
|
|
51
|
+
#stopTracking = null;
|
|
52
|
+
#closedAt = undefined;
|
|
53
|
+
#undismiss = null;
|
|
54
|
+
#exit = null;
|
|
55
|
+
|
|
56
|
+
connectedCallback() {
|
|
57
|
+
super.connectedCallback();
|
|
58
|
+
if (this.#wired) return;
|
|
59
|
+
this.#wired = true;
|
|
60
|
+
this.panel = this.$(".panel");
|
|
61
|
+
this.panel.setAttribute("popover", /** @type {typeof Floating} */ (this.constructor).popoverMode);
|
|
62
|
+
this.panel.addEventListener("toggle", (e) => this._onToggle(e));
|
|
63
|
+
// `toggle` is async: by then focus has already left a hidden panel.
|
|
64
|
+
this.panel.addEventListener("beforetoggle", (e) => {
|
|
65
|
+
if (e.newState === "closed") this.focusWasInside = this.contains(document.activeElement) && !this.trigger?.contains(document.activeElement);
|
|
66
|
+
});
|
|
67
|
+
const slot = this.$(/** @type {typeof Floating} */ (this.constructor).triggerSlot);
|
|
68
|
+
slot?.addEventListener("slotchange", () => this._wireTrigger());
|
|
69
|
+
this._wireTrigger();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
disconnectedCallback() {
|
|
73
|
+
super.disconnectedCallback();
|
|
74
|
+
this.#stopTracking?.();
|
|
75
|
+
this.#undismiss?.();
|
|
76
|
+
this.#undismiss = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The element slotted as trigger, if any. */
|
|
80
|
+
get trigger() {
|
|
81
|
+
return this.$(/** @type {typeof Floating} */ (this.constructor).triggerSlot)?.assignedElements()[0] ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_wireTrigger() {
|
|
85
|
+
const t = this.trigger;
|
|
86
|
+
if (t === this.#trigger) return;
|
|
87
|
+
this.#trigger = t;
|
|
88
|
+
if (!t) return;
|
|
89
|
+
const hp = this.popupKind();
|
|
90
|
+
if (hp) {
|
|
91
|
+
t.setAttribute("aria-haspopup", hp);
|
|
92
|
+
t.setAttribute("aria-expanded", String(Boolean(this.open)));
|
|
93
|
+
}
|
|
94
|
+
this.wireTrigger(t);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** aria-haspopup of the trigger for this instance (null: not a popup trigger). */
|
|
98
|
+
popupKind() {
|
|
99
|
+
return /** @type {typeof Floating} */ (this.constructor).haspopup;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Subclasses attach their trigger listeners here. */
|
|
103
|
+
wireTrigger(_trigger) {}
|
|
104
|
+
|
|
105
|
+
/** The rectangle the panel is placed against (default: the trigger). */
|
|
106
|
+
anchorRect() {
|
|
107
|
+
return (this.trigger ?? this).getBoundingClientRect();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
show() { this.open = true; }
|
|
111
|
+
hide() { this.open = false; }
|
|
112
|
+
toggle() { this.open = !this.open; }
|
|
113
|
+
|
|
114
|
+
update(changed) {
|
|
115
|
+
if (changed.has("open") || !this.#synced) {
|
|
116
|
+
this.#synced = true;
|
|
117
|
+
const isOpen = this.panel.matches(":popover-open");
|
|
118
|
+
if (this.open) {
|
|
119
|
+
// Reopened while animating out: stay open.
|
|
120
|
+
this.#exit?.cancel();
|
|
121
|
+
this.#exit = null;
|
|
122
|
+
this.panel.style.pointerEvents = "";
|
|
123
|
+
if (!isOpen) this.panel.showPopover();
|
|
124
|
+
this._dismissable();
|
|
125
|
+
} else if (isOpen && !this.#exit) {
|
|
126
|
+
this._closePanel();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (this.open) this.reposition();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
_dismissable() {
|
|
133
|
+
const ctor = /** @type {typeof Floating} */ (this.constructor);
|
|
134
|
+
if (this.#undismiss || !ctor.lightDismiss || ctor.popoverMode !== "manual") return;
|
|
135
|
+
this.#undismiss = dismissable({ panel: this.panel, owner: this, trigger: this.trigger, close: () => { this.open = false; } });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The keyframes the panel leaves with (subclasses may change them). */
|
|
139
|
+
exitKeyframes() {
|
|
140
|
+
return POP_OUT;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Animate out, then really hide (unless reopened meanwhile).
|
|
144
|
+
async _closePanel() {
|
|
145
|
+
this.#undismiss?.();
|
|
146
|
+
this.#undismiss = null;
|
|
147
|
+
const exit = play(this.panel, this.exitKeyframes(), { speed: "fast" });
|
|
148
|
+
this.#exit = exit;
|
|
149
|
+
// Leaving: no more clicks on it (focus stays where it is until hidden).
|
|
150
|
+
this.panel.style.pointerEvents = "none";
|
|
151
|
+
await exit.finished;
|
|
152
|
+
this.panel.style.pointerEvents = "";
|
|
153
|
+
if (this.#exit !== exit) return;
|
|
154
|
+
this.#exit = null;
|
|
155
|
+
if (!this.open && this.panel.matches(":popover-open")) this.panel.hidePopover();
|
|
156
|
+
exit.cancel();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
reposition() {
|
|
160
|
+
if (!this.panel.matches(":popover-open")) return;
|
|
161
|
+
place(this.anchorRect(), this.panel, { placement: this.placement || "bottom", offset: this.offset ?? 8, ...this.placeOptions() });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Extra place() options for subclasses (e.g. matchWidth). */
|
|
165
|
+
placeOptions() { return {}; }
|
|
166
|
+
|
|
167
|
+
_onToggle(e) {
|
|
168
|
+
const opening = e.newState === "open";
|
|
169
|
+
if (this.popupKind()) this.#trigger?.setAttribute("aria-expanded", String(opening));
|
|
170
|
+
if (opening) {
|
|
171
|
+
this.reposition();
|
|
172
|
+
const anchor = this.trigger ?? this;
|
|
173
|
+
this.#stopTracking = autoUpdate(anchor, this.panel, () => this.reposition());
|
|
174
|
+
} else {
|
|
175
|
+
this.#stopTracking?.();
|
|
176
|
+
this.#stopTracking = null;
|
|
177
|
+
this.#closedAt = performance.now();
|
|
178
|
+
}
|
|
179
|
+
// Light dismiss and Escape close the panel behind our back: sync `open`.
|
|
180
|
+
if (Boolean(this.open) !== opening) this.commitProperty("open", opening);
|
|
181
|
+
this.emit(opening ? "ray-open" : "ray-close");
|
|
182
|
+
this.toggled(opening);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Hook after the panel opened/closed. */
|
|
186
|
+
toggled(_open) {}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether a click on the trigger should be ignored because the same
|
|
190
|
+
* pointer gesture just light-dismissed the panel (it would reopen it).
|
|
191
|
+
*/
|
|
192
|
+
justClosed() {
|
|
193
|
+
return this.#closedAt !== undefined && performance.now() - this.#closedAt < 250;
|
|
194
|
+
}
|
|
195
|
+
}
|
package/core/focus.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Focus helpers that see through shadow roots and slots (the flat tree), so
|
|
2
|
+
// components made of other components can find what is focusable.
|
|
3
|
+
|
|
4
|
+
const FOCUSABLE = [
|
|
5
|
+
"a[href]",
|
|
6
|
+
"area[href]",
|
|
7
|
+
"button:not([disabled])",
|
|
8
|
+
"input:not([disabled]):not([type=hidden])",
|
|
9
|
+
"select:not([disabled])",
|
|
10
|
+
"textarea:not([disabled])",
|
|
11
|
+
"iframe",
|
|
12
|
+
"audio[controls]",
|
|
13
|
+
"video[controls]",
|
|
14
|
+
"summary",
|
|
15
|
+
"[contenteditable]:not([contenteditable=false])",
|
|
16
|
+
"[tabindex]",
|
|
17
|
+
].join(",");
|
|
18
|
+
|
|
19
|
+
function visible(el) {
|
|
20
|
+
if (typeof el.checkVisibility === "function") return el.checkVisibility({ visibilityProperty: true });
|
|
21
|
+
return el.getClientRects().length > 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Tabbable elements under `root` (an element or a shadow root), in tree
|
|
26
|
+
* order, descending into open shadow roots and slotted content.
|
|
27
|
+
* @returns {HTMLElement[]}
|
|
28
|
+
*/
|
|
29
|
+
export function tabbables(root, { lightOnly = false } = {}) {
|
|
30
|
+
const out = [];
|
|
31
|
+
const walk = (node) => {
|
|
32
|
+
for (const child of childrenOf(node)) {
|
|
33
|
+
if (!(child instanceof HTMLElement)) continue;
|
|
34
|
+
if (child.matches(FOCUSABLE) && child.tabIndex >= 0 && !child.hasAttribute("inert") && visible(child)) {
|
|
35
|
+
out.push(child);
|
|
36
|
+
}
|
|
37
|
+
if (child.hasAttribute("inert")) continue;
|
|
38
|
+
walk(child);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (lightOnly) {
|
|
42
|
+
// Only the root's own (slotted) content, not its shadow root.
|
|
43
|
+
for (const child of root.children) {
|
|
44
|
+
if (!(child instanceof HTMLElement) || child.hasAttribute("inert")) continue;
|
|
45
|
+
if (child.matches(FOCUSABLE) && child.tabIndex >= 0 && visible(child)) out.push(child);
|
|
46
|
+
walk(child);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
walk(root);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function childrenOf(node) {
|
|
55
|
+
if (node instanceof HTMLSlotElement) return node.assignedElements({ flatten: true });
|
|
56
|
+
if (node instanceof Element && node.shadowRoot) return [...node.shadowRoot.children];
|
|
57
|
+
return [...node.children];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Focuses the first [autofocus] or tabbable element; returns whether
|
|
62
|
+
* something got focus. `lightOnly`: look only at a host's own content.
|
|
63
|
+
*/
|
|
64
|
+
export function focusFirst(root, { lightOnly = false } = {}) {
|
|
65
|
+
const auto = root.querySelector?.("[autofocus]");
|
|
66
|
+
const target = auto ?? tabbables(root, { lightOnly })[0];
|
|
67
|
+
if (!target) return false;
|
|
68
|
+
target.focus({ preventScroll: true });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
package/core/form.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Form-associated base classes.
|
|
2
|
+
//
|
|
3
|
+
// FormControl: a custom element that takes part in <form> like a native
|
|
4
|
+
// control (value submitted under `name`, constraint validation, reset,
|
|
5
|
+
// disabled by <fieldset>), through ElementInternals.
|
|
6
|
+
//
|
|
7
|
+
// FieldControl: a FormControl around one native control in its shadow root,
|
|
8
|
+
// with Mantine's input wrapper — label, description, error and the required
|
|
9
|
+
// asterisk — wired with ids and ARIA *inside* the shadow root, so the
|
|
10
|
+
// accessible name works (a <label for> outside cannot reach into it).
|
|
11
|
+
|
|
12
|
+
import { RayElement, css } from "./core.js";
|
|
13
|
+
|
|
14
|
+
export class FormControl extends RayElement {
|
|
15
|
+
static formAssociated = true;
|
|
16
|
+
static delegatesFocus = true;
|
|
17
|
+
|
|
18
|
+
#fieldsetDisabled = false;
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
this.internals = this.attachInternals();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The <form> this control belongs to. */
|
|
26
|
+
get form() { return this.internals.form; }
|
|
27
|
+
get validity() { return this.internals.validity; }
|
|
28
|
+
get validationMessage() { return this.internals.validationMessage; }
|
|
29
|
+
get willValidate() { return this.internals.willValidate; }
|
|
30
|
+
checkValidity() { return this.internals.checkValidity(); }
|
|
31
|
+
reportValidity() { return this.internals.reportValidity(); }
|
|
32
|
+
|
|
33
|
+
/** Disabled by its own attribute or by a disabled <fieldset>. */
|
|
34
|
+
get effectivelyDisabled() {
|
|
35
|
+
return Boolean(this.disabled) || this.#fieldsetDisabled === true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
formDisabledCallback(disabled) {
|
|
39
|
+
this.#fieldsetDisabled = disabled;
|
|
40
|
+
this.requestUpdate();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Re-dispatches a non-composed event (e.g. `change`) from the host. */
|
|
44
|
+
forward(event) {
|
|
45
|
+
this.dispatchEvent(new Event(event.type, { bubbles: true, cancelable: event.cancelable }));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The field wrapper around a control: label, description, box, error. */
|
|
50
|
+
export const fieldStyles = css`
|
|
51
|
+
:host {
|
|
52
|
+
display: flex;
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
font-size: var(--_fs);
|
|
55
|
+
}
|
|
56
|
+
/* description-position="below": after the control, with the error (the DOM
|
|
57
|
+
order and aria-describedby stay the same). */
|
|
58
|
+
:host([description-position="below"]) .description { order: 1; margin: var(--ray-space-1) 0 0; }
|
|
59
|
+
:host([description-position="below"]) .error { order: 2; }
|
|
60
|
+
.label {
|
|
61
|
+
display: inline-block;
|
|
62
|
+
align-self: flex-start;
|
|
63
|
+
margin-bottom: var(--ray-space-1);
|
|
64
|
+
font-size: var(--_fs);
|
|
65
|
+
font-weight: var(--ray-font-weight-medium);
|
|
66
|
+
color: var(--ray-foreground);
|
|
67
|
+
cursor: default;
|
|
68
|
+
}
|
|
69
|
+
.required { color: var(--ray-destructive); }
|
|
70
|
+
.description {
|
|
71
|
+
margin: calc(var(--ray-space-1) * -0.5) 0 var(--ray-space-1);
|
|
72
|
+
font-size: calc(var(--_fs) - 0.125rem);
|
|
73
|
+
line-height: var(--ray-line-height-sm);
|
|
74
|
+
color: var(--ray-muted-foreground);
|
|
75
|
+
}
|
|
76
|
+
.error {
|
|
77
|
+
margin-top: var(--ray-space-1);
|
|
78
|
+
font-size: calc(var(--_fs) - 0.125rem);
|
|
79
|
+
line-height: var(--ray-line-height-sm);
|
|
80
|
+
color: var(--ray-destructive);
|
|
81
|
+
}
|
|
82
|
+
[hidden] { display: none !important; }
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
/** A text-like control box: input, textarea, select. */
|
|
86
|
+
export const boxStyles = css`
|
|
87
|
+
.box {
|
|
88
|
+
position: relative;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
min-height: var(--_h);
|
|
92
|
+
color: var(--ray-foreground);
|
|
93
|
+
background: var(--ray-background);
|
|
94
|
+
border: 1px solid var(--ray-input);
|
|
95
|
+
border-radius: var(--_r);
|
|
96
|
+
border-start-start-radius: var(--ray-group-radius-ss, var(--_r));
|
|
97
|
+
border-start-end-radius: var(--ray-group-radius-se, var(--_r));
|
|
98
|
+
border-end-start-radius: var(--ray-group-radius-es, var(--_r));
|
|
99
|
+
border-end-end-radius: var(--ray-group-radius-ee, var(--_r));
|
|
100
|
+
transition: border-color var(--ray-duration-fast) var(--ray-easing-standard);
|
|
101
|
+
}
|
|
102
|
+
:host([variant="filled"]) .box { background: var(--ray-muted); border-color: transparent; }
|
|
103
|
+
:host([variant="unstyled"]) .box { background: transparent; border-color: transparent; }
|
|
104
|
+
.box:focus-within {
|
|
105
|
+
border-color: var(--ray-primary);
|
|
106
|
+
outline: var(--ray-focus-ring-width) solid var(--ray-ring);
|
|
107
|
+
outline-offset: -1px;
|
|
108
|
+
}
|
|
109
|
+
:host([variant="unstyled"]) .box:focus-within { outline: none; }
|
|
110
|
+
.box[data-invalid] { border-color: var(--ray-destructive); }
|
|
111
|
+
.box[data-invalid]:focus-within { outline-color: color-mix(in srgb, var(--ray-destructive) 45%, transparent); }
|
|
112
|
+
.box[data-disabled] { background: var(--ray-muted); color: var(--ray-muted-foreground); cursor: not-allowed; opacity: 0.75; }
|
|
113
|
+
.control {
|
|
114
|
+
flex: 1;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
height: 100%;
|
|
117
|
+
min-height: calc(var(--_h) - 2px);
|
|
118
|
+
margin: 0;
|
|
119
|
+
padding: 0 calc(var(--_px) * 0.66);
|
|
120
|
+
font: inherit;
|
|
121
|
+
font-size: var(--_fs);
|
|
122
|
+
color: inherit;
|
|
123
|
+
background: transparent;
|
|
124
|
+
border: 0;
|
|
125
|
+
outline: none;
|
|
126
|
+
}
|
|
127
|
+
.control::placeholder { color: var(--ray-muted-foreground); opacity: 0.8; }
|
|
128
|
+
.control:disabled { cursor: not-allowed; }
|
|
129
|
+
.section {
|
|
130
|
+
display: inline-flex;
|
|
131
|
+
flex: none;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
min-width: var(--_h);
|
|
135
|
+
height: calc(var(--_h) - 2px);
|
|
136
|
+
color: var(--ray-muted-foreground);
|
|
137
|
+
}
|
|
138
|
+
.section:not([data-filled]) { display: none; }
|
|
139
|
+
.section[data-filled] + .control, .control:has(+ .section[data-filled]) { padding-inline: 0; }
|
|
140
|
+
`;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Markup of a field around `control`, whose element must carry
|
|
144
|
+
* id="control" and class="control". Sections are slots "left" and "right".
|
|
145
|
+
*/
|
|
146
|
+
export function fieldTemplate(control, { sections = true } = {}) {
|
|
147
|
+
const left = sections ? `<span class="section" part="left-section"><slot name="left"></slot></span>` : "";
|
|
148
|
+
const right = sections ? `<span class="section" part="right-section"><slot name="right"></slot></span>` : "";
|
|
149
|
+
return `
|
|
150
|
+
<label class="label" part="label" for="control" hidden><span class="label-text"></span><span class="required" aria-hidden="true" hidden> *</span></label>
|
|
151
|
+
<div class="description" part="description" id="description" hidden></div>
|
|
152
|
+
<div class="box" part="wrapper">${left}${control}${right}</div>
|
|
153
|
+
<div class="error" part="error" id="error" hidden></div>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A FormControl around one native control (#control in the shadow root).
|
|
158
|
+
* Subclasses declare the field properties with FieldControl.fieldProperties.
|
|
159
|
+
*/
|
|
160
|
+
export class FieldControl extends FormControl {
|
|
161
|
+
#wired = false;
|
|
162
|
+
|
|
163
|
+
static fieldProperties = {
|
|
164
|
+
name: { type: String, reflect: true },
|
|
165
|
+
label: { type: String },
|
|
166
|
+
description: { type: String },
|
|
167
|
+
error: { type: String },
|
|
168
|
+
required: { type: Boolean, reflect: true },
|
|
169
|
+
disabled: { type: Boolean, reflect: true },
|
|
170
|
+
size: { type: String, reflect: true, default: "sm" },
|
|
171
|
+
radius: { type: String, reflect: true },
|
|
172
|
+
variant: { type: String, reflect: true, default: "default" },
|
|
173
|
+
descriptionPosition: { type: String, reflect: true, default: "above" },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
constructor() {
|
|
177
|
+
super();
|
|
178
|
+
/** The native control in the shadow root. */
|
|
179
|
+
this.control = this.$("#control");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
connectedCallback() {
|
|
183
|
+
super.connectedCallback();
|
|
184
|
+
if (!this.#wired) {
|
|
185
|
+
this.#wired = true;
|
|
186
|
+
for (const side of ["left", "right"]) {
|
|
187
|
+
const slot = this.$(`slot[name="${side}"]`);
|
|
188
|
+
slot?.addEventListener("slotchange", () => {
|
|
189
|
+
slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
this.wire?.();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Label, description, error, ARIA and validity. Call from update(). */
|
|
197
|
+
syncField() {
|
|
198
|
+
const label = this.$(".label");
|
|
199
|
+
label.hidden = !this.label;
|
|
200
|
+
this.$(".label-text").textContent = this.label ?? "";
|
|
201
|
+
this.$(".required").hidden = !this.required;
|
|
202
|
+
|
|
203
|
+
const desc = this.$("#description");
|
|
204
|
+
desc.hidden = !this.description;
|
|
205
|
+
desc.textContent = this.description ?? "";
|
|
206
|
+
const err = this.$("#error");
|
|
207
|
+
err.hidden = !this.error;
|
|
208
|
+
err.textContent = this.error ?? "";
|
|
209
|
+
|
|
210
|
+
const c = this.control;
|
|
211
|
+
const described = [this.description ? "description" : "", this.error ? "error" : ""].filter(Boolean).join(" ");
|
|
212
|
+
if (described) c.setAttribute("aria-describedby", described);
|
|
213
|
+
else c.removeAttribute("aria-describedby");
|
|
214
|
+
if (this.error) c.setAttribute("aria-invalid", "true");
|
|
215
|
+
else c.removeAttribute("aria-invalid");
|
|
216
|
+
// Without a visible label, the host's aria-label names the control.
|
|
217
|
+
const aria = this.getAttribute("aria-label");
|
|
218
|
+
if (!this.label && aria) c.setAttribute("aria-label", aria);
|
|
219
|
+
else c.removeAttribute("aria-label");
|
|
220
|
+
|
|
221
|
+
c.required = Boolean(this.required);
|
|
222
|
+
c.disabled = this.effectivelyDisabled;
|
|
223
|
+
const box = this.$(".box");
|
|
224
|
+
box?.toggleAttribute("data-invalid", Boolean(this.error));
|
|
225
|
+
box?.toggleAttribute("data-disabled", this.effectivelyDisabled);
|
|
226
|
+
this.syncValidity();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Mirrors the native control's validity into the form. */
|
|
230
|
+
syncValidity() {
|
|
231
|
+
const c = this.control;
|
|
232
|
+
if (c.validity.valid) this.internals.setValidity({});
|
|
233
|
+
else this.internals.setValidity(c.validity, c.validationMessage, c);
|
|
234
|
+
}
|
|
235
|
+
}
|