Skip to content

Commit 7a40fdd

Browse files
[RMS 8] Keyboard Navigable (#1)
This PR addresses some issues with the keyboard focus and active state of option. It also shows the no results message when searching in addition to when everything is selected
1 parent 273743f commit 7a40fdd

File tree

3 files changed

+56
-32
lines changed

3 files changed

+56
-32
lines changed

index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
<option value="8">Record 8</option>
2222
<option value="9">Record 9</option>
2323
<option value="10">Record 10</option>
24+
<option value="11">Record 11</option>
25+
<option value="12">Record 12</option>
26+
<option value="13">Record 13</option>
27+
<option value="14">Record 14</option>
28+
<option value="15">Record 15</option>
2429
</tailored-select>
2530
</form>
2631
<script type="module" src="/src/assets/javascript/application.js"></script>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@rolemodel/tailored-select",
33
"description": "Tailored Select is a Web Component built to be a searchable select box. Inspired by tom-select.js to provide a framework agnostic autocomplete widget with native-feeling keyboard navigation. Useful for tagging, contact lists, etc.",
4-
"version": "0.0.2",
4+
"version": "0.0.3",
55
"author": "RoleModel Software",
66
"license": "MIT",
77
"type": "module",

src/assets/javascript/components/tailored-select.component.js

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ export default class TailoredSelect extends LitElement {
2727

2828
connectedCallback() {
2929
super.connectedCallback()
30-
31-
// Ensure focusable
32-
// this.tabIndex = 0
3330
}
3431

3532
firstUpdated(changedProperties) {
@@ -38,19 +35,19 @@ export default class TailoredSelect extends LitElement {
3835
// On load, everything starts in available. We need to move selected over.
3936
this.availableOptions.forEach((option) => {
4037
option.addEventListener('click', () => this.toggleOption(option))
41-
option.addEventListener('mouseover', () => this.handleOptionFocus(option))
38+
option.addEventListener('mouseover', () => this.handleOptionActive(option))
4239
this.assignOptionSlot(option)
4340
})
4441

45-
this.resetOptionFocus()
42+
this.resetActiveOption()
4643
this.updateFormValue()
4744
}
4845

4946
// Input Behavior
5047

5148
handleInputBlur() {
5249
this.hasFocus = false
53-
this.resetOptionFocus()
50+
this.resetActiveOption()
5451
// this.emit('ts-blur')
5552
}
5653

@@ -66,16 +63,21 @@ export default class TailoredSelect extends LitElement {
6663
handleInputKeyDown(event) {
6764
switch (event.key) {
6865
case 'ArrowDown':
69-
this.focusNextOption()
66+
this.activateNextOption()
7067
break
7168
case 'ArrowUp':
72-
this.focusPreviousOption()
69+
this.activatePreviousOption()
7370
break
7471
case 'Enter':
75-
this.toggleOption(this.focusedOption)
72+
if (this.availableOptions.length > 0) {
73+
this.toggleOption(this.activeOption)
74+
}
7675
break
7776
case 'Backspace':
7877
this.deleteSelection()
78+
if (!this.activeOption) {
79+
this.resetActiveOption()
80+
}
7981
break
8082
}
8183
}
@@ -85,13 +87,17 @@ export default class TailoredSelect extends LitElement {
8587
if (!value) {
8688
// Make all options visible
8789
this.availableOptions.forEach((opt) => (opt.hidden = false))
90+
this.resetActiveOption()
8891
return
8992
}
9093

9194
const matcher = new RegExp(value, 'i')
9295
this.availableOptions.forEach((opt) => {
9396
opt.hidden = !Boolean(opt.value.match(matcher))
9497
})
98+
99+
this.resetActiveOption()
100+
this.updateNoResultsMessage()
95101
}
96102

97103
deleteSelection() {
@@ -121,7 +127,7 @@ export default class TailoredSelect extends LitElement {
121127

122128
// Option Behavior
123129

124-
handleOptionFocus(option) {
130+
handleOptionActive(option) {
125131
if (option.selected) {
126132
return
127133
}
@@ -150,42 +156,52 @@ export default class TailoredSelect extends LitElement {
150156
return this.shadowRoot.querySelector('div[role="listbox"]')
151157
}
152158

153-
focusNextOption(option = this.focusedOption) {
159+
activateNextOption(option = this.activeOption) {
154160
const index = this.availableOptions.indexOf(option)
155-
if (index == this.availableOptions.length - 1) return
161+
if (index == this.availableOptions.length - 1) return false
156162

157163
const nextOption = this.availableOptions[index + 1]
158164
this.setActiveOption(nextOption)
165+
return true
159166
}
160167

161-
focusPreviousOption() {
162-
const index = this.availableOptions.indexOf(this.focusedOption)
163-
if (index == 0) return
168+
activatePreviousOption() {
169+
const index = this.availableOptions.indexOf(this.activeOption)
170+
if (index == 0) return false
164171

165172
const nextOption = this.availableOptions[index - 1]
166173
this.setActiveOption(nextOption)
174+
return true
175+
}
176+
177+
ensureActiveOption(option) {
178+
if (this.activateNextOption(option)) return
179+
if (this.activatePreviousOption(option)) return
180+
181+
this.clearActiveOption()
167182
}
168183

169184
setActiveOption(option) {
170-
this.clearOptionFocus()
185+
this.clearActiveOption()
171186
this.setHeight(option)
172-
option.classList.add('focused')
187+
option.classList.add('active')
173188
}
174189

175-
resetOptionFocus() {
176-
this.clearOptionFocus()
190+
resetActiveOption() {
191+
this.clearActiveOption()
177192

178-
const firstOption = this.availableOptions[0]
179-
if (firstOption) {
180-
this.setActiveOption(firstOption)
193+
const firstSelectableOption = this.availableOptions.filter((option) => !option.hidden)[0]
194+
if (firstSelectableOption) {
195+
this.setActiveOption(firstSelectableOption)
181196
}
182197
}
183198

184-
clearOptionFocus() {
185-
this.removeFocus(this.focusedOption)
199+
clearActiveOption() {
200+
this.removeActiveOption(this.activeOption)
186201
}
187-
removeFocus(option) {
188-
option?.classList.remove('focused')
202+
203+
removeActiveOption(option) {
204+
option?.classList.remove('active')
189205
}
190206

191207
setHeight(option) {
@@ -201,8 +217,8 @@ export default class TailoredSelect extends LitElement {
201217
}
202218
}
203219

204-
get focusedOption() {
205-
return this.availableOptions.find((opt) => opt.classList.contains('focused'))
220+
get activeOption() {
221+
return this.availableOptions.find((opt) => opt.classList.contains('active'))
206222
}
207223

208224
// handleChange(event) {
@@ -236,7 +252,10 @@ export default class TailoredSelect extends LitElement {
236252
}
237253

238254
toggleOption(option) {
239-
if (!option.selected) this.focusNextOption(option)
255+
if (!option.selected) {
256+
// Only performed when toggling on
257+
this.ensureActiveOption(option)
258+
}
240259

241260
option.selected = !option.selected
242261
this.assignOptionSlot(option)
@@ -260,7 +279,7 @@ export default class TailoredSelect extends LitElement {
260279
}
261280

262281
updateNoResultsMessage() {
263-
const noResults = this.availableOptions.every((opt) => opt.hidden)
282+
const noResults = this.availableOptions.every((option) => option.hidden)
264283
this.noResultsMessage.classList.toggle('active', noResults)
265284
}
266285

@@ -474,7 +493,7 @@ export default class TailoredSelect extends LitElement {
474493
cursor: pointer;
475494
}
476495
477-
::slotted(option.focused) {
496+
::slotted(option.active) {
478497
background-color: var(--option-background-color-hover);
479498
color: var(--option-text-color-hover);
480499
}

0 commit comments

Comments
 (0)