From cdbfb4687d9e96ae229f285dfa523fecd3c9a59f Mon Sep 17 00:00:00 2001 From: Mike Perrotti Date: Fri, 19 Apr 2024 10:15:21 -0400 Subject: [PATCH 1/4] first attempt --- examples/index.html | 25 +++++++++++++++++++++++-- src/tab-container-element.ts | 18 ++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/examples/index.html b/examples/index.html index 4e3553e..0d7f7e2 100644 --- a/examples/index.html +++ b/examples/index.html @@ -46,6 +46,27 @@

Horizontal (custom tablist)

+

Horizontal (custom tablist and tablist-wrapper)

+ + +
+
+ + + +
+
+
+ Panel 1 +
+ + +
+

Vertical (shadow tablist)

@@ -140,7 +161,7 @@

Panel with extra buttons

- - + + diff --git a/src/tab-container-element.ts b/src/tab-container-element.ts index 831409d..382405b 100644 --- a/src/tab-container-element.ts +++ b/src/tab-container-element.ts @@ -104,7 +104,7 @@ export class TabContainerElement extends HTMLElement { } get #tabListTabWrapper() { - return this.shadowRoot!.querySelector('div[part="tablist-tab-wrapper"]')! + return this.shadowRoot!.querySelector('slot[part="tablist-tab-wrapper"]')! } get #beforeTabsSlot() { @@ -165,8 +165,9 @@ export class TabContainerElement extends HTMLElement { const tabListContainer = document.createElement('div') tabListContainer.style.display = 'flex' tabListContainer.setAttribute('part', 'tablist-wrapper') - const tabListTabWrapper = document.createElement('div') + const tabListTabWrapper = document.createElement('slot') tabListTabWrapper.setAttribute('part', 'tablist-tab-wrapper') + tabListTabWrapper.setAttribute('name', 'tablist-tab-wrapper') const tabListSlot = document.createElement('slot') tabListSlot.setAttribute('part', 'tablist') tabListSlot.setAttribute('name', 'tablist') @@ -275,13 +276,22 @@ export class TabContainerElement extends HTMLElement { if (!this.#setupComplete) { const tabListSlot = this.#tabListSlot const customTabList = this.querySelector('[role=tablist]') - if (customTabList && customTabList.closest(this.tagName) === this) { + const customTabListWrapper = this.querySelector('[slot=tablist-tab-wrapper]') + if (customTabListWrapper && customTabListWrapper.closest(this.tagName) === this) { + if (manualSlotsSupported) { + tabListSlot.assign(customTabListWrapper) + } else { + customTabListWrapper.setAttribute('slot', 'tablist') + } + } + else if (customTabList && customTabList.closest(this.tagName) === this) { if (manualSlotsSupported) { tabListSlot.assign(customTabList) } else { customTabList.setAttribute('slot', 'tablist') } - } else { + } + else { this.#tabListTabWrapper.role = 'tablist' if (manualSlotsSupported) { tabListSlot.assign(...[...this.children].filter(e => e.matches('[role=tab]'))) From 2a0e8d2a5fced1395ee55f2f18d4349b614c0fee Mon Sep 17 00:00:00 2001 From: Mike Perrotti Date: Mon, 22 Apr 2024 11:21:35 -0400 Subject: [PATCH 2/4] adds the ability to have a custom tablist-tab-wrapper --- examples/index.html | 4 +-- src/tab-container-element.ts | 12 +++++---- test/test.js | 49 ++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 7 deletions(-) diff --git a/examples/index.html b/examples/index.html index 0d7f7e2..e752ef6 100644 --- a/examples/index.html +++ b/examples/index.html @@ -161,7 +161,7 @@

Panel with extra buttons

