@reliability-design/web 1.4.5 → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +211 -49
- package/dist/components/rel-accordion/README.md +132 -132
- package/dist/components/rel-alert/README.md +145 -141
- package/dist/components/rel-autocomplete/README.md +156 -61
- package/dist/components/rel-avatar/README.md +116 -130
- package/dist/components/rel-badge/README.md +98 -93
- package/dist/components/rel-breadcrumb/README.md +96 -102
- package/dist/components/rel-button/README.md +160 -130
- package/dist/components/rel-calendar/README.md +109 -109
- package/dist/components/rel-card/README.md +75 -94
- package/dist/components/rel-cardmedia/README.md +100 -127
- package/dist/components/rel-carousel/README.md +184 -166
- package/dist/components/rel-checkbox/README.md +93 -75
- package/dist/components/rel-chip/README.md +132 -121
- package/dist/components/rel-container/README.md +107 -99
- package/dist/components/rel-date-picker/README.md +200 -289
- package/dist/components/rel-divider/README.md +78 -100
- package/dist/components/rel-drawer/README.md +187 -200
- package/dist/components/rel-kbd/README.md +141 -151
- package/dist/components/rel-menu/README.md +178 -151
- package/dist/components/rel-menubar/README.md +218 -329
- package/dist/components/rel-otp/README.md +170 -440
- package/dist/components/rel-pagination/README.md +100 -424
- package/dist/components/rel-popover/README.md +203 -568
- package/dist/components/rel-progress/README.md +88 -110
- package/dist/components/rel-radio/README.md +131 -135
- package/dist/components/rel-range-picker/README.md +162 -0
- package/dist/components/rel-resizable/README.md +199 -140
- package/dist/components/rel-segmented-button/README.md +155 -141
- package/dist/components/rel-select/README.md +154 -93
- package/dist/components/rel-sheet/README.md +168 -224
- package/dist/components/rel-skeleton/README.md +103 -0
- package/dist/components/rel-slider/README.md +130 -135
- package/dist/components/rel-snackbar/README.md +162 -134
- package/dist/components/rel-stepper/README.md +158 -160
- package/dist/components/rel-switch/README.md +108 -102
- package/dist/components/rel-tab/README.md +171 -163
- package/dist/components/rel-textfield/README.md +159 -69
- package/dist/components/rel-time-picker/README.md +122 -157
- package/dist/components/rel-toast/README.md +157 -148
- package/dist/components/rel-tooltip/README.md +108 -103
- package/dist/components/rel-upload/README.md +220 -0
- package/dist/index.d.mts +1387 -350
- package/dist/index.d.ts +1387 -350
- package/dist/index.js +5863 -3600
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5864 -3592
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +877 -866
- package/dist/{vars-XNMTSRVN.css → vars-JHTUGNRG.css} +877 -866
- package/package.json +4 -3
- package/dist/components/rel-accordion/rel-accordion.ts +0 -336
- package/dist/components/rel-alert/rel-alert.ts +0 -305
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +0 -689
- package/dist/components/rel-avatar/rel-avatar.ts +0 -142
- package/dist/components/rel-badge/rel-badge.ts +0 -170
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +0 -106
- package/dist/components/rel-button/rel-button.ts +0 -560
- package/dist/components/rel-calendar/index.ts +0 -1
- package/dist/components/rel-calendar/rel-calendar.ts +0 -519
- package/dist/components/rel-card/rel-card.ts +0 -88
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +0 -88
- package/dist/components/rel-carousel/index.ts +0 -1
- package/dist/components/rel-carousel/rel-carousel.ts +0 -700
- package/dist/components/rel-chart/README.md +0 -286
- package/dist/components/rel-chart/index.ts +0 -1
- package/dist/components/rel-chart/rel-chart.ts +0 -754
- package/dist/components/rel-checkbox/rel-checkbox.ts +0 -382
- package/dist/components/rel-chip/rel-chip.ts +0 -472
- package/dist/components/rel-container/rel-container.ts +0 -95
- package/dist/components/rel-date-picker/date-format.test.ts +0 -43
- package/dist/components/rel-date-picker/date-format.ts +0 -47
- package/dist/components/rel-date-picker/index.ts +0 -1
- package/dist/components/rel-date-picker/rel-date-picker.ts +0 -1237
- package/dist/components/rel-divider/rel-divider.ts +0 -54
- package/dist/components/rel-drawer/index.ts +0 -1
- package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -27
- package/dist/components/rel-drawer/rel-drawer.ts +0 -604
- package/dist/components/rel-kbd/rel-kbd.ts +0 -188
- package/dist/components/rel-layout/rel-layout.ts +0 -49
- package/dist/components/rel-menu/rel-menu.ts +0 -535
- package/dist/components/rel-menubar/index.ts +0 -2
- package/dist/components/rel-menubar/rel-menubar-menu.css +0 -3
- package/dist/components/rel-menubar/rel-menubar-menu.ts +0 -17
- package/dist/components/rel-menubar/rel-menubar.ts +0 -777
- package/dist/components/rel-nav-menu/README.md +0 -289
- package/dist/components/rel-nav-menu/index.ts +0 -3
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +0 -243
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +0 -72
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +0 -270
- package/dist/components/rel-navbar/README.md +0 -126
- package/dist/components/rel-navbar/rel-navbar.ts +0 -105
- package/dist/components/rel-otp/index.ts +0 -1
- package/dist/components/rel-otp/rel-otp.ts +0 -517
- package/dist/components/rel-pagination/index.ts +0 -1
- package/dist/components/rel-pagination/rel-pagination.ts +0 -509
- package/dist/components/rel-popover/index.ts +0 -2
- package/dist/components/rel-popover/rel-popover-content.ts +0 -81
- package/dist/components/rel-popover/rel-popover.ts +0 -444
- package/dist/components/rel-progress/rel-progress.ts +0 -225
- package/dist/components/rel-radio/rel-radio.ts +0 -305
- package/dist/components/rel-range-picker/rel-range-picker.ts +0 -1331
- package/dist/components/rel-resizable/index.ts +0 -3
- package/dist/components/rel-resizable/rel-resizable-group.ts +0 -405
- package/dist/components/rel-resizable/rel-resizable-handle.ts +0 -150
- package/dist/components/rel-resizable/rel-resizable-panel.ts +0 -58
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +0 -213
- package/dist/components/rel-select/rel-select.ts +0 -1385
- package/dist/components/rel-sheet/rel-sheet.ts +0 -465
- package/dist/components/rel-sidebar/README.md +0 -209
- package/dist/components/rel-sidebar/rel-sidebar.ts +0 -1185
- package/dist/components/rel-skeleton/rel-skeleton.ts +0 -134
- package/dist/components/rel-slider/rel-slider.ts +0 -670
- package/dist/components/rel-snackbar/rel-snackbar.ts +0 -325
- package/dist/components/rel-stepper/rel-stepper.ts +0 -565
- package/dist/components/rel-switch/rel-switch.ts +0 -197
- package/dist/components/rel-tab/rel-tab.ts +0 -264
- package/dist/components/rel-table/README.md +0 -178
- package/dist/components/rel-table/rel-table-body.ts +0 -22
- package/dist/components/rel-table/rel-table-caption.ts +0 -30
- package/dist/components/rel-table/rel-table-cell.ts +0 -43
- package/dist/components/rel-table/rel-table-footer.ts +0 -24
- package/dist/components/rel-table/rel-table-head.ts +0 -47
- package/dist/components/rel-table/rel-table-header.ts +0 -22
- package/dist/components/rel-table/rel-table-row.ts +0 -37
- package/dist/components/rel-table/rel-table.ts +0 -62
- package/dist/components/rel-textfield/rel-textfield.ts +0 -866
- package/dist/components/rel-time-picker/rel-time-picker.ts +0 -1126
- package/dist/components/rel-toast/rel-toast.ts +0 -204
- package/dist/components/rel-tooltip/rel-tooltip.ts +0 -190
- package/dist/components/rel-topbar/rel-topbar-actions.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar-brand.ts +0 -54
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +0 -69
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +0 -31
- package/dist/components/rel-topbar/rel-topbar-item.ts +0 -324
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar-profile.ts +0 -243
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar.ts +0 -446
- package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +0 -134
- package/dist/components/rel-upload/rel-upload-actions.ts +0 -163
- package/dist/components/rel-upload/rel-upload.ts +0 -943
|
@@ -1,465 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, css } from "lit";
|
|
2
|
-
import { customElement, property, query } from "lit/decorators.js";
|
|
3
|
-
import "@reliability-design/icons/rel-icon";
|
|
4
|
-
import { getDeepActiveElement, getFocusableElements, trapTabKey } from "../../utils/focus.js";
|
|
5
|
-
|
|
6
|
-
export type ModalSide = "top" | "bottom" | "center";
|
|
7
|
-
|
|
8
|
-
@customElement("rel-modal")
|
|
9
|
-
export class RelModal extends LitElement {
|
|
10
|
-
@property({ type: Boolean, reflect: true }) open = false;
|
|
11
|
-
@property({ type: String }) side: ModalSide = "bottom";
|
|
12
|
-
@property({ type: String }) title = "";
|
|
13
|
-
@property({ type: String }) description = "";
|
|
14
|
-
@property({ type: Boolean }) dismissible = true;
|
|
15
|
-
@property({ type: Boolean }) modal = true;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Figma "Breakpoint" axis. responsive (default) collapses to a full-width
|
|
19
|
-
* bottom sheet below 480px; web keeps the fixed desktop panel at every size.
|
|
20
|
-
*/
|
|
21
|
-
@property({ type: String, reflect: true })
|
|
22
|
-
breakpoint: "responsive" | "web" = "responsive";
|
|
23
|
-
|
|
24
|
-
@query(".overlay") private overlayElement!: HTMLElement;
|
|
25
|
-
@query(".panel") private panelElement!: HTMLElement;
|
|
26
|
-
|
|
27
|
-
private previousFocus: HTMLElement | null = null;
|
|
28
|
-
private originalBodyOverflow = "";
|
|
29
|
-
private originalBodyPaddingRight = "";
|
|
30
|
-
|
|
31
|
-
static styles = css`
|
|
32
|
-
:host {
|
|
33
|
-
color: var(--rel-text-default);
|
|
34
|
-
--modal-transition-duration: 0.3s;
|
|
35
|
-
--modal-transition-timing: cubic-bezier(0.32, 0.72, 0, 1);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.modal-container {
|
|
39
|
-
position: fixed;
|
|
40
|
-
top: 0;
|
|
41
|
-
left: 0;
|
|
42
|
-
right: 0;
|
|
43
|
-
bottom: 0;
|
|
44
|
-
z-index: 1000;
|
|
45
|
-
pointer-events: none;
|
|
46
|
-
/* pointer-events:none only blocks the mouse — the closed modal's
|
|
47
|
-
close button and slotted content would otherwise stay in the Tab
|
|
48
|
-
order and swallow keyboard focus into invisible UI. visibility
|
|
49
|
-
also removes them from the tab order; the transition delays the
|
|
50
|
-
flip to hidden until the close animation has played. */
|
|
51
|
-
visibility: hidden;
|
|
52
|
-
transition: visibility 0s linear var(--modal-transition-duration);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.modal-container.open {
|
|
56
|
-
pointer-events: auto;
|
|
57
|
-
visibility: visible;
|
|
58
|
-
transition-delay: 0s;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.overlay {
|
|
62
|
-
position: absolute;
|
|
63
|
-
top: 0;
|
|
64
|
-
left: 0;
|
|
65
|
-
right: 0;
|
|
66
|
-
bottom: 0;
|
|
67
|
-
background: var(--rel-overlay-scrim);
|
|
68
|
-
opacity: 0;
|
|
69
|
-
transition: opacity var(--modal-transition-duration) var(--modal-transition-timing);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
.modal-container.open .overlay {
|
|
73
|
-
opacity: 1;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.overlay.no-modal {
|
|
77
|
-
display: none !important;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.panel {
|
|
81
|
-
position: absolute;
|
|
82
|
-
background: var(--rel-bg-surface);
|
|
83
|
-
box-shadow: var(--rel-shadows-shadow-500);
|
|
84
|
-
display: flex;
|
|
85
|
-
flex-direction: column;
|
|
86
|
-
overflow: hidden;
|
|
87
|
-
outline: none;
|
|
88
|
-
font-family: var(--rel-fonts-font-family);
|
|
89
|
-
width: clamp(360px, 49vw, 708px);
|
|
90
|
-
left: 50%;
|
|
91
|
-
transition: transform var(--modal-transition-duration) var(--modal-transition-timing),
|
|
92
|
-
opacity var(--modal-transition-duration) var(--modal-transition-timing);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.panel[data-side="bottom"] {
|
|
96
|
-
bottom: 48px;
|
|
97
|
-
left: 50%;
|
|
98
|
-
width: clamp(360px, 49vw, 708px);
|
|
99
|
-
max-height: calc(100vh - 96px);
|
|
100
|
-
border-radius: var(--rel-border-radius-sm);
|
|
101
|
-
transform: translateX(-50%) translateY(calc(100% + 48px));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.modal-container.open .panel[data-side="bottom"] {
|
|
105
|
-
transform: translateX(-50%) translateY(0);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.panel[data-side="top"] {
|
|
109
|
-
top: 48px;
|
|
110
|
-
left: 50%;
|
|
111
|
-
width: clamp(360px, 49vw, 708px);
|
|
112
|
-
max-height: calc(100vh - 96px);
|
|
113
|
-
border-radius: var(--rel-border-radius-sm);
|
|
114
|
-
transform: translateX(-50%) translateY(calc(-100% - 48px));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.modal-container.open .panel[data-side="top"] {
|
|
118
|
-
transform: translateX(-50%) translateY(0);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.panel[data-side="center"] {
|
|
122
|
-
top: 50%;
|
|
123
|
-
left: 50%;
|
|
124
|
-
width: clamp(360px, 49vw, 708px);
|
|
125
|
-
max-height: calc(100vh - 96px);
|
|
126
|
-
border-radius: var(--rel-border-radius-sm);
|
|
127
|
-
transform: translate(-50%, -50%) scale(0.96);
|
|
128
|
-
opacity: 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.modal-container.open .panel[data-side="center"] {
|
|
132
|
-
transform: translate(-50%, -50%) scale(1);
|
|
133
|
-
opacity: 1;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/* Figma Breakpoint=Responsive: below 480px every side collapses to a
|
|
137
|
-
full-width bottom sheet. Breakpoint=Web keeps the desktop panel. */
|
|
138
|
-
@media (max-width: 480px) {
|
|
139
|
-
:host([breakpoint="responsive"]) .panel,
|
|
140
|
-
:host([breakpoint="responsive"]) .panel[data-side="top"],
|
|
141
|
-
:host([breakpoint="responsive"]) .panel[data-side="center"],
|
|
142
|
-
:host([breakpoint="responsive"]) .panel[data-side="bottom"] {
|
|
143
|
-
width: 100%;
|
|
144
|
-
left: 0;
|
|
145
|
-
right: 0;
|
|
146
|
-
top: auto;
|
|
147
|
-
bottom: 0;
|
|
148
|
-
max-height: 90vh;
|
|
149
|
-
border-radius: var(--rel-border-radius-md) var(--rel-border-radius-md) 0 0;
|
|
150
|
-
opacity: 1;
|
|
151
|
-
transform: translateY(100%);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
:host([breakpoint="responsive"]) .modal-container.open .panel,
|
|
155
|
-
:host([breakpoint="responsive"]) .modal-container.open .panel[data-side="top"],
|
|
156
|
-
:host([breakpoint="responsive"]) .modal-container.open .panel[data-side="center"],
|
|
157
|
-
:host([breakpoint="responsive"]) .modal-container.open .panel[data-side="bottom"] {
|
|
158
|
-
transform: translateY(0);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.header {
|
|
163
|
-
display: flex;
|
|
164
|
-
align-items: flex-start;
|
|
165
|
-
justify-content: space-between;
|
|
166
|
-
padding: var(--rel-spacing-md) var(--rel-spacing-lg);
|
|
167
|
-
border-bottom: var(--rel-stroke-sm) solid var(--rel-border-default);
|
|
168
|
-
flex-shrink: 0;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.header.empty {
|
|
172
|
-
display: none;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
.header-text {
|
|
176
|
-
gap: var(--rel-spacing-2xs);
|
|
177
|
-
display: flex;
|
|
178
|
-
flex-direction: column;
|
|
179
|
-
gap: var(--rel-spacing-2xs);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
.title {
|
|
183
|
-
color: var(--rel-text-label);
|
|
184
|
-
margin: 0;
|
|
185
|
-
|
|
186
|
-
font-family: var(--rel-headings-lg-font-family);
|
|
187
|
-
|
|
188
|
-
font-weight: var(--rel-headings-lg-font-weight);
|
|
189
|
-
|
|
190
|
-
font-size: var(--rel-headings-lg-font-size);
|
|
191
|
-
|
|
192
|
-
line-height: var(--rel-headings-lg-line-height);
|
|
193
|
-
|
|
194
|
-
letter-spacing: var(--rel-headings-lg-letter-spacing);
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
.description {
|
|
199
|
-
color: var(--rel-text-body);
|
|
200
|
-
margin: 0;
|
|
201
|
-
|
|
202
|
-
font-family: var(--rel-body-lg-regular-font-family);
|
|
203
|
-
|
|
204
|
-
font-weight: var(--rel-body-lg-regular-font-weight);
|
|
205
|
-
|
|
206
|
-
font-size: var(--rel-body-lg-regular-font-size);
|
|
207
|
-
|
|
208
|
-
line-height: var(--rel-body-lg-regular-line-height);
|
|
209
|
-
|
|
210
|
-
letter-spacing: var(--rel-body-lg-regular-letter-spacing);
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/* Emphasised inline text in slotted content (Figma body-lg-medium) */
|
|
215
|
-
.content ::slotted(strong),
|
|
216
|
-
.content ::slotted(b) {
|
|
217
|
-
font-weight: var(--rel-body-lg-medium-font-weight);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
.close-button {
|
|
221
|
-
background: transparent;
|
|
222
|
-
border: none;
|
|
223
|
-
cursor: pointer;
|
|
224
|
-
padding: var(--rel-spacing-2xs);
|
|
225
|
-
display: flex;
|
|
226
|
-
align-items: center;
|
|
227
|
-
justify-content: center;
|
|
228
|
-
align-self: flex-start;
|
|
229
|
-
flex-shrink: 0;
|
|
230
|
-
border-radius: var(--rel-border-radius-xs);
|
|
231
|
-
color: var(--rel-text-default);
|
|
232
|
-
transition: background 0.15s ease;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
.close-button:hover {
|
|
236
|
-
background: var(--rel-bg-neutral-elevated-1);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
.close-button:focus-visible {
|
|
240
|
-
outline: 2px solid var(--rel-border-focus);
|
|
241
|
-
outline-offset: 2px;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
.content {
|
|
245
|
-
flex: 1;
|
|
246
|
-
overflow-y: auto;
|
|
247
|
-
padding: var(--rel-spacing-xl);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
.footer {
|
|
251
|
-
display: none;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
:host([has-footer]) .footer {
|
|
255
|
-
display: flex;
|
|
256
|
-
flex-shrink: 0;
|
|
257
|
-
padding: var(--rel-spacing-md) var(--rel-spacing-lg);
|
|
258
|
-
border-top: var(--rel-stroke-sm) solid var(--rel-border-default);
|
|
259
|
-
justify-content: flex-end;
|
|
260
|
-
gap: var(--rel-spacing-xs);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
@media (prefers-reduced-motion: reduce) {
|
|
264
|
-
.overlay,
|
|
265
|
-
.panel {
|
|
266
|
-
transition: none;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.modal-container .panel {
|
|
270
|
-
transform: none;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.modal-container:not(.open) .panel {
|
|
274
|
-
display: none;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
`;
|
|
278
|
-
|
|
279
|
-
connectedCallback() {
|
|
280
|
-
super.connectedCallback();
|
|
281
|
-
if (this.open) {
|
|
282
|
-
this.lockScroll();
|
|
283
|
-
}
|
|
284
|
-
this.updateComplete.then(() => {
|
|
285
|
-
const footerSlot = this.shadowRoot?.querySelector(
|
|
286
|
-
'slot[name="footer"]'
|
|
287
|
-
) as HTMLSlotElement | null;
|
|
288
|
-
if (footerSlot) {
|
|
289
|
-
const sync = () => {
|
|
290
|
-
const hasContent = footerSlot.assignedElements({ flatten: true }).length > 0;
|
|
291
|
-
this.toggleAttribute('has-footer', hasContent);
|
|
292
|
-
};
|
|
293
|
-
footerSlot.addEventListener('slotchange', sync);
|
|
294
|
-
sync();
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
disconnectedCallback() {
|
|
300
|
-
super.disconnectedCallback();
|
|
301
|
-
this.unlockScroll();
|
|
302
|
-
document.removeEventListener("keydown", this.handleKeydown);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
protected willUpdate() {
|
|
306
|
-
// `title` doubles as HTML's global tooltip attribute: leaving it on the
|
|
307
|
-
// host shows a native tooltip anywhere over the modal, including the
|
|
308
|
-
// full-viewport backdrop. Sync it into the property, then strip it.
|
|
309
|
-
if (this.hasAttribute("title")) {
|
|
310
|
-
const value = this.getAttribute("title") ?? "";
|
|
311
|
-
this.removeAttribute("title");
|
|
312
|
-
this.title = value;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
updated(changed: Map<string, unknown>) {
|
|
317
|
-
if (changed.has("open")) {
|
|
318
|
-
if (this.open) {
|
|
319
|
-
this.handleOpen();
|
|
320
|
-
} else {
|
|
321
|
-
this.handleClose();
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private handleOpen() {
|
|
327
|
-
// Deep-resolved — document.activeElement retargets to shadow hosts whose
|
|
328
|
-
// .focus() may be a no-op, silently dropping the restore.
|
|
329
|
-
this.previousFocus = getDeepActiveElement();
|
|
330
|
-
this.lockScroll();
|
|
331
|
-
document.addEventListener("keydown", this.handleKeydown);
|
|
332
|
-
|
|
333
|
-
// Move focus into the dialog — before this, a keyboard user had to Tab
|
|
334
|
-
// through the entire page behind the overlay to reach the sheet.
|
|
335
|
-
if (this.modal) {
|
|
336
|
-
this.updateComplete.then(() => {
|
|
337
|
-
const panel = this.panelElement;
|
|
338
|
-
if (!panel) return;
|
|
339
|
-
const focusTarget = getFocusableElements(panel)[0];
|
|
340
|
-
if (focusTarget) focusTarget.focus();
|
|
341
|
-
else panel.focus();
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
this.dispatchEvent(
|
|
345
|
-
new CustomEvent("rel-open-change", {
|
|
346
|
-
detail: true,
|
|
347
|
-
bubbles: true,
|
|
348
|
-
composed: true,
|
|
349
|
-
})
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
private handleClose() {
|
|
354
|
-
this.unlockScroll();
|
|
355
|
-
document.removeEventListener("keydown", this.handleKeydown);
|
|
356
|
-
if (this.previousFocus?.focus) {
|
|
357
|
-
this.updateComplete.then(() => {
|
|
358
|
-
this.previousFocus?.focus();
|
|
359
|
-
this.previousFocus = null;
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
this.dispatchEvent(
|
|
363
|
-
new CustomEvent("rel-open-change", {
|
|
364
|
-
detail: false,
|
|
365
|
-
bubbles: true,
|
|
366
|
-
composed: true,
|
|
367
|
-
})
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
private lockScroll() {
|
|
372
|
-
if (typeof document === "undefined") return;
|
|
373
|
-
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
374
|
-
this.originalBodyOverflow = document.body.style.overflow;
|
|
375
|
-
this.originalBodyPaddingRight = document.body.style.paddingRight;
|
|
376
|
-
document.body.style.overflow = "hidden";
|
|
377
|
-
if (scrollbarWidth > 0) {
|
|
378
|
-
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
private unlockScroll() {
|
|
383
|
-
if (typeof document === "undefined") return;
|
|
384
|
-
document.body.style.overflow = this.originalBodyOverflow;
|
|
385
|
-
document.body.style.paddingRight = this.originalBodyPaddingRight;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private handleKeydown = (e: KeyboardEvent) => {
|
|
389
|
-
if (!this.open) return;
|
|
390
|
-
if (e.key === "Escape" && this.dismissible) {
|
|
391
|
-
e.preventDefault();
|
|
392
|
-
this.open = false;
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
// aria-modal promised containment; actually deliver it.
|
|
396
|
-
if (e.key === "Tab" && this.modal && this.panelElement) {
|
|
397
|
-
trapTabKey(e, this.panelElement);
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
private handleOverlayClick = (e: MouseEvent) => {
|
|
402
|
-
if (!this.dismissible) return;
|
|
403
|
-
if (e.target === this.overlayElement) {
|
|
404
|
-
this.open = false;
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
|
|
408
|
-
private handleCloseButtonClick() {
|
|
409
|
-
this.open = false;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
render() {
|
|
413
|
-
const hasTitle = !!this.title;
|
|
414
|
-
const hasDescription = !!this.description;
|
|
415
|
-
const showHeader = hasTitle || hasDescription || this.dismissible;
|
|
416
|
-
|
|
417
|
-
return html`
|
|
418
|
-
<div class="modal-container ${this.open ? "open" : ""}">
|
|
419
|
-
<div
|
|
420
|
-
part="overlay"
|
|
421
|
-
class="overlay ${!this.modal ? 'no-modal' : ''}"
|
|
422
|
-
@click="${this.handleOverlayClick}"></div>
|
|
423
|
-
|
|
424
|
-
<div
|
|
425
|
-
part="panel"
|
|
426
|
-
class="panel"
|
|
427
|
-
data-side="${this.side}"
|
|
428
|
-
role="${this.modal ? "dialog" : "region"}"
|
|
429
|
-
aria-modal="${this.modal}"
|
|
430
|
-
tabindex="-1">
|
|
431
|
-
|
|
432
|
-
<div part="header" class="header ${!showHeader ? "empty" : ""}">
|
|
433
|
-
<div class="header-text">
|
|
434
|
-
${hasTitle ? html`<h2 class="title">${this.title}</h2>` : ""}
|
|
435
|
-
${hasDescription ? html`<p class="description">${this.description}</p>` : ""}
|
|
436
|
-
</div>
|
|
437
|
-
${this.dismissible
|
|
438
|
-
? html`<button
|
|
439
|
-
part="close-button"
|
|
440
|
-
class="close-button"
|
|
441
|
-
@click="${this.handleCloseButtonClick}"
|
|
442
|
-
aria-label="Close">
|
|
443
|
-
<rel-icon name="close" size="sm"></rel-icon>
|
|
444
|
-
</button>`
|
|
445
|
-
: ""}
|
|
446
|
-
</div>
|
|
447
|
-
|
|
448
|
-
<div part="content" class="content">
|
|
449
|
-
<slot></slot>
|
|
450
|
-
</div>
|
|
451
|
-
|
|
452
|
-
<div part="footer" class="footer">
|
|
453
|
-
<slot name="footer"></slot>
|
|
454
|
-
</div>
|
|
455
|
-
</div>
|
|
456
|
-
</div>
|
|
457
|
-
`;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
declare global {
|
|
462
|
-
interface HTMLElementTagNameMap {
|
|
463
|
-
"rel-modal": RelModal;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
# rel-sidebar
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
A sophisticated sidebar navigation system with multiple components: `rel-sidebar-provider` (state manager), `rel-sidebar` (visual container), `rel-sidebar-trigger` (toggle button), and layout helpers (`rel-sidebar-header`, `rel-sidebar-footer`, `rel-sidebar-content`, `rel-sidebar-rail`). Supports three variants (sidebar, floating, inset), two collapsible modes (offcanvas, icon), mobile detection, localStorage persistence, keyboard shortcuts, and focus trapping.
|
|
5
|
-
|
|
6
|
-
## Components
|
|
7
|
-
This sidebar system consists of multiple components that work together:
|
|
8
|
-
- **`<rel-sidebar-provider>`**: Root wrapper that manages state
|
|
9
|
-
- **`<rel-sidebar>`**: Visual sidebar container
|
|
10
|
-
- **`<rel-sidebar-trigger>`**: Toggle button
|
|
11
|
-
- **`<rel-sidebar-header>`**: Header layout helper
|
|
12
|
-
- **`<rel-sidebar-footer>`**: Footer layout helper
|
|
13
|
-
- **`<rel-sidebar-content>`**: Main content wrapper
|
|
14
|
-
- **`<rel-sidebar-rail>`**: Collapsed (icon-only) state content
|
|
15
|
-
|
|
16
|
-
## Usage
|
|
17
|
-
```html
|
|
18
|
-
<!-- Basic sidebar with provider -->
|
|
19
|
-
<rel-sidebar-provider>
|
|
20
|
-
<rel-sidebar>
|
|
21
|
-
<rel-sidebar-header>
|
|
22
|
-
<h2>App Name</h2>
|
|
23
|
-
<rel-sidebar-trigger>
|
|
24
|
-
<button>☰</button>
|
|
25
|
-
</rel-sidebar-trigger>
|
|
26
|
-
</rel-sidebar-header>
|
|
27
|
-
|
|
28
|
-
<rel-sidebar-content>
|
|
29
|
-
<nav>
|
|
30
|
-
<a href="#home">Home</a>
|
|
31
|
-
<a href="#about">About</a>
|
|
32
|
-
<a href="#contact">Contact</a>
|
|
33
|
-
</nav>
|
|
34
|
-
</rel-sidebar-content>
|
|
35
|
-
|
|
36
|
-
<rel-sidebar-footer>
|
|
37
|
-
<p>© 2024 Company</p>
|
|
38
|
-
</rel-sidebar-footer>
|
|
39
|
-
</rel-sidebar>
|
|
40
|
-
|
|
41
|
-
<main>
|
|
42
|
-
<h1>Main Content</h1>
|
|
43
|
-
<p>Page content goes here</p>
|
|
44
|
-
</main>
|
|
45
|
-
</rel-sidebar-provider>
|
|
46
|
-
|
|
47
|
-
<!-- Controlled open state -->
|
|
48
|
-
<rel-sidebar-provider open="true">
|
|
49
|
-
<rel-sidebar>
|
|
50
|
-
<nav>Navigation items</nav>
|
|
51
|
-
</rel-sidebar>
|
|
52
|
-
<main>Content</main>
|
|
53
|
-
</rel-sidebar-provider>
|
|
54
|
-
|
|
55
|
-
<!-- Right-side floating variant -->
|
|
56
|
-
<rel-sidebar-provider side="right" variant="floating" width="lg">
|
|
57
|
-
<rel-sidebar>
|
|
58
|
-
<h3>Right Sidebar</h3>
|
|
59
|
-
</rel-sidebar>
|
|
60
|
-
<main>Content</main>
|
|
61
|
-
</rel-sidebar-provider>
|
|
62
|
-
|
|
63
|
-
<!-- Offcanvas mode (overlay on all screen sizes) -->
|
|
64
|
-
<rel-sidebar-provider collapsible="offcanvas">
|
|
65
|
-
<rel-sidebar>
|
|
66
|
-
<nav>Menu</nav>
|
|
67
|
-
</rel-sidebar>
|
|
68
|
-
<main>Content</main>
|
|
69
|
-
</rel-sidebar-provider>
|
|
70
|
-
|
|
71
|
-
<!-- Listen for state changes -->
|
|
72
|
-
<rel-sidebar-provider id="sidebarProvider">
|
|
73
|
-
<rel-sidebar>Nav</rel-sidebar>
|
|
74
|
-
<main>Content</main>
|
|
75
|
-
</rel-sidebar-provider>
|
|
76
|
-
<script>
|
|
77
|
-
const provider = document.getElementById('sidebarProvider');
|
|
78
|
-
provider.addEventListener('rel-sidebar-change', (e) => {
|
|
79
|
-
console.log('Open:', e.detail.open);
|
|
80
|
-
console.log('Collapsed:', e.detail.collapsed);
|
|
81
|
-
console.log('Reason:', e.detail.reason);
|
|
82
|
-
});
|
|
83
|
-
</script>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## API
|
|
87
|
-
|
|
88
|
-
### rel-sidebar-provider Properties
|
|
89
|
-
| Name | Type | Default | Description |
|
|
90
|
-
|------|------|---------|-------------|
|
|
91
|
-
| `open` (reflected) | `boolean?` | `undefined` | Controlled open state (undefined = uncontrolled) |
|
|
92
|
-
| `defaultopen` | `boolean` | `true` | Default open state (uncontrolled mode) |
|
|
93
|
-
| `collapsed` (reflected) | `boolean?` | `undefined` | Controlled collapsed state |
|
|
94
|
-
| `defaultcollapsed` | `boolean` | `false` | Default collapsed state (uncontrolled mode) |
|
|
95
|
-
| `variant` | `"sidebar" \| "floating" \| "inset"` | `"sidebar"` | Visual variant |
|
|
96
|
-
| `side` | `"left" \| "right"` | `"left"` | Which side the sidebar appears on |
|
|
97
|
-
| `collapsible` | `"offcanvas" \| "icon" \| "none"` | `"icon"` | Collapsible behavior mode |
|
|
98
|
-
| `width` | `"sm" \| "md" \| "lg"` | `"md"` | Sidebar width (240/280/320px) |
|
|
99
|
-
| `persist-key` | `string` | `"rel-sidebar"` | localStorage key for persistence |
|
|
100
|
-
| `keyboard-shortcut` | `boolean` | `true` | Enable Ctrl/Cmd+B shortcut |
|
|
101
|
-
| `disable-persist` | `boolean` | `false` | Disable localStorage persistence |
|
|
102
|
-
| `disable-overlay-close` | `boolean` | `false` | Prevent closing on overlay click |
|
|
103
|
-
|
|
104
|
-
### rel-sidebar Properties
|
|
105
|
-
| Name | Type | Default | Description |
|
|
106
|
-
|------|------|---------|-------------|
|
|
107
|
-
| `inset` | `boolean` | `false` | Use inset variant styling (overrides provider variant) |
|
|
108
|
-
|
|
109
|
-
## Events
|
|
110
|
-
| Event | Detail | Description |
|
|
111
|
-
|-------|--------|-------------|
|
|
112
|
-
| `rel-sidebar-change` | `{ open, collapsed, reason }` | Fired when sidebar state changes |
|
|
113
|
-
| `rel-sidebar-state` | `{ isOpen, isCollapsed, isMobile, side, variant, collapsible, width }` | Internal state sync event |
|
|
114
|
-
|
|
115
|
-
### Change Reasons
|
|
116
|
-
- `"trigger"`: Toggled via trigger button
|
|
117
|
-
- `"rail"`: Toggled collapsed state
|
|
118
|
-
- `"overlay"`: Closed by clicking overlay
|
|
119
|
-
- `"escape"`: Closed via Escape key
|
|
120
|
-
- `"shortcut"`: Toggled via keyboard shortcut (Ctrl/Cmd+B)
|
|
121
|
-
- `"api"`: Changed programmatically via `setOpen()` or `setCollapsed()`
|
|
122
|
-
|
|
123
|
-
## Methods
|
|
124
|
-
|
|
125
|
-
### Provider Methods
|
|
126
|
-
```typescript
|
|
127
|
-
provider.setOpen(nextOpen: boolean, reason?: string): void
|
|
128
|
-
provider.setCollapsed(nextCollapsed: boolean, reason?: string): void
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
##Slots
|
|
132
|
-
| Slot | Component | Description |
|
|
133
|
-
|------|-----------|-------------|
|
|
134
|
-
| (default) | All | Main content area |
|
|
135
|
-
|
|
136
|
-
## CSS Parts
|
|
137
|
-
None exposed.
|
|
138
|
-
|
|
139
|
-
## Tokens / CSS Variables
|
|
140
|
-
|
|
141
|
-
### Width Tokens (defined on `<rel-sidebar>`)
|
|
142
|
-
- `--sidebar-width-sm`: `240px` (small width)
|
|
143
|
-
- `--sidebar-width-md`: `280px` (medium width)
|
|
144
|
-
- `--sidebar-width-lg`: `320px` (large width)
|
|
145
|
-
- `--sidebar-width-collapsed`: `56px` (collapsed/icon-only width)
|
|
146
|
-
|
|
147
|
-
### Transition Tokens
|
|
148
|
-
- `--sidebar-transition-duration`: `0.2s`
|
|
149
|
-
- `--sidebar-transition-timing`: `cubic-bezier(0.4, 0, 0.2, 1)`
|
|
150
|
-
|
|
151
|
-
### Color Tokens
|
|
152
|
-
- `--rel-bg-surface-elevated` - Sidebar background (`#fafafa`)
|
|
153
|
-
- `--rel-text-default` - Text color
|
|
154
|
-
- `--rel-border-default` - Border color
|
|
155
|
-
- `--rel-overlay-scrim` - Overlay background (`rgba(0, 0, 0, 0.5)`)
|
|
156
|
-
|
|
157
|
-
### Shadow Tokens (floating variant)
|
|
158
|
-
- `--rel-shadows-shadow-400` - Floating variant shadow
|
|
159
|
-
|
|
160
|
-
### Other Tokens
|
|
161
|
-
- `--rel-border-radius-md` - Floating variant border radius
|
|
162
|
-
- `--rel-spacing-md` - Floating variant inset, header/footer padding
|
|
163
|
-
- `--rel-spacing-2xs` - Used in various layouts
|
|
164
|
-
|
|
165
|
-
## Accessibility
|
|
166
|
-
- Sidebar uses `role="navigation"`
|
|
167
|
-
- Focus trap active in offcanvas mode
|
|
168
|
-
- Escape key closes offcanvas
|
|
169
|
-
- Keyboard shortcut (Ctrl/Cmd+B) for toggling
|
|
170
|
-
- Restores focus to trigger when closing
|
|
171
|
-
|
|
172
|
-
### Keyboard Interactions
|
|
173
|
-
| Key | Action |
|
|
174
|
-
|-----|--------|
|
|
175
|
-
| `Ctrl/Cmd + B` | Toggle sidebar (open/collapsed depending on mode) |
|
|
176
|
-
| `Escape` | Close sidebar (offcanvas mode only) |
|
|
177
|
-
| `Tab` | Focus trap within sidebar when open in offcanvas mode |
|
|
178
|
-
|
|
179
|
-
### Focus Behavior
|
|
180
|
-
- In offcanvas mode, focus moves to first focusable element when opened
|
|
181
|
-
- Focus is trapped within sidebar in offcanvas mode
|
|
182
|
-
- Focus restores to trigger button when closed
|
|
183
|
-
|
|
184
|
-
## Notes / Edge Cases
|
|
185
|
-
- **Mobile detection**: Automatically detects `max-width: 768px` breakpoint and switches to offcanvas mode
|
|
186
|
-
- **Collapsible modes**:
|
|
187
|
-
- **offcanvas**: Sidebar slides in/out with overlay (like a drawer)
|
|
188
|
-
- **icon**: Sidebar collapses to icon-only width (56px)
|
|
189
|
-
- **none**: Sidebar cannot be collapsed (always full width)
|
|
190
|
-
|
|
191
|
-
- **Variants**:
|
|
192
|
-
- **sidebar**: Fixed position, full height
|
|
193
|
-
- **floating**: Inset with margins, rounded corners, shadow
|
|
194
|
-
- **inset**: Relative position, no shadow
|
|
195
|
-
|
|
196
|
-
- **State persistence**: By default, sidebar state is saved to localStorage and restored on reload (disable with `disable-persist`)
|
|
197
|
-
- **Controlled vs uncontrolled**: If `open` or `collapsed` props are set, the component is controlled and won't manage its own state
|
|
198
|
-
- **Mobile behavior**: On mobile, sidebar starts closed and ignores persisted state
|
|
199
|
-
- **Scroll locking**: In offcanvas mode, body scroll is locked when sidebar is open
|
|
200
|
-
- **Transitions disabled**: When `prefers-reduced-motion: reduce`
|
|
201
|
-
- The provider uses `display: contents` to avoid affecting layout
|
|
202
|
-
- Sidebar is overlaid with `z-index: 1000` in offcanvas mode
|
|
203
|
-
- Overlay is `z-index: 999`
|
|
204
|
-
|
|
205
|
-
### Width Values
|
|
206
|
-
- **sm**: 240px
|
|
207
|
-
- **md**: 280px
|
|
208
|
-
- **lg**: 320px
|
|
209
|
-
- **collapsed**: 56px
|