@reliability-design/web 1.4.5 → 1.4.6
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-alert/rel-alert.ts +1 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
- package/dist/components/rel-badge/rel-badge.ts +6 -1
- package/dist/components/rel-calendar/rel-calendar.ts +64 -18
- package/dist/components/rel-carousel/rel-carousel.ts +25 -2
- package/dist/components/rel-chip/rel-chip.ts +31 -20
- package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
- package/dist/components/rel-date-picker/date-parse.ts +178 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +529 -90
- package/dist/components/rel-drawer/rel-drawer.ts +6 -3
- package/dist/components/rel-menu/rel-menu.ts +6 -0
- package/dist/components/rel-otp/rel-otp.ts +12 -2
- package/dist/components/rel-pagination/rel-pagination.ts +13 -0
- package/dist/components/rel-popover/rel-popover.ts +33 -2
- package/dist/components/rel-progress/rel-progress.ts +13 -4
- package/dist/components/rel-radio/README.md +6 -3
- package/dist/components/rel-radio/rel-radio.ts +236 -60
- package/dist/components/rel-range-picker/rel-range-picker.ts +452 -95
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +5 -1
- package/dist/components/rel-select/rel-select.ts +44 -13
- package/dist/components/rel-sheet/rel-sheet.ts +163 -18
- package/dist/components/rel-sidebar/rel-sidebar.ts +7 -1
- package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
- package/dist/components/rel-slider/rel-slider.ts +6 -3
- package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
- package/dist/components/rel-stepper/rel-stepper.ts +3 -1
- package/dist/components/rel-switch/rel-switch.ts +3 -1
- package/dist/components/rel-textfield/rel-textfield.ts +263 -75
- package/dist/components/rel-time-picker/rel-time-picker.ts +450 -94
- package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
- package/dist/components/rel-time-picker/time-parse.ts +85 -0
- package/dist/components/rel-toast/rel-toast.ts +4 -2
- package/dist/components/rel-tooltip/README.md +13 -5
- package/dist/components/rel-tooltip/rel-tooltip.ts +3 -0
- package/dist/components/rel-upload/rel-upload.ts +25 -0
- package/dist/index.d.mts +241 -11
- package/dist/index.d.ts +241 -11
- package/dist/index.js +2573 -598
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2614 -640
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, html, css } from "lit";
|
|
1
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property } from "lit/decorators.js";
|
|
3
3
|
|
|
4
4
|
export type SegmentSize = "small" | "medium";
|
|
@@ -20,6 +20,9 @@ export class RelSegmentedButton extends LitElement {
|
|
|
20
20
|
|
|
21
21
|
@property({ type: String }) align: SegmentAlign = "left";
|
|
22
22
|
|
|
23
|
+
/** Accessible name for the radiogroup (it renders no visible group label). */
|
|
24
|
+
@property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
|
|
25
|
+
|
|
23
26
|
static styles = css`
|
|
24
27
|
:host {
|
|
25
28
|
color: var(--rel-text-default);
|
|
@@ -204,6 +207,7 @@ export class RelSegmentedButton extends LitElement {
|
|
|
204
207
|
<div
|
|
205
208
|
part="base" class="${containerClasses}"
|
|
206
209
|
role="radiogroup"
|
|
210
|
+
aria-label="${this.fieldAriaLabel ?? nothing}"
|
|
207
211
|
@keydown="${this.onKeyDown}"
|
|
208
212
|
>
|
|
209
213
|
${this.options.map((option) => this.renderSegment(option, rovingValue))}
|
|
@@ -5,6 +5,7 @@ import "@reliability-design/icons/rel-icon";
|
|
|
5
5
|
import "../rel-chip/rel-chip.js";
|
|
6
6
|
import "../rel-checkbox/rel-checkbox.js";
|
|
7
7
|
import type { FieldLabelStyle } from "../rel-textfield/rel-textfield.js";
|
|
8
|
+
import { announce } from "../../utils/announce";
|
|
8
9
|
|
|
9
10
|
export type SelectState = "default" | "error" | "disabled";
|
|
10
11
|
export type SelectSize = "small" | "medium" | "large";
|
|
@@ -731,13 +732,15 @@ export class RelSelect extends LitElement {
|
|
|
731
732
|
}
|
|
732
733
|
|
|
733
734
|
private addGlobalListeners() {
|
|
734
|
-
|
|
735
|
+
// Capture phase so a pointerdown that another component stops in its own
|
|
736
|
+
// capture listener (e.g. a rel-popover trigger) still closes this listbox.
|
|
737
|
+
document.addEventListener("pointerdown", this.onOutsidePointerDown, true);
|
|
735
738
|
window.addEventListener("resize", this.updatePosition);
|
|
736
739
|
window.addEventListener("scroll", this.onPageScroll, true);
|
|
737
740
|
}
|
|
738
741
|
|
|
739
742
|
private removeGlobalListeners() {
|
|
740
|
-
document.removeEventListener("pointerdown", this.onOutsidePointerDown);
|
|
743
|
+
document.removeEventListener("pointerdown", this.onOutsidePointerDown, true);
|
|
741
744
|
window.removeEventListener("resize", this.updatePosition);
|
|
742
745
|
window.removeEventListener("scroll", this.onPageScroll, true);
|
|
743
746
|
}
|
|
@@ -882,6 +885,7 @@ export class RelSelect extends LitElement {
|
|
|
882
885
|
idx >= 0
|
|
883
886
|
? this.selectedValues.filter((v) => v !== option.value)
|
|
884
887
|
: [...this.selectedValues, option.value];
|
|
888
|
+
announce(`${option.label} ${idx >= 0 ? "deselected" : "selected"}`);
|
|
885
889
|
if (this.state === "error") this.state = "default";
|
|
886
890
|
this.emitChange();
|
|
887
891
|
}
|
|
@@ -896,6 +900,7 @@ export class RelSelect extends LitElement {
|
|
|
896
900
|
const toAdd = filtered.map((o) => o.value).filter((v) => !this.selectedValues.includes(v));
|
|
897
901
|
this.selectedValues = [...this.selectedValues, ...toAdd];
|
|
898
902
|
}
|
|
903
|
+
announce(status === "checked" ? "All options deselected" : "All options selected");
|
|
899
904
|
if (this.state === "error") this.state = "default";
|
|
900
905
|
this.emitChange();
|
|
901
906
|
}
|
|
@@ -903,6 +908,8 @@ export class RelSelect extends LitElement {
|
|
|
903
908
|
private removeChip(value: string) {
|
|
904
909
|
if (this.state === "disabled") return;
|
|
905
910
|
this.selectedValues = this.selectedValues.filter((v) => v !== value);
|
|
911
|
+
const opt = this.normalizedOptions.find((o) => o.value === value);
|
|
912
|
+
announce(`${opt?.label ?? value} removed`);
|
|
906
913
|
this.emitChange();
|
|
907
914
|
}
|
|
908
915
|
|
|
@@ -941,8 +948,14 @@ export class RelSelect extends LitElement {
|
|
|
941
948
|
|
|
942
949
|
private onSearchInput = (e: InputEvent) => {
|
|
943
950
|
this.searchValue = (e.target as HTMLInputElement).value;
|
|
944
|
-
|
|
951
|
+
const count = this.filteredOptions.length;
|
|
952
|
+
this.highlightedIndex = count > 0 ? 0 : -1;
|
|
945
953
|
this.selectAllHighlighted = false;
|
|
954
|
+
if (count === 0) {
|
|
955
|
+
announce(this.options.length === 0 ? "No options available" : "No matches found");
|
|
956
|
+
} else {
|
|
957
|
+
announce(`${count} option${count === 1 ? "" : "s"} available`);
|
|
958
|
+
}
|
|
946
959
|
requestAnimationFrame(() => this.updatePosition());
|
|
947
960
|
};
|
|
948
961
|
|
|
@@ -1186,6 +1199,17 @@ export class RelSelect extends LitElement {
|
|
|
1186
1199
|
|
|
1187
1200
|
const selectedSingle = opts.find((o) => o.value === this.selected);
|
|
1188
1201
|
|
|
1202
|
+
const baseName = accessibleName || this.placeholder;
|
|
1203
|
+
let triggerLabel = baseName;
|
|
1204
|
+
if (this.multiple && this.selectedValues.length > 0) {
|
|
1205
|
+
const labels = this.selectedValues.map((v) => opts.find((o) => o.value === v)?.label ?? v);
|
|
1206
|
+
const shown = labels.slice(0, 3);
|
|
1207
|
+
const more = labels.length - shown.length;
|
|
1208
|
+
triggerLabel = `${baseName}, ${shown.join(", ")}${more > 0 ? `, plus ${more} more` : ""}`;
|
|
1209
|
+
} else if (!this.multiple && selectedSingle) {
|
|
1210
|
+
triggerLabel = `${baseName}, ${selectedSingle.label}`;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1189
1213
|
return html`
|
|
1190
1214
|
<div class="${wrapperClasses}">
|
|
1191
1215
|
${labelHidden
|
|
@@ -1209,9 +1233,9 @@ export class RelSelect extends LitElement {
|
|
|
1209
1233
|
aria-haspopup="listbox"
|
|
1210
1234
|
aria-expanded="${this.open}"
|
|
1211
1235
|
aria-controls="${listboxId}"
|
|
1212
|
-
aria-activedescendant="${activeOptionId}"
|
|
1236
|
+
aria-activedescendant="${this.noSearch ? activeOptionId : nothing}"
|
|
1213
1237
|
aria-invalid="${this.state === "error"}"
|
|
1214
|
-
aria-label="${this.fieldAriaLabelledby ? nothing :
|
|
1238
|
+
aria-label="${this.fieldAriaLabelledby ? nothing : triggerLabel}"
|
|
1215
1239
|
aria-labelledby="${this.fieldAriaLabelledby ?? nothing}"
|
|
1216
1240
|
?disabled="${isDisabled}"
|
|
1217
1241
|
@click="${this.onTriggerClick}"
|
|
@@ -1271,6 +1295,11 @@ export class RelSelect extends LitElement {
|
|
|
1271
1295
|
<input
|
|
1272
1296
|
part="input" class="search-input"
|
|
1273
1297
|
type="text"
|
|
1298
|
+
role="combobox"
|
|
1299
|
+
aria-expanded="${this.open}"
|
|
1300
|
+
aria-controls="${listboxId}"
|
|
1301
|
+
aria-activedescendant="${activeOptionId}"
|
|
1302
|
+
aria-label="${accessibleName || this.placeholder}"
|
|
1274
1303
|
placeholder="${this.searchPlaceholder}"
|
|
1275
1304
|
.value="${this.searchValue}"
|
|
1276
1305
|
@input="${this.onSearchInput}"
|
|
@@ -1285,19 +1314,20 @@ export class RelSelect extends LitElement {
|
|
|
1285
1314
|
class="select-all-row ${this.selectAllHighlighted ? "highlighted" : ""}"
|
|
1286
1315
|
role="option"
|
|
1287
1316
|
aria-selected="${this.selectAllStatus === "checked"}"
|
|
1317
|
+
aria-label="${this.selectAllLabel}"
|
|
1288
1318
|
@pointerenter="${() => {
|
|
1289
1319
|
this.selectAllHighlighted = true;
|
|
1290
1320
|
this.highlightedIndex = -1;
|
|
1291
1321
|
}}"
|
|
1292
1322
|
@pointerleave="${() => (this.selectAllHighlighted = false)}"
|
|
1323
|
+
@mousedown="${(e: Event) => e.preventDefault()}"
|
|
1324
|
+
@click="${() => this.toggleSelectAll()}"
|
|
1293
1325
|
>
|
|
1294
1326
|
<rel-checkbox
|
|
1327
|
+
inert
|
|
1328
|
+
aria-hidden="true"
|
|
1295
1329
|
label="${this.selectAllLabel}"
|
|
1296
1330
|
status="${this.selectAllStatus}"
|
|
1297
|
-
@rel-change=${(e: Event) => {
|
|
1298
|
-
e.stopPropagation();
|
|
1299
|
-
this.toggleSelectAll();
|
|
1300
|
-
}}
|
|
1301
1331
|
></rel-checkbox>
|
|
1302
1332
|
</div>`
|
|
1303
1333
|
: ""}
|
|
@@ -1320,20 +1350,21 @@ export class RelSelect extends LitElement {
|
|
|
1320
1350
|
class="checkbox-option ${index === this.highlightedIndex ? "highlighted" : ""}"
|
|
1321
1351
|
role="option"
|
|
1322
1352
|
aria-selected="${this.selectedValues.includes(option.value)}"
|
|
1353
|
+
aria-label="${option.label}"
|
|
1323
1354
|
@pointerenter="${() => {
|
|
1324
1355
|
this.highlightedIndex = index;
|
|
1325
1356
|
this.selectAllHighlighted = false;
|
|
1326
1357
|
}}"
|
|
1358
|
+
@mousedown="${(e: Event) => e.preventDefault()}"
|
|
1359
|
+
@click="${() => this.toggleMultiOption(option)}"
|
|
1327
1360
|
>
|
|
1328
1361
|
<rel-checkbox
|
|
1362
|
+
inert
|
|
1363
|
+
aria-hidden="true"
|
|
1329
1364
|
label="${option.label}"
|
|
1330
1365
|
icon="${option.icon}"
|
|
1331
1366
|
supportingtext="${option.supportingText}"
|
|
1332
1367
|
status="${this.selectedValues.includes(option.value) ? "checked" : "unchecked"}"
|
|
1333
|
-
@rel-change=${(e: Event) => {
|
|
1334
|
-
e.stopPropagation();
|
|
1335
|
-
this.toggleMultiOption(option);
|
|
1336
|
-
}}
|
|
1337
1368
|
></rel-checkbox>
|
|
1338
1369
|
</div>
|
|
1339
1370
|
`
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { LitElement, html, css } from "lit";
|
|
1
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property, query } from "lit/decorators.js";
|
|
3
3
|
import "@reliability-design/icons/rel-icon";
|
|
4
|
+
import "../rel-progress/rel-progress.js";
|
|
4
5
|
import { getDeepActiveElement, getFocusableElements, trapTabKey } from "../../utils/focus.js";
|
|
5
6
|
|
|
6
7
|
export type ModalSide = "top" | "bottom" | "center";
|
|
8
|
+
export type ModalSize = "sm" | "md" | "lg" | "xl" | "fullscreen";
|
|
7
9
|
|
|
8
10
|
@customElement("rel-modal")
|
|
9
11
|
export class RelModal extends LitElement {
|
|
@@ -11,9 +13,37 @@ export class RelModal extends LitElement {
|
|
|
11
13
|
@property({ type: String }) side: ModalSide = "bottom";
|
|
12
14
|
@property({ type: String }) title = "";
|
|
13
15
|
@property({ type: String }) description = "";
|
|
16
|
+
/** Alias for `description`; rendered only when `description` is empty. */
|
|
17
|
+
@property({ type: String }) subtitle = "";
|
|
14
18
|
@property({ type: Boolean }) dismissible = true;
|
|
15
19
|
@property({ type: Boolean }) modal = true;
|
|
16
20
|
|
|
21
|
+
/** Panel width scale. `md` matches the Figma Modal Sheet width; `fullscreen` covers the viewport. */
|
|
22
|
+
@property({ type: String, reflect: true }) size: ModalSize = "md";
|
|
23
|
+
|
|
24
|
+
/* Granular dismissal switches — each is ANDed with the legacy coarse
|
|
25
|
+
`dismissible` switch so existing `dismissible={false}` consumers keep
|
|
26
|
+
their fully-locked behavior. */
|
|
27
|
+
@property({ type: Boolean, attribute: "close-on-backdrop-click" }) closeOnBackdropClick = true;
|
|
28
|
+
@property({ type: Boolean, attribute: "close-on-escape" }) closeOnEscape = true;
|
|
29
|
+
@property({ type: Boolean, attribute: "show-close-button" }) showCloseButton = true;
|
|
30
|
+
|
|
31
|
+
/** Move focus into the panel when opened (modal mode only). */
|
|
32
|
+
@property({ type: Boolean, attribute: "auto-focus" }) autoFocus = true;
|
|
33
|
+
/** Return focus to the opener when closed. */
|
|
34
|
+
@property({ type: Boolean, attribute: "restore-focus" }) restoreFocus = true;
|
|
35
|
+
|
|
36
|
+
/** When false, the body no longer scrolls internally — tall content clips at the panel's max height. */
|
|
37
|
+
@property({ type: Boolean }) scrollable = true;
|
|
38
|
+
|
|
39
|
+
/** Shows a spinner overlay and blocks all interaction inside the panel. */
|
|
40
|
+
@property({ type: Boolean, reflect: true }) loading = false;
|
|
41
|
+
/** Blocks user-initiated closes (X / backdrop / Escape) and footer actions. */
|
|
42
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
43
|
+
|
|
44
|
+
/** Stacking order of the whole modal (backdrop + panel). */
|
|
45
|
+
@property({ type: Number, attribute: "z-index" }) zIndex = 1000;
|
|
46
|
+
|
|
17
47
|
/**
|
|
18
48
|
* Figma "Breakpoint" axis. responsive (default) collapses to a full-width
|
|
19
49
|
* bottom sheet below 480px; web keeps the fixed desktop panel at every size.
|
|
@@ -86,16 +116,30 @@ export class RelModal extends LitElement {
|
|
|
86
116
|
overflow: hidden;
|
|
87
117
|
outline: none;
|
|
88
118
|
font-family: var(--rel-fonts-font-family);
|
|
89
|
-
width
|
|
119
|
+
/* md is the Figma Modal Sheet Breakpoint=Web width (708px cap). */
|
|
120
|
+
--modal-width: clamp(360px, 49vw, 708px);
|
|
121
|
+
width: var(--modal-width);
|
|
90
122
|
left: 50%;
|
|
91
123
|
transition: transform var(--modal-transition-duration) var(--modal-transition-timing),
|
|
92
124
|
opacity var(--modal-transition-duration) var(--modal-transition-timing);
|
|
93
125
|
}
|
|
94
126
|
|
|
127
|
+
.panel[data-size="sm"] {
|
|
128
|
+
--modal-width: min(400px, calc(100vw - 32px));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.panel[data-size="lg"] {
|
|
132
|
+
--modal-width: min(900px, calc(100vw - 48px));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.panel[data-size="xl"] {
|
|
136
|
+
--modal-width: min(1140px, calc(100vw - 48px));
|
|
137
|
+
}
|
|
138
|
+
|
|
95
139
|
.panel[data-side="bottom"] {
|
|
96
140
|
bottom: 48px;
|
|
97
141
|
left: 50%;
|
|
98
|
-
width:
|
|
142
|
+
width: var(--modal-width);
|
|
99
143
|
max-height: calc(100vh - 96px);
|
|
100
144
|
border-radius: var(--rel-border-radius-sm);
|
|
101
145
|
transform: translateX(-50%) translateY(calc(100% + 48px));
|
|
@@ -108,7 +152,7 @@ export class RelModal extends LitElement {
|
|
|
108
152
|
.panel[data-side="top"] {
|
|
109
153
|
top: 48px;
|
|
110
154
|
left: 50%;
|
|
111
|
-
width:
|
|
155
|
+
width: var(--modal-width);
|
|
112
156
|
max-height: calc(100vh - 96px);
|
|
113
157
|
border-radius: var(--rel-border-radius-sm);
|
|
114
158
|
transform: translateX(-50%) translateY(calc(-100% - 48px));
|
|
@@ -121,7 +165,7 @@ export class RelModal extends LitElement {
|
|
|
121
165
|
.panel[data-side="center"] {
|
|
122
166
|
top: 50%;
|
|
123
167
|
left: 50%;
|
|
124
|
-
width:
|
|
168
|
+
width: var(--modal-width);
|
|
125
169
|
max-height: calc(100vh - 96px);
|
|
126
170
|
border-radius: var(--rel-border-radius-sm);
|
|
127
171
|
transform: translate(-50%, -50%) scale(0.96);
|
|
@@ -159,6 +203,50 @@ export class RelModal extends LitElement {
|
|
|
159
203
|
}
|
|
160
204
|
}
|
|
161
205
|
|
|
206
|
+
/* size="fullscreen" — edge-to-edge at every side value; declared after the
|
|
207
|
+
side and responsive-collapse rules so it wins their specificity ties. */
|
|
208
|
+
.panel[data-size="fullscreen"],
|
|
209
|
+
.panel[data-size="fullscreen"][data-side="top"],
|
|
210
|
+
.panel[data-size="fullscreen"][data-side="center"],
|
|
211
|
+
.panel[data-size="fullscreen"][data-side="bottom"] {
|
|
212
|
+
top: 0;
|
|
213
|
+
bottom: 0;
|
|
214
|
+
left: 0;
|
|
215
|
+
right: 0;
|
|
216
|
+
width: 100%;
|
|
217
|
+
height: 100%;
|
|
218
|
+
max-height: none;
|
|
219
|
+
border-radius: 0;
|
|
220
|
+
transform: translateY(16px);
|
|
221
|
+
opacity: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.modal-container.open .panel[data-size="fullscreen"],
|
|
225
|
+
.modal-container.open .panel[data-size="fullscreen"][data-side="top"],
|
|
226
|
+
.modal-container.open .panel[data-size="fullscreen"][data-side="center"],
|
|
227
|
+
.modal-container.open .panel[data-size="fullscreen"][data-side="bottom"] {
|
|
228
|
+
transform: translateY(0);
|
|
229
|
+
opacity: 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@media (max-width: 480px) {
|
|
233
|
+
:host([breakpoint="responsive"]) .panel[data-size="fullscreen"] {
|
|
234
|
+
top: 0;
|
|
235
|
+
bottom: 0;
|
|
236
|
+
left: 0;
|
|
237
|
+
right: 0;
|
|
238
|
+
width: 100%;
|
|
239
|
+
height: 100%;
|
|
240
|
+
max-height: none;
|
|
241
|
+
border-radius: 0;
|
|
242
|
+
transform: translateY(16px);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
:host([breakpoint="responsive"]) .modal-container.open .panel[data-size="fullscreen"] {
|
|
246
|
+
transform: translateY(0);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
162
250
|
.header {
|
|
163
251
|
display: flex;
|
|
164
252
|
align-items: flex-start;
|
|
@@ -241,12 +329,51 @@ export class RelModal extends LitElement {
|
|
|
241
329
|
outline-offset: 2px;
|
|
242
330
|
}
|
|
243
331
|
|
|
332
|
+
.close-button:disabled {
|
|
333
|
+
cursor: not-allowed;
|
|
334
|
+
opacity: 0.5;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.close-button:disabled:hover {
|
|
338
|
+
background: transparent;
|
|
339
|
+
}
|
|
340
|
+
|
|
244
341
|
.content {
|
|
245
342
|
flex: 1;
|
|
246
343
|
overflow-y: auto;
|
|
247
344
|
padding: var(--rel-spacing-xl);
|
|
248
345
|
}
|
|
249
346
|
|
|
347
|
+
.content.no-scroll {
|
|
348
|
+
overflow-y: visible;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
:host([disabled]) .footer {
|
|
352
|
+
opacity: 0.5;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.loading-overlay {
|
|
356
|
+
position: absolute;
|
|
357
|
+
inset: 0;
|
|
358
|
+
z-index: 10;
|
|
359
|
+
display: flex;
|
|
360
|
+
align-items: center;
|
|
361
|
+
justify-content: center;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Scrim as a pseudo-element so its opacity doesn't dull the spinner. */
|
|
365
|
+
.loading-overlay::before {
|
|
366
|
+
content: "";
|
|
367
|
+
position: absolute;
|
|
368
|
+
inset: 0;
|
|
369
|
+
background: var(--rel-bg-surface);
|
|
370
|
+
opacity: 0.7;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.loading-overlay rel-progress {
|
|
374
|
+
position: relative;
|
|
375
|
+
}
|
|
376
|
+
|
|
250
377
|
.footer {
|
|
251
378
|
display: none;
|
|
252
379
|
}
|
|
@@ -332,7 +459,7 @@ export class RelModal extends LitElement {
|
|
|
332
459
|
|
|
333
460
|
// Move focus into the dialog — before this, a keyboard user had to Tab
|
|
334
461
|
// through the entire page behind the overlay to reach the sheet.
|
|
335
|
-
if (this.modal) {
|
|
462
|
+
if (this.modal && this.autoFocus) {
|
|
336
463
|
this.updateComplete.then(() => {
|
|
337
464
|
const panel = this.panelElement;
|
|
338
465
|
if (!panel) return;
|
|
@@ -353,11 +480,13 @@ export class RelModal extends LitElement {
|
|
|
353
480
|
private handleClose() {
|
|
354
481
|
this.unlockScroll();
|
|
355
482
|
document.removeEventListener("keydown", this.handleKeydown);
|
|
356
|
-
if (this.previousFocus?.focus) {
|
|
483
|
+
if (this.restoreFocus && this.previousFocus?.focus) {
|
|
357
484
|
this.updateComplete.then(() => {
|
|
358
485
|
this.previousFocus?.focus();
|
|
359
486
|
this.previousFocus = null;
|
|
360
487
|
});
|
|
488
|
+
} else {
|
|
489
|
+
this.previousFocus = null;
|
|
361
490
|
}
|
|
362
491
|
this.dispatchEvent(
|
|
363
492
|
new CustomEvent("rel-open-change", {
|
|
@@ -387,7 +516,7 @@ export class RelModal extends LitElement {
|
|
|
387
516
|
|
|
388
517
|
private handleKeydown = (e: KeyboardEvent) => {
|
|
389
518
|
if (!this.open) return;
|
|
390
|
-
if (e.key === "Escape" && this.dismissible) {
|
|
519
|
+
if (e.key === "Escape" && this.dismissible && this.closeOnEscape && !this.disabled) {
|
|
391
520
|
e.preventDefault();
|
|
392
521
|
this.open = false;
|
|
393
522
|
return;
|
|
@@ -399,23 +528,26 @@ export class RelModal extends LitElement {
|
|
|
399
528
|
};
|
|
400
529
|
|
|
401
530
|
private handleOverlayClick = (e: MouseEvent) => {
|
|
402
|
-
if (!this.dismissible) return;
|
|
531
|
+
if (!this.dismissible || !this.closeOnBackdropClick || this.disabled || this.loading) return;
|
|
403
532
|
if (e.target === this.overlayElement) {
|
|
404
533
|
this.open = false;
|
|
405
534
|
}
|
|
406
535
|
};
|
|
407
536
|
|
|
408
537
|
private handleCloseButtonClick() {
|
|
538
|
+
if (this.disabled || this.loading) return;
|
|
409
539
|
this.open = false;
|
|
410
540
|
}
|
|
411
541
|
|
|
412
542
|
render() {
|
|
413
543
|
const hasTitle = !!this.title;
|
|
414
|
-
const
|
|
415
|
-
const
|
|
544
|
+
const effectiveDescription = this.description || this.subtitle;
|
|
545
|
+
const hasDescription = !!effectiveDescription;
|
|
546
|
+
const showCloseX = this.dismissible && this.showCloseButton;
|
|
547
|
+
const showHeader = hasTitle || hasDescription || showCloseX;
|
|
416
548
|
|
|
417
549
|
return html`
|
|
418
|
-
<div class="modal-container ${this.open ? "open" : ""}">
|
|
550
|
+
<div class="modal-container ${this.open ? "open" : ""}" style="z-index: ${this.zIndex}">
|
|
419
551
|
<div
|
|
420
552
|
part="overlay"
|
|
421
553
|
class="overlay ${!this.modal ? 'no-modal' : ''}"
|
|
@@ -425,19 +557,26 @@ export class RelModal extends LitElement {
|
|
|
425
557
|
part="panel"
|
|
426
558
|
class="panel"
|
|
427
559
|
data-side="${this.side}"
|
|
560
|
+
data-size="${this.size}"
|
|
428
561
|
role="${this.modal ? "dialog" : "region"}"
|
|
429
562
|
aria-modal="${this.modal}"
|
|
563
|
+
aria-busy="${this.loading ? "true" : nothing}"
|
|
564
|
+
aria-labelledby="${hasTitle ? "modal-title" : nothing}"
|
|
565
|
+
aria-describedby="${hasDescription ? "modal-description" : nothing}"
|
|
566
|
+
aria-label="${hasTitle ? nothing : "Dialog"}"
|
|
430
567
|
tabindex="-1">
|
|
431
568
|
|
|
432
|
-
<div part="header" class="header ${!showHeader ? "empty" : ""}">
|
|
569
|
+
<div part="header" class="header ${!showHeader ? "empty" : ""}" ?inert=${this.loading}>
|
|
433
570
|
<div class="header-text">
|
|
434
|
-
${hasTitle ? html`<h2 class="title">${this.title}</h2>` : ""}
|
|
435
|
-
${hasDescription ? html`<p class="description">${
|
|
571
|
+
${hasTitle ? html`<h2 class="title" id="modal-title">${this.title}</h2>` : ""}
|
|
572
|
+
${hasDescription ? html`<p class="description" id="modal-description">${effectiveDescription}</p>` : ""}
|
|
573
|
+
<slot name="header"></slot>
|
|
436
574
|
</div>
|
|
437
|
-
${
|
|
575
|
+
${showCloseX
|
|
438
576
|
? html`<button
|
|
439
577
|
part="close-button"
|
|
440
578
|
class="close-button"
|
|
579
|
+
?disabled=${this.disabled}
|
|
441
580
|
@click="${this.handleCloseButtonClick}"
|
|
442
581
|
aria-label="Close">
|
|
443
582
|
<rel-icon name="close" size="sm"></rel-icon>
|
|
@@ -445,13 +584,19 @@ export class RelModal extends LitElement {
|
|
|
445
584
|
: ""}
|
|
446
585
|
</div>
|
|
447
586
|
|
|
448
|
-
<div part="content" class="content">
|
|
587
|
+
<div part="content" class="content ${this.scrollable ? "" : "no-scroll"}" ?inert=${this.loading}>
|
|
449
588
|
<slot></slot>
|
|
450
589
|
</div>
|
|
451
590
|
|
|
452
|
-
<div part="footer" class="footer">
|
|
591
|
+
<div part="footer" class="footer" ?inert=${this.loading || this.disabled}>
|
|
453
592
|
<slot name="footer"></slot>
|
|
454
593
|
</div>
|
|
594
|
+
|
|
595
|
+
${this.loading
|
|
596
|
+
? html`<div part="loading" class="loading-overlay">
|
|
597
|
+
<rel-progress mode="circular-indeterminate" label="Loading"></rel-progress>
|
|
598
|
+
</div>`
|
|
599
|
+
: ""}
|
|
455
600
|
</div>
|
|
456
601
|
</div>
|
|
457
602
|
`;
|
|
@@ -670,6 +670,7 @@ export class RelSidebar extends LitElement {
|
|
|
670
670
|
data-side="${this.side}"
|
|
671
671
|
style="--sidebar-width: ${this.getWidthValue()}"
|
|
672
672
|
role="navigation"
|
|
673
|
+
aria-label="Sidebar"
|
|
673
674
|
aria-hidden="${isOffcanvas && !this.isOpen}"
|
|
674
675
|
tabindex="-1"
|
|
675
676
|
>
|
|
@@ -778,11 +779,16 @@ export class RelSidebarTrigger extends LitElement {
|
|
|
778
779
|
};
|
|
779
780
|
|
|
780
781
|
render() {
|
|
782
|
+
const p = this.provider;
|
|
783
|
+
const expanded =
|
|
784
|
+
p && !p.isMobile && p.collapsible === "icon"
|
|
785
|
+
? !this.isCollapsed
|
|
786
|
+
: this.isOpen;
|
|
781
787
|
return html`
|
|
782
788
|
<button
|
|
783
789
|
@click="${this.handleClick}"
|
|
784
790
|
type="button"
|
|
785
|
-
aria-expanded="${
|
|
791
|
+
aria-expanded="${expanded}"
|
|
786
792
|
aria-label="Toggle sidebar"
|
|
787
793
|
>
|
|
788
794
|
<slot>
|
|
@@ -16,6 +16,15 @@ export class RelSkeleton extends LitElement {
|
|
|
16
16
|
protected override updated(changed: Map<string, unknown>) {
|
|
17
17
|
if (changed.has("width") && this.width) this.style.width = this.width;
|
|
18
18
|
if (changed.has("height") && this.height) this.style.height = this.height;
|
|
19
|
+
if (changed.has("loaded")) {
|
|
20
|
+
if (this.loaded) {
|
|
21
|
+
this.removeAttribute("role");
|
|
22
|
+
this.removeAttribute("aria-label");
|
|
23
|
+
} else {
|
|
24
|
+
this.setAttribute("role", "status");
|
|
25
|
+
this.setAttribute("aria-label", "Loading");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
static styles = css`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LitElement, html, css } from "lit";
|
|
1
|
+
import { LitElement, html, css, nothing } from "lit";
|
|
2
2
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -34,6 +34,8 @@ export class RelSlider extends LitElement {
|
|
|
34
34
|
/** Upper value when type="range" (attribute value-end). */
|
|
35
35
|
@property({ type: Number, attribute: "value-end" }) valueEnd: number = 100;
|
|
36
36
|
|
|
37
|
+
@property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
|
|
38
|
+
|
|
37
39
|
@state() private ticks: number[] = [];
|
|
38
40
|
|
|
39
41
|
private _onChange: (value: number) => void = () => {};
|
|
@@ -619,7 +621,7 @@ export class RelSlider extends LitElement {
|
|
|
619
621
|
<input
|
|
620
622
|
class="slider-input"
|
|
621
623
|
type="range"
|
|
622
|
-
aria-label="
|
|
624
|
+
aria-label="${this.fieldAriaLabel ? `${this.fieldAriaLabel} minimum` : "Minimum value"}"
|
|
623
625
|
.min="${String(this.min)}"
|
|
624
626
|
.max="${String(this.max)}"
|
|
625
627
|
.step="${String(this.step)}"
|
|
@@ -630,7 +632,7 @@ export class RelSlider extends LitElement {
|
|
|
630
632
|
<input
|
|
631
633
|
class="slider-input"
|
|
632
634
|
type="range"
|
|
633
|
-
aria-label="
|
|
635
|
+
aria-label="${this.fieldAriaLabel ? `${this.fieldAriaLabel} maximum` : "Maximum value"}"
|
|
634
636
|
.min="${String(this.min)}"
|
|
635
637
|
.max="${String(this.max)}"
|
|
636
638
|
.step="${String(this.step)}"
|
|
@@ -653,6 +655,7 @@ export class RelSlider extends LitElement {
|
|
|
653
655
|
<input
|
|
654
656
|
class="slider-input"
|
|
655
657
|
type="range"
|
|
658
|
+
aria-label="${this.fieldAriaLabel || nothing}"
|
|
656
659
|
.min="${String(this.min)}"
|
|
657
660
|
.max="${String(this.max)}"
|
|
658
661
|
.step="${this.type === 'discrete' ? String(this.step) : '1'}"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LitElement, html, css } from "lit";
|
|
2
2
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
3
|
import "@reliability-design/icons/rel-icon";
|
|
4
|
+
import { announce } from "../../utils/announce";
|
|
4
5
|
|
|
5
6
|
export type SnackbarLines = "single" | "double";
|
|
6
7
|
export type SnackbarType = "default" | "success" | "error" | "warning";
|
|
@@ -100,19 +101,19 @@ export class RelSnackbar extends LitElement {
|
|
|
100
101
|
min-width: 0;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
.snackbar-message {
|
|
104
|
+
.snackbar-message {
|
|
104
105
|
margin-right: var(--rel-spacing-sm);
|
|
105
|
-
flex: 1;
|
|
106
|
+
flex: 1;
|
|
106
107
|
|
|
107
|
-
font-family: var(--rel-body-md-regular-font-family);
|
|
108
|
+
font-family: var(--rel-body-md-regular-font-family);
|
|
108
109
|
|
|
109
|
-
font-weight: var(--rel-body-md-regular-font-weight);
|
|
110
|
+
font-weight: var(--rel-body-md-regular-font-weight);
|
|
110
111
|
|
|
111
|
-
font-size: var(--rel-body-md-regular-font-size);
|
|
112
|
+
font-size: var(--rel-body-md-regular-font-size);
|
|
112
113
|
|
|
113
|
-
line-height: var(--rel-body-md-regular-line-height);
|
|
114
|
+
line-height: var(--rel-body-md-regular-line-height);
|
|
114
115
|
|
|
115
|
-
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
116
|
+
letter-spacing: var(--rel-body-md-regular-letter-spacing);
|
|
116
117
|
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -137,17 +138,17 @@ export class RelSnackbar extends LitElement {
|
|
|
137
138
|
.snackbar-action {
|
|
138
139
|
background: none;
|
|
139
140
|
border: none;
|
|
140
|
-
color: var(--rel-text-static-light);
|
|
141
|
+
color: var(--rel-text-static-light);
|
|
141
142
|
cursor: pointer;
|
|
142
143
|
padding: 0;
|
|
143
144
|
display: flex;
|
|
144
145
|
align-items: center;
|
|
145
|
-
transition: opacity 0.2s ease;
|
|
146
|
-
font-family: var(--rel-action-md-font-family);
|
|
147
|
-
font-weight: var(--rel-action-md-font-weight);
|
|
148
|
-
font-size: var(--rel-action-md-font-size);
|
|
149
|
-
line-height: var(--rel-action-md-line-height);
|
|
150
|
-
letter-spacing: var(--rel-action-md-letter-spacing);
|
|
146
|
+
transition: opacity 0.2s ease;
|
|
147
|
+
font-family: var(--rel-action-md-font-family);
|
|
148
|
+
font-weight: var(--rel-action-md-font-weight);
|
|
149
|
+
font-size: var(--rel-action-md-font-size);
|
|
150
|
+
line-height: var(--rel-action-md-line-height);
|
|
151
|
+
letter-spacing: var(--rel-action-md-letter-spacing);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
.snackbar-action:hover {
|
|
@@ -232,6 +233,9 @@ export class RelSnackbar extends LitElement {
|
|
|
232
233
|
if ((changed.has("duration") || changed.has("longerAction")) && this.show) {
|
|
233
234
|
this.setAutoDismiss();
|
|
234
235
|
}
|
|
236
|
+
if ((changed.has("show") || changed.has("message")) && this.show) {
|
|
237
|
+
announce(this.message, this.type === "error" ? "assertive" : "polite");
|
|
238
|
+
}
|
|
235
239
|
}
|
|
236
240
|
|
|
237
241
|
disconnectedCallback() {
|
|
@@ -297,7 +301,7 @@ export class RelSnackbar extends LitElement {
|
|
|
297
301
|
.join(" ");
|
|
298
302
|
|
|
299
303
|
return html`
|
|
300
|
-
<div class="${snackbarClasses}">
|
|
304
|
+
<div class="${snackbarClasses}" role="${this.type === "error" ? "alert" : "status"}">
|
|
301
305
|
<div class="snackbar-body">
|
|
302
306
|
${this.type !== "default" ? this.renderIcon() : ""}
|
|
303
307
|
<div part="label" class="snackbar-message">${this.message}</div>
|
|
@@ -313,7 +317,7 @@ export class RelSnackbar extends LitElement {
|
|
|
313
317
|
: ""}
|
|
314
318
|
${this.iconClose
|
|
315
319
|
? html`
|
|
316
|
-
<button part="close-button" class="snackbar-close" @click="${this.dismiss}">
|
|
320
|
+
<button part="close-button" class="snackbar-close" @click="${this.dismiss}" aria-label="Close">
|
|
317
321
|
<rel-icon name="close" size="md"></rel-icon>
|
|
318
322
|
</button>
|
|
319
323
|
`
|