Skip to content

Commit

Permalink
Add aria for accessibility and kb navigation
Browse files Browse the repository at this point in the history
Base element accessibilty integrations

add accessibility options to components

Accessibilty changes internal

Accessibilty additions to components

last (?) changes for Improve accessibility getAlby#182

more aria additions
  • Loading branch information
chebizarro committed Nov 1, 2024
1 parent 863b0b3 commit f72addd
Show file tree
Hide file tree
Showing 20 changed files with 387 additions and 53 deletions.
52 changes: 52 additions & 0 deletions src/components/BitcoinConnectElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,56 @@ export class BitcoinConnectElement extends InternalElement {
this._modalOpen = currentState.modalOpen;
});
}

// Override updated method to set ARIA attributes based on state
protected override updated(
changedProperties: Map<string | number | symbol, unknown>
): void {
super.updated(changedProperties);

// Set aria-live for connection status changes
if (this._connecting) {
this.setAttribute('aria-busy', 'true');
this.setAttribute('aria-live', 'polite');
} else {
this.removeAttribute('aria-busy');
}


// Announce errors if they exist
if (this._error) {
this.setAttribute('aria-live', 'assertive');
this.setAttribute('aria-label', `Error: ${this._error}`);
} else {
this.removeAttribute('aria-live');
this.removeAttribute('aria-label');
}

// Modal accessibility: handle modal state
if (this._modalOpen) {
this.setAttribute('aria-modal', 'true');
this._trapFocusInModal();
} else {
this.removeAttribute('aria-modal');
this._restoreFocus();
}
}

// Trap focus when modal is open
protected _trapFocusInModal() {
const focusableElements = this.shadowRoot?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements?.length) {
(focusableElements[0] as HTMLElement).focus();
}
}