- - + + diff --git a/src/tab-container-element.ts b/src/tab-container-element.ts index 382405b..bdd9f1a 100644 --- a/src/tab-container-element.ts +++ b/src/tab-container-element.ts @@ -283,15 +283,13 @@ export class TabContainerElement extends HTMLElement { } else { customTabListWrapper.setAttribute('slot', 'tablist') } - } - else if (customTabList && customTabList.closest(this.tagName) === this) { + } else if (customTabList && customTabList.closest(this.tagName) === this) { if (manualSlotsSupported) { tabListSlot.assign(customTabList) } else { customTabList.setAttribute('slot', 'tablist') } - } - else { + } else { this.#tabListTabWrapper.role = 'tablist' if (manualSlotsSupported) { tabListSlot.assign(...[...this.children].filter(e => e.matches('[role=tab]'))) @@ -312,7 +310,11 @@ export class TabContainerElement extends HTMLElement { const afterSlotted: Element[] = [] let autoSlotted = beforeSlotted for (const child of this.children) { - if (child.getAttribute('role') === 'tab' || child.getAttribute('role') === 'tablist') { + if ( + child.getAttribute('role') === 'tab' || + child.getAttribute('role') === 'tablist' || + child.getAttribute('slot') === 'tablist-tab-wrapper' + ) { autoSlotted = afterTabSlotted continue } diff --git a/test/test.js b/test/test.js index 441aa41..28cb708 100644 --- a/test/test.js +++ b/test/test.js @@ -669,4 +669,53 @@ describe('tab-container', function () { ) }) }) + describe('with custom tablist-tab-wrapper', function () { + beforeEach(function () { + document.body.innerHTML = ` + +
+
+ + + +
+
+ +
+ Panel 2 +
+ +
+ ` + tabs = Array.from(document.querySelectorAll('button')) + panels = Array.from(document.querySelectorAll('[role="tabpanel"]')) + }) + + afterEach(function () { + // Check to make sure we still have accessible markup after the test finishes running. + expect(document.body).to.be.accessible() + + document.body.innerHTML = '' + }) + + it('has accessible markup', function () { + expect(document.body).to.be.accessible() + }) + + it('the second tab is still selected', function () { + assert.deepStrictEqual(tabs.map(isSelected), [false, true, false], 'Second tab is selected') + assert.deepStrictEqual(panels.map(isHidden), [true, false, true], 'Second panel is visible') + }) + + it('selects the clicked tab', function () { + tabs[0].click() + + assert.deepStrictEqual(tabs.map(isSelected), [true, false, false], 'First tab is selected') + assert.deepStrictEqual(panels.map(isHidden), [false, true, true], 'First panel is visible') + }) + }) }) From 029187e65f6109ec49569fe3b381a0502efbba7c Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 24 Apr 2024 13:03:34 +0100 Subject: [PATCH 3/4] allow overriding of the tablist-wrapper --- custom-elements.json | 6 +++ examples/index.html | 21 +++++++++ src/tab-container-element.ts | 89 ++++++++++++++++++++++-------------- test/test.js | 51 +++++++++++++++++++++ 4 files changed, 133 insertions(+), 34 deletions(-) diff --git a/custom-elements.json b/custom-elements.json index fe0601c..8355ce3 100644 --- a/custom-elements.json +++ b/custom-elements.json @@ -439,6 +439,12 @@ "privacy": "private", "readonly": true }, + { + "kind": "field", + "name": "#tabListWrapper", + "privacy": "private", + "readonly": true + }, { "kind": "field", "name": "#tabListTabWrapper", diff --git a/examples/index.html b/examples/index.html index e752ef6..12fc8e7 100644 --- a/examples/index.html +++ b/examples/index.html @@ -48,6 +48,27 @@

Horizontal (custom tablist)

Horizontal (custom tablist and tablist-wrapper)

+ +
+
+ + + +
+
+
+ Panel 1 +
+ + +
+ +

Horizontal (custom tablist and tablist-tab-wrapper)

+
diff --git a/src/tab-container-element.ts b/src/tab-container-element.ts index bdd9f1a..93e625a 100644 --- a/src/tab-container-element.ts +++ b/src/tab-container-element.ts @@ -95,6 +95,10 @@ export class TabContainerElement extends HTMLElement { static observedAttributes = ['vertical'] get #tabList() { + const wrapper = this.querySelector('[slot=tablist-wrapper],[slot=tablist-tab-wrapper]') + if (wrapper?.closest(this.tagName) === this) { + return wrapper.querySelector('[role=tablist]') as HTMLElement + } const slot = this.#tabListSlot if (this.#tabListTabWrapper.hasAttribute('role')) { return this.#tabListTabWrapper @@ -103,6 +107,10 @@ export class TabContainerElement extends HTMLElement { } } + get #tabListWrapper() { + return this.shadowRoot!.querySelector('slot[part="tablist-wrapper"]')! + } + get #tabListTabWrapper() { return this.shadowRoot!.querySelector('slot[part="tablist-tab-wrapper"]')! } @@ -162,9 +170,10 @@ export class TabContainerElement extends HTMLElement { connectedCallback(): void { this.#internals ||= this.attachInternals ? this.attachInternals() : null const shadowRoot = this.shadowRoot || this.attachShadow({mode: 'open', slotAssignment: 'manual'}) - const tabListContainer = document.createElement('div') + const tabListContainer = document.createElement('slot') tabListContainer.style.display = 'flex' tabListContainer.setAttribute('part', 'tablist-wrapper') + tabListContainer.setAttribute('name', 'tablist-wrapper') const tabListTabWrapper = document.createElement('slot') tabListTabWrapper.setAttribute('part', 'tablist-tab-wrapper') tabListTabWrapper.setAttribute('name', 'tablist-tab-wrapper') @@ -275,13 +284,22 @@ export class TabContainerElement extends HTMLElement { selectTab(index: number): void { if (!this.#setupComplete) { const tabListSlot = this.#tabListSlot + const tabListWrapper = this.#tabListWrapper + const tabListTabWrapper = this.#tabListTabWrapper const customTabList = this.querySelector('[role=tablist]') - const customTabListWrapper = this.querySelector('[slot=tablist-tab-wrapper]') + const customTabListWrapper = this.querySelector('[slot=tablist-wrapper]') + const customTabListTabWrapper = this.querySelector('[slot=tablist-tab-wrapper]') if (customTabListWrapper && customTabListWrapper.closest(this.tagName) === this) { if (manualSlotsSupported) { - tabListSlot.assign(customTabListWrapper) + tabListWrapper.assign(customTabListWrapper) } else { - customTabListWrapper.setAttribute('slot', 'tablist') + customTabListWrapper.setAttribute('slot', 'tablist-wrapper') + } + } else if (customTabListTabWrapper && customTabListTabWrapper.closest(this.tagName) === this) { + if (manualSlotsSupported) { + tabListTabWrapper.assign(customTabListTabWrapper) + } else { + customTabListTabWrapper.setAttribute('slot', 'tablist-tab-wrapper') } } else if (customTabList && customTabList.closest(this.tagName) === this) { if (manualSlotsSupported) { @@ -305,40 +323,43 @@ export class TabContainerElement extends HTMLElement { if (this.vertical) { this.#tabList.setAttribute('aria-orientation', 'vertical') } - const beforeSlotted: Element[] = [] - const afterTabSlotted: Element[] = [] - const afterSlotted: Element[] = [] - let autoSlotted = beforeSlotted - for (const child of this.children) { - if ( - child.getAttribute('role') === 'tab' || - child.getAttribute('role') === 'tablist' || - child.getAttribute('slot') === 'tablist-tab-wrapper' - ) { - autoSlotted = afterTabSlotted - continue - } - if (child.getAttribute('role') === 'tabpanel') { - autoSlotted = afterSlotted - continue + const bringsOwnWrapper = this.querySelector('[slot=tablist-wrapper]')?.closest(this.tagName) === this + if (!bringsOwnWrapper) { + const beforeSlotted: Element[] = [] + const afterTabSlotted: Element[] = [] + const afterSlotted: Element[] = [] + let autoSlotted = beforeSlotted + for (const child of this.children) { + if ( + child.getAttribute('role') === 'tab' || + child.getAttribute('role') === 'tablist' || + child.getAttribute('slot') === 'tablist-tab-wrapper' + ) { + autoSlotted = afterTabSlotted + continue + } + if (child.getAttribute('role') === 'tabpanel') { + autoSlotted = afterSlotted + continue + } + if (child.getAttribute('slot') === 'before-tabs') { + beforeSlotted.push(child) + } else if (child.getAttribute('slot') === 'after-tabs') { + afterTabSlotted.push(child) + } else { + autoSlotted.push(child) + } } - if (child.getAttribute('slot') === 'before-tabs') { - beforeSlotted.push(child) - } else if (child.getAttribute('slot') === 'after-tabs') { - afterTabSlotted.push(child) + if (manualSlotsSupported) { + this.#beforeTabsSlot.assign(...beforeSlotted) + this.#afterTabsSlot.assign(...afterTabSlotted) + this.#afterPanelsSlot.assign(...afterSlotted) } else { - autoSlotted.push(child) + for (const el of beforeSlotted) el.setAttribute('slot', 'before-tabs') + for (const el of afterTabSlotted) el.setAttribute('slot', 'after-tabs') + for (const el of afterSlotted) el.setAttribute('slot', 'after-panels') } } - if (manualSlotsSupported) { - this.#beforeTabsSlot.assign(...beforeSlotted) - this.#afterTabsSlot.assign(...afterTabSlotted) - this.#afterPanelsSlot.assign(...afterSlotted) - } else { - for (const el of beforeSlotted) el.setAttribute('slot', 'before-tabs') - for (const el of afterTabSlotted) el.setAttribute('slot', 'after-tabs') - for (const el of afterSlotted) el.setAttribute('slot', 'after-panels') - } const defaultTab = this.defaultTabIndex const defaultIndex = defaultTab >= 0 ? defaultTab : this.selectedTabIndex index = index >= 0 ? index : Math.max(0, defaultIndex) diff --git a/test/test.js b/test/test.js index 28cb708..f5dce71 100644 --- a/test/test.js +++ b/test/test.js @@ -669,6 +669,57 @@ describe('tab-container', function () { ) }) }) + + describe('with custom tablist-wrapper', function () { + beforeEach(function () { + document.body.innerHTML = ` + +
+
+ + + +
+
+ +
+ Panel 2 +
+ +
+ ` + tabs = Array.from(document.querySelectorAll('button')) + panels = Array.from(document.querySelectorAll('[role="tabpanel"]')) + }) + + afterEach(function () { + // Check to make sure we still have accessible markup after the test finishes running. + expect(document.body).to.be.accessible() + + document.body.innerHTML = '' + }) + + it('has accessible markup', function () { + expect(document.body).to.be.accessible() + }) + + it('the second tab is still selected', function () { + assert.deepStrictEqual(tabs.map(isSelected), [false, true, false], 'Second tab is selected') + assert.deepStrictEqual(panels.map(isHidden), [true, false, true], 'Second panel is visible') + }) + + it('selects the clicked tab', function () { + tabs[0].click() + + assert.deepStrictEqual(tabs.map(isSelected), [true, false, false], 'First tab is selected') + assert.deepStrictEqual(panels.map(isHidden), [false, true, true], 'First panel is visible') + }) + }) + describe('with custom tablist-tab-wrapper', function () { beforeEach(function () { document.body.innerHTML = ` From ac3a4092e713ffddddebd6b15cd0c1cb582e165c Mon Sep 17 00:00:00 2001 From: Keith Cirkel Date: Wed, 24 Apr 2024 13:16:23 +0100 Subject: [PATCH 4/4] update readme --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/README.md b/README.md index f5b20ad..09354f6 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,70 @@ In those cases, apply `data-tab-container-no-tabstop` to the `tabpanel` element.
``` +### Unmanaged slots + +`` aims to simplify complex markup away in the ShadowDOM, so that the HTML you end up writing is overall less. However sometimes it can be useful to have _full_ control over the markup. Each of the `::part` selectors are also ``s, this means you can take any part and slot it, overriding the built-in ShadowDOM. +#### Unmanaged `tablist` + +You are able to provide your own `role=tablist` and `` will accommodate. This can be useful if you need extra presentational markup in the tablist. But remember: + + - You must ensure that all child elements are `role=tab` or `role=presentational`. + - The element will still slot contents before and after this element, in order to correctly present the tablist. + +```html + +
+ + + +
+
+
+
+``` + +#### Unmanaged `tablist-tab-wrapper` + +You are able to slot the `tablist-tab-wrapper` part. This slot manages the tabs but not the before or after elements. In this way, you can put custom HTML inside the tab list. Bear in mind if you're supplying this element that: + + - You must also supply a `role=tablist` as a child. + - You must ensure that all child elements are `role=tab` or `role=presentational`. + - The element will still slot contents before and after this element, in order to correctly present the tablist. + +```html + +
+
+ + +
+
+
+
+
+``` +#### Unmanaged `tablist-wrapper` + +If you want to take full control over the entire tab region, including managing the content before and after the tabs, then you can slot the `tablist-wrapper` element. Bear in mind if you're supplying this element that: + + - `` will only manage slotting of `role=panel`. It won't manage elements before or after the tabs or panels. + - You won't be able to also slot the `tablist-tab-wrapper`. You can chose to omit this element though. + - You must also supply a `role=tablist` as a descendant. + - You must ensure that all child elements of the tablist `role=tab` or `role=presentational`. + - The element will still slot contents before and after this element, in order to correctly present the tablist. + +```html + +
+
+ + +
+
+
+
+
+``` ## Browser support