Skip to content

Commit

Permalink
fix: elements usage of a11y controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
bennypowers committed Jul 18, 2024
1 parent a3a670e commit 104d648
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 98 deletions.
2 changes: 1 addition & 1 deletion elements/pf-accordion/pf-accordion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class PfAccordion extends LitElement {

#mo = new MutationObserver(() => this.#init());

#headerIndex = new RovingTabindexController<PfAccordionHeader>(this, {
#headerIndex = RovingTabindexController.of(this, {
getItems: () => this.headers,
});

Expand Down
2 changes: 1 addition & 1 deletion elements/pf-chip/pf-chip-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class PfChipGroup extends LitElement {

#buttons: HTMLElement[] = [];

#tabindex = new RovingTabindexController(this, {
#tabindex = RovingTabindexController.of(this, {
getItems: () => this.#buttons.filter(x => !x.hidden),
});

Expand Down
2 changes: 1 addition & 1 deletion elements/pf-dropdown/pf-dropdown-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class PfDropdownMenu extends LitElement {

#internals = InternalsController.of(this, { role: 'menu' });

#tabindex = new RovingTabindexController(this, {
#tabindex = RovingTabindexController.of(this, {
getItems: () => this.items.map(x => x.menuItem),
});

Expand Down
22 changes: 9 additions & 13 deletions elements/pf-jump-links/pf-jump-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ export class PfJumpLinks extends LitElement {

#kids = this.querySelectorAll?.<LitElement>(':is(pf-jump-links-item, pf-jump-links-list)');

#tabindex?: RovingTabindexController<HTMLAnchorElement>;
#tabindex = RovingTabindexController.of<HTMLAnchorElement>(this, {
getItems: () => Array.from(this.#kids)
.flatMap(i => [
...i.shadowRoot?.querySelectorAll?.('a') ?? [],
...i.querySelectorAll?.('a') ?? [],
]),
});

#spy = new ScrollSpyController(this, {
rootMargin: `${this.offset}px 0px 0px 0px`,
Expand All @@ -97,16 +103,6 @@ export class PfJumpLinks extends LitElement {
}

override firstUpdated(): void {
this.#tabindex = new RovingTabindexController<HTMLAnchorElement>(this, {
getItems: () => {
const items = Array.from(this.#kids)
.flatMap(i => [
...i.shadowRoot?.querySelectorAll?.('a') ?? [],
...i.querySelectorAll?.('a') ?? [],
]);
return items;
},
});
const active = this.querySelector?.<PfJumpLinksItem>('pf-jump-links-item[active]');
if (active) {
this.#setActiveItem(active);
Expand Down Expand Up @@ -140,7 +136,7 @@ export class PfJumpLinks extends LitElement {
}

#updateItems() {
this.#tabindex?.updateItems();
this.#tabindex.updateItems();
}

#onSelect(event: Event) {
Expand All @@ -150,7 +146,7 @@ export class PfJumpLinks extends LitElement {
}

#setActiveItem(item: PfJumpLinksItem) {
this.#tabindex?.setActiveItem(item.shadowRoot?.querySelector?.('a') ?? undefined);
this.#tabindex.setActiveItem(item.shadowRoot?.querySelector?.('a') ?? undefined);
this.#spy.setActive(item);
}

Expand Down
165 changes: 85 additions & 80 deletions elements/pf-select/pf-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { PfChipGroup } from '../pf-chip/pf-chip-group.js';
import type { Placement } from '@patternfly/pfe-core/controllers/floating-dom-controller.js';
import type { PropertyValues, TemplateResult } from 'lit';

import { LitElement, html, isServer } from 'lit';
import { LitElement, html, isServer, nothing } from 'lit';
import { customElement } from 'lit/decorators/custom-element.js';
import { property } from 'lit/decorators/property.js';
import { query } from 'lit/decorators/query.js';
Expand All @@ -11,10 +11,7 @@ import { styleMap } from 'lit/directives/style-map.js';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';

import {
type ListboxAccessibilityController,
ListboxController,
} from '@patternfly/pfe-core/controllers/listbox-controller.js';
import { ListboxController } from '@patternfly/pfe-core/controllers/listbox-controller.js';
import { ActivedescendantController } from '@patternfly/pfe-core/controllers/activedescendant-controller.js';
import { RovingTabindexController } from '@patternfly/pfe-core/controllers/roving-tabindex-controller.js';
import { SlotController } from '@patternfly/pfe-core/controllers/slot-controller.js';
Expand Down Expand Up @@ -53,23 +50,13 @@ export class PfSelectChangeEvent extends Event {
export class PfSelect extends LitElement {
static readonly styles: CSSStyleSheet[] = [styles];

static readonly formAssociated = true;

static override readonly shadowRootOptions: ShadowRootInit = {
...LitElement.shadowRootOptions,
delegatesFocus: true,
};

static readonly formAssociated = true;

#internals = InternalsController.of(this);

#float = new FloatingDOMController(this, {
content: () => this.shadowRoot?.getElementById('listbox-container') ?? null,
});

