Skip to content

Commit

Permalink
Merge pull request #91 from github/unmanaged-slots
Browse files Browse the repository at this point in the history
Unmanaged slots
  • Loading branch information
keithamus authored Apr 24, 2024
2 parents 66334bc + ac3a409 commit c70a9d0
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 34 deletions.
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,70 @@ In those cases, apply `data-tab-container-no-tabstop` to the `tabpanel` element.
</div>
</tab-container>
```
### Unmanaged slots

`<tab-container>` 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 `<slot>`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 `<tab-container>` 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
<tab-container>
<div role="tablist">
<my-icon name="tabs" role="presentational"></my-icon>
<button type="button" role="tab">Tab one</button>
<button type="button" role="tab">Tab two</button>
</div>
<div role="tabpanel"></div>
<div role="tabpanel"></div>
</tab-container>
```

#### 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
<tab-container>
<div slot="tablist-tab-wrapper">
<div role="tablist">
<button type="button" role="tab">Tab one</button>
<button type="button" role="tab">Tab two</button>
</div>
</div>
<div role="tabpanel"></div>
<div role="tabpanel"></div>
</tab-container>
```
#### 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:

- `<tab-container>` 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
<tab-container>
<div slot="tablist-wrapper">
<div role="tablist">
<button type="button" role="tab">Tab one</button>
<button type="button" role="tab">Tab two</button>
</div>
</div>
<div role="tabpanel"></div>
<div role="tabpanel"></div>
</tab-container>
```

## Browser support

Expand Down
6 changes: 6 additions & 0 deletions custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,12 @@
"privacy": "private",
"readonly": true
},
{
"kind": "field",
"name": "#tabListWrapper",
"privacy": "private",
"readonly": true
},
{
"kind": "field",
"name": "#tabListTabWrapper",
Expand Down
21 changes: 21 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,27 @@ <h2>Horizontal (custom tablist)</h2>

<h2>Horizontal (custom tablist and tablist-wrapper)</h2>

<tab-container>
<div slot="tablist-wrapper">
<div role="tablist" aria-label="Horizontal Tabs Example">
<button type="button" id="tab-one" role="tab">Tab one</button>
<button type="button" id="tab-two" role="tab">Tab two</button>
<button type="button" id="tab-three" role="tab">Tab three</button>
</div>
</div>
<div role="tabpanel" aria-labelledby="tab-one">
Panel 1
</div>
<div role="tabpanel" aria-labelledby="tab-two" hidden>
Panel 2
</div>
<div role="tabpanel" aria-labelledby="tab-three" hidden>
Panel 3
</div>
</tab-container>

<h2>Horizontal (custom tablist and tablist-tab-wrapper)</h2>

<tab-container>
<div slot="tablist-tab-wrapper">
<div role="tablist" aria-label="Horizontal Tabs Example">
Expand Down
89 changes: 55 additions & 34 deletions src/tab-container-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -103,6 +107,10 @@ export class TabContainerElement extends HTMLElement {
}
}

get #tabListWrapper() {
return this.shadowRoot!.querySelector<HTMLSlotElement>('slot[part="tablist-wrapper"]')!
}

get #tabListTabWrapper() {
return this.shadowRoot!.querySelector<HTMLSlotElement>('slot[part="tablist-tab-wrapper"]')!
}
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,57 @@ describe('tab-container', function () {
)
})
})

describe('with custom tablist-wrapper', function () {
beforeEach(function () {
document.body.innerHTML = `
<tab-container>
<div slot="tablist-wrapper">
<div role="tablist">
<button type="button" role="tab">Tab one</button>
<button type="button" role="tab" aria-selected="true">Tab two</button>
<button type="button" role="tab">Tab three</button>
</div>
</div>
<div role="tabpanel" hidden>
Panel 1
</div>
<div role="tabpanel">
Panel 2
</div>
<div role="tabpanel" hidden data-tab-container-no-tabstop>
Panel 3
</div>
</tab-container>
`
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 = `
Expand Down

0 comments on commit c70a9d0

Please sign in to comment.