Skip to content

Commit

Permalink
[DateRangeField] Fixes and improvements (#138)
Browse files Browse the repository at this point in the history
* [DateRangeField] Support passing classes to underlying Field and Dialog instances

* Fix handling of presets (including double clicking odd condition)

* Fix non-selected button style

* Cleanup default preset labels

* [DateRange] Support custom presets

* [DateRange] Improve layout on small viewports

* Rename `periodTypeOptions` to `periodTypes`

* Additional layout improvements on small viewports.  Support omitting Type or Preset sections.

* Add changeset
  • Loading branch information
techniq authored Nov 21, 2023
1 parent 7409910 commit 8857668
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 119 deletions.
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

1 comment on commit 8857668

@vercel
Copy link

@vercel vercel bot commented on 8857668 Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.