#slots = new SlotController(this, null, 'placeholder');

#listbox?: ListboxController<PfOption>;

/** Variant of rendered Select */
@property() variant: 'single' | 'checkbox' | 'typeahead' | 'typeaheadmulti' = 'single';

Expand Down Expand Up @@ -128,18 +115,65 @@ export class PfSelect extends LitElement {

@property({ attribute: false }) filter?: (option: PfOption) => boolean;

@query('pf-chip-group') private _chipGroup?: PfChipGroup;

@query('#toggle-input') private _input?: HTMLInputElement;

@query('#toggle-button') private _toggle?: HTMLButtonElement;

@query('#listbox') private _listbox?: HTMLElement;

@query('#listbox-container') private _listboxContainer?: HTMLElement;

@query('#placeholder') private _placeholder?: PfOption;

#getListboxContainer = () => this._listbox ?? null;

#getComboboxInput = () => this._input ?? null;

#isOptionSelected = (option: PfOption) => option.selected;

#isNotPlaceholderOption = (option: PfOption) => option !== this._placeholder;

// TODO: differentiate between selection and focus in a11yControllers
#requestSelect = (option: PfOption, selected: boolean) => {
option.selected = !option.disabled && !!selected;
if (selected) {
this.selected = option;
}
return selected;
};

#a11yController = this.#getA11yController();

#internals = InternalsController.of(this);

#float = new FloatingDOMController(this, {
content: () => this._listboxContainer,
});

#slots = new SlotController(this, null, 'placeholder');

#listbox = ListboxController.of<PfOption>(this, {
multi: this.variant === 'typeaheadmulti' || this.variant === 'checkbox',
getA11yController: () => this.#a11yController,
getItemsContainer: this.#getListboxContainer,
isSelected: this.#isOptionSelected,
requestSelect: this.#requestSelect,
});

