Skip to content

Commit

Permalink
Fix dropdown & toaster accessibility
Browse files Browse the repository at this point in the history
  • Loading branch information
louisescher committed Dec 15, 2024
1 parent 2341e34 commit d2ff6ee
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 27 deletions.
10 changes: 8 additions & 2 deletions packages/studiocms_ui/src/components/Dropdown/Dropdown.astro
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ const {
<div class="sui-dropdown-toggle" id={`${id}-toggle-btn`}>
<slot />
</div>
<ul class="sui-dropdown" class:list={[align]} role="listbox" id={`${id}-dropdown`} transition:persist transition:persist-props>
<ul
class="sui-dropdown"
class:list={[align]}
role="listbox" id={`${id}-dropdown`}
transition:persist
transition:persist-props
>
{options.map(({ value, disabled, color, label, icon, href }) => (
<li
class="sui-dropdown-option"
Expand Down Expand Up @@ -240,7 +246,7 @@ const {
user-select: none;
}

.sui-dropdown-option:hover {
.sui-dropdown-option:hover, .sui-dropdown-option:focus, .sui-dropdown-option.focused {
background-color: hsl(var(--background-step-3));
}

Expand Down
99 changes: 79 additions & 20 deletions packages/studiocms_ui/src/components/Dropdown/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class DropdownHelper {
private alignment: 'start' | 'center' | 'end';
private triggerOn: 'left' | 'right' | 'both';
private fullWidth = false;
private focusIndex = -1;

active = false;

Expand All @@ -27,30 +28,11 @@ class DropdownHelper {
this.toggleEl = document.getElementById(`${id}-toggle-btn`) as HTMLDivElement;
this.dropdown = document.getElementById(`${id}-dropdown`) as HTMLUListElement;

if (this.triggerOn === 'left') {
this.toggleEl.addEventListener('click', this.toggle);
} else if (this.triggerOn === 'both') {
this.toggleEl.addEventListener('click', this.toggle);
this.toggleEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.toggle();
});
} else {
this.toggleEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.toggle();
});
}

if (fullWidth) this.fullWidth = true;

window.addEventListener('scroll', this.hide);
document.addEventListener('astro:before-preparation', () => {
this.dropdown.classList.remove('initialized');
});

this.hideOnClickOutside(this.container);

this.initialBehaviorRegistration();
this.initialOptClickRegistration();
}

Expand All @@ -71,6 +53,78 @@ class DropdownHelper {
});
}
};

/**
* Sets up all listeners for the dropdown.
*/
private initialBehaviorRegistration = () => {
window.addEventListener('scroll', this.hide);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.hide();
});
document.addEventListener('astro:before-preparation', () => {
this.dropdown.classList.remove('initialized');
});

if (this.triggerOn === 'left') {
this.toggleEl.addEventListener('click', this.toggle);
} else if (this.triggerOn === 'both') {
this.toggleEl.addEventListener('click', this.toggle);
this.toggleEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.toggle();
});
} else {
this.toggleEl.addEventListener('contextmenu', (e) => {
e.preventDefault();
this.toggle();
});
}

this.toggleEl.addEventListener('keydown', (e) => {
if (!this.active) return;

if (e.key === 'Enter') {
e.preventDefault();

const focused = this.dropdown.querySelector('li.focused') as HTMLLIElement;

if (!focused) {
this.hide();
return;
};

focused.click();
}

if (e.key === 'ArrowDown') {
e.preventDefault();

this.focusIndex = this.focusIndex === this.dropdown.children.length - 1 ? 0 : this.focusIndex + 1;
}

if (e.key === 'ArrowUp') {
e.preventDefault();

this.focusIndex = this.focusIndex === 0 ? this.dropdown.children.length - 1 : this.focusIndex - 1;
}

if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
if (this.focusIndex > this.dropdown.children.length - 1) {
this.focusIndex = 0;
}

this.dropdown.querySelector('li.focused')?.classList.remove('focused');

const newFocus = this.dropdown.children[this.focusIndex] as HTMLLIElement;

if (!newFocus) return;

newFocus.classList.add('focused');
newFocus.focus();
}
});
}

/**
* Registers callbacks to hide the dropdown when an option is clicked.
Expand Down Expand Up @@ -101,6 +155,9 @@ class DropdownHelper {
public hide = () => {
this.dropdown.classList.remove('active');
this.active = false;
this.focusIndex = -1;

this.dropdown.querySelector('li.focused')?.classList.remove('focused');

setTimeout(() => this.dropdown.classList.remove('above', 'below'), 200);
};
Expand Down Expand Up @@ -167,8 +224,10 @@ class DropdownHelper {
CustomRect.right <= (window.innerWidth || document.documentElement.clientWidth)
) {
this.dropdown.classList.add('active', 'below');
this.focusIndex = -1;
} else {
this.dropdown.classList.add('active', 'above');
this.focusIndex = this.dropdown.children.length;
}
};

Expand Down
5 changes: 0 additions & 5 deletions packages/studiocms_ui/src/components/Modal/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,6 @@ class ModalHelper {
if (triggerID) {
this.bindTrigger(triggerID);
}

// For debugging purposes: When any element on the page is focused, log the element.
document.addEventListener('focus', (e) => {
console.log(e.target);
}, true);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/studiocms_ui/src/components/Toast/Toaster.astro
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ const {

const toastContainer = document.createElement('div');
const toastID = generateID('toast');
toastContainer.tabIndex = 0;
toastContainer.ariaLive = 'polite';
toastContainer.role = 'alert';
toastContainer.id = toastID;
toastContainer.classList.add('sui-toast-container', props.type, `${props.closeButton || props.persistent && "closeable"}`, `${props.persistent && 'persistent'}`);

Expand Down

0 comments on commit d2ff6ee

Please sign in to comment.