@vollowx/seele 0.13.10 → 0.14.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 (77) hide show
  1. package/custom-elements.json +604 -359
  2. package/package.json +1 -1
  3. package/src/all.d.ts +2 -2
  4. package/src/all.js +2 -2
  5. package/src/base/autocomplete.d.ts +9 -33
  6. package/src/base/autocomplete.js +42 -65
  7. package/src/base/button.d.ts +0 -1
  8. package/src/base/button.js +7 -11
  9. package/src/base/checkbox.d.ts +0 -2
  10. package/src/base/checkbox.js +6 -13
  11. package/src/base/controllers/list-controller.d.ts +1 -0
  12. package/src/base/controllers/list-controller.js +6 -1
  13. package/src/base/dialog.d.ts +2 -2
  14. package/src/base/dialog.js +11 -2
  15. package/src/base/list-item.d.ts +1 -1
  16. package/src/base/list-item.js +11 -12
  17. package/src/base/list.d.ts +16 -33
  18. package/src/base/list.js +91 -173
  19. package/src/base/menu.d.ts +41 -61
  20. package/src/base/menu.js +184 -306
  21. package/src/base/{experimental/popup.d.ts → popup.d.ts} +7 -11
  22. package/src/base/{experimental/popup.js → popup.js} +45 -90
  23. package/src/base/radio.js +5 -5
  24. package/src/base/select.d.ts +7 -53
  25. package/src/base/select.js +45 -79
  26. package/src/base/tooltip.js +2 -2
  27. package/src/core/focus.d.ts +11 -0
  28. package/src/core/focus.js +53 -0
  29. package/src/m3/autocomplete/autocomplete-styles.css.js +1 -1
  30. package/src/m3/autocomplete/autocomplete.d.ts +1 -0
  31. package/src/m3/autocomplete/autocomplete.js +2 -1
  32. package/src/m3/button/common-button-styles.css.js +1 -1
  33. package/src/m3/button/icon-button-styles.css.js +1 -1
  34. package/src/m3/button/icon-button-toggle.js +1 -1
  35. package/src/m3/button/shared-button-styles.css.js +1 -1
  36. package/src/m3/button-group/standard-button-group.js +1 -1
  37. package/src/m3/checkbox/checkbox.js +1 -1
  38. package/src/m3/dialog/dialog.d.ts +2 -2
  39. package/src/m3/dialog/dialog.js +2 -2
  40. package/src/m3/field/field-styles.css.js +1 -1
  41. package/src/m3/field/field.js +2 -2
  42. package/src/m3/field/filled-field-styles.css.js +1 -1
  43. package/src/m3/field/filled-field.js +1 -1
  44. package/src/m3/field/outlined-field-styles.css.js +1 -1
  45. package/src/m3/focus-ring/focus-ring.js +1 -1
  46. package/src/m3/list/list-item.d.ts +2 -2
  47. package/src/m3/list/list-item.js +6 -6
  48. package/src/m3/menu/composed-menu.d.ts +19 -0
  49. package/src/m3/menu/composed-menu.js +42 -0
  50. package/src/m3/menu/menu-styles.css.js +1 -1
  51. package/src/m3/menu/menu.d.ts +1 -7
  52. package/src/m3/menu/menu.js +1 -13
  53. package/src/m3/navigation/navigation-rail-item-styles.css.js +1 -1
  54. package/src/m3/popup/popup-styles.css.js +2 -0
  55. package/src/m3/{experimental → popup}/popup.d.ts +2 -1
  56. package/src/m3/popup/popup.js +18 -0
  57. package/src/m3/radio/radio.js +1 -1
  58. package/src/m3/ripple/ripple.js +3 -3
  59. package/src/m3/select/filled-select.d.ts +2 -2
  60. package/src/m3/select/filled-select.js +5 -5
  61. package/src/m3/select/outlined-select.d.ts +2 -2
  62. package/src/m3/select/outlined-select.js +5 -5
  63. package/src/m3/select/select-styles.css.js +1 -1
  64. package/src/m3/select/select.d.ts +2 -5
  65. package/src/m3/select/select.js +3 -6
  66. package/src/m3/switch/switch-styles.css.js +1 -1
  67. package/src/m3/switch/switch.js +1 -1
  68. package/src/m3/tab/tabs-styles.css.js +1 -1
  69. package/src/m3/tab/tabs.js +1 -1
  70. package/src/m3/text-field/filled-text-field.js +2 -2
  71. package/src/m3/text-field/outlined-text-field.js +2 -2
  72. package/src/m3/tooltip/tooltip-styles.css.js +1 -1
  73. package/src/core/focus-visible.d.ts +0 -2
  74. package/src/core/focus-visible.js +0 -13
  75. package/src/m3/experimental/popup-styles.css.js +0 -2
  76. package/src/m3/experimental/popup.js +0 -11
  77. /package/src/base/{experimental/popup-styles.css.js → popup-styles.css.js} +0 -0
