@vollowx/seele 0.11.2 → 0.12.2
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.
- package/custom-elements.json +1 -1396
- package/package.json +8 -8
- package/src/all.js +1 -0
- package/src/base/autocomplete.js +188 -0
- package/src/base/controllers/list-controller.js +36 -16
- package/src/base/input.js +8 -8
- package/src/base/list.js +245 -0
- package/src/base/menu.js +13 -31
- package/src/base/select.js +2 -0
- package/src/m3/autocomplete/autocomplete-styles.css.js +2 -0
- package/src/m3/autocomplete/autocomplete.js +37 -0
- package/src/m3/focus-ring/focus-ring-styles.css.js +1 -1
- package/src/m3/list/list-item-styles.css.js +1 -1
- package/src/m3/list/list-styles.css.js +1 -1
- package/src/m3/list/list.js +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vollowx/seele",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Standard Extensible Elements. A web components library that can be styled and extended freely, pre-providing components in Material Design 3.",
|
|
5
5
|
"author": "vollowx",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,20 +49,20 @@
|
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
52
|
-
"@swc/core": "^1.15.
|
|
53
|
-
"@types/bun": "^1.3.
|
|
52
|
+
"@swc/core": "^1.15.18",
|
|
53
|
+
"@types/bun": "^1.3.11",
|
|
54
54
|
"@web/dev-server": "^0.4.6",
|
|
55
|
-
"@web/dev-server-esbuild": "^1.0.
|
|
55
|
+
"@web/dev-server-esbuild": "^1.0.5",
|
|
56
56
|
"chokidar": "^5.0.0",
|
|
57
|
-
"lightningcss": "^1.
|
|
58
|
-
"postcss": "^8.5.
|
|
57
|
+
"lightningcss": "^1.32.0",
|
|
58
|
+
"postcss": "^8.5.8",
|
|
59
59
|
"postcss-cli": "^11.0.1",
|
|
60
60
|
"postcss-sorting": "^9.1.0",
|
|
61
61
|
"prettier": "^3.8.1"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@floating-ui/dom": "^1.7.
|
|
65
|
-
"@swc/helpers": "^0.5.
|
|
64
|
+
"@floating-ui/dom": "^1.7.6",
|
|
65
|
+
"@swc/helpers": "^0.5.19",
|
|
66
66
|
"lit": "^3.3.2"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/src/all.js
CHANGED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { _ as _ts_decorate } from "@swc/helpers/_/_ts_decorate";
|
|
2
|
+
import { LitElement, html } from 'lit';
|
|
3
|
+
import { property, query, queryAssignedElements } from 'lit/decorators.js';
|
|
4
|
+
import { InternalsAttached } from './mixins/internals-attached.js';
|
|
5
|
+
import { FocusDelegated } from './mixins/focus-delegated.js';
|
|
6
|
+
const Base = FocusDelegated(InternalsAttached(LitElement));
|
|
7
|
+
/**
|
|
8
|
+
* TODO: Check if manually dispatching input/change events on input is necessary
|
|
9
|
+
*/ export class Autocomplete extends Base {
|
|
10
|
+
get $input() {
|
|
11
|
+
return this.inputSlotElements[0];
|
|
12
|
+
}
|
|
13
|
+
render() {
|
|
14
|
+
return html`
|
|
15
|
+
<slot name="input" @slotchange=${this.handleInputSlotChange}></slot>
|
|
16
|
+
${this.renderMenu()}
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Example content:
|
|
21
|
+
*
|
|
22
|
+
* ```html
|
|
23
|
+
* <your-menu
|
|
24
|
+
* part="menu"
|
|
25
|
+
* id="menu"
|
|
26
|
+
* type="listbox"
|
|
27
|
+
* data-tabindex="-1"
|
|
28
|
+
* ?quick="${this.quick}"
|
|
29
|
+
* .offset=${this.offset}
|
|
30
|
+
* .align=${this.align}
|
|
31
|
+
* .alignStrategy=${this.alignStrategy}
|
|
32
|
+
* ?keep-open-select=${this.keepOpenSelect}
|
|
33
|
+
* no-focus-control
|
|
34
|
+
* ?open=${this.open}
|
|
35
|
+
* @open="${() => (this.open = true)}"
|
|
36
|
+
* @close="${() => (this.open = false)}"
|
|
37
|
+
* @select=${this.handleMenuSelect}
|
|
38
|
+
* >
|
|
39
|
+
* <slot @slotchange=${this.handleItemsSlotChange}></slot>
|
|
40
|
+
* </your-menu>
|
|
41
|
+
* ```
|
|
42
|
+
*/ renderMenu() {
|
|
43
|
+
return html``;
|
|
44
|
+
}
|
|
45
|
+
handleInputSlotChange() {
|
|
46
|
+
const input = this.$input;
|
|
47
|
+
if (!input) return;
|
|
48
|
+
const $realInput = this.$input.$inputOrTextarea;
|
|
49
|
+
if ($realInput) {
|
|
50
|
+
$realInput.role = 'combobox';
|
|
51
|
+
$realInput.ariaExpanded = String(this.open);
|
|
52
|
+
$realInput.ariaHasPopup = 'listbox';
|
|
53
|
+
$realInput.ariaAutoComplete = this.mode;
|
|
54
|
+
$realInput.ariaControlsElements = [
|
|
55
|
+
this.$menu
|
|
56
|
+
];
|
|
57
|
+
input.addEventListener('input', this.handleInput.bind(this));
|
|
58
|
+
input.addEventListener('keydown', this.handleInputKeydown.bind(this));
|
|
59
|
+
input.addEventListener('click', ()=>this.open = !this.open);
|
|
60
|
+
this.$menu.attach($realInput);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
handleItemsSlotChange() {
|
|
64
|
+
// Initial filter based on current input value (if any)
|
|
65
|
+
this.filterOptions(this.$input?.value || '');
|
|
66
|
+
}
|
|
67
|
+
handleInput(event) {
|
|
68
|
+
const inputEl = this.$input.$inputOrTextarea;
|
|
69
|
+
const currentValue = inputEl.value;
|
|
70
|
+
this.open = true;
|
|
71
|
+
// Filter items based on current value
|
|
72
|
+
const firstMatch = this.filterOptions(currentValue);
|
|
73
|
+
// Inline completion logic (mode = both)
|
|
74
|
+
if (this.mode === 'both' && event.inputType !== 'deleteContentBackward') {
|
|
75
|
+
if (firstMatch && currentValue.length > 0) {
|
|
76
|
+
this.applyInlineAutoComplete(inputEl, firstMatch, currentValue);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
applyInlineAutoComplete(inputEl, item, typedValue) {
|
|
81
|
+
const suggestion = item.textContent?.trim() || '';
|
|
82
|
+
if (suggestion.toLowerCase().startsWith(typedValue.toLowerCase())) {
|
|
83
|
+
inputEl.value = suggestion;
|
|
84
|
+
inputEl.setSelectionRange(typedValue.length, suggestion.length);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
filterOptions(searchTerm) {
|
|
88
|
+
if (this.mode === 'none') return null;
|
|
89
|
+
const normalizedSearch = searchTerm.toLowerCase();
|
|
90
|
+
let firstMatch = null;
|
|
91
|
+
this.itemSlotElements.forEach((item)=>{
|
|
92
|
+
const text = (item.textContent || '').toLowerCase().trim();
|
|
93
|
+
const isMatch = text.startsWith(normalizedSearch);
|
|
94
|
+
item.hidden = !isMatch;
|
|
95
|
+
if (isMatch && !firstMatch) {
|
|
96
|
+
firstMatch = item;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return firstMatch;
|
|
100
|
+
}
|
|
101
|
+
handleInputKeydown(event) {
|
|
102
|
+
if (this.$input?.disabled) return;
|
|
103
|
+
if ([
|
|
104
|
+
'Enter',
|
|
105
|
+
'Escape',
|
|
106
|
+
'ArrowUp',
|
|
107
|
+
'ArrowDown'
|
|
108
|
+
].includes(event.key)) {
|
|
109
|
+
const eventClone = new KeyboardEvent(event.type, event);
|
|
110
|
+
eventClone.preventDefault = ()=>event.preventDefault();
|
|
111
|
+
eventClone.stopPropagation = ()=>event.stopPropagation();
|
|
112
|
+
this.$menu.$menu.dispatchEvent(eventClone);
|
|
113
|
+
if (event.key === 'Enter' && !this.keepOpenSelect) this.open = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
handleMenuSelect(event) {
|
|
117
|
+
const selectedItem = event.detail.item;
|
|
118
|
+
const newValue = selectedItem.getAttribute('value') || selectedItem.textContent?.trim() || '';
|
|
119
|
+
if (this.$input) {
|
|
120
|
+
this.$input.value = newValue;
|
|
121
|
+
}
|
|
122
|
+
if (!this.keepOpenSelect) {
|
|
123
|
+
this.open = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
updated(changed) {
|
|
127
|
+
if (changed.has('open') && this.$input) {
|
|
128
|
+
const $input = this.$input.$inputOrTextarea;
|
|
129
|
+
if ($input) {
|
|
130
|
+
$input.ariaExpanded = String(this.open);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
constructor(...args){
|
|
135
|
+
super(...args), this.open = false, // Passed to menu
|
|
136
|
+
this.quick = false, this.offset = 0, this.align = 'bottom-start', this.alignStrategy = 'absolute', this.keepOpenSelect = false, this.mode = 'none';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
_ts_decorate([
|
|
140
|
+
property({
|
|
141
|
+
type: Boolean
|
|
142
|
+
})
|
|
143
|
+
], Autocomplete.prototype, "open", void 0);
|
|
144
|
+
_ts_decorate([
|
|
145
|
+
property({
|
|
146
|
+
type: Boolean
|
|
147
|
+
})
|
|
148
|
+
], Autocomplete.prototype, "quick", void 0);
|
|
149
|
+
_ts_decorate([
|
|
150
|
+
property({
|
|
151
|
+
type: Number
|
|
152
|
+
})
|
|
153
|
+
], Autocomplete.prototype, "offset", void 0);
|
|
154
|
+
_ts_decorate([
|
|
155
|
+
property({
|
|
156
|
+
reflect: true
|
|
157
|
+
})
|
|
158
|
+
], Autocomplete.prototype, "align", void 0);
|
|
159
|
+
_ts_decorate([
|
|
160
|
+
property({
|
|
161
|
+
type: String,
|
|
162
|
+
reflect: true,
|
|
163
|
+
attribute: 'align-strategy'
|
|
164
|
+
})
|
|
165
|
+
], Autocomplete.prototype, "alignStrategy", void 0);
|
|
166
|
+
_ts_decorate([
|
|
167
|
+
property({
|
|
168
|
+
type: Boolean,
|
|
169
|
+
attribute: 'keep-open-select'
|
|
170
|
+
})
|
|
171
|
+
], Autocomplete.prototype, "keepOpenSelect", void 0);
|
|
172
|
+
_ts_decorate([
|
|
173
|
+
property()
|
|
174
|
+
], Autocomplete.prototype, "mode", void 0);
|
|
175
|
+
_ts_decorate([
|
|
176
|
+
query('[part="menu"]')
|
|
177
|
+
], Autocomplete.prototype, "$menu", void 0);
|
|
178
|
+
_ts_decorate([
|
|
179
|
+
queryAssignedElements({
|
|
180
|
+
slot: 'input',
|
|
181
|
+
flatten: true
|
|
182
|
+
})
|
|
183
|
+
], Autocomplete.prototype, "inputSlotElements", void 0);
|
|
184
|
+
_ts_decorate([
|
|
185
|
+
queryAssignedElements({
|
|
186
|
+
flatten: true
|
|
187
|
+
})
|
|
188
|
+
], Autocomplete.prototype, "itemSlotElements", void 0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { filterOptions } from '../menu.js';
|
|
2
2
|
export class ListController {
|
|
3
3
|
constructor(host, config){
|
|
4
|
-
this.
|
|
4
|
+
this._focusedItem = null;
|
|
5
5
|
this.searchString = '';
|
|
6
6
|
this.searchTimeout = null;
|
|
7
7
|
const { isItem, getPossibleItems, blurItem, focusItem, wrapNavigation } = config;
|
|
@@ -18,8 +18,8 @@ export class ListController {
|
|
|
18
18
|
return this.getPossibleItems().filter(this.isItem);
|
|
19
19
|
}
|
|
20
20
|
get currentIndex() {
|
|
21
|
-
|
|
22
|
-
return items.
|
|
21
|
+
if (!this._focusedItem) return -1;
|
|
22
|
+
return this.items.indexOf(this._focusedItem);
|
|
23
23
|
}
|
|
24
24
|
handleType(char) {
|
|
25
25
|
const searchString = this.getSearchString(char);
|
|
@@ -53,13 +53,13 @@ export class ListController {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
_focusItem(item) {
|
|
56
|
-
if (this.
|
|
56
|
+
if (this._focusedItem !== null) this._blurItem(this._focusedItem);
|
|
57
57
|
this.focusItem(item);
|
|
58
|
-
this.
|
|
58
|
+
this._focusedItem = item;
|
|
59
59
|
}
|
|
60
60
|
_blurItem(item) {
|
|
61
61
|
this.blurItem(item);
|
|
62
|
-
this.
|
|
62
|
+
this._focusedItem = null;
|
|
63
63
|
}
|
|
64
64
|
focusFirstItem() {
|
|
65
65
|
this._focusItem(this.items[0]);
|
|
@@ -68,24 +68,44 @@ export class ListController {
|
|
|
68
68
|
this._focusItem(this.items[this.items.length - 1]);
|
|
69
69
|
}
|
|
70
70
|
focusNextItem() {
|
|
71
|
-
const
|
|
71
|
+
const items = this.items;
|
|
72
|
+
const count = items.length;
|
|
72
73
|
if (count === 0) return;
|
|
73
|
-
let nextIndex = this.
|
|
74
|
+
let nextIndex = this.currentIndex + 1;
|
|
74
75
|
if (nextIndex >= count) {
|
|
75
|
-
nextIndex = this.wrapNavigation() ? count - 1
|
|
76
|
+
nextIndex = this.wrapNavigation() ? 0 : count - 1;
|
|
76
77
|
}
|
|
77
|
-
this._focusItem(
|
|
78
|
+
this._focusItem(items[nextIndex]);
|
|
78
79
|
}
|
|
79
80
|
focusPreviousItem() {
|
|
80
|
-
const
|
|
81
|
+
const items = this.items;
|
|
82
|
+
const count = items.length;
|
|
81
83
|
if (count === 0) return;
|
|
82
|
-
let prevIndex = this.
|
|
84
|
+
let prevIndex = this.currentIndex - 1;
|
|
83
85
|
if (prevIndex < 0) {
|
|
84
|
-
prevIndex = this.wrapNavigation() ?
|
|
86
|
+
prevIndex = this.wrapNavigation() ? count - 1 : 0;
|
|
85
87
|
}
|
|
86
|
-
this._focusItem(
|
|
88
|
+
this._focusItem(items[prevIndex]);
|
|
87
89
|
}
|
|
88
90
|
handleSlotChange() {
|
|
89
|
-
|
|
91
|
+
const items = this.items;
|
|
92
|
+
const index = this.currentIndex;
|
|
93
|
+
this._focusedItem = index >= 0 ? items[index] : null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function getIndexByLetter(options, filter, startIndex = 0) {
|
|
97
|
+
const orderedOptions = [
|
|
98
|
+
...options.slice(startIndex),
|
|
99
|
+
...options.slice(0, startIndex)
|
|
100
|
+
];
|
|
101
|
+
const firstMatch = filterOptions(orderedOptions, filter)[0];
|
|
102
|
+
const allSameLetter = (array)=>array.every((letter)=>letter === array[0]);
|
|
103
|
+
if (firstMatch) {
|
|
104
|
+
return options.indexOf(firstMatch);
|
|
105
|
+
} else if (allSameLetter(filter.split(''))) {
|
|
106
|
+
const matches = filterOptions(orderedOptions, filter[0]);
|
|
107
|
+
return options.indexOf(matches[0]);
|
|
108
|
+
} else {
|
|
109
|
+
return -1;
|
|
90
110
|
}
|
|
91
111
|
}
|
package/src/base/input.js
CHANGED
|
@@ -89,22 +89,22 @@ export class Input extends Base {
|
|
|
89
89
|
this.dispatchEvent(newEvent);
|
|
90
90
|
}
|
|
91
91
|
syncValidity() {
|
|
92
|
-
if (!this
|
|
93
|
-
this[internals].setValidity(this
|
|
92
|
+
if (!this.$inputOrTextarea) return;
|
|
93
|
+
this[internals].setValidity(this.$inputOrTextarea.validity, this.$inputOrTextarea.validationMessage, this.$inputOrTextarea);
|
|
94
94
|
}
|
|
95
95
|
select() {
|
|
96
|
-
this
|
|
96
|
+
this.$inputOrTextarea?.select();
|
|
97
97
|
}
|
|
98
98
|
stepUp(n) {
|
|
99
|
-
this
|
|
99
|
+
this.$inputOrTextarea?.stepUp(n);
|
|
100
100
|
this.handleInput({
|
|
101
|
-
target: this
|
|
101
|
+
target: this.$inputOrTextarea
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
stepDown(n) {
|
|
105
|
-
this
|
|
105
|
+
this.$inputOrTextarea?.stepDown(n);
|
|
106
106
|
this.handleInput({
|
|
107
|
-
target: this
|
|
107
|
+
target: this.$inputOrTextarea
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
formResetCallback() {
|
|
@@ -189,4 +189,4 @@ _ts_decorate([
|
|
|
189
189
|
], Input.prototype, "focused", void 0);
|
|
190
190
|
_ts_decorate([
|
|
191
191
|
query('[part~=input]')
|
|
192
|
-
], Input.prototype, "inputOrTextarea", void 0);
|
|
192
|
+
], Input.prototype, "$inputOrTextarea", void 0);
|
package/src/base/list.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { _ as _ts_decorate } from "@swc/helpers/_/_ts_decorate";
|
|
2
|
+
import { LitElement, html } from 'lit';
|
|
3
|
+
import { property, query, queryAssignedElements } from 'lit/decorators.js';
|
|
4
|
+
import { setFocusVisible } from '../core/focus-visible.js';
|
|
5
|
+
import { InternalsAttached } from './mixins/internals-attached.js';
|
|
6
|
+
import { FocusDelegated } from './mixins/focus-delegated.js';
|
|
7
|
+
import { ListController } from './controllers/list-controller.js';
|
|
8
|
+
const Base = FocusDelegated(InternalsAttached(LitElement));
|
|
9
|
+
/**
|
|
10
|
+
* @csspart list
|
|
11
|
+
* @csspart items
|
|
12
|
+
*
|
|
13
|
+
* @fires {Event} open - Fires when the menu is opened.
|
|
14
|
+
* @fires {Event} close - Fires when the menu is closed.
|
|
15
|
+
* @fires {ListSelectEvent} select - Fires when an item is selected.
|
|
16
|
+
* @fires {ListItemFocusEvent} item-focus - Fires when an item is focused
|
|
17
|
+
*/ export class List extends Base {
|
|
18
|
+
get $items() {
|
|
19
|
+
return this.listController.items || [];
|
|
20
|
+
}
|
|
21
|
+
render() {
|
|
22
|
+
return html`<div
|
|
23
|
+
part="list"
|
|
24
|
+
role="listbox"
|
|
25
|
+
tabindex="0"
|
|
26
|
+
@keydown=${this.#handleKeyDown}
|
|
27
|
+
@focusin=${this.#handleFocusIn}
|
|
28
|
+
@focusout=${this.#handleFocusOut}
|
|
29
|
+
@pointerdown=${this.#handlePointerDown}
|
|
30
|
+
@click=${this.#handleClick}
|
|
31
|
+
@mouseover=${this.#handleMouseOver}
|
|
32
|
+
>
|
|
33
|
+
${this.renderItemSlot()}
|
|
34
|
+
</div>`;
|
|
35
|
+
}
|
|
36
|
+
renderItemSlot() {
|
|
37
|
+
return html`<slot part="items"></slot>`;
|
|
38
|
+
}
|
|
39
|
+
updated(changed) {
|
|
40
|
+
// TODO: Find somewhere to put this
|
|
41
|
+
// this.listController.clearSearch();
|
|
42
|
+
}
|
|
43
|
+
#handleKeyDown(event) {
|
|
44
|
+
if (event.defaultPrevented) return;
|
|
45
|
+
const action = getActionFromKey(event);
|
|
46
|
+
const items = this.$items;
|
|
47
|
+
const currentIndex = this.listController.currentIndex;
|
|
48
|
+
const maxIndex = items.length - 1;
|
|
49
|
+
switch(action){
|
|
50
|
+
case MenuActions.Last:
|
|
51
|
+
case MenuActions.First:
|
|
52
|
+
case MenuActions.Next:
|
|
53
|
+
case MenuActions.Previous:
|
|
54
|
+
case MenuActions.PageUp:
|
|
55
|
+
case MenuActions.PageDown:
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
const nextIndex = getUpdatedIndex(currentIndex, maxIndex, action);
|
|
58
|
+
this.listController._focusItem(items[nextIndex]);
|
|
59
|
+
return;
|
|
60
|
+
case MenuActions.CloseSelect:
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
if (currentIndex >= 0) {
|
|
63
|
+
items[currentIndex].focused = false;
|
|
64
|
+
this.dispatchEvent(new CustomEvent('select', {
|
|
65
|
+
detail: {
|
|
66
|
+
item: items[currentIndex],
|
|
67
|
+
index: currentIndex
|
|
68
|
+
},
|
|
69
|
+
bubbles: true,
|
|
70
|
+
composed: true
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
case MenuActions.Type:
|
|
75
|
+
this.listController.handleType(event.key);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
#handleFocusIn() {
|
|
80
|
+
if (this.currentIndex == -1) this.listController.focusFirstItem();
|
|
81
|
+
else this.$items[this.currentIndex].focused = true;
|
|
82
|
+
}
|
|
83
|
+
#handleFocusOut() {
|
|
84
|
+
this.$items[this.currentIndex].focused = false;
|
|
85
|
+
}
|
|
86
|
+
#handleMouseOver(event) {
|
|
87
|
+
setFocusVisible(false);
|
|
88
|
+
const item = event.target.closest(this._possibleItemTags.join(','));
|
|
89
|
+
if (item && this.listController.items.includes(item)) {
|
|
90
|
+
this.listController._focusItem(item);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
#handlePointerDown(event) {
|
|
94
|
+
event.preventDefault(); // This makes sure that the container is focused
|
|
95
|
+
this.$list.focus();
|
|
96
|
+
const item = this.#getEventItem(event);
|
|
97
|
+
if (!item || !this.listController.items.includes(item)) return;
|
|
98
|
+
this.listController._focusItem(item);
|
|
99
|
+
}
|
|
100
|
+
#handleClick(event) {
|
|
101
|
+
this.$list.focus();
|
|
102
|
+
const item = this.#getEventItem(event);
|
|
103
|
+
if (!item || !this.listController.items.includes(item)) return;
|
|
104
|
+
this.dispatchEvent(new CustomEvent('select', {
|
|
105
|
+
detail: {
|
|
106
|
+
item: item,
|
|
107
|
+
index: this.listController.items.indexOf(item)
|
|
108
|
+
},
|
|
109
|
+
bubbles: true,
|
|
110
|
+
composed: true
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
#getEventItem(event) {
|
|
114
|
+
const selector = this._possibleItemTags.join(',');
|
|
115
|
+
return event.target.closest(selector);
|
|
116
|
+
}
|
|
117
|
+
get currentIndex() {
|
|
118
|
+
return this.listController?.currentIndex;
|
|
119
|
+
}
|
|
120
|
+
focusFirstItem() {
|
|
121
|
+
this.listController.focusFirstItem();
|
|
122
|
+
}
|
|
123
|
+
focusLastItem() {
|
|
124
|
+
this.listController.focusLastItem();
|
|
125
|
+
}
|
|
126
|
+
focusItem(item) {
|
|
127
|
+
this.listController._focusItem(item);
|
|
128
|
+
}
|
|
129
|
+
constructor(...args){
|
|
130
|
+
super(...args), this._possibleItemTags = [], this._scrollPadding = 0, this.noFocusControl = false, this.listController = new ListController(this, {
|
|
131
|
+
isItem: (item)=>this._possibleItemTags.includes(item.tagName.toLowerCase()) && !item.hasAttribute('disabled') && !item.hidden,
|
|
132
|
+
getPossibleItems: ()=>this.slotItems,
|
|
133
|
+
blurItem: (item)=>{
|
|
134
|
+
item.focused = false;
|
|
135
|
+
},
|
|
136
|
+
focusItem: (item)=>{
|
|
137
|
+
item.focused = true;
|
|
138
|
+
if (!this.noFocusControl) {
|
|
139
|
+
this.$list.ariaActiveDescendantElement = item;
|
|
140
|
+
}
|
|
141
|
+
scrollItemIntoView(this.$list, item, this._scrollPadding);
|
|
142
|
+
this.dispatchEvent(new CustomEvent('item-focus', {
|
|
143
|
+
detail: {
|
|
144
|
+
item: item
|
|
145
|
+
},
|
|
146
|
+
bubbles: true,
|
|
147
|
+
composed: true
|
|
148
|
+
}));
|
|
149
|
+
},
|
|
150
|
+
wrapNavigation: ()=>false
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
_ts_decorate([
|
|
155
|
+
property({
|
|
156
|
+
type: Boolean,
|
|
157
|
+
attribute: 'no-focus-control'
|
|
158
|
+
})
|
|
159
|
+
], List.prototype, "noFocusControl", void 0);
|
|
160
|
+
_ts_decorate([
|
|
161
|
+
query('[part="list"]')
|
|
162
|
+
], List.prototype, "$list", void 0);
|
|
163
|
+
_ts_decorate([
|
|
164
|
+
queryAssignedElements({
|
|
165
|
+
flatten: true
|
|
166
|
+
})
|
|
167
|
+
], List.prototype, "slotItems", void 0);
|
|
168
|
+
// Reference: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/
|
|
169
|
+
export const MenuActions = {
|
|
170
|
+
Close: 0,
|
|
171
|
+
CloseSelect: 1,
|
|
172
|
+
First: 2,
|
|
173
|
+
Last: 3,
|
|
174
|
+
Next: 4,
|
|
175
|
+
Open: 5,
|
|
176
|
+
PageDown: 6,
|
|
177
|
+
PageUp: 7,
|
|
178
|
+
Previous: 8,
|
|
179
|
+
Select: 9,
|
|
180
|
+
Type: 10
|
|
181
|
+
};
|
|
182
|
+
export function filterOptions(options = [], filter, exclude = []) {
|
|
183
|
+
return options.filter((option)=>{
|
|
184
|
+
const matches = option.toLowerCase().indexOf(filter.toLowerCase()) === 0;
|
|
185
|
+
return matches && exclude.indexOf(option) < 0;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
export function getActionFromKey(event) {
|
|
189
|
+
const { key, altKey, ctrlKey, metaKey } = event;
|
|
190
|
+
if (key === 'Home') {
|
|
191
|
+
return MenuActions.First;
|
|
192
|
+
}
|
|
193
|
+
if (key === 'End') {
|
|
194
|
+
return MenuActions.Last;
|
|
195
|
+
}
|
|
196
|
+
if (key === 'Backspace' || key === 'Clear' || key.length === 1 && key !== ' ' && !altKey && !ctrlKey && !metaKey) {
|
|
197
|
+
return MenuActions.Type;
|
|
198
|
+
}
|
|
199
|
+
if (key === 'ArrowUp' && altKey) {
|
|
200
|
+
return MenuActions.CloseSelect;
|
|
201
|
+
} else if (key === 'ArrowDown' && !altKey) {
|
|
202
|
+
return MenuActions.Next;
|
|
203
|
+
} else if (key === 'ArrowUp') {
|
|
204
|
+
return MenuActions.Previous;
|
|
205
|
+
} else if (key === 'PageUp') {
|
|
206
|
+
return MenuActions.PageUp;
|
|
207
|
+
} else if (key === 'PageDown') {
|
|
208
|
+
return MenuActions.PageDown;
|
|
209
|
+
} else if (key === 'Escape') {
|
|
210
|
+
return MenuActions.Close;
|
|
211
|
+
} else if (key === 'Enter' || key === ' ') {
|
|
212
|
+
return MenuActions.CloseSelect;
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
export function getUpdatedIndex(currentIndex, maxIndex, action) {
|
|
217
|
+
const pageSize = 10;
|
|
218
|
+
switch(action){
|
|
219
|
+
case MenuActions.First:
|
|
220
|
+
return 0;
|
|
221
|
+
case MenuActions.Last:
|
|
222
|
+
return maxIndex;
|
|
223
|
+
case MenuActions.Previous:
|
|
224
|
+
return Math.max(0, currentIndex - 1);
|
|
225
|
+
case MenuActions.Next:
|
|
226
|
+
return Math.min(maxIndex, currentIndex + 1);
|
|
227
|
+
case MenuActions.PageUp:
|
|
228
|
+
return Math.max(0, currentIndex - pageSize);
|
|
229
|
+
case MenuActions.PageDown:
|
|
230
|
+
return Math.min(maxIndex, currentIndex + pageSize);
|
|
231
|
+
default:
|
|
232
|
+
return currentIndex;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export function scrollItemIntoView(list, item, paddingY = 0) {
|
|
236
|
+
if (!list) return;
|
|
237
|
+
// Basic scroll into view logic
|
|
238
|
+
const menuRect = list.getBoundingClientRect();
|
|
239
|
+
const itemRect = item.getBoundingClientRect();
|
|
240
|
+
if (itemRect.bottom + paddingY > menuRect.bottom) {
|
|
241
|
+
list.scrollTop += itemRect.bottom - menuRect.bottom + paddingY;
|
|
242
|
+
} else if (itemRect.top - paddingY < menuRect.top) {
|
|
243
|
+
list.scrollTop -= menuRect.top - itemRect.top + paddingY;
|
|
244
|
+
}
|
|
245
|
+
}
|