feat(components): add Tile

Supports #34

TODO:

- remove exported props
- compose TileGroup, SelectableTile, RadioTile components
This commit is contained in:
Eric Liu 2019-12-19 04:20:20 -08:00
commit 46cb9aa44b
12 changed files with 472 additions and 2 deletions

View file

@ -0,0 +1,42 @@
<script>
let className = undefined;
export { className as class };
export let href = undefined;
export let rel = undefined;
export let light = false;
export let clicked = false;
export let props = {};
import { createEventDispatcher, tick } from 'svelte';
import { cx } from '../../lib';
const dispatch = createEventDispatcher();
async function handleClick(event) {
clicked = !clicked;
await tick();
dispatch('click', event);
}
async function handleKeyDown(event) {
if (event.key === ' ' || event.key === 'Enter') {
clicked = !clicked;
await tick();
}
dispatch('keydown', event);
}
$: _class = cx(
'--link',
'--tile',
'--tile--clickable',
clicked && '--tile--is-clicked',
light && '--tile--light',
className
);
</script>
<a {...props} class={_class} on:click={handleClick} on:keydown={handleKeyDown} {href} {rel}>
<slot />
</a>

View file

@ -0,0 +1,88 @@
<script>
let className = undefined;
export { className as class };
export let expanded = false;
export let id = Math.random();
export let tileCollapsedIconText = 'Interact to expand Tile';
export let tileExpandedIconText = 'Interact to collapse Tile';
export let tileMaxHeight = 0;
export let tilePadding = 0;
export let tabIndex = 0;
export let light = false;
export let props = {};
import { createEventDispatcher, tick, onMount } from 'svelte';
import ChevronDown16 from 'carbon-icons-svelte/lib/ChevronDown16';
import { cx } from '../../lib';
let tile = undefined;
let tileContent = undefined;
let aboveTheFold = undefined;
const dispatch = createEventDispatcher();
onMount(() => {
const tileStyle = window.getComputedStyle(tile, null);
tileMaxHeight = aboveTheFold.getBoundingClientRect().height;
tilePadding =
parseInt(tileStyle.getPropertyValue('padding-top'), 10) +
parseInt(tileStyle.getPropertyValue('padding-bottom'), 10);
});
function setMaxHeight() {
tileMaxHeight = expanded
? tileContent.getBoundingClientRect().height
: aboveTheFold.getBoundingClientRect().height;
}
async function handleClick(event) {
expanded = !expanded;
await tick();
setMaxHeight();
dispatch('click', event);
}
async function handleKeyPress(event) {
if (event.key === ' ' || event.key === 'Enter') {
expanded = !expanded;
await tick();
setMaxHeight();
}
dispatch('keypress', event);
}
$: _class = cx(
'--tile',
'--tile--expandable',
expanded && '--tile--is-expanded',
light && '--tile--light',
className
);
$: tileStyle = expanded ? undefined : `max-height: ${tileMaxHeight + tilePadding}px`;
</script>
<div
{...props}
bind:this={tile}
style={tileStyle}
class={_class}
on:click={handleClick}
on:keypress={handleKeyPress}
tabindex={tabIndex}
{id}>
<div bind:this={tileContent}>
<div bind:this={aboveTheFold} class={cx('--tile-content')}>
<slot name="above" />
</div>
<button
aria-expanded={expanded}
aria-label={expanded ? tileExpandedIconText : tileCollapsedIconText}
class={cx('--tile__chevron')}>
<ChevronDown16 />
</button>
<div class={cx('--tile-content')}>
<slot name="below" />
</div>
</div>
</div>

View file

@ -0,0 +1,59 @@
<script>
// TODO: compose as "children" components
let className = undefined;
export { className as class };
export let checked = false;
export let id = Math.random();
export let name = '';
export let iconDescription = 'Tile checkmark';
export let value = '';
export let tabIndex = 0;
export let light = false;
export let props = {};
import { createEventDispatcher } from 'svelte';
import CheckmarkFilled16 from 'carbon-icons-svelte/lib/CheckmarkFilled16';
import { cx } from '../../lib';
const dispatch = createEventDispatcher();
function handleChange(event) {
dispatch('change', event);
}
function handleKeyDown(event) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
handleChange(event);
}
dispatch('keydown', event);
}
$: _class = cx(
'--tile',
'--tile--selectable',
checked && '--tile--is-selected',
light && '--tile--light',
className
);
</script>
<input
{...props}
type="radio"
class={cx('--tile-input')}
on:change
on:change={handleChange}
{id}
{name}
{value}
{checked} />
<label for={id} class={_class} tabindex={tabIndex} on:keydown={handleKeyDown}>
<span class={cx('--tile__checkmark')}>
<CheckmarkFilled16 aria-label={iconDescription} title={iconDescription} />
</span>
<span class={cx('--tile-content')}>
<slot />
</span>
</label>

