@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.
Files changed (124) hide show
  1. package/dist/accordion.js +240 -1
  2. package/dist/accordion.styles.js +13 -7
  3. package/dist/button-group.button.js +143 -1
  4. package/dist/button-group.button.styles.js +43 -15
  5. package/dist/button-group.js +249 -1
  6. package/dist/button-group.styles.js +10 -5
  7. package/dist/button.js +206 -1
  8. package/dist/button.styles.js +12 -7
  9. package/dist/checkbox-group.js +479 -14
  10. package/dist/checkbox-group.styles.js +5 -2
  11. package/dist/checkbox.js +519 -32
  12. package/dist/checkbox.styles.js +10 -5
  13. package/dist/drawer.js +168 -1
  14. package/dist/drawer.styles.js +5 -2
  15. package/dist/dropdown.js +2423 -123
  16. package/dist/dropdown.option.js +536 -1
  17. package/dist/dropdown.option.styles.js +5 -2
  18. package/dist/dropdown.styles.js +14 -7
  19. package/dist/form-controls-layout.js +102 -1
  20. package/dist/form-controls-layout.styles.js +5 -2
  21. package/dist/icon-button.js +139 -1
  22. package/dist/icon-button.styles.js +19 -7
  23. package/dist/icons/checked.js +28 -1
  24. package/dist/icons/chevron.js +21 -1
  25. package/dist/icons/magnifying-glass.js +23 -1
  26. package/dist/icons/pencil.js +21 -1
  27. package/dist/icons/severity-critical.js +20 -1
  28. package/dist/icons/severity-informational.js +20 -1
  29. package/dist/icons/severity-medium.js +20 -1
  30. package/dist/icons/x.js +21 -1
  31. package/dist/inline-alert.js +118 -1
  32. package/dist/inline-alert.styles.js +5 -2
  33. package/dist/input.d.ts +8 -2
  34. package/dist/input.js +505 -41
  35. package/dist/input.styles.js +25 -4
  36. package/dist/label.js +303 -1
  37. package/dist/label.styles.js +11 -5
  38. package/dist/library/assert-slot.js +136 -1
  39. package/dist/library/expect-unhandled-rejection.js +14 -1
  40. package/dist/library/expect-window-error.js +26 -1
  41. package/dist/library/final.js +18 -1
  42. package/dist/library/form-control.js +1 -1
  43. package/dist/library/localize.js +10 -1
  44. package/dist/library/mouse.js +35 -1
  45. package/dist/library/on-resize.js +24 -1
  46. package/dist/library/required.js +35 -1
  47. package/dist/library/shadow-root-mode.js +4 -1
  48. package/dist/library/unique-id.js +3 -1
  49. package/dist/link.js +92 -1
  50. package/dist/link.styles.js +10 -5
  51. package/dist/menu.d.ts +3 -2
  52. package/dist/menu.js +1259 -1
  53. package/dist/menu.styles.js +34 -17
  54. package/dist/modal.d.ts +4 -0
  55. package/dist/modal.icon-button.js +60 -1
  56. package/dist/modal.icon-button.styles.js +5 -2
  57. package/dist/modal.js +473 -1
  58. package/dist/modal.styles.js +71 -22
  59. package/dist/option.d.ts +74 -0
  60. package/dist/option.js +498 -0
  61. package/dist/option.styles.js +140 -0
  62. package/dist/{menu.options.d.ts → options.d.ts} +5 -6
  63. package/dist/options.js +130 -0
  64. package/dist/options.styles.js +21 -0
  65. package/dist/popover.js +620 -1
  66. package/dist/popover.styles.js +11 -5
  67. package/dist/radio-group.js +624 -17
  68. package/dist/radio-group.radio.js +211 -1
  69. package/dist/radio-group.radio.styles.js +9 -4
  70. package/dist/radio-group.styles.js +5 -2
  71. package/dist/slider.js +1040 -61
  72. package/dist/slider.styles.js +9 -4
  73. package/dist/spinner.js +60 -1
  74. package/dist/spinner.styles.js +5 -2
  75. package/dist/split-button.js +116 -1
  76. package/dist/split-button.primary-button.js +100 -1
  77. package/dist/split-button.primary-button.styles.js +13 -6
  78. package/dist/split-button.primary-link.js +102 -1
  79. package/dist/split-button.secondary-button.d.ts +2 -3
  80. package/dist/split-button.secondary-button.js +121 -1
  81. package/dist/split-button.secondary-button.styles.js +12 -7
  82. package/dist/split-button.styles.js +9 -4
  83. package/dist/styles/focus-outline.js +9 -3
  84. package/dist/styles/fonts.css +6 -1
  85. package/dist/styles/opacity-and-scale-animation.js +6 -3
  86. package/dist/styles/skeleton.js +6 -3
  87. package/dist/styles/variables.css +410 -1
  88. package/dist/styles/visually-hidden.js +6 -3
  89. package/dist/tab.group.js +386 -1
  90. package/dist/tab.group.styles.js +5 -2
  91. package/dist/tab.js +133 -1
  92. package/dist/tab.panel.js +93 -1
  93. package/dist/tab.panel.styles.js +11 -5
  94. package/dist/tab.styles.js +9 -4
  95. package/dist/tag.js +207 -1
  96. package/dist/tag.styles.js +10 -5
  97. package/dist/textarea.js +353 -19
  98. package/dist/textarea.styles.js +23 -4
  99. package/dist/toast.js +130 -1
  100. package/dist/toast.toasts.js +248 -25
  101. package/dist/toast.toasts.styles.js +9 -4
  102. package/dist/toggle.js +178 -1
  103. package/dist/toggle.styles.js +25 -5
  104. package/dist/tooltip.container.d.ts +2 -0
  105. package/dist/tooltip.container.js +130 -1
  106. package/dist/tooltip.container.styles.js +18 -4
  107. package/dist/tooltip.d.ts +6 -0
  108. package/dist/tooltip.js +484 -1
  109. package/dist/tooltip.styles.js +21 -5
  110. package/dist/translations/en.js +36 -1
  111. package/dist/translations/fr.js +37 -1
  112. package/dist/translations/ja.js +37 -1
  113. package/package.json +8 -12
  114. package/dist/menu.button.d.ts +0 -42
  115. package/dist/menu.button.js +0 -1
  116. package/dist/menu.button.styles.js +0 -32
  117. package/dist/menu.link.d.ts +0 -44
  118. package/dist/menu.link.js +0 -1
  119. package/dist/menu.link.styles.js +0 -35
  120. package/dist/menu.options.js +0 -1
  121. package/dist/menu.options.styles.d.ts +0 -2
  122. package/dist/menu.options.styles.js +0 -20
  123. /package/dist/{menu.button.styles.d.ts → option.styles.d.ts} +0 -0
  124. /package/dist/{menu.link.styles.d.ts → options.styles.d.ts} +0 -0
package/dist/menu.js CHANGED
@@ -1 +1,1259 @@
1
- var __decorate=this&&this.__decorate||function(t,e,i,o){var n,s=arguments.length,a=s<3?e:null===o?o=Object.getOwnPropertyDescriptor(e,i):o;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,i,o);else for(var l=t.length-1;l>=0;l--)(n=t[l])&&(a=(s<3?n(a):s>3?n(e,i,a):n(e,i))||a);return s>3&&a&&Object.defineProperty(e,i,a),a};import{html,LitElement}from"lit";import{autoUpdate,computePosition,flip,offset}from"@floating-ui/dom";import{createRef,ref}from"lit/directives/ref.js";import{customElement,property}from"lit/decorators.js";import packageJson from"../package.json"with{type:"json"};import MenuButton from"./menu.button.js";import{LocalizeController}from"./library/localize.js";import MenuLink from"./menu.link.js";import MenuOptions from"./menu.options.js";import assertSlot from"./library/assert-slot.js";import styles from"./menu.styles.js";import shadowRootMode from"./library/shadow-root-mode.js";import final from"./library/final.js";import uniqueId from"./library/unique-id.js";let Menu=class Menu extends LitElement{constructor(){super(...arguments),this.placement="bottom-start",this.version=packageJson.version,this.#t=createRef(),this.#e=createRef(),this.#i=!1,this.#o=!1,this.#n=!1,this.#s=!1,this.#a=new LocalizeController(this),this.#l=createRef(),this.#r=t=>{t.target===this.#e.value&&t.preventDefault()},this.#p=()=>{this.#i=!0},this.#c=()=>{this.#i?this.#i=!1:this.#s?this.#s=!1:(this.open=!1,this.#h&&(this.#h.ariaActivedescendant=""))},this.#d=()=>{this.#s=!0}}static{this.shadowRootOptions={...LitElement.shadowRootOptions,mode:shadowRootMode}}static{this.styles=styles}get loading(){return this.#o}set loading(t){this.#o=t;const e=this.querySelector("glide-core-menu-options");e&&this.#u&&(e.privateLoading=t,this.#u.ariaDescription=t?this.#a.term("loading"):null)}get offset(){return this.#m??Number.parseFloat(window.getComputedStyle(document.body).getPropertyValue("--glide-core-spacing-base-xxs"))*Number.parseFloat(window.getComputedStyle(document.documentElement).fontSize)}set offset(t){this.#m=t}get open(){return this.#n}set open(t){const e=t!==this.#n;this.#n=t,t&&e&&!this.isTargetDisabled?(this.#f(),this.dispatchEvent(new Event("toggle",{bubbles:!0,composed:!0}))):e&&(this.#v(),this.dispatchEvent(new Event("toggle",{bubbles:!0,composed:!0})))}connectedCallback(){super.connectedCallback(),document.addEventListener("click",this.#c,{capture:!0})}createRenderRoot(){return this.#E=super.createRenderRoot(),this.#E}disconnectedCallback(){super.disconnectedCallback(),document.removeEventListener("click",this.#c,{capture:!0})}firstUpdated(){const t=this.querySelector("glide-core-menu-options");t&&this.#u&&(t.privateLoading=this.loading,this.#u.ariaDescription=this.loading?this.#a.term("loading"):null),this.#e.value&&(this.#e.value.popover="manual",this.open&&!this.isTargetDisabled&&this.#f()),this.#l.value?.addEventListener("mouseup",this.#d),this.#e.value?.addEventListener("mousedown",this.#r),this.#e.value?.addEventListener("mouseup",this.#p)}get isTargetDisabled(){const t=this.#u&&"disabled"in this.#u&&this.#u.disabled,e=this.#u&&"true"===this.#u.ariaDisabled;return Boolean(t)||Boolean(e)}render(){return html`<div class="component" @focusout="${this.#g}" ${ref(this.#t)}><slot class="target-slot" name="target" @click="${this.#S}" @keydown="${this.#y}" @slotchange="${this.#b}" ${assertSlot([Element])} ${ref(this.#l)}></slot><slot class="default-slot" @click="${this.#D}" @focusin="${this.#M}" @keydown="${this.#y}" @mouseover="${this.#w}" @private-disabled="${this.#k}" @private-slot-change="${this.#R}" ${assertSlot([MenuOptions])} ${ref(this.#e)}></slot></div>`}#O;#t;#e;#i;#o;#n;#s;#a;#m;#E;#l;get#A(){return this.#L?.find((({privateActive:t})=>t))}#r;#p;#c;#d;#T(t){this.#u&&"focus"in this.#u&&this.#u?.focus(t)}#v(){this.#O?.(),this.#h&&(this.#h.ariaActivedescendant=""),this.#u&&(this.#u.ariaExpanded="false"),this.#e.value?.hidePopover()}#g(t){const e=t.relatedTarget instanceof HTMLElement&&this.#E?.contains(t.relatedTarget),i=t.relatedTarget instanceof MenuOptions,o=t.relatedTarget instanceof MenuButton||t.relatedTarget instanceof MenuLink;e||i||o||(this.open=!1)}#D(t){t.defaultPrevented||t.target===this.#e.value||(this.open=!1)}#M(t){(t.target instanceof MenuButton||t.target instanceof MenuLink)&&this.#A&&this.#h&&!t.target.disabled&&(this.#A.privateActive=!1,t.target.privateActive=!0,this.#h.ariaActivedescendant=t.target.id)}#w(t){if((t.target instanceof MenuLink||t.target instanceof MenuButton)&&!t.target.disabled){if(this.#L)for(const e of this.#L)e.privateActive=e===t.target;this.#h&&(this.#h.ariaActivedescendant=t.target.id)}}#k(){if(this.#L&&this.#A){const t=this.#L.indexOf(this.#A);this.#A.privateActive=!1;const e=this.#L?.find(((e,i)=>!e.disabled&&i>t));if(e)return void(e.privateActive=!0);const i=this.#L.findLast(((e,i)=>!e.disabled&&i<t));i&&(i.privateActive=!0)}}#R(){const t=this.#L?.find((t=>!t.disabled));!this.#A&&t&&(t.privateActive=!0)}#y(t){const e=this.#u instanceof HTMLSpanElement||this.#u instanceof HTMLDivElement;if([" ","Enter"].includes(t.key)&&this.open)return this.#i=!0,t.preventDefault()," "===t.key&&e&&t.preventDefault(),this.#T(),void this.#A?.click();if([" ","Enter"].includes(t.key)&&!this.open&&e)return t.preventDefault(),void(this.open=!0);if(["Escape"].includes(t.key)&&this.open)return t.preventDefault(),this.open=!1,void this.#T();if(["ArrowUp","ArrowDown"].includes(t.key)&&!this.open&&this.#A&&this.#h)return t.preventDefault(),this.open=!0,void(this.#h.ariaActivedescendant=this.#A.id);if(this.open&&this.#A&&this.#L){const e=this.#L.indexOf(this.#A);if("ArrowUp"===t.key&&!t.metaKey){t.preventDefault();const i=this.#L.findLast(((t,i)=>!t.disabled&&i<e));return void(i&&this.#h&&(this.#A.privateActive=!1,this.#h.ariaActivedescendant=i.id,i.privateActive=!0))}if("ArrowDown"===t.key&&!t.metaKey){t.preventDefault();const i=this.#L.find(((t,i)=>!t.disabled&&i>e));return void(i&&this.#h&&(this.#A.privateActive=!1,this.#h.ariaActivedescendant=i.id,i.privateActive=!0))}if("ArrowUp"===t.key&&t.metaKey||"Home"===t.key||"PageUp"===t.key){t.preventDefault();const e=[...this.#L].reverse().findLast((t=>!t.disabled));return void(e&&this.#h&&(this.#A.privateActive=!1,this.#h.ariaActivedescendant=e.id,e.privateActive=!0))}if("ArrowDown"===t.key&&t.metaKey||"End"===t.key||"PageDown"===t.key){t.preventDefault();const e=[...this.#L].findLast((t=>!t.disabled));return void(e&&this.#h&&(this.#A.privateActive=!1,this.#h.ariaActivedescendant=e.id,e.privateActive=!0))}}}#b(){const t=new MutationObserver((()=>{this.open&&!this.isTargetDisabled?this.#f():this.#v()}));this.#u&&this.#h&&(t.observe(this.#u,{attributes:!0,attributeFilter:["aria-disabled","disabled"]}),this.#u.ariaHasPopup="true",this.#u.id=uniqueId(),this.#u.setAttribute("aria-controls",this.#h.id),this.#h.ariaLabelledby=this.#u.id);(this.#u instanceof HTMLSpanElement||this.#u instanceof HTMLDivElement)&&this.#u instanceof HTMLElement&&(this.#u.tabIndex=0),this.open&&!this.isTargetDisabled?this.#f():this.#v()}#S(t){t.defaultPrevented||(this.isTargetDisabled?this.#v():this.#L&&this.#L.length>0&&(this.open=!this.open))}get#h(){const t=this.#e.value?.assignedElements().at(0);return t instanceof MenuOptions?t:null}get#L(){let t=this.#e.value?.assignedElements()?.at(0)?.children;const e=t?.[0];if(e instanceof HTMLSlotElement&&(t=e.assignedElements()),t)return[...t].filter((t=>t instanceof MenuLink||t instanceof MenuButton))}#f(){this.#O?.(),this.#u&&this.#e.value&&(this.#O=autoUpdate(this.#u,this.#e.value,(()=>{(async()=>{if(this.#u&&this.#e.value){const{x:t,y:e,placement:i}=await computePosition(this.#u,this.#e.value,{placement:this.placement,middleware:[offset(this.offset),flip()]});this.#e.value.dataset.placement=i,Object.assign(this.#e.value.style,{left:`${t}px`,top:`${e}px`})}this.#e.value?.showPopover(),this.#h&&this.#A?.id&&(this.#h.ariaActivedescendant=this.#A.id),this.#u&&(this.#u.ariaExpanded="true")})()})))}get#u(){return this.#l.value?.assignedElements().at(0)}};__decorate([property({reflect:!0,type:Boolean})],Menu.prototype,"loading",null),__decorate([property({reflect:!0,type:Number})],Menu.prototype,"offset",null),__decorate([property({reflect:!0,type:Boolean})],Menu.prototype,"open",null),__decorate([property({reflect:!0,useDefault:!0})],Menu.prototype,"placement",void 0),__decorate([property({reflect:!0})],Menu.prototype,"version",void 0),Menu=__decorate([customElement("glide-core-menu"),final],Menu);export default Menu;
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;