Skip to content

Commit

Permalink
Implement roving tabindex for tabs (#2041)
Browse files Browse the repository at this point in the history
* implement roving tabindex tabs

* implement roving tabindex tabs

* prettier

* implement roving tabindex tabs

* prettier

* remove test.only

* remove unncessary syncing

* Fix manual tab activations not working with roving tabindex

* prettier

* remove unnecessary extra code

* update changelog

* update changelog

* prettier
  • Loading branch information
KonnorRogers committed May 30, 2024
1 parent f757d51 commit f256d7a
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 49 deletions.
20 changes: 0 additions & 20 deletions docs/pages/components/tab.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,6 @@ meta:
layout: component
---

```html:preview
<sl-tab>Tab</sl-tab>
<sl-tab active>Active</sl-tab>
<sl-tab closable>Closable</sl-tab>
<sl-tab disabled>Disabled</sl-tab>
```

```jsx:react
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
const App = () => (
<>
<SlTab>Tab</SlTab>
<SlTab active>Active</SlTab>
<SlTab closable>Closable</SlTab>
<SlTab disabled>Disabled</SlTab>
</>
);
```

:::tip
Additional demonstrations can be found in the [tab group examples](/components/tab-group).
:::
2 changes: 2 additions & 0 deletions docs/pages/resources/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti

## Next

- `<sl-tab>` `closable` property now reflects. [#2041]
- `<sl-tab-group>` now implements a proper "roving tabindex" and `<sl-tab>` is no longer tabbable by default. This aligns closer to the APG pattern for tabs. [#2041]
- Fixed a bug in the submenu controller that prevented submenus from rendering in RTL without explicitly setting `dir` on the parent menu item [#1992]

## 2.15.1
Expand Down
16 changes: 13 additions & 3 deletions src/components/tab-group/tab-group.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,16 @@ export default class SlTabGroup extends ShoelaceElement {
index = 0;
}

this.tabs[index].focus({ preventScroll: true });
const currentTab = this.tabs[index];
currentTab.tabIndex = 0;
currentTab.focus({ preventScroll: true });

if (this.activation === 'auto') {
this.setActiveTab(this.tabs[index], { scrollBehavior: 'smooth' });
this.setActiveTab(currentTab, { scrollBehavior: 'smooth' });
} else {
this.tabs.forEach(tabEl => {
tabEl.tabIndex = tabEl === currentTab ? 0 : -1;
});
}

if (['top', 'bottom'].includes(this.placement)) {
Expand Down Expand Up @@ -253,7 +259,10 @@ export default class SlTabGroup extends ShoelaceElement {
this.activeTab = tab;

// Sync active tab and panel
this.tabs.forEach(el => (el.active = el === this.activeTab));
this.tabs.forEach(el => {
el.active = el === this.activeTab;
el.tabIndex = el === this.activeTab ? 0 : -1;
});
this.panels.forEach(el => (el.active = el.name === this.activeTab?.panel));
this.syncIndicator();

Expand Down Expand Up @@ -326,6 +335,7 @@ 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.panels = this.getAllPanels();
this.syncIndicator();

Expand Down
16 changes: 4 additions & 12 deletions src/components/tab/tab.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ export default class SlTab extends ShoelaceElement {
@property({ type: Boolean, reflect: true }) active = false;

/** Makes the tab closable and shows a close button. */
@property({ type: Boolean }) closable = false;
@property({ type: Boolean, reflect: true }) closable = false;

/** Disables the tab and prevents selection. */
@property({ type: Boolean, reflect: true }) disabled = false;

tabIndex = -1;

connectedCallback() {
super.connectedCallback();
this.setAttribute('role', 'tab');
Expand All @@ -68,16 +70,7 @@ export default class SlTab extends ShoelaceElement {
@watch('disabled')
handleDisabledChange() {
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
}

/** Sets focus to the tab. */
focus(options?: FocusOptions) {
this.tab.focus(options);
}

/** Removes focus from the tab. */
blur() {
this.tab.blur();
this.tabIndex = -1;
}

render() {
Expand All @@ -93,7 +86,6 @@ export default class SlTab extends ShoelaceElement {
'tab--closable': this.closable,
'tab--disabled': this.disabled
})}
tabindex=${this.disabled ? '-1' : '0'}
>
<slot></slot>
${this.closable
Expand Down
8 changes: 4 additions & 4 deletions src/components/tab/tab.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ export default css`
color: var(--sl-color-primary-600);
}
.tab:focus {
outline: none;
:host(:focus) {
outline: transparent;
}
.tab:focus-visible:not(.tab--disabled) {
:host(:focus-visible):not([disabled]) {
color: var(--sl-color-primary-600);
}
.tab:focus-visible {
:host(:focus-visible) {
outline: var(--sl-focus-ring);
outline-offset: calc(-1 * var(--sl-focus-ring-width) - var(--sl-focus-ring-offset));
}
Expand Down
21 changes: 11 additions & 10 deletions src/components/tab/tab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('<sl-tab>', () => {
<sl-tab slot="nav">Test</sl-tab>
</sl-tab-group>
`);

await expect(el).to.be.accessible();
});

Expand All @@ -23,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(base.getAttribute('tabindex')).to.equal('0');
expect(el.getAttribute('tabindex')).to.equal('-1');
expect(base.getAttribute('class')).to.equal(' tab ');
expect(el.active).to.equal(false);
expect(el.closable).to.equal(false);
Expand All @@ -38,7 +39,7 @@ describe('<sl-tab>', () => {
expect(el.disabled).to.equal(true);
expect(el.getAttribute('aria-disabled')).to.equal('true');
expect(base.getAttribute('class')).to.equal(' tab tab--disabled ');
expect(base.getAttribute('tabindex')).to.equal('-1');
expect(el.getAttribute('tabindex')).to.equal('-1');
});

it('should set active tab by attribute', async () => {
Expand All @@ -49,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(base.getAttribute('tabindex')).to.equal('0');
expect(el.getAttribute('tabindex')).to.equal('-1');
});

it('should set closable by attribute', async () => {
Expand All @@ -59,34 +60,34 @@ describe('<sl-tab>', () => {
const closeButton = el.shadowRoot!.querySelector('[part~="close-button"]');

expect(el.closable).to.equal(true);
expect(base.getAttribute('class')).to.equal(' tab tab--closable ');
expect(base.getAttribute('class')).to.match(/tab tab--closable/);
expect(closeButton).not.to.be.null;
});

describe('focus', () => {
it('should focus inner div', async () => {
it('should focus itself', async () => {
const el = await fixture<SlTab>(html` <sl-tab>Test</sl-tab> `);

const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;

el.focus();
await el.updateComplete;

expect(el.shadowRoot!.activeElement).to.equal(base);
expect(document.activeElement).to.equal(el);
});
});

describe('blur', () => {
it('should blur inner div', async () => {
it('should blur itself', async () => {
const el = await fixture<SlTab>(html` <sl-tab>Test</sl-tab> `);

el.focus();
await el.updateComplete;

expect(document.activeElement).to.equal(el);

el.blur();
await el.updateComplete;

expect(el.shadowRoot!.activeElement).to.equal(null);
expect(document.activeElement).to.not.equal(el);
});
});

Expand Down

0 comments on commit f256d7a

Please sign in to comment.