/**
* Single select option value for single select menus,
* or array of select option values for multi select.
*/
set selected(optionsList: PfOption | PfOption[]) {
this.#lastSelected = this.selected;
this.#listbox?.setValue(optionsList);
this.#listbox.setValue(optionsList);
this.requestUpdate('selected', this.#lastSelected);
}

get selected(): PfOption | PfOption[] | undefined {
return this.#listbox?.value;
get selected(): PfOption | PfOption[] {
return this.#listbox.value;
}

/**
Expand All @@ -150,21 +184,14 @@ export class PfSelect extends LitElement {
return []; // TODO: expose a DOM property to allow setting options in SSR scenarios
} else {
const opts = Array.from(this.querySelectorAll('pf-option'));
const placeholder = this.shadowRoot?.getElementById('placeholder') as PfOption | null;
if (placeholder) {
return [placeholder, ...opts];
if (this._placeholder) {
return [this._placeholder, ...opts];
} else {
return opts;
}
}
}

@query('pf-chip-group') private _chipGroup?: PfChipGroup;

@query('#toggle-input') private _input?: HTMLInputElement;

@query('#toggle-button') private _toggle?: HTMLButtonElement;

#lastSelected = this.selected;

/**
Expand All @@ -178,13 +205,13 @@ export class PfSelect extends LitElement {
get #buttonLabel() {
switch (this.variant) {
case 'typeaheadmulti':
return `${this.#listbox?.selectedOptions?.length ?? 0} ${this.itemsSelectedText}`;
return `${this.#listbox.selectedOptions?.length ?? 0} ${this.itemsSelectedText}`;
case 'checkbox':
return this.#listbox
?.selectedOptions
?.map?.(option => option.optionText || '')
?.join(' ')
?.trim()
.selectedOptions
.map(option => option.optionText || '')
.join(' ')
.trim()
|| this.#computePlaceholderText()
|| 'Options';
default:
Expand All @@ -208,7 +235,7 @@ export class PfSelect extends LitElement {
this.#internals.setFormValue(this.value ?? '');
}
if (changed.has('disabled')) {
this.#listbox!.disabled = this.disabled;
this.#listbox.disabled = this.disabled;
}
// TODO: handle filtering in the element, not the controller
}
Expand All @@ -220,7 +247,7 @@ export class PfSelect extends LitElement {
const { height, width } = this.getBoundingClientRect?.() || {};
const buttonLabel = this.#buttonLabel;
const hasBadge = this.#hasBadge;
const selectedOptions = this.#listbox?.selectedOptions ?? [];
const selectedOptions = this.#listbox.selectedOptions ?? [];
const typeahead = variant.startsWith('typeahead');
const checkboxes = variant === 'checkbox';
const offscreen = typeahead && 'offscreen';
Expand Down Expand Up @@ -300,7 +327,8 @@ export class PfSelect extends LitElement {
?hidden="${!this.placeholder && !this.#slots.hasSlotted('placeholder')}">
<slot name="placeholder">${this.placeholder}</slot>
</pf-option>
${this.#listbox?.render()}
${!(this.#a11yController instanceof ActivedescendantController) ? nothing
: this.#a11yController.renderItemsToShadowRoot()}
<slot @slotchange="${this.#onListboxSlotchange}"
?hidden=${typeahead && !ActivedescendantController.canControlLightDom()}></slot>
</div>
Expand Down Expand Up @@ -336,47 +364,23 @@ export class PfSelect extends LitElement {
// TODO: don't do filtering in the controller
}

#a11yController?: ListboxAccessibilityController<PfOption>;

#variantChanged() {
this.#listbox?.hostDisconnected();
#getA11yController() {
const getItems = () => this.options;
const getHTMLElement = () => this.shadowRoot?.getElementById('listbox') ?? null;
const isSelected = (option: PfOption) => option.selected;
const requestSelect = (option: PfOption, selected: boolean) => {
option.selected = !option.disabled && !!selected;
if (selected) {
this.selected = option;
}
return selected;
};
switch (this.variant) {
case 'typeahead':
case 'typeaheadmulti': {
this.#a11yController = ActivedescendantController.of(this, {
getItems,
getControllingElement: () => this.shadowRoot?.getElementById('toggle-input') ?? null,
getItemContainer: () => this.shadowRoot?.getElementById('listbox') ?? null,
});
return this.#listbox = ListboxController.of<PfOption>(this, {
a11yController: this.#a11yController,
multi: this.variant === 'typeaheadmulti',
getHTMLElement,
isSelected,
requestSelect,
});
} default:
this.#a11yController = RovingTabindexController.of(this, { getHTMLElement, getItems });
return this.#listbox = ListboxController.of<PfOption>(this, {
a11yController: this.#a11yController,
multi: this.variant === 'checkbox',
getHTMLElement,
isSelected,
requestSelect,
});
const getItemsContainer = this.#getListboxContainer;
const getOwningElement = this.#getComboboxInput;
if (this.variant.startsWith('typeahead')) {
return ActivedescendantController.of(this, { getItems, getItemsContainer, getOwningElement });
} else {
return RovingTabindexController.of(this, { getItems, getItemsContainer });
}
}

#variantChanged() {
this.#listbox.multi = this.variant === 'typeaheadmulti' || this.variant === 'checkbox';
this.#a11yController.hostDisconnected();
this.#a11yController = this.#getA11yController();
}

async #expandedChanged() {
const will = this.expanded ? 'close' : 'open';
this.dispatchEvent(new Event(will));
Expand All @@ -385,7 +389,7 @@ export class PfSelect extends LitElement {
switch (this.variant) {
case 'single':
case 'checkbox': {
const focusableItem = this.#listbox?.activeItem ?? this.#listbox?.nextItem;
const focusableItem = this.#listbox.activeItem ?? this.#listbox.nextItem;
focusableItem?.focus();
}
}
Expand Down Expand Up @@ -463,7 +467,7 @@ export class PfSelect extends LitElement {
await this.show();
// TODO: thread the needle of passing state between controllers
await new Promise(r => setTimeout(r));
this._input!.value = this.#listbox?.activeItem?.value ?? '';
this._input!.value = this.#listbox.activeItem?.value ?? '';
break;
case 'Enter':
this.hide();
Expand All @@ -488,7 +492,7 @@ export class PfSelect extends LitElement {
}

#onListboxSlotchange() {
this.#listbox?.setOptions(this.options);
this.#listbox.setOptions(this.options);
this.options.forEach((option, index, options) => {
option.setSize = options.length;
option.posInSet = index;
Expand Down Expand Up @@ -524,9 +528,10 @@ export class PfSelect extends LitElement {
|| this.querySelector?.<HTMLSlotElement>('[slot=placeholder]')
?.assignedNodes()
?.reduce((acc, node) => `${acc}${node.textContent}`, '')?.trim()
|| this.#listbox?.options
?.filter(x => x !== this.shadowRoot?.getElementById('placeholder'))
?.at(0)?.value
|| this.#listbox.options
.filter(this.#isNotPlaceholderOption)
.at(0)
?.value
|| '';
}

Expand Down
4 changes: 2 additions & 2 deletions elements/pf-tabs/pf-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ export class PfTabs extends LitElement {
isActiveTab: x => x.active,
});

#tabindex = new RovingTabindexController(this, {
getHTMLElement: () => this.shadowRoot?.getElementById('tabs') ?? null,
#tabindex = RovingTabindexController.of(this, {
getItemsContainer: () => this.tabsContainer ?? null,
getItems: () => this.tabs ?? [],
});

Expand Down

0 comments on commit 104d648

Please sign in to comment.