// Restore focus to the previously focused element when modal closes
protected _restoreFocus() {
const previouslyFocusedElement = document.activeElement as HTMLElement;
if (previouslyFocusedElement) {
previouslyFocusedElement.focus();
}
}
}
5 changes: 5 additions & 0 deletions src/components/bc-balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export class Balance extends withTwind()(BitcoinConnectElement) {
class="font-medium font-sans mr-2 flex justify-center items-center gap-0.5 ${classes[
'text-brand-mixed'
]}"
role="status"
aria-live="${this._loading ? 'polite' : 'off'}"
aria-busy="${this._loading ? 'true' : 'false'}"
>
<span class="font-mono">${this._balance || 'Loading...'} </span></span
>`;
Expand Down Expand Up @@ -109,6 +112,8 @@ export class Balance extends withTwind()(BitcoinConnectElement) {
this._balance = '⚠️';
// FIXME: better error handling
console.error(error);
} finally {
this._loading = false;
}
})();
}
Expand Down
14 changes: 14 additions & 0 deletions src/components/bc-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ export class Button extends withTwind()(BitcoinConnectElement) {
<div
class="relative inline-flex ${classes.interactive} cursor-pointer
rounded-lg gap-2 justify-center items-center"
role="button"
tabindex="0"
aria-pressed="${this._connected ? 'true' : 'false'}"
aria-busy="${isLoading ? 'true' : 'false'}"
aria-label="${isLoading ? 'Connecting...' : this.title}"
@click=${this._onClick}
@keydown=${this._handleKeydown}
>
<div
class="absolute top-0 left-0 w-full h-full rounded-lg pointer-events-none ${this
Expand Down Expand Up @@ -75,6 +81,14 @@ export class Button extends withTwind()(BitcoinConnectElement) {
private _onClick() {
launchModal();
}

// Handle keyboard events for accessibility (Enter/Space key triggers click)
public override _handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this._onClick(); // Simulate click on Enter or Space key
}
}
}

declare global {
Expand Down
39 changes: 38 additions & 1 deletion src/components/bc-currency-switcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,14 @@ export class CurrencySwitcher extends withTwind()(BitcoinConnectElement) {

override render() {
if (!this._isSwitchingCurrency) {
return html`<div class="flex justify-center items-center gap-2">
return html`<div class="flex justify-center items-center gap-2">
<div
class="${classes.interactive}"
role="button"
tabindex="0"
aria-label="Open currency selection"
@click=${this._showSelectVisibility}
@keydown=${this._handleKeydown}
>
<slot></slot>
</div>
Expand All @@ -81,7 +85,12 @@ export class CurrencySwitcher extends withTwind()(BitcoinConnectElement) {
class="${selectedCurrency === currency.value
? 'bg-blue-500 text-white'
: ''} flex items-center justify-center py-2 px-4 hover:text-white hover:bg-blue-500 rounded-lg hover:border-blue-500 cursor-pointer"
role="option"
aria-selected="${selectedCurrency === currency.value}"
tabindex="0"
@click=${() => this._selectCurrency(currency.value)}
@keydown=${(event: KeyboardEvent) =>
this._handleCurrencyKeydown(event, currency.value)}
>
<span class="text-orange-400 inline-block mr-2 text-xl"
>${currency.flag}</span
Expand All @@ -94,11 +103,39 @@ export class CurrencySwitcher extends withTwind()(BitcoinConnectElement) {

private _showSelectVisibility() {
this._isSwitchingCurrency = true;
// Focus the first currency option when switching is enabled
setTimeout(() => {
const firstCurrencyOption = this.shadowRoot?.querySelector(
'[role="option"]'
) as HTMLElement;
firstCurrencyOption?.focus();
}, 0);
}

private _selectCurrency(selectedCurrency: string) {
store.getState().setCurrency(selectedCurrency);
this._isSwitchingCurrency = false;
// Ensure focus returns to the triggering button
const triggerButton = this.shadowRoot?.querySelector(
'[role="button"]'
) as HTMLElement;
triggerButton?.focus();
}

// Handle keydown events for currency selection
private _handleCurrencyKeydown(event: KeyboardEvent, currency: string) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this._selectCurrency(currency);
}
}

// Handle keydown events for opening the currency list
public override _handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this._showSelectVisibility();
}
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/components/bc-modal-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,32 @@ export class ModalHeader extends withTwind()(BitcoinConnectElement) {
override render() {
return html`<div
class="flex justify-center items-center gap-2 w-full relative"
role="heading"
aria-level="1"
>
<div
class="absolute right-0 h-full flex items-center justify-center gap-2"
>
${this.showHelp
? html`<div
class="${classes.interactive} ${classes['text-neutral-tertiary']}"
role="button"
tabindex="0"
aria-label="Help"
@click=${() => store.getState().pushRoute('/help')}
@keydown=${this._handleKeydownHelp}
>
${helpIcon}
</div>`
: null}
${this.closable
? html`<div
class="${classes.interactive} ${classes['text-neutral-tertiary']}"
role="button"
tabindex="0"
aria-label="Close"
@click=${this._handleClose}
@keydown=${this._handleKeydownClose}
>
${crossIcon}
</div>`
Expand All @@ -54,6 +64,22 @@ export class ModalHeader extends withTwind()(BitcoinConnectElement) {
private _handleClose() {
this.dispatchEvent(new Event('onclose', {bubbles: true, composed: true}));
}

// Handle keyboard interactions for the close button
private _handleKeydownClose(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this._handleClose();
}
}

// Handle keyboard interactions for the help button
private _handleKeydownHelp(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
store.getState().pushRoute('/help');
}
}
}

