@crowdstrike/glide-core 0.29.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accordion.js +240 -1
- package/dist/accordion.styles.js +13 -7
- package/dist/button-group.button.js +143 -1
- package/dist/button-group.button.styles.js +43 -15
- package/dist/button-group.js +249 -1
- package/dist/button-group.styles.js +10 -5
- package/dist/button.js +206 -1
- package/dist/button.styles.js +12 -7
- package/dist/checkbox-group.js +479 -14
- package/dist/checkbox-group.styles.js +5 -2
- package/dist/checkbox.js +519 -32
- package/dist/checkbox.styles.js +10 -5
- package/dist/drawer.js +168 -1
- package/dist/drawer.styles.js +5 -2
- package/dist/dropdown.js +2423 -123
- package/dist/dropdown.option.js +536 -1
- package/dist/dropdown.option.styles.js +5 -2
- package/dist/dropdown.styles.js +14 -7
- package/dist/form-controls-layout.js +102 -1
- package/dist/form-controls-layout.styles.js +5 -2
- package/dist/icon-button.js +139 -1
- package/dist/icon-button.styles.js +19 -7
- package/dist/icons/checked.js +28 -1
- package/dist/icons/chevron.js +21 -1
- package/dist/icons/magnifying-glass.js +23 -1
- package/dist/icons/pencil.js +21 -1
- package/dist/icons/severity-critical.js +20 -1
- package/dist/icons/severity-informational.js +20 -1
- package/dist/icons/severity-medium.js +20 -1
- package/dist/icons/x.js +21 -1
- package/dist/inline-alert.js +118 -1
- package/dist/inline-alert.styles.js +5 -2
- package/dist/input.d.ts +8 -2
- package/dist/input.js +505 -41
- package/dist/input.styles.js +25 -4
- package/dist/label.js +303 -1
- package/dist/label.styles.js +11 -5
- package/dist/library/assert-slot.js +136 -1
- package/dist/library/expect-unhandled-rejection.js +14 -1
- package/dist/library/expect-window-error.js +26 -1
- package/dist/library/final.js +18 -1
- package/dist/library/form-control.js +1 -1
- package/dist/library/localize.js +10 -1
- package/dist/library/mouse.js +35 -1
- package/dist/library/on-resize.js +24 -1
- package/dist/library/required.js +35 -1
- package/dist/library/shadow-root-mode.js +4 -1
- package/dist/library/unique-id.js +3 -1
- package/dist/link.js +92 -1
- package/dist/link.styles.js +10 -5
- package/dist/menu.d.ts +3 -2
- package/dist/menu.js +1259 -1
- package/dist/menu.styles.js +34 -17
- package/dist/modal.d.ts +4 -0
- package/dist/modal.icon-button.js +60 -1
- package/dist/modal.icon-button.styles.js +5 -2
- package/dist/modal.js +473 -1
- package/dist/modal.styles.js +71 -22
- package/dist/option.d.ts +74 -0
- package/dist/option.js +498 -0
- package/dist/option.styles.js +140 -0
- package/dist/{menu.options.d.ts → options.d.ts} +5 -6
- package/dist/options.js +130 -0
- package/dist/options.styles.js +21 -0
- package/dist/popover.js +620 -1
- package/dist/popover.styles.js +11 -5
- package/dist/radio-group.js +624 -17
- package/dist/radio-group.radio.js +211 -1
- package/dist/radio-group.radio.styles.js +9 -4
- package/dist/radio-group.styles.js +5 -2
- package/dist/slider.js +1040 -61
- package/dist/slider.styles.js +9 -4
- package/dist/spinner.js +60 -1
- package/dist/spinner.styles.js +5 -2
- package/dist/split-button.js +116 -1
- package/dist/split-button.primary-button.js +100 -1
- package/dist/split-button.primary-button.styles.js +13 -6
- package/dist/split-button.primary-link.js +102 -1
- package/dist/split-button.secondary-button.d.ts +2 -3
- package/dist/split-button.secondary-button.js +121 -1
- package/dist/split-button.secondary-button.styles.js +12 -7
- package/dist/split-button.styles.js +9 -4
- package/dist/styles/focus-outline.js +9 -3
- package/dist/styles/fonts.css +6 -1
- package/dist/styles/opacity-and-scale-animation.js +6 -3
- package/dist/styles/skeleton.js +6 -3
- package/dist/styles/variables.css +410 -1
- package/dist/styles/visually-hidden.js +6 -3
- package/dist/tab.group.js +386 -1
- package/dist/tab.group.styles.js +5 -2
- package/dist/tab.js +133 -1
- package/dist/tab.panel.js +93 -1
- package/dist/tab.panel.styles.js +11 -5
- package/dist/tab.styles.js +9 -4
- package/dist/tag.js +207 -1
- package/dist/tag.styles.js +10 -5
- package/dist/textarea.js +353 -19
- package/dist/textarea.styles.js +23 -4
- package/dist/toast.js +130 -1
- package/dist/toast.toasts.js +248 -25
- package/dist/toast.toasts.styles.js +9 -4
- package/dist/toggle.js +178 -1
- package/dist/toggle.styles.js +25 -5
- package/dist/tooltip.container.d.ts +2 -0
- package/dist/tooltip.container.js +130 -1
- package/dist/tooltip.container.styles.js +18 -4
- package/dist/tooltip.d.ts +6 -0
- package/dist/tooltip.js +484 -1
- package/dist/tooltip.styles.js +21 -5
- package/dist/translations/en.js +36 -1
- package/dist/translations/fr.js +37 -1
- package/dist/translations/ja.js +37 -1
- package/package.json +8 -12
- package/dist/menu.button.d.ts +0 -42
- package/dist/menu.button.js +0 -1
- package/dist/menu.button.styles.js +0 -32
- package/dist/menu.link.d.ts +0 -44
- package/dist/menu.link.js +0 -1
- package/dist/menu.link.styles.js +0 -35
- package/dist/menu.options.js +0 -1
- package/dist/menu.options.styles.d.ts +0 -2
- package/dist/menu.options.styles.js +0 -20
- /package/dist/{menu.button.styles.d.ts → option.styles.d.ts} +0 -0
- /package/dist/{menu.link.styles.d.ts → options.styles.d.ts} +0 -0
package/dist/menu.js
CHANGED
@@ -1 +1,1259 @@
|
|
1
|
-
var __decorate
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
6
|
+
};
|
7
|
+
var _a;
|
8
|
+
var Menu_1;
|
9
|
+
import { html, LitElement } from 'lit';
|
10
|
+
import { autoUpdate, computePosition, flip, offset } from '@floating-ui/dom';
|
11
|
+
import { createRef, ref } from 'lit/directives/ref.js';
|
12
|
+
import { customElement, property } from 'lit/decorators.js';
|
13
|
+
import packageJson from '../package.json' with { type: 'json' };
|
14
|
+
import { LocalizeController } from './library/localize.js';
|
15
|
+
import Options from './options.js';
|
16
|
+
import Option from './option.js';
|
17
|
+
import Input from './input.js';
|
18
|
+
import assertSlot from './library/assert-slot.js';
|
19
|
+
import styles from './menu.styles.js';
|
20
|
+
import shadowRootMode from './library/shadow-root-mode.js';
|
21
|
+
import final from './library/final.js';
|
22
|
+
import uniqueId from './library/unique-id.js';
|
23
|
+
/**
|
24
|
+
* @attr {boolean} [loading=false]
|
25
|
+
* @attr {number} [offset=4]
|
26
|
+
* @attr {boolean} [open=false]
|
27
|
+
* @attr {'bottom'|'left'|'right'|'top'|'bottom-start'|'bottom-end'|'left-start'|'left-end'|'right-start'|'right-end'|'top-start'|'top-end'} [placement='bottom-start']
|
28
|
+
*
|
29
|
+
* @readonly
|
30
|
+
* @attr {string} [version]
|
31
|
+
*
|
32
|
+
* @slot {Element}
|
33
|
+
* @slot {Element} target - The element to which Menu will anchor. Can be any focusable element unless it's the target of a sub-Menu, in which case the element shouldn't be focusable. If you want Menu to be filterable, put an Input in this slot. Listen for Input's "input" event, then add and remove Option(s) from Menu's default slot based on Input's value.
|
34
|
+
*
|
35
|
+
* @fires {Event} toggle
|
36
|
+
*/
|
37
|
+
let Menu = class Menu extends LitElement {
|
38
|
+
constructor() {
|
39
|
+
super(...arguments);
|
40
|
+
this.placement = 'bottom-start';
|
41
|
+
// Used in `#show()` to open the active Option's tooltip when Menu is opened via
|
42
|
+
// keyboard. Unlike mouse users, keyboard users can't hover an Option to reveal
|
43
|
+
// its tooltip. So we always open the tooltip for them when an Option becomes
|
44
|
+
// active.
|
45
|
+
//
|
46
|
+
// A property instead of a private field because `#onTargetAndDefaultSlotKeyDown()`
|
47
|
+
// additionally uses this field to signal to sub-Menus that they've been opened
|
48
|
+
// via keyboard.
|
49
|
+
this.privateOpenedViaKeyboard = false;
|
50
|
+
this.version = packageJson.version;
|
51
|
+
this.#componentElementRef = createRef();
|
52
|
+
this.#defaultSlotElementRef = createRef();
|
53
|
+
// Set in `#onComponentFocusIn()` and `#onComponentFocusOut()`. Used in
|
54
|
+
// `#onComponentFocusOut()` to decide if Menu should close. Also used in
|
55
|
+
// `#onTargetAndDefaultSlotKeyDown()` to decide if we need to move focus.
|
56
|
+
this.#hasVoiceOverMovedFocusToOptionsOrAnOption = false;
|
57
|
+
// Set in `#onTargetSlotMouseUp()`. Used in `#onDocumentClick()` to guard against
|
58
|
+
// Menu closing when any number of things that are not an Option are clicked. Those
|
59
|
+
// "click" events will be retargeted to Menu's host the moment they bubble out of
|
60
|
+
// Menu. So checking in `#onDocumentClick()` if the click's `event.target` came
|
61
|
+
// from inside Menu won't do.
|
62
|
+
this.#isDefaultSlotClick = false;
|
63
|
+
this.#isLoading = false;
|
64
|
+
this.#isOpen = false;
|
65
|
+
// Set in `#onDefaultSlotToggle()`. Used in `#onTargetAndDefaultSlotKeyDown()` to
|
66
|
+
// guard against redispatching the event to a sub-Menu when one isn't open. Also
|
67
|
+
// used in `connectedCallback()` to guard against listening for document clicks for
|
68
|
+
// sub-Menus.
|
69
|
+
this.#isSubMenuOpen = false;
|
70
|
+
// Set in `#onTargetSlotMouseUp()` and `#onDocumentClick()`. Used in
|
71
|
+
// `#onDocumentClick()`:
|
72
|
+
//
|
73
|
+
// 1. Menu is open.
|
74
|
+
// 2. User clicks Menu's target.
|
75
|
+
// 3. `#onDocumentClick()` sets `open` to `false`.
|
76
|
+
// 4. `#onTargetSlotClick()` sets `open` to true`.
|
77
|
+
// 5. Menu never closes.
|
78
|
+
//
|
79
|
+
// Setting `#isTargetSlotMouseUp` to `true` in `#onTargetSlotMouseUp()` gives
|
80
|
+
// `#onDocumentClick()` the information it needs to not set `open` to `false`.
|
81
|
+
//
|
82
|
+
// The normal approach would be to set an `#isTargetSlotClick` property in
|
83
|
+
// `#onTargetSlotClick()`. But `#onDocumentClick()` listens for "click" events in
|
84
|
+
// their capture phase. So `#onDocumentClick()` would be called before
|
85
|
+
// `#onTargetSlotClick()`.
|
86
|
+
//
|
87
|
+
// Note too that `#onDocumentClick()` sets `#isTargetSlotMouseUp` to `false`
|
88
|
+
// instead of `#onTargetSlotClick()` doing it. That's so `#isTargetSlotMouseUp`
|
89
|
+
// is set to `false` even if the user mouses down on Menu's target then moves the
|
90
|
+
// mouse outside of Menu before mousing up.
|
91
|
+
this.#isTargetSlotMouseUp = false;
|
92
|
+
this.#localize = new LocalizeController(this);
|
93
|
+
this.#targetSlotElementRef = createRef();
|
94
|
+
// An arrow function field instead of a method so `this` is closed over and set to
|
95
|
+
// the component instead of `document`.
|
96
|
+
this.#onDocumentClick = () => {
|
97
|
+
if (this.#isDefaultSlotClick) {
|
98
|
+
this.#isDefaultSlotClick = false;
|
99
|
+
return;
|
100
|
+
}
|
101
|
+
if (this.#isTargetSlotMouseUp) {
|
102
|
+
this.#isTargetSlotMouseUp = false;
|
103
|
+
return;
|
104
|
+
}
|
105
|
+
if (this.#optionsElement) {
|
106
|
+
this.#optionsElement.ariaActivedescendant = '';
|
107
|
+
}
|
108
|
+
for (const subMenu of this.#subMenus) {
|
109
|
+
subMenu.open = false;
|
110
|
+
}
|
111
|
+
this.open = false;
|
112
|
+
};
|
113
|
+
}
|
114
|
+
static { Menu_1 = this; }
|
115
|
+
static { this.shadowRootOptions = {
|
116
|
+
...LitElement.shadowRootOptions,
|
117
|
+
mode: shadowRootMode,
|
118
|
+
}; }
|
119
|
+
static { this.styles = styles; }
|
120
|
+
/**
|
121
|
+
* @default false
|
122
|
+
*/
|
123
|
+
get loading() {
|
124
|
+
return this.#isLoading;
|
125
|
+
}
|
126
|
+
set loading(isLoading) {
|
127
|
+
this.#isLoading = isLoading;
|
128
|
+
if (this.#optionsElement && this.#targetElement) {
|
129
|
+
this.#optionsElement.privateLoading = isLoading;
|
130
|
+
this.#targetElement.ariaDescription = isLoading
|
131
|
+
? this.#localize.term('loading')
|
132
|
+
: null;
|
133
|
+
}
|
134
|
+
}
|
135
|
+
/**
|
136
|
+
* @default 4
|
137
|
+
*/
|
138
|
+
get offset() {
|
139
|
+
return (this.#offset ??
|
140
|
+
Number.parseFloat(window
|
141
|
+
.getComputedStyle(document.body)
|
142
|
+
.getPropertyValue('--glide-core-spacing-base-xxs')) *
|
143
|
+
Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize));
|
144
|
+
}
|
145
|
+
set offset(offset) {
|
146
|
+
this.#offset = offset;
|
147
|
+
}
|
148
|
+
/**
|
149
|
+
* @default false
|
150
|
+
*/
|
151
|
+
get open() {
|
152
|
+
return this.#isOpen;
|
153
|
+
}
|
154
|
+
set open(isOpen) {
|
155
|
+
const hasChanged = isOpen !== this.#isOpen;
|
156
|
+
this.#isOpen = isOpen;
|
157
|
+
if (isOpen && hasChanged && !this.isTargetDisabled) {
|
158
|
+
this.#show();
|
159
|
+
this.dispatchEvent(new Event('toggle', { bubbles: true, composed: true }));
|
160
|
+
}
|
161
|
+
else if (hasChanged) {
|
162
|
+
this.#hide();
|
163
|
+
this.dispatchEvent(new Event('toggle', { bubbles: true, composed: true }));
|
164
|
+
}
|
165
|
+
}
|
166
|
+
connectedCallback() {
|
167
|
+
super.connectedCallback();
|
168
|
+
// Guarding against `#isSubMenu` isn't strictly necessary. We guard against it
|
169
|
+
// nonetheless to prevent subtle bugs being introduced later. It's hard to say what
|
170
|
+
// those bugs would be. But given the complexity of event handling throughout, it
|
171
|
+
// seems prudent to only handle document clicks only where we need to: the
|
172
|
+
// top-level Menu.
|
173
|
+
//
|
174
|
+
// Additionally, not handling document clicks for sub-Menus makes things overall
|
175
|
+
// easier to understand because developers don't have to think about sub-Menus when
|
176
|
+
// looking at `#onDocumentClick()`.
|
177
|
+
if (!this.#isSubMenu) {
|
178
|
+
// 1. The consumer has a "click" handler on an element that isn't Menu's target.
|
179
|
+
// 2. The user clicks that element.
|
180
|
+
// 3. The handler is called. It sets `open` to `true`.
|
181
|
+
// 4. The "click" bubbles up and is handled by `#onDocumentClick()`.
|
182
|
+
// 5. `#onDocumentClick()` sets `open` to `false` because the click came from
|
183
|
+
// outside Menu.
|
184
|
+
// 6. Menu is opened then immediately closed and so never opens.
|
185
|
+
//
|
186
|
+
// `capture` ensures `#onDocumentClick()` is called before #3, so that `open` set
|
187
|
+
// `true` in the consumer's handler isn't overwritten by `#onDocumentClick()`
|
188
|
+
// handler setting it to `false`.
|
189
|
+
document.addEventListener('click', this.#onDocumentClick, {
|
190
|
+
capture: true,
|
191
|
+
});
|
192
|
+
}
|
193
|
+
}
|
194
|
+
createRenderRoot() {
|
195
|
+
this.#shadowRoot = super.createRenderRoot();
|
196
|
+
return this.#shadowRoot;
|
197
|
+
}
|
198
|
+
disconnectedCallback() {
|
199
|
+
super.disconnectedCallback();
|
200
|
+
document.removeEventListener('click', this.#onDocumentClick, {
|
201
|
+
capture: true,
|
202
|
+
});
|
203
|
+
}
|
204
|
+
firstUpdated() {
|
205
|
+
if (this.#optionsElement && this.#targetElement) {
|
206
|
+
this.#optionsElement.privateLoading = this.loading;
|
207
|
+
this.#targetElement.ariaDescription = this.loading
|
208
|
+
? this.#localize.term('loading')
|
209
|
+
: null;
|
210
|
+
}
|
211
|
+
if (this.open && !this.isTargetDisabled) {
|
212
|
+
const openedSubMenus = this.#subMenus.filter(({ open }) => open);
|
213
|
+
if (openedSubMenus.length > 1) {
|
214
|
+
for (const [index, subMenu] of openedSubMenus.entries()) {
|
215
|
+
if (index !== 0) {
|
216
|
+
// We have to close all but one sub-Menu so they don't overlap. And because it
|
217
|
+
// wouldn't be clear to us or the user which open sub-Menu keyboard interactions
|
218
|
+
// should manipulate.
|
219
|
+
//
|
220
|
+
// Keeping either the first or the last open sub-Menu open is reasonable. So we
|
221
|
+
// arbitrarily keep the first one open.
|
222
|
+
subMenu.open = false;
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
}
|
227
|
+
else if (!this.open || this.isTargetDisabled) {
|
228
|
+
for (const subMenu of this.#subMenus) {
|
229
|
+
if (subMenu.open) {
|
230
|
+
// We have to do something if one or more sub-Menus are initially open and the top-
|
231
|
+
// level Menu isn't open or its target is disabled.
|
232
|
+
//
|
233
|
+
// What if we keep a sub-Menu and the top-level Menu isn't initially open? Then,
|
234
|
+
// when the top-level Menu is opened, it'll appear on top of the sub-Menu because
|
235
|
+
// the top-level Menu was opened second. So we'd have to toggle the sub-Menu
|
236
|
+
// closed then open.
|
237
|
+
//
|
238
|
+
// But what is the use case for having a sub-Menu open when the top-level Menu
|
239
|
+
// isn't open? It's possible one exists. Until a case presents itself, however,
|
240
|
+
// closing every initially sub-Menu when the top-level Menu isn't open is the
|
241
|
+
// simplest approach.
|
242
|
+
//
|
243
|
+
// `#subMenus` is an array of only the current Menu's sub-Menus. So you may wonder
|
244
|
+
// how nested sub-Menus get closed. They're closed via their `#hide()`, which
|
245
|
+
// necessarily closes a Menu's sub-Menus when the Menu is hidden.
|
246
|
+
subMenu.open = false;
|
247
|
+
}
|
248
|
+
}
|
249
|
+
}
|
250
|
+
if (this.#defaultSlotElementRef.value) {
|
251
|
+
// `popover` so Options can break out of Modal or another container that has
|
252
|
+
// `overflow: hidden`. Elements with `popover` are positioned relative to the
|
253
|
+
// viewport. Thus Floating UI in addition to `popover` until anchor positioning is
|
254
|
+
// well supported.
|
255
|
+
//
|
256
|
+
// "manual" is set here instead of in the template to circumvent Lit Analyzer,
|
257
|
+
// which isn't aware of `popover` and doesn't provide a way to disable its
|
258
|
+
// "no-unknown-attribute" rule.
|
259
|
+
//
|
260
|
+
// "manual" instead of "auto" because the latter only allows one popover to be open
|
261
|
+
// at a time. And consumers may have other popovers that need to remain open while
|
262
|
+
// this popover is open.
|
263
|
+
//
|
264
|
+
// "auto" also automatically opens the popover when its target is clicked. We want
|
265
|
+
// it to remain closed when clicked when there are no Options or Menu's target is
|
266
|
+
// disabled.
|
267
|
+
this.#defaultSlotElementRef.value.popover = 'manual';
|
268
|
+
if (this.open && !this.isTargetDisabled) {
|
269
|
+
this.#show();
|
270
|
+
}
|
271
|
+
}
|
272
|
+
}
|
273
|
+
get isTargetDisabled() {
|
274
|
+
const isDisabled = this.#targetElement &&
|
275
|
+
'disabled' in this.#targetElement &&
|
276
|
+
this.#targetElement.disabled;
|
277
|
+
const isAriaDisabled = this.#targetElement && this.#targetElement.ariaDisabled === 'true';
|
278
|
+
return Boolean(isDisabled) || Boolean(isAriaDisabled);
|
279
|
+
}
|
280
|
+
render() {
|
281
|
+
// The linter wants a "focus" handler on the default slot. And the "focusin" below
|
282
|
+
// doesn't satisfy it.
|
283
|
+
//
|
284
|
+
/* eslint-disable lit-a11y/mouse-events-have-key-events */
|
285
|
+
return html `
|
286
|
+
<div
|
287
|
+
@focusin=${this.#onComponentFocusIn}
|
288
|
+
@focusout=${this.#onComponentFocusOut}
|
289
|
+
${ref(this.#componentElementRef)}
|
290
|
+
>
|
291
|
+
<slot
|
292
|
+
class="target-slot"
|
293
|
+
name="target"
|
294
|
+
@click=${this.#onTargetSlotClick}
|
295
|
+
@keydown=${this.#onTargetAndDefaultSlotKeyDown}
|
296
|
+
@mouseup=${this.#onTargetSlotMouseUp}
|
297
|
+
@input=${this.#onTargetSlotInput}
|
298
|
+
@slotchange=${this.#onTargetSlotChange}
|
299
|
+
${assertSlot([Element])}
|
300
|
+
${ref(this.#targetSlotElementRef)}
|
301
|
+
>
|
302
|
+
<!--
|
303
|
+
The element to which Menu will anchor. Can be any focusable element unless it's
|
304
|
+
the target of a sub-Menu, in which case the element shouldn't be focusable.
|
305
|
+
|
306
|
+
If you want Menu to be filterable, put an Input in this slot. Listen for Input's
|
307
|
+
"input" event, then add and remove Option(s) from Menu's default slot based on
|
308
|
+
Input's value.
|
309
|
+
|
310
|
+
@required
|
311
|
+
@type {Element}
|
312
|
+
-->
|
313
|
+
</slot>
|
314
|
+
|
315
|
+
<slot
|
316
|
+
class="default-slot"
|
317
|
+
data-test="default-slot"
|
318
|
+
@click=${this.#onDefaultSlotClick}
|
319
|
+
@keydown=${this.#onTargetAndDefaultSlotKeyDown}
|
320
|
+
@mousedown=${this.#onDefaultSlotMouseDown}
|
321
|
+
@mouseover=${this.#onDefaultSlotMouseOver}
|
322
|
+
@mouseup=${this.#onDefaultSlotMouseUp}
|
323
|
+
@private-disabled-change=${this.#onDefaultSlotDisabledChange}
|
324
|
+
@private-slot-change=${this.#onDefaultSlotSlotChange}
|
325
|
+
@toggle=${this.#onDefaultSlotToggle}
|
326
|
+
${assertSlot([Element])}
|
327
|
+
${ref(this.#defaultSlotElementRef)}
|
328
|
+
>
|
329
|
+
<!--
|
330
|
+
@required
|
331
|
+
@type {Element}
|
332
|
+
-->
|
333
|
+
</slot>
|
334
|
+
</div>
|
335
|
+
`;
|
336
|
+
}
|
337
|
+
#cleanUpFloatingUi;
|
338
|
+
#componentElementRef;
|
339
|
+
#defaultSlotElementRef;
|
340
|
+
// Set in `#onComponentFocusIn()` and `#onComponentFocusOut()`. Used in
|
341
|
+
// `#onComponentFocusOut()` to decide if Menu should close. Also used in
|
342
|
+
// `#onTargetAndDefaultSlotKeyDown()` to decide if we need to move focus.
|
343
|
+
#hasVoiceOverMovedFocusToOptionsOrAnOption;
|
344
|
+
// Set in `#onTargetSlotMouseUp()`. Used in `#onDocumentClick()` to guard against
|
345
|
+
// Menu closing when any number of things that are not an Option are clicked. Those
|
346
|
+
// "click" events will be retargeted to Menu's host the moment they bubble out of
|
347
|
+
// Menu. So checking in `#onDocumentClick()` if the click's `event.target` came
|
348
|
+
// from inside Menu won't do.
|
349
|
+
#isDefaultSlotClick;
|
350
|
+
#isLoading;
|
351
|
+
#isOpen;
|
352
|
+
// Set in `#onDefaultSlotToggle()`. Used in `#onTargetAndDefaultSlotKeyDown()` to
|
353
|
+
// guard against redispatching the event to a sub-Menu when one isn't open. Also
|
354
|
+
// used in `connectedCallback()` to guard against listening for document clicks for
|
355
|
+
// sub-Menus.
|
356
|
+
#isSubMenuOpen;
|
357
|
+
// Set in `#onTargetSlotMouseUp()` and `#onDocumentClick()`. Used in
|
358
|
+
// `#onDocumentClick()`:
|
359
|
+
//
|
360
|
+
// 1. Menu is open.
|
361
|
+
// 2. User clicks Menu's target.
|
362
|
+
// 3. `#onDocumentClick()` sets `open` to `false`.
|
363
|
+
// 4. `#onTargetSlotClick()` sets `open` to true`.
|
364
|
+
// 5. Menu never closes.
|
365
|
+
//
|
366
|
+
// Setting `#isTargetSlotMouseUp` to `true` in `#onTargetSlotMouseUp()` gives
|
367
|
+
// `#onDocumentClick()` the information it needs to not set `open` to `false`.
|
368
|
+
//
|
369
|
+
// The normal approach would be to set an `#isTargetSlotClick` property in
|
370
|
+
// `#onTargetSlotClick()`. But `#onDocumentClick()` listens for "click" events in
|
371
|
+
// their capture phase. So `#onDocumentClick()` would be called before
|
372
|
+
// `#onTargetSlotClick()`.
|
373
|
+
//
|
374
|
+
// Note too that `#onDocumentClick()` sets `#isTargetSlotMouseUp` to `false`
|
375
|
+
// instead of `#onTargetSlotClick()` doing it. That's so `#isTargetSlotMouseUp`
|
376
|
+
// is set to `false` even if the user mouses down on Menu's target then moves the
|
377
|
+
// mouse outside of Menu before mousing up.
|
378
|
+
#isTargetSlotMouseUp;
|
379
|
+
#localize;
|
380
|
+
#offset;
|
381
|
+
// Used in various situations to reactivate the previously active Option.
|
382
|
+
#previouslyActiveOption;
|
383
|
+
#shadowRoot;
|
384
|
+
#targetSlotElementRef;
|
385
|
+
get #activeOption() {
|
386
|
+
return this.#optionElements?.find(({ privateActive }) => privateActive);
|
387
|
+
}
|
388
|
+
get #activeOptionSubMenu() {
|
389
|
+
return this.#activeOption?.querySelector('glide-core-menu');
|
390
|
+
}
|
391
|
+
get #firstEnabledOption() {
|
392
|
+
return this.#optionElements?.find(({ disabled }) => !disabled);
|
393
|
+
}
|
394
|
+
get #lastEnabledOption() {
|
395
|
+
return this.#optionElements?.findLast(({ disabled }) => !disabled);
|
396
|
+
}
|
397
|
+
get #isFilterable() {
|
398
|
+
let isKeepLooking = true;
|
399
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
400
|
+
let topLevelMenu = this;
|
401
|
+
while (isKeepLooking) {
|
402
|
+
const menu = topLevelMenu.parentElement?.closest('glide-core-menu');
|
403
|
+
isKeepLooking = Boolean(menu);
|
404
|
+
if (menu) {
|
405
|
+
topLevelMenu = menu;
|
406
|
+
}
|
407
|
+
}
|
408
|
+
const slottedTarget = topLevelMenu?.querySelector('[slot="target"]');
|
409
|
+
const target = slottedTarget instanceof HTMLSlotElement
|
410
|
+
? slottedTarget?.assignedElements().at(0)
|
411
|
+
: slottedTarget;
|
412
|
+
return target instanceof Input;
|
413
|
+
}
|
414
|
+
get #isSubMenu() {
|
415
|
+
return Boolean(this.closest('glide-core-option'));
|
416
|
+
}
|
417
|
+
get #isTargetSpanOrDivOrSvg() {
|
418
|
+
return (this.#targetElement instanceof HTMLSpanElement ||
|
419
|
+
this.#targetElement instanceof HTMLDivElement ||
|
420
|
+
this.#targetElement instanceof SVGElement);
|
421
|
+
}
|
422
|
+
get #openedSubMenu() {
|
423
|
+
return this.#subMenus.find(({ open }) => open);
|
424
|
+
}
|
425
|
+
get #optionsElement() {
|
426
|
+
return this.#defaultSlotElementRef.value
|
427
|
+
?.assignedElements({ flatten: true })
|
428
|
+
?.find((element) => element instanceof Options);
|
429
|
+
}
|
430
|
+
get #optionElements() {
|
431
|
+
// If we're dealing with a slot, then the consumer of Menu has placed a slot inside
|
432
|
+
// Options, in which case we need to get its assigned elements.
|
433
|
+
if (this.#optionsElement) {
|
434
|
+
return [...this.#optionsElement.children]
|
435
|
+
.flatMap((element) => {
|
436
|
+
return element instanceof HTMLSlotElement
|
437
|
+
? element.assignedElements({ flatten: true })
|
438
|
+
: element;
|
439
|
+
})
|
440
|
+
?.filter((element) => {
|
441
|
+
return element instanceof Option;
|
442
|
+
});
|
443
|
+
}
|
444
|
+
}
|
445
|
+
get #parentOption() {
|
446
|
+
return this.closest('glide-core-option');
|
447
|
+
}
|
448
|
+
get #subMenus() {
|
449
|
+
return [
|
450
|
+
...this.querySelectorAll(
|
451
|
+
// The "content" slot case.
|
452
|
+
':scope > glide-core-options > glide-core-option > [slot="content"] > glide-core-menu'),
|
453
|
+
...this.querySelectorAll(
|
454
|
+
// The "content" slot fallback case.
|
455
|
+
':scope > glide-core-options > glide-core-option > [slot="submenu"]'),
|
456
|
+
];
|
457
|
+
}
|
458
|
+
get #targetElement() {
|
459
|
+
const element = this.#targetSlotElementRef.value
|
460
|
+
?.assignedElements({ flatten: true })
|
461
|
+
.at(0);
|
462
|
+
if (element instanceof HTMLElement || element instanceof SVGElement) {
|
463
|
+
return element;
|
464
|
+
}
|
465
|
+
}
|
466
|
+
// An arrow function field instead of a method so `this` is closed over and set to
|
467
|
+
// the component instead of `document`.
|
468
|
+
#onDocumentClick;
|
469
|
+
// Private because consumers haver direct access to the target and can focus it
|
470
|
+
// without the help of Menu.
|
471
|
+
#focus(options) {
|
472
|
+
if (this.#targetElement && 'focus' in this.#targetElement) {
|
473
|
+
this.#targetElement?.focus(options);
|
474
|
+
}
|
475
|
+
}
|
476
|
+
#hide() {
|
477
|
+
this.#cleanUpFloatingUi?.();
|
478
|
+
this.#defaultSlotElementRef.value?.hidePopover();
|
479
|
+
if (this.#optionsElement) {
|
480
|
+
this.#optionsElement.ariaActivedescendant = '';
|
481
|
+
}
|
482
|
+
if (this.#isSubMenu && this.#parentOption) {
|
483
|
+
this.#parentOption.ariaExpanded = 'false';
|
484
|
+
}
|
485
|
+
else if (!this.#isSubMenu && this.#targetElement && !this.#isFilterable) {
|
486
|
+
this.#targetElement.ariaExpanded = 'false';
|
487
|
+
}
|
488
|
+
if (this.#activeOption) {
|
489
|
+
this.#previouslyActiveOption = this.#activeOption;
|
490
|
+
this.#activeOption.privateTooltipOpen = false;
|
491
|
+
this.#activeOption.privateActive = false;
|
492
|
+
}
|
493
|
+
for (const subMenu of this.#subMenus) {
|
494
|
+
subMenu.open = false;
|
495
|
+
}
|
496
|
+
}
|
497
|
+
#onComponentFocusIn(event) {
|
498
|
+
this.#hasVoiceOverMovedFocusToOptionsOrAnOption =
|
499
|
+
event.target instanceof Option || event.target instanceof Options;
|
500
|
+
const isFromSubMenu = event.target instanceof Element &&
|
501
|
+
event.target.closest('glide-core-menu') !== this;
|
502
|
+
// VoiceOver again. If VoiceOver has focused an Option, we make it active so it's
|
503
|
+
// displayed as such and so the correct Option is selected on Enter or Space in
|
504
|
+
// `#onTargetAndDefaultSlotKeyDown()`.
|
505
|
+
if (event.target instanceof Option &&
|
506
|
+
this.#activeOption &&
|
507
|
+
this.#optionsElement &&
|
508
|
+
!isFromSubMenu) {
|
509
|
+
this.#activeOption.privateActive = false;
|
510
|
+
this.#optionsElement.ariaActivedescendant = event.target.id;
|
511
|
+
event.target.privateActive = true;
|
512
|
+
}
|
513
|
+
}
|
514
|
+
#onComponentFocusOut(event) {
|
515
|
+
const isTargetFocused = event.relatedTarget instanceof Element &&
|
516
|
+
this.contains(event.relatedTarget);
|
517
|
+
this.#hasVoiceOverMovedFocusToOptionsOrAnOption =
|
518
|
+
event.target instanceof Option || event.target instanceof Options;
|
519
|
+
if (!isTargetFocused && !this.#hasVoiceOverMovedFocusToOptionsOrAnOption) {
|
520
|
+
this.open = false;
|
521
|
+
}
|
522
|
+
}
|
523
|
+
#onDefaultSlotClick(event) {
|
524
|
+
// When the padding or border on the default slot of a sub-Menu is clicked, the
|
525
|
+
// event will be retargeted by the browser to the sub-Menu's parent Option.
|
526
|
+
//
|
527
|
+
// The event will then get picked by this handler in the super-Menu, and the
|
528
|
+
// super-Menu will close because `event.target` will be an Option.
|
529
|
+
//
|
530
|
+
// Stopping propagation of the event at the sub-Menu prevents the event from being
|
531
|
+
// handled by the super-Menu, and the super-Menu stays open.
|
532
|
+
if (this.#isSubMenu && event.target === this.#defaultSlotElementRef.value) {
|
533
|
+
event.stopPropagation();
|
534
|
+
}
|
535
|
+
// The timeout gives consumers a chance to cancel the event to prevent Menu from
|
536
|
+
// closing.
|
537
|
+
setTimeout(() => {
|
538
|
+
// `event.target instanceof Option` because clicks can come from sub-Menu targets,
|
539
|
+
// arbitrary content in the default slot, and the default slot's border and
|
540
|
+
// padding.
|
541
|
+
//
|
542
|
+
// When arbitrary content in the default slot is clicked, Menu should remain open
|
543
|
+
// because we don't know what the arbitrary content is. So inaction is best.
|
544
|
+
// Consumers can listen for clicks on the arbitrary content and close Menu
|
545
|
+
// themselves if they need to.
|
546
|
+
//
|
547
|
+
// When the default slot's padding is clicked, Menu should remain open because the
|
548
|
+
// user most likely meant to click an Option but missed.
|
549
|
+
if (!event.defaultPrevented && event.target instanceof Option) {
|
550
|
+
this.open = false;
|
551
|
+
}
|
552
|
+
});
|
553
|
+
}
|
554
|
+
#onDefaultSlotDisabledChange(event) {
|
555
|
+
if (this.#activeOption === event.target &&
|
556
|
+
event.target instanceof Option &&
|
557
|
+
event.target.disabled &&
|
558
|
+
this.#optionElements) {
|
559
|
+
const activeOptionIndex = this.#optionElements.indexOf(this.#activeOption);
|
560
|
+
const nextEnabledOption = this.#optionElements?.find(({ disabled }, index) => {
|
561
|
+
return !disabled && index > activeOptionIndex;
|
562
|
+
});
|
563
|
+
if (nextEnabledOption && this.#optionsElement) {
|
564
|
+
this.#previouslyActiveOption = this.#activeOption;
|
565
|
+
this.#activeOption.privateActive = false;
|
566
|
+
this.#optionsElement.ariaActivedescendant = nextEnabledOption.id;
|
567
|
+
nextEnabledOption.privateActive = true;
|
568
|
+
return;
|
569
|
+
}
|
570
|
+
const previousEnabledOption = this.#optionElements.findLast(({ disabled }, index) => {
|
571
|
+
return !disabled && index < activeOptionIndex;
|
572
|
+
});
|
573
|
+
if (previousEnabledOption && this.#optionsElement) {
|
574
|
+
this.#previouslyActiveOption = this.#activeOption;
|
575
|
+
this.#activeOption.privateActive = false;
|
576
|
+
this.#optionsElement.ariaActivedescendant = previousEnabledOption.id;
|
577
|
+
previousEnabledOption.privateActive = true;
|
578
|
+
}
|
579
|
+
}
|
580
|
+
}
|
581
|
+
#onDefaultSlotMouseDown(event) {
|
582
|
+
// So that clicking an Option doesn't move focus to `document.body`.
|
583
|
+
//
|
584
|
+
// Imagine a case where the user opens Menu by clicking its target, then clicks an
|
585
|
+
// Option. The user should be able to immediately press Space or Enter afterward
|
586
|
+
// to reopen Menu. This might seem like an odd case to support. But, in practice,
|
587
|
+
// it's not uncommon for users to change modalities when interacting with
|
588
|
+
// something.
|
589
|
+
event.preventDefault();
|
590
|
+
}
|
591
|
+
#onDefaultSlotMouseOver(event) {
|
592
|
+
// Chrome has this funky Popover API bug where the popover is, for about 10
|
593
|
+
// milliseconds, rendered above where it's supposed to be whenever `showPopover()`
|
594
|
+
// is called.
|
595
|
+
//
|
596
|
+
// It's not clear if the popover is invisible during that time, or if it's visible
|
597
|
+
// and 10 milliseconds isn't enough to be noticable. Either way, it's effectively
|
598
|
+
// invisible but still picks up "mouseover" events.
|
599
|
+
//
|
600
|
+
// If the user's mouse happens to be over one of the Option(s) that's inside our
|
601
|
+
// invisible, mispositioned popover, then the Option is activated. Thus this
|
602
|
+
// variable and the guard below it.
|
603
|
+
//
|
604
|
+
// There's no mention of the above issue in it. But this bug is probably a good one
|
605
|
+
// to keep an eye on: https://issues.chromium.org/issues/364669918.
|
606
|
+
const isOutOfBounds = this.#componentElementRef.value &&
|
607
|
+
event.y < this.#componentElementRef.value.getBoundingClientRect().y;
|
608
|
+
if (!isOutOfBounds) {
|
609
|
+
const option = event.target instanceof Element &&
|
610
|
+
event.target.closest('glide-core-option');
|
611
|
+
const isSubMenuTarget = event.target instanceof Element &&
|
612
|
+
event.target.closest('[slot="target"]');
|
613
|
+
const isOwnOption = option && this.#optionElements?.includes(option);
|
614
|
+
// This handler is also called when a sub-Menu Option is hovered because sub-Menu
|
615
|
+
// Option(s) are children of their super-Menu's default slot. And hovering a
|
616
|
+
// sub-Menu Option shouldn't deactivate the super-Menu's active Option. Thus
|
617
|
+
// `isOwnOption`.
|
618
|
+
if (isOwnOption && !isSubMenuTarget && !option.disabled) {
|
619
|
+
this.#previouslyActiveOption = this.#activeOption;
|
620
|
+
if (this.#activeOption) {
|
621
|
+
this.#activeOption.privateActive = false;
|
622
|
+
}
|
623
|
+
option.privateActive = true;
|
624
|
+
if (this.#optionsElement) {
|
625
|
+
this.#optionsElement.ariaActivedescendant = event.target.id;
|
626
|
+
}
|
627
|
+
}
|
628
|
+
if (this.#isSubMenu) {
|
629
|
+
// Allowing the event to propagate from a sub-Menu's parent Option means it would
|
630
|
+
// get picked up by the super-Menu Option's Tooltip "mouseover" handler. Then it
|
631
|
+
// would open the super-Menu's tooltip.
|
632
|
+
event.stopPropagation();
|
633
|
+
}
|
634
|
+
if (isSubMenuTarget && this.#activeOption) {
|
635
|
+
// When the cursor is already inside an Option and the user mouses to the Option's
|
636
|
+
// sub-Menu target, the browser will dispatch "mouseout" followed by "mouseover".
|
637
|
+
//
|
638
|
+
// The Option's tooltip will pick up both events and will remain open because the
|
639
|
+
// tooltip will be closed then immediately reopened. But we want the tooltip to
|
640
|
+
// close when a sub-Menu target is hovered. Canceling the event stops the tooltip
|
641
|
+
// from reopening.
|
642
|
+
event.preventDefault();
|
643
|
+
}
|
644
|
+
}
|
645
|
+
}
|
646
|
+
#onDefaultSlotMouseUp() {
|
647
|
+
this.#isDefaultSlotClick = true;
|
648
|
+
}
|
649
|
+
#onDefaultSlotSlotChange() {
|
650
|
+
const wasActiveOptionRemoved = this.#optionElements?.every((option) => option !== this.#activeOption);
|
651
|
+
if (wasActiveOptionRemoved &&
|
652
|
+
this.#firstEnabledOption &&
|
653
|
+
this.#optionsElement &&
|
654
|
+
this.open &&
|
655
|
+
!this.isTargetDisabled) {
|
656
|
+
this.#firstEnabledOption.privateActive = true;
|
657
|
+
this.#optionsElement.ariaActivedescendant = this.#firstEnabledOption.id;
|
658
|
+
}
|
659
|
+
if (this.#optionElements) {
|
660
|
+
for (const option of this.#optionElements) {
|
661
|
+
const hasSubMenu = Boolean(option.querySelector('[slot="target"]'));
|
662
|
+
if (hasSubMenu) {
|
663
|
+
option.ariaHasPopup = 'true';
|
664
|
+
}
|
665
|
+
}
|
666
|
+
}
|
667
|
+
}
|
668
|
+
#onDefaultSlotToggle(event) {
|
669
|
+
this.#isSubMenuOpen = this.#subMenus.some(({ open }) => open);
|
670
|
+
if (event.target instanceof Menu_1) {
|
671
|
+
for (const subMenu of this.#subMenus) {
|
672
|
+
const isOwnSubMenu = this.#subMenus.includes(event.target);
|
673
|
+
// Menu can have more than one Option with a sub-Menu. If the event was a result
|
674
|
+
// of a sub-Menu being opened and another sub-Menu is already open, we make sure
|
675
|
+
// to close the already open sub-Menu.
|
676
|
+
//
|
677
|
+
// Nested sub-Menus also dispatch "toggle" events when they're opened. So we use
|
678
|
+
// `isOwnSubMenu` to guard against closing our own open sub-Menu when a nested
|
679
|
+
// sub-Menu is opened.
|
680
|
+
if (isOwnSubMenu &&
|
681
|
+
subMenu !== event.target &&
|
682
|
+
subMenu.open &&
|
683
|
+
event.target.open) {
|
684
|
+
subMenu.open = false;
|
685
|
+
}
|
686
|
+
}
|
687
|
+
}
|
688
|
+
}
|
689
|
+
// On both slots because VoiceOver can focus Options, causing them to emit
|
690
|
+
// "keydown" events.
|
691
|
+
#onTargetAndDefaultSlotKeyDown(event) {
|
692
|
+
const isOwnTarget = event.target === this.#targetElement;
|
693
|
+
const isChildOfOptions = event.target instanceof Element &&
|
694
|
+
event.target.closest('glide-core-options');
|
695
|
+
const isArbitraryContent = !isOwnTarget && !isChildOfOptions;
|
696
|
+
if (isArbitraryContent) {
|
697
|
+
// Arbitrary interactive content, either at the top or bottom of Menu, isn't a
|
698
|
+
// great pattern because the user has to move focus away from Menu's target to
|
699
|
+
// interact with the content. Then the user has to tab back to Menu's target to
|
700
|
+
// continue interacting with Menu. It's also not great because screenreader users
|
701
|
+
// won't know the content exists until they tab past Menu's target. Still, it's a
|
702
|
+
// pattern we have to support.
|
703
|
+
//
|
704
|
+
// If the event originated from arbitrary content, then the arbitrary content has
|
705
|
+
// focus and not Menu's target, and the user has signaled that his intention isn't
|
706
|
+
// to interact with Menu itself. So we return.
|
707
|
+
//
|
708
|
+
// Still, the page shouldn't scroll when the arbitrary content has focus and the
|
709
|
+
// user presses one of the following keys. The consumer could very well prevent
|
710
|
+
// page scroll himself. But there's a good chance he won't think to. So we do so
|
711
|
+
// for him.
|
712
|
+
const isKeyThatScrollsThePage = [
|
713
|
+
'ArrowUp',
|
714
|
+
'ArrowDown',
|
715
|
+
'ArrowRight',
|
716
|
+
'ArrowLeft',
|
717
|
+
'PageUp',
|
718
|
+
'PageDown',
|
719
|
+
'Home',
|
720
|
+
'End',
|
721
|
+
].includes(event.key);
|
722
|
+
if (isKeyThatScrollsThePage) {
|
723
|
+
event.preventDefault();
|
724
|
+
}
|
725
|
+
if (event.key === 'Escape') {
|
726
|
+
this.open = false;
|
727
|
+
this.#focus();
|
728
|
+
}
|
729
|
+
return;
|
730
|
+
}
|
731
|
+
const isOwnOption = this.#optionElements?.some((option) => option === event.target);
|
732
|
+
const isOwnOptions = this.querySelector(':scope > glide-core-options') === event.target;
|
733
|
+
// The event came from a sub-Menu if all the below conditions are met, in which
|
734
|
+
// case the state of this Menu shouldn't change. So we return.
|
735
|
+
//
|
736
|
+
// Returning also prevents a loop where the event is redispatched on the sub-Menu
|
737
|
+
// then picked up again by its super-Menu and on and on.
|
738
|
+
if (!isOwnTarget && !isOwnOption && !isOwnOptions) {
|
739
|
+
return;
|
740
|
+
}
|
741
|
+
if (this.#isSubMenuOpen) {
|
742
|
+
const subevent = new KeyboardEvent(event.type, {
|
743
|
+
bubbles: true,
|
744
|
+
cancelable: true,
|
745
|
+
key: event.key,
|
746
|
+
metaKey: event.metaKey,
|
747
|
+
shiftKey: event.shiftKey,
|
748
|
+
});
|
749
|
+
// The event is dispatched on `#openedSubMenu` instead of `#activeOptionSubMenu`
|
750
|
+
// because they may not be the same. And `#activeOptionSubMenu` may not even be
|
751
|
+
// open.
|
752
|
+
//
|
753
|
+
// Imagine a situation where Menu has two Options ("One", "Two") each with a
|
754
|
+
// sub-Menu:
|
755
|
+
//
|
756
|
+
// 1. User activates "Two" by hovering it.
|
757
|
+
// 2. User opens the sub-Menu of "Two".
|
758
|
+
// 3. User activates "One" by hovering it.
|
759
|
+
// 4. User presses ArrowUp or ArrowDown.
|
760
|
+
//
|
761
|
+
// `#activeOptionSubMenu` would be "One". But "One" isn't open and isn't the
|
762
|
+
// sub-Menu the user wants to interact with.
|
763
|
+
// Normally this rule makes sense. But here we're not dispatching an event for
|
764
|
+
// consumers. We're simply redispatching an event. So the event doesn't belong in
|
765
|
+
// the JSDoc comment.
|
766
|
+
//
|
767
|
+
// eslint-disable-next-line @crowdstrike/glide-core/event-dispatch-from-this
|
768
|
+
this.#openedSubMenu
|
769
|
+
?.querySelector('[slot="target"]')
|
770
|
+
?.dispatchEvent(subevent);
|
771
|
+
// The event is canceled in a couple situations:
|
772
|
+
//
|
773
|
+
// 1. When the user presses ArrowRight or ArrowLeft, the top-level Menu's target is
|
774
|
+
// an Input, and the active Option has sub-Menu. The event is canceled to prevent
|
775
|
+
// the insertion point from moving in addition to the sub-Menu opening or closing.
|
776
|
+
//
|
777
|
+
// 2. When the user presses ArrowUp, the top-level Menu's target is an Input, and
|
778
|
+
// an Option that's not the first Option is active. The event is canceled to
|
779
|
+
// prevent the insertion point from moving in addition to the previous Option
|
780
|
+
// being made active. Similar for Home.
|
781
|
+
//
|
782
|
+
// 3. When the user presses ArrowDown, the top-level Menu's target is an Input, and
|
783
|
+
// an Option that's not the last Option is active. The event is canceled to prevent
|
784
|
+
// the insertion point from moving in addition to the next Option being made
|
785
|
+
// active. Similar for End.
|
786
|
+
//
|
787
|
+
// 4. When the user presses ArrowRight, ArrowLeft, ArrowUp, ArrowDown, PageUp,
|
788
|
+
// PageDown, Home, or End and Menu's target is not an Input. The event is canceled
|
789
|
+
// to prevent the page from scrolling.
|
790
|
+
//
|
791
|
+
// The dispatch above is synchronous. The handling of the dispatched event is also
|
792
|
+
// synchronous. That means this entire handler will have run for the sub-Menu by
|
793
|
+
// the time we've arrived here. So the sub-Menu (or nested sub-Menu) will have had
|
794
|
+
// a chance to cancel the event.
|
795
|
+
//
|
796
|
+
// Below, the super-Menu checks if the event that was dispatched on its sub-Menu
|
797
|
+
// was canceled and, if so, cancels its own event. This happens all the way up to
|
798
|
+
// the top-level Menu, which then cancels its event to prevent the page from
|
799
|
+
// scrolling or the insertion point from moving.
|
800
|
+
//
|
801
|
+
// Why can't the top-level Menu simply cancel its own event right away in one of
|
802
|
+
// the situations above? Because Menu, rightly, has limited knowledge of sub-Menus.
|
803
|
+
// It only knows that one of its own Option(s) has a sub-Menu and so dispatches the
|
804
|
+
// event to its sub-Menu.
|
805
|
+
//
|
806
|
+
// Imagine:
|
807
|
+
//
|
808
|
+
// 1. Menu is filterable.
|
809
|
+
// 2. Menu has a sub-Menu that is open.
|
810
|
+
// 3. The sub-Menu's active Option also has a sub-Menu.
|
811
|
+
// 4. The user presses ArrowRight to open that Option's sub-Menu.
|
812
|
+
//
|
813
|
+
// The top-level Menu only knows the state of its own Option(s). It doesn't know
|
814
|
+
// which sub-Menu Option is active or if the sub-Menu Option has itself a sub-Menu.
|
815
|
+
// So it doesn't know if it should cancel the event to prevent the insertion point
|
816
|
+
// from moving. Instead sub-Menus communicate to the top-level Menu, by a cascade
|
817
|
+
// of event cancelations, that its own event should be canceled.
|
818
|
+
//
|
819
|
+
// While convoluted to explain in writing, this approach is quite simple in
|
820
|
+
// practice, and it preserves boundaries between a Menu and its sub-Menus.
|
821
|
+
if (subevent.defaultPrevented) {
|
822
|
+
event.preventDefault();
|
823
|
+
}
|
824
|
+
// This is the one case where both a super-Menu and sub-Menu both handle an event
|
825
|
+
// because it's the one case where two things need to happen. The sub-Menu needs to
|
826
|
+
// close itself. And the super-Menu needs to open the tooltip of its active Option.
|
827
|
+
//
|
828
|
+
// Note that this logic comes after the event dispatch above. That's because
|
829
|
+
// `#isSubMenuOpen` will be `true` until the sub-Menu has handled the event and
|
830
|
+
// closed itself.
|
831
|
+
if (['ArrowLeft', 'Escape'].includes(event.key) &&
|
832
|
+
this.#activeOption &&
|
833
|
+
!this.#isSubMenuOpen) {
|
834
|
+
this.#activeOption.privateTooltipOpen = true;
|
835
|
+
}
|
836
|
+
return;
|
837
|
+
}
|
838
|
+
// SPANs, DIVs, and SVGs don't emit "click" events on Enter or Space. If they did,
|
839
|
+
// Menu would be opened by `#onTargetSlotClick()` and we wouldn't have to open it
|
840
|
+
// here.
|
841
|
+
if (!this.open &&
|
842
|
+
[' ', 'Enter'].includes(event.key) &&
|
843
|
+
this.#isTargetSpanOrDivOrSvg) {
|
844
|
+
event.preventDefault(); // Prevent page scroll
|
845
|
+
this.privateOpenedViaKeyboard = true;
|
846
|
+
this.open = true;
|
847
|
+
this.privateOpenedViaKeyboard = false;
|
848
|
+
return;
|
849
|
+
}
|
850
|
+
if (!this.open && event.key === ' ' && this.#isFilterable) {
|
851
|
+
// Normally, pressing Space produces a "click" event and Menu is opened via
|
852
|
+
// `#onTargetSlotClick()`. Pressing Space in an input field, however, doesn't.
|
853
|
+
// So we have to open Menu here.
|
854
|
+
//
|
855
|
+
// The event is canceled because, as elsewhere, a single user interaction shouldn't
|
856
|
+
// produce multiple interface changes. So, if Menu is closed, pressing Space
|
857
|
+
// shouldn't both insert a space and open Menu.
|
858
|
+
event.preventDefault();
|
859
|
+
this.privateOpenedViaKeyboard = true;
|
860
|
+
this.open = true;
|
861
|
+
this.privateOpenedViaKeyboard = false;
|
862
|
+
}
|
863
|
+
if (!this.open &&
|
864
|
+
['ArrowUp', 'ArrowDown'].includes(event.key) &&
|
865
|
+
this.#optionsElement) {
|
866
|
+
// - Prevents page scroll when Menu is not filterable.
|
867
|
+
// - Prevents the insertion point from moving when Menu is filterable.
|
868
|
+
event.preventDefault();
|
869
|
+
this.privateOpenedViaKeyboard = true;
|
870
|
+
this.open = true;
|
871
|
+
this.privateOpenedViaKeyboard = false;
|
872
|
+
return;
|
873
|
+
}
|
874
|
+
if (this.open && event.key === 'Escape') {
|
875
|
+
event.preventDefault(); // Prevent Safari from leaving full screen.
|
876
|
+
this.open = false;
|
877
|
+
// If VoiceOver has moved focus, the browser will move focus to `document.body` now
|
878
|
+
// that Menu is closed. We move focus to either the parent Option or the top-level
|
879
|
+
// target instead so the VoiceOver user isn't kicked back to the top of the page.
|
880
|
+
if (this.#hasVoiceOverMovedFocusToOptionsOrAnOption && this.#isSubMenu) {
|
881
|
+
this.#parentOption?.focus();
|
882
|
+
}
|
883
|
+
else if (this.#hasVoiceOverMovedFocusToOptionsOrAnOption) {
|
884
|
+
this.#focus();
|
885
|
+
}
|
886
|
+
return;
|
887
|
+
}
|
888
|
+
// Everything below this point only applies when Option(s) and sub-Menus are
|
889
|
+
// usable. And they're not usable when Menu is loading.
|
890
|
+
if (this.loading) {
|
891
|
+
return;
|
892
|
+
}
|
893
|
+
if (this.open && event.key === 'ArrowRight' && !event.metaKey) {
|
894
|
+
if (this.#activeOptionSubMenu) {
|
895
|
+
// If the active Option has a sub-Menu, the user expects ArrowRight to open the
|
896
|
+
// sub-Menu and not to also move the insertion point.
|
897
|
+
event.preventDefault();
|
898
|
+
if (this.#activeOption) {
|
899
|
+
this.#activeOption.privateTooltipOpen = false;
|
900
|
+
}
|
901
|
+
this.#activeOptionSubMenu.privateOpenedViaKeyboard = true;
|
902
|
+
this.#activeOptionSubMenu.open = true;
|
903
|
+
this.#activeOptionSubMenu.privateOpenedViaKeyboard = false;
|
904
|
+
}
|
905
|
+
else if (!this.#isFilterable) {
|
906
|
+
event.preventDefault(); // Prevent page scroll
|
907
|
+
}
|
908
|
+
return;
|
909
|
+
}
|
910
|
+
if (this.open && event.key === 'ArrowLeft' && !event.metaKey) {
|
911
|
+
if (this.#isSubMenu) {
|
912
|
+
// If we're in a sub-Menu, the user expects ArrowLeft to close the sub-Menu and not
|
913
|
+
// to also move the insertion point.
|
914
|
+
event.preventDefault();
|
915
|
+
if (this.#activeOption) {
|
916
|
+
this.#activeOption.privateTooltipOpen = false;
|
917
|
+
}
|
918
|
+
this.open = false;
|
919
|
+
}
|
920
|
+
else if (!this.#isFilterable) {
|
921
|
+
event.preventDefault(); // Prevent page scroll
|
922
|
+
}
|
923
|
+
return;
|
924
|
+
}
|
925
|
+
if (this.open && event.key === 'Enter') {
|
926
|
+
if (!this.#isFilterable) {
|
927
|
+
// Enter will produce a "click" event. But so will `#activeOption?.click()` below.
|
928
|
+
// Canceling this event simplifies logic elsewhere so we don't have to account for
|
929
|
+
// two successive clicks and Menu closing then immediately reopening.
|
930
|
+
event.preventDefault();
|
931
|
+
// If VoiceOver has moved focus, the browser will move focus to `document.body`
|
932
|
+
// when Menu is closed after the active Option is clicked below. We move focus to
|
933
|
+
// the target instead so the VoiceOver user isn't kicked to the top of the page.
|
934
|
+
if (this.#hasVoiceOverMovedFocusToOptionsOrAnOption &&
|
935
|
+
!this.#isSubMenu) {
|
936
|
+
this.#focus();
|
937
|
+
}
|
938
|
+
}
|
939
|
+
this.#activeOption?.click();
|
940
|
+
return;
|
941
|
+
}
|
942
|
+
// `this.#isFilterable` is guarded against because pressing Space is meant to
|
943
|
+
// insert a space and not select an Option.
|
944
|
+
if (this.open && event.key === ' ' && !this.#isFilterable) {
|
945
|
+
event.preventDefault(); // Prevent page scroll
|
946
|
+
// If VoiceOver has moved focus, the browser will move focus to `document.body`
|
947
|
+
// when Menu is closed after the active Option is clicked below. We move focus to
|
948
|
+
// the target instead so the VoiceOver user isn't kicked to the top of the page.
|
949
|
+
if (this.#hasVoiceOverMovedFocusToOptionsOrAnOption && !this.#isSubMenu) {
|
950
|
+
this.#focus();
|
951
|
+
}
|
952
|
+
this.#activeOption?.click();
|
953
|
+
return;
|
954
|
+
}
|
955
|
+
if (this.open && this.#activeOption && this.#optionElements) {
|
956
|
+
const activeOptionIndex = this.#optionElements.indexOf(this.#activeOption);
|
957
|
+
if (event.key === 'ArrowUp' && !event.metaKey) {
|
958
|
+
const previousOption = this.#optionElements.findLast((option, index) => {
|
959
|
+
return !option.disabled && index < activeOptionIndex;
|
960
|
+
});
|
961
|
+
if (this.#isFilterable && previousOption) {
|
962
|
+
// Prevent the insertion point from moving to the beginning of the Input.
|
963
|
+
event.preventDefault();
|
964
|
+
}
|
965
|
+
else if (!this.#isFilterable) {
|
966
|
+
// Prevent page scroll
|
967
|
+
event.preventDefault();
|
968
|
+
}
|
969
|
+
if (previousOption && this.#optionsElement) {
|
970
|
+
this.#previouslyActiveOption = this.#activeOption;
|
971
|
+
this.#activeOption.privateTooltipOpen = false;
|
972
|
+
this.#activeOption.privateActive = false;
|
973
|
+
this.#optionsElement.ariaActivedescendant = previousOption.id;
|
974
|
+
previousOption.privateActive = true;
|
975
|
+
previousOption.privateTooltipOpen = true;
|
976
|
+
const { scrollX, scrollY } = window;
|
977
|
+
previousOption.scrollIntoView({
|
978
|
+
block: 'center', // So Options before the current one are in view.
|
979
|
+
});
|
980
|
+
// Scrolling the Option into view will scroll both its scrollable container and the
|
981
|
+
// page. Scrolling the page so the container is in view can be helpful. But more
|
982
|
+
// often than not it's disruptive. So we put scroll back where it was.
|
983
|
+
window.scrollTo(scrollX, scrollY);
|
984
|
+
}
|
985
|
+
return;
|
986
|
+
}
|
987
|
+
if (event.key === 'ArrowDown' && !event.metaKey) {
|
988
|
+
const nextOption = this.#optionElements.find((option, index) => {
|
989
|
+
return !option.disabled && index > activeOptionIndex;
|
990
|
+
});
|
991
|
+
if (this.#isFilterable && nextOption) {
|
992
|
+
// Prevent the insertion point from moving to the end of the Input.
|
993
|
+
event.preventDefault();
|
994
|
+
}
|
995
|
+
else if (!this.#isFilterable) {
|
996
|
+
// Prevent page scroll
|
997
|
+
event.preventDefault();
|
998
|
+
}
|
999
|
+
if (nextOption && this.#optionsElement) {
|
1000
|
+
this.#previouslyActiveOption = this.#activeOption;
|
1001
|
+
this.#activeOption.privateTooltipOpen = false;
|
1002
|
+
this.#activeOption.privateActive = false;
|
1003
|
+
this.#optionsElement.ariaActivedescendant = nextOption.id;
|
1004
|
+
nextOption.privateActive = true;
|
1005
|
+
nextOption.privateTooltipOpen = true;
|
1006
|
+
const { scrollX, scrollY } = window;
|
1007
|
+
nextOption.scrollIntoView({
|
1008
|
+
block: 'center', // So Options after the current one are in view.
|
1009
|
+
});
|
1010
|
+
// Scrolling the Option into view will scroll both its scrollable container and the
|
1011
|
+
// page. Scrolling the page so the container is in view can be helpful. But more
|
1012
|
+
// often than not it's disruptive. So we put scroll back where it was.
|
1013
|
+
window.scrollTo(scrollX, scrollY);
|
1014
|
+
}
|
1015
|
+
return;
|
1016
|
+
}
|
1017
|
+
if ((event.key === 'ArrowUp' && event.metaKey) ||
|
1018
|
+
['Home', 'PageUp'].includes(event.key)) {
|
1019
|
+
// - Prevents page scroll when Menu is not filterable.
|
1020
|
+
// - Prevents the insertion point from moving when Menu is filterable.
|
1021
|
+
event.preventDefault();
|
1022
|
+
if (this.#firstEnabledOption && this.#optionsElement) {
|
1023
|
+
this.#previouslyActiveOption = this.#activeOption;
|
1024
|
+
this.#activeOption.privateTooltipOpen = false;
|
1025
|
+
this.#activeOption.privateActive = false;
|
1026
|
+
this.#optionsElement.ariaActivedescendant =
|
1027
|
+
this.#firstEnabledOption.id;
|
1028
|
+
this.#firstEnabledOption.privateActive = true;
|
1029
|
+
this.#firstEnabledOption.privateTooltipOpen = true;
|
1030
|
+
const { scrollX, scrollY } = window;
|
1031
|
+
this.#firstEnabledOption.scrollIntoView();
|
1032
|
+
// Scrolling the Option into view will scroll both its scrollable container and the
|
1033
|
+
// page. Scrolling the page so the container is in view can be helpful. But more
|
1034
|
+
// often than not it's disruptive. So we put scroll back where it was.
|
1035
|
+
window.scrollTo(scrollX, scrollY);
|
1036
|
+
}
|
1037
|
+
return;
|
1038
|
+
}
|
1039
|
+
if ((event.key === 'ArrowDown' && event.metaKey) ||
|
1040
|
+
['End', 'PageDown'].includes(event.key)) {
|
1041
|
+
// - Prevents page scroll when Menu is not filterable.
|
1042
|
+
// - Prevents the insertion point from moving when Menu is filterable.
|
1043
|
+
event.preventDefault();
|
1044
|
+
if (this.#lastEnabledOption && this.#optionsElement) {
|
1045
|
+
this.#previouslyActiveOption = this.#activeOption;
|
1046
|
+
this.#activeOption.privateTooltipOpen = false;
|
1047
|
+
this.#activeOption.privateActive = false;
|
1048
|
+
this.#optionsElement.ariaActivedescendant =
|
1049
|
+
this.#lastEnabledOption.id;
|
1050
|
+
this.#lastEnabledOption.privateActive = true;
|
1051
|
+
this.#lastEnabledOption.privateTooltipOpen = true;
|
1052
|
+
const { scrollX, scrollY } = window;
|
1053
|
+
this.#lastEnabledOption.scrollIntoView();
|
1054
|
+
// Scrolling the Option into view will scroll both its scrollable container and the
|
1055
|
+
// page. Scrolling the page so the container is in view can be helpful. But more
|
1056
|
+
// often than not it's disruptive. So we put scroll back where it was.
|
1057
|
+
window.scrollTo(scrollX, scrollY);
|
1058
|
+
}
|
1059
|
+
return;
|
1060
|
+
}
|
1061
|
+
}
|
1062
|
+
}
|
1063
|
+
#onTargetSlotChange() {
|
1064
|
+
if (this.#isSubMenu && this.#targetElement instanceof Input) {
|
1065
|
+
throw new Error('An Input is semantically an invalid target for a sub-Menu.');
|
1066
|
+
}
|
1067
|
+
if (this.open && !this.isTargetDisabled) {
|
1068
|
+
this.#show();
|
1069
|
+
}
|
1070
|
+
else {
|
1071
|
+
this.#hide();
|
1072
|
+
}
|
1073
|
+
const observer = new MutationObserver(() => {
|
1074
|
+
if (this.open && !this.isTargetDisabled) {
|
1075
|
+
this.#show();
|
1076
|
+
}
|
1077
|
+
else {
|
1078
|
+
this.#hide();
|
1079
|
+
}
|
1080
|
+
});
|
1081
|
+
if (this.#targetElement && this.#optionsElement) {
|
1082
|
+
observer.observe(this.#targetElement, {
|
1083
|
+
attributeFilter: ['aria-disabled', 'disabled'],
|
1084
|
+
});
|
1085
|
+
this.#targetElement.id = this.#targetElement.id || uniqueId();
|
1086
|
+
this.#optionsElement.ariaLabelledby = this.#targetElement.id;
|
1087
|
+
if (this.#isSubMenu && this.#parentOption) {
|
1088
|
+
this.#parentOption.ariaHasPopup = 'true';
|
1089
|
+
}
|
1090
|
+
else if (!this.#isSubMenu && this.#targetElement) {
|
1091
|
+
this.#targetElement.ariaHasPopup = 'true';
|
1092
|
+
}
|
1093
|
+
if (this.#isFilterable && !this.#isSubMenu) {
|
1094
|
+
this.#targetElement.setAttribute('aria-controls', this.#optionsElement.id);
|
1095
|
+
}
|
1096
|
+
// We want consumers to use a button as Menu's target. But we've found that's not
|
1097
|
+
// always the case. So, for their convenience and to ensure accessibility, we make
|
1098
|
+
// sure the target works correctly.
|
1099
|
+
//
|
1100
|
+
// We guard against sub-Menu targets because there's no need for the user to be
|
1101
|
+
// able to focus them given the entire component, including sub-Menus, can be
|
1102
|
+
// interacted with via keyboard. That's also why we change `tabIndex` below.
|
1103
|
+
if (this.#isTargetSpanOrDivOrSvg && !this.#isSubMenu) {
|
1104
|
+
this.#targetElement.role = 'button';
|
1105
|
+
this.#targetElement.tabIndex = 0;
|
1106
|
+
}
|
1107
|
+
if (this.#isSubMenu && this.#targetElement) {
|
1108
|
+
// This won't cover every case because the target may be a custom element that has
|
1109
|
+
// a focusable element in its shadow DOM. Or, for example, the target may be a DIV
|
1110
|
+
// with a BUTTON inside it.
|
1111
|
+
//
|
1112
|
+
// We can't do anything about the former case. Best we can do is document in Menu's
|
1113
|
+
// story that sub-Menu targets shouldn't be focusable. The latter case is unhandled
|
1114
|
+
// because of the documentation and for the sake of simplicity.
|
1115
|
+
this.#targetElement.tabIndex = -1;
|
1116
|
+
}
|
1117
|
+
}
|
1118
|
+
}
|
1119
|
+
#onTargetSlotClick(event) {
|
1120
|
+
const closestOption = event.target instanceof Element &&
|
1121
|
+
event.target.closest('glide-core-option');
|
1122
|
+
const isSubMenuTarget = event.target instanceof Element && Boolean(closestOption);
|
1123
|
+
const isClosestOptionALink = closestOption instanceof Option &&
|
1124
|
+
closestOption.href !== undefined &&
|
1125
|
+
closestOption.role !== 'option';
|
1126
|
+
const didMenuCancelTheEvent = isSubMenuTarget && isClosestOptionALink;
|
1127
|
+
if (isSubMenuTarget && isClosestOptionALink) {
|
1128
|
+
// When an Option is a link and it has a sub-Menu, clicking the sub-Menu's target
|
1129
|
+
// shouldn't cause a navigation. So we cancel the event. And we have to cancel it
|
1130
|
+
// before the timeout.
|
1131
|
+
event.preventDefault();
|
1132
|
+
}
|
1133
|
+
if (isSubMenuTarget) {
|
1134
|
+
// Sub-Menu target clicks aren't let to propagate because they muddy the waters
|
1135
|
+
// for consumers. Consumers listen for "click" to know when an Option is clicked.
|
1136
|
+
// And they listen for "toggle" to know when a Menu or sub-Menu is opened or
|
1137
|
+
// closed. So sub-Menu target "click" events are just noise.
|
1138
|
+
event.stopPropagation();
|
1139
|
+
}
|
1140
|
+
// The timeout gives consumers a chance to cancel the event to prevent Menu from
|
1141
|
+
// opening.
|
1142
|
+
setTimeout(() => {
|
1143
|
+
const isClosestOptionDisabled = closestOption instanceof Option && closestOption.disabled;
|
1144
|
+
const didTheConsumerCancelTheEvent = event.defaultPrevented && !didMenuCancelTheEvent;
|
1145
|
+
if (didTheConsumerCancelTheEvent ||
|
1146
|
+
isClosestOptionDisabled ||
|
1147
|
+
this.isTargetDisabled) {
|
1148
|
+
return;
|
1149
|
+
}
|
1150
|
+
// It's `0` when the event came from the user pressing Space or Enter. It's also
|
1151
|
+
// `0` when a developer calls `click()` or dispatches the event progratically. It's
|
1152
|
+
// not ideal that we show the Option's tooltip in those cases. But it should be
|
1153
|
+
// okay.
|
1154
|
+
if (event.detail === 0) {
|
1155
|
+
this.privateOpenedViaKeyboard = true;
|
1156
|
+
}
|
1157
|
+
if (this.#optionElements &&
|
1158
|
+
this.#optionElements.length > 0 &&
|
1159
|
+
// If Menu is filterable, Menu doesn't close on click because the user may have
|
1160
|
+
// clicked the Input to select or change its text.
|
1161
|
+
this.#isFilterable &&
|
1162
|
+
// Only the top-level Menu is filterable. All other Menu targets are just buttons
|
1163
|
+
// of one kind or another. So clicking them should always close the Menu in
|
1164
|
+
// question.
|
1165
|
+
!this.#isSubMenu) {
|
1166
|
+
this.open = true;
|
1167
|
+
}
|
1168
|
+
else if (this.#optionElements && this.#optionElements.length > 0) {
|
1169
|
+
this.open = !this.open;
|
1170
|
+
}
|
1171
|
+
if (event.detail === 0) {
|
1172
|
+
this.privateOpenedViaKeyboard = false;
|
1173
|
+
}
|
1174
|
+
});
|
1175
|
+
}
|
1176
|
+
// This handler in addition to the "keydown" one because entering characters into
|
1177
|
+
// the Input should open Menu. And an "input" handler is an easy way to filter out
|
1178
|
+
// non-character input.
|
1179
|
+
#onTargetSlotInput() {
|
1180
|
+
this.open = true;
|
1181
|
+
}
|
1182
|
+
#onTargetSlotMouseUp() {
|
1183
|
+
this.#isTargetSlotMouseUp = true;
|
1184
|
+
}
|
1185
|
+
#show() {
|
1186
|
+
this.#cleanUpFloatingUi?.();
|
1187
|
+
if (this.#previouslyActiveOption &&
|
1188
|
+
!this.#previouslyActiveOption.disabled &&
|
1189
|
+
this.#optionsElement) {
|
1190
|
+
this.#previouslyActiveOption.privateActive = true;
|
1191
|
+
this.#previouslyActiveOption.privateTooltipOpen =
|
1192
|
+
this.privateOpenedViaKeyboard;
|
1193
|
+
this.#optionsElement.ariaActivedescendant =
|
1194
|
+
this.#previouslyActiveOption.id;
|
1195
|
+
}
|
1196
|
+
else if (this.#firstEnabledOption && this.#optionsElement) {
|
1197
|
+
this.#firstEnabledOption.privateActive = true;
|
1198
|
+
this.#firstEnabledOption.privateTooltipOpen =
|
1199
|
+
this.privateOpenedViaKeyboard;
|
1200
|
+
this.#previouslyActiveOption = this.#firstEnabledOption;
|
1201
|
+
this.#optionsElement.ariaActivedescendant = this.#firstEnabledOption.id;
|
1202
|
+
}
|
1203
|
+
else if (this.#optionsElement) {
|
1204
|
+
this.#optionsElement.ariaActivedescendant = '';
|
1205
|
+
}
|
1206
|
+
if (this.#targetElement && this.#defaultSlotElementRef.value) {
|
1207
|
+
this.#cleanUpFloatingUi = autoUpdate(this.#targetElement, this.#defaultSlotElementRef.value, () => {
|
1208
|
+
if (this.#targetElement && this.#defaultSlotElementRef.value) {
|
1209
|
+
computePosition(this.#targetElement, this.#defaultSlotElementRef.value, {
|
1210
|
+
placement: this.placement,
|
1211
|
+
middleware: [offset(this.offset), flip()],
|
1212
|
+
}).then(({ x, y, placement }) => {
|
1213
|
+
if (this.#targetElement && this.#defaultSlotElementRef.value) {
|
1214
|
+
this.#defaultSlotElementRef.value.dataset.placement = placement;
|
1215
|
+
Object.assign(this.#defaultSlotElementRef.value.style, {
|
1216
|
+
left: `${x}px`,
|
1217
|
+
top: `${y}px`,
|
1218
|
+
});
|
1219
|
+
if (this.#isSubMenu && this.#parentOption) {
|
1220
|
+
this.#parentOption.ariaExpanded = 'true';
|
1221
|
+
}
|
1222
|
+
else if (!this.#isSubMenu && this.#targetElement) {
|
1223
|
+
this.#targetElement.ariaExpanded = 'true';
|
1224
|
+
}
|
1225
|
+
this.#defaultSlotElementRef.value.showPopover();
|
1226
|
+
}
|
1227
|
+
if (this.#optionsElement && this.#activeOption?.id) {
|
1228
|
+
this.#optionsElement.ariaActivedescendant =
|
1229
|
+
this.#activeOption.id;
|
1230
|
+
}
|
1231
|
+
});
|
1232
|
+
}
|
1233
|
+
});
|
1234
|
+
}
|
1235
|
+
}
|
1236
|
+
};
|
1237
|
+
__decorate([
|
1238
|
+
property({ reflect: true, type: Boolean })
|
1239
|
+
], Menu.prototype, "loading", null);
|
1240
|
+
__decorate([
|
1241
|
+
property({ reflect: true, type: Number })
|
1242
|
+
], Menu.prototype, "offset", null);
|
1243
|
+
__decorate([
|
1244
|
+
property({ reflect: true, type: Boolean })
|
1245
|
+
], Menu.prototype, "open", null);
|
1246
|
+
__decorate([
|
1247
|
+
property({ reflect: true, useDefault: true })
|
1248
|
+
], Menu.prototype, "placement", void 0);
|
1249
|
+
__decorate([
|
1250
|
+
property({ type: Boolean })
|
1251
|
+
], Menu.prototype, "privateOpenedViaKeyboard", void 0);
|
1252
|
+
__decorate([
|
1253
|
+
property({ reflect: true })
|
1254
|
+
], Menu.prototype, "version", void 0);
|
1255
|
+
Menu = Menu_1 = __decorate([
|
1256
|
+
customElement('glide-core-menu'),
|
1257
|
+
final
|
1258
|
+
], Menu);
|
1259
|
+
export default Menu;
|