@@ -1,68 +1,20 @@
1
1
  /**
2
2
  * NOTE: demos at dev/pending.html
3
- * NOTE: should apply writing style on event listeners to other files
4
- * TODO: consider removing popoverController, but in case of tooltip, reserve
5
- * for now
6
3
  * TODO: hijack all `hidePopover()`s to allow more complex JS animations
7
- * TODO: simpler Menu that just inherits List - do we really need a separated
8
- * Menu? Why not just `<md-list role="presentation">`?
9
- *
10
- * Design: TO BE ADDED
11
4
  */
12
5
  var _a;
13
6
  import { __decorate } from "tslib";
14
7
  import { LitElement, html, isServer } from 'lit';
15
8
  import { customElement, property } from 'lit/decorators.js';
16
9
  import { autoUpdate, computePosition, arrow, flip, offset, shift, } from '@floating-ui/dom';
17
- import { InternalsAttached } from '../mixins/internals-attached.js';
18
- import { Attachable, autoAttachToParent, handleControlChange, } from '../mixins/attachable.js';
19
- import { transformOriginFromArrow } from '../controllers/popover-controller.js';
10
+ import { getFirstTabbable } from '../core/focus.js';
11
+ import { InternalsAttached } from './mixins/internals-attached.js';
12
+ import { Attachable, autoAttachToParent, handleControlChange, } from './mixins/attachable.js';
13
+ import { transformOriginFromArrow } from './controllers/popover-controller.js';
20
14
  import { popupStyles } from './popup-styles.css.js';
21
- // Returns the first tabbable element within an eleent
22
- //
23
- // TODO: reverse search?
24
- export function lookForTabbable(root) {
25
- const FOCUSABLE_SELECTOR = [
26
- 'a[href]',
27
- 'button',
28
- 'input',
29
- 'textarea',
30
- 'select',
31
- 'summary',
32
- '[tabindex]',
33
- '[contenteditable=true]',
34
- ].join(',');
35
- const isTabbable = (el) => el.matches(FOCUSABLE_SELECTOR) &&
36
- !el.hasAttribute('disabled') &&
37
- el.getAttribute('tabindex') !== '-1';
38
- if ('shadowRoot' in root && root.shadowRoot) {
39
- const shadowResult = lookForTabbable(root.shadowRoot);
40
- if (shadowResult)
41
- return shadowResult;
42
- }
43
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
44
- acceptNode: (node) => {
45
- const el = node;
46
- return isTabbable(el) || el.shadowRoot
47
- ? NodeFilter.FILTER_ACCEPT
48
- : NodeFilter.FILTER_SKIP;
49
- },
50
- });
51
- let currentNode = walker.nextNode();
52
- while (currentNode) {
53
- if (isTabbable(currentNode)) {
54
- return currentNode;
55
- }
56
- if (currentNode.shadowRoot) {
57
- const shadowFocusable = lookForTabbable(currentNode.shadowRoot);
58
- if (shadowFocusable) {
59
- return shadowFocusable;
60
- }
61
- }
62
- currentNode = walker.nextNode();
63
- }
64
- return null;
65
- }
15
+ /**
16
+ * When using popup with menu, you need to manually bind them using `aria-controls`
17
+ */
66
18
  let Popup = class Popup extends Attachable(InternalsAttached(LitElement)) {
67
19
  static { this.styles = [popupStyles]; }
68
20
  render() {
@@ -72,16 +24,16 @@ let Popup = class Popup extends Attachable(InternalsAttached(LitElement)) {
72
24
  super();
73
25
  this.open = false;
74
26
  this.noFocusControl = false;
75
- this.placement = 'bottom-start';
27
+ this.align = 'bottom-start';
76
28
  this.strategy = 'absolute';
77
29
  this.offset = 0;
78
30
  this.windowPadding = 8;
79
31
  this[_a] = false;
80
- this.#onRequestHide = () => {
32
+ this.#handleRequestHide = () => {
81
33
  this.hide();
82
34
  };
83
35
  // #allowHidePopover = false;
84
- // #onBeforeToggle = (e: ToggleEvent) => {
36
+ // #handleBeforeToggle = (e: ToggleEvent) => {
85
37
  // // Intercept browser light-dismiss (ESC, click outside) or external hidePopover() calls
86
38
  // if (e.newState === 'closed' && !this.#allowHidePopover) {
87
39
  // e.preventDefault();
@@ -90,12 +42,12 @@ let Popup = class Popup extends Attachable(InternalsAttached(LitElement)) {
90
42
  // }
91
43
  // }
92
44
  // };
93
- this.#onToggle = (e) => {
45
+ this.#handleToggle = (e) => {
94
46
  e.preventDefault();
95
47
  if (e.newState === 'closed' && this.open)
96
48
  this.hide();
97
49
  };
98
- this.#onFocusOut = (e) => {
50
+ this.#handleFocusOut = (e) => {
99
51
  // for `this` and `this.$control`
100
52
  if (this.noFocusControl || !this.open)
101
53
  return;
@@ -112,20 +64,20 @@ let Popup = class Popup extends Attachable(InternalsAttached(LitElement)) {
112
64
  this.hide();
113
65
  });
114
66
  };
