Skip to content

Commit 3ad8017

Browse files
committed
Aria improvements.
1 parent 6d4a7c8 commit 3ad8017

11 files changed

Lines changed: 548 additions & 702 deletions

File tree

dist/render.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,5 @@ export default class Render {
8787
ensureElementInView(container: HTMLElement, element: HTMLElement): void;
8888
putContent(): 'up' | 'down';
8989
updateDeselectAll(): void;
90+
private setActiveDescendant;
9091
}

dist/slimselect.cjs.js

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ class Render {
387387
const highlighted = this.content.list.querySelectorAll('.' + this.classes.highlighted);
388388
highlighted.forEach(el => el.classList.remove(this.classes.highlighted));
389389
this.content.search.input.removeAttribute('aria-activedescendant');
390+
this.main.main.removeAttribute('aria-activedescendant');
390391
}
391392
constructor(settings, classes, store, callbacks) {
392393
var _a;
@@ -438,7 +439,7 @@ class Render {
438439
open() {
439440
this.main.arrow.path.setAttribute('d', this.classes.arrowOpen);
440441
this.main.main.classList.add(this.settings.openPosition === 'up' ? this.classes.openAbove : this.classes.openBelow);
441-
this.content.search.input.setAttribute('aria-expanded', 'true');
442+
this.main.main.setAttribute('aria-expanded', 'true');
442443
this.moveContent();
443444
const selectedOptions = this.store.getSelectedOptions();
444445
if (selectedOptions.length) {
@@ -459,7 +460,8 @@ class Render {
459460
}
460461
close() {
461462
this.main.main.classList.remove(this.classes.openAbove, this.classes.openBelow);
462-
this.content.search.input.setAttribute('aria-expanded', 'false');
463+
this.content.search.input.removeAttribute('aria-expanded');
464+
this.main.main.setAttribute('aria-expanded', 'false');
463465
this.content.main.classList.remove(this.classes.openAbove, this.classes.openBelow);
464466
this.main.arrow.path.setAttribute('d', this.classes.arrowClose);
465467
if (this.scrollHandler) {
@@ -503,18 +505,21 @@ class Render {
503505
this.content.list.setAttribute('role', 'listbox');
504506
this.content.list.setAttribute('id', this.content.main.id + '-list');
505507
this.content.list.setAttribute('aria-label', this.settings.contentAriaLabel);
508+
this.main.main.setAttribute('role', 'combobox');
509+
this.main.main.setAttribute('aria-controls', this.content.list.id);
510+
this.main.main.setAttribute('aria-haspopup', 'listbox');
511+
this.main.main.setAttribute('aria-expanded', 'false');
506512
if (this.settings.isMultiple) {
507513
this.content.list.setAttribute('aria-multiselectable', 'true');
508514
}
509515
this.main.main.removeAttribute('aria-labelledby');
510516
this.main.main.removeAttribute('aria-label');
511517
const input = this.content.search.input;
512-
input.setAttribute('role', 'combobox');
513-
input.setAttribute('aria-haspopup', 'listbox');
518+
input.setAttribute('role', 'searchbox');
514519
input.setAttribute('aria-controls', this.content.list.id);
515520
input.setAttribute('aria-owns', this.content.list.id);
516-
input.setAttribute('aria-expanded', 'false');
517521
input.setAttribute('aria-autocomplete', 'list');
522+
input.removeAttribute('aria-expanded');
518523
let labelledById = (this.settings.ariaLabelledBy || '').trim();
519524
let labelEl = null;
520525
if (!labelledById) {
@@ -566,28 +571,49 @@ class Render {
566571
main.onkeydown = (e) => {
567572
if (e.target !== main)
568573
return true;
574+
const focusAndThen = (fn) => {
575+
this.callbacks.open();
576+
requestAnimationFrame(() => {
577+
this.searchFocus();
578+
fn();
579+
});
580+
};
569581
switch (e.key) {
570-
case 'ArrowUp':
571582
case 'ArrowDown':
572-
this.callbacks.open();
573-
e.key === 'ArrowDown' ? this.highlight('down') : this.highlight('up');
583+
e.preventDefault();
584+
focusAndThen(() => this.highlight('down'));
585+
return false;
586+
case 'ArrowUp':
587+
e.preventDefault();
588+
focusAndThen(() => this.highlight('up'));
574589
return false;
575590
case 'Tab':
576591
this.callbacks.close();
577592
return true;
578593
case 'Enter':
579594
case ' ':
580-
this.callbacks.open();
581-
const highlighted = this.content.list.querySelector('.' + this.classes.highlighted);
582-
if (highlighted)
583-
highlighted.click();
584-
return false;
585-
case 'Escape':
586-
this.callbacks.close();
595+
e.preventDefault();
596+
focusAndThen(() => {
597+
const highlighted = this.content.list.querySelector('.' + this.classes.highlighted);
598+
if (highlighted)
599+
highlighted.click();
600+
});
587601
return false;
588602
}
589-
if (e.key.length === 1) {
603+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
604+
e.preventDefault();
590605
this.callbacks.open();
606+
requestAnimationFrame(() => {
607+
var _a, _b;
608+
this.searchFocus();
609+
const inp = this.content.search.input;
610+
const selStart = (_a = inp.selectionStart) !== null && _a !== void 0 ? _a : inp.value.length;
611+
const selEnd = (_b = inp.selectionEnd) !== null && _b !== void 0 ? _b : inp.value.length;
612+
inp.value = inp.value.slice(0, selStart) + e.key + inp.value.slice(selEnd);
613+
inp.setSelectionRange(selStart + 1, selStart + 1);
614+
this.callbacks.search(inp.value);
615+
});
616+
return false;
591617
}
592618
return true;
593619
};
@@ -702,6 +728,7 @@ class Render {
702728
const placeholder = document.createElement('div');
703729
placeholder.classList.add(this.classes.placeholder);
704730
placeholder.innerHTML = placeholderText;
731+
placeholder.setAttribute('aria-hidden', 'true');
705732
return placeholder;
706733
}
707734
renderValues() {
@@ -717,6 +744,7 @@ class Render {
717744
return o.selected && !o.placeholder;
718745
}, false);
719746
const selectedSingle = selected.length > 0 ? selected[0] : null;
747+
this.main.values.removeAttribute('role');
720748
if (!selectedSingle) {
721749
this.main.values.innerHTML = this.placeholder().outerHTML;
722750
}
@@ -744,10 +772,12 @@ class Render {
744772
return opt.selected && opt.display;
745773
}, false);
746774
if (selectedOptions.length === 0) {
775+
this.main.values.removeAttribute('role');
747776
this.main.values.innerHTML = this.placeholder().outerHTML;
748777
return;
749778
}
750779
else {
780+
this.main.values.setAttribute('role', 'list');
751781
const placeholder = this.main.values.querySelector('.' + this.classes.placeholder);
752782
if (placeholder) {
753783
placeholder.remove();
@@ -1125,6 +1155,8 @@ class Render {
11251155
if (options.length === 1) {
11261156
if (!options[0].classList.contains(this.classes.highlighted)) {
11271157
options[0].classList.add(this.classes.highlighted);
1158+
const id = options[0].id;
1159+
this.setActiveDescendant(id);
11281160
return;
11291161
}
11301162
}
@@ -1138,6 +1170,7 @@ class Render {
11381170
for (const o of options) {
11391171
if (o.classList.contains(this.classes.selected)) {
11401172
o.classList.add(this.classes.highlighted);
1173+
this.setActiveDescendant(o.id);
11411174
break;
11421175
}
11431176
}
@@ -1146,16 +1179,9 @@ class Render {
11461179
if (options[i].classList.contains(this.classes.highlighted)) {
11471180
const prevOption = options[i];
11481181
prevOption.classList.remove(this.classes.highlighted);
1149-
const prevParent = prevOption.parentElement;
1150-
if (prevParent && prevParent.classList.contains(this.classes.open)) {
1151-
const optgroupLabel = prevParent.querySelector('.' + this.classes.optgroupLabel);
1152-
if (optgroupLabel) {
1153-
optgroupLabel.click();
1154-
}
1155-
}
1156-
let selectOption = options[dir === 'down' ? (i + 1 < options.length ? i + 1 : 0) : i - 1 >= 0 ? i - 1 : options.length - 1];
1182+
const selectOption = options[dir === 'down' ? (i + 1 < options.length ? i + 1 : 0) : i - 1 >= 0 ? i - 1 : options.length - 1];
11571183
selectOption.classList.add(this.classes.highlighted);
1158-
this.content.search.input.setAttribute('aria-activedescendant', selectOption.id);
1184+
this.setActiveDescendant(selectOption.id);
11591185
this.ensureElementInView(this.content.list, selectOption);
11601186
const selectParent = selectOption.parentElement;
11611187
if (selectParent && selectParent.classList.contains(this.classes.close)) {
@@ -1169,7 +1195,7 @@ class Render {
11691195
}
11701196
const newly = options[dir === 'down' ? 0 : options.length - 1];
11711197
newly.classList.add(this.classes.highlighted);
1172-
this.content.search.input.setAttribute('aria-activedescendant', newly.id);
1198+
this.setActiveDescendant(newly.id);
11731199
this.ensureElementInView(this.content.list, newly);
11741200
}
11751201
listDiv() {
@@ -1410,7 +1436,7 @@ class Render {
14101436
if (option.selected) {
14111437
optionEl.classList.add(this.classes.selected);
14121438
optionEl.setAttribute('aria-selected', 'true');
1413-
this.content.search.input.setAttribute('aria-activedescendant', optionEl.id);
1439+
this.setActiveDescendant(optionEl.id);
14141440
}
14151441
else {
14161442
optionEl.classList.remove(this.classes.selected);
@@ -1441,8 +1467,8 @@ class Render {
14411467
if (!this.settings.closeOnSelect) {
14421468
if (e.shiftKey && this.lastSelectedOption) {
14431469
const options = this.store.getDataOptions();
1444-
let lastClickedOptionIndex = options.findIndex((o) => o.id === this.lastSelectedOption.id);
1445-
let currentOptionIndex = options.findIndex((o) => o.id === option.id);
1470+
const lastClickedOptionIndex = options.findIndex((o) => o.id === this.lastSelectedOption.id);
1471+
const currentOptionIndex = options.findIndex((o) => o.id === option.id);
14461472
if (lastClickedOptionIndex >= 0 && currentOptionIndex >= 0) {
14471473
const startIndex = Math.min(lastClickedOptionIndex, currentOptionIndex);
14481474
const endIndex = Math.max(lastClickedOptionIndex, currentOptionIndex);
@@ -1585,6 +1611,15 @@ class Render {
15851611
deselectButton.classList.add(hideClass);
15861612
}
15871613
}
1614+
setActiveDescendant(id) {
1615+
if (!id) {
1616+
this.main.main.removeAttribute('aria-activedescendant');
1617+
this.content.search.input.removeAttribute('aria-activedescendant');
1618+
return;
1619+
}
1620+
this.main.main.setAttribute('aria-activedescendant', id);
1621+
this.content.search.input.setAttribute('aria-activedescendant', id);
1622+
}
15881623
}
15891624
Render._livePolite = null;
15901625
Render._liveAssertive = null;

0 commit comments

Comments
 (0)