kempo-ui 0.1.4 → 0.1.5
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/dist/components/FilterItem.js +3 -0
- package/dist/components/FilterList.js +1 -1
- package/docs/nav.inc.js +3 -0
- package/docs/src/components/FilterItem.js +3 -0
- package/docs/src/components/FilterList.js +1 -1
- package/package.json +1 -1
- package/src/components/FilterItem.js +3 -0
- package/src/components/FilterList.js +37 -0
- package/tests/components/FilterList.browser-test.js +183 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import ShadowComponent from"./ShadowComponent.js";import{html,css}from"../lit-all.min.js";export default class FilterList extends ShadowComponent{render(){return html`<slot></slot>`}
|
|
1
|
+
import ShadowComponent from"./ShadowComponent.js";import{html,css}from"../lit-all.min.js";export default class FilterList extends ShadowComponent{#e=-1;render(){return html`<slot></slot>`}handleKeydown=e=>{const t=this.#t();t.length&&("ArrowDown"===e.key?(e.preventDefault(),this.#e=Math.min(this.#e+1,t.length-1),this.#s(t)):"ArrowUp"===e.key?(e.preventDefault(),this.#e=Math.max(this.#e-1,0),this.#s(t)):"Enter"===e.key&&this.#e>=0&&this.#e<t.length&&(e.preventDefault(),t[this.#e].querySelector("a")?.click()))};clearFocus=()=>{this.#e=-1,this.querySelectorAll("k-filter-item[kb-focus]").forEach(e=>e.removeAttribute("kb-focus"))};filter(e){const t=e.toLowerCase().split(/\s+/).filter(e=>e.length>0);this.querySelectorAll("k-filter-item").forEach(e=>{const s=(e.getAttribute("filter-keywords")||"").toLowerCase();e.hidden=t.length>0&&!t.every(e=>s.includes(e))}),this.clearFocus()}#t=()=>[...this.querySelectorAll("k-filter-item:not([hidden])")];#s=e=>{this.querySelectorAll("k-filter-item[kb-focus]").forEach(e=>e.removeAttribute("kb-focus")),e[this.#e]?.setAttribute("kb-focus",""),e[this.#e]?.scrollIntoView({block:"nearest"})};static styles=css`
|
|
2
2
|
:host {
|
|
3
3
|
display: block;
|
|
4
4
|
}
|
package/docs/nav.inc.js
CHANGED
|
@@ -34,6 +34,7 @@ const openSearch = async () => {
|
|
|
34
34
|
|
|
35
35
|
const closeSearch = () => {
|
|
36
36
|
searchDropdown.hidden = true;
|
|
37
|
+
navSearchList?.clearFocus();
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
searchInput.addEventListener('focus', openSearch);
|
|
@@ -47,6 +48,8 @@ searchInput.addEventListener('keydown', e => {
|
|
|
47
48
|
searchInput.value = '';
|
|
48
49
|
searchInput.blur();
|
|
49
50
|
closeSearch();
|
|
51
|
+
} else if(e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter') {
|
|
52
|
+
navSearchList?.handleKeydown(e);
|
|
50
53
|
}
|
|
51
54
|
});
|
|
52
55
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import ShadowComponent from"./ShadowComponent.js";import{html,css}from"../lit-all.min.js";export default class FilterList extends ShadowComponent{render(){return html`<slot></slot>`}
|
|
1
|
+
import ShadowComponent from"./ShadowComponent.js";import{html,css}from"../lit-all.min.js";export default class FilterList extends ShadowComponent{#e=-1;render(){return html`<slot></slot>`}handleKeydown=e=>{const t=this.#t();t.length&&("ArrowDown"===e.key?(e.preventDefault(),this.#e=Math.min(this.#e+1,t.length-1),this.#s(t)):"ArrowUp"===e.key?(e.preventDefault(),this.#e=Math.max(this.#e-1,0),this.#s(t)):"Enter"===e.key&&this.#e>=0&&this.#e<t.length&&(e.preventDefault(),t[this.#e].querySelector("a")?.click()))};clearFocus=()=>{this.#e=-1,this.querySelectorAll("k-filter-item[kb-focus]").forEach(e=>e.removeAttribute("kb-focus"))};filter(e){const t=e.toLowerCase().split(/\s+/).filter(e=>e.length>0);this.querySelectorAll("k-filter-item").forEach(e=>{const s=(e.getAttribute("filter-keywords")||"").toLowerCase();e.hidden=t.length>0&&!t.every(e=>s.includes(e))}),this.clearFocus()}#t=()=>[...this.querySelectorAll("k-filter-item:not([hidden])")];#s=e=>{this.querySelectorAll("k-filter-item[kb-focus]").forEach(e=>e.removeAttribute("kb-focus")),e[this.#e]?.setAttribute("kb-focus",""),e[this.#e]?.scrollIntoView({block:"nearest"})};static styles=css`
|
|
2
2
|
:host {
|
|
3
3
|
display: block;
|
|
4
4
|
}
|
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@ import ShadowComponent from './ShadowComponent.js';
|
|
|
2
2
|
import { html, css } from '../lit-all.min.js';
|
|
3
3
|
|
|
4
4
|
export default class FilterList extends ShadowComponent {
|
|
5
|
+
#focusedIndex = -1;
|
|
6
|
+
|
|
5
7
|
/*
|
|
6
8
|
Rendering
|
|
7
9
|
*/
|
|
@@ -12,14 +14,49 @@ export default class FilterList extends ShadowComponent {
|
|
|
12
14
|
/*
|
|
13
15
|
Public Methods
|
|
14
16
|
*/
|
|
17
|
+
handleKeydown = e => {
|
|
18
|
+
const items = this.#visibleItems();
|
|
19
|
+
if(!items.length) return;
|
|
20
|
+
|
|
21
|
+
if(e.key === 'ArrowDown') {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
this.#focusedIndex = Math.min(this.#focusedIndex + 1, items.length - 1);
|
|
24
|
+
this.#applyFocus(items);
|
|
25
|
+
} else if(e.key === 'ArrowUp') {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
this.#focusedIndex = Math.max(this.#focusedIndex - 1, 0);
|
|
28
|
+
this.#applyFocus(items);
|
|
29
|
+
} else if(e.key === 'Enter' && this.#focusedIndex >= 0 && this.#focusedIndex < items.length) {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
items[this.#focusedIndex].querySelector('a')?.click();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
clearFocus = () => {
|
|
36
|
+
this.#focusedIndex = -1;
|
|
37
|
+
this.querySelectorAll('k-filter-item[kb-focus]').forEach(el => el.removeAttribute('kb-focus'));
|
|
38
|
+
};
|
|
39
|
+
|
|
15
40
|
filter(term) {
|
|
16
41
|
const words = term.toLowerCase().split(/\s+/).filter(w => w.length > 0);
|
|
17
42
|
this.querySelectorAll('k-filter-item').forEach(item => {
|
|
18
43
|
const keywords = (item.getAttribute('filter-keywords') || '').toLowerCase();
|
|
19
44
|
item.hidden = words.length > 0 && !words.every(w => keywords.includes(w));
|
|
20
45
|
});
|
|
46
|
+
this.clearFocus();
|
|
21
47
|
}
|
|
22
48
|
|
|
49
|
+
/*
|
|
50
|
+
Private Methods
|
|
51
|
+
*/
|
|
52
|
+
#visibleItems = () => [...this.querySelectorAll('k-filter-item:not([hidden])')];
|
|
53
|
+
|
|
54
|
+
#applyFocus = items => {
|
|
55
|
+
this.querySelectorAll('k-filter-item[kb-focus]').forEach(el => el.removeAttribute('kb-focus'));
|
|
56
|
+
items[this.#focusedIndex]?.setAttribute('kb-focus', '');
|
|
57
|
+
items[this.#focusedIndex]?.scrollIntoView({ block: 'nearest' });
|
|
58
|
+
};
|
|
59
|
+
|
|
23
60
|
/*
|
|
24
61
|
Styles
|
|
25
62
|
*/
|
|
@@ -10,6 +10,17 @@ const createFilterList = async (items = []) => {
|
|
|
10
10
|
return { container, el };
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
const createFilterListWithLinks = async (items = []) => {
|
|
14
|
+
const container = document.createElement('div');
|
|
15
|
+
container.innerHTML = `<k-filter-list>${items.map(kw => `<k-filter-item filter-keywords="${kw}"><a href="#">${kw}</a></k-filter-item>`).join('')}</k-filter-list>`;
|
|
16
|
+
document.body.appendChild(container);
|
|
17
|
+
const el = container.querySelector('k-filter-list');
|
|
18
|
+
await el.updateComplete;
|
|
19
|
+
return { container, el };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const keydown = (el, key) => el.handleKeydown(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
23
|
+
|
|
13
24
|
const cleanup = (container) => {
|
|
14
25
|
if(container && container.parentNode){
|
|
15
26
|
container.parentNode.removeChild(container);
|
|
@@ -159,4 +170,176 @@ export default {
|
|
|
159
170
|
cleanup(container);
|
|
160
171
|
pass('All items restored after clearing filter');
|
|
161
172
|
},
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
handleKeydown() — ArrowDown
|
|
176
|
+
*/
|
|
177
|
+
'ArrowDown should set kb-focus on the first item': async ({pass, fail}) => {
|
|
178
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana', 'cherry']);
|
|
179
|
+
keydown(el, 'ArrowDown');
|
|
180
|
+
const focused = el.querySelector('k-filter-item[kb-focus]');
|
|
181
|
+
if(!focused){
|
|
182
|
+
cleanup(container);
|
|
183
|
+
return fail('Expected a kb-focus item after ArrowDown');
|
|
184
|
+
}
|
|
185
|
+
if(focused.getAttribute('filter-keywords') !== 'apple'){
|
|
186
|
+
cleanup(container);
|
|
187
|
+
return fail(`Expected first item focused, got "${focused.getAttribute('filter-keywords')}"`);
|
|
188
|
+
}
|
|
189
|
+
cleanup(container);
|
|
190
|
+
pass('ArrowDown focuses first item');
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
'ArrowDown twice should focus the second item': async ({pass, fail}) => {
|
|
194
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana', 'cherry']);
|
|
195
|
+
keydown(el, 'ArrowDown');
|
|
196
|
+
keydown(el, 'ArrowDown');
|
|
197
|
+
const focused = el.querySelector('k-filter-item[kb-focus]');
|
|
198
|
+
if(focused?.getAttribute('filter-keywords') !== 'banana'){
|
|
199
|
+
cleanup(container);
|
|
200
|
+
return fail(`Expected second item focused, got "${focused?.getAttribute('filter-keywords')}"`);
|
|
201
|
+
}
|
|
202
|
+
cleanup(container);
|
|
203
|
+
pass('ArrowDown twice focuses second item');
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
'ArrowDown should not go past the last item': async ({pass, fail}) => {
|
|
207
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana']);
|
|
208
|
+
keydown(el, 'ArrowDown');
|
|
209
|
+
keydown(el, 'ArrowDown');
|
|
210
|
+
keydown(el, 'ArrowDown');
|
|
211
|
+
keydown(el, 'ArrowDown');
|
|
212
|
+
const focused = el.querySelector('k-filter-item[kb-focus]');
|
|
213
|
+
if(focused?.getAttribute('filter-keywords') !== 'banana'){
|
|
214
|
+
cleanup(container);
|
|
215
|
+
return fail('Focus should clamp at last item');
|
|
216
|
+
}
|
|
217
|
+
cleanup(container);
|
|
218
|
+
pass('ArrowDown clamps at last item');
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
'ArrowDown should skip hidden items': async ({pass, fail}) => {
|
|
222
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana', 'cherry']);
|
|
223
|
+
el.filter('a');
|
|
224
|
+
keydown(el, 'ArrowDown');
|
|
225
|
+
const focused = el.querySelector('k-filter-item[kb-focus]');
|
|
226
|
+
if(focused?.hidden){
|
|
227
|
+
cleanup(container);
|
|
228
|
+
return fail('Focused item should not be hidden');
|
|
229
|
+
}
|
|
230
|
+
cleanup(container);
|
|
231
|
+
pass('ArrowDown skips hidden items');
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
handleKeydown() — ArrowUp
|
|
236
|
+
*/
|
|
237
|
+
'ArrowUp should move focus backward': async ({pass, fail}) => {
|
|
238
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana', 'cherry']);
|
|
239
|
+
keydown(el, 'ArrowDown');
|
|
240
|
+
keydown(el, 'ArrowDown');
|
|
241
|
+
keydown(el, 'ArrowDown');
|
|
242
|
+
keydown(el, 'ArrowUp');
|
|
243
|
+
const focused = el.querySelector('k-filter-item[kb-focus]');
|
|
244
|
+
if(focused?.getAttribute('filter-keywords') !== 'banana'){
|
|
245
|
+
cleanup(container);
|
|
246
|
+
return fail(`Expected "banana" focused, got "${focused?.getAttribute('filter-keywords')}"`);
|
|
247
|
+
}
|
|
248
|
+
cleanup(container);
|
|
249
|
+
pass('ArrowUp moves focus backward');
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
'ArrowUp should not go before the first item': async ({pass, fail}) => {
|
|
253
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana']);
|
|
254
|
+
keydown(el, 'ArrowDown');
|
|
255
|
+
keydown(el, 'ArrowUp');
|
|
256
|
+
keydown(el, 'ArrowUp');
|
|
257
|
+
const focused = el.querySelector('k-filter-item[kb-focus]');
|
|
258
|
+
if(focused?.getAttribute('filter-keywords') !== 'apple'){
|
|
259
|
+
cleanup(container);
|
|
260
|
+
return fail('Focus should clamp at first item');
|
|
261
|
+
}
|
|
262
|
+
cleanup(container);
|
|
263
|
+
pass('ArrowUp clamps at first item');
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/*
|
|
267
|
+
handleKeydown() — Enter
|
|
268
|
+
*/
|
|
269
|
+
'Enter should click the link in the focused item': async ({pass, fail}) => {
|
|
270
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana']);
|
|
271
|
+
keydown(el, 'ArrowDown');
|
|
272
|
+
let clicked = false;
|
|
273
|
+
el.querySelector('k-filter-item[kb-focus] a').addEventListener('click', e => {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
clicked = true;
|
|
276
|
+
});
|
|
277
|
+
keydown(el, 'Enter');
|
|
278
|
+
if(!clicked){
|
|
279
|
+
cleanup(container);
|
|
280
|
+
return fail('Enter should click the focused link');
|
|
281
|
+
}
|
|
282
|
+
cleanup(container);
|
|
283
|
+
pass('Enter clicks the focused link');
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
'Enter without focus should do nothing': async ({pass, fail}) => {
|
|
287
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana']);
|
|
288
|
+
keydown(el, 'Enter');
|
|
289
|
+
cleanup(container);
|
|
290
|
+
pass('Enter without focus does nothing');
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
/*
|
|
294
|
+
clearFocus()
|
|
295
|
+
*/
|
|
296
|
+
'clearFocus should remove kb-focus attribute': async ({pass, fail}) => {
|
|
297
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana']);
|
|
298
|
+
keydown(el, 'ArrowDown');
|
|
299
|
+
el.clearFocus();
|
|
300
|
+
const focused = el.querySelector('k-filter-item[kb-focus]');
|
|
301
|
+
if(focused){
|
|
302
|
+
cleanup(container);
|
|
303
|
+
return fail('clearFocus should remove all kb-focus attributes');
|
|
304
|
+
}
|
|
305
|
+
cleanup(container);
|
|
306
|
+
pass('clearFocus removes kb-focus');
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
'filter() should reset keyboard focus': async ({pass, fail}) => {
|
|
310
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana']);
|
|
311
|
+
keydown(el, 'ArrowDown');
|
|
312
|
+
keydown(el, 'ArrowDown');
|
|
313
|
+
el.filter('');
|
|
314
|
+
const focused = el.querySelector('k-filter-item[kb-focus]');
|
|
315
|
+
if(focused){
|
|
316
|
+
cleanup(container);
|
|
317
|
+
return fail('filter() should reset keyboard focus');
|
|
318
|
+
}
|
|
319
|
+
keydown(el, 'ArrowDown');
|
|
320
|
+
const newFocused = el.querySelector('k-filter-item[kb-focus]');
|
|
321
|
+
if(newFocused?.getAttribute('filter-keywords') !== 'apple'){
|
|
322
|
+
cleanup(container);
|
|
323
|
+
return fail('After reset, ArrowDown should focus first item again');
|
|
324
|
+
}
|
|
325
|
+
cleanup(container);
|
|
326
|
+
pass('filter() resets keyboard focus');
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
/*
|
|
330
|
+
Only one kb-focus at a time
|
|
331
|
+
*/
|
|
332
|
+
'only one item should have kb-focus at a time': async ({pass, fail}) => {
|
|
333
|
+
const { container, el } = await createFilterListWithLinks(['apple', 'banana', 'cherry']);
|
|
334
|
+
keydown(el, 'ArrowDown');
|
|
335
|
+
keydown(el, 'ArrowDown');
|
|
336
|
+
keydown(el, 'ArrowDown');
|
|
337
|
+
const allFocused = el.querySelectorAll('k-filter-item[kb-focus]');
|
|
338
|
+
if(allFocused.length !== 1){
|
|
339
|
+
cleanup(container);
|
|
340
|
+
return fail(`Expected 1 kb-focus item, got ${allFocused.length}`);
|
|
341
|
+
}
|
|
342
|
+
cleanup(container);
|
|
343
|
+
pass('Only one item has kb-focus');
|
|
344
|
+
},
|
|
162
345
|
};
|