Skip to content

Commit 57dfbef

Browse files
authored
Mobile display tweaks + better keyboard support
https://claude.ai/share/98fc30dc-be94-4879-b856-06fa027bad15
1 parent 5821338 commit 57dfbef

File tree

1 file changed

+150
-19
lines changed

1 file changed

+150
-19
lines changed

keyboard-filters.html

+150-19
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
.filter-badge-value {
8080
padding: 0 12px;
8181
background-color: #f8faff;
82+
flex-grow: 1; /* Allow this to grow to take available space */
8283
}
8384

8485
.filter-badge-remove {
@@ -89,6 +90,7 @@
8990
background-color: #f8faff;
9091
border-left: 1px solid #cce0ff;
9192
transition: background-color 0.2s ease;
93+
margin-left: auto; /* Push to the right end */
9294
}
9395

9496
.filter-badge-remove:hover {
@@ -231,6 +233,15 @@
231233

232234
.filter-badge {
233235
width: 100%;
236+
justify-content: space-between; /* Ensure space distribution */
237+
}
238+
239+
.filter-badge-value {
240+
flex-grow: 1; /* Allow value to grow */
241+
}
242+
243+
.filter-badge-remove {
244+
margin-left: auto; /* Always push to end */
234245
}
235246
}
236247
</style>
@@ -276,6 +287,7 @@ <h1>Keyboard accessible filter prototype</h1>
276287

277288
// Current active filter being edited
278289
let activeFilter = null;
290+
let lastFocusedElementId = null; // Track the last focused element
279291

280292
// DOM references
281293
const filterBadgesContainer = document.getElementById('filterBadges');
@@ -316,15 +328,24 @@ <h1>Keyboard accessible filter prototype</h1>
316328

317329
// Render filter badges
318330
function renderFilterBadges() {
331+
// Save focus state before re-rendering
332+
saveCurrentFocus();
333+
319334
filterBadgesContainer.innerHTML = '';
320335

321336
filters.forEach(filter => {
322337
const badge = document.createElement('div');
323338
badge.className = 'filter-badge';
324339
badge.dataset.id = filter.id;
325340

341+
// Create unique identifiers for each focusable element
342+
const columnId = `filter-column-${filter.id}`;
343+
const operatorId = `filter-operator-${filter.id}`;
344+
const valueId = `filter-value-${filter.id}`;
345+
const removeId = `filter-remove-${filter.id}`;
346+
326347
badge.innerHTML = `
327-
<div class="filter-badge-part filter-badge-column" data-id="${filter.id}">
348+
<div id="${columnId}" class="filter-badge-part filter-badge-column" data-id="${filter.id}">
328349
${filter.column}
329350
<select class="dropdown-select" data-id="${filter.id}" tabindex="0" aria-label="Select column">
330351
<option value="">- remove filter -</option>
@@ -336,7 +357,7 @@ <h1>Keyboard accessible filter prototype</h1>
336357
<option value="keywords">keywords</option>
337358
</select>
338359
</div>
339-
<div class="filter-badge-part filter-badge-operator" data-id="${filter.id}">
360+
<div id="${operatorId}" class="filter-badge-part filter-badge-operator" data-id="${filter.id}">
340361
${operatorDisplay[filter.operator] || filter.operator}
341362
<select class="dropdown-select" data-id="${filter.id}" tabindex="0" aria-label="Select operator">
342363
<option value="exact">=</option>
@@ -363,8 +384,8 @@ <h1>Keyboard accessible filter prototype</h1>
363384
<option value="notblank__1">is not blank</option>
364385
</select>
365386
</div>
366-
<div class="filter-badge-part filter-badge-value" data-id="${filter.id}" tabindex="0" role="button" aria-label="Edit value">${filter.value}</div>
367-
<div class="filter-badge-part filter-badge-remove" data-id="${filter.id}" tabindex="0" role="button" aria-label="Remove filter">×</div>
387+
<div id="${valueId}" class="filter-badge-part filter-badge-value" data-id="${filter.id}" tabindex="0" role="button" aria-label="Edit value">${filter.value}</div>
388+
<div id="${removeId}" class="filter-badge-part filter-badge-remove" data-id="${filter.id}" tabindex="0" role="button" aria-label="Remove filter">×</div>
368389
`;
369390

370391
filterBadgesContainer.appendChild(badge);
@@ -376,10 +397,45 @@ <h1>Keyboard accessible filter prototype</h1>
376397
if (columnSelect) columnSelect.value = filter.column;
377398
if (operatorSelect) operatorSelect.value = filter.operator;
378399
});
400+
401+
// Restore focus after re-rendering
402+
restoreFocus();
403+
}
404+
405+
// Save current focused element
406+
function saveCurrentFocus() {
407+
const activeElement = document.activeElement;
408+
if (activeElement && activeElement.closest('.filter-badge')) {
409+
lastFocusedElementId = activeElement.id ||
410+
activeElement.closest('[id]')?.id ||
411+
activeElement.closest('[data-id]')?.dataset.id;
412+
}
413+
}
414+
415+
// Restore focus to the previously focused element
416+
function restoreFocus() {
417+
if (lastFocusedElementId) {
418+
const elementToFocus = document.getElementById(lastFocusedElementId);
419+
if (elementToFocus) {
420+
setTimeout(() => {
421+
elementToFocus.focus();
422+
}, 0);
423+
}
424+
}
379425
}
380426

