pull/2101/head
Cory LaViska 2024-06-21 13:38:25 -04:00
commit d92a9d9bd5
5 zmienionych plików z 64 dodań i 34 usunięć

Wyświetl plik

@ -125,7 +125,6 @@ export default class SlRadioButton extends ShoelaceElement {
aria-disabled=${this.disabled}
type="button"
value=${ifDefined(this.value)}
tabindex="${this.checked ? '0' : '-1'}"
@blur=${this.handleBlur}
@focus=${this.handleFocus}
@click=${this.handleClick}

Wyświetl plik

@ -169,7 +169,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
radio.checked = false;
if (!this.hasButtonGroup) {
radio.tabIndex = -1;
radio.setAttribute('tabindex', '-1');
}
});
@ -177,7 +177,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
radios[index].checked = true;
if (!this.hasButtonGroup) {
radios[index].tabIndex = 0;
radios[index].setAttribute('tabindex', '0');
radios[index].focus();
} else {
radios[index].shadowRoot!.querySelector('button')!.focus();
@ -226,10 +226,10 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
const buttonRadio = radios[0].shadowRoot?.querySelector('button');
if (buttonRadio) {
buttonRadio.tabIndex = 0;
buttonRadio.setAttribute('tabindex', '0');
}
} else {
radios[0].tabIndex = 0;
radios[0].setAttribute('tabindex', '0');
}
}

Wyświetl plik

@ -50,6 +50,7 @@ export default class SlTabGroup extends ShoelaceElement {
private mutationObserver: MutationObserver;
private resizeObserver: ResizeObserver;
private tabs: SlTab[] = [];
private focusableTabs: SlTab[] = [];
private panels: SlTabPanel[] = [];
@query('.tab-group') tabGroup: HTMLElement;
@ -123,14 +124,10 @@ export default class SlTabGroup extends ShoelaceElement {
this.resizeObserver.unobserve(this.nav);
}
private getAllTabs(options: { includeDisabled: boolean } = { includeDisabled: true }) {
private getAllTabs() {
const slot = this.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="nav"]')!;
return [...(slot.assignedElements() as SlTab[])].filter(el => {
return options.includeDisabled
? el.tagName.toLowerCase() === 'sl-tab'
: el.tagName.toLowerCase() === 'sl-tab' && !el.disabled;
});
return slot.assignedElements() as SlTab[];
}
private getAllPanels() {
@ -178,48 +175,44 @@ export default class SlTabGroup extends ShoelaceElement {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
const activeEl = this.tabs.find(t => t.matches(':focus'));
const isRtl = this.matches(':dir(rtl)');
let nextTab: null | SlTab = null;
if (activeEl?.tagName.toLowerCase() === 'sl-tab') {
let index = this.tabs.indexOf(activeEl);
if (event.key === 'Home') {
index = 0;
nextTab = this.focusableTabs[0];
} else if (event.key === 'End') {
index = this.tabs.length - 1;
nextTab = this.focusableTabs[this.focusableTabs.length - 1];
} else if (
(['top', 'bottom'].includes(this.placement) && event.key === (isRtl ? 'ArrowRight' : 'ArrowLeft')) ||
(['start', 'end'].includes(this.placement) && event.key === 'ArrowUp')
) {
index--;
const currentIndex = this.tabs.findIndex(el => el === activeEl);
nextTab = this.findNextFocusableTab(currentIndex, 'backward');
} else if (
(['top', 'bottom'].includes(this.placement) && event.key === (isRtl ? 'ArrowLeft' : 'ArrowRight')) ||
(['start', 'end'].includes(this.placement) && event.key === 'ArrowDown')
) {
index++;
const currentIndex = this.tabs.findIndex(el => el === activeEl);
nextTab = this.findNextFocusableTab(currentIndex, 'forward');
}
if (index < 0) {
index = this.tabs.length - 1;
if (!nextTab) {
return;
}
if (index > this.tabs.length - 1) {
index = 0;
}
const currentTab = this.tabs[index];
currentTab.tabIndex = 0;
currentTab.focus({ preventScroll: true });
nextTab.tabIndex = 0;
nextTab.focus({ preventScroll: true });
if (this.activation === 'auto') {
this.setActiveTab(currentTab, { scrollBehavior: 'smooth' });
this.setActiveTab(nextTab, { scrollBehavior: 'smooth' });
} else {
this.tabs.forEach(tabEl => {
tabEl.tabIndex = tabEl === currentTab ? 0 : -1;
tabEl.tabIndex = tabEl === nextTab ? 0 : -1;
});
}
if (['top', 'bottom'].includes(this.placement)) {
scrollIntoView(this.tabs[index], this.nav, 'horizontal');
scrollIntoView(nextTab, this.nav, 'horizontal');
}
event.preventDefault();
@ -334,7 +327,8 @@ export default class SlTabGroup extends ShoelaceElement {
// This stores tabs and panels so we can refer to a cache instead of calling querySelectorAll() multiple times.
private syncTabsAndPanels() {
this.tabs = this.getAllTabs({ includeDisabled: false });
this.tabs = this.getAllTabs();
this.focusableTabs = this.tabs.filter(el => !el.disabled);
this.panels = this.getAllPanels();
this.syncIndicator();
@ -343,6 +337,34 @@ export default class SlTabGroup extends ShoelaceElement {
this.updateComplete.then(() => this.updateScrollControls());
}
private findNextFocusableTab(currentIndex: number, direction: 'forward' | 'backward') {
let nextTab = null;
const iterator = direction === 'forward' ? 1 : -1;
let nextIndex = currentIndex + iterator;
while (currentIndex < this.tabs.length) {
nextTab = this.tabs[nextIndex] || null;
if (nextTab === null) {
// This is where wrapping happens. If we're moving forward and get to the end, then we jump to the beginning. If we're moving backward and get to the start, then we jump to the end.
if (direction === 'forward') {
nextTab = this.focusableTabs[0];
} else {
nextTab = this.focusableTabs[this.focusableTabs.length - 1];
}
break;
}
if (!nextTab.disabled) {
break;
}
nextIndex += iterator;
}
return nextTab;
}
@watch('noScrollControls', { waitUntilFirstUpdate: true })
updateScrollControls() {
if (this.noScrollControls) {

Wyświetl plik

@ -50,7 +50,11 @@ export default class SlTab extends ShoelaceElement {
/** Disables the tab and prevents selection. */
@property({ type: Boolean, reflect: true }) disabled = false;
tabIndex = -1;
/**
* @internal
* Need to wrap in a `@property()` otherwise CustomElement throws a "The result must not have attributes" runtime error.
*/
@property({ type: Number, reflect: true }) tabIndex = 0;
connectedCallback() {
super.connectedCallback();
@ -70,7 +74,12 @@ export default class SlTab extends ShoelaceElement {
@watch('disabled')
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
this.tabIndex = -1;
if (this.disabled && !this.active) {
this.tabIndex = -1;
} else {
this.tabIndex = 0;
}
}
render() {

Wyświetl plik

@ -24,7 +24,7 @@ describe('<sl-tab>', () => {
expect(el.getAttribute('role')).to.equal('tab');
expect(el.getAttribute('aria-disabled')).to.equal('false');
expect(el.getAttribute('aria-selected')).to.equal('false');
expect(el.getAttribute('tabindex')).to.equal('-1');
expect(el.getAttribute('tabindex')).to.equal('0');
expect(base.getAttribute('class')).to.equal(' tab ');
expect(el.active).to.equal(false);
expect(el.closable).to.equal(false);
@ -50,7 +50,7 @@ describe('<sl-tab>', () => {
expect(el.active).to.equal(true);
expect(el.getAttribute('aria-selected')).to.equal('true');
expect(base.getAttribute('class')).to.equal(' tab tab--active ');
expect(el.getAttribute('tabindex')).to.equal('-1');
expect(el.getAttribute('tabindex')).to.equal('0');
});
it('should set closable by attribute', async () => {