@redvars/peacock 3.3.0 → 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 (105) hide show
  1. package/dist/assets/components.css +1 -1
  2. package/dist/assets/components.css.map +1 -1
  3. package/dist/assets/styles.css +1 -1
  4. package/dist/assets/styles.css.map +1 -1
  5. package/dist/button-group-DA7xoziD.js +292 -0
  6. package/dist/button-group-DA7xoziD.js.map +1 -0
  7. package/dist/button-group.js +6 -107
  8. package/dist/button-group.js.map +1 -1
  9. package/dist/{button-BGFJfbT2.js → button-trIfcqC7.js} +2 -3
  10. package/dist/{button-BGFJfbT2.js.map → button-trIfcqC7.js.map} +1 -1
  11. package/dist/button.js +2 -3
  12. package/dist/button.js.map +1 -1
  13. package/dist/{class-map-DpeNtqCn.js → class-map-hJdvjl-W.js} +9 -3
  14. package/dist/class-map-hJdvjl-W.js.map +1 -0
  15. package/dist/code-editor.js +5 -5
  16. package/dist/code-editor.js.map +1 -1
  17. package/dist/code-highlighter.js +5 -23
  18. package/dist/code-highlighter.js.map +1 -1
  19. package/dist/custom-elements-jsdocs.json +4706 -2471
  20. package/dist/custom-elements.json +3444 -1007
  21. package/dist/index.js +4 -5
  22. package/dist/index.js.map +1 -1
  23. package/dist/peacock-loader.js +26 -496
  24. package/dist/peacock-loader.js.map +1 -1
  25. package/dist/src/accordion/accordion-item.d.ts +1 -0
  26. package/dist/src/breadcrumb/breadcrumb/breadcrumb.d.ts +2 -0
  27. package/dist/src/breadcrumb/breadcrumb-item/breadcrumb-item.d.ts +1 -0
  28. package/dist/src/button/button-group/button-group.d.ts +4 -0
  29. package/dist/src/code-editor/code-editor.d.ts +4 -3
  30. package/dist/src/code-highlighter/code-highlighter.d.ts +4 -7
  31. package/dist/src/index.d.ts +4 -0
  32. package/dist/src/menu/index.d.ts +3 -0
  33. package/dist/src/menu/menu/MenuSurfaceController.d.ts +18 -0
  34. package/dist/src/menu/menu/menu.d.ts +54 -12
  35. package/dist/src/menu/menu-item/menu-item.d.ts +12 -5
  36. package/dist/src/menu/sub-menu/sub-menu.d.ts +36 -0
  37. package/dist/src/pagination/index.d.ts +1 -0
  38. package/dist/src/pagination/pagination.d.ts +38 -0
  39. package/dist/src/popover/PopoverController.d.ts +4 -1
  40. package/dist/src/table/index.d.ts +1 -0
  41. package/dist/src/table/table.d.ts +110 -0
  42. package/dist/src/tabs/tab-group.d.ts +4 -0
  43. package/dist/src/tabs/tab-panel.d.ts +1 -0
  44. package/dist/src/tabs/tab.d.ts +1 -0
  45. package/dist/src/tabs/tabs.d.ts +2 -0
  46. package/dist/src/tooltip/tooltip.d.ts +1 -3
  47. package/dist/src/tree-view/index.d.ts +2 -0
  48. package/dist/src/tree-view/tree-node.d.ts +69 -0
  49. package/dist/src/tree-view/tree-view.d.ts +40 -0
  50. package/dist/src/tree-view/wc-tree-view.d.ts +6 -0
  51. package/dist/test/icon.test.d.ts +1 -1
  52. package/dist/test/menu.test.d.ts +1 -0
  53. package/dist/test/sub-menu.test.d.ts +1 -0
  54. package/dist/test/tree-view.test.d.ts +1 -0
  55. package/dist/{slider-Dk9CFWTG.js → tree-view-CLolVlU0.js} +3317 -1180
  56. package/dist/tree-view-CLolVlU0.js.map +1 -0
  57. package/dist/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +1 -1
  59. package/readme.md +40 -40
  60. package/src/accordion/accordion-item.ts +2 -1
  61. package/src/breadcrumb/breadcrumb/breadcrumb.ts +3 -0
  62. package/src/breadcrumb/breadcrumb-item/breadcrumb-item.ts +1 -0
  63. package/src/button/button-group/button-group.ts +6 -0
  64. package/src/code-editor/code-editor.ts +4 -3
  65. package/src/code-highlighter/code-highlighter.ts +4 -22
  66. package/src/divider/divider.scss +2 -2
  67. package/src/empty-state/empty-state.scss +1 -1
  68. package/src/empty-state/empty-state.ts +1 -1
  69. package/src/index.ts +6 -2
  70. package/src/menu/index.ts +3 -0
  71. package/src/menu/menu/MenuSurfaceController.ts +61 -0
  72. package/src/menu/{menu-list/menu-list.scss → menu/menu.scss} +19 -4
  73. package/src/menu/menu/menu.ts +389 -81
  74. package/src/menu/menu-item/menu-item.ts +115 -36
  75. package/src/menu/sub-menu/sub-menu.scss +7 -0
  76. package/src/menu/sub-menu/sub-menu.ts +243 -0
  77. package/src/pagination/index.ts +1 -0
  78. package/src/pagination/pagination.scss +59 -0
  79. package/src/pagination/pagination.ts +135 -0
  80. package/src/peacock-loader.ts +25 -11
  81. package/src/popover/PopoverController.ts +13 -7
  82. package/src/table/index.ts +1 -0
  83. package/src/table/table.scss +174 -0
  84. package/src/table/table.ts +475 -0
  85. package/src/tabs/tab-group.ts +12 -6
  86. package/src/tabs/tab-panel.ts +1 -0
  87. package/src/tabs/tab.ts +1 -0
  88. package/src/tabs/tabs.scss +6 -5
  89. package/src/tabs/tabs.ts +5 -3
  90. package/src/text/text.css-component.scss +6 -3
  91. package/src/tooltip/tooltip.scss +16 -13
  92. package/src/tooltip/tooltip.ts +7 -9
  93. package/src/tree-view/demo/index.html +57 -0
  94. package/src/tree-view/index.ts +2 -0
  95. package/src/tree-view/tree-node.scss +101 -0
  96. package/src/tree-view/tree-node.ts +268 -0
  97. package/src/tree-view/tree-view.scss +12 -0
  98. package/src/tree-view/tree-view.ts +182 -0
  99. package/src/tree-view/wc-tree-view.ts +9 -0
  100. package/dist/class-map-DpeNtqCn.js.map +0 -1
  101. package/dist/slider-Dk9CFWTG.js.map +0 -1
  102. package/dist/src/menu/menu-list/menu-list.d.ts +0 -22
  103. package/dist/state-8v48Exzh.js +0 -10
  104. package/dist/state-8v48Exzh.js.map +0 -1
  105. package/src/menu/menu-list/menu-list.ts +0 -48