View file

@ -0,0 +1,78 @@
<script>
// TODO: emit current selected tile
let className = undefined;
export { className as class };
export let selected = false;
export let id = Math.random();
export let value = 'value';
export let title = 'title';
export let name = '';
export let iconDescription = 'Tile checkmark';
export let tabIndex = 0;
export let light = false;
export let props = {};
import { createEventDispatcher, tick } from 'svelte';
import CheckmarkFilled16 from 'carbon-icons-svelte/lib/CheckmarkFilled16';
import { cx } from '../../lib';
let input = undefined;
const dispatch = createEventDispatcher();
function handleChange(event) {
dispatch('change', event);
}
async function handleClick(event) {
if (event.target !== input) {
selected = !selected;
await tick();
}
dispatch('click', event);
}
async function handleKeyDown(event) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault();
selected = !selected;
await tick();
}
dispatch('keydown', event);
}
$: _class = cx(
'--tile',
'--tile--selectable',
selected && '--tile--is-selected',
light && '--tile--light',
className
);
</script>
<input
bind:this={input}
type="checkbox"
tabindex={-1}
class={cx('--tile-input')}
on:change={handleChange}
checked={selected}
{id}
{value}
{name}
{title} />
<label
{...props}
for={id}
class={_class}
tabindex={tabIndex}
on:click|preventDefault={handleClick}
on:keydown={handleKeyDown}>
<span class={cx('--tile__checkmark')}>
<CheckmarkFilled16 aria-label={iconDescription} title={iconDescription} />
</span>
<span class={cx('--tile-content')}>
<slot />
</span>
</label>

View file

