Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DateRangeField] Fixes and improvements #138

Merged
merged 9 commits into from
Nov 21, 2023
5 changes: 5 additions & 0 deletions .changeset/gorgeous-crabs-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte-ux': minor
---

DateRange[Field] improvements including responsive design, passing classes to underlying Field/Dialog instances, setting custom preset options, and hiding type/preset sections, and general bug fixes.
2 changes: 1 addition & 1 deletion packages/svelte-ux/src/lib/components/DateButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
(disabled || fade) && 'opacity-25',
isCurrent ? 'font-bold' : 'font-normal'
)}
variant={isSelected ? 'fill' : 'text'}
variant={isSelected ? 'fill' : 'default'}
color={isSelected || isCurrent ? 'accent' : 'default'}
{disabled}
on:click={() => {
Expand Down
220 changes: 157 additions & 63 deletions packages/svelte-ux/src/lib/components/DateRange.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,21 @@
} from '../utils/date';
import { getDateRangePresets } from '../utils/dateRange';
import type { DateRange } from '../utils/dateRange';
import { cls } from '../utils/styles';
import { omit } from '../utils/object';

import DateSelect from './DateSelect.svelte';
import MenuField from './MenuField.svelte';
import ToggleGroup from './ToggleGroup.svelte';
import ToggleOption from './ToggleOption.svelte';
import DateField from './DateField.svelte';
import { cls } from '$lib/utils/styles';
import { getComponentTheme } from './theme';
import { mdScreen } from '$lib/stores/matchMedia';

export let selected: DateRange = { from: null, to: null, periodType: null };
export let periodTypeOptions: PeriodType[] = [
export let selected: DateRange | null = { from: null, to: null, periodType: null };

/** Period types to show */
export let periodTypes: PeriodType[] = [
PeriodType.Day,
PeriodType.WeekSun,
PeriodType.BiWeek1Sun,
Expand All @@ -30,17 +35,41 @@
PeriodType.CalendarYear,
PeriodType.FiscalYearOctober,
];
export let getPeriodTypePresets = getDateRangePresets;

const theme = getComponentTheme('DateRange');

let selectedPeriodType = selected?.periodType ?? periodTypes[0];
let selectedPreset: string | null = null;
let selectedDayOfWeek: DayOfWeek = DayOfWeek.SUN;
let activeDate: 'from' | 'to' = 'from';

$: periodTypeOptions = periodTypes.map((pt) => {
const value = adjustPeriodType(pt);
return {
label: getPeriodTypeName(adjustPeriodType(pt)),
value,
};
});

$: presetOptions = getPeriodTypePresets(selectedPeriodType).map((preset) => {
return {
label: preset.label,
value: getDateRangeStr(preset.value),
preset,
};
});

/** Get date range (without period type) as string */
function getDateRangeStr(range: DateRange) {
return JSON.stringify(omit(range, ['periodType']));
}

function onDateChange(date: Date) {
// Apply date-fns function based on type and from/to.
let newSelected = { ...selected };
let newSelected = { ...selected, periodType: selectedPeriodType };

const { start, end } = getDateFuncsByPeriodType(selected.periodType);
const { start, end } = getDateFuncsByPeriodType(selectedPeriodType);

let newActiveDate: typeof activeDate = activeDate === 'from' ? 'to' : 'from';

Expand All @@ -61,16 +90,35 @@
activeDate = newActiveDate;
}

// Expand selection range to match period type (day => month, etc)
function onPeriodTypeChange(periodType: PeriodType) {
const { start, end } = getDateFuncsByPeriodType(periodType);
if (selected.from) {
selected.from = start(selected.from);
}
if (selected.to) {
selected.to = end(selected.to);
}
}

// Lookup selected preset string value to actual preset
function onPresetChange(presetValueString: string) {
const selectedPresetObj = presetOptions.find((o) => o.value === presetValueString)?.preset;
if (selectedPresetObj) {
selected = selectedPresetObj.value;
}
}

// Update selection after changing `selectedDayOfWeek`
$: {
if (selected.periodType && hasDayOfWeek(selected.periodType)) {
const newPeriodType = replaceDayOfWeek(selected.periodType, selectedDayOfWeek);
if (hasDayOfWeek(selectedPeriodType)) {
const newPeriodType = replaceDayOfWeek(selectedPeriodType, selectedDayOfWeek);

const newSelected = { ...selected, periodType: newPeriodType };

// Attempt to maintain selected preset if labels match
if (selected.from && selected.to && selected.periodType) {
const prevPeriodTypePreset = [...getDateRangePresets(selected.periodType)].find(
const prevPeriodTypePreset = [...getPeriodTypePresets(selected.periodType)].find(
(x) =>
x.value.from &&
isSameDay(x.value.from, selected.from) &&
Expand All @@ -79,7 +127,7 @@
);

if (prevPeriodTypePreset && newPeriodType) {
const newPeriodTypePreset = [...getDateRangePresets(newPeriodType)].find(
const newPeriodTypePreset = [...getPeriodTypePresets(newPeriodType)].find(
(x) => x.label === prevPeriodTypePreset.label
);

Expand All @@ -100,10 +148,22 @@
? replaceDayOfWeek(periodType, selectedDayOfWeek) || periodType
: periodType;
}

$: showPeriodTypes = periodTypeOptions.length > 1;
$: showPresets = presetOptions.length > 0;
$: showSidebar = showPeriodTypes || showPresets;
</script>

<div class={cls('DateRange grid grid-cols-[2fr,3fr] gap-2', theme.root, $$props.class)}>
<div class="col-start-2">
<div
class={cls(
'DateRange grid gap-2',
'w-[min(90vw,384px)]',
showSidebar && 'md:w-[640px] md:grid-cols-[2fr,3fr]',
theme.root,
$$props.class
)}
>
<div class={cls(showSidebar && 'md:col-start-2')}>
<ToggleGroup bind:value={activeDate} variant="outline" inset class="bg-white">
<ToggleOption value="from" class="flex-1">
<div class="text-xs text-black/50">Start</div>
Expand Down Expand Up @@ -151,66 +211,100 @@
</ToggleGroup>
</div>

<div class="grid gap-2">
<div>
<div class="text-xs text-black/50 uppercase mb-1 -mt-5">Type</div>
<ToggleGroup
bind:value={selected.periodType}
on:change={(e) => {
// Expand selection range to match period type (day => month, etc)
const { start, end } = getDateFuncsByPeriodType(e.detail.value);
if (selected.from) {
selected.from = start(selected.from);
}
if (selected.to) {
selected.to = end(selected.to);
}
}}
variant="outline"
inset
vertical
class="bg-white"
>
{#each periodTypeOptions ?? [] as pt}
<ToggleOption value={adjustPeriodType(pt)}>
{getPeriodTypeName(adjustPeriodType(pt))}
</ToggleOption>
{/each}
</ToggleGroup>

{#if selected.periodType}
<div class="text-xs text-black/50 uppercase mb-1 mt-4">Presets</div>
<ToggleGroup bind:value={selected} variant="outline" inset vertical class="bg-white">
{#each getDateRangePresets(selected.periodType) ?? [] as preset}
<ToggleOption value={preset.value}>{preset.label}</ToggleOption>
{/each}
</ToggleGroup>
{#if showSidebar}
<div class="flex flex-col gap-2 md:gap-4 md:-mt-5">
{#if showPeriodTypes}
{#if $mdScreen}
<div>
<div class="text-xs text-black/50 uppercase mb-1">Type</div>
<ToggleGroup
bind:value={selectedPeriodType}
on:change={(e) => onPeriodTypeChange(e.detail.value)}
variant="outline"
inset
vertical
class="bg-white"
>
{#each periodTypeOptions as option}
<ToggleOption value={option.value}>
{option.label}
</ToggleOption>
{/each}
</ToggleGroup>
</div>
{:else}
<MenuField
label="Type"
bind:value={selectedPeriodType}
options={periodTypeOptions}
on:change={(e) => onPeriodTypeChange(e.detail.value)}
/>
{/if}
{/if}

{#if selected.periodType && hasDayOfWeek(selected.periodType)}
<div class="text-xs text-black/50 uppercase mb-1 mt-4">Start day of week</div>
<ToggleGroup
bind:value={selectedDayOfWeek}
variant="outline"
inset
classes={{ root: 'bg-white', option: 'px-0' }}
>
<ToggleOption value={DayOfWeek.SUN}>Sun</ToggleOption>
<ToggleOption value={DayOfWeek.MON}>Mon</ToggleOption>
<ToggleOption value={DayOfWeek.TUE}>Tue</ToggleOption>
<ToggleOption value={DayOfWeek.WED}>Wed</ToggleOption>
<ToggleOption value={DayOfWeek.THU}>Thu</ToggleOption>
<ToggleOption value={DayOfWeek.FRI}>Fri</ToggleOption>
<ToggleOption value={DayOfWeek.SAT}>Sat</ToggleOption>
</ToggleGroup>
{#if showPresets}
{#key selectedPeriodType}
{#if $mdScreen}
<div>
<div class="text-xs text-black/50 uppercase mb-1">Presets</div>
<ToggleGroup
bind:value={selectedPreset}
variant="outline"
inset
vertical
class="bg-white"
>
{#each presetOptions as option}
<ToggleOption
value={option.value}
on:click={() => {
onPresetChange(option.value);
}}
>
{option.label}
</ToggleOption>
{/each}
</ToggleGroup>
</div>
{:else}
<MenuField
label="Presets"
bind:value={selectedPreset}
options={presetOptions}
on:change={(e) => {
onPresetChange(e.detail.value);
}}
/>
{/if}
{/key}
{/if}

{#if hasDayOfWeek(selectedPeriodType)}
<div>
<div class="text-xs text-black/50 uppercase mb-1">Start day of week</div>
<ToggleGroup
bind:value={selectedDayOfWeek}
variant="outline"
inset
classes={{ root: 'bg-white', option: 'px-0' }}
>
<ToggleOption value={DayOfWeek.SUN}>Sun</ToggleOption>
<ToggleOption value={DayOfWeek.MON}>Mon</ToggleOption>
<ToggleOption value={DayOfWeek.TUE}>Tue</ToggleOption>
<ToggleOption value={DayOfWeek.WED}>Wed</ToggleOption>
<ToggleOption value={DayOfWeek.THU}>Thu</ToggleOption>
<ToggleOption value={DayOfWeek.FRI}>Fri</ToggleOption>
<ToggleOption value={DayOfWeek.SAT}>Sat</ToggleOption>
</ToggleGroup>
</div>
{/if}
</div>
</div>
{/if}

<div class="bg-white border rounded overflow-auto">
<DateSelect
{selected}
periodType={selected?.periodType}
periodType={selectedPeriodType}
on:dateChange={(e) => onDateChange(e.detail)}
{activeDate}
/>
Expand Down
Loading