@reliability-design/web 1.4.6 → 1.4.8
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 +246 -49
- package/dist/components/rel-accordion/README.md +133 -132
- package/dist/components/rel-alert/README.md +145 -141
- package/dist/components/rel-autocomplete/README.md +172 -61
- package/dist/components/rel-avatar/README.md +118 -130
- package/dist/components/rel-badge/README.md +97 -93
- package/dist/components/rel-breadcrumb/README.md +96 -102
- package/dist/components/rel-button/README.md +190 -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 +186 -166
- package/dist/components/rel-checkbox/README.md +96 -75
- package/dist/components/rel-chip/README.md +138 -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 +88 -100
- package/dist/components/rel-drawer/README.md +187 -200
- package/dist/components/rel-kbd/README.md +141 -151
- package/dist/components/rel-layout/README.md +34 -0
- package/dist/components/rel-menu/README.md +180 -151
- package/dist/components/rel-menubar/README.md +218 -329
- package/dist/components/rel-otp/README.md +171 -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 -138
- package/dist/components/rel-range-picker/README.md +165 -0
- package/dist/components/rel-resizable/README.md +199 -140
- package/dist/components/rel-segmented-button/README.md +156 -141
- package/dist/components/rel-select/README.md +156 -93
- package/dist/components/rel-sheet/README.md +168 -224
- package/dist/components/rel-skeleton/README.md +104 -0
- package/dist/components/rel-slider/README.md +131 -135
- package/dist/components/rel-snackbar/README.md +168 -134
- package/dist/components/rel-stepper/README.md +162 -160
- package/dist/components/rel-switch/README.md +117 -102
- package/dist/components/rel-tab/README.md +173 -163
- package/dist/components/rel-textfield/README.md +176 -69
- package/dist/components/rel-time-picker/README.md +122 -157
- package/dist/components/rel-toast/README.md +165 -148
- package/dist/components/rel-tooltip/README.md +119 -111
- package/dist/components/rel-upload/README.md +220 -0
- package/dist/index.d.mts +1726 -447
- package/dist/index.d.ts +1726 -447
- package/dist/index.js +13715 -10870
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14018 -11163
- package/dist/index.mjs.map +1 -1
- package/dist/styles/focus-ring.ts +84 -0
- package/dist/styles/vars.css +892 -866
- package/dist/{vars-XNMTSRVN.css → vars-DXX6QOAG.css} +892 -866
- package/package.json +3 -2
- 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 -720
- package/dist/components/rel-avatar/rel-avatar.ts +0 -142
- package/dist/components/rel-badge/rel-badge.ts +0 -175
- 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 -565
- 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 -723
- 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 -483
- 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/date-parse.test.ts +0 -116
- package/dist/components/rel-date-picker/date-parse.ts +0 -178
- package/dist/components/rel-date-picker/index.ts +0 -1
- package/dist/components/rel-date-picker/rel-date-picker.ts +0 -1676
- 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 -607
- 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 -541
- 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 -527
- package/dist/components/rel-pagination/index.ts +0 -1
- package/dist/components/rel-pagination/rel-pagination.ts +0 -522
- 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 -475
- package/dist/components/rel-progress/rel-progress.ts +0 -234
- package/dist/components/rel-radio/rel-radio.ts +0 -481
- package/dist/components/rel-range-picker/rel-range-picker.ts +0 -1688
- 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 -217
- package/dist/components/rel-select/rel-select.ts +0 -1416
- package/dist/components/rel-sheet/rel-sheet.ts +0 -610
- package/dist/components/rel-sidebar/README.md +0 -209
- package/dist/components/rel-sidebar/rel-sidebar.ts +0 -1191
- package/dist/components/rel-skeleton/rel-skeleton.ts +0 -143
- package/dist/components/rel-slider/rel-slider.ts +0 -673
- package/dist/components/rel-snackbar/rel-snackbar.ts +0 -329
- package/dist/components/rel-stepper/rel-stepper.ts +0 -567
- package/dist/components/rel-switch/rel-switch.ts +0 -199
- 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 -1054
- package/dist/components/rel-time-picker/rel-time-picker.ts +0 -1482
- package/dist/components/rel-time-picker/time-parse.test.ts +0 -67
- package/dist/components/rel-time-picker/time-parse.ts +0 -85
- package/dist/components/rel-toast/rel-toast.ts +0 -206
- package/dist/components/rel-tooltip/rel-tooltip.ts +0 -193
- 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 -968
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
import { LitElement, html, nothing } from "lit";
|
|
2
|
-
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
-
import "@reliability-design/icons/rel-icon";
|
|
4
|
-
import "../rel-checkbox/rel-checkbox.js";
|
|
5
|
-
import "../rel-radio/rel-radio.js";
|
|
6
|
-
|
|
7
|
-
export type MenuType = "default" | "checkbox" | "arrow" | "select-option" | "fixed" | "fit";
|
|
8
|
-
|
|
9
|
-
export interface MenuItem {
|
|
10
|
-
label: string;
|
|
11
|
-
active?: boolean;
|
|
12
|
-
checked?: boolean;
|
|
13
|
-
disabled?: boolean;
|
|
14
|
-
submenu?: MenuItem[];
|
|
15
|
-
open?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
@customElement("rel-menu-item")
|
|
19
|
-
export class RelMenuItem extends LitElement {
|
|
20
|
-
@property({ type: String }) label: string = "";
|
|
21
|
-
@property({ type: String }) value: string = "";
|
|
22
|
-
@property({ type: Boolean }) disabled: boolean = false;
|
|
23
|
-
@property({ type: String }) icon: string = "";
|
|
24
|
-
|
|
25
|
-
createRenderRoot() {
|
|
26
|
-
return this;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
render() {
|
|
30
|
-
return html``;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@customElement("rel-menu")
|
|
35
|
-
export class RelMenu extends LitElement {
|
|
36
|
-
@property({ type: String }) type: MenuType = "default";
|
|
37
|
-
|
|
38
|
-
@property({ type: Array }) items: MenuItem[] = [];
|
|
39
|
-
|
|
40
|
-
@state() private focusedIndex: number = 0;
|
|
41
|
-
/** Index inside the open submenu that virtual focus sits on; -1 = main list. */
|
|
42
|
-
@state() private subFocusedIndex: number = -1;
|
|
43
|
-
@state() private _items: MenuItem[] = [];
|
|
44
|
-
|
|
45
|
-
/** Stable id prefix for aria-activedescendant targets. */
|
|
46
|
-
private menuId = `rel-menu-${Math.random().toString(36).slice(2)}`;
|
|
47
|
-
|
|
48
|
-
public static readonly _css = `
|
|
49
|
-
:host {
|
|
50
|
-
color: var(--rel-text-default);
|
|
51
|
-
display: inline-block;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.rel-menu {
|
|
55
|
-
position: relative;
|
|
56
|
-
display: inline-block;
|
|
57
|
-
background: var(--rel-bg-neutral);
|
|
58
|
-
color: var(--rel-text-default);
|
|
59
|
-
border-radius: var(--rel-border-radius-md);
|
|
60
|
-
box-shadow: var(--rel-shadows-shadow-300);
|
|
61
|
-
min-width: 220px;
|
|
62
|
-
/* No vertical padding: items fill edge-to-edge so a first/last-item hover
|
|
63
|
-
reaches the menu's top/bottom. NOT overflow:hidden — that clips the
|
|
64
|
-
type=arrow flyout (position:absolute at left:100%). Instead the first
|
|
65
|
-
and last items carry the outer corner radius so hover backgrounds clip
|
|
66
|
-
to the rounded shape without any overflow. */
|
|
67
|
-
padding: 0;
|
|
68
|
-
outline: none;
|
|
69
|
-
overflow: visible;
|
|
70
|
-
font-family: var(--rel-body-lg-regular-font-family);
|
|
71
|
-
font-weight: var(--rel-body-lg-regular-font-weight);
|
|
72
|
-
font-size: var(--rel-body-lg-regular-font-size);
|
|
73
|
-
line-height: var(--rel-body-lg-regular-line-height);
|
|
74
|
-
letter-spacing: var(--rel-body-lg-regular-letter-spacing);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.rel-menu:focus-visible,
|
|
78
|
-
:host(:focus-visible) .rel-menu {
|
|
79
|
-
outline: 2px solid var(--rel-border-focus);
|
|
80
|
-
outline-offset: 2px;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
.rel-menu-item {
|
|
84
|
-
position: relative;
|
|
85
|
-
display: flex;
|
|
86
|
-
align-items: center;
|
|
87
|
-
gap: var(--rel-spacing-xs);
|
|
88
|
-
padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
|
|
89
|
-
line-height: var(--rel-body-sm-regular-line-height);
|
|
90
|
-
cursor: pointer;
|
|
91
|
-
transition: background 0.15s ease, color 0.15s ease;
|
|
92
|
-
user-select: none;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* First/last items carry the menu's outer radius so their hover fill clips
|
|
96
|
-
to the rounded corners (replaces the removed overflow:hidden). */
|
|
97
|
-
.rel-menu > .rel-menu-item:first-child {
|
|
98
|
-
border-top-left-radius: var(--rel-border-radius-md);
|
|
99
|
-
border-top-right-radius: var(--rel-border-radius-md);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
.rel-menu > .rel-menu-item:last-child {
|
|
103
|
-
border-bottom-left-radius: var(--rel-border-radius-md);
|
|
104
|
-
border-bottom-right-radius: var(--rel-border-radius-md);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.rel-menu-item:hover {
|
|
108
|
-
background: var(--rel-bg-neutral-elevated-1);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.rel-menu-item:active {
|
|
112
|
-
background: var(--rel-bg-neutral-elevated-1);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.rel-menu-item.active {
|
|
116
|
-
background: var(--rel-bg-neutral-elevated-2);
|
|
117
|
-
font-weight: var(--rel-body-lg-medium-font-weight);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.rel-menu-item.disabled {
|
|
121
|
-
background-color: transparent;
|
|
122
|
-
color: var(--rel-text-disabled);
|
|
123
|
-
/* Keep the item pointer-interactive so cursor:not-allowed actually
|
|
124
|
-
renders; clicks are already neutralised by the item.disabled guard in
|
|
125
|
-
onItemClick/onItemEnter. pointer-events:none here would make the row
|
|
126
|
-
transparent to the pointer and suppress the cursor. */
|
|
127
|
-
cursor: not-allowed;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/* [data-focused] (presence, not ="true"): the ?data-focused boolean
|
|
131
|
-
binding renders the attribute with an EMPTY value, so a ="true"
|
|
132
|
-
selector never matches. */
|
|
133
|
-
.rel-menu-item[data-focused] {
|
|
134
|
-
outline: 2px solid var(--rel-border-focus);
|
|
135
|
-
outline-offset: -2px;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/* Width modes (Figma Type=Fixed / Type=Fit) */
|
|
139
|
-
.rel-menu.fixed {
|
|
140
|
-
width: var(--rel-menu-width, 300px);
|
|
141
|
-
min-width: 0;
|
|
142
|
-
box-sizing: border-box;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.rel-menu.fit {
|
|
146
|
-
min-width: 0;
|
|
147
|
-
width: max-content;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/* Checkbox (Type=Checkbox) / radio (Type=Select Option) rows render the real
|
|
151
|
-
rel-checkbox / rel-radio. pointer-events:none keeps the whole row the click
|
|
152
|
-
target so onItemClick stays the single source of truth. */
|
|
153
|
-
.menu-control {
|
|
154
|
-
pointer-events: none;
|
|
155
|
-
width: 100%;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
.menu-label {
|
|
159
|
-
flex: 1 1 auto;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/* Submenu affordance — same rel-icon chevron the menubar uses. */
|
|
163
|
-
.submenu-chevron {
|
|
164
|
-
margin-left: auto;
|
|
165
|
-
display: inline-flex;
|
|
166
|
-
align-items: center;
|
|
167
|
-
opacity: 0.6;
|
|
168
|
-
pointer-events: none;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/* Shortcut / hint text (Figma body-sm-regular, caption tone) */
|
|
172
|
-
.menu-shortcut,
|
|
173
|
-
::slotted([slot="shortcut"]) {
|
|
174
|
-
font-family: var(--rel-body-sm-regular-font-family);
|
|
175
|
-
font-weight: var(--rel-body-sm-regular-font-weight);
|
|
176
|
-
font-size: var(--rel-body-sm-regular-font-size);
|
|
177
|
-
line-height: var(--rel-body-sm-regular-line-height);
|
|
178
|
-
letter-spacing: var(--rel-body-sm-regular-letter-spacing);
|
|
179
|
-
color: var(--rel-text-caption);
|
|
180
|
-
margin-left: auto;
|
|
181
|
-
padding-left: var(--rel-spacing-md);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.rel-submenu {
|
|
186
|
-
border: 1px solid var(--rel-border-subtle);
|
|
187
|
-
position: absolute;
|
|
188
|
-
top: calc(-1 * var(--rel-spacing-3xs));
|
|
189
|
-
/* flush against the trigger (no gap) so moving the pointer into the flyout
|
|
190
|
-
never crosses a dead zone that would fire the trigger's mouseleave */
|
|
191
|
-
left: 100%;
|
|
192
|
-
min-width: 200px;
|
|
193
|
-
background: var(--rel-bg-neutral);
|
|
194
|
-
color: var(--rel-text-default);
|
|
195
|
-
border-radius: var(--rel-border-radius-md);
|
|
196
|
-
box-shadow: var(--rel-shadows-shadow-300);
|
|
197
|
-
/* padding:0 + overflow:hidden — safe here (a submenu has no further nested
|
|
198
|
-
flyout to clip) — so sub-item hovers fill fully and clip to the corners */
|
|
199
|
-
padding: 0;
|
|
200
|
-
overflow: hidden;
|
|
201
|
-
visibility: hidden;
|
|
202
|
-
pointer-events: none;
|
|
203
|
-
z-index: 20;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
.rel-submenu.visible {
|
|
207
|
-
visibility: visible;
|
|
208
|
-
pointer-events: auto;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
.rel-submenu .rel-menu-item {
|
|
212
|
-
padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
|
|
213
|
-
}
|
|
214
|
-
`;
|
|
215
|
-
|
|
216
|
-
createRenderRoot() {
|
|
217
|
-
return this;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
connectedCallback() {
|
|
221
|
-
super.connectedCallback();
|
|
222
|
-
document.addEventListener("click", this.handleDocumentClick);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
disconnectedCallback() {
|
|
226
|
-
super.disconnectedCallback();
|
|
227
|
-
document.removeEventListener("click", this.handleDocumentClick);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
updated(changedProperties: Map<string, unknown>) {
|
|
231
|
-
if (changedProperties.has('items')) {
|
|
232
|
-
this._items = this.items.map(item => ({ ...item }));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** Index of a submenu locked open by a click (survives hover-out). -1 = none. */
|
|
237
|
-
private lockedIndex = -1;
|
|
238
|
-
|
|
239
|
-
private handleDocumentClick = (e: MouseEvent) => {
|
|
240
|
-
// composedPath (NOT e.target): document-level e.target is retargeted to the
|
|
241
|
-
// shadow host when this menu lives inside a shadow root, so this.contains()
|
|
242
|
-
// wrongly reads an inside-click as outside and closes an open submenu on the
|
|
243
|
-
// click that opened it. composedPath pierces shadow boundaries.
|
|
244
|
-
if (!e.composedPath().includes(this)) {
|
|
245
|
-
this.lockedIndex = -1;
|
|
246
|
-
this.subFocusedIndex = -1;
|
|
247
|
-
this._items = this._items.map(i => ({ ...i, open: false }));
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
private onItemEnter(item: MenuItem, index: number) {
|
|
252
|
-
if (item.disabled) return;
|
|
253
|
-
if (this.type === "arrow" && item.submenu?.length) {
|
|
254
|
-
// Hover opens this submenu; a click-locked one also stays open.
|
|
255
|
-
this._items = this._items.map((i, idx) => ({ ...i, open: idx === index || idx === this.lockedIndex }));
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private onItemLeave(_item: MenuItem, index: number) {
|
|
260
|
-
if (this.type !== "arrow") return;
|
|
261
|
-
// Hover-out closes the submenu UNLESS it was locked open by a click. Moving
|
|
262
|
-
// into the flyout doesn't fire this (the flyout is a DOM descendant of the
|
|
263
|
-
// item and sits flush at left:100%, so there's no dead-zone gap).
|
|
264
|
-
if (this.lockedIndex === index) return;
|
|
265
|
-
this._items = this._items.map((i, idx) => ({ ...i, open: idx === this.lockedIndex }));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private onItemClick(item: MenuItem, index: number, ev?: Event) {
|
|
269
|
-
ev?.stopPropagation();
|
|
270
|
-
if (item.disabled) return;
|
|
271
|
-
|
|
272
|
-
this.focusedIndex = index;
|
|
273
|
-
|
|
274
|
-
if (this.type === "checkbox") {
|
|
275
|
-
const updated = { ...item, checked: !item.checked };
|
|
276
|
-
this._items = this._items.map((i, idx) => idx === index ? updated : i);
|
|
277
|
-
this.dispatchEvent(
|
|
278
|
-
new CustomEvent("rel-check", {
|
|
279
|
-
detail: { item: updated, checked: updated.checked },
|
|
280
|
-
bubbles: true,
|
|
281
|
-
composed: true,
|
|
282
|
-
})
|
|
283
|
-
);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (this.type === "arrow" && item.submenu?.length) {
|
|
288
|
-
// Click LOCKS the submenu open (click again to unlock/close).
|
|
289
|
-
const lock = this.lockedIndex !== index;
|
|
290
|
-
this.lockedIndex = lock ? index : -1;
|
|
291
|
-
this._items = this._items.map((i, idx) => ({ ...i, open: idx === index ? lock : false }));
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Default: set active
|
|
296
|
-
const updated = { ...item, active: true };
|
|
297
|
-
this._items = this._items.map((i, idx) => idx === index ? updated : { ...i, active: false });
|
|
298
|
-
this.dispatchEvent(
|
|
299
|
-
new CustomEvent("rel-select", {
|
|
300
|
-
detail: { item: updated },
|
|
301
|
-
bubbles: true,
|
|
302
|
-
composed: true,
|
|
303
|
-
})
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private onSubClick(parentIndex: number, subIndex: number, ev?: Event) {
|
|
308
|
-
ev?.stopPropagation();
|
|
309
|
-
this.lockedIndex = -1;
|
|
310
|
-
this._items = this._items.map((i, idx) => ({
|
|
311
|
-
...i,
|
|
312
|
-
active: false,
|
|
313
|
-
open: false,
|
|
314
|
-
submenu: i.submenu?.map((s, sIdx) => ({
|
|
315
|
-
...s,
|
|
316
|
-
active: idx === parentIndex && sIdx === subIndex,
|
|
317
|
-
})),
|
|
318
|
-
}));
|
|
319
|
-
this.dispatchEvent(
|
|
320
|
-
new CustomEvent("rel-submenu-select", {
|
|
321
|
-
detail: {
|
|
322
|
-
parent: this._items[parentIndex],
|
|
323
|
-
sub: this._items[parentIndex]?.submenu?.[subIndex],
|
|
324
|
-
},
|
|
325
|
-
bubbles: true,
|
|
326
|
-
composed: true,
|
|
327
|
-
})
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Keyboard-open locks the submenu (no hover-out to close it) and moves
|
|
332
|
-
* virtual focus onto its first entry. */
|
|
333
|
-
private openSubmenuAt(index: number) {
|
|
334
|
-
this.lockedIndex = index;
|
|
335
|
-
this.subFocusedIndex = 0;
|
|
336
|
-
this._items = this._items.map((i, idx) => ({ ...i, open: idx === index }));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private closeSubmenus() {
|
|
340
|
-
this.lockedIndex = -1;
|
|
341
|
-
this.subFocusedIndex = -1;
|
|
342
|
-
this._items = this._items.map(i => ({ ...i, open: false }));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
private onKeydown(ev: KeyboardEvent) {
|
|
346
|
-
const last = this._items.length - 1;
|
|
347
|
-
const focusedItem = this._items[this.focusedIndex];
|
|
348
|
-
|
|
349
|
-
// ── Virtual focus is inside an open submenu ──
|
|
350
|
-
if (
|
|
351
|
-
this.subFocusedIndex >= 0 &&
|
|
352
|
-
this.type === "arrow" &&
|
|
353
|
-
focusedItem?.open &&
|
|
354
|
-
focusedItem.submenu?.length
|
|
355
|
-
) {
|
|
356
|
-
const subLast = focusedItem.submenu.length - 1;
|
|
357
|
-
switch (ev.key) {
|
|
358
|
-
case "ArrowDown":
|
|
359
|
-
this.subFocusedIndex = Math.min(subLast, this.subFocusedIndex + 1);
|
|
360
|
-
ev.preventDefault();
|
|
361
|
-
return;
|
|
362
|
-
case "ArrowUp":
|
|
363
|
-
this.subFocusedIndex = Math.max(0, this.subFocusedIndex - 1);
|
|
364
|
-
ev.preventDefault();
|
|
365
|
-
return;
|
|
366
|
-
case "Home":
|
|
367
|
-
this.subFocusedIndex = 0;
|
|
368
|
-
ev.preventDefault();
|
|
369
|
-
return;
|
|
370
|
-
case "End":
|
|
371
|
-
this.subFocusedIndex = subLast;
|
|
372
|
-
ev.preventDefault();
|
|
373
|
-
return;
|
|
374
|
-
case "ArrowLeft":
|
|
375
|
-
case "Escape":
|
|
376
|
-
// Close only the submenu; don't let Escape also dismiss an
|
|
377
|
-
// enclosing popover in the same keystroke.
|
|
378
|
-
this.closeSubmenus();
|
|
379
|
-
ev.preventDefault();
|
|
380
|
-
ev.stopPropagation();
|
|
381
|
-
return;
|
|
382
|
-
case "Enter":
|
|
383
|
-
case " ":
|
|
384
|
-
this.onSubClick(this.focusedIndex, this.subFocusedIndex);
|
|
385
|
-
ev.preventDefault();
|
|
386
|
-
return;
|
|
387
|
-
default:
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ── Virtual focus is in the main list ──
|
|
393
|
-
if (ev.key === "ArrowDown") {
|
|
394
|
-
this.focusedIndex = Math.min(last, this.focusedIndex + 1);
|
|
395
|
-
ev.preventDefault();
|
|
396
|
-
} else if (ev.key === "ArrowUp") {
|
|
397
|
-
this.focusedIndex = Math.max(0, this.focusedIndex - 1);
|
|
398
|
-
ev.preventDefault();
|
|
399
|
-
} else if (ev.key === "Home") {
|
|
400
|
-
this.focusedIndex = 0;
|
|
401
|
-
ev.preventDefault();
|
|
402
|
-
} else if (ev.key === "End") {
|
|
403
|
-
this.focusedIndex = last;
|
|
404
|
-
ev.preventDefault();
|
|
405
|
-
} else if (ev.key === "ArrowRight") {
|
|
406
|
-
const item = this._items[this.focusedIndex];
|
|
407
|
-
if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
|
|
408
|
-
this.openSubmenuAt(this.focusedIndex);
|
|
409
|
-
ev.preventDefault();
|
|
410
|
-
}
|
|
411
|
-
} else if (ev.key === "ArrowLeft" || ev.key === "Escape") {
|
|
412
|
-
const anyOpen = this._items.some(i => i.open);
|
|
413
|
-
if (anyOpen) {
|
|
414
|
-
this.closeSubmenus();
|
|
415
|
-
ev.preventDefault();
|
|
416
|
-
if (ev.key === "Escape") ev.stopPropagation();
|
|
417
|
-
}
|
|
418
|
-
} else if (ev.key === "Enter" || ev.key === " ") {
|
|
419
|
-
const item = this._items[this.focusedIndex];
|
|
420
|
-
if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
|
|
421
|
-
this.openSubmenuAt(this.focusedIndex);
|
|
422
|
-
} else {
|
|
423
|
-
this.onItemClick(item, this.focusedIndex);
|
|
424
|
-
}
|
|
425
|
-
ev.preventDefault();
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private renderMenuItem(item: MenuItem, index: number) {
|
|
430
|
-
const isFocused = this.focusedIndex === index && this.subFocusedIndex < 0;
|
|
431
|
-
const hasSubmenu = this.type === "arrow" && !!item.submenu?.length;
|
|
432
|
-
const role =
|
|
433
|
-
this.type === "checkbox"
|
|
434
|
-
? "menuitemcheckbox"
|
|
435
|
-
: this.type === "select-option"
|
|
436
|
-
? "menuitemradio"
|
|
437
|
-
: "menuitem";
|
|
438
|
-
const ariaChecked =
|
|
439
|
-
this.type === "checkbox"
|
|
440
|
-
? String(!!item.checked)
|
|
441
|
-
: this.type === "select-option"
|
|
442
|
-
? String(!!item.active)
|
|
443
|
-
: nothing;
|
|
444
|
-
|
|
445
|
-
// Checkbox / select-option rows delegate their control to the real
|
|
446
|
-
// rel-checkbox / rel-radio components (pointer-events:none so the whole ROW
|
|
447
|
-
// stays the click target — onItemClick is the single source of truth and
|
|
448
|
-
// the control just reflects item state).
|
|
449
|
-
const control =
|
|
450
|
-
this.type === "checkbox"
|
|
451
|
-
? html`<rel-checkbox
|
|
452
|
-
class="menu-control"
|
|
453
|
-
inert
|
|
454
|
-
aria-hidden="true"
|
|
455
|
-
label="${item.label}"
|
|
456
|
-
status="${item.checked ? "checked" : "unchecked"}"
|
|
457
|
-
state="${item.disabled ? "disabled" : "default"}"
|
|
458
|
-
></rel-checkbox>`
|
|
459
|
-
: this.type === "select-option"
|
|
460
|
-
? html`<rel-radio
|
|
461
|
-
class="menu-control"
|
|
462
|
-
inert
|
|
463
|
-
aria-hidden="true"
|
|
464
|
-
.options="${[{ value: item.label, label: item.label, disabled: !!item.disabled }]}"
|
|
465
|
-
.selectedValue="${item.active ? item.label : null}"
|
|
466
|
-
></rel-radio>`
|
|
467
|
-
: html`
|
|
468
|
-
<span class="menu-label">${item.label}</span>
|
|
469
|
-
${hasSubmenu
|
|
470
|
-
? html`<span class="submenu-chevron"
|
|
471
|
-
><rel-icon name="chevron-right" size="sm"></rel-icon
|
|
472
|
-
></span>`
|
|
473
|
-
: ""}
|
|
474
|
-
`;
|
|
475
|
-
|
|
476
|
-
return html`
|
|
477
|
-
<div
|
|
478
|
-
id="${this.menuId}-item-${index}"
|
|
479
|
-
class="rel-menu-item ${item.active ? "active" : ""} ${item.disabled ? "disabled" : ""}"
|
|
480
|
-
role="${role}"
|
|
481
|
-
aria-checked="${ariaChecked}"
|
|
482
|
-
aria-label="${this.type === "checkbox" || this.type === "select-option" ? item.label : nothing}"
|
|
483
|
-
aria-disabled="${item.disabled ? "true" : nothing}"
|
|
484
|
-
aria-haspopup="${hasSubmenu ? "menu" : nothing}"
|
|
485
|
-
aria-expanded="${hasSubmenu ? String(!!item.open) : nothing}"
|
|
486
|
-
?data-focused="${isFocused}"
|
|
487
|
-
@mouseenter="${() => this.onItemEnter(item, index)}"
|
|
488
|
-
@mouseleave="${() => this.onItemLeave(item, index)}"
|
|
489
|
-
@click="${(e: Event) => this.onItemClick(item, index, e)}">
|
|
490
|
-
${control}
|
|
491
|
-
${hasSubmenu
|
|
492
|
-
? html`
|
|
493
|
-
<div
|
|
494
|
-
part="panel" class="rel-submenu ${item.open ? "visible" : ""}"
|
|
495
|
-
role="menu"
|
|
496
|
-
@click="${(e: Event) => e.stopPropagation()}">
|
|
497
|
-
${item.submenu!.map(
|
|
498
|
-
(sub, subIndex) => html`
|
|
499
|
-
<div
|
|
500
|
-
id="${this.menuId}-item-${index}-sub-${subIndex}"
|
|
501
|
-
class="rel-menu-item ${sub.active ? "active" : ""} ${sub.disabled ? "disabled" : ""}"
|
|
502
|
-
role="menuitem"
|
|
503
|
-
aria-disabled="${sub.disabled ? "true" : nothing}"
|
|
504
|
-
?data-focused="${this.focusedIndex === index && this.subFocusedIndex === subIndex}"
|
|
505
|
-
@click="${(e: Event) => this.onSubClick(index, subIndex, e)}">
|
|
506
|
-
<span class="menu-label">${sub.label}</span>
|
|
507
|
-
</div>
|
|
508
|
-
`
|
|
509
|
-
)}
|
|
510
|
-
</div>
|
|
511
|
-
`
|
|
512
|
-
: ""}
|
|
513
|
-
</div>
|
|
514
|
-
`;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
render() {
|
|
518
|
-
// DOM focus stays on the container; this tells AT which item virtual
|
|
519
|
-
// focus is on as the user arrows around (main list or open submenu).
|
|
520
|
-
const activeDescendant =
|
|
521
|
-
this._items.length === 0
|
|
522
|
-
? nothing
|
|
523
|
-
: this.subFocusedIndex >= 0
|
|
524
|
-
? `${this.menuId}-item-${this.focusedIndex}-sub-${this.subFocusedIndex}`
|
|
525
|
-
: `${this.menuId}-item-${this.focusedIndex}`;
|
|
526
|
-
|
|
527
|
-
return html`
|
|
528
|
-
<style>${RelMenu._css}</style>
|
|
529
|
-
<div
|
|
530
|
-
part="list" class="rel-menu ${this.type === "fixed" || this.type === "fit" ? this.type : ""}"
|
|
531
|
-
role="menu"
|
|
532
|
-
aria-label="Menu"
|
|
533
|
-
tabindex="0"
|
|
534
|
-
aria-activedescendant="${activeDescendant}"
|
|
535
|
-
@keydown="${this.onKeydown}"
|
|
536
|
-
@click="${(e: Event) => e.stopPropagation()}">
|
|
537
|
-
${this._items.map((item, index) => this.renderMenuItem(item, index))}
|
|
538
|
-
</div>
|
|
539
|
-
`;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { LitElement, html } from "lit";
|
|
2
|
-
import { customElement, property } from "lit/decorators.js";
|
|
3
|
-
import "./rel-menubar-menu.css";
|
|
4
|
-
|
|
5
|
-
@customElement("rel-menubar-menu")
|
|
6
|
-
export class RelMenubarMenu extends LitElement {
|
|
7
|
-
@property({ type: String }) label = "";
|
|
8
|
-
|
|
9
|
-
// Disable shadow DOM to allow parent to access children
|
|
10
|
-
createRenderRoot() {
|
|
11
|
-
return this;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
render() {
|
|
15
|
-
return html``;
|
|
16
|
-
}
|
|
17
|
-
}
|