Skip to content

Commit

Permalink
[SelectField] Handle disabled options with keyboard navigation and in…
Browse files Browse the repository at this point in the history
…itial highlighted option
  • Loading branch information
techniq committed Jul 16, 2024
1 parent 7fa6534 commit fee78c6
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/early-swans-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte-ux': patch
---

[SelectField] Handle disabled options with keyboard navigation and initial highlighted option
52 changes: 34 additions & 18 deletions packages/svelte-ux/src/lib/components/SelectField.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,33 @@
let menuOptionsEl: HTMLDivElement;
let selectFieldEl: HTMLButtonElement;
function nextOptionIndex(currentIndex: number) {
// Find next non-disabled option
let nextIndex = filteredOptions.findIndex((o, i) => i > currentIndex && !o.disabled);
if (nextIndex === -1) {
// Find first non-disabled (wrap to top)
nextIndex = filteredOptions.findIndex((o, i) => !o.disabled);
}
return nextIndex;
}
function prevOptionIndex(currentIndex: number) {
// Find prev non-disabled option
let prevIndex = filteredOptions.findLastIndex((o, i) => i < currentIndex && !o.disabled);
if (prevIndex === -1) {
// Find first non-disabled (wrap to top)
prevIndex = filteredOptions.findLastIndex((o, i) => !o.disabled);
}
return prevIndex;
}
// UI state
export let open = false;
let highlightIndex = 0;
let highlightIndex = -1;
$: if (open === false) {
// Restore text if cleared but selection remains
Expand All @@ -180,6 +204,10 @@
// Capture current highlighted item (attempt to restore after searching)
const prevHighlightedOption = filteredOptions[highlightIndex];
if (highlightIndex === -1 && menuOptionsEl) {
highlightIndex = nextOptionIndex(highlightIndex);
}
// Do not search if menu is not open / closing on selection
search(searchText).then(() => {
// TODO: Find a way for scrollIntoView to still highlight after the menu height transition finishes
Expand All @@ -188,7 +216,7 @@
// Highlight selected if none currently
highlightIndex = selectedIndex === -1 ? 0 : selectedIndex;
} else {
// Attempt to re-highlight previously highlighted item after search
// Attempt to re-highlight previously highlighted option after search
const prevHighlightedOptionIndex = filteredOptions.findIndex(
(o) => o === prevHighlightedOption
);
Expand All @@ -197,8 +225,8 @@
// Maintain previously highlight index after filter update (option still available)
highlightIndex = prevHighlightedOptionIndex;
} else {
// Highlight first item
highlightIndex = 0;
// Highlight first option
highlightIndex = nextOptionIndex(-1);
}
}
});
Expand Down Expand Up @@ -239,8 +267,6 @@
function onKeyDown(e: KeyboardEvent) {
logger.debug('onKeyDown', { key: e.key });
// e.preventDefault();
switch (e.key) {
case 'Tab':
if (e.shiftKey) {
Expand All @@ -250,22 +276,12 @@
case 'ArrowDown':
show();
if (highlightIndex < filteredOptions.length - 1) {
highlightIndex++;
} else {
// wrap to top
highlightIndex = 0;
}
highlightIndex = nextOptionIndex(highlightIndex);
break;
case 'ArrowUp':
show();
if (highlightIndex > 0) {
highlightIndex--;
} else {
// wrap to bottom
highlightIndex = filteredOptions.length - 1;
}
highlightIndex = prevOptionIndex(highlightIndex);
break;
case 'Escape':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
];
let optionsWithDisabled: MenuOption[] = [
{ label: 'One', value: 1 },
{ label: 'One', value: 1, disabled: true },
{ label: 'Two', value: 2 },
{ label: 'Three', value: 3, disabled: true },
{ label: 'Four', value: 4 },
{ label: 'Three', value: 3 },
{ label: 'Four', value: 4, disabled: true },
];
const optionsWithGroup: MenuOption[] = [
Expand Down Expand Up @@ -229,7 +229,30 @@
option.group ? 'px-4' : 'px-2'
)}
scrollIntoView={index === highlightIndex}
disabled={index === 2}
disabled={option.disabled}
>
<div>
<div>{option.label}</div>
<div class="text-sm text-text-surface-content/50">{option.value}</div>
</div>
</MenuItem>
</svelte:fragment>
</SelectField>
</Preview>

<h2>option slot with disabled</h2>

<Preview>
<SelectField options={optionsWithDisabled}>
<svelte:fragment slot="option" let:option let:index let:selected let:highlightIndex>
<MenuItem
class={cls(
index === highlightIndex && 'bg-surface-content/5',
option === selected && 'font-semibold',
option.group ? 'px-4' : 'px-2'
)}
scrollIntoView={index === highlightIndex}
disabled={option.disabled}
>
<div>
<div>{option.label}</div>
Expand Down

0 comments on commit fee78c6

Please sign in to comment.