@reliability-design/web 1.0.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/rel-accordion/README.md +132 -0
- package/dist/components/rel-accordion/rel-accordion.ts +47 -16
- package/dist/components/rel-alert/README.md +141 -0
- package/dist/components/rel-alert/rel-alert.ts +101 -20
- package/dist/components/rel-avatar/README.md +119 -0
- package/dist/components/rel-avatar/rel-avatar.ts +3 -2
- package/dist/components/rel-badge/README.md +86 -0
- package/dist/components/rel-badge/rel-badge.ts +11 -7
- package/dist/components/rel-breadcrumb/README.md +102 -0
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
- package/dist/components/rel-button/README.md +130 -0
- package/dist/components/rel-button/rel-button.ts +64 -8
- package/dist/components/rel-calendar/README.md +109 -0
- package/dist/components/rel-calendar/index.ts +1 -0
- package/dist/components/rel-calendar/rel-calendar.ts +467 -0
- package/dist/components/rel-card/README.md +94 -0
- package/dist/components/rel-card/rel-card.ts +12 -3
- package/dist/components/rel-cardmedia/README.md +127 -0
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
- package/dist/components/rel-carousel/README.md +166 -0
- package/dist/components/rel-carousel/index.ts +1 -0
- package/dist/components/rel-carousel/rel-carousel.ts +468 -0
- package/dist/components/rel-chart/README.md +286 -0
- package/dist/components/rel-chart/index.ts +1 -0
- package/dist/components/rel-chart/rel-chart.ts +736 -0
- package/dist/components/rel-checkbox/README.md +77 -0
- package/dist/components/rel-checkbox/rel-checkbox.ts +51 -12
- package/dist/components/rel-chip/README.md +114 -0
- package/dist/components/rel-chip/rel-chip.ts +83 -9
- package/dist/components/rel-container/README.md +99 -0
- package/dist/components/rel-date-picker/README.md +253 -0
- package/dist/components/rel-date-picker/index.ts +1 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
- package/dist/components/rel-divider/README.md +100 -0
- package/dist/components/rel-divider/rel-divider.ts +9 -0
- package/dist/components/rel-drawer/README.md +200 -0
- package/dist/components/rel-drawer/index.ts +1 -0
- package/dist/components/rel-drawer/rel-drawer-footer.ts +29 -0
- package/dist/components/rel-drawer/rel-drawer.ts +612 -0
- package/dist/components/rel-kbd/README.md +151 -0
- package/dist/components/rel-kbd/rel-kbd.ts +118 -0
- package/dist/components/rel-layout/README.md +88 -0
- package/dist/components/rel-menu/README.md +151 -0
- package/dist/components/rel-menu/rel-menu.ts +64 -24
- package/dist/components/rel-menubar/README.md +329 -0
- package/dist/components/rel-menubar/index.ts +2 -0
- package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
- package/dist/components/rel-menubar/rel-menubar.ts +605 -0
- package/dist/components/rel-nav-menu/README.md +289 -0
- package/dist/components/rel-nav-menu/index.ts +3 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -0
- package/dist/components/rel-navbar/README.md +126 -0
- package/dist/components/rel-navbar/rel-navbar.ts +104 -0
- package/dist/components/rel-otp/README.md +440 -0
- package/dist/components/rel-otp/index.ts +1 -0
- package/dist/components/rel-otp/rel-otp.ts +504 -0
- package/dist/components/rel-pagination/README.md +410 -0
- package/dist/components/rel-pagination/index.ts +1 -0
- package/dist/components/rel-pagination/rel-pagination.ts +381 -0
- package/dist/components/rel-popover/README.md +568 -0
- package/dist/components/rel-popover/index.ts +2 -0
- package/dist/components/rel-popover/rel-popover-content.ts +55 -0
- package/dist/components/rel-popover/rel-popover.ts +330 -0
- package/dist/components/rel-progress/README.md +110 -0
- package/dist/components/rel-progress/rel-progress.ts +32 -5
- package/dist/components/rel-radio/README.md +120 -0
- package/dist/components/rel-radio/rel-radio.ts +31 -6
- package/dist/components/rel-resizable/README.md +140 -0
- package/dist/components/rel-resizable/index.ts +3 -0
- package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
- package/dist/components/rel-resizable/rel-resizable-handle.ts +145 -0
- package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
- package/dist/components/rel-segmented-button/README.md +141 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
- package/dist/components/rel-select/README.md +107 -0
- package/dist/components/rel-select/rel-select.ts +28 -14
- package/dist/components/rel-sheet/README.md +224 -0
- package/dist/components/rel-sheet/rel-sheet.ts +684 -0
- package/dist/components/rel-sidebar/README.md +209 -0
- package/dist/components/rel-sidebar/rel-sidebar.ts +1166 -0
- package/dist/components/rel-slider/README.md +135 -0
- package/dist/components/rel-slider/rel-slider.ts +44 -9
- package/dist/components/rel-snackbar/README.md +134 -0
- package/dist/components/rel-snackbar/rel-snackbar.ts +109 -14
- package/dist/components/rel-stepper/README.md +160 -0
- package/dist/components/rel-stepper/rel-stepper.ts +38 -27
- package/dist/components/rel-switch/README.md +102 -0
- package/dist/components/rel-switch/rel-switch.ts +34 -12
- package/dist/components/rel-tab/README.md +163 -0
- package/dist/components/rel-tab/rel-tab.ts +79 -23
- package/dist/components/rel-table/README.md +178 -0
- package/dist/components/rel-table/rel-table-body.ts +21 -0
- package/dist/components/rel-table/rel-table-caption.ts +27 -0
- package/dist/components/rel-table/rel-table-cell.ts +42 -0
- package/dist/components/rel-table/rel-table-footer.ts +23 -0
- package/dist/components/rel-table/rel-table-head.ts +45 -0
- package/dist/components/rel-table/rel-table-header.ts +21 -0
- package/dist/components/rel-table/rel-table-row.ts +36 -0
- package/dist/components/rel-table/rel-table.ts +53 -0
- package/dist/components/rel-textfield/README.md +90 -0
- package/dist/components/rel-textfield/rel-textfield.ts +55 -39
- package/dist/components/rel-toast/README.md +148 -0
- package/dist/components/rel-toast/rel-toast.ts +63 -24
- package/dist/components/rel-tooltip/README.md +103 -0
- package/dist/components/rel-tooltip/rel-tooltip.ts +123 -80
- package/dist/components/rel-topbar/rel-topbar-actions.ts +23 -0
- package/dist/components/rel-topbar/rel-topbar-brand.ts +46 -0
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +33 -0
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
- package/dist/components/rel-topbar/rel-topbar-item.ts +315 -0
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +23 -0
- package/dist/components/rel-topbar/rel-topbar-profile.ts +248 -0
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +23 -0
- package/dist/components/rel-topbar/rel-topbar.ts +329 -0
- package/dist/index.d.mts +831 -7
- package/dist/index.d.ts +831 -7
- package/dist/index.js +9500 -1564
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9240 -1344
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +187 -25
- package/dist/vars-TBB4BMSD.css +346 -0
- package/package.json +2 -1
- package/dist/vars-6ZIUPIU4.css +0 -184
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement, property, state, query } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
type SideType = "top" | "right" | "bottom" | "left";
|
|
5
|
+
type AlignType = "start" | "center" | "end";
|
|
6
|
+
|
|
7
|
+
@customElement("rel-popover")
|
|
8
|
+
export class RelPopover extends LitElement {
|
|
9
|
+
/* =======================
|
|
10
|
+
* PUBLIC API
|
|
11
|
+
* ======================= */
|
|
12
|
+
|
|
13
|
+
@property({ type: Boolean, reflect: true }) open?: boolean;
|
|
14
|
+
@property({ type: Boolean, attribute: "defaultopen" }) defaultOpen = false;
|
|
15
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
16
|
+
|
|
17
|
+
@property({ type: String }) side: SideType = "bottom";
|
|
18
|
+
@property({ type: String }) align: AlignType = "center";
|
|
19
|
+
@property({ type: Number, attribute: "sideoffset" }) sideOffset = 8;
|
|
20
|
+
@property({ type: Number, attribute: "alignoffset" }) alignOffset = 0;
|
|
21
|
+
|
|
22
|
+
@property({ type: Boolean, attribute: "trapfocus" }) trapFocus = false;
|
|
23
|
+
@property({ type: Boolean, attribute: "closeonescape" }) closeOnEscape = true;
|
|
24
|
+
@property({ type: Boolean, attribute: "closeonoutsideclick" })
|
|
25
|
+
closeOnOutsideClick = true;
|
|
26
|
+
|
|
27
|
+
/* =======================
|
|
28
|
+
* INTERNAL STATE
|
|
29
|
+
* ======================= */
|
|
30
|
+
|
|
31
|
+
@state() private internalOpen = false;
|
|
32
|
+
@state() private contentStyle = "";
|
|
33
|
+
|
|
34
|
+
@query(".content-wrapper") private contentWrapper!: HTMLDivElement;
|
|
35
|
+
|
|
36
|
+
private triggerEl: HTMLElement | null = null;
|
|
37
|
+
private returnFocusEl: HTMLElement | null = null;
|
|
38
|
+
private uniqueId = `popover-${Math.random().toString(36).slice(2)}`;
|
|
39
|
+
|
|
40
|
+
/* =======================
|
|
41
|
+
* STYLES
|
|
42
|
+
* ======================= */
|
|
43
|
+
|
|
44
|
+
static styles = css`
|
|
45
|
+
:host {
|
|
46
|
+
display: inline-block;
|
|
47
|
+
position: relative;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
:host([disabled]) {
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
opacity: 0.6;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.content-wrapper {
|
|
56
|
+
position: fixed;
|
|
57
|
+
z-index: 1001;
|
|
58
|
+
display: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.content-wrapper[open] {
|
|
62
|
+
display: block;
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
/* =======================
|
|
67
|
+
* LIFECYCLE
|
|
68
|
+
* ======================= */
|
|
69
|
+
|
|
70
|
+
connectedCallback() {
|
|
71
|
+
super.connectedCallback();
|
|
72
|
+
this.internalOpen = this.isControlled() ? !!this.open : this.defaultOpen;
|
|
73
|
+
|
|
74
|
+
// ONE interaction entry point — capture phase
|
|
75
|
+
this.addEventListener("pointerdown", this.onHostPointerDown, true);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
disconnectedCallback() {
|
|
79
|
+
super.disconnectedCallback();
|
|
80
|
+
this.removeEventListener("pointerdown", this.onHostPointerDown, true);
|
|
81
|
+
this.removeGlobalListeners();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
firstUpdated() {
|
|
85
|
+
// triggerEl is resolved via onTriggerSlotChange; call aria in case slot
|
|
86
|
+
// content was already assigned before firstUpdated
|
|
87
|
+
this.applyTriggerAria();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
updated(changed: Map<string, unknown>) {
|
|
91
|
+
if (changed.has("open") && this.isControlled()) {
|
|
92
|
+
this.internalOpen = !!this.open;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (changed.has("internalOpen") || changed.has("open")) {
|
|
96
|
+
this.applyTriggerAria();
|
|
97
|
+
this.activeOpen ? this.onOpen() : this.onClose();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* =======================
|
|
102
|
+
* STATE HELPERS
|
|
103
|
+
* ======================= */
|
|
104
|
+
|
|
105
|
+
private isControlled() {
|
|
106
|
+
return this.open !== undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private get activeOpen() {
|
|
110
|
+
return this.isControlled() ? !!this.open : this.internalOpen;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private setOpen(next: boolean) {
|
|
114
|
+
if (!this.isControlled()) {
|
|
115
|
+
this.internalOpen = next;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.dispatchEvent(
|
|
119
|
+
new CustomEvent("rel-open-change", {
|
|
120
|
+
detail: { open: next },
|
|
121
|
+
bubbles: true,
|
|
122
|
+
composed: true,
|
|
123
|
+
}),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* =======================
|
|
128
|
+
* POINTER HANDLING (THE FIX)
|
|
129
|
+
* ======================= */
|
|
130
|
+
|
|
131
|
+
private onHostPointerDown = (e: PointerEvent) => {
|
|
132
|
+
if (this.disabled) return;
|
|
133
|
+
|
|
134
|
+
const path = e.composedPath();
|
|
135
|
+
|
|
136
|
+
// Toggle ONLY if pointerdown originated from the trigger element
|
|
137
|
+
if (!this.triggerEl || !path.includes(this.triggerEl)) return;
|
|
138
|
+
|
|
139
|
+
// Prevent this interaction from becoming an "outside click"
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
e.stopPropagation();
|
|
142
|
+
|
|
143
|
+
this.setOpen(!this.activeOpen);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/* =======================
|
|
147
|
+
* OPEN / CLOSE
|
|
148
|
+
* ======================= */
|
|
149
|
+
|
|
150
|
+
private onOpen() {
|
|
151
|
+
this.returnFocusEl =
|
|
152
|
+
(document.activeElement as HTMLElement) ?? this.triggerEl;
|
|
153
|
+
|
|
154
|
+
this.addGlobalListeners();
|
|
155
|
+
|
|
156
|
+
requestAnimationFrame(() => {
|
|
157
|
+
this.updatePosition();
|
|
158
|
+
this.focusFirst();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private onClose() {
|
|
163
|
+
this.removeGlobalListeners();
|
|
164
|
+
this.restoreFocus();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* =======================
|
|
168
|
+
* GLOBAL LISTENERS
|
|
169
|
+
* ======================= */
|
|
170
|
+
|
|
171
|
+
private addGlobalListeners() {
|
|
172
|
+
if (this.closeOnOutsideClick) {
|
|
173
|
+
document.addEventListener("pointerdown", this.onOutsidePointerDown);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (this.closeOnEscape || this.trapFocus) {
|
|
177
|
+
document.addEventListener("keydown", this.onKeyDown);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
window.addEventListener("resize", this.updatePosition);
|
|
181
|
+
window.addEventListener("scroll", this.updatePosition, true);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private removeGlobalListeners() {
|
|
185
|
+
document.removeEventListener("pointerdown", this.onOutsidePointerDown);
|
|
186
|
+
document.removeEventListener("keydown", this.onKeyDown);
|
|
187
|
+
window.removeEventListener("resize", this.updatePosition);
|
|
188
|
+
window.removeEventListener("scroll", this.updatePosition, true);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private onOutsidePointerDown = (e: PointerEvent) => {
|
|
192
|
+
if (!this.activeOpen) return;
|
|
193
|
+
|
|
194
|
+
const path = e.composedPath();
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
path.includes(this) ||
|
|
198
|
+
(this.triggerEl && path.includes(this.triggerEl)) ||
|
|
199
|
+
(this.contentWrapper && path.includes(this.contentWrapper))
|
|
200
|
+
) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.setOpen(false);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
private onKeyDown = (e: KeyboardEvent) => {
|
|
208
|
+
if (!this.activeOpen) return;
|
|
209
|
+
|
|
210
|
+
if (e.key === "Escape" && this.closeOnEscape) {
|
|
211
|
+
e.preventDefault();
|
|
212
|
+
this.setOpen(false);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (this.trapFocus && e.key === "Tab") {
|
|
216
|
+
this.trapFocusLoop(e);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/* =======================
|
|
221
|
+
* FOCUS
|
|
222
|
+
* ======================= */
|
|
223
|
+
|
|
224
|
+
private focusFirst() {
|
|
225
|
+
if (!this.contentWrapper) return;
|
|
226
|
+
|
|
227
|
+
const focusable = this.getFocusable(this.contentWrapper);
|
|
228
|
+
if (focusable[0]) {
|
|
229
|
+
focusable[0].focus();
|
|
230
|
+
} else {
|
|
231
|
+
this.contentWrapper.focus();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private restoreFocus() {
|
|
236
|
+
this.triggerEl?.focus();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private trapFocusLoop(e: KeyboardEvent) {
|
|
240
|
+
const focusable = this.getFocusable(this.contentWrapper);
|
|
241
|
+
if (focusable.length === 0) return;
|
|
242
|
+
|
|
243
|
+
const first = focusable[0];
|
|
244
|
+
const last = focusable[focusable.length - 1];
|
|
245
|
+
|
|
246
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
last.focus();
|
|
249
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
first.focus();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private getFocusable(root: HTMLElement): HTMLElement[] {
|
|
256
|
+
return Array.from(
|
|
257
|
+
root.querySelectorAll<HTMLElement>(
|
|
258
|
+
'a[href],button:not([disabled]),input:not([disabled]),[tabindex]:not([tabindex="-1"])',
|
|
259
|
+
),
|
|
260
|
+
).filter((el) => el.offsetWidth || el.offsetHeight);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* =======================
|
|
264
|
+
* POSITIONING
|
|
265
|
+
* ======================= */
|
|
266
|
+
|
|
267
|
+
private updatePosition = () => {
|
|
268
|
+
if (!this.triggerEl || !this.contentWrapper) return;
|
|
269
|
+
|
|
270
|
+
this.contentWrapper.setAttribute("open", "");
|
|
271
|
+
|
|
272
|
+
const t = this.triggerEl.getBoundingClientRect();
|
|
273
|
+
const c = this.contentWrapper.getBoundingClientRect();
|
|
274
|
+
|
|
275
|
+
let top = t.bottom + this.sideOffset;
|
|
276
|
+
let left = t.left + t.width / 2 - c.width / 2;
|
|
277
|
+
|
|
278
|
+
if (this.side === "top") top = t.top - c.height - this.sideOffset;
|
|
279
|
+
if (this.side === "left") left = t.left - c.width - this.sideOffset;
|
|
280
|
+
if (this.side === "right") left = t.right + this.sideOffset;
|
|
281
|
+
|
|
282
|
+
top = Math.max(8, Math.min(top, window.innerHeight - c.height - 8));
|
|
283
|
+
left = Math.max(8, Math.min(left, window.innerWidth - c.width - 8));
|
|
284
|
+
|
|
285
|
+
this.contentStyle = `top:${top}px;left:${left}px;`;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/* =======================
|
|
289
|
+
* ARIA
|
|
290
|
+
* ======================= */
|
|
291
|
+
|
|
292
|
+
private onTriggerSlotChange(e: Event) {
|
|
293
|
+
const slot = e.target as HTMLSlotElement;
|
|
294
|
+
const first = slot.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
|
|
295
|
+
this.triggerEl = first ?? null;
|
|
296
|
+
this.applyTriggerAria();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private applyTriggerAria() {
|
|
300
|
+
if (!this.triggerEl) return;
|
|
301
|
+
this.triggerEl.setAttribute("aria-haspopup", "dialog");
|
|
302
|
+
this.triggerEl.setAttribute("aria-expanded", String(this.activeOpen));
|
|
303
|
+
this.triggerEl.setAttribute("aria-controls", this.uniqueId);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* =======================
|
|
307
|
+
* RENDER
|
|
308
|
+
* ======================= */
|
|
309
|
+
|
|
310
|
+
render() {
|
|
311
|
+
return html`
|
|
312
|
+
<slot @slotchange=${this.onTriggerSlotChange}></slot>
|
|
313
|
+
|
|
314
|
+
<div
|
|
315
|
+
id="${this.uniqueId}"
|
|
316
|
+
class="content-wrapper"
|
|
317
|
+
style="${this.contentStyle}"
|
|
318
|
+
?open="${this.activeOpen}"
|
|
319
|
+
>
|
|
320
|
+
<slot name="content"></slot>
|
|
321
|
+
</div>
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
declare global {
|
|
327
|
+
interface HTMLElementTagNameMap {
|
|
328
|
+
"rel-popover": RelPopover;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# rel-progress
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
A progress indicator component supporting four modes: linear-determinate, linear-indeterminate, circular-determinate, and circular-indeterminate. Features smooth animations, proper ARIA attributes, and respects user motion preferences by disabling animations when requested.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
```html
|
|
8
|
+
<!-- Linear progress (determinate) -->
|
|
9
|
+
<rel-progress mode="linear-determinate" value="45"></rel-progress>
|
|
10
|
+
<rel-progress mode="linear-determinate" value="100"></rel-progress>
|
|
11
|
+
|
|
12
|
+
<!-- Linear progress (indeterminate/loading) -->
|
|
13
|
+
<rel-progress mode="linear-indeterminate"></rel-progress>
|
|
14
|
+
|
|
15
|
+
<!-- Circular progress (determinate) -->
|
|
16
|
+
<rel-progress mode="circular-determinate" value="75"></rel-progress>
|
|
17
|
+
|
|
18
|
+
<!-- Circular progress (indeterminate/spinner) -->
|
|
19
|
+
<rel-progress mode="circular-indeterminate"></rel-progress>
|
|
20
|
+
|
|
21
|
+
<!-- Dynamic progress update -->
|
|
22
|
+
<rel-progress id="uploadProgress" mode="linear-determinate" value="0"></rel-progress>
|
|
23
|
+
<script>
|
|
24
|
+
const progress = document.getElementById('uploadProgress');
|
|
25
|
+
let value = 0;
|
|
26
|
+
const interval = setInterval(() => {
|
|
27
|
+
value += 10;
|
|
28
|
+
progress.value = value;
|
|
29
|
+
if (value >= 100) clearInterval(interval);
|
|
30
|
+
}, 500);
|
|
31
|
+
</script>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
### Attributes/Properties
|
|
37
|
+
| Name | Type | Default | Description |
|
|
38
|
+
|------|------|---------|-------------|
|
|
39
|
+
| `mode` (reflected) | `"linear-determinate" \| "linear-indeterminate" \| "circular-determinate" \| "circular-indeterminate"` | `"linear-determinate"` | Progress indicator type |
|
|
40
|
+
| `value` | `number` | `0` | Progress value (0-100) for determinate modes (ignored in indeterminate modes) |
|
|
41
|
+
|
|
42
|
+
## Events
|
|
43
|
+
None.
|
|
44
|
+
|
|
45
|
+
## Slots
|
|
46
|
+
Default slot not used.
|
|
47
|
+
|
|
48
|
+
## CSS Parts
|
|
49
|
+
None exposed.
|
|
50
|
+
|
|
51
|
+
## Tokens / CSS Variables
|
|
52
|
+
|
|
53
|
+
### Semantic Tokens
|
|
54
|
+
- `--rel-fontFamilies-primary` - Font family
|
|
55
|
+
|
|
56
|
+
### Color Tokens
|
|
57
|
+
- `--rel-colors-gray-border` - Track/background color
|
|
58
|
+
- `--rel-colors-black` - Active progress bar color
|
|
59
|
+
|
|
60
|
+
### Border Tokens
|
|
61
|
+
- `--rel-borders-radius-border-radius-200` - Linear track border radius
|
|
62
|
+
|
|
63
|
+
## Accessibility
|
|
64
|
+
- Determinate modes use `role="progressbar"` with `aria-valuenow`, `aria-valuemin="0"`, `aria-valuemax="100"`
|
|
65
|
+
- Indeterminate modes use `role="progressbar"` without value attributes
|
|
66
|
+
- Respects `prefers-reduced-motion: reduce` by disabling animations
|
|
67
|
+
|
|
68
|
+
### Keyboard Interactions
|
|
69
|
+
- None (not focusable)
|
|
70
|
+
|
|
71
|
+
### Focus Behavior
|
|
72
|
+
- Not focusable
|
|
73
|
+
|
|
74
|
+
## Notes / Edge Cases
|
|
75
|
+
- **Linear modes**:
|
|
76
|
+
- Height: `6px`
|
|
77
|
+
- Track uses gray border color
|
|
78
|
+
- Bar uses black color
|
|
79
|
+
- Border radius: `--rel-borders-radius-border-radius-200`
|
|
80
|
+
- **Determinate**: bar width transitions smoothly (0.3s ease)
|
|
81
|
+
- **Indeterminate**: 30% width bar slides left-to-right (1.5s infinite loop)
|
|
82
|
+
|
|
83
|
+
- **Circular modes**:
|
|
84
|
+
- Size: `48px × 48px` (viewBox varies by mode)
|
|
85
|
+
- Stroke width: `3px` (determinate) or `4px` (indeterminate)
|
|
86
|
+
- Stroke linecap: `round`
|
|
87
|
+
- **Determinate**:
|
|
88
|
+
- Uses `stroke-dasharray` and `stroke-dashoffset` to animate progress
|
|
89
|
+
- Circle radius: `15.9155`, circumference: `~100`
|
|
90
|
+
- Rotated -90° to start at top
|
|
91
|
+
- Progress transition: `0.35s`
|
|
92
|
+
- **Indeterminate (spinner)**:
|
|
93
|
+
- Rotates continuously (2s linear infinite)
|
|
94
|
+
- Stroke dash animates (1.5s ease-in-out infinite)
|
|
95
|
+
|
|
96
|
+
- **Value clamping**: Values are automatically clamped to 0-100 range via `safeValue` getter
|
|
97
|
+
|
|
98
|
+
- **Motion preferences**:
|
|
99
|
+
- When `prefers-reduced-motion: reduce`:
|
|
100
|
+
- Linear determinate: no transition
|
|
101
|
+
- Linear indeterminate: no animation, shows full-width bar at 50% opacity
|
|
102
|
+
- Circular determinate: no transition
|
|
103
|
+
- Circular indeterminate: no rotation/dash animation, shows static partial arc
|
|
104
|
+
|
|
105
|
+
- **Display modes**:
|
|
106
|
+
- Linear modes: `display: block` (full width)
|
|
107
|
+
- Circular modes: `display: inline-block` (sized to content)
|
|
108
|
+
|
|
109
|
+
- The component automatically switches render methods based on `mode` property
|
|
110
|
+
- Indeterminate modes ignore the `value` property
|
|
@@ -21,7 +21,7 @@ export class RelProgress extends LitElement {
|
|
|
21
21
|
static styles = css`
|
|
22
22
|
:host {
|
|
23
23
|
display: block;
|
|
24
|
-
font-family: var(--rel-fontFamilies-primary);
|
|
24
|
+
font-family: var(--rel-fontFamilies-primary, sans-serif);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
:host([mode*="circular"]) {
|
|
@@ -30,8 +30,8 @@ export class RelProgress extends LitElement {
|
|
|
30
30
|
|
|
31
31
|
/* Linear */
|
|
32
32
|
.linear-track {
|
|
33
|
-
background: var(--rel-colors-gray-border);
|
|
34
|
-
border-radius: var(--rel-borders-radius-border-radius-200);
|
|
33
|
+
background: var(--rel-colors-gray-border, #ddd);
|
|
34
|
+
border-radius: var(--rel-borders-radius-border-radius-200, 8px);
|
|
35
35
|
height: 6px;
|
|
36
36
|
overflow: hidden;
|
|
37
37
|
width: 100%;
|
|
@@ -118,6 +118,32 @@ export class RelProgress extends LitElement {
|
|
|
118
118
|
stroke-dashoffset: -124;
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
|
|
122
|
+
@media (prefers-reduced-motion: reduce) {
|
|
123
|
+
.linear-bar {
|
|
124
|
+
transition: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.linear-bar.indeterminate {
|
|
128
|
+
animation: none;
|
|
129
|
+
width: 100%;
|
|
130
|
+
opacity: 0.5;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.spinner {
|
|
134
|
+
animation: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.spinner-path {
|
|
138
|
+
animation: none;
|
|
139
|
+
stroke-dasharray: 90, 150;
|
|
140
|
+
stroke-dashoffset: -35;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.circular-progress {
|
|
144
|
+
transition: none;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
121
147
|
`;
|
|
122
148
|
|
|
123
149
|
private get safeValue() {
|
|
@@ -126,7 +152,8 @@ export class RelProgress extends LitElement {
|
|
|
126
152
|
|
|
127
153
|
private renderLinearDeterminate() {
|
|
128
154
|
return html`
|
|
129
|
-
<div class="linear-track"
|
|
155
|
+
<div class="linear-track" role="progressbar"
|
|
156
|
+
aria-valuenow="${this.safeValue}" aria-valuemin="0" aria-valuemax="100">
|
|
130
157
|
<div class="linear-bar" style="width: ${this.safeValue}%"></div>
|
|
131
158
|
</div>
|
|
132
159
|
`;
|
|
@@ -134,7 +161,7 @@ export class RelProgress extends LitElement {
|
|
|
134
161
|
|
|
135
162
|
private renderLinearIndeterminate() {
|
|
136
163
|
return html`
|
|
137
|
-
<div class="linear-track">
|
|
164
|
+
<div class="linear-track" role="progressbar">
|
|
138
165
|
<div class="linear-bar indeterminate"></div>
|
|
139
166
|
</div>
|
|
140
167
|
`;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# rel-radio
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
A radio button group component that allows users to select a single option from a list. Supports individual option states (disabled, error), supporting text, and comprehensive styling with design tokens.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
```html
|
|
8
|
+
<!-- Basic example -->
|
|
9
|
+
<rel-radio id="colorRadio" name="color-group" selectedValue="blue"></rel-radio>
|
|
10
|
+
<script>
|
|
11
|
+
const radio = document.getElementById('colorRadio');
|
|
12
|
+
radio.options = [
|
|
13
|
+
{ value: 'red', label: 'Red' },
|
|
14
|
+
{ value: 'blue', label: 'Blue' },
|
|
15
|
+
{ value: 'green', label: 'Green' }
|
|
16
|
+
];
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<!-- Example with supporting text and states -->
|
|
20
|
+
<rel-radio id="planRadio" name="plan-group"></rel-radio>
|
|
21
|
+
<script>
|
|
22
|
+
const planRadio = document.getElementById('planRadio');
|
|
23
|
+
planRadio.options = [
|
|
24
|
+
{ value: 'basic', label: 'Basic Plan', supportingText: '$9.99/month' },
|
|
25
|
+
{ value: 'pro', label: 'Pro Plan', supportingText: '$19.99/month' },
|
|
26
|
+
{ value: 'enterprise', label: 'Enterprise', supportingText: 'Contact sales', disabled: true }
|
|
27
|
+
];
|
|
28
|
+
planRadio.selectedValue = 'basic';
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<!-- Listen for changes -->
|
|
32
|
+
<rel-radio id="preferenceRadio"></rel-radio>
|
|
33
|
+
<script>
|
|
34
|
+
const prefRadio = document.getElementById('preferenceRadio');
|
|
35
|
+
prefRadio.options = [
|
|
36
|
+
{ value: 'email', label: 'Email' },
|
|
37
|
+
{ value: 'sms', label: 'SMS' }
|
|
38
|
+
];
|
|
39
|
+
prefRadio.addEventListener('changed', (e) => {
|
|
40
|
+
console.log('Selected:', prefRadio.selectedValue);
|
|
41
|
+
});
|
|
42
|
+
</script>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## API
|
|
46
|
+
|
|
47
|
+
### Attributes/Properties
|
|
48
|
+
| Name | Type | Default | Description |
|
|
49
|
+
|------|------|---------|-------------|
|
|
50
|
+
| `options` | `RadioOption[]` | `[]` | Array of radio options (see RadioOption interface below) |
|
|
51
|
+
| `name` | `string` | `"radio-group"` | Name attribute for the radio group (for form grouping) |
|
|
52
|
+
| `selectedValue` | `string \| null` | `null` | Currently selected option value (reflected attribute) |
|
|
53
|
+
| `disabled` | `boolean` | `false` | Disables the entire radio group |
|
|
54
|
+
|
|
55
|
+
### RadioOption Interface
|
|
56
|
+
```typescript
|
|
57
|
+
interface RadioOption {
|
|
58
|
+
value: string; // Unique value for this option
|
|
59
|
+
label: string; // Display label
|
|
60
|
+
supportingText?: string; // Optional helper text below the label
|
|
61
|
+
disabled?: boolean; // Disable individual option
|
|
62
|
+
error?: boolean; // Show error state for individual option
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Events
|
|
67
|
+
| Event | Detail | Description |
|
|
68
|
+
|-------|--------|-------------|
|
|
69
|
+
| `changed` | None (read `selectedValue` property) | Fired when the selected option changes |
|
|
70
|
+
|
|
71
|
+
## Slots
|
|
72
|
+
Default slot not used.
|
|
73
|
+
|
|
74
|
+
## CSS Parts
|
|
75
|
+
None exposed.
|
|
76
|
+
|
|
77
|
+
## Tokens / CSS Variables
|
|
78
|
+
|
|
79
|
+
### Semantic Tokens
|
|
80
|
+
- `--rel-fontFamilies-primary` - Font family
|
|
81
|
+
- `--rel-fontSizes-sm`, `--rel-fontSizes-xs` - Font sizes for label and supporting text
|
|
82
|
+
- `--rel-fontWeights-medium` - Font weight for labels
|
|
83
|
+
|
|
84
|
+
### Spacing Tokens
|
|
85
|
+
- `--rel-spacing-space-xs` - Gap between radio options
|
|
86
|
+
- `--rel-spacing-space-3xs` - Gap within each option (label and supporting text)
|
|
87
|
+
- `--rel-spacing-space-2xs` - Gap between radio circle and label
|
|
88
|
+
|
|
89
|
+
### Color Tokens
|
|
90
|
+
- `--rel-text-heading` - Radio button border and checked dot color
|
|
91
|
+
- `--rel-text-primary` - Label text color
|
|
92
|
+
- `--rel-text-placeholder` - Supporting text color
|
|
93
|
+
- `--rel-border-info` - Focus outline color
|
|
94
|
+
- `--rel-colors-red-secondary` - Error state border and dot color
|
|
95
|
+
|
|
96
|
+
## Accessibility
|
|
97
|
+
- Uses visually hidden native radio inputs for proper screen reader support
|
|
98
|
+
- Custom visual radio circle with proper focus indicators
|
|
99
|
+
- Focus visible state with outline offset for keyboard navigation
|
|
100
|
+
- Label click targets the radio for larger interaction area
|
|
101
|
+
- Disabled state communicated through reduced opacity and cursor change
|
|
102
|
+
- Proper `name` grouping ensures only one option can be selected
|
|
103
|
+
|
|
104
|
+
### Keyboard Interactions
|
|
105
|
+
- `Tab` - Move focus to/from radio group
|
|
106
|
+
- `Arrow Up/Down` - Navigate between radio options
|
|
107
|
+
- `Space` - Select focused option
|
|
108
|
+
|
|
109
|
+
### Focus Behavior
|
|
110
|
+
- Focus outline appears with offset around the custom radio circle
|
|
111
|
+
- Uses info border color for high contrast
|
|
112
|
+
|
|
113
|
+
## Notes / Edge Cases
|
|
114
|
+
- The `selectedValue` property is reflected as an attribute and can be set programmatically or via HTML
|
|
115
|
+
- When `disabled` is true on the component, all options are disabled regardless of individual option settings
|
|
116
|
+
- Individual options can be disabled via the `disabled` property in the `RadioOption` object
|
|
117
|
+
- The `error` property on `RadioOption` changes the radio button color to red but does not prevent selection
|
|
118
|
+
- Supporting text is optional and appears below the label with left margin alignment
|
|
119
|
+
- Native radio inputs are visually hidden but maintain all accessibility features
|
|
120
|
+
- The `name` attribute groups radios together for form submission and mutual exclusion
|