declare global {
Expand Down
29 changes: 28 additions & 1 deletion src/components/bc-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,40 @@ import {closeModal} from '../api';

@customElement('bc-modal')
export class Modal extends withTwind()(BitcoinConnectElement) {
private _focusTrapElement: HTMLSlotElement | null | undefined;

override firstUpdated() {
this._focusTrapElement = this.shadowRoot?.querySelector('slot');
this._trapFocusInModal();
window.addEventListener('keydown', this._handleKeydown);
}

override disconnectedCallback() {
window.removeEventListener('keydown', this._handleKeydown);
this._restoreFocus();
}

override render() {
return html` <div
return html`<div
class="fixed top-0 left-0 w-full h-full flex justify-center items-end sm:items-center z-[21000]"
role="dialog"
aria-modal="true"
aria-labelledby="modal-header"
aria-describedby="modal-content"
>
<div
class="absolute top-0 left-0 w-full h-full -z-10 ${classes[
'bg-foreground'
]} animate-darken"
@click=${this._handleClose}
role="button"
tabindex="0"
aria-label="Close modal"
></div>
<div
class="transition-all p-4 pt-6 pb-8 rounded-2xl shadow-2xl flex justify-center items-center w-full bg-white dark:bg-black max-w-md max-sm:rounded-b-none
animate-fade-in"
id="modal-content"
>
<slot @onclose=${this._handleClose}></slot>
</div>
Expand All @@ -32,6 +53,12 @@ export class Modal extends withTwind()(BitcoinConnectElement) {
private _handleClose = () => {
closeModal();
};

public override _handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
this._handleClose();
}
};
}

declare global {
Expand Down
26 changes: 23 additions & 3 deletions src/components/bc-navbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,47 @@ export class Navbar extends withTwind()(BitcoinConnectElement) {
heading?: string;

override render() {
return html`<div
return html`<nav
class="flex justify-center items-center gap-2 w-full relative pb-4"
role="navigation"
aria-label="${this.heading ? this.heading + ' navigation' : 'navigation'}"
>
<!-- Back button -->
<div class="absolute left-8 h-full flex items-center justify-center">
<div
class="${classes.interactive} ${classes['text-neutral-tertiary']}"
role="button"
tabindex="0"
aria-label="Go back"
@click=${this._goBack}
@keydown=${this._handleKeydown}
>
${backIcon}
</div>
</div>
<div class="font-sans font-medium ${classes['text-neutral-secondary']}">
<!-- Heading -->
<div
class="font-sans font-medium ${classes['text-neutral-secondary']}"
role="heading"
aria-level="1"
>
${this.heading}
</div>
</div>`;
</nav>`;
}

private _goBack = () => {
store.getState().popRoute();
store.getState().setError(undefined);
};

public override _handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this._goBack();
}
}
}

declare global {
Expand Down
15 changes: 14 additions & 1 deletion src/components/bc-pay-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,19 @@ export class PayButton extends withTwind()(BitcoinConnectElement) {
override render() {
const isLoading = this._waitingForInvoice || this._modalOpen;

return html` <div class="inline-flex" @click=${this._onClick}>
return html`<div
class="inline-flex"
role="button"
tabindex="0"
aria-live="polite"
aria-label="${isLoading
? 'Loading payment'
: this._paid
? 'Payment complete'
: this.title}"
@click=${this._onClick}
@keydown=${this._handleKeydown}
>
<bci-button variant="primary">
${isLoading
? html`${waitingIcon(`w-11 h-11 -mr-2 -ml-2.5 `)}`
Expand Down Expand Up @@ -104,6 +116,7 @@ export class PayButton extends withTwind()(BitcoinConnectElement) {
});
this._setPaid = setPaid;
}

}

declare global {
Expand Down
4 changes: 3 additions & 1 deletion src/components/bc-router-outlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {routes} from './routes';
export class RouterOutlet extends withTwind()(BitcoinConnectElement) {
override render() {
//TODO: r = routes[this._route](this._routeParams);
return html`<div class="flex flex-col w-full">${routes[this._route]}</div>`;
return html`<div class="flex flex-col w-full" aria-live="polite">
${routes[this._route]}
</div>`;
}
}

Expand Down
Loading

0 comments on commit f72addd

Please sign in to comment.