@vollowx/seele 0.7.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -0
  3. package/package.json +62 -0
  4. package/src/all.js +19 -0
  5. package/src/base/button.js +61 -0
  6. package/src/base/checkbox.js +118 -0
  7. package/src/base/controllers/list-controller.js +96 -0
  8. package/src/base/controllers/popover-controller.js +163 -0
  9. package/src/base/field.js +3 -0
  10. package/src/base/hidden-styles.css.js +2 -0
  11. package/src/base/input.js +182 -0
  12. package/src/base/item.js +7 -0
  13. package/src/base/list-item.js +54 -0
  14. package/src/base/menu-item.js +12 -0
  15. package/src/base/menu-utils.js +111 -0
  16. package/src/base/menu.js +244 -0
  17. package/src/base/mixins/attachable.js +71 -0
  18. package/src/base/mixins/form-associated.js +69 -0
  19. package/src/base/mixins/internals-attached.js +13 -0
  20. package/src/base/option.js +17 -0
  21. package/src/base/select.js +285 -0
  22. package/src/base/switch.js +86 -0
  23. package/src/base/tooltip.js +139 -0
  24. package/src/core/focus-visible.js +13 -0
  25. package/src/core/shared.d.ts +1 -0
  26. package/src/core/unique-id.js +11 -0
  27. package/src/m3/button/common-button-styles.css.js +2 -0
  28. package/src/m3/button/common-button-toggle-styles.css.js +2 -0
  29. package/src/m3/button/common-button-toggle.js +69 -0
  30. package/src/m3/button/common-button.js +65 -0
  31. package/src/m3/button/icon-button-styles.css.js +2 -0
  32. package/src/m3/button/icon-button-toggle-styles.css.js +2 -0
  33. package/src/m3/button/icon-button-toggle.js +57 -0
  34. package/src/m3/button/icon-button.js +51 -0
  35. package/src/m3/button/shared-button-styles.css.js +2 -0
  36. package/src/m3/checkbox-styles.css.js +2 -0
  37. package/src/m3/checkbox.js +46 -0
  38. package/src/m3/fab-styles.css.js +2 -0
  39. package/src/m3/fab.js +48 -0
  40. package/src/m3/field/field-styles.css.js +2 -0
  41. package/src/m3/field/field.js +93 -0
  42. package/src/m3/field/filled-field-styles.css.js +2 -0
  43. package/src/m3/field/filled-field.js +30 -0
  44. package/src/m3/field/outlined-field-styles.css.js +2 -0
  45. package/src/m3/field/outlined-field.js +34 -0
  46. package/src/m3/focus-ring-styles.css.js +2 -0
  47. package/src/m3/focus-ring.js +72 -0
  48. package/src/m3/item-styles.css.js +2 -0
  49. package/src/m3/item.js +46 -0
  50. package/src/m3/list-item-styles.css.js +2 -0
  51. package/src/m3/list-item.js +52 -0
  52. package/src/m3/list-styles.css.js +2 -0
  53. package/src/m3/list.js +16 -0
  54. package/src/m3/menu-item.js +15 -0
  55. package/src/m3/menu-part-styles.css.js +2 -0
  56. package/src/m3/menu-styles.css.js +2 -0
  57. package/src/m3/menu.js +30 -0
  58. package/src/m3/option.js +15 -0
  59. package/src/m3/ripple-styles.css.js +2 -0
  60. package/src/m3/ripple.js +199 -0
  61. package/src/m3/select/filled-select.js +41 -0
  62. package/src/m3/select/outlined-select.js +41 -0
  63. package/src/m3/select/select-styles.css.js +2 -0
  64. package/src/m3/select/select.js +34 -0
  65. package/src/m3/switch-styles.css.js +2 -0
  66. package/src/m3/switch.js +129 -0
  67. package/src/m3/target-styles.css.js +2 -0
  68. package/src/m3/text-field/filled-text-field.js +38 -0
  69. package/src/m3/text-field/outlined-text-field.js +40 -0
  70. package/src/m3/text-field/text-field-styles.css.js +2 -0
  71. package/src/m3/toolbar-styles.css.js +2 -0
  72. package/src/m3/toolbar.js +53 -0
  73. package/src/m3/tooltip-styles.css.js +2 -0
  74. package/src/m3/tooltip.js +18 -0
@@ -0,0 +1,182 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement, html, nothing } from 'lit';
3
+ import { property, query } from 'lit/decorators.js';
4
+ import { live } from 'lit/directives/live.js';
5
+ import { FormAssociated } from './mixins/form-associated.js';
6
+ import { InternalsAttached, internals } from './mixins/internals-attached.js';
7
+ const Base = FormAssociated(InternalsAttached(LitElement));
8
+ export class Input extends Base {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.type = 'text';
12
+ this.value = '';
13
+ this.placeholder = '';
14
+ this.required = false;
15
+ this.readOnly = false;
16
+ /**
17
+ * Whether the input accepts multiple values (e.g. email).
18
+ */
19
+ this.multiple = false;
20
+ this.min = '';
21
+ this.max = '';
22
+ this.step = '';
23
+ this.minLength = -1;
24
+ this.maxLength = -1;
25
+ /**
26
+ * The pattern regex.
27
+ */
28
+ this.pattern = '';
29
+ this.autocomplete = '';
30
+ this.focused = false;
31
+ }
32
+ static { this.shadowRootOptions = {
33
+ ...LitElement.shadowRootOptions,
34
+ delegatesFocus: true,
35
+ }; }
36
+ render() {
37
+ const isTextarea = this.type === 'textarea';
38
+ const minLength = this.minLength > -1 ? this.minLength : nothing;
39
+ const maxLength = this.maxLength > -1 ? this.maxLength : nothing;
40
+ if (isTextarea) {
41
+ return html `
42
+ <textarea
43
+ part="input"
44
+ .value=${live(this.value)}
45
+ placeholder=${(this.placeholder || nothing)}
46
+ ?required=${this.required}
47
+ ?readonly=${this.readOnly}
48
+ ?disabled=${this.disabled}
49
+ minlength=${minLength}
50
+ maxlength=${maxLength}
51
+ autocomplete=${(this.autocomplete || nothing)}
52
+ @input=${this.handleInput}
53
+ @change=${this.handleChange}
54
+ @focus=${this.handleFocus}
55
+ @blur=${this.handleBlur}
56
+ ></textarea>
57
+ `;
58
+ }
59
+ return html `
60
+ <input
61
+ part="input"
62
+ type=${this.type}
63
+ .value=${live(this.value)}
64
+ placeholder=${(this.placeholder || nothing)}
65
+ ?required=${this.required}
66
+ ?readonly=${this.readOnly}
67
+ ?disabled=${this.disabled}
68
+ ?multiple=${this.multiple}
69
+ min=${(this.min || nothing)}
70
+ max=${(this.max || nothing)}
71
+ step=${(this.step || nothing)}
72
+ minlength=${minLength}
73
+ maxlength=${maxLength}
74
+ pattern=${(this.pattern || nothing)}
75
+ autocomplete=${(this.autocomplete || nothing)}
76
+ @input=${this.handleInput}
77
+ @change=${this.handleChange}
78
+ @focus=${this.handleFocus}
79
+ @blur=${this.handleBlur}
80
+ />
81
+ `;
82
+ }
83
+ updated(changedProperties) {
84
+ super.updated(changedProperties);
85
+ if (changedProperties.has('value')) {
86
+ this[internals].setFormValue(this.value);
87
+ this.syncValidity();
88
+ }
89
+ }
90
+ handleInput(event) {
91
+ const target = event.target;
92
+ this.value = target.value;
93
+ this.syncValidity();
94
+ }
95
+ handleChange(event) {
96
+ this.redispatchEvent(event);
97
+ }
98
+ handleFocus() {
99
+ this.focused = true;
100
+ }
101
+ handleBlur() {
102
+ this.focused = false;
103
+ }
104
+ redispatchEvent(event) {
105
+ // Redispatch 'change' event as composed to escape shadow root
106
+ const newEvent = new Event(event.type, {
107
+ bubbles: event.bubbles,
108
+ cancelable: event.cancelable,
109
+ composed: true,
110
+ });
111
+ this.dispatchEvent(newEvent);
112
+ }
113
+ syncValidity() {
114
+ if (!this.inputOrTextarea)
115
+ return;
116
+ this[internals].setValidity(this.inputOrTextarea.validity, this.inputOrTextarea.validationMessage, this.inputOrTextarea);
117
+ }
118
+ select() {
119
+ this.inputOrTextarea?.select();
120
+ }
121
+ stepUp(n) {
122
+ this.inputOrTextarea?.stepUp(n);
123
+ this.handleInput({ target: this.inputOrTextarea });
124
+ }
125
+ stepDown(n) {
126
+ this.inputOrTextarea?.stepDown(n);
127
+ this.handleInput({ target: this.inputOrTextarea });
128
+ }
129
+ formResetCallback() {
130
+ this.value = this.getAttribute('value') || '';
131
+ this.syncValidity();
132
+ }
133
+ formStateRestoreCallback(state) {
134
+ this.value = state;
135
+ this.syncValidity();
136
+ }
137
+ }
138
+ __decorate([
139
+ property({ reflect: true })
140
+ ], Input.prototype, "type", void 0);
141
+ __decorate([
142
+ property()
143
+ ], Input.prototype, "value", void 0);
144
+ __decorate([
145
+ property({ reflect: true })
146
+ ], Input.prototype, "placeholder", void 0);
147
+ __decorate([
148
+ property({ type: Boolean, reflect: true })
149
+ ], Input.prototype, "required", void 0);
150
+ __decorate([
151
+ property({ type: Boolean, reflect: true })
152
+ ], Input.prototype, "readOnly", void 0);
153
+ __decorate([
154
+ property({ type: Boolean, reflect: true })
155
+ ], Input.prototype, "multiple", void 0);
156
+ __decorate([
157
+ property()
158
+ ], Input.prototype, "min", void 0);
159
+ __decorate([
160
+ property()
161
+ ], Input.prototype, "max", void 0);
162
+ __decorate([
163
+ property()
164
+ ], Input.prototype, "step", void 0);
165
+ __decorate([
166
+ property({ type: Number })
167
+ ], Input.prototype, "minLength", void 0);
168
+ __decorate([
169
+ property({ type: Number })
170
+ ], Input.prototype, "maxLength", void 0);
171
+ __decorate([
172
+ property()
173
+ ], Input.prototype, "pattern", void 0);
174
+ __decorate([
175
+ property({ reflect: true })
176
+ ], Input.prototype, "autocomplete", void 0);
177
+ __decorate([
178
+ property({ type: Boolean, reflect: true })
179
+ ], Input.prototype, "focused", void 0);
180
+ __decorate([
181
+ query('[part~=input]')
182
+ ], Input.prototype, "inputOrTextarea", void 0);
@@ -0,0 +1,7 @@
1
+ import { LitElement } from 'lit';
2
+ /**
3
+ * This class works as a "UI item", it should be contained in menu items,
4
+ * options, etc.
5
+ */
6
+ export class Item extends LitElement {
7
+ }
@@ -0,0 +1,54 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement } from 'lit';
3
+ import { property } from 'lit/decorators.js';
4
+ import { genUniqueId } from '../core/unique-id.js';
5
+ import { InternalsAttached, internals } from './mixins/internals-attached.js';
6
+ import { FormAssociated } from './mixins/form-associated.js';
7
+ import { hiddenStyles } from './hidden-styles.css.js';
8
+ export class ListItem extends FormAssociated(InternalsAttached(LitElement)) {
9
+ constructor() {
10
+ super(...arguments);
11
+ this.selected = false;
12
+ this.focused = false;
13
+ this._role = 'option';
14
+ }
15
+ static { this.styles = [hiddenStyles]; }
16
+ connectedCallback() {
17
+ super.connectedCallback();
18
+ this[internals].role = this._role;
19
+ this.setAttribute('tabindex', '-1');
20
+ this.#updateInternals();
21
+ if (!this.id)
22
+ this.id = genUniqueId('item');
23
+ }
24
+ updated(changed) {
25
+ super.updated(changed);
26
+ if (changed.has('disabled') ||
27
+ changed.has('focused') ||
28
+ changed.has('selected')) {
29
+ this.#updateInternals();
30
+ }
31
+ }
32
+ #updateInternals() {
33
+ this[internals].ariaDisabled = this.disabled ? 'true' : 'false';
34
+ this.focused
35
+ ? this[internals].states.add('focused')
36
+ : this[internals].states.delete('focused');
37
+ this[internals].ariaSelected = this.selected ? 'true' : 'false';
38
+ this.selected
39
+ ? this[internals].states.add('selected')
40
+ : this[internals].states.delete('selected');
41
+ }
42
+ focus() {
43
+ this.focused = true;
44
+ }
45
+ blur() {
46
+ this.focused = false;
47
+ }
48
+ }
49
+ __decorate([
50
+ property({ type: Boolean, reflect: true })
51
+ ], ListItem.prototype, "selected", void 0);
52
+ __decorate([
53
+ property({ type: Boolean, reflect: true })
54
+ ], ListItem.prototype, "focused", void 0);
@@ -0,0 +1,12 @@
1
+ import { ListItem } from './list-item.js';
2
+ export const MenuItemMixin = (superClass) => {
3
+ class OptionElement extends superClass {
4
+ constructor() {
5
+ super(...arguments);
6
+ this._role = 'menuitem';
7
+ }
8
+ }
9
+ return OptionElement;
10
+ };
11
+ export class MenuItem extends MenuItemMixin(ListItem) {
12
+ }
@@ -0,0 +1,111 @@
1
+ export const MenuActions = {
2
+ Close: 0,
3
+ CloseSelect: 1,
4
+ First: 2,
5
+ Last: 3,
6
+ Next: 4,
7
+ Open: 5,
8
+ PageDown: 6,
9
+ PageUp: 7,
10
+ Previous: 8,
11
+ Select: 9,
12
+ Type: 10,
13
+ };
14
+ export function filterOptions(options = [], filter, exclude = []) {
15
+ return options.filter((option) => {
16
+ const matches = option.toLowerCase().indexOf(filter.toLowerCase()) === 0;
17
+ return matches && exclude.indexOf(option) < 0;
18
+ });
19
+ }
20
+ export function getActionFromKey(event, menuOpen) {
21
+ const { key, altKey, ctrlKey, metaKey } = event;
22
+ const openKeys = ['ArrowDown', 'ArrowUp', 'Enter', ' '];
23
+ if (!menuOpen && openKeys.includes(key)) {
24
+ return MenuActions.Open;
25
+ }
26
+ if (key === 'Home') {
27
+ return MenuActions.First;
28
+ }
29
+ if (key === 'End') {
30
+ return MenuActions.Last;
31
+ }
32
+ if (key === 'Backspace' ||
33
+ key === 'Clear' ||
34
+ (key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey)) {
35
+ return MenuActions.Type;
36
+ }
37
+ if (menuOpen) {
38
+ if (key === 'ArrowUp' && altKey) {
39
+ return MenuActions.CloseSelect;
40
+ }
41
+ else if (key === 'ArrowDown' && !altKey) {
42
+ return MenuActions.Next;
43
+ }
44
+ else if (key === 'ArrowUp') {
45
+ return MenuActions.Previous;
46
+ }
47
+ else if (key === 'PageUp') {
48
+ return MenuActions.PageUp;
49
+ }
50
+ else if (key === 'PageDown') {
51
+ return MenuActions.PageDown;
52
+ }
53
+ else if (key === 'Escape') {
54
+ return MenuActions.Close;
55
+ }
56
+ else if (key === 'Enter' || key === ' ') {
57
+ return MenuActions.CloseSelect;
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+ export function getIndexByLetter(options, filter, startIndex = 0) {
63
+ const orderedOptions = [
64
+ ...options.slice(startIndex),
65
+ ...options.slice(0, startIndex),
66
+ ];
67
+ const firstMatch = filterOptions(orderedOptions, filter)[0];
68
+ const allSameLetter = (array) => array.every((letter) => letter === array[0]);
69
+ if (firstMatch) {
70
+ return options.indexOf(firstMatch);
71
+ }
72
+ else if (allSameLetter(filter.split(''))) {
73
+ const matches = filterOptions(orderedOptions, filter[0]);
74
+ return options.indexOf(matches[0]);
75
+ }
76
+ else {
77
+ return -1;
78
+ }
79
+ }
80
+ export function getUpdatedIndex(currentIndex, maxIndex, action) {
81
+ const pageSize = 10;
82
+ switch (action) {
83
+ case MenuActions.First:
84
+ return 0;
85
+ case MenuActions.Last:
86
+ return maxIndex;
87
+ case MenuActions.Previous:
88
+ return Math.max(0, currentIndex - 1);
89
+ case MenuActions.Next:
90
+ return Math.min(maxIndex, currentIndex + 1);
91
+ case MenuActions.PageUp:
92
+ return Math.max(0, currentIndex - pageSize);
93
+ case MenuActions.PageDown:
94
+ return Math.min(maxIndex, currentIndex + pageSize);
95
+ default:
96
+ return currentIndex;
97
+ }
98
+ }
99
+ export function scrollItemIntoView(menu, item, paddingY = 0) {
100
+ if (!menu)
101
+ return;
102
+ // Basic scroll into view logic
103
+ const menuRect = menu.getBoundingClientRect();
104
+ const itemRect = item.getBoundingClientRect();
105
+ if (itemRect.bottom + paddingY > menuRect.bottom) {
106
+ menu.scrollTop += itemRect.bottom - menuRect.bottom + paddingY;
107
+ }
108
+ else if (itemRect.top - paddingY < menuRect.top) {
109
+ menu.scrollTop -= menuRect.top - itemRect.top + paddingY;
110
+ }
111
+ }
@@ -0,0 +1,244 @@
1
+ import { __decorate } from "tslib";
2
+ import { LitElement, html } from 'lit';
3
+ import { property, query } from 'lit/decorators.js';
4
+ import { setFocusVisible } from '../core/focus-visible.js';
5
+ import { Attachable } from './mixins/attachable.js';
6
+ import { internals, InternalsAttached } from './mixins/internals-attached.js';
7
+ import { PopoverController } from './controllers/popover-controller.js';
8
+ import { ListController } from './controllers/list-controller.js';
9
+ import { MenuActions, getActionFromKey, getUpdatedIndex, scrollItemIntoView, } from './menu-utils.js';
10
+ const Base = InternalsAttached(Attachable(LitElement));
11
+ /**
12
+ * @csspart menu
13
+ * @csspart items
14
+ *
15
+ * @fires {Event} select - Fired when a menu item has been selected.
16
+ */
17
+ export class Menu extends Base {
18
+ constructor() {
19
+ super();
20
+ this._possibleItemTags = [];
21
+ this._durations = { show: 0, hide: 0 };
22
+ this._scrollPadding = 0;
23
+ this.open = false;
24
+ this.quick = false;
25
+ this.align = 'bottom-start';
26
+ this.alignStrategy = 'absolute';
27
+ this.offset = 0;
28
+ this.keepOpenBlur = false;
29
+ this.keepOpenClickItem = false;
30
+ this.keepOpenClickOutside = false;
31
+ this.$lastFocused = null;
32
+ this.popoverController = new PopoverController(this, {
33
+ popover: () => this.$menu,
34
+ trigger: () => this.$control,
35
+ positioning: {
36
+ placement: () => this.align,
37
+ strategy: () => this.alignStrategy,
38
+ offset: () => this.offset,
39
+ windowPadding: () => 16,
40
+ },
41
+ durations: {
42
+ open: () => (this.quick ? 0 : this._durations.show),
43
+ close: () => (this.quick ? 0 : this._durations.hide),
44
+ },
45
+ onClickOutside: () => {
46
+ if (!this.keepOpenClickOutside)
47
+ this.open = false;
48
+ },
49
+ });
50
+ this.listController = new ListController(this, {
51
+ isItem: (item) => this._possibleItemTags.includes(item.tagName.toLowerCase()) &&
52
+ !item.hasAttribute('disabled'),
53
+ getPossibleItems: () => Array.from(this.children).filter((child) => this._possibleItemTags.includes(child.tagName.toLowerCase()) &&
54
+ !child.hasAttribute('disabled')),
55
+ blurItem: (item) => {
56
+ item.focused = false;
57
+ },
58
+ focusItem: (item) => {
59
+ item.focused = true;
60
+ // this[internals].ariaActiveDescendantElement = item;
61
+ // Somehow setting ariaActiveDescendantElement doesn't actually update it
62
+ this.setAttribute('aria-activedescendant', item.id);
63
+ scrollItemIntoView(this.$menu, item, this._scrollPadding);
64
+ },
65
+ wrapNavigation: () => false,
66
+ });
67
+ this[internals].role = 'menu';
68
+ this.tabIndex = -1;
69
+ }
70
+ render() {
71
+ return html `<div part="menu">${this.renderItemSlot()}</div>`;
72
+ }
73
+ renderItemSlot() {
74
+ return html `<slot part="items"></slot>`;
75
+ }
76
+ connectedCallback() {
77
+ super.connectedCallback();
78
+ this.addEventListener('keydown', this.#handleKeyDown.bind(this));
79
+ this.addEventListener('focusout', this.#handleFocusOut.bind(this));
80
+ if (this.$control) {
81
+ // TODO: Handle $control change
82
+ this.$control.ariaHasPopup = 'true';
83
+ this.$control.ariaExpanded = 'false';
84
+ this.$control.ariaControlsElements = [this];
85
+ this[internals].ariaLabelledByElements = [this.$control];
86
+ this.$control.addEventListener('focusout', this.#handleFocusOut.bind(this));
87
+ }
88
+ this.listController.items.forEach((item) => {
89
+ item.addEventListener('mouseover', this.#handleItemMouseOver.bind(this));
90
+ item.addEventListener('click', this.#handleItemClick.bind(this));
91
+ });
92
+ }
93
+ disconnectedCallback() {
94
+ super.disconnectedCallback();
95
+ this.removeEventListener('keydown', this.#handleKeyDown.bind(this));
96
+ this.removeEventListener('focusout', this.#handleFocusOut.bind(this));
97
+ if (this.$control) {
98
+ this.$control.removeEventListener('focusout', this.#handleFocusOut.bind(this));
99
+ }
100
+ }
101
+ updated(changed) {
102
+ if (changed.has('open')) {
103
+ if (this.open) {
104
+ this.$lastFocused = document.activeElement;
105
+ if (this.$control) {
106
+ this.$control.ariaExpanded = 'true';
107
+ }
108
+ this.popoverController.animateOpen().then(() => {
109
+ this.focus();
110
+ this.listController.focusFirstItem();
111
+ });
112
+ }
113
+ else {
114
+ this.listController.clearSearch();
115
+ if (this.$control) {
116
+ this.$control.ariaExpanded = 'false';
117
+ }
118
+ this.popoverController.animateClose().then(() => {
119
+ if (this.$lastFocused) {
120
+ this.$lastFocused.focus();
121
+ this.$lastFocused = null;
122
+ }
123
+ });
124
+ }
125
+ }
126
+ }
127
+ #handleKeyDown(event) {
128
+ if (event.defaultPrevented)
129
+ return;
130
+ const action = getActionFromKey(event, this.open);
131
+ const items = this.listController.items;
132
+ const currentIndex = this.listController.currentIndex;
133
+ const maxIndex = items.length - 1;
134
+ switch (action) {
135
+ case MenuActions.Last:
136
+ case MenuActions.First:
137
+ this.open = true;
138
+ // intentional fallthrough
139
+ case MenuActions.Next:
140
+ case MenuActions.Previous:
141
+ case MenuActions.PageUp:
142
+ case MenuActions.PageDown:
143
+ event.preventDefault();
144
+ const nextIndex = getUpdatedIndex(currentIndex, maxIndex, action);
145
+ this.listController._focusItem(items[nextIndex]);
146
+ return;
147
+ case MenuActions.CloseSelect:
148
+ event.preventDefault();
149
+ if (currentIndex >= 0) {
150
+ this.listController.items[currentIndex].focused = false;
151
+ this.dispatchEvent(new CustomEvent('select', {
152
+ detail: {
153
+ item: this.listController.items[currentIndex],
154
+ index: currentIndex,
155
+ },
156
+ bubbles: true,
157
+ composed: true,
158
+ }));
159
+ if (this.keepOpenClickItem)
160
+ return;
161
+ this.open = false;
162
+ }
163
+ return;
164
+ case MenuActions.Close:
165
+ event.preventDefault();
166
+ this.open = false;
167
+ return;
168
+ case MenuActions.Type:
169
+ this.open = true;
170
+ this.listController.handleType(event.key);
171
+ return;
172
+ case MenuActions.Open:
173
+ event.preventDefault();
174
+ this.open = true;
175
+ return;
176
+ }
177
+ }
178
+ #handleFocusOut(event) {
179
+ if (this.keepOpenBlur)
180
+ return;
181
+ const newFocus = event.relatedTarget;
182
+ const isInside = this.contains(newFocus) ||
183
+ this.shadowRoot?.contains(newFocus) ||
184
+ this.$control?.contains(newFocus);
185
+ if (!isInside) {
186
+ this.open = false;
187
+ }
188
+ }
189
+ #handleItemMouseOver(event) {
190
+ setFocusVisible(false);
191
+ const hoveredItem = event.currentTarget;
192
+ this.listController._focusItem(hoveredItem);
193
+ }
194
+ #handleItemClick(event) {
195
+ const clickedItem = event.currentTarget;
196
+ const index = this.listController.items.indexOf(clickedItem);
197
+ this.listController.items[index].focused = false;
198
+ this.dispatchEvent(new CustomEvent('select', {
199
+ detail: {
200
+ item: this.listController.items[index],
201
+ index: index,
202
+ },
203
+ bubbles: true,
204
+ composed: true,
205
+ }));
206
+ if (this.keepOpenClickItem)
207
+ return;
208
+ this.open = false;
209
+ }
210
+ // Exposed functions
211
+ show() {
212
+ this.open = true;
213
+ }
214
+ close() {
215
+ this.open = false;
216
+ }
217
+ }
218
+ __decorate([
219
+ property({ type: Boolean, reflect: true })
220
+ ], Menu.prototype, "open", void 0);
221
+ __decorate([
222
+ property({ type: Boolean, reflect: true })
223
+ ], Menu.prototype, "quick", void 0);
224
+ __decorate([
225
+ property({ reflect: true })
226
+ ], Menu.prototype, "align", void 0);
227
+ __decorate([
228
+ property({ type: String, reflect: true })
229
+ ], Menu.prototype, "alignStrategy", void 0);
230
+ __decorate([
231
+ property({ type: Number, reflect: true })
232
+ ], Menu.prototype, "offset", void 0);
233
+ __decorate([
234
+ property({ type: Boolean })
235
+ ], Menu.prototype, "keepOpenBlur", void 0);
236
+ __decorate([
237
+ property({ type: Boolean })
238
+ ], Menu.prototype, "keepOpenClickItem", void 0);
239
+ __decorate([
240
+ property({ type: Boolean })
241
+ ], Menu.prototype, "keepOpenClickOutside", void 0);
242
+ __decorate([
243
+ query('[part="menu"]')
244
+ ], Menu.prototype, "$menu", void 0);
@@ -0,0 +1,71 @@
1
+ import { __decorate } from "tslib";
2
+ import { property } from 'lit/decorators.js';
3
+ export const Attachable = (superClass) => {
4
+ class AttachableElement extends superClass {
5
+ constructor() {
6
+ super(...arguments);
7
+ this.currentControl = null;
8
+ }
9
+ connectedCallback() {
10
+ super.connectedCallback();
11
+ this.setCurrentControl(this.$control);
12
+ }
13
+ disconnectedCallback() {
14
+ this.setCurrentControl(null);
15
+ super.disconnectedCallback();
16
+ }
17
+ /**
18
+ * If has `for` attribute, use it to find the control element.
19
+ * Otherwise, use the parent element as the control.
20
+ */
21
+ get $control() {
22
+ if (this.hasAttribute('for')) {
23
+ if (!this.htmlFor || !this.isConnected) {
24
+ return null;
25
+ }
26
+ return this.getRootNode().querySelector(`#${this.htmlFor}`);
27
+ }
28
+ return this.currentControl || this.parentNode instanceof ShadowRoot
29
+ ? this.parentNode.host
30
+ : this.parentElement;
31
+ }
32
+ set $control(control) {
33
+ if (control) {
34
+ this.attach(control);
35
+ }
36
+ else {
37
+ this.detach();
38
+ }
39
+ }
40
+ updated(changed) {
41
+ super.updated(changed);
42
+ if (changed.has('htmlFor')) {
43
+ // Will be triggered when first render using `for` attribute, will be
44
+ // prevented in setCurrentControl since it's unnecessary.
45
+ this.setCurrentControl(this.$control);
46
+ }
47
+ }
48
+ setCurrentControl(control) {
49
+ if (control === this.currentControl)
50
+ return;
51
+ this.handleControlChange(this.currentControl, control);
52
+ this.currentControl = control;
53
+ }
54
+ attach(control) {
55
+ this.setCurrentControl(control);
56
+ this.removeAttribute('for');
57
+ }
58
+ detach() {
59
+ this.setCurrentControl(null);
60
+ this.setAttribute('for', '');
61
+ }
62
+ /**
63
+ * Handles the first attaching and actual control element changing
64
+ */
65
+ handleControlChange(prev = null, next = null) { }
66
+ }
67
+ __decorate([
68
+ property({ attribute: 'for', type: String })
69
+ ], AttachableElement.prototype, "htmlFor", void 0);
70
+ return AttachableElement;
71
+ };