@ -0,0 +1,68 @@
<script>
export let story = undefined;
import Layout from '../../internal/ui/Layout.svelte';
import Tile from './Tile.svelte';
import ClickableTile from './ClickableTile.svelte';
import SelectableTile from './SelectableTile.svelte';
import ExpandableTile from './ExpandableTile.svelte';
import TileAboveTheFoldContent from './TileAboveTheFoldContent.svelte';
import TileBelowTheFoldContent from './TileBelowTheFoldContent.svelte';
import TileGroup from './TileGroup.svelte';
import RadioTile from './RadioTile.svelte';
const radioTiles = [
{ value: 'standard', id: 'tile-1', labelText: 'Selectable Tile' },
{ value: 'default-selected', id: 'tile-2', labelText: 'Selectable Tile' },
{ value: 'selected', id: 'tile-3', labelText: 'Selectable Tile' }
];
let selected = radioTiles[1].value;
</script>
<Layout>
<div>
{#if story === 'filter'}
<Tile {...$$props} />
{:else if story === 'clickable'}
<ClickableTile {...$$props}>Clickable Tile</ClickableTile>
{:else if story === 'multi-select'}
<div role="group" aria-label="selectable tiles">
<SelectableTile id="tile-1" name="tiles" {...$$props}>Multi-select Tile</SelectableTile>
<SelectableTile id="tile-2" name="tiles" {...$$props}>Multi-select Tile</SelectableTile>
<SelectableTile id="tile-3" name="tiles" {...$$props}>Multi-select Tile</SelectableTile>
</div>
{:else if story === 'selectable'}
<TileGroup legend="Selectable Tile Group">
{#each radioTiles as { value, id, labelText }, i (id)}
<RadioTile
{...$$props}
checked={selected === value}
on:change={() => {
selected = value;
}}
{value}
{id}
{labelText}>
Selectable Tile
</RadioTile>
{/each}
</TileGroup>
{:else if story === 'expandable'}
<ExpandableTile {...$$props}>
<div slot="above">
<TileAboveTheFoldContent>
<div style="height: 200px">Above the fold content here</div>
</TileAboveTheFoldContent>
</div>
<div slot="below">
<TileBelowTheFoldContent>
<div style="height: 400px">Below the fold content here</div>
</TileBelowTheFoldContent>
</div>
</ExpandableTile>
{:else}
<Tile {...$$props}>Default Tile</Tile>
{/if}
</div>
</Layout>

View file

@ -0,0 +1,64 @@
import { withKnobs, select, number, boolean, text } from '@storybook/addon-knobs';
import Component from './Tile.Story.svelte';
export default { title: 'Tile', decorators: [withKnobs] };
export const Default = () => ({
Component,
props: {
light: boolean('Light variant (light)', false)
}
});
export const Clickable = () => ({
Component,
props: {
story: 'clickable',
href: text('Href for clickable UI (href)', 'javascript:void(0)'),
light: boolean('Light variant (light)', false)
}
});
export const MultiSelect = () => ({
Component,
props: {
story: 'multi-select',
selected: boolean('Selected (selected)', false),
light: boolean('Light variant (light)', false)
}
});
const radioValues = {
None: '',
standard: 'standard',
'default-selected': 'default-selected',
selected: 'selected'
};
export const Selectable = () => ({
Component,
props: {
story: 'selectable',
name: text('Form item name (name in <RadioTile>)', 'tiles'),
light: boolean('Light variant (light)', false)
}
});
export const Expandable = () => ({
Component,
props: {
story: 'expandable',
tabIndex: number('Tab index (tabIndex)', 0),
expanded: boolean('Expanded (expanded)', false),
tileMaxHeight: number('Max height (tileMaxHeight)', 0),
tileCollapsedIconText: text(
'Collapsed icon text (tileCollapsedIconText)',
'Interact to Expand tile'
),
tileExpandedIconText: text(
'Collapsed icon text (tileExpandedIconText)',
'Interact to Collapse tile'
),
light: boolean('Light variant (light)', false)
}
});

View file

@ -0,0 +1,14 @@
<script>
let className = undefined;
export { className as class };
export let light = false;
export let props = {};
import { cx } from '../../lib';
$: _class = cx('--tile', light && '--tile--light', className);
</script>
<div class={_class} {...props}>
<slot />
</div>

View file

@ -0,0 +1,7 @@
<script>
import { cx } from '../../lib';
</script>
<span class={cx('--tile-content__above-the-fold')}>
<slot />
</span>

View file

@ -0,0 +1,7 @@
<script>
import { cx } from '../../lib';
</script>
<span class={cx('--tile-content__below-the-fold')}>
<slot />
</span>

View file

@ -0,0 +1,19 @@
<script>
let className = undefined;
export { className as class };
export let disabled = false;
export let legend = '';
import { cx } from '../../lib';
$: _class = cx('--tile-group', className);
</script>
<fieldset class={_class} {disabled}>
{#if legend}
<legend>{legend}</legend>
{/if}
<div>
<slot />
</div>
</fieldset>

View file

@ -0,0 +1,9 @@
import Tile from './Tile.svelte';
export default Tile;
export { default as ClickableTile } from './ClickableTile.svelte';
export { default as ExpandableTile } from './ExpandableTile.svelte';
export { default as SelectableTile } from './SelectableTile.svelte';
export { default as TileGroup } from './TileGroup.svelte';
export { default as TileAboveTheFoldContent } from './TileAboveTheFoldContent.svelte';
export { default as TileBelowTheFoldContent } from './TileBelowTheFoldContent.svelte';

View file

@ -18,6 +18,14 @@ import SkeletonText from './components/SkeletonText';
import Tag, { TagSkeleton } from './components/Tag';
import TextArea, { TextAreaSkeleton } from './components/TextArea';
import TextInput, { TextInputSkeleton, PasswordInput } from './components/TextInput';
import Tile, {
ClickableTile,
ExpandableTile,
SelectableTile,
TileGroup,
TileAboveTheFoldContent,
TileBelowTheFoldContent
} from './components/Tile';
import Toggle, { ToggleSkeleton } from './components/Toggle';
import ToggleSmall, { ToggleSmallSkeleton } from './components/ToggleSmall';
import TooltipDefinition from './components/TooltipDefinition';
@ -35,20 +43,24 @@ export {
ButtonSkeleton,
Checkbox,
CheckboxSkeleton,
ClickableTile,
CodeSnippet,
CodeSnippetSkeleton,
Copy,
CopyButton,
DataTableSkeleton,
ExpandableTile,
InlineLoading,
Loading,
Link,
ListItem,
Loading,
OrderedList,
PasswordInput,
RadioButton,
RadioButtonSkeleton,
Search,
SearchSkeleton,
SelectableTile,
SkeletonPlaceholder,
SkeletonText,
Tag,
@ -57,7 +69,10 @@ export {
TextAreaSkeleton,
TextInput,
TextInputSkeleton,
PasswordInput,
Tile,
TileAboveTheFoldContent,
TileBelowTheFoldContent,
TileGroup,
Toggle,
ToggleSkeleton,
ToggleSmall,