115
- this.#onTriggerClick = (e) => {
67
+ this.#handleTriggerClick = (e) => {
116
68
  e.preventDefault();
117
69
  this.toggle();
118
70
  };
119
71
  this.#dummyArrow = isServer ? null : document.createElement('div');
72
+ this.setAttribute('notransition', '');
120
73
  if (!this.hasAttribute('popover'))
121
74
  this.setAttribute('popover', 'auto');
122
75
  if (!isServer) {
123
- this.addEventListener('request-popup-hide', this.#onRequestHide);
124
- // this.addEventListener('beforetoggle', this.#onBeforeToggle);
125
- this.addEventListener('toggle', this.#onToggle);
126
- this.addEventListener('focusout', this.#onFocusOut);
76
+ this.addEventListener('request-popup-hide', this.#handleRequestHide);
77
+ // this.addEventListener('beforetoggle', this.#handleBeforeToggle);
78
+ this.addEventListener('toggle', this.#handleToggle);
79
+ this.addEventListener('focusout', this.#handleFocusOut);
127
80
  }
128
- this.setAttribute('notransition', '');
129
81
  }
130
82
  connectedCallback() {
131
83
  super.connectedCallback();
@@ -137,29 +89,30 @@ let Popup = class Popup extends Attachable(InternalsAttached(LitElement)) {
137
89
  }
138
90
  updated(changedProperties) {
139
91
  if (changedProperties.has('open')) {
92
+ this.#syncTriggerAria();
140
93
  if (this.open)
141
94
  this._show();
142
95
  else
143
96
  this._hide();
144
- this.#syncTriggerAria();
145
97
  }
146
98
  }
147
99
  [(_a = autoAttachToParent, handleControlChange)](prev, next) {
148
100
  if (prev) {
149
- prev.removeEventListener('click', this.#onTriggerClick);
150
- prev.removeEventListener('focusout', this.#onFocusOut);
101
+ prev.removeEventListener('click', this.#handleTriggerClick);
102
+ prev.removeEventListener('focusout', this.#handleFocusOut);
151
103
  }
152
104
  if (next) {
153
- next.addEventListener('click', this.#onTriggerClick);
154
- next.addEventListener('focusout', this.#onFocusOut);
155
- next.ariaHasPopup = 'true';
156
- next.ariaExpanded = String(this.open);
157
- next.ariaControlsElements = [this];
105
+ next.addEventListener('click', this.#handleTriggerClick);
106
+ next.addEventListener('focusout', this.#handleFocusOut);
107
+ const ariaNext = this.$ariaControl ? this.$ariaControl : next;
108
+ if (!next.ariaHasPopup)
109
+ ariaNext.ariaHasPopup = 'true';
110
+ ariaNext.ariaExpanded = String(this.open);
158
111
  }
159
112
  }
160
- #onRequestHide;
113
+ #handleRequestHide;
161
114
  // #allowHidePopover = false;
162
- // #onBeforeToggle = (e: ToggleEvent) => {
115
+ // #handleBeforeToggle = (e: ToggleEvent) => {
163
116
  // // Intercept browser light-dismiss (ESC, click outside) or external hidePopover() calls
164
117
  // if (e.newState === 'closed' && !this.#allowHidePopover) {
165
118
  // e.preventDefault();
@@ -168,23 +121,25 @@ let Popup = class Popup extends Attachable(InternalsAttached(LitElement)) {
168
121
  // }
169
122
  // }
170
123
  // };
171
- #onToggle;
172
- #onFocusOut;
173
- #onTriggerClick;
124
+ #handleToggle;
125
+ #handleFocusOut;
126
+ #handleTriggerClick;
174
127
  #syncTriggerAria() {
175
- this.$control?.setAttribute('aria-expanded', String(this.open));
128
+ if (this.$ariaControl)
129
+ this.$ariaControl.ariaExpanded = String(this.open);
130
+ else if (this.$control)
131
+ this.$control.ariaExpanded = String(this.open);
176
132
  }
177
133
  #focusFirstInteractiveElement() {
178
134
  if (this.noFocusControl)
179
135
  return;
180
- const autoFocusEl = this.querySelector('[autofocus]');
181
- if (autoFocusEl) {
182
- autoFocusEl.focus();
183
- return;
136
+ const autoFocus = this.querySelector('[autofocus]');
137
+ if (autoFocus) {
138
+ autoFocus.focus();
184
139
  }
185
- const firstFocusable = lookForTabbable(this);
186
- if (firstFocusable) {
187
- firstFocusable.focus();
140
+ else {
141
+ const firstTabbable = getFirstTabbable(this);
142
+ firstTabbable?.focus();
188
143
  }
189
144
  }
190
145
  async toggle() {
@@ -234,7 +189,7 @@ let Popup = class Popup extends Attachable(InternalsAttached(LitElement)) {
234
189
  if (!trigger)
235
190
  return;
236
191
  const { x, y, placement, middlewareData } = await computePosition(trigger, this, {
237
- placement: this.placement,
192
+ placement: this.align,
238
193
  strategy: this.strategy,
239
194
  middleware: [
240
195
  offset(this.offset),
@@ -262,7 +217,7 @@ __decorate([
262
217
  ], Popup.prototype, "noFocusControl", void 0);
263
218
  __decorate([
264
219
  property({ type: String })
265
- ], Popup.prototype, "placement", void 0);
220
+ ], Popup.prototype, "align", void 0);
266
221
  __decorate([
267
222
  property({ type: String })
268
223
  ], Popup.prototype, "strategy", void 0);
@@ -270,7 +225,7 @@ __decorate([
270
225
  property({ type: Number })
271
226
  ], Popup.prototype, "offset", void 0);
272
227
  __decorate([
273
- property({ type: Number })
228
+ property({ type: Number, attribute: 'window-padding' })
274
229
  ], Popup.prototype, "windowPadding", void 0);
275
230
  Popup = __decorate([
276
231
  customElement('complementary-popup')
package/src/base/radio.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var _a;
2
2
  import { __decorate } from "tslib";
3
- import { LitElement } from 'lit';
3
+ import { isServer, LitElement } from 'lit';
4
4
  import { property } from 'lit/decorators.js';
5
5
  import { FormAssociated } from './mixins/form-associated.js';
6
6
  import { InternalsAttached, internals, replaceStates, updateInternals, } from './mixins/internals-attached.js';
@@ -15,17 +15,17 @@ export class Radio extends FormAssociated(InternalsAttached(LitElement)) {
15
15
  this.#currentName = '';
16
16
  this.checked = false;
17
17
  this[internals].role = 'radio';
18
+ if (!isServer) {
19
+ this.addEventListener('click', this.#handleClick);
20
+ this.addEventListener('keydown', this.#handleKeyDown);
21
+ }
18
22
  }
19
23
  connectedCallback() {
20
24
  super.connectedCallback();
21
25
  this.#registerToGroup();
22
26
  this[updateInternals]();
23
- this.addEventListener('click', this.#handleClick);
24
- this.addEventListener('keydown', this.#handleKeyDown);
25
27
  }
26
28
  disconnectedCallback() {
27
- this.removeEventListener('click', this.#handleClick);
28
- this.removeEventListener('keydown', this.#handleKeyDown);
29
29
  this.#unregisterFromGroup();
30
30
  super.disconnectedCallback();
31
31
  }
@@ -1,29 +1,24 @@
1
1
  import { LitElement, PropertyValues } from 'lit';
2
- import type { Placement, Strategy } from '@floating-ui/dom';
3
2
  import type { Field } from './field.js';
4
- import type { Menu, MenuItemFocusEvent, MenuSelectEvent } from './menu.js';
5
3
  import type { Option } from './option.js';
4
+ import type { Popup } from './popup.js';
5
+ import type { Menu, MenuItemFocusEvent, MenuActionEvent } from './menu.js';
6
6
  declare const VALUE: unique symbol;
7
7
  declare const Base: Constructor<import("./mixins/form-associated.js").FormAssociatedInterface> & Constructor<import("./mixins/focus-delegated.js").FocusDelegatedInterface> & Constructor<import("./mixins/internals-attached.js").InternalsAttachedInterface> & typeof LitElement;
8
8
  /**
9
9
  * @csspart field
10
+ * @csspart popup
10
11
  * @csspart menu
11
- * @csspart items
12
12
  *
13
13
  * @fires {Event} change - Fires when the value is changed.
14
14
  * @fires {Event} input - Fires when the value is changed.
15
15
  *
16
16
  * TODO: Render multiple values
17
- * FIXME: `aria-expanded` set twice by both Lit and Menu
18
17
  */
19
18
  export declare class Select extends Base {
20
19
  #private;
21
20
  required: boolean;
22
21
  error: boolean;
23
- quick: boolean;
24
- offset: number;
25
- align: Placement;
26
- alignStrategy: Strategy;
27
22
  open: boolean;
28
23
  /**
29
24
  * Text to display in the field. Only set for SSR.
@@ -37,6 +32,7 @@ export declare class Select extends Base {
37
32
  set selectedIndex(index: number);
38
33
  get selectedOptions(): Option[];
39
34
  $field: Field;
35
+ $popup: Popup;
40
36
  $menu: Menu;
41
37
  private lastUserSetValue;
42
38
  private lastUserSetSelectedIndex;
@@ -44,49 +40,11 @@ export declare class Select extends Base {
44
40
  private lastSelectedOptionRecords;
45
41
  render(): import("lit-html").TemplateResult<1>;
46
42
  /**
47
- * Example content:
48
- *
49
- * ```html
50
- * <your-field
51
- * id="field"
52
- * part="field"
53
- * @click=${this.toggle}
54
- * @keydown=${this.handleFieldKeydown}
55
- * tabindex=${this.disabled ? '-1' : '0'}
56
- * role="combobox"
57
- * aria-haspopup="listbox"
58
- * aria-expanded=${this.open}
59
- * aria-controls="menu"
60
- * aria-disabled=${this.disabled}
61
- * aria-required=${this.required}
62
- * >
63
- * ${this.renderFieldContent()}
64
- * </your-field>
65
- * ```
43
+ * Check example content from the m3 implementation
66
44
  */
67
45
  protected renderField(): import("lit-html").TemplateResult<1>;
68
46
  /**
69
- * Example content:
70
- *
71
- * ```html
72
- * <your-menu
73
- * id="menu"
74
- * for="field"
75
- * type="listbox"
76
- * data-tabindex="-1"
77
- * ?quick="${this.quick}"
78
- * offset="${this.offset}"
79
- * align="${this.align}"
80
- * align-strategy="${this.alignStrategy}"
81
- * keep-open-blur
82
- * no-focus-control
83
- * ?open="${this.open}"
84
- * @item-focus="${this.handleMenuItemFocus}"
85
- * @select="${this.handleMenuSelect}"
86
- * >
87
- * <slot part="items" @slotchange=${this.handleSlotChange}></slot>
88
- * </your-menu>
89
- * ```
47
+ * Check example content from the m3 implementation
90
48
  */
91
49
  protected renderMenu(): import("lit-html").TemplateResult<1>;
92
50
  protected renderFieldContent(): import("lit-html").TemplateResult<1>;
@@ -94,13 +52,9 @@ export declare class Select extends Base {
94
52
  protected firstUpdated(changed: PropertyValues<Select>): Promise<void>;
95
53
  protected update(changed: PropertyValues<Select>): void;
96
54
  protected updated(changed: PropertyValues): void;
97
- /**
98
- * TODO: Handle clear action
99
- */
100
55
  protected _handleFieldKeydown(event: KeyboardEvent): void;
101
- protected _handleMenuSelect(event: MenuSelectEvent): void;
56
+ protected _handleMenuAction(event: MenuActionEvent): void;
102
57
  protected _handleMenuItemFocus(event: MenuItemFocusEvent): void;
103
- protected _handleFocusOut(event: FocusEvent): void;
104
58
  protected _handleSlotChange(): void;
105
59
  private selectItem;
106
60
  private initUserSelection;
@@ -5,18 +5,18 @@ import { property, query } from 'lit/decorators.js';
5
5
  import { InternalsAttached } from './mixins/internals-attached.js';
6
6
  import { FocusDelegated } from './mixins/focus-delegated.js';
7
7
  import { FormAssociated } from './mixins/form-associated.js';
8
+ import { getActionFromKey, MenuAction } from './menu.js';
8
9
  const VALUE = Symbol('value');
9
10
  const Base = FormAssociated(FocusDelegated(InternalsAttached(LitElement)));
10
11
  /**
11
12
  * @csspart field
13
+ * @csspart popup
12
14
  * @csspart menu
13
- * @csspart items
14
15
  *
15
16
  * @fires {Event} change - Fires when the value is changed.
16
17
  * @fires {Event} input - Fires when the value is changed.
17
18
  *
18
19
  * TODO: Render multiple values
19
- * FIXME: `aria-expanded` set twice by both Lit and Menu
20
20
  */
21
21
  export class Select extends Base {
22
22
  static { _a = VALUE; }
@@ -50,51 +50,13 @@ export class Select extends Base {
50
50
  return html `${this.renderField()}${this.renderMenu()}`;
51
51
  }
52
52
  /**
53
- * Example content:
54
- *
55
- * ```html
56
- * <your-field
57
- * id="field"
58
- * part="field"
59
- * @click=${this.toggle}
60
- * @keydown=${this.handleFieldKeydown}
61
- * tabindex=${this.disabled ? '-1' : '0'}
62
- * role="combobox"
63
- * aria-haspopup="listbox"
64
- * aria-expanded=${this.open}
65
- * aria-controls="menu"
66
- * aria-disabled=${this.disabled}
67
- * aria-required=${this.required}
68
- * >
69
- * ${this.renderFieldContent()}
70
- * </your-field>
71
- * ```
53
+ * Check example content from the m3 implementation
72
54
  */
73
55
  renderField() {
74
56
  return html ``;
75
57
  }
76
58
  /**
77
- * Example content:
78
- *
79
- * ```html
80
- * <your-menu
81
- * id="menu"
82
- * for="field"
83
- * type="listbox"
84
- * data-tabindex="-1"
85
- * ?quick="${this.quick}"
86
- * offset="${this.offset}"
87
- * align="${this.align}"
88
- * align-strategy="${this.alignStrategy}"
89
- * keep-open-blur
90
- * no-focus-control
91
- * ?open="${this.open}"
92
- * @item-focus="${this.handleMenuItemFocus}"
93
- * @select="${this.handleMenuSelect}"
94
- * >
95
- * <slot part="items" @slotchange=${this.handleSlotChange}></slot>
96
- * </your-menu>
97
- * ```
59
+ * Check example content from the m3 implementation
98
60
  */
99
61
  renderMenu() {
100
62
  return html ``;
@@ -104,15 +66,8 @@ export class Select extends Base {
104
66
  }
105
67
  constructor() {
106
68
  super();
107
- // Form-related
108
69
  this.required = false;
109
- // Passed to field
110
70
  this.error = false;
111
- // Passed to menu
112
- this.quick = false;
113
- this.offset = 0;
114
- this.align = 'bottom-start';
115
- this.alignStrategy = 'absolute';
116
71
  this.open = false;
117
72
  /**
118
73
  * Text to display in the field. Only set for SSR.
@@ -128,7 +83,7 @@ export class Select extends Base {
128
83
  this.dispatchEvent(new Event('change', { bubbles: true }));
129
84
  };
130
85
  if (!isServer) {
131
- this.addEventListener('focusout', this._handleFocusOut);
86
+ this.addEventListener('focusout', this.#handleFocusOut.bind(this));
132
87
  }
133
88
  }
134
89
  async firstUpdated(changed) {
@@ -160,35 +115,55 @@ export class Select extends Base {
160
115
  this.#focusSelectedItemOrFirst();
161
116
  }
162
117
  else {
163
- this.$field.setAttribute('aria-activedescendant', '');
118
+ this.$field.ariaActiveDescendantElement = null;
164
119
  }
165
120
  }
166
121
  }
167
- /**
168
- * TODO: Handle clear action
169
- */
170
122
  _handleFieldKeydown(event) {
171
123
  if (this.disabled)
172
124
  return;
173
- const eventClone = new KeyboardEvent(event.type, event);
174
- eventClone.preventDefault = () => event.preventDefault();
175
- eventClone.stopPropagation = () => event.stopPropagation();
176
- this.$menu.$menu.dispatchEvent(eventClone);
125
+ if (!this.open) {
126
+ if (['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(event.key)) {
127
+ event.preventDefault();
128
+ this.open = true;
129
+ return;
130
+ }
131
+ const action = getActionFromKey(event);
132
+ if (action === MenuAction.Type) {
133
+ this.open = true;
134
+ }
135
+ else {
136
+ return;
137
+ }
138
+ }
139
+ if (this.open)
140
+ this.$menu.handleKeyDown(event);
177
141
  }
178
- _handleMenuSelect(event) {
142
+ _handleMenuAction(event) {
179
143
  if (this.selectItem(event.detail.item)) {
180
144
  this.#dispatchChangeEvent();
145
+ this.$field.focus();
146
+ // For Narrator, activating an option moves focus the document then
181
147
  }
182
- this.open = false;
183
148
  }
184
149
  _handleMenuItemFocus(event) {
185
- this.$field.setAttribute('aria-activedescendant', event.detail.item.id);
150
+ this.$field.ariaActiveDescendantElement = event.detail.item;
186
151
  }
187
- _handleFocusOut(event) {
188
- const relatedTarget = event.relatedTarget;
189
- if (!this.contains(relatedTarget) && !this.$menu.contains(relatedTarget)) {
190
- this.open = false;
152
+ #handleFocusOut(event) {
153
+ if (!this.open)
154
+ return;
155
+ const target = event.relatedTarget;
156
+ if (target && this.contains(target)) {
157
+ return;
191
158
  }
159
+ if (target) {
160
+ this.$popup.hide();
161
+ return;
162
+ }
163
+ requestAnimationFrame(() => {
164
+ if (!document.hasFocus())
165
+ this.$popup.hide();
166
+ });
192
167
  }
193
168
  _handleSlotChange() {
194
169
  // When slots change, check for initially selected items if value is not set
@@ -298,18 +273,6 @@ __decorate([
298
273
  __decorate([
299
274
  property({ type: Boolean })
300
275
  ], Select.prototype, "error", void 0);
301
- __decorate([
302
- property({ type: Boolean })
303
- ], Select.prototype, "quick", void 0);
304
- __decorate([
305
- property({ type: Number })
306
- ], Select.prototype, "offset", void 0);
307
- __decorate([
308
- property({ reflect: true })
309
- ], Select.prototype, "align", void 0);
310
- __decorate([
311
- property({ type: String, reflect: true, attribute: 'align-strategy' })
312
- ], Select.prototype, "alignStrategy", void 0);
313
276
  __decorate([
314
277
  property({ type: Boolean })
315
278
  ], Select.prototype, "open", void 0);
@@ -323,8 +286,11 @@ __decorate([
323
286
  property({ type: Number, attribute: 'selected-index' })
324
287
  ], Select.prototype, "selectedIndex", null);
325
288
  __decorate([
326
- query('[part="field"]')
289
+ query('[part=field]')
327
290
  ], Select.prototype, "$field", void 0);
328
291
  __decorate([
329
- query('[part="menu"]')
292
+ query('[part=popup]')
293
+ ], Select.prototype, "$popup", void 0);
294
+ __decorate([
295
+ query('[part=menu]')
330
296
  ], Select.prototype, "$menu", void 0);
@@ -1,7 +1,7 @@
1
1
  import { __decorate } from "tslib";
2
2
  import { LitElement, html } from 'lit';
3
3
  import { property, query } from 'lit/decorators.js';
4
- import { focusVisible } from '../core/focus-visible.js';
4
+ import { focusVisible } from '../core/focus.js';
5
5
  import { Attachable, handleControlChange } from './mixins/attachable.js';
6
6
  import { InternalsAttached, internals } from './mixins/internals-attached.js';
7
7
  import { PopoverController } from './controllers/popover-controller.js';
@@ -139,7 +139,7 @@ __decorate([
139
139
  property({ type: Number, reflect: true })
140
140
  ], Tooltip.prototype, "offset", void 0);
141
141
  __decorate([
142
- property({ type: Boolean, reflect: true })
142
+ property({ type: Boolean, reflect: true, attribute: 'force-invisible' })
143
143
  ], Tooltip.prototype, "forceInvisible", void 0);
144
144
  __decorate([
145
145
  query('slot')
@@ -0,0 +1,11 @@
1
+ export declare let focusVisible: boolean;
2
+ export declare function setFocusVisible(value: boolean): void;
3
+ /**
4
+ * Recursively walks the DOM tree, piercing Shadow DOM boundaries and
5
+ * resolving slots, to find the first tabbable element.
6
+ *
7
+ * TODO: consider use document.createTreeWalker which might be faster, yet more
8
+ * complex in code; may find references in the core/experimental/popup
9
+ * before the commit that added this function
10
+ */
11
+ export declare function getFirstTabbable(root: Element | DocumentFragment): HTMLElement | null;
@@ -0,0 +1,53 @@
1
+ import { isServer } from 'lit';
2
+ export let focusVisible = false;
3
+ export function setFocusVisible(value) {
4
+ focusVisible = value;
5
+ }
6
+ if (!isServer) {
7
+ window.addEventListener('keydown', () => (focusVisible = true), {
8
+ capture: true,
9
+ });
10
+ window.addEventListener('mousedown', () => (focusVisible = false), {
11
+ capture: true,
12
+ });
13
+ }
14
+ /**
15
+ * Recursively walks the DOM tree, piercing Shadow DOM boundaries and
16
+ * resolving slots, to find the first tabbable element.
17
+ *
18
+ * TODO: consider use document.createTreeWalker which might be faster, yet more
19
+ * complex in code; may find references in the core/experimental/popup
20
+ * before the commit that added this function
21
+ */
22
+ export function getFirstTabbable(root) {
23
+ if (root.nodeName === 'SLOT') {
24
+ const assigned = root.assignedElements({
25
+ flatten: true,
26
+ });
27
+ for (const el of assigned) {
28
+ const found = getFirstTabbable(el);
29
+ if (found)
30
+ return found;
31
+ }
32
+ return null;
33
+ }
34
+ if (root instanceof HTMLElement) {
35
+ const isTabbable = root.tabIndex >= 0 &&
36
+ !root.hasAttribute('disabled') &&
37
+ !root.hidden &&
38
+ root.getAttribute('tabindex') !== '-1';
39
+ if (isTabbable)
40
+ return root;
41
+ }
42
+ if ('shadowRoot' in root && root.shadowRoot) {
43
+ const found = getFirstTabbable(root.shadowRoot);
44
+ if (found)
45
+ return found;
46
+ }
47
+ for (const child of root.children) {
48
+ const found = getFirstTabbable(child);
49
+ if (found)
50
+ return found;
51
+ }
52
+ return null;
53
+ }
@@ -1,2 +1,2 @@
1
1
  import { css } from 'lit';
2
- export const autocompleteStyles = css`:host{-webkit-user-select:none;user-select:none;min-width:210px;display:inline-block;position:relative}::slotted([slot=input]){cursor:pointer;outline:none;width:100%}[part=menu]{--md-menu-max-height:300px;min-width:100%}`;
2
+ export const autocompleteStyles = css`:host{-webkit-user-select:none;user-select:none;min-width:210px;display:inline-block;position:relative}::slotted([slot=input]){cursor:pointer;outline:none;width:100%}[part=popup]{--md-menu-max-height:300px;min-width:inherit}`;
@@ -1,4 +1,5 @@
1
1
  import { Autocomplete } from '../../base/autocomplete.js';
2
+ import '../popup/popup.js';
2
3
  import '../menu/menu.js';
3
4
  /**
4
5
  * TODO: Check if manually dispatching input/change events on input is necessary