@@ -1,136 +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';
8
+
9
+ type CloseReason =
10
+ | { kind: 'click-selection' }
11
+ | { kind: 'keydown'; key: string }
12
+ | { kind: 'outside-click' }
13
+ | { kind: 'focusout' }
14
+ | { kind: 'programmatic' };
3
15
 
4
16
  /**
5
17
  * @label Menu
6
18
  * @tag wc-menu
7
19
  * @rawTag menu
8
- * @summary A dropdown menu component.
20
+ * @summary A list of menu items.
9
21
  * @tags navigation
10
22
  *
11
23
  * @example
12
24
  * ```html
13
25
  * <wc-menu>
14
- * <wc-menu-list>
15
- * <wc-menu-item>Item 1</wc-menu-item>
16
- * </wc-menu-list>
26
+ * <wc-menu-item>Item 1</wc-menu-item>
27
+ * <wc-menu-item>Item 2</wc-menu-item>
17
28
  * </wc-menu>
18
29
  * ```
19
30
  */
20
31
  export class Menu extends LitElement {
32
+ static styles = [styles];
33
+
34
+ static Item = MenuItem;
35
+
21
36
  @property({ type: Boolean, reflect: true }) open = false;
22
37
 
23
- // Position: 'bottom-start' | 'bottom-end' | etc. (Simplified here to generic dropdown)
24
- @property({ type: String }) align = 'start';
38
+ @property({ type: String, reflect: true }) variant: 'standard' | 'vibrant' =
39
+ 'standard';
25
40
 
26
- @query('.menu-wrapper') menuWrapper!: HTMLElement;
41
+ @property({ type: String }) anchor = '';
27
42
 
28
- private _boundClickOutside: (e: MouseEvent) => void;
43
+ @property({ type: Boolean, attribute: 'stay-open-on-outside-click' })
44
+ stayOpenOnOutsideClick = false;
29
45
 
30
- constructor() {
31
- super();
32
- this._boundClickOutside = this._handleClickOutside.bind(this);
33
- }
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' };
34
66
 
35
67
  connectedCallback() {
36
68
  // eslint-disable-next-line wc/guard-super-call
37
69
  super.connectedCallback();
38
- window.addEventListener('click', this._boundClickOutside);
39
- // Listen for menu-item clicks bubbling up
40
- 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();
41
78
  }
42
79
 
43
80
  disconnectedCallback() {
44
- // 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 });
45
89
  super.disconnectedCallback();
46
- window.removeEventListener('click', this._boundClickOutside);
47
- this.removeEventListener('click', this._handleItemClick);
48
90
  }
49
91
 
50
- private _handleClickOutside(e: MouseEvent) {
51
- 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
+ }
52
193
 
53
- const path = e.composedPath();
54
- if (!path.includes(this)) {
55
- 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;
56
199
  }
200
+
201
+ this._syncRovingTabIndex();
202
+ enabledItems[this.activeIndex]?.focus();
57
203
  }
58
204
 
59
- private _handleItemClick(e: Event) {
60
- const target = e.target as HTMLElement;
61
- // Check if the clicked element is a menu-item
62
- if (target.tagName.toLowerCase() === 'menu-item') {
63
- // Dispatch custom event with value
64
- const value = (target as any).value;
65
- this.dispatchEvent(
66
- new CustomEvent('menu-selected', {
67
- detail: { value },
68
- bubbles: true,
69
- composed: true,
70
- }),
71
- );
205
+ private _setBoundaryActive(index: number) {
206
+ const enabledItems = this._enabledItems();
207
+ if (!enabledItems.length) {
208
+ return;
209
+ }
72
210
 
73
- 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;
74
220
  }
221
+
222
+ return enabledItems[this.activeIndex] ?? null;
75
223
  }
76
224
 
77
- private _toggleMenu(e: Event) {
78
- e.stopPropagation(); // Prevent immediate closing via window listener
79
- this.open = !this.open;
225
+ private _getFirstEnabledItem() {
226
+ return this._enabledItems()[0] ?? null;
80
227
  }
81
228
 
82
- static styles = css`
83
- :host {
84
- display: inline-block;
85
- 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();
86
236
  }
237
+ };
87
238
 
88
- .trigger {
89
- cursor: pointer;
90
- 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;
91
247
  }
92
248
 
93
- .menu-wrapper {
94
- position: absolute;
95
- top: 100%;
96
- z-index: 10;
97
- opacity: 0;
98
- transform: scale(0.95);
99
- transform-origin: top left;
100
- transition:
101
- opacity 0.1s ease-out,
102
- transform 0.1s ease-out;
103
- pointer-events: none; /* Prevent clicking when hidden */
104
- margin-top: 4px; /* Slight gap */
249
+ if (customEvent.detail.reason === 'click-selection') {
250
+ this.close({ kind: 'click-selection' });
251
+ return;
105
252
  }
106
253
 
107
- :host([open]) .menu-wrapper {
108
- opacity: 1;
109
- transform: scale(1);
110
- 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;
111
260
  }
112
261
 
113
- /* Alignment logic */
114
- :host([align='end']) .menu-wrapper {
115
- right: 0;
116
- 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;
117
287
  }
118
- :host([align='start']) .menu-wrapper {
119
- left: 0;
120
- transform-origin: top left;
288
+ };
289
+
290
+ private _onWindowClick = (event: MouseEvent) => {
291
+ if (!this.open || this.stayOpenOnOutsideClick) {
292
+ return;
121
293
  }
122
- `;
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
+ }
123
425
 
124
426
  render() {
125
- return html`
126
- <div class="trigger" @click="${this._toggleMenu}">
127
- <slot name="trigger"></slot>
128
- </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>
129
438
 
130
- <div class="menu-wrapper">
131
- <!-- We expect a menu-list to be passed here -->
132
- <slot></slot>
439
+ <div class="menu-content">
440
+ <slot @slotchange=${this._onSlotChange}></slot>
133
441
  </div>
134
- `;
442
+ </div>`;
135
443
  }
136
444
  }