mirror of
https://github.com/carbon-design-system/carbon-components-svelte.git
synced 2025-09-15 18:31:06 +00:00
parent
45da25ce1b
commit
6ca56d67a6
23 changed files with 602 additions and 21 deletions
21
src/components/DatePicker/DatePicker.Skeleton.svelte
Normal file
21
src/components/DatePicker/DatePicker.Skeleton.svelte
Normal file
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
let className = undefined;
|
||||
export { className as class };
|
||||
export let id = Math.random();
|
||||
export let range = false;
|
||||
export let style = undefined;
|
||||
|
||||
import { cx, fillArray } from '../../lib';
|
||||
</script>
|
||||
|
||||
<div on:click on:mouseover on:mouseenter on:mouseleave class={cx('--form-item')} {style}>
|
||||
<div
|
||||
class={cx('--date-picker', '--skeleton', range && '--date-picker--range', !range && '--date-picker--short', !range && '--date-picker--simple', className)}>
|
||||
{#each fillArray(range ? 2 : 1) as input, i (input)}
|
||||
<div class={cx('--date-picker-container')}>
|
||||
<label class={cx('--label')} for={id} />
|
||||
<div class={cx('--date-picker__input', '--skeleton')} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
65
src/components/DatePicker/DatePicker.Story.svelte
Normal file
65
src/components/DatePicker/DatePicker.Story.svelte
Normal file
|
@ -0,0 +1,65 @@
|
|||
<script>
|
||||
export let story = undefined;
|
||||
|
||||
import Layout from '../../internal/ui/Layout.svelte';
|
||||
import DatePicker from './DatePicker.svelte';
|
||||
import DatePickerSkeleton from './DatePicker.Skeleton.svelte';
|
||||
import DatePickerInput from './DatePickerInput.svelte';
|
||||
|
||||
let datePickerType = 'simple';
|
||||
let value = '';
|
||||
</script>
|
||||
|
||||
<Layout>
|
||||
|
||||
{#if story === 'skeleton'}
|
||||
<DatePickerSkeleton range />
|
||||
{:else if story === 'single'}
|
||||
<div>
|
||||
<DatePicker
|
||||
{...$$props.datePicker}
|
||||
bind:value
|
||||
datePickerType="single"
|
||||
on:change={({ detail }) => {
|
||||
console.log('change', detail);
|
||||
}}>
|
||||
<DatePickerInput
|
||||
{...$$props.datePickerInput}
|
||||
on:close={() => {
|
||||
console.log('on:close');
|
||||
}}
|
||||
on:input={() => {
|
||||
console.log('on:input');
|
||||
}} />
|
||||
</DatePicker>
|
||||
<button
|
||||
on:click|preventDefault={() => {
|
||||
value = '12/12/2020';
|
||||
}}
|
||||
style="margin-top: 1rem">
|
||||
Set date to 12/12/2020
|
||||
</button>
|
||||
</div>
|
||||
{:else if story === 'range'}
|
||||
<DatePicker {...$$props.datePicker} bind:value datePickerType="range">
|
||||
<DatePickerInput
|
||||
{...$$props.datePickerInput}
|
||||
id="date-picker-input-id-start"
|
||||
labelText="Start date" />
|
||||
<DatePickerInput
|
||||
{...$$props.datePickerInput}
|
||||
id="date-picker-input-id-end"
|
||||
labelText="End date" />
|
||||
</DatePicker>
|
||||
{:else}
|
||||
<DatePicker
|
||||
{...$$props.datePicker}
|
||||
bind:datePickerType
|
||||
bind:value
|
||||
on:change={({ detail }) => {
|
||||
console.log('on:change', detail);
|
||||
}}>
|
||||
<DatePickerInput {...$$props.datePickerInput} />
|
||||
</DatePicker>
|
||||
{/if}
|
||||
</Layout>
|
99
src/components/DatePicker/DatePicker.stories.js
Normal file
99
src/components/DatePicker/DatePicker.stories.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { withKnobs, select, text, boolean } from '@storybook/addon-knobs';
|
||||
import Component from './DatePicker.Story.svelte';
|
||||
|
||||
export default { title: 'DatePicker', decorators: [withKnobs] };
|
||||
|
||||
const patterns = {
|
||||
'Short (d{1,2}/d{4})': 'd{1,2}/d{4}',
|
||||
'Regular (d{1,2}/d{1,2}/d{4})': 'd{1,2}/d{1,2}/d{4}'
|
||||
};
|
||||
|
||||
export const Default = () => ({
|
||||
Component,
|
||||
props: {
|
||||
datePicker: {
|
||||
id: 'date-picker',
|
||||
light: boolean('Light variant (light in <DatePicker>)', false),
|
||||
short: boolean('Use shorter width (short in <DatePicker>)', false)
|
||||
},
|
||||
datePickerInput: {
|
||||
id: 'date-picker-input-id',
|
||||
labelText: text('Label text (labelText in <DatePickerInput>)', 'Date Picker label'),
|
||||
hideLabel: boolean('Hide label (hideLabel)', false),
|
||||
pattern: select('The date format (pattern in <DatePickerInput>)', patterns, 'd{1,2}/d{4}'),
|
||||
placeholder: text('Placeholder text (placeholder in <DatePickerInput>)', 'mm/dd/yyyy'),
|
||||
disabled: boolean('Disabled (disabled in <DatePickerInput>)', false),
|
||||
invalid: boolean('Show form validation UI (invalid in <DatePickerInput>)', false),
|
||||
invalidText: text(
|
||||
'Form validation UI content (invalidText in <DatePickerInput>)',
|
||||
'A valid value is required'
|
||||
),
|
||||
iconDescription: text(
|
||||
'Icon description (iconDescription in <DatePickerInput>)',
|
||||
'Icon description'
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Default.story = { name: 'Default (simple)' };
|
||||
|
||||
export const Single = () => ({
|
||||
Component,
|
||||
props: {
|
||||
story: 'single',
|
||||
datePicker: {
|
||||
id: 'date-picker',
|
||||
light: boolean('Light variant (light in <DatePicker>)', false),
|
||||
dateFormat: text('The date format (dateFormat in <DatePicker>)', 'm/d/Y')
|
||||
},
|
||||
datePickerInput: {
|
||||
id: 'date-picker-input-id',
|
||||
labelText: text('Label text (labelText in <DatePickerInput>)', 'Date Picker label'),
|
||||
hideLabel: boolean('Hide label (hideLabel)', false),
|
||||
pattern: select('The date format (pattern in <DatePickerInput>)', patterns, 'd{1,2}/d{4}'),
|
||||
placeholder: text('Placeholder text (placeholder in <DatePickerInput>)', 'mm/dd/yyyy'),
|
||||
disabled: boolean('Disabled (disabled in <DatePickerInput>)', false),
|
||||
invalid: boolean('Show form validation UI (invalid in <DatePickerInput>)', false),
|
||||
invalidText: text(
|
||||
'Form validation UI content (invalidText in <DatePickerInput>)',
|
||||
'A valid value is required'
|
||||
),
|
||||
iconDescription: text(
|
||||
'Icon description (iconDescription in <DatePickerInput>)',
|
||||
'Icon description'
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const Range = () => ({
|
||||
Component,
|
||||
props: {
|
||||
story: 'range',
|
||||
datePicker: {
|
||||
id: 'date-picker',
|
||||
light: boolean('Light variant (light in <DatePicker>)', false),
|
||||
dateFormat: text('The date format (dateFormat in <DatePicker>)', 'm/d/Y')
|
||||
},
|
||||
datePickerInput: {
|
||||
id: 'date-picker-input-id',
|
||||
labelText: text('Label text (labelText in <DatePickerInput>)', 'Date Picker label'),
|
||||
hideLabel: boolean('Hide label (hideLabel)', false),
|
||||
pattern: select('The date format (pattern in <DatePickerInput>)', patterns, 'd{1,2}/d{4}'),
|
||||
placeholder: text('Placeholder text (placeholder in <DatePickerInput>)', 'mm/dd/yyyy'),
|
||||
disabled: boolean('Disabled (disabled in <DatePickerInput>)', false),
|
||||
invalid: boolean('Show form validation UI (invalid in <DatePickerInput>)', false),
|
||||
invalidText: text(
|
||||
'Form validation UI content (invalidText in <DatePickerInput>)',
|
||||
'A valid value is required'
|
||||
),
|
||||
iconDescription: text(
|
||||
'Icon description (iconDescription in <DatePickerInput>)',
|
||||
'Icon description'
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const Skeleton = () => ({ Component, props: { story: 'skeleton' } });
|
145
src/components/DatePicker/DatePicker.svelte
Normal file
145
src/components/DatePicker/DatePicker.svelte
Normal file
|
@ -0,0 +1,145 @@
|
|||
<script>
|
||||
let className = undefined;
|
||||
export { className as class };
|
||||
export let id = Math.random();
|
||||
export let short = false;
|
||||
export let datePickerType = 'simple';
|
||||
export let dateFormat = 'm/d/Y';
|
||||
export let locale = 'en';
|
||||
export let value = '';
|
||||
export let appendTo = document.body;
|
||||
export let minDate = null;
|
||||
export let maxDate = null;
|
||||
export let light = false;
|
||||
export let style = undefined;
|
||||
|
||||
import { createEventDispatcher, setContext, afterUpdate, onDestroy } from 'svelte';
|
||||
import { writable, derived } from 'svelte/store';
|
||||
import { createCalendar } from './flatpickr';
|
||||
import { cx } from '../../lib';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let inputs = writable([]);
|
||||
let inputIds = derived(inputs, _ => _.map(({ id }) => id));
|
||||
let inputsById = derived(inputs, _ => _.reduce((a, c) => ({ ...a, [c.id]: c }), {}));
|
||||
let labelTextEmpty = derived(inputs, _ => _.filter(({ labelText }) => !!labelText).length === 0);
|
||||
let inputValue = writable(value);
|
||||
let mode = writable(datePickerType);
|
||||
let range = derived(mode, _ => _ === 'range');
|
||||
let hasCalendar = derived(mode, _ => _ === 'single' || _ === 'range');
|
||||
|
||||
let calendar = undefined;
|
||||
let datePickerRef = undefined;
|
||||
let inputRef = undefined;
|
||||
let inputRefTo = undefined;
|
||||
|
||||
setContext('DatePicker', {
|
||||
range,
|
||||
inputValue,
|
||||
hasCalendar,
|
||||
add: data => {
|
||||
inputs.update(_ => [..._, data]);
|
||||
},
|
||||
declareRef: ({ id, ref }) => {
|
||||
if ($inputIds.indexOf(id) === 0) {
|
||||
inputRef = ref;
|
||||
} else {
|
||||
inputRefTo = ref;
|
||||
}
|
||||
},
|
||||
updateValue: ({ id, type, value }) => {
|
||||
if ((!calendar && type === 'input') || type === 'change') {
|
||||
inputValue.set(value);
|
||||
}
|
||||
|
||||
if (!calendar && type === 'change') {
|
||||
dispatch('change', value);
|
||||
}
|
||||
},
|
||||
blurInput: relatedTarget => {
|
||||
if (calendar && !calendar.calendarContainer.contains(relatedTarget)) {
|
||||
calendar.close();
|
||||
}
|
||||
},
|
||||
openCalendar: () => {
|
||||
calendar.open();
|
||||
},
|
||||
focusCalendar: () => {
|
||||
(
|
||||
calendar.selectedDateElem ||
|
||||
calendar.todayDateElem ||
|
||||
calendar.calendarContainer.querySelector('.flatpickr-day[tabindex]') ||
|
||||
calendar.calendarContainer
|
||||
).focus();
|
||||
}
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
if ($hasCalendar && !calendar) {
|
||||
calendar = createCalendar({
|
||||
options: {
|
||||
appendTo,
|
||||
dateFormat,
|
||||
defaultDate: $inputValue,
|
||||
locale,
|
||||
maxDate,
|
||||
minDate,
|
||||
mode: $mode
|
||||
},
|
||||
base: inputRef,
|
||||
input: inputRefTo,
|
||||
dispatch: event => {
|
||||
const detail = { selectedDates: calendar.selectedDates };
|
||||
|
||||
if ($range) {
|
||||
detail.dateStr = {
|
||||
from: inputRef.value,
|
||||
to: inputRefTo.value
|
||||
};
|
||||
} else {
|
||||
detail.dateStr = inputRef.value;
|
||||
}
|
||||
|
||||
return dispatch(event, detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (calendar && !$range) {
|
||||
calendar.setDate($inputValue);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (calendar) {
|
||||
calendar.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// $: hasCalendar.set($mode === 'single' || $mode === 'range');
|
||||
$: inputValue.set(value);
|
||||
$: value = $inputValue;
|
||||
</script>
|
||||
|
||||
<svelte:body
|
||||
on:click={({ target }) => {
|
||||
if (!calendar || !calendar.isOpen) {
|
||||
return;
|
||||
}
|
||||
if (datePickerRef && datePickerRef.contains(target)) {
|
||||
return;
|
||||
}
|
||||
if (!calendar.calendarContainer.contains(target)) {
|
||||
calendar.close();
|
||||
}
|
||||
}} />
|
||||
|
||||
<div on:click on:mouseover on:mouseenter on:mouseleave class={cx('--form-item', className)} {style}>
|
||||
<div
|
||||
bind:this={datePickerRef}
|
||||
class={cx('--date-picker', short && '--date-picker--short', light && '--date-picker--light', datePickerType && `--date-picker--${datePickerType}`, datePickerType === 'range' && $labelTextEmpty && '--date-picker--nolabel')}
|
||||
{id}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
92
src/components/DatePicker/DatePickerInput.svelte
Normal file
92
src/components/DatePicker/DatePickerInput.svelte
Normal file
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
let className = undefined;
|
||||
export { className as class };
|
||||
export let id = Math.random();
|
||||
export let iconDescription = '';
|
||||
export let labelText = '';
|
||||
export let hideLabel = false;
|
||||
export let pattern = '\\d{1,2}\\/\\d{1,2}\\/\\d{4}';
|
||||
export let type = 'text';
|
||||
export let placeholder = '';
|
||||
export let disabled = false;
|
||||
export let invalid = false;
|
||||
export let invalidText = '';
|
||||
export let style = undefined;
|
||||
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import Calendar16 from 'carbon-icons-svelte/lib/Calendar16';
|
||||
import { cx } from '../../lib';
|
||||
|
||||
const {
|
||||
range,
|
||||
add,
|
||||
hasCalendar,
|
||||
declareRef,
|
||||
updateValue,
|
||||
blurInput,
|
||||
openCalendar,
|
||||
focusCalendar,
|
||||
inputValue
|
||||
} = getContext('DatePicker');
|
||||
|
||||
let inputRef = undefined;
|
||||
let iconRef = undefined;
|
||||
|
||||
add({ id, labelText });
|
||||
|
||||
onMount(() => {
|
||||
declareRef({ id, ref: inputRef });
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cx('--date-picker-container', !labelText && '--date-picker--nolabel', className)}
|
||||
{style}>
|
||||
{#if labelText}
|
||||
<label
|
||||
class={cx('--label', hideLabel && '--visually-hidden', disabled && '--label--disabled')}
|
||||
for={id}>
|
||||
{labelText}
|
||||
</label>
|
||||
{/if}
|
||||
<div class={cx('--date-picker-input__wrapper')}>
|
||||
<input
|
||||
bind:this={inputRef}
|
||||
data-invalid={invalid || undefined}
|
||||
class={cx('--date-picker__input')}
|
||||
on:input
|
||||
on:input={({ target }) => {
|
||||
updateValue({ id, type: 'input', value: target.value });
|
||||
}}
|
||||
on:change={({ target }) => {
|
||||
updateValue({ id, type: 'change', value: target.value });
|
||||
}}
|
||||
on:keydown
|
||||
on:keydown={({ key }) => {
|
||||
if (key === 'ArrowDown') {
|
||||
focusCalendar();
|
||||
}
|
||||
}}
|
||||
on:blur
|
||||
on:blur={({ relatedTarget }) => {
|
||||
blurInput(relatedTarget);
|
||||
}}
|
||||
{id}
|
||||
{placeholder}
|
||||
{type}
|
||||
{pattern}
|
||||
{disabled}
|
||||
value={!$range ? $inputValue : undefined} />
|
||||
{#if $hasCalendar}
|
||||
<Calendar16
|
||||
role="img"
|
||||
class={cx('--date-picker__icon')}
|
||||
aria-label={iconDescription}
|
||||
title={iconDescription}
|
||||
on:click={openCalendar} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if invalid}
|
||||
<div class={cx('--form-requirement')}>{invalidText}</div>
|
||||
{/if}
|
||||
</div>
|
79
src/components/DatePicker/flatpickr.js
Normal file
79
src/components/DatePicker/flatpickr.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import flatpickr from 'flatpickr';
|
||||
import l10n from 'flatpickr/dist/l10n';
|
||||
import rangePlugin from 'flatpickr/dist/plugins/rangePlugin';
|
||||
import { cx } from '../../lib';
|
||||
|
||||
l10n.en.weekdays.shorthand.forEach((_, index) => {
|
||||
const shorthand = _.slice(0, 2);
|
||||
l10n.en.weekdays.shorthand[index] = shorthand === 'Th' ? 'Th' : shorthand.charAt(0);
|
||||
});
|
||||
|
||||
function updateClasses(instance) {
|
||||
const { calendarContainer, days, daysContainer, weekdayContainer, selectedDates } = instance;
|
||||
|
||||
calendarContainer.classList.add(cx('--date-picker__calendar'));
|
||||
calendarContainer.querySelector('.flatpickr-month').classList.add(cx('--date-picker__month'));
|
||||
|
||||
weekdayContainer.classList.add(cx('--date-picker__weekdays'));
|
||||
weekdayContainer.querySelectorAll('.flatpickr-weekday').forEach(node => {
|
||||
node.classList.add(cx('--date-picker__weekday'));
|
||||
});
|
||||
|
||||
daysContainer.classList.add(cx('--date-picker__days'));
|
||||
days.querySelectorAll('.flatpickr-day').forEach(node => {
|
||||
node.classList.add(cx('--date-picker__day'));
|
||||
if (node.classList.contains('today') && selectedDates.length > 0) {
|
||||
node.classList.add('no-border');
|
||||
} else if (node.classList.contains('today') && selectedDates.length === 0) {
|
||||
node.classList.remove('no-border');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateMonthNode(instance) {
|
||||
const monthText = instance.l10n.months.longhand[instance.currentMonth];
|
||||
const staticMonthNode = instance.monthNav.querySelector('.cur-month');
|
||||
|
||||
if (staticMonthNode) {
|
||||
staticMonthNode.textContent = monthText;
|
||||
} else {
|
||||
const monthSelectNode = instance.monthsDropdownContainer;
|
||||
const span = document.createElement('span');
|
||||
span.setAttribute('class', 'cur-month');
|
||||
span.textContent = monthText;
|
||||
monthSelectNode.parentNode.replaceChild(span, monthSelectNode);
|
||||
}
|
||||
}
|
||||
|
||||
function createCalendar({ options, base, input, dispatch }) {
|
||||
return new flatpickr(base, {
|
||||
...options,
|
||||
allowInput: true,
|
||||
disableMobile: true,
|
||||
clickOpens: true,
|
||||
locale: l10n[options.locale],
|
||||
plugins: [options.mode === 'range' && new rangePlugin({ position: 'left', input })].filter(
|
||||
Boolean
|
||||
),
|
||||
nextArrow:
|
||||
'<svg width="16px" height="16px" viewBox="0 0 16 16"><polygon points="11,8 6,13 5.3,12.3 9.6,8 5.3,3.7 6,3 "/><rect width="16" height="16" style="fill: none" /></svg>',
|
||||
prevArrow:
|
||||
'<svg width="16px" height="16px" viewBox="0 0 16 16"><polygon points="5,8 10,3 10.7,3.7 6.4,8 10.7,12.3 10,13 "/><rect width="16" height="16" style="fill: none" /></svg>',
|
||||
onChange: () => {
|
||||
dispatch('change');
|
||||
},
|
||||
onClose: () => {
|
||||
dispatch('close');
|
||||
},
|
||||
onMonthChange: ({}, {}, instance) => {
|
||||
updateMonthNode(instance);
|
||||
},
|
||||
onOpen: ({}, {}, instance) => {
|
||||
dispatch('open');
|
||||
updateClasses(instance);
|
||||
updateMonthNode(instance);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { createCalendar };
|
4
src/components/DatePicker/index.js
Normal file
4
src/components/DatePicker/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import DatePicker from './DatePicker.svelte';
|
||||
|
||||
export default DatePicker;
|
||||
export { default as DatePickerInput } from './DatePickerInput.svelte';
|
Loading…
Add table
Add a link
Reference in a new issue