mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-14 18:01:06 +00:00
parent
b8c2a105a5
commit
4ba8df4425
9 changed files with 331 additions and 0 deletions
|
@ -38,6 +38,10 @@ Currently, the following components are supported:
|
||||||
- SearchSkeleton
|
- SearchSkeleton
|
||||||
- SkeletonPlaceholder
|
- SkeletonPlaceholder
|
||||||
- SkeletonText
|
- SkeletonText
|
||||||
|
- Tabs
|
||||||
|
- Tab
|
||||||
|
- TabContent
|
||||||
|
- TabsSkeleton
|
||||||
- Tag
|
- Tag
|
||||||
- TagSkeleton
|
- TagSkeleton
|
||||||
- TextArea
|
- TextArea
|
||||||
|
|
69
src/components/Tabs/Tab.svelte
Normal file
69
src/components/Tabs/Tab.svelte
Normal 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>
|
20
src/components/Tabs/TabContent.svelte
Normal file
20
src/components/Tabs/TabContent.svelte
Normal 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>
|
43
src/components/Tabs/Tabs.Story.svelte
Normal file
43
src/components/Tabs/Tabs.Story.svelte
Normal 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>
|
45
src/components/Tabs/Tabs.stories.js
Normal file
45
src/components/Tabs/Tabs.stories.js
Normal 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' } });
|
114
src/components/Tabs/Tabs.svelte
Normal file
114
src/components/Tabs/Tabs.svelte
Normal 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" />
|
25
src/components/Tabs/TabsSkeleton.svelte
Normal file
25
src/components/Tabs/TabsSkeleton.svelte
Normal 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')}> </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')}> </div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
6
src/components/Tabs/index.js
Normal file
6
src/components/Tabs/index.js
Normal 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';
|
|
@ -16,6 +16,7 @@ import RadioButton, { RadioButtonSkeleton } from './components/RadioButton';
|
||||||
import Search, { SearchSkeleton } from './components/Search';
|
import Search, { SearchSkeleton } from './components/Search';
|
||||||
import SkeletonPlaceholder from './components/SkeletonPlaceholder';
|
import SkeletonPlaceholder from './components/SkeletonPlaceholder';
|
||||||
import SkeletonText from './components/SkeletonText';
|
import SkeletonText from './components/SkeletonText';
|
||||||
|
import Tabs, { Tab, TabContent, TabsSkeleton } from './components/Tabs';
|
||||||
import Tag, { TagSkeleton } from './components/Tag';
|
import Tag, { TagSkeleton } from './components/Tag';
|
||||||
import TextArea, { TextAreaSkeleton } from './components/TextArea';
|
import TextArea, { TextAreaSkeleton } from './components/TextArea';
|
||||||
import TextInput, { TextInputSkeleton, PasswordInput } from './components/TextInput';
|
import TextInput, { TextInputSkeleton, PasswordInput } from './components/TextInput';
|
||||||
|
@ -66,6 +67,10 @@ export {
|
||||||
SkeletonPlaceholder,
|
SkeletonPlaceholder,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
Switch,
|
Switch,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
TabContent,
|
||||||
|
TabsSkeleton,
|
||||||
Tag,
|
Tag,
|
||||||
TagSkeleton,
|
TagSkeleton,
|
||||||
TextArea,
|
TextArea,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue