Merge pull request #65 from metonym/tabs

feat(components): add Tabs
This commit is contained in:
Eric Liu 2019-12-22 05:57:51 -08:00 committed by GitHub
commit 766bb26f2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 331 additions and 0 deletions

View file

@ -61,6 +61,10 @@ Currently, the following components are supported:
- StructuredListRow
- StructuredListInput
- StructuredListWrapper
- Tabs
- Tab
- TabContent
- TabsSkeleton
- Tag
- TagSkeleton
- TextArea

View file

@ -0,0 +1,69 @@
<script>
// TODO: fix space not selecting
let className = undefined;
export { className as class };
export let role = 'presentation';
export let label = '';
export let tabindex = '0';
export let href = '#';
export let disabled = false;
export let style = undefined;
import { getContext } from 'svelte';
import { cx } from '../../lib';
const id = Math.random();
const { selectedTab, add, update, change } = getContext('Tabs');
let anchorRef = undefined;
add({ id, label, disabled });
$: selected = $selectedTab === id;
$: if (selected && anchorRef) {
anchorRef.focus();
}
$: _class = cx(
'--tabs__nav-item',
disabled && '--tabs__nav-item--disabled',
selected && '--tabs__nav-item--selected',
className
);
</script>
<li
tabindex="-1"
class={_class}
on:click|preventDefault={() => {
if (!disabled) {
update(id);
}
}}
on:mouseover
on:mouseenter
on:mouseleave
on:keydown={event => {
if (disabled) {
return;
}
if (event.key === 'ArrowRight') {
change(1);
} else if (event.key === 'ArrowLeft') {
change(-1);
} else if (event.key === ' ' || event.key === 'Enter') {
update(id);
}
}}
{role}
{style}>
<a
bind:this={anchorRef}
role="tab"
class={cx('--tabs__nav-link')}
tabindex={disabled ? '-1' : tabindex}
aria-selected={selected}
aria-disabled={disabled}
{href}>
{label}
</a>
</li>

View file

@ -0,0 +1,20 @@
<script>
let className = undefined;
export { className as class };
export let style = undefined;
import { getContext } from 'svelte';
import { cx } from '../../lib';
const _class = cx('--tab-content', className);
const id = Math.random();
const { selectedContent, addContent } = getContext('Tabs');
addContent({ id });
$: selected = $selectedContent === id;
</script>
<div class={_class} aria-hidden={!selected} hidden={!selected} {style}>
<slot />
</div>

View file

