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
|
||||
- SkeletonPlaceholder
|
||||
- SkeletonText
|
||||
- Tabs
|
||||
- Tab
|
||||
- TabContent
|
||||
- TabsSkeleton
|
||||
- Tag
|
||||
- TagSkeleton
|
||||
- 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 SkeletonPlaceholder from './components/SkeletonPlaceholder';
|
||||
import SkeletonText from './components/SkeletonText';
|
||||
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';
|
||||
|
@ -66,6 +67,10 @@ export {
|
|||
SkeletonPlaceholder,
|
||||
SkeletonText,
|
||||
Switch,
|
||||
Tabs,
|
||||
Tab,
|
||||
TabContent,
|
||||
TabsSkeleton,
|
||||
Tag,
|
||||
TagSkeleton,
|
||||
TextArea,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue