diff --git a/docs/pages/components/tab.md b/docs/pages/components/tab.md
index 0c04e0ce..9b4d9e61 100644
--- a/docs/pages/components/tab.md
+++ b/docs/pages/components/tab.md
@@ -5,26 +5,6 @@ meta:
layout: component
---
-```html:preview
-Tab
-Active
-Closable
-Disabled
-```
-
-```jsx:react
-import SlTab from '@shoelace-style/shoelace/dist/react/tab';
-
-const App = () => (
- <>
- Tab
- Active
- Closable
- Disabled
- >
-);
-```
-
:::tip
Additional demonstrations can be found in the [tab group examples](/components/tab-group).
:::
diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md
index 833af4a3..44204ac1 100644
--- a/docs/pages/resources/changelog.md
+++ b/docs/pages/resources/changelog.md
@@ -14,6 +14,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
## Next
+- `` `closable` property now reflects. [#2041]
+- `` now implements a proper "roving tabindex" and `` 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
diff --git a/src/components/tab-group/tab-group.component.ts b/src/components/tab-group/tab-group.component.ts
index be582806..1485f31c 100644
--- a/src/components/tab-group/tab-group.component.ts
+++ b/src/components/tab-group/tab-group.component.ts
@@ -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)) {
@@ -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();
@@ -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();
diff --git a/src/components/tab/tab.component.ts b/src/components/tab/tab.component.ts
index 1aa14fc8..d059f343 100644
--- a/src/components/tab/tab.component.ts
+++ b/src/components/tab/tab.component.ts
@@ -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');
@@ -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() {
@@ -93,7 +86,6 @@ export default class SlTab extends ShoelaceElement {
'tab--closable': this.closable,
'tab--disabled': this.disabled
})}
- tabindex=${this.disabled ? '-1' : '0'}
>
${this.closable
diff --git a/src/components/tab/tab.styles.ts b/src/components/tab/tab.styles.ts
index e2e1e3a0..0473b3c6 100644
--- a/src/components/tab/tab.styles.ts
+++ b/src/components/tab/tab.styles.ts
@@ -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));
}
diff --git a/src/components/tab/tab.test.ts b/src/components/tab/tab.test.ts
index 48cc660a..362dcd77 100644
--- a/src/components/tab/tab.test.ts
+++ b/src/components/tab/tab.test.ts
@@ -12,6 +12,7 @@ describe('', () => {
Test
`);
+
await expect(el).to.be.accessible();
});
@@ -23,7 +24,7 @@ describe('', () => {
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);
@@ -38,7 +39,7 @@ describe('', () => {
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 () => {
@@ -49,7 +50,7 @@ describe('', () => {
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 () => {
@@ -59,34 +60,34 @@ describe('', () => {
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(html` Test `);
- const base = el.shadowRoot!.querySelector('[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(html` Test `);
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);
});
});