Skip to content

Commit 1d89f73

Browse files
fix(core)!: add getItems and getItemsContainer to RTI constructor (#2678)
* fix(core)!: add getItems and getItemsContainer to RTI constructor Closes #2644 * fix(core): update initialization of RTI in TabsController * fix: keyboard a11y for select et al --------- Co-authored-by: Steven Spriggs <[email protected]>
1 parent 305610b commit 1d89f73

File tree

11 files changed

+96
-79
lines changed

11 files changed

+96
-79
lines changed

.changeset/empty-otters-join.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@patternfly/pfe-core": major
3+
---
4+
5+
`RovingTabindexController`: deprecate the `initItems` method and add `getItems` and `getItemContainer` instead
6+
7+
BEFORE:
8+
```ts
9+
#tabindex = new RovingTabindexController(this);
10+
constructor() {
11+
super();
12+
this.#tabindex.initItems(this.#items);
13+
}
14+
```
15+
16+
AFTER:
17+
```ts
18+
#tabindex = new RovingTabindexController(this, {
19+
getItems: () => this.#items,
20+
});
21+
```
22+

core/pfe-core/controllers/listbox-controller.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,10 @@ export class ListboxController<Item extends HTMLElement> implements ReactiveCont
177177

178178
private internals: InternalsController;
179179

180-
private tabindex = new RovingTabindexController<Item>(this.host);
180+
private tabindex = new RovingTabindexController<Item>(this.host, {
181+
getElement: () => this.controllerOptions.getHTMLElement?.() ?? this.element,
182+
getItems: () => this.visibleOptions,
183+
});
181184

182185
public static of<Item extends HTMLElement>(
183186
host: ReactiveControllerHost,
@@ -283,7 +286,7 @@ export class ListboxController<Item extends HTMLElement> implements ReactiveCont
283286
const oldValue = this.value;
284287

285288
if (this.#updateFocus) {
286-
this.tabindex.updateItems(this.visibleOptions);
289+
this.tabindex.updateItems();
287290
this.#updateFocus = false;
288291
} else {
289292
this.tabindex.initItems(this.visibleOptions);

core/pfe-core/controllers/roving-tabindex-controller.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ const isFocusableElement = (el: Element): el is HTMLElement =>
66
!el.ariaHidden &&
77
!el.hasAttribute('hidden');
88

9+
export interface RovingTabindexControllerOptions<Item extends HTMLElement> {
10+
getElement?: () => Element;
11+
getItems?: () => Item[];
12+
getItemContainer?: () => HTMLElement;
13+
}
14+
915
/**
1016
* Implements roving tabindex, as described in WAI-ARIA practices, [Managing Focus Within
1117
* Components Using a Roving
@@ -30,8 +36,6 @@ export class RovingTabindexController<
3036
/** array of all focusable elements */
3137
#items: Item[] = [];
3238

33-
#getElement: () => Element | null;
34-
3539
/**
3640
* finds focusable items from a group of items
3741
*/
@@ -121,16 +125,30 @@ export class RovingTabindexController<
121125
return this.#caseSensitive;
122126
}
123127

124-
constructor(public host: ReactiveControllerHost, options?: {
125-
getElement: () => Element;
126-
}) {
127-
this.#getElement = options?.getElement ?? (() => host instanceof Element ? host : null);
128+
#options: {
129+
getElement(): Element | null;
130+
getItems?(): Item[];
131+
getItemContainer?(): HTMLElement;
132+
};
133+
134+
constructor(
135+
public host: ReactiveControllerHost,
136+
options?: RovingTabindexControllerOptions<Item>,
137+
) {
138+
this.#options = {
139+
getElement: options?.getElement ?? (() => host instanceof HTMLElement ? host : null),
140+
getItems: options?.getItems,
141+
getItemContainer: options?.getItemContainer,
142+
};
128143
const instance = RovingTabindexController.hosts.get(host);
129144
if (instance) {
130145
return instance as RovingTabindexController<Item>;
131146
}
132147
RovingTabindexController.hosts.set(host, this);
133148
this.host.addController(this);
149+
if (typeof this.#options?.getItems === 'function') {
150+
this.initItems(this.#options.getItems(), this.#options.getItemContainer?.());
151+
}
134152
}
135153

136154
#nextMatchingItem(key: string) {
@@ -157,7 +175,7 @@ export class RovingTabindexController<
157175
return;
158176
}
159177

160-
const orientation = this.#getElement()?.getAttribute('aria-orientation');
178+
const orientation = this.#options.getElement()?.getAttribute('aria-orientation');
161179

162180
const item = this.activeItem;
163181
let shouldPreventDefault = false;
@@ -252,8 +270,8 @@ export class RovingTabindexController<
252270
/**
253271
* Focuses next focusable item
254272
*/
255-
updateItems(items: Item[]) {
256-
const hasActive = document.activeElement && this.#getElement()?.contains(document.activeElement);
273+
updateItems(items: Item[] = this.#options.getItems?.() ?? []) {
274+
const hasActive = document.activeElement && this.#options.getElement()?.contains(document.activeElement);
257275
const sequence = [...items.slice(this.#itemIndex - 1), ...items.slice(0, this.#itemIndex - 1)];
258276
const first = sequence.find(item => this.#focusableItems.includes(item));
259277
this.#items = items ?? [];
@@ -268,6 +286,7 @@ export class RovingTabindexController<
268286

269287
/**
270288
* from array of HTML items, and sets active items
289+
* @deprecated: use getItems and getItemContainer option functions
271290
*/
272291
initItems(items: Item[], itemsContainer?: Element) {
273292
this.#items = items ?? [];
@@ -276,12 +295,12 @@ export class RovingTabindexController<
276295
this.#activeItem = focusableItem;
277296
this.#updateTabindex();
278297

279-
itemsContainer ??= this.#getElement() ?? undefined;
298+
itemsContainer ??= this.#options.getElement() ?? undefined;
280299

281300
/**
282301
* removes listener on previous contained and applies it to new container
283302
*/
284-
if (!this.#itemsContainer || itemsContainer !== this.#itemsContainer) {
303+
if (itemsContainer !== this.#itemsContainer) {
285304
this.#itemsContainer?.removeEventListener('keydown', this.#onKeydown);
286305
this.#itemsContainer = itemsContainer;
287306
this.hostConnected();

core/pfe-core/controllers/tabs-controller.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ export class TabsController implements ReactiveController {
157157
this.#logger = new Logger(host);
158158
if (host instanceof HTMLElement) {
159159
this.#element = host;
160-
this.#tabindex = new RovingTabindexController(host);
160+
this.#tabindex = new RovingTabindexController(host, {
161+
getItems: () => this._tabs,
162+
});
161163
} else {
162164
const element = options.getHTMLElement?.();
163165
if (!element) {
@@ -223,8 +225,7 @@ export class TabsController implements ReactiveController {
223225

224226
if (this._tabs.length > 0) {
225227
this.#updateAccessibility();
226-
// TODO(bennypowers): adjust to fit, in or after #2570
227-
this.#tabindex.initItems(this._tabs, this.#element);
228+
this.#tabindex.updateItems();
228229
this.#setActiveTab();
229230
}
230231

core/pfe-core/controllers/toggle-controller.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,6 @@ export class ToggleController implements ReactiveController {
419419
if (focus) {
420420
this.#popupElement?.focus();
421421
}
422-
this.#getToggleElement()?.dispatchEvent(new CustomEvent('open'));
423422
}
424423
}
425424

@@ -451,7 +450,6 @@ export class ToggleController implements ReactiveController {
451450
if (focused) {
452451
(el as HTMLElement)?.focus();
453452
}
454-
el?.dispatchEvent(new CustomEvent('close'));
455453
}
456454
}
457455
}

elements/pf-accordion/BaseAccordion.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export abstract class BaseAccordion extends LitElement {
4646
return event instanceof AccordionHeaderChangeEvent;
4747
}
4848

49-
#headerIndex = new RovingTabindexController<BaseAccordionHeader>(this);
49+
#headerIndex = new RovingTabindexController<BaseAccordionHeader>(this, {
50+
getItems: () => this.headers,
51+
});
5052

5153
#expandedIndex: number[] = [];
5254

@@ -146,14 +148,13 @@ export abstract class BaseAccordion extends LitElement {
146148
*/
147149
async #init() {
148150
this.#initialized ||= !!await this.updateComplete;
149-
this.#headerIndex.initItems(this.headers);
150151
// Event listener to the accordion header after the accordion has been initialized to add the roving tabindex
151152
this.addEventListener('focusin', this.#updateActiveHeader);
152153
this.updateAccessibility();
153154
}
154155

155156
#updateActiveHeader() {
156-
if (this.#activeHeader) {
157+
if (this.#activeHeader !== this.#headerIndex.activeItem) {
157158
this.#headerIndex.updateActiveItem(this.#activeHeader);
158159
}
159160
}
@@ -231,6 +232,7 @@ export abstract class BaseAccordion extends LitElement {
231232
}
232233

233234
public updateAccessibility() {
235+
this.#headerIndex.updateItems();
234236
const { headers } = this;
235237

236238
// For each header in the accordion, attach the aria connections

elements/pf-chip/pf-chip-group.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,9 @@ export class PfChipGroup extends LitElement {
9393

9494
#buttons: HTMLElement[] = [];
9595

96-
#itemsInit = false;
97-
98-
#tabindex = new RovingTabindexController(this);
96+
#tabindex = new RovingTabindexController(this, {
97+
getItems: () => this.#buttons,
98+
});
9999

100100
constructor() {
101101
super();
@@ -188,13 +188,8 @@ export class PfChipGroup extends LitElement {
188188
].filter((x): x is PfChip => !!x);
189189
if (oldButtons.length !== this.#buttons.length ||
190190
!oldButtons.every((element, index) => element === this.#buttons[index])) {
191-
if (this.#itemsInit) {
192-
this.#tabindex.updateItems(this.#buttons);
193-
} else {
194-
this.#tabindex.initItems(this.#buttons);
195-
}
191+
this.#tabindex.updateItems();
196192
}
197-
this.#itemsInit = true;
198193
this.#updateOverflow();
199194
}
200195
}

elements/pf-dropdown/pf-dropdown-menu.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ export class PfDropdownMenu extends LitElement {
3737
'click': this.#onMenuitemClick.bind(this),
3838
};
3939

40-
#itemsInit = false;
41-
42-
#tabindex = new RovingTabindexController(this);
40+
#tabindex = new RovingTabindexController(this, {
41+
getItems: () => this.#menuitems,
42+
});
4343

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

@@ -108,15 +108,15 @@ export class PfDropdownMenu extends LitElement {
108108
*/
109109
#handleItemChange(event: Event) {
110110
if (event instanceof DropdownItemChange) {
111-
this.#updateItems();
111+
this.#tabindex.updateItems();
112112
}
113113
}
114114

115115
/**
116116
* handles slot change event
117117
*/
118118
#handleSlotChange() {
119-
this.#updateItems();
119+
this.#tabindex.updateItems();
120120
}
121121

122122
#nextMatchingItem(key: string) {
@@ -180,18 +180,6 @@ export class PfDropdownMenu extends LitElement {
180180
}
181181
}
182182
}
183-
184-
/**
185-
* updates menu items list
186-
*/
187-
#updateItems() {
188-
if (this.#itemsInit) {
189-
this.#tabindex.updateItems(this.#menuitems);
190-
} else {
191-
this.#tabindex.initItems(this.#menuitems);
192-
}
193-
this.#itemsInit = true;
194-
}
195183
}
196184

197185
declare global {

elements/pf-jump-links/pf-jump-links.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ export class PfJumpLinks extends LitElement {
8383

8484
#initialized = false;
8585

86-
#rovingTabindexController = new RovingTabindexController(this);
86+
#rovingTabindexController = new RovingTabindexController<HTMLAnchorElement>(this, {
87+
getItems: () => this.#getItems(),
88+
});
8789

8890
#spy = new ScrollSpyController(this, {
8991
rootMargin: `${this.offset}px 0px 0px 0px`,
@@ -121,18 +123,16 @@ export class PfJumpLinks extends LitElement {
121123
`;
122124
}
123125

124-
#updateItems() {
125-
const items = Array.from(this.querySelectorAll(':is(pf-jump-links-item, pf-jump-links-list)'))
126+
#getItems() {
127+
return Array.from(this.querySelectorAll(':is(pf-jump-links-item, pf-jump-links-list)'))
126128
.flatMap(i => [
127129
...i.shadowRoot?.querySelectorAll('a') ?? [],
128130
...i.querySelectorAll('a') ?? [],
129131
]);
130-
if (this.#initialized) {
131-
this.#rovingTabindexController.updateItems(items);
132-
} else {
133-
this.#rovingTabindexController.initItems(items);
134-
this.#initialized = true;
135-
}
132+
}
133+
134+
#updateItems() {
135+
this.#rovingTabindexController.updateItems();
136136
}
137137

138138
async #onSelect(event: Event) {

elements/pf-select/pf-select.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ export class PfSelect extends LitElement {
147147
kind: 'listbox',
148148
onChange: expanded => {
149149
this.dispatchEvent(new Event(expanded ? 'open' : 'close'));
150+
if (expanded) {
151+
this.querySelector<PfOption>('pf-option[tabindex="0"]')?.focus();
152+
}
150153
}
151154
});
152155

0 commit comments

Comments
 (0)