@ -0,0 +1,43 @@
<script>
export let story = undefined;
import Layout from '../../internal/ui/Layout.svelte';
import Tabs from './Tabs.svelte';
import Tab from './Tab.svelte';
import TabContent from './TabContent.svelte';
import TabsSkeleton from './TabsSkeleton.svelte';
const { tabProps, ...tabsProps } = $$props;
let selected = 0;
</script>
<Layout>
{#if story === 'skeleton'}
<TabsSkeleton />
{:else if story === 'container'}
<Tabs {...tabsProps} type="container" bind:selected>
<Tab {...tabProps} label="Tab label 1" />
<Tab {...tabProps} label="Tab label 2" />
<Tab {...tabProps} label="Tab label 3" />
<div slot="content">
<TabContent>Content 1</TabContent>
<TabContent>Content 2</TabContent>
<TabContent>Content 3</TabContent>
</div>
</Tabs>
{:else}
<Tabs {...tabsProps} bind:selected>
<Tab {...tabProps} label="Tab label 1" />
<Tab {...tabProps} label="Tab label 2" />
<Tab {...tabProps} label="Tab label 3" disabled />
<Tab {...tabProps} label="Tab label 4" />
<div slot="content">
<TabContent>Content 1</TabContent>
<TabContent>Content 2</TabContent>
<TabContent>Content 3</TabContent>
<TabContent>Content 4</TabContent>
</div>
</Tabs>
{/if}
</Layout>

View file

@ -0,0 +1,45 @@
import { withKnobs, boolean, number, text } from '@storybook/addon-knobs';
import Component from './Tabs.Story.svelte';
export default { title: 'Tabs', decorators: [withKnobs] };
export const Default = () => ({
Component,
props: {
tabProps: {
disabled: boolean('Disabled (disabled in <Tab>)', false),
href: text('The href for tab (href in <Tab>)', '#'),
role: text('ARIA role (role in <Tab>)', 'presentation'),
tabindex: number('Tab index (tabindex in <Tab>)', 0)
},
tabsProps: {
className: 'some-class',
selected: number('The index of the selected tab (selected in <Tabs>)', 1),
triggerHref: text('The href of trigger button for narrow mode (triggerHref in <Tabs>)', '#'),
role: text('ARIA role (role in <Tabs>)', 'navigation'),
iconDescription: text(
'The description of the trigger icon for narrow mode (iconDescription in <Tabs>)',
'show menu options'
),
tabContentClassName: text(
'The className for the child `<TabContent>` components',
'tab-content'
)
}
}
});
export const Container = () => ({
Component,
props: {
story: 'container',
tabProps: {
disabled: boolean('Disabled (disabled in <Tab>)', false),
href: text('The href for tab (href in <Tab>)', '#'),
role: text('ARIA role (role in <Tab>)', 'presentation'),
tabindex: number('Tab index (tabindex in <Tab>)', 0)
}
}
});
export const Skeleton = () => ({ Component, props: { story: 'skeleton' } });

View file

@ -0,0 +1,114 @@
<script>
let className = undefined;
export { className as class };
export let selected = 0;
export let iconDescription = 'Show menu options';
export let role = 'navigation';
export let type = 'default';
export let triggerHref = '#';
export let style = undefined;
import { createEventDispatcher, setContext } from 'svelte';
import { writable, derived } from 'svelte/store';
import ChevronDownGlyph from 'carbon-icons-svelte/lib/ChevronDownGlyph';
import { cx } from '../../lib';
const dispatch = createEventDispatcher();
const _class = cx('--tabs', type === 'container' && '--tabs--container', className);
let dropdownHidden = true;
let tabs = writable([]);
let tabsById = derived(tabs, _ => _.reduce((a, c) => ({ ...a, [c.id]: c }), {}));
let currentIndex = selected;
let selectedTab = writable(undefined);
let content = writable([]);
let selectedContent = writable(undefined);
setContext('Tabs', {
selectedTab,
selectedContent,
add: data => {
tabs.update(_ => [..._, { ...data, index: _.length }]);
},
addContent: data => {
content.update(_ => [..._, { ...data, index: _.length }]);
},
update: id => {
currentIndex = $tabsById[id].index;
},
change: direction => {
let index = currentIndex + direction;
if (index < 0) {
index = $tabs.length - 1;
} else if (index >= $tabs.length) {
index = 0;
}
let disabled = $tabs[index].disabled;
while (disabled) {
index = index + direction;
if (index < 0) {
index = $tabs.length - 1;
} else if (index >= $tabs.length) {
index = 0;
}
disabled = $tabs[index].disabled;
}
currentIndex = index;
}
});
$: currentTab = $tabs[currentIndex] || undefined;
$: currentContent = $content[currentIndex] || undefined;
$: {
selected = currentIndex;
dispatch('change', currentIndex);
if (currentTab) {
selectedTab.set(currentTab.id);
}
if (currentContent) {
selectedContent.set(currentContent.id);
}
}
$: if ($selectedTab) {
dropdownHidden = true;
}
$: _listClass = cx('--tabs__nav', dropdownHidden && '--tabs__nav--hidden');
</script>
<div class={_class} {style} {role}>
<div
role="listbox"
tabindex="0"
class={cx('--tabs-trigger')}
aria-label={$$props['aria-label'] || 'listbox'}
on:click={() => {
dropdownHidden = !dropdownHidden;
}}
on:keypress
on:keypress={() => {
dropdownHidden = !dropdownHidden;
}}>
<a
tabindex="-1"
class={cx('--tabs-trigger-text')}
href={triggerHref}
on:click
on:click={() => {
dropdownHidden = !dropdownHidden;
}}>
{#if currentTab}{currentTab.label}{/if}
</a>
<ChevronDownGlyph aria-hidden="true" title={iconDescription} />
</div>
<ul role="tablist" class={_listClass}>
<slot />
</ul>
</div>
<slot name="content" />

View file

@ -0,0 +1,25 @@
<script>
let className = undefined;
export { className as class };
export let style = undefined;
import { cx } from '../../lib';
const _class = cx('--tabs', '--skeleton', className);
</script>
<div on:click on:mouseover on:mouseenter on:mouseleave class={_class} {style}>
<div class={cx('--tabs-trigger')}>
<div class={cx('--tabs-trigger-text')}>&nbsp;</div>
<svg width="10" height="5" viewBox="0 0 10 5" fillRule="evenodd">
<path d="M10 0L5 5 0 0z" />
</svg>
</div>
<ul class={cx('--tabs__nav', '--tabs__nav--hidden')}>
{#each [0, 1, 2, 3] as item, i (item)}
<li class={cx('--tabs__nav-item')}>
<div class={cx('--tabs__nav-link')}>&nbsp;</div>
</li>
{/each}
</ul>
</div>

View file

@ -0,0 +1,6 @@
import Tabs from './Tabs.svelte';
export default Tabs;
export { default as Tab } from './Tab.svelte';
export { default as TabContent } from './TabContent.svelte';
export { default as TabsSkeleton } from './TabsSkeleton.svelte';

View file

@ -39,6 +39,7 @@ import {
StructuredListInput,
StructuredListWrapper
} from './components/StructuredList';
import Tabs, { Tab, TabContent, TabsSkeleton } from './components/Tabs';
import Tag, { TagSkeleton } from './components/Tag';
import TextArea, { TextAreaSkeleton } from './components/TextArea';
import TextInput, { TextInputSkeleton, PasswordInput } from './components/TextInput';
@ -109,6 +110,10 @@ export {
StructuredListRow,
StructuredListInput,
StructuredListWrapper,
Tabs,
Tab,
TabContent,
TabsSkeleton,
Tag,
TagSkeleton,
TextArea,