feat(tabs): proof of concept for keyed tabs

This commit is contained in:
Eric Liu 2020-11-29 15:00:22 -08:00
commit 43f06f2be3
8 changed files with 466 additions and 3 deletions

View file

@ -1,6 +1,6 @@
# Component Index
> 155 components exported from carbon-components-svelte@0.25.1.
> 156 components exported from carbon-components-svelte@0.25.1.
## Components
@ -134,6 +134,7 @@
- [`TableRow`](#tablerow)
- [`Tabs`](#tabs)
- [`TabsSkeleton`](#tabsskeleton)
- [`TabsV2`](#tabsv2)
- [`Tag`](#tag)
- [`TagSkeleton`](#tagskeleton)
- [`TextArea`](#textarea)
@ -3582,6 +3583,44 @@ None.
| mouseenter | forwarded | -- |
| mouseleave | forwarded | -- |
## `TabsV2`
### Types
```ts
export type TabsV2ItemId = number | string;
export interface TabsV2Item {
id: TabsV2ItemId;
label?: string;
disabled?: boolean;
}
```
### Props
| Prop name | Kind | Reactive | Type | Default value | Description |
| :-------------- | :--------------- | :------- | :---------------------------------------- | -------------------------------- | ------------------------------------------- |
| selectedId | <code>let</code> | Yes | <code>TabsV2ItemId</code> | -- | Specify the selected tab id |
| selectedIndex | <code>let</code> | Yes | <code>number</code> | <code>0</code> | Specify the selected tab index |
| items | <code>let</code> | No | <code>TabsV2Item[]</code> | <code>[]</code> | Provide the tab items |
| type | <code>let</code> | No | <code>"default" &#124; "container"</code> | <code>"default"</code> | Specify the type of tabs |
| iconDescription | <code>let</code> | No | <code>string</code> | <code>"Show menu options"</code> | Specify the ARIA label for the chevron icon |
| triggerHref | <code>let</code> | No | <code>string</code> | <code>"#"</code> | Specify the tab trigger href attribute |
### Slots
| Slot name | Default | Props | Fallback |
| :-------- | :------ | :------------------------------------------------------------------- | :------------------------ |
| -- | Yes | <code>{ id: TabsV2ItemId; index: number; item: TabsV2Item; } </code> | -- |
| tab | No | -- | <code>{item.label}</code> |
### Events
| Event name | Type | Detail |
| :--------- | :--------- | :---------------------------------------------------------------------------------------- |
| change | dispatched | <code>{ selectedIndex: number; selectedId: TabsV2ItemId; currentItem: TabsV2Item }</code> |
## `Tag`
### Props

View file

@ -1,5 +1,5 @@
{
"total": 155,
"total": 156,
"components": [
{
"moduleName": "SkeletonText",
@ -7637,6 +7637,104 @@
"typedefs": [],
"rest_props": { "type": "Element", "name": "div" }
},
{
"moduleName": "TabsV2",
"filePath": "/src/Tabs/TabsV2.svelte",
"props": [
{
"name": "items",
"kind": "let",
"description": "Provide the tab items",
"type": "TabsV2Item[]",
"value": "[]",
"isFunction": false,
"constant": false,
"reactive": false
},
{
"name": "selectedIndex",
"kind": "let",
"description": "Specify the selected tab index",
"type": "number",
"value": "0",
"isFunction": false,
"constant": false,
"reactive": true
},
{
"name": "selectedId",
"kind": "let",
"description": "Specify the selected tab id",
"type": "TabsV2ItemId",
"isFunction": false,
"constant": false,
"reactive": true
},
{
"name": "type",
"kind": "let",
"description": "Specify the type of tabs",
"type": "\"default\" | \"container\"",
"value": "\"default\"",
"isFunction": false,
"constant": false,
"reactive": false
},
{
"name": "iconDescription",
"kind": "let",
"description": "Specify the ARIA label for the chevron icon",
"type": "string",
"value": "\"Show menu options\"",
"isFunction": false,
"constant": false,
"reactive": false
},
{
"name": "triggerHref",
"kind": "let",
"description": "Specify the tab trigger href attribute",
"type": "string",
"value": "\"#\"",
"isFunction": false,
"constant": false,
"reactive": false
}
],
"slots": [
{
"name": "__default__",
"default": true,
"slot_props": "{ id: TabsV2ItemId; index: number; item: TabsV2Item; }"
},
{
"name": "tab",
"default": false,
"fallback": "{item.label}",
"slot_props": "{}"
}
],
"events": [
{
"type": "dispatched",
"name": "change",
"detail": "{ selectedIndex: number; selectedId: TabsV2ItemId; currentItem: TabsV2Item }"
}
],
"typedefs": [
{
"type": "number | string",
"name": "TabsV2ItemId",
"ts": "type TabsV2ItemId = number | string"
},
{
"type": "{ id: TabsV2ItemId; label?: string; disabled?: boolean; }",
"name": "TabsV2Item",
"ts": "interface TabsV2Item { id: TabsV2ItemId; label?: string; disabled?: boolean; }"
}
],
"rest_props": { "type": "Element", "name": "div" }
},
{
"moduleName": "TagSkeleton",
"filePath": "/src/Tag/TagSkeleton.svelte",

View file

@ -0,0 +1,86 @@
<script>
import { TabsV2, Button } from "carbon-components-svelte";
import Add16 from "carbon-icons-svelte/lib/Add16";
import { tick } from "svelte";
const initialItems = [
{ id: "id" + 0, label: "Tab 1" },
{ id: "id" + 1, label: "Tab 2", disabled: true },
{ id: "id" + 3, label: "Tab 3" },
];
const differentItems = [
{ id: "id" + -1, label: "Diff Tab 0" },
{ id: "id" + 0, label: "Diff Tab 1" },
{ id: "id" + 1, label: "Diff Tab 2" },
{ id: "id" + 3, label: "Diff Tab 3" },
{ id: "id" + 4, label: "Diff Tab 4" },
];
let selectedIndex;
let selectedId;
let items = initialItems;
</script>
<div><strong>selectedIndex:</strong> {selectedIndex}</div>
<div><strong>selectedId:</strong> {selectedId}</div>
<Button
on:click="{async () => {
items = differentItems;
tick().then(() => {
selectedId = 'id4';
});
}}"
>
Update items
</Button>
<Button
on:click="{() => {
items = initialItems;
}}"
>
Reset
</Button>
<Button
on:click="{() => {
selectedIndex = 1;
}}"
>
Update selectedIndex
</Button>
<Button
on:click="{() => {
selectedId = 'id3';
}}"
>
Update selectedId
</Button>
<TabsV2
bind:selectedId
bind:selectedIndex
items="{items}"
let:item
let:id
let:index
on:change="{(e) => {
console.log('change', e.detail);
}}"
>
<span slot="tab" style="{!item.disabled && 'color: blue'}">
{#if index === 1}
<Add16 />
{/if}
{item.label}
</span>
{#if selectedIndex === 0}Tab content {id} {index}{/if}
{#if selectedIndex === 1}Tab content {id} {index}{/if}
{#if selectedIndex === 2}Tab content {id} {index}{/if}
{#if selectedIndex === 3}Tab content {id} {index}{/if}
{#if selectedIndex === 4}Tab content {id} {index}{/if}
</TabsV2>

178
src/Tabs/TabsV2.svelte Normal file
View file

@ -0,0 +1,178 @@
<script>
/**
* @typedef {number | string} TabsV2ItemId
* @typedef {{ id: TabsV2ItemId; label?: string; disabled?: boolean; }} TabsV2Item
* @event {{ selectedIndex: number; selectedId: TabsV2ItemId; currentItem: TabsV2Item }} change
* @slot {{ id: TabsV2ItemId; index: number; item: TabsV2Item; }}
*/
/**
* Provide the tab items
* @type {TabsV2Item[]}
*/
export let items = [];
/** Specify the selected tab index */
export let selectedIndex = 0;
/**
* Specify the selected tab id
* @type {TabsV2ItemId}
*/
export let selectedId = undefined;
/**
* Specify the type of tabs
* @type {"default" | "container"}
*/
export let type = "default";
/**
* Specify the ARIA label for the chevron icon
* @type {string}
*/
export let iconDescription = "Show menu options";
/** Specify the tab trigger href attribute */
export let triggerHref = "#";
import { createEventDispatcher, afterUpdate } from "svelte";
import ChevronDownGlyph from "carbon-icons-svelte/lib/ChevronDownGlyph/ChevronDownGlyph.svelte";
const dispatch = createEventDispatcher();
let dropdownHidden = true;
let prevSelectedIndex = -1;
$: itemIds = items.map((item) => item.id);
$: if (items[selectedIndex] === undefined) {
// if the items array shrinks, `selectedIndex` could be greater than the number of items
selectedIndex = items.length - 1;
}
$: currentItem = items[selectedIndex];
$: selectedId = currentItem.id;
afterUpdate(() => {
if (currentItem.id !== selectedId) {
selectedIndex = itemIds.indexOf(selectedId);
}
if (prevSelectedIndex !== selectedIndex) {
// only dispatch the "change" event when the current item changes
dispatch("change", { selectedIndex, selectedId, currentItem });
prevSelectedIndex = selectedIndex;
}
});
function changeIndex(direction) {
let index = selectedIndex + direction;
if (index < 0) {
index = items.length - 1;
} else if (index >= items.length) {
index = 0;
}
let disabled = items[index].disabled;
while (disabled) {
index = index + direction;
if (index < 0) {
index = items.length - 1;
} else if (index >= items.length) {
index = 0;
}
disabled = items[index].disabled;
}
selectedIndex = index;
}
</script>
<div
role="navigation"
class:bx--tabs="{true}"
class:bx--tabs--container="{type === 'container'}"
{...$$restProps}
>
<div
role="listbox"
tabindex="0"
class:bx--tabs-trigger="{true}"
aria-label="{$$props['aria-label'] || 'listbox'}"
on:click="{() => {
dropdownHidden = !dropdownHidden;
}}"
on:keydown="{() => {
dropdownHidden = !dropdownHidden;
}}"
>
<a
tabindex="-1"
class:bx--tabs-trigger-text="{true}"
href="{triggerHref}"
on:click="{() => {
dropdownHidden = !dropdownHidden;
}}"
>
{#if currentItem}{currentItem.label}{/if}
</a>
<ChevronDownGlyph aria-hidden="true" title="{iconDescription}" />
</div>
<ul
role="tablist"
class:bx--tabs__nav="{true}"
class:bx--tabs__nav--hidden="{dropdownHidden}"
>
{#each items as item, i (item.id)}
<li
tabindex="-1"
role="presentation"
class:bx--tabs__nav-item="{true}"
class:bx--tabs__nav-item--disabled="{item.disabled}"
class:bx--tabs__nav-item--selected="{item.id === currentItem.id}"
{...$$restProps}
on:click|preventDefault="{() => {
if (!item.disabled) selectedIndex = i;
dropdownHidden = true;
}}"
on:keydown="{({ key }) => {
if (item.disabled || !dropdownHidden) return;
console.log(key);
if (key === 'ArrowRight') {
changeIndex(1);
} else if (key === 'ArrowLeft') {
changeIndex(-1);
}
}}"
>
<a
role="tab"
tabindex="{item.disabled ? '-1' : item.tabindex || '0'}"
aria-selected="{item.id === currentItem.id}"
aria-disabled="{item.disabled}"
id="{item.id}"
href="{item.href || '#'}"
class:bx--tabs__nav-link="{true}"
>
<slot name="tab">{item.label}</slot>
</a>
</li>
{/each}
</ul>
</div>
{#each items as item, i (item.id)}
<div
role="tabpanel"
aria-labelledby="{item.id}"
aria-hidden="{item.id !== currentItem.id}"
hidden="{item.id === currentItem.id ? undefined : 'true'}"
id="tabpanel-{item.id}"
class:bx--tab-content="{true}"
>
<slot id="{item.id}" index="{i}" item="{item}" />
</div>
{/each}

View file

@ -2,3 +2,4 @@ export { default as Tabs } from "./Tabs.svelte";
export { default as Tab } from "./Tab.svelte";
export { default as TabContent } from "./TabContent.svelte";
export { default as TabsSkeleton } from "./TabsSkeleton.svelte";
export { default as TabsV2 } from "./TabsV2.svelte";

View file

@ -96,7 +96,7 @@ export {
StructuredListRow,
StructuredListInput,
} from "./StructuredList";
export { Tabs, Tab, TabContent, TabsSkeleton } from "./Tabs";
export { Tabs, Tab, TabContent, TabsSkeleton, TabsV2 } from "./Tabs";
export { Tag, TagSkeleton } from "./Tag";
export { TextArea, TextAreaSkeleton } from "./TextArea";
export { TextInput, TextInputSkeleton, PasswordInput } from "./TextInput";

60
types/Tabs/TabsV2.d.ts vendored Normal file
View file

@ -0,0 +1,60 @@
/// <reference types="svelte" />
export type TabsV2ItemId = number | string;
export interface TabsV2Item {
id: TabsV2ItemId;
label?: string;
disabled?: boolean;
}
export interface TabsV2Props extends svelte.JSX.HTMLAttributes<HTMLElementTagNameMap["div"]> {
/**
* Provide the tab items
* @default []
*/
items?: TabsV2Item[];
/**
* Specify the selected tab index
* @default 0
*/
selectedIndex?: number;
/**
* Specify the selected tab id
*/
selectedId?: TabsV2ItemId;
/**
* Specify the type of tabs
* @default "default"
*/
type?: "default" | "container";
/**
* Specify the ARIA label for the chevron icon
* @default "Show menu options"
*/
iconDescription?: string;
/**
* Specify the tab trigger href attribute
* @default "#"
*/
triggerHref?: string;
}
export default class TabsV2 {
$$prop_def: TabsV2Props;
$$slot_def: {
default: { id: TabsV2ItemId; index: number; item: TabsV2Item };
tab: {};
};
$on(
eventname: "change",
cb: (event: CustomEvent<{ selectedIndex: number; selectedId: TabsV2ItemId; currentItem: TabsV2Item }>) => void
): () => void;
$on(eventname: string, cb: (event: Event) => void): () => void;
}

1
types/index.d.ts vendored
View file

@ -110,6 +110,7 @@ export { default as Tabs } from "./Tabs/Tabs";
export { default as Tab } from "./Tabs/Tab";
export { default as TabContent } from "./Tabs/TabContent";
export { default as TabsSkeleton } from "./Tabs/TabsSkeleton";
export { default as TabsV2 } from "./Tabs/TabsV2";
export { default as TagSkeleton } from "./Tag/TagSkeleton";
export { default as Tag } from "./Tag/Tag";
export { default as TextArea } from "./TextArea/TextArea";