@redvars/peacock 3.2.10 → 3.3.1

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 (242) hide show
  1. package/dist/{PeacockComponent-CxJc63xj.js → IndividualComponent-tDnXrOLV.js} +3 -3
  2. package/dist/IndividualComponent-tDnXrOLV.js.map +1 -0
  3. package/dist/assets/components.css +1 -1
  4. package/dist/assets/components.css.map +1 -1
  5. package/dist/assets/styles.css +1 -1
  6. package/dist/assets/styles.css.map +1 -1
  7. package/dist/button-group-DA7xoziD.js +292 -0
  8. package/dist/button-group-DA7xoziD.js.map +1 -0
  9. package/dist/button-group.js +6 -107
  10. package/dist/button-group.js.map +1 -1
  11. package/dist/{button-DaL4va7Q.js → button-trIfcqC7.js} +21 -35
  12. package/dist/button-trIfcqC7.js.map +1 -0
  13. package/dist/button.js +5 -5
  14. package/dist/chart-donut.js +307 -0
  15. package/dist/chart-donut.js.map +1 -0
  16. package/dist/chart-doughnut.js +307 -0
  17. package/dist/chart-doughnut.js.map +1 -0
  18. package/dist/chart-pie.js +259 -0
  19. package/dist/chart-pie.js.map +1 -0
  20. package/dist/{class-map-BvQRv7eW.js → class-map-hJdvjl-W.js} +9 -3
  21. package/dist/class-map-hJdvjl-W.js.map +1 -0
  22. package/dist/clock.js +5 -6
  23. package/dist/clock.js.map +1 -1
  24. package/dist/code-editor.js +38 -25
  25. package/dist/code-editor.js.map +1 -1
  26. package/dist/code-highlighter.js +10 -14
  27. package/dist/code-highlighter.js.map +1 -1
  28. package/dist/custom-elements-jsdocs.json +8144 -3654
  29. package/dist/custom-elements.json +7925 -3901
  30. package/dist/{dispatch-event-utils-vbdiOSeC.js → dispatch-event-utils-B4odODQf.js} +2 -15
  31. package/dist/dispatch-event-utils-B4odODQf.js.map +1 -0
  32. package/dist/index.js +13 -10
  33. package/dist/index.js.map +1 -1
  34. package/dist/number-counter.js +12 -10
  35. package/dist/number-counter.js.map +1 -1
  36. package/dist/{observe-theme-change-NneLARW8.js → observe-theme-change-BISF-Gl5.js} +2 -2
  37. package/dist/{observe-theme-change-NneLARW8.js.map → observe-theme-change-BISF-Gl5.js.map} +1 -1
  38. package/dist/peacock-loader.js +94 -502
  39. package/dist/peacock-loader.js.map +1 -1
  40. package/dist/query-QBcUV-L_.js +15 -0
  41. package/dist/query-QBcUV-L_.js.map +1 -0
  42. package/dist/src/IndividualComponent.d.ts +1 -0
  43. package/dist/src/accordion/{accordion-item/accordion-item.d.ts → accordion-item.d.ts} +5 -4
  44. package/dist/src/accordion/{accordion/accordion.d.ts → accordion.d.ts} +6 -6
  45. package/dist/src/accordion/{accordion-item/index.d.ts → index.d.ts} +1 -0
  46. package/dist/src/avatar/avatar.d.ts +2 -2
  47. package/dist/src/badge/badge.d.ts +2 -2
  48. package/dist/src/breadcrumb/breadcrumb/breadcrumb.d.ts +9 -8
  49. package/dist/src/breadcrumb/breadcrumb-item/breadcrumb-item.d.ts +4 -3
  50. package/dist/src/button/button/button.d.ts +2 -2
  51. package/dist/src/button/button-group/button-group.d.ts +9 -5
  52. package/dist/src/button/icon-button/icon-button.d.ts +2 -2
  53. package/dist/src/chart-donut/chart-donut.d.ts +53 -0
  54. package/dist/src/chart-donut/index.d.ts +1 -0
  55. package/dist/src/chart-doughnut/chart-doughnut.d.ts +53 -0
  56. package/dist/src/chart-doughnut/index.d.ts +1 -0
  57. package/dist/src/chart-pie/chart-pie.d.ts +50 -0
  58. package/dist/src/chart-pie/index.d.ts +1 -0
  59. package/dist/src/checkbox/checkbox.d.ts +3 -6
  60. package/dist/src/chip/chip/chip.d.ts +4 -4
  61. package/dist/src/chip/tag/tag.d.ts +3 -3
  62. package/dist/src/clock/clock.d.ts +3 -4
  63. package/dist/src/code-editor/code-editor.d.ts +13 -10
  64. package/dist/src/code-highlighter/code-highlighter.d.ts +4 -7
  65. package/dist/src/container/container.d.ts +6 -11
  66. package/dist/src/date-picker/date-picker.d.ts +3 -3
  67. package/dist/src/divider/divider.d.ts +2 -2
  68. package/dist/src/elevation/elevation.d.ts +2 -2
  69. package/dist/src/empty-state/empty-state.d.ts +9 -2
  70. package/dist/src/field/field.d.ts +17 -0
  71. package/dist/src/focus-ring/focus-ring.d.ts +1 -1
  72. package/dist/src/icon/icon.d.ts +2 -2
  73. package/dist/src/image/image.d.ts +4 -12
  74. package/dist/src/index.d.ts +9 -1
  75. package/dist/src/input/input.d.ts +2 -2
  76. package/dist/src/link/link.d.ts +4 -5
  77. package/dist/src/menu/index.d.ts +3 -0
  78. package/dist/src/menu/menu/MenuSurfaceController.d.ts +18 -0
  79. package/dist/src/menu/menu/menu.d.ts +66 -8
  80. package/dist/src/menu/menu-item/menu-item.d.ts +24 -5
  81. package/dist/src/menu/sub-menu/sub-menu.d.ts +36 -0
  82. package/dist/src/number-counter/number-counter.d.ts +9 -7
  83. package/dist/src/number-field/number-field.d.ts +1 -1
  84. package/dist/src/pagination/index.d.ts +1 -0
  85. package/dist/src/pagination/pagination.d.ts +38 -0
  86. package/dist/src/popover/PopoverController.d.ts +4 -1
  87. package/dist/src/popover/index.d.ts +1 -1
  88. package/dist/src/progress/circular-progress/circular-progress.d.ts +3 -3
  89. package/dist/src/progress/linear-progress/linear-progress.d.ts +3 -3
  90. package/dist/src/ripple/ripple.d.ts +60 -4
  91. package/dist/src/skeleton/skeleton.d.ts +6 -5
  92. package/dist/src/slider/index.d.ts +1 -0
  93. package/dist/src/slider/slider.d.ts +52 -0
  94. package/dist/src/spinner/spinner.d.ts +2 -2
  95. package/dist/src/switch/switch.d.ts +2 -2
  96. package/dist/src/table/index.d.ts +1 -0
  97. package/dist/src/table/table.d.ts +110 -0
  98. package/dist/src/tabs/index.d.ts +4 -0
  99. package/dist/src/tabs/tab-group.d.ts +45 -0
  100. package/dist/src/tabs/tab-panel.d.ts +22 -0
  101. package/dist/src/tabs/tab.d.ts +59 -0
  102. package/dist/src/tabs/tabs.d.ts +29 -0
  103. package/dist/src/textarea/textarea.d.ts +3 -3
  104. package/dist/src/time-picker/time-picker.d.ts +3 -3
  105. package/dist/src/{popover/tooltip → tooltip}/tooltip.d.ts +4 -3
  106. package/dist/src/tree-view/index.d.ts +2 -0
  107. package/dist/src/tree-view/tree-node.d.ts +69 -0
  108. package/dist/src/tree-view/tree-view.d.ts +40 -0
  109. package/dist/src/tree-view/wc-tree-view.d.ts +6 -0
  110. package/dist/{style-map-B8xgVEc9.js → style-map-CfNHEkQp.js} +2 -2
  111. package/dist/{style-map-B8xgVEc9.js.map → style-map-CfNHEkQp.js.map} +1 -1
  112. package/dist/test/icon.test.d.ts +1 -1
  113. package/dist/test/menu.test.d.ts +1 -0
  114. package/dist/test/sub-menu.test.d.ts +1 -0
  115. package/dist/test/tree-view.test.d.ts +1 -0
  116. package/dist/transform-DRuHEvar.js +3312 -0
  117. package/dist/transform-DRuHEvar.js.map +1 -0
  118. package/dist/{image-v3BujlY5.js → tree-view-CLolVlU0.js} +4088 -672
  119. package/dist/tree-view-CLolVlU0.js.map +1 -0
  120. package/dist/tsconfig.tsbuildinfo +1 -1
  121. package/dist/{unsafe-html-B-dV3Jps.js → unsafe-html-CV6Je6HL.js} +2 -2
  122. package/dist/{unsafe-html-B-dV3Jps.js.map → unsafe-html-CV6Je6HL.js.map} +1 -1
  123. package/package.json +3 -1
  124. package/readme.md +40 -40
  125. package/src/{PeacockComponent.ts → IndividualComponent.ts} +1 -1
  126. package/src/accordion/{accordion-item/accordion-item.scss → accordion-item.scss} +1 -1
  127. package/src/accordion/{accordion-item/accordion-item.ts → accordion-item.ts} +7 -6
  128. package/src/accordion/{accordion/accordion.scss → accordion.scss} +2 -1
  129. package/src/accordion/{accordion/accordion.ts → accordion.ts} +6 -6
  130. package/src/accordion/{accordion-item/index.ts → index.ts} +2 -0
  131. package/src/avatar/avatar.ts +2 -2
  132. package/src/badge/badge.ts +2 -2
  133. package/src/breadcrumb/breadcrumb/breadcrumb.ts +10 -8
  134. package/src/breadcrumb/breadcrumb-item/breadcrumb-item.ts +4 -3
  135. package/src/button/BaseButton.ts +1 -1
  136. package/src/button/button/button.scss +9 -23
  137. package/src/button/button/button.ts +8 -8
  138. package/src/button/button-group/button-group.ts +13 -7
  139. package/src/button/icon-button/icon-button.ts +8 -8
  140. package/src/chart-donut/chart-donut.scss +37 -0
  141. package/src/chart-donut/chart-donut.ts +287 -0
  142. package/src/chart-donut/demo/index.html +51 -0
  143. package/src/chart-donut/index.ts +1 -0
  144. package/src/chart-doughnut/chart-donut.scss +37 -0
  145. package/src/chart-doughnut/chart-doughnut.ts +287 -0
  146. package/src/chart-doughnut/demo/index.html +51 -0
  147. package/src/chart-doughnut/index.ts +1 -0
  148. package/src/chart-pie/chart-pie.scss +27 -0
  149. package/src/chart-pie/chart-pie.ts +256 -0
  150. package/src/chart-pie/demo/index.html +51 -0
  151. package/src/chart-pie/index.ts +1 -0
  152. package/src/checkbox/checkbox.ts +3 -6
  153. package/src/chip/chip/chip.ts +6 -6
  154. package/src/chip/tag/tag.ts +6 -6
  155. package/src/clock/clock.ts +5 -6
  156. package/src/code-editor/code-editor.scss +3 -5
  157. package/src/code-editor/code-editor.ts +32 -16
  158. package/src/code-highlighter/code-highlighter.ts +8 -11
  159. package/src/container/container.ts +6 -11
  160. package/src/date-picker/date-picker.ts +7 -7
  161. package/src/divider/divider.scss +2 -2
  162. package/src/divider/divider.ts +2 -2
  163. package/src/elevation/elevation.ts +2 -2
  164. package/src/empty-state/empty-state.scss +1 -1
  165. package/src/empty-state/empty-state.ts +10 -3
  166. package/src/field/field.scss +4 -4
  167. package/src/field/field.ts +19 -2
  168. package/src/focus-ring/focus-ring.scss +2 -1
  169. package/src/focus-ring/focus-ring.ts +1 -1
  170. package/src/icon/icon.ts +2 -2
  171. package/src/icon/p-icon.ts +1 -1
  172. package/src/image/image.ts +4 -12
  173. package/src/index.ts +11 -3
  174. package/src/input/input.ts +6 -6
  175. package/src/link/link.ts +4 -5
  176. package/src/menu/index.ts +3 -0
  177. package/src/menu/menu/MenuSurfaceController.ts +61 -0
  178. package/src/menu/{menu-list/menu-list.scss → menu/menu.scss} +19 -4
  179. package/src/menu/menu/menu.ts +401 -77
  180. package/src/menu/menu-item/menu-item-colors.scss +2 -2
  181. package/src/menu/menu-item/menu-item.ts +128 -37
  182. package/src/menu/sub-menu/sub-menu.scss +7 -0
  183. package/src/menu/sub-menu/sub-menu.ts +243 -0
  184. package/src/number-counter/demo/index.html +1 -1
  185. package/src/number-counter/number-counter.ts +11 -9
  186. package/src/number-field/number-field.ts +7 -7
  187. package/src/pagination/index.ts +1 -0
  188. package/src/pagination/pagination.scss +59 -0
  189. package/src/pagination/pagination.ts +135 -0
  190. package/src/peacock-loader.ts +92 -51
  191. package/src/popover/PopoverController.ts +13 -7
  192. package/src/popover/index.ts +1 -1
  193. package/src/progress/circular-progress/circular-progress.scss +1 -1
  194. package/src/progress/circular-progress/circular-progress.ts +3 -3
  195. package/src/progress/linear-progress/linear-progress.ts +3 -3
  196. package/src/ripple/ripple.ts +478 -94
  197. package/src/skeleton/skeleton.ts +6 -5
  198. package/src/slider/index.ts +1 -0
  199. package/src/slider/slider.scss +130 -0
  200. package/src/slider/slider.ts +178 -0
  201. package/src/spinner/spinner.ts +2 -2
  202. package/src/switch/switch.ts +4 -4
  203. package/src/table/index.ts +1 -0
  204. package/src/table/table.scss +174 -0
  205. package/src/table/table.ts +475 -0
  206. package/src/tabs/index.ts +4 -0
  207. package/src/tabs/tab-group.scss +10 -0
  208. package/src/tabs/tab-group.ts +143 -0
  209. package/src/tabs/tab-panel.scss +12 -0
  210. package/src/tabs/tab-panel.ts +29 -0
  211. package/src/tabs/tab.scss +157 -0
  212. package/src/tabs/tab.ts +243 -0
  213. package/src/tabs/tabs.scss +19 -0
  214. package/src/tabs/tabs.ts +66 -0
  215. package/src/text/text.css-component.scss +6 -3
  216. package/src/textarea/textarea.ts +5 -5
  217. package/src/time-picker/time-picker.ts +7 -7
  218. package/src/{popover/tooltip → tooltip}/tooltip.scss +17 -14
  219. package/src/{popover/tooltip → tooltip}/tooltip.ts +12 -10
  220. package/src/tree-view/demo/index.html +57 -0
  221. package/src/tree-view/index.ts +2 -0
  222. package/src/tree-view/tree-node.scss +101 -0
  223. package/src/tree-view/tree-node.ts +268 -0
  224. package/src/tree-view/tree-view.scss +12 -0
  225. package/src/tree-view/tree-view.ts +182 -0
  226. package/src/tree-view/wc-tree-view.ts +9 -0
  227. package/dist/PeacockComponent-CxJc63xj.js.map +0 -1
  228. package/dist/button-DaL4va7Q.js.map +0 -1
  229. package/dist/class-map-BvQRv7eW.js.map +0 -1
  230. package/dist/dispatch-event-utils-vbdiOSeC.js.map +0 -1
  231. package/dist/image-v3BujlY5.js.map +0 -1
  232. package/dist/src/PeacockComponent.d.ts +0 -1
  233. package/dist/src/accordion/accordion/index.d.ts +0 -1
  234. package/dist/src/avatar/p-avatar.d.ts +0 -3
  235. package/dist/src/badge/p-badge.d.ts +0 -3
  236. package/dist/src/menu/menu-list/menu-list.d.ts +0 -7
  237. package/dist/state-B09bP3XH.js +0 -10
  238. package/dist/state-B09bP3XH.js.map +0 -1
  239. package/src/accordion/accordion/index.ts +0 -1
  240. package/src/avatar/p-avatar.ts +0 -5
  241. package/src/badge/p-badge.ts +0 -5
  242. package/src/menu/menu-list/menu-list.ts +0 -33
@@ -1,120 +1,444 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement, property, query, state } from 'lit/decorators.js';
1
+ import { LitElement, html } from 'lit';
2
+ import { property, query, state } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
4
+ import type { Placement } from '@floating-ui/dom';
5
+ import styles from './menu.scss';
6
+ import { MenuItem } from '../menu-item/menu-item.js';
7
+ import { MenuSurfaceController } from './MenuSurfaceController.js';
3
8
 
9
+ type CloseReason =
10
+ | { kind: 'click-selection' }
11
+ | { kind: 'keydown'; key: string }
12
+ | { kind: 'outside-click' }
13
+ | { kind: 'focusout' }
14
+ | { kind: 'programmatic' };
15
+
16
+ /**
17
+ * @label Menu
18
+ * @tag wc-menu
19
+ * @rawTag menu
20
+ * @summary A list of menu items.
21
+ * @tags navigation
22
+ *
23
+ * @example
24
+ * ```html
25
+ * <wc-menu>
26
+ * <wc-menu-item>Item 1</wc-menu-item>
27
+ * <wc-menu-item>Item 2</wc-menu-item>
28
+ * </wc-menu>
29
+ * ```
30
+ */
4
31
  export class Menu extends LitElement {
32
+ static styles = [styles];
33
+
34
+ static Item = MenuItem;
35
+
5
36
  @property({ type: Boolean, reflect: true }) open = false;
6
37
 
7
- // Position: 'bottom-start' | 'bottom-end' | etc. (Simplified here to generic dropdown)
8
- @property({ type: String }) align = 'start';
38
+ @property({ type: String, reflect: true }) variant: 'standard' | 'vibrant' =
39
+ 'standard';
9
40
 
10
- @query('.menu-wrapper') menuWrapper!: HTMLElement;
41
+ @property({ type: String }) anchor = '';
11
42
 
12
- private _boundClickOutside: (e: MouseEvent) => void;
43
+ @property({ type: Boolean, attribute: 'stay-open-on-outside-click' })
44
+ stayOpenOnOutsideClick = false;
13
45
 
14
- constructor() {
15
- super();
16
- this._boundClickOutside = this._handleClickOutside.bind(this);
17
- }
46
+ @property({ type: Boolean, attribute: 'stay-open-on-focusout' })
47
+ stayOpenOnFocusout = false;
48
+
49
+ @property({ type: Boolean, attribute: 'is-submenu' }) isSubmenu = false;
50
+
51
+ @property({ type: String }) placement: Placement = 'bottom-start';
52
+
53
+ @property({ type: Number }) offset = 6;
54
+
55
+ @state() private activeIndex = -1;
56
+
57
+ @query('.menu') private readonly menuListElement!: HTMLElement;
58
+
59
+ anchorElement: HTMLElement | null = null;
60
+
61
+ private readonly _surfaceController = new MenuSurfaceController(this);
62
+
63
+ private _lastFocusedElement: HTMLElement | null = null;
64
+
65
+ private _closeReason: CloseReason = { kind: 'programmatic' };
18
66
 
19
67
  connectedCallback() {
20
68
  // eslint-disable-next-line wc/guard-super-call
21
69
  super.connectedCallback();
22
- window.addEventListener('click', this._boundClickOutside);
23
- // Listen for menu-item clicks bubbling up
24
- this.addEventListener('click', this._handleItemClick);
70
+ this.setAttribute('role', 'menu');
71
+
72
+ this.addEventListener('keydown', this._onKeyDown);
73
+ this.addEventListener('focusout', this._onFocusOut);
74
+ this.addEventListener('menu-item-activate', this._onItemActivate);
75
+ this.addEventListener('menu-item-request-close', this._onItemRequestClose);
76
+ window.addEventListener('click', this._onWindowClick, { capture: true });
77
+ this._syncAnchorAria();
25
78
  }
26
79
 
27
80
  disconnectedCallback() {
28
- // eslint-disable-next-line wc/guard-super-call
81
+ this.removeEventListener('keydown', this._onKeyDown);
82
+ this.removeEventListener('focusout', this._onFocusOut);
83
+ this.removeEventListener('menu-item-activate', this._onItemActivate);
84
+ this.removeEventListener(
85
+ 'menu-item-request-close',
86
+ this._onItemRequestClose,
87
+ );
88
+ window.removeEventListener('click', this._onWindowClick, { capture: true });
29
89
  super.disconnectedCallback();
30
- window.removeEventListener('click', this._boundClickOutside);
31
- this.removeEventListener('click', this._handleItemClick);
32
90
  }
33
91
 
34
- private _handleClickOutside(e: MouseEvent) {
35
- if (!this.open) return;
92
+ get items(): MenuItem[] {
93
+ const slot = this.shadowRoot?.querySelector('slot');
94
+ const elements = slot?.assignedElements({ flatten: true }) ?? [];
95
+ const items: MenuItem[] = [];
96
+
97
+ for (const element of elements) {
98
+ if (element instanceof MenuItem) {
99
+ items.push(element);
100
+ } else {
101
+ const maybeItem = (element as { item?: unknown }).item;
102
+ if (maybeItem instanceof MenuItem) {
103
+ items.push(maybeItem);
104
+ }
105
+ }
106
+ }
107
+
108
+ return items;
109
+ }
110
+
111
+ show() {
112
+ if (this.open) {
113
+ return;
114
+ }
115
+
116
+ this._closeReason = { kind: 'programmatic' };
117
+ this.open = true;
118
+ }
119
+
120
+ close(reason: CloseReason = { kind: 'programmatic' }) {
121
+ if (!this.open) {
122
+ return;
123
+ }
124
+
125
+ this._closeReason = reason;
126
+ this.open = false;
127
+ }
128
+
129
+ override focus() {
130
+ const target = this._getActiveItem() ?? this._getFirstEnabledItem();
131
+ target?.focus();
132
+ }
133
+
134
+ private _resolveAnchorElement() {
135
+ if (this.anchorElement) {
136
+ return this.anchorElement;
137
+ }
138
+
139
+ if (!this.anchor) {
140
+ return null;
141
+ }
142
+
143
+ const root = this.getRootNode() as Document | ShadowRoot;
144
+ if ('getElementById' in root) {
145
+ return root.getElementById(this.anchor);
146
+ }
147
+
148
+ return document.getElementById(this.anchor);
149
+ }
150
+
151
+ private _syncAnchorAria() {
152
+ const anchorEl = this._resolveAnchorElement();
153
+ if (!anchorEl) {
154
+ return;
155
+ }
156
+
157
+ if (!this.id) {
158
+ this.id = `wc-menu-${Math.random().toString(36).slice(2, 9)}`;
159
+ }
160
+
161
+ anchorEl.setAttribute('aria-haspopup', 'menu');
162
+ anchorEl.setAttribute('aria-controls', this.id);
163
+ anchorEl.setAttribute('aria-expanded', String(this.open));
164
+ }
165
+
166
+ private _enabledItems() {
167
+ return this.items.filter(item => !item.disabled);
168
+ }
169
+
170
+ private _syncRovingTabIndex() {
171
+ const enabledItems = this._enabledItems();
172
+ if (!enabledItems.length) {
173
+ this.activeIndex = -1;
174
+ return;
175
+ }
176
+
177
+ if (this.activeIndex < 0 || this.activeIndex >= enabledItems.length) {
178
+ this.activeIndex = 0;
179
+ }
180
+
181
+ for (let index = 0; index < enabledItems.length; index += 1) {
182
+ const currentItem = enabledItems[index];
183
+ currentItem.tabIndex = index === this.activeIndex ? 0 : -1;
184
+ currentItem.selected = index === this.activeIndex;
185
+ }
186
+ }
187
+
188
+ private _setActiveByOffset(offset: 1 | -1) {
189
+ const enabledItems = this._enabledItems();
190
+ if (!enabledItems.length) {
191
+ return;
192
+ }
36
193
 
37
- const path = e.composedPath();
38
- if (!path.includes(this)) {
39
- this.open = false;
194
+ if (this.activeIndex < 0) {
195
+ this.activeIndex = 0;
196
+ } else {
197
+ const count = enabledItems.length;
198
+ this.activeIndex = (this.activeIndex + offset + count) % count;
40
199
  }
200
+
201
+ this._syncRovingTabIndex();
202
+ enabledItems[this.activeIndex]?.focus();
41
203
  }
42
204
 
43
- private _handleItemClick(e: Event) {
44
- const target = e.target as HTMLElement;
45
- // Check if the clicked element is a menu-item
46
- if (target.tagName.toLowerCase() === 'menu-item') {
47
- // Dispatch custom event with value
48
- const value = (target as any).value;
49
- this.dispatchEvent(
50
- new CustomEvent('menu-selected', {
51
- detail: { value },
52
- bubbles: true,
53
- composed: true,
54
- }),
55
- );
205
+ private _setBoundaryActive(index: number) {
206
+ const enabledItems = this._enabledItems();
207
+ if (!enabledItems.length) {
208
+ return;
209
+ }
56
210
 
57
- this.open = false;
211
+ this.activeIndex = index;
212
+ this._syncRovingTabIndex();
213
+ enabledItems[this.activeIndex]?.focus();
214
+ }
215
+
216
+ private _getActiveItem() {
217
+ const enabledItems = this._enabledItems();
218
+ if (!enabledItems.length || this.activeIndex < 0) {
219
+ return null;
58
220
  }
221
+
222
+ return enabledItems[this.activeIndex] ?? null;
59
223
  }
60
224
 
61
- private _toggleMenu(e: Event) {
62
- e.stopPropagation(); // Prevent immediate closing via window listener
63
- this.open = !this.open;
225
+ private _getFirstEnabledItem() {
226
+ return this._enabledItems()[0] ?? null;
64
227
  }
65
228
 
66
- static styles = css`
67
- :host {
68
- display: inline-block;
69
- position: relative;
229
+ private _onItemActivate = (event: Event) => {
230
+ const customEvent = event as CustomEvent<{ item: MenuItem }>;
231
+ const enabledItems = this._enabledItems();
232
+ const nextIndex = enabledItems.indexOf(customEvent.detail.item);
233
+ if (nextIndex >= 0) {
234
+ this.activeIndex = nextIndex;
235
+ this._syncRovingTabIndex();
70
236
  }
237
+ };
71
238
 
72
- .trigger {
73
- cursor: pointer;
74
- display: inline-block;
239
+ private _onItemRequestClose = (event: Event) => {
240
+ const customEvent = event as CustomEvent<{
241
+ reason: 'click-selection' | 'keydown';
242
+ key?: string;
243
+ }>;
244
+
245
+ if (customEvent.defaultPrevented) {
246
+ return;
75
247
  }
76
248
 
77
- .menu-wrapper {
78
- position: absolute;
79
- top: 100%;
80
- z-index: 10;
81
- opacity: 0;
82
- transform: scale(0.95);
83
- transform-origin: top left;
84
- transition:
85
- opacity 0.1s ease-out,
86
- transform 0.1s ease-out;
87
- pointer-events: none; /* Prevent clicking when hidden */
88
- margin-top: 4px; /* Slight gap */
249
+ if (customEvent.detail.reason === 'click-selection') {
250
+ this.close({ kind: 'click-selection' });
251
+ return;
89
252
  }
90
253
 
91
- :host([open]) .menu-wrapper {
92
- opacity: 1;
93
- transform: scale(1);
94
- pointer-events: auto;
254
+ this.close({ kind: 'keydown', key: customEvent.detail.key ?? 'Enter' });
255
+ };
256
+
257
+ private _onKeyDown = (event: KeyboardEvent) => {
258
+ if (!this.open) {
259
+ return;
95
260
  }
96
261
 
97
- /* Alignment logic */
98
- :host([align='end']) .menu-wrapper {
99
- right: 0;
100
- transform-origin: top right;
262
+ switch (event.key) {
263
+ case 'ArrowDown':
264
+ event.preventDefault();
265
+ this._setActiveByOffset(1);
266
+ break;
267
+ case 'ArrowUp':
268
+ event.preventDefault();
269
+ this._setActiveByOffset(-1);
270
+ break;
271
+ case 'Home':
272
+ event.preventDefault();
273
+ this._setBoundaryActive(0);
274
+ break;
275
+ case 'End': {
276
+ event.preventDefault();
277
+ const last = Math.max(this._enabledItems().length - 1, 0);
278
+ this._setBoundaryActive(last);
279
+ break;
280
+ }
281
+ case 'Escape':
282
+ event.preventDefault();
283
+ this.close({ kind: 'keydown', key: 'Escape' });
284
+ break;
285
+ default:
286
+ break;
101
287
  }
102
- :host([align='start']) .menu-wrapper {
103
- left: 0;
104
- transform-origin: top left;
288
+ };
289
+
290
+ private _onWindowClick = (event: MouseEvent) => {
291
+ if (!this.open || this.stayOpenOnOutsideClick) {
292
+ return;
105
293
  }
106
- `;
294
+
295
+ const path = event.composedPath();
296
+ const anchorEl = this._resolveAnchorElement();
297
+ const inMenuTree = path.some(
298
+ target => target === this || (target instanceof Node && this.contains(target)),
299
+ );
300
+
301
+ if (inMenuTree || (anchorEl && path.includes(anchorEl))) {
302
+ return;
303
+ }
304
+
305
+ this.close({ kind: 'outside-click' });
306
+ };
307
+
308
+ private _isWithinMenuTree(node: Node | null) {
309
+ if (!node) {
310
+ return false;
311
+ }
312
+
313
+ let current: Node | null = node;
314
+ while (current) {
315
+ if (current === this || this.contains(current)) {
316
+ return true;
317
+ }
318
+
319
+ const root = current.getRootNode();
320
+ if (root instanceof ShadowRoot) {
321
+ current = root.host;
322
+ } else {
323
+ current = null;
324
+ }
325
+ }
326
+
327
+ return false;
328
+ }
329
+
330
+ private _onFocusOut = (event: FocusEvent) => {
331
+ if (!this.open || this.stayOpenOnFocusout) {
332
+ return;
333
+ }
334
+
335
+ const next = event.relatedTarget;
336
+ if (!next) {
337
+ return;
338
+ }
339
+
340
+ if (next instanceof Node && this._isWithinMenuTree(next)) {
341
+ return;
342
+ }
343
+
344
+ this.close({ kind: 'focusout' });
345
+ };
346
+
347
+ private _onSlotChange = () => {
348
+ this._syncRovingTabIndex();
349
+ };
350
+
351
+ private _applyPositioning() {
352
+ if (!this.open || !this.menuListElement) {
353
+ return;
354
+ }
355
+
356
+ const anchorEl = this._resolveAnchorElement();
357
+ if (!anchorEl) {
358
+ return;
359
+ }
360
+
361
+ this._surfaceController.start({
362
+ reference: anchorEl,
363
+ floating: this.menuListElement,
364
+ placement: this.placement,
365
+ offset: this.offset,
366
+ strategy: 'fixed',
367
+ });
368
+ }
369
+
370
+ protected override updated(changedProperties: Map<string, unknown>) {
371
+ if (changedProperties.has('anchor') || changedProperties.has('open')) {
372
+ this._syncAnchorAria();
373
+ }
374
+
375
+ if (changedProperties.has('open')) {
376
+ if (this.open) {
377
+ this._lastFocusedElement = document.activeElement as HTMLElement | null;
378
+ this._syncRovingTabIndex();
379
+ this.dispatchEvent(
380
+ new CustomEvent('opened', {
381
+ bubbles: true,
382
+ composed: true,
383
+ }),
384
+ );
385
+
386
+ this._applyPositioning();
387
+ } else {
388
+ this._surfaceController.stop();
389
+
390
+ const reason = this._closeReason;
391
+ this.dispatchEvent(
392
+ new CustomEvent('close-menu', {
393
+ bubbles: true,
394
+ composed: true,
395
+ detail: {
396
+ reason,
397
+ itemPath: [],
398
+ },
399
+ }),
400
+ );
401
+ this.dispatchEvent(
402
+ new CustomEvent('closed', {
403
+ bubbles: true,
404
+ composed: true,
405
+ detail: { reason },
406
+ }),
407
+ );
408
+
409
+ if (!this.isSubmenu) {
410
+ this._lastFocusedElement?.focus();
411
+ }
412
+ }
413
+ }
414
+
415
+ if (
416
+ (changedProperties.has('open') ||
417
+ changedProperties.has('anchor') ||
418
+ changedProperties.has('placement') ||
419
+ changedProperties.has('offset')) &&
420
+ this.open
421
+ ) {
422
+ this._applyPositioning();
423
+ }
424
+ }
107
425
 
108
426
  render() {
109
- return html`
110
- <div class="trigger" @click="${this._toggleMenu}">
111
- <slot name="trigger"></slot>
112
- </div>
427
+ return html`<div
428
+ class=${classMap({
429
+ 'menu': true,
430
+ open: this.open,
431
+ closed: !this.open,
432
+ [`variant-${this.variant}`]: true,
433
+ })}
434
+ aria-hidden=${String(!this.open)}
435
+ >
436
+ <div class="background"></div>
437
+ <wc-elevation class="elevation"></wc-elevation>
113
438
 
114
- <div class="menu-wrapper">
115
- <!-- We expect a menu-list to be passed here -->
116
- <slot></slot>
439
+ <div class="menu-content">
440
+ <slot @slotchange=${this._onSlotChange}></slot>
117
441
  </div>
118
- `;
442
+ </div>`;
119
443
  }
120
444
  }
@@ -1,12 +1,12 @@
1
1
  @use "../../../scss/mixin";
2
2
 
3
- :host-context(menu-list[variant=standard]) {
3
+ :host-context([variant=standard]) {
4
4
  --menu-item-label-color: var(--color-on-surface-variant);
5
5
  --menu-item-label-selected-color: var(--color-on-tertiary-container);
6
6
  --menu-item-container-selected-color: var(--color-tertiary-container);
7
7
  }
8
8
 
9
- :host-context(menu-list[variant=vibrant]) {
9
+ :host-context([variant=vibrant]) {
10
10
  --menu-item-label-color: var(--color-on-tertiary-container);
11
11
  --menu-item-label-selected-color: var(--color-on-tertiary);
12
12
  --menu-item-container-selected-color: var(--color-tertiary);