-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from area17/tabs-components
[Draft] - Tabs components
- Loading branch information
Showing
15 changed files
with
494 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
import { createBehavior } from '@area17/a17-behaviors' | ||
import { customEvents } from '../constants/customEvents' | ||
|
||
/* | ||
Markup and JS taken from: | ||
https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/ | ||
*/ | ||
|
||
const Tabs = createBehavior( | ||
'Tabs', | ||
{ | ||
setSelectedTab(selectedTab, init) { | ||
if (this.current.tab === selectedTab) { | ||
return | ||
} | ||
|
||
let nextCurrentTab = { | ||
tab: selectedTab, | ||
tabpanel: document.getElementById( | ||
selectedTab.getAttribute('aria-controls') | ||
) | ||
} | ||
|
||
this.transitionTime = | ||
16 + | ||
parseInt( | ||
window | ||
.getComputedStyle(this.$node) | ||
.getPropertyValue('--tab-transition-time') || 200 | ||
) | ||
|
||
if ( | ||
this.$node.dataset.tabsImmediate === true || | ||
this.$node.dataset.tabsImmediate === 'true' | ||
) { | ||
this.transitionTime = 0 | ||
} | ||
|
||
this.current.tab.setAttribute('aria-selected', 'false') | ||
this.current.tab.tabIndex = -1 | ||
this.current.tabpanel.inert = true | ||
this.current.tabpanel.dispatchEvent(new CustomEvent(customEvents.TABS_HIDDEN)) | ||
document.dispatchEvent( | ||
new CustomEvent(customEvents.TABS_HIDDEN) | ||
) | ||
if (!init && this.options.scrollonclick) { | ||
this.$node.scrollIntoView(true) | ||
} | ||
|
||
setTimeout(() => { | ||
this.current.tabpanel.hidden = true | ||
|
||
nextCurrentTab.tab.setAttribute('aria-selected', 'true') | ||
nextCurrentTab.tab.removeAttribute('tabindex') | ||
nextCurrentTab.tabpanel.hidden = false | ||
window.requestAnimationFrame(() => { | ||
nextCurrentTab.tabpanel.inert = false | ||
nextCurrentTab.tabpanel.dispatchEvent( | ||
new CustomEvent(customEvents.TABS_SHOWN) | ||
) | ||
document.dispatchEvent( | ||
new CustomEvent(customEvents.TABS_SHOWN, { detail: this.current }) | ||
) | ||
}) | ||
|
||
this.current = nextCurrentTab | ||
this.$node.dataset.tabsImmediate = false | ||
if (!init) { | ||
document.dispatchEvent( | ||
new CustomEvent(customEvents.TABS_OPENED, { detail: this.current }) | ||
) | ||
} | ||
}, this.transitionTime) | ||
}, | ||
moveFocusToTab(currentTab) { | ||
currentTab.focus() | ||
}, | ||
moveFocusToPreviousTab(currentTab) { | ||
if (currentTab === this.firstTab) { | ||
this.moveFocusToTab(this.lastTab) | ||
} else { | ||
const index = this.tabs.indexOf(currentTab) | ||
this.moveFocusToTab(this.tabs[index - 1]) | ||
} | ||
}, | ||
moveFocusToNextTab(currentTab) { | ||
if (currentTab === this.lastTab) { | ||
this.moveFocusToTab(this.firstTab) | ||
} else { | ||
const index = this.tabs.indexOf(currentTab) | ||
this.moveFocusToTab(this.tabs[index + 1]) | ||
} | ||
}, | ||
onKeydown(event) { | ||
const tgt = event.currentTarget | ||
let flag = false | ||
|
||
switch (event.key) { | ||
case 'ArrowLeft': | ||
this.moveFocusToPreviousTab(tgt) | ||
flag = true | ||
break | ||
|
||
case 'ArrowRight': | ||
this.moveFocusToNextTab(tgt) | ||
flag = true | ||
break | ||
|
||
case 'Home': | ||
this.moveFocusToTab(this.firstTab) | ||
flag = true | ||
break | ||
|
||
case 'End': | ||
this.moveFocusToTab(this.lastTab) | ||
flag = true | ||
break | ||
|
||
default: | ||
break | ||
} | ||
|
||
if (flag) { | ||
event.stopPropagation() | ||
event.preventDefault() | ||
} | ||
}, | ||
onClick(event) { | ||
event.preventDefault() | ||
event.stopPropagation() | ||
this.setSelectedTab(event.currentTarget, false) | ||
}, | ||
selectTab(event) { | ||
if (event && event.detail && event.detail.immediate === true) { | ||
this.$node.dataset.tabsImmediate = true | ||
} | ||
this.setSelectedTab(event.currentTarget, true) | ||
} | ||
}, | ||
{ | ||
init() { | ||
this.transitionTime = 216 | ||
this.options.scrollonclick = this.options.scrollonclick === 'true' | ||
|
||
this.tablistNode = this.$node | ||
|
||
this.tabs = [] | ||
|
||
this.firstTab = null | ||
this.lastTab = null | ||
|
||
this.firstTabPanel = null | ||
|
||
this.tabs = Array.from( | ||
this.tablistNode.querySelectorAll('[role=tab]') | ||
) | ||
this.tabpanels = [] | ||
|
||
this.tabs?.forEach((tab) => { | ||
const tabpanel = document.getElementById( | ||
tab.getAttribute('aria-controls') | ||
) | ||
|
||
if (!tab.getAttribute('aria-selected')) { | ||
tab.setAttribute('aria-selected', 'false') | ||
} | ||
|
||
this.tabpanels.push(tabpanel) | ||
|
||
tab.addEventListener('keydown', this.onKeydown) | ||
tab.addEventListener('click', this.onClick) | ||
tab.addEventListener('tabs:select', this.selectTab) | ||
|
||
if (!this.firstTab) { | ||
this.firstTab = tab | ||
this.firstTabPanel = tabpanel | ||
} | ||
this.lastTab = tab | ||
|
||
if (tab.getAttribute('aria-selected') === 'true') { | ||
this.current = { | ||
tab: tab, | ||
tabpanel: tabpanel | ||
} | ||
} else { | ||
tabpanel.hidden = true | ||
tabpanel.inert = true | ||
} | ||
}) | ||
|
||
if (!this.current) { | ||
this.current = { | ||
tab: this.firstTab, | ||
tabpanel: this.firstTabPanel | ||
} | ||
} | ||
|
||
this.$node.dataset.tabsImmediate = true | ||
this.setSelectedTab(this.current.tab, true) | ||
}, | ||
destroy() { | ||
this.tabs?.forEach((tab) => { | ||
tab.removeEventListener('keydown', this.onKeydown) | ||
tab.removeEventListener('click', this.onClick) | ||
tab.removeEventListener('tabs:select', this.selectTab) | ||
}) | ||
} | ||
} | ||
) | ||
|
||
export default Tabs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"base": "flex items-center gap-6", | ||
"button": "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"base": "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"base": "container", | ||
"title": "f-heading-03 w-full", | ||
"tabList": "" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<div {{ $attributes->class([$ui('tab-panel', 'base')]) }} | ||
@if($selected) data-panel-active="true" @else inert hidden @endif | ||
id="{{ $name.'-panel-'.$index }}" | ||
data-Tabs-panel="" | ||
role="tabpanel" | ||
aria-labelledby="{{ $name.'-'.$index }}"> | ||
{{ $slot }} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<ul role="tablist" | ||
{{ $attributes->class([$ui('tab-list', 'base')]) }} | ||
@if ($tabListId) aria-labelledby="{{ $tabListId }}" | ||
@elseif($ariaLabel) aria-label="{{ $ariaLabel }}" @endif> | ||
|
||
@foreach ($tabsNames as $tabName) | ||
<li @if (count($tabsNames) == 1) role="presentation" @endif> | ||
<x-vui-button id="{{ $name . '-' . $loop->index }}" | ||
data-Tabs-button="" | ||
role="tab" | ||
aria-controls="{{ $name . '-panel-' . $loop->index }}" | ||
aria-selected="{{ $loop->index === $selectedIndex ? 'true' : 'false' }}" | ||
:class="$ui('tab-list', 'button')" | ||
:variant="$tabButtonVariant"> | ||
{{ $tabName }} | ||
</x-vui-button> | ||
</li> | ||
@endforeach | ||
</ul> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<div {{ $attributes->class([$ui('tabs', 'base')]) }} data-behavior="Tabs"> | ||
|
||
@if($title) | ||
<x-vui-heading :class="$ui('tabs', 'title')" :id="$tabListId" :level="$titleLevel">{{ $title }}</x-vui-heading> | ||
@endif | ||
|
||
<x-vui-tabs-list :class="$ui('tabs', 'tablist')" | ||
:tabs-names="$tabsNames" | ||
:name="$name" | ||
:tab-button-variant="$tabButtonVariant" | ||
:tabListId="isset($title) ? $tabListId : null"/> | ||
|
||
{{ $slot }} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
The tabs component renders a list with button to show/hide an active content defined into the slot. | ||
Each component must be defined using the tabs-panel component with the same $name so ids are matching between panels and buttons. | ||
|
||
In the example below, the `$tabNames` array will define the label text for each button and the `$contents` Array will define the inner content for each panel. | ||
|
||
## Usage | ||
|
||
```html | ||
<x-vui-tabs | ||
:title="$title" | ||
:name="$name" | ||
:tabs-names="$tabsNames" | ||
:title-level="$titleLevel" | ||
> | ||
@foreach ($contents as $content) | ||
<x-vui-tabs-panel :name="$name" | ||
:index="$loop->index" | ||
:selected="$loop->first"> | ||
{{ $content }} | ||
</x-vui-tabs-panel> | ||
@endforeach | ||
</x-vui-tabs> | ||
``` | ||
|
||
## Accessibility | ||
|
||
The markup and the JS behavior are based on https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/ | ||
Hidden panels are not focusable, when selecting a tab the content is getting focused. | ||
|
||
You can navigate using keyboard : | ||
|
||
`Right Arrow`: | ||
When a tab has focus: Moves focus to the next tab. | ||
If focus is on the last tab, moves focus to the first tab. | ||
|
||
`Left Arrow`: | ||
When a tab has focus: | ||
Moves focus to the previous tab. | ||
If focus is on the first tab, moves focus to the last tab. | ||
|
||
## Theming | ||
|
||
### Config | ||
|
||
```json | ||
{ | ||
"base": "container", | ||
"title": "f-heading-03 w-full", | ||
"tabList": "" | ||
} | ||
``` | ||
|
||
`title`: | ||
Style of the main tab component title | ||
|
||
`tabList`: | ||
Visual styling for the wraper element for the list of tab buttons | ||
|
||
### Config for tab panel | ||
|
||
```json | ||
{ | ||
"base": "" | ||
} | ||
``` | ||
|
||
You can also set some default styling for the tab panels wrapper div. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
@storybook([ | ||
'status' => 'readyForQA', | ||
'layout' => 'fullscreen', | ||
'args' => [ | ||
'title' => 'Tabs', | ||
'name' => 'faq', | ||
'tabsNames' => ['Tab 1', 'Tab 2', 'Tab 3'], | ||
'titleLevel' => 3, | ||
'contents' => ['<p>Content 1: Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo autem cum voluptatibus exercitationem ea explicabo eum deleniti repudiandae alias delectus nam minima, vel totam consectetur officiis ex! Reprehenderit, sunt accusamus.</p>', '<p>Content 2: Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo autem cum voluptatibus exercitationem ea explicabo eum deleniti repudiandae alias delectus nam minima, vel totam consectetur officiis ex! Reprehenderit, sunt accusamus.</p>', '<p>Content 3: Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo autem cum voluptatibus exercitationem ea explicabo eum deleniti repudiandae alias delectus nam minima, vel totam consectetur officiis ex! Reprehenderit, sunt accusamus.</p>'], | ||
], | ||
]) | ||
|
||
<x-vui-tabs :title="$title" | ||
:name="$name" | ||
:tabs-names="$tabsNames" | ||
:title-level="$titleLevel"> | ||
@foreach ($contents as $content) | ||
<x-vui-tabs-panel :name="$name" | ||
:index="$loop->index" | ||
:selected="$loop->first"> | ||
{!! $content !!} | ||
</x-vui-tabs-panel> | ||
@endforeach | ||
</x-vui-tabs> |
Oops, something went wrong.