381427
// Setup event listeners
382428
function setupEventListeners() {
429+
// Track focus events
430+
document.addEventListener('focusin', (e) => {
431+
if (e.target.closest('.filter-badge') || e.target.closest('.add-filter-badge')) {
432+
const element = e.target.closest('[id]');
433+
if (element) {
434+
lastFocusedElementId = element.id;
435+
}
436+
}
437+
});
438+
383439
// Add filter badge
384440
addFilterBadge.addEventListener('click', () => {
385441
const newFilter = {
@@ -392,13 +448,18 @@ <h1>Keyboard accessible filter prototype</h1>
392448
filters.push(newFilter);
393449
renderFilterBadges();
394450

395-
// Auto-focus the column select
451+
// Auto-focus the column select of the new filter
396452
setTimeout(() => {
397-
const columnSelect = document.querySelector(`.filter-badge[data-id="${newFilter.id}"] .filter-badge-column select`);
398-
if (columnSelect) {
399-
columnSelect.focus();
453+
const columnId = `filter-column-${newFilter.id}`;
454+
const columnElement = document.getElementById(columnId);
455+
if (columnElement) {
456+
const columnSelect = columnElement.querySelector('select');
457+
if (columnSelect) {
458+
columnSelect.focus();
459+
lastFocusedElementId = columnId;
460+
}
400461
}
401-
}, 100);
462+
}, 50);
402463
});
403464

404465
// Add keyboard support for "add filter" button
@@ -423,7 +484,7 @@ <h1>Keyboard accessible filter prototype</h1>
423484
return;
424485
}
425486

426-
// Update the display text
487+
// Update the display text without full re-render
427488
e.target.parentElement.childNodes[0].textContent = filter.column;
428489
}
429490
}
@@ -437,7 +498,7 @@ <h1>Keyboard accessible filter prototype</h1>
437498
if (filter) {
438499
filter.operator = e.target.value;
439500

440-
// Update the display text
501+
// Update the display text without full re-render
441502
e.target.parentElement.childNodes[0].textContent = operatorDisplay[filter.operator] || filter.operator;
442503
}
443504
}
@@ -454,11 +515,41 @@ <h1>Keyboard accessible filter prototype</h1>
454515

