feat(component): add NumberInput

Closes #21
This commit is contained in:
Eric Liu 2019-12-29 14:08:42 -08:00
commit 4e578bc55d
17 changed files with 282 additions and 20 deletions

View file

@ -0,0 +1,15 @@
<script>
let className = undefined;
export { className as class };
export let hideLabel = false;
export let style = undefined;
import { cx } from '../../lib';
</script>
<div on:click on:mouseover on:mouseenter on:mouseleave class={cx('--form-item', className)} {style}>
{#if !hideLabel}
<span class={cx('--label', '--skeleton')} />
{/if}
<div class={cx('--number', '--skeleton')} />
</div>

View file

@ -0,0 +1,23 @@
<script>
export let story = undefined;
import Layout from '../../internal/ui/Layout.svelte';
import NumberInput from './NumberInput.svelte';
import NumberInputSkeleton from './NumberInput.Skeleton.svelte';
let value = $$props.value;
</script>
<Layout>
{#if story === 'skeleton'}
<NumberInputSkeleton {...$$props} />
{:else}
<NumberInput
{...$$props}
id="slider"
bind:value
on:change={({ detail }) => {
console.log('on:change', detail);
}} />
{/if}
</Layout>

View file

@ -0,0 +1,32 @@
import { withKnobs, text, boolean, number } from '@storybook/addon-knobs';
import Component from './NumberInput.Story.svelte';
export default { title: 'NumberInput', decorators: [withKnobs] };
export const Default = () => ({
Component,
props: {
id: 'number-input',
label: text('Label (label)', 'Number Input label'),
hideLabel: boolean('Hide label (hideLabel)', false),
min: number('Minimum value (min)', 0),
max: number('Maximum value (max)', 100),
value: number('Value (value)', 50),
step: number('Step of up/down arrow (step)', 10),
disabled: boolean('Disabled (disabled)', false),
readonly: boolean('Read only (readonly)', false),
invalid: boolean('Show form validation UI (invalid)', false),
isMobile: boolean('Mobile variant', false),
invalidText: text('Form validation UI content (invalidText)', 'Number is not valid'),
helperText: text('Helper text (helperText)', 'Optional helper text.'),
light: boolean('Light variant (light)', false)
}
});
export const Skeleton = () => ({
Component,
props: {
story: 'skeleton',
hideLabel: boolean('Hide label (hideLabel)', false)
}
});

View file

@ -0,0 +1,183 @@
<script>
let className = undefined;
export { className as class };
export let disabled = false;
export let hideLabel = false;
export let iconDescription = '';
export let id = Math.random();
export let label = '';
export let max = undefined;
export let min = undefined;
export let step = 1;
export let value = '';
export let readonly = false;
export let invalid = false;
export let invalidText = 'Provide invalidText';
export let helperText = '';
export let light = false;
export let allowEmpty = false;
export let isMobile = false;
export const translationIds = { increment: 'increment', decrement: 'decrement' };
export let translateWithId = id => defaultTranslations[id];
export let style = undefined;
import { createEventDispatcher, afterUpdate } from 'svelte';
import WarningFilled16 from 'carbon-icons-svelte/lib/WarningFilled16';
import CaretDownGlyph from 'carbon-icons-svelte/lib/CaretDownGlyph';
import CaretUpGlyph from 'carbon-icons-svelte/lib/CaretUpGlyph';
import { cx } from '../../lib';
const defaultTranslations = {
[translationIds.increment]: 'Increment number',
[translationIds.decrement]: 'Decrement number'
};
const dispatch = createEventDispatcher();
function updateValue(direction) {
const nextValue = (value += direction * step);
if (nextValue < min) {
value = min;
} else if (nextValue > max) {
value = max;
} else {
value = nextValue;
}
}
afterUpdate(() => {
dispatch('change', value);
});
$: incrementLabel = translateWithId('increment');
$: decrementLabel = translateWithId('decrement');
$: value = Number(value);
$: error = invalid || (!allowEmpty && value === '') || value > max || value < min;
$: errorId = `error-${id}`;
$: ariaLabel =
$$props['aria-label'] || 'Numeric input field with increment and decrement buttons';
</script>
<div on:click on:mouseover on:mouseenter on:mouseleave class={cx('--form-item', className)} {style}>
<div
data-invalid={error || undefined}
class={cx('--number', '--number--helpertext', readonly && '--number--readonly', light && '--number--light', hideLabel && '--number--nolabel', isMobile && '--number--mobile')}>
{#if isMobile}
{#if label}
<label class={cx('--label', hideLabel && '--visually-hidden')} for={id}>
<slot name="label">{label}</slot>
</label>
{/if}
{#if helperText}
<div class={cx('--form__helper-text')}>{helperText}</div>
{/if}
<div class={cx('--number__input-wrapper')}>
<button
type="button"
aria-live="polite"
aria-atomic="true"
title={decrementLabel}
aria-label={decrementLabel || iconDescription}
class={cx('--number__control-btn', 'down-icon')}
on:click={() => {
updateValue(-1);
}}
{disabled}>
<CaretDownGlyph class="down-icon" />
</button>
<input
type="number"
pattern="[0-9]*"
aria-label={label ? undefined : ariaLabel}
on:input
on:input={({ target }) => {
value = target.value;
}}
{disabled}
{id}
{max}
{min}
{step}
{value}
{readonly} />
<button
type="button"
aria-live="polite"
aria-atomic="true"
title={incrementLabel}
aria-label={incrementLabel || iconDescription}
class={cx('--number__control-btn', 'up-icon')}
on:click={() => {
updateValue(1);
}}
{disabled}>
<CaretUpGlyph class="up-icon" />
</button>
</div>
{:else}
{#if label}
<label class={cx('--label', hideLabel && '--visually-hidden')} for={id}>
<slot name="label">{label}</slot>
</label>
{/if}
{#if helperText}
<div class={cx('--form__helper-text')}>{helperText}</div>
{/if}
<div class={cx('--number__input-wrapper')}>
<input
type="number"
pattern="[0-9]*"
aria-describedby={errorId}
data-invalid={invalid || undefined}
aria-invalid={invalid || undefined}
aria-label={label ? undefined : ariaLabel}
on:input
on:input={({ target }) => {
value = target.value;
}}
{disabled}
{id}
{max}
{min}
{step}
{value}
{readonly} />
{#if invalid}
<WarningFilled16 class={cx('--number__invalid')} />
{/if}
<div class={cx('--number__controls')}>
<button
type="button"
aria-live="polite"
aria-atomic="true"
title={incrementLabel || iconDescription}
aria-label={incrementLabel || iconDescription}
class={cx('--number__control-btn', 'up-icon')}
on:click={() => {
updateValue(1);
}}
{disabled}>
<CaretUpGlyph class="up-icon" />
</button>
<button
type="button"
aria-live="polite"
aria-atomic="true"
title={decrementLabel || iconDescription}
aria-label={decrementLabel || iconDescription}
class={cx('--number__control-btn', 'down-icon')}
on:click={() => {
updateValue(-1);
}}
{disabled}>
<CaretDownGlyph class="down-icon" />
</button>
</div>
</div>
{/if}
{#if error}
<div class={cx('--form-requirement')} id={errorId}>{invalidText}</div>
{/if}
</div>
</div>

View file

@ -0,0 +1,4 @@
import NumberInput from './NumberInput.svelte';
export default NumberInput;
export { default as NumberInputSkeleton } from './NumberInput.Skeleton.svelte';

View file

@ -33,6 +33,7 @@ import {
NotificationIcon,
NotificationTextDetails
} from './components/Notification';
import NumberInput, { NumberInputSkeleton } from './components/NumberInput';
import OrderedList from './components/OrderedList';
import OverflowMenu, { OverflowMenuItem } from './components/OverflowMenu';
import Pagination, { PaginationSkeleton } from './components/Pagination';
@ -122,6 +123,8 @@ export {
NotificationButton,
NotificationIcon,
NotificationTextDetails,
NumberInput,
NumberInputSkeleton,
OrderedList,
OverflowMenu,
OverflowMenuItem,