455516
// Filter badge value keyboard
456517
document.addEventListener('keydown', (e) => {
457-
if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('filter-badge-value')) {
458-
e.preventDefault();
459-
const id = parseInt(e.target.dataset.id);
460-
activeFilter = filters.find(f => f.id === id);
461-
showValuePopover(e.target);
518+
if (e.target.classList.contains('filter-badge-value')) {
519+
// Open popover on Enter/Space
520+
if (e.key === 'Enter' || e.key === ' ') {
521+
e.preventDefault();
522+
const id = parseInt(e.target.dataset.id);
523+
activeFilter = filters.find(f => f.id === id);
524+
showValuePopover(e.target);
525+
}
526+
// Open popover on any typing (except navigation keys)
527+
else if (
528+
e.key.length === 1 || // Single character keys
529+
e.key === 'Backspace' ||
530+
e.key === 'Delete'
531+
) {
532+
e.preventDefault();
533+
const id = parseInt(e.target.dataset.id);
534+
activeFilter = filters.find(f => f.id === id);
535+
showValuePopover(e.target);
536+
537+
// After popover is open, forward the typed character to the input
538+
setTimeout(() => {
539+
const valueInput = document.getElementById('valueInput');
540+
if (valueInput) {
541+
if (e.key.length === 1) {
542+
// For printable characters, set the input value to that character
543+
valueInput.value = e.key;
544+
// Position cursor at the end
545+
valueInput.selectionStart = valueInput.selectionEnd = valueInput.value.length;
546+
} else if (e.key === 'Backspace' || e.key === 'Delete') {
547+
// For delete keys, start with empty value
548+
valueInput.value = '';
549+
}
550+
}
551+
}, 10);
552+
}
462553
}
463554
});
464555

@@ -513,6 +604,11 @@ <h1>Keyboard accessible filter prototype</h1>
513604

514605
// Show value popover
515606
function showValuePopover(targetElement) {
607+
// If popover is already active, don't reinitialize
608+
if (valuePopover.classList.contains('active')) {
609+
return;
610+
}
611+
516612
const rect = targetElement.getBoundingClientRect();
517613

518614
// Calculate available space on the right
@@ -548,9 +644,11 @@ <h1>Keyboard accessible filter prototype</h1>
548644

549645
// Return focus to the value element that opened it
550646
if (activeFilter) {
551-
const valueElement = document.querySelector(`.filter-badge-value[data-id="${activeFilter.id}"]`);
647+
const valueId = `filter-value-${activeFilter.id}`;
648+
const valueElement = document.getElementById(valueId);
552649
if (valueElement) {
553650
valueElement.focus();
651+
lastFocusedElementId = valueId;
554652
}
555653
}
556654
}
@@ -560,20 +658,53 @@ <h1>Keyboard accessible filter prototype</h1>
560658
const valueInput = document.getElementById('valueInput');
561659
if (activeFilter) {
562660
activeFilter.value = valueInput.value;
563-
const valueElement = document.querySelector(`.filter-badge-value[data-id="${activeFilter.id}"]`);
661+
662+
// Update the display text without full re-render
663+
const valueId = `filter-value-${activeFilter.id}`;
664+
const valueElement = document.getElementById(valueId);
564665
if (valueElement) {
565666
valueElement.textContent = activeFilter.value;
667+
closeValuePopover();
668+
valueElement.focus();
669+
lastFocusedElementId = valueId;
670+
} else {
671+
closeValuePopover();
566672
}
567-
closeValuePopover();
568673
}
569674
}
570675

571676
// Remove filter
572677
function removeFilter(id) {
573678
const index = filters.findIndex(f => f.id === id);
574679
if (index !== -1) {
680+
// Find next filter to focus on
681+
let nextFocusFilter;
682+
if (index < filters.length - 1) {
683+
nextFocusFilter = filters[index + 1];
684+
} else if (index > 0) {
685+
nextFocusFilter = filters[index - 1];
686+
}
687+
688+
// Remove the filter
575689
filters.splice(index, 1);
690+
691+
// Render and set focus
576692
renderFilterBadges();
693+
694+
// If there are no filters left, focus the add button
695+
if (filters.length === 0) {
696+
addFilterBadge.focus();
697+
lastFocusedElementId = addFilterBadge.id;
698+
}
699+
// Otherwise focus a related filter
700+
else if (nextFocusFilter) {
701+
const columnId = `filter-column-${nextFocusFilter.id}`;
702+
const columnElement = document.getElementById(columnId);
703+
if (columnElement) {
704+
columnElement.focus();
705+
lastFocusedElementId = columnId;
706+
}
707+
}
577708
}
578709
}
579710

0 commit comments

Comments
 (0)