Datepicker range with presets
In this guide you'll learn how to build a date range picker with presets:
Layout
One way to create the layout above, is by combining <Box>, <Grid>, and
<Inline>:
<Box border radius="s" background>
<Inline gap={0} style={{ flexWrap: "nowrap" }}>
<Box padding={12}>
<Grid gap={0}>[presetbuttons]</Grid>
</Box>
<Box
background="base-2"
padding
radiusTopRight="s"
radiusBottomRight="s"
borderLeft
style={{ alignSelf: "stretch" }}
>
<Grid gap={16}>
[datepicker]
<div>
<span className="bfc-base-2">Selected: </span>
[duration]
</div>
</Grid>
</Box>
</Inline>
</Box>Preset buttons
The preset buttons are pretty close to what we can achieve using
<Button>, with two small exceptions:
- The button text should be left-aligned (not centered)
- The font-weight should be normal (not bold)
Let's create our own component that solves this and also lets us mark it as
"active" by styling it as a filled button.
PresetButton.tsx
import Button from "@intility/bifrost-react/Button";
export default function PresetButton({
children,
onClick,
active = false,
}: {
children?: React.ReactNode;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
active?: boolean;
}) {
return (
<Button
style={{ textAlign: "left", fontWeight: "normal" }}
state={active ? "default" : "neutral"}
variant={active ? "filled" : "flat"}
onClick={onClick}
>
{children}
</Button>
);
}Usage would look something like this
<PresetButton onClick={setDurationToday}>Today</PresetButton>
<PresetButton onClick={setDurationThisWeek} active>This week</PresetButton>
<PresetButton onClick={setDurationThisMonth}>This month</PresetButton>Datepicker
The inline prop makes the <DatePicker> render as an
always-visible calendar without an input field.
To give users both the visual calendar and a text input (for copy/paste or
typing dates), render two <DatePicker> instances sharing the same state:
- Input field: Set
open={false}to render only the input without a popup - Visual calendar: Set
inlineto render the always-visible calendar
Both instances share the same selected, startDate, endDate, and
onChange props:
<DatePicker
selected={from}
onChange={handleDateChange}
startDate={from}
endDate={to}
selectsRange
label="Select start and end date"
open={false}
isClearable
/>
<DatePicker
selected={from}
onChange={handleDateChange}
startDate={from}
endDate={to}
selectsRange
inline
label=""
hideLabel
swapRange
/>Duration
Use <FormatDuration> to get a localized duration text
with a tooltip based on two Date objects.
<FormatDuration start={from} end={to} />State and functions
In order to keep a date range in react state, we need to store a "from date" and "to date".
const [from, setFrom] = useState<Date | null>(null);
const [to, setTo] = useState<Date | null>(null);The from and to dates should probably live outside our component, so they can be used to filter data elsewhere in the app, let's make them props along with a way to update them:
from: Date | nullto: Date | nullonChange: (dates: [Date | null, Date | null]) => void
We also want to highlight the active preset button. Let's make it a number in
days for buttons like "last 24 hours" = 1 day, and "last 365 days" = 365,
and a string for buttons like "Today" as "today" etc.
const [activeButton, setActiveButton] = useState<number | string | undefined>();When clicking a preset button, we can use
date-fns helper functions to update both:
import { endOfDay, startOfDay } from "date-fns";<PresetButton
active={activeButton === "today"}
onClick={() => {
const fromStartOfDay = startOfDay(new Date());
const toEndOfDay = endOfDay(new Date());
onChange([fromStartOfDay, toEndOfDay]);
setActiveButton("today");
}}
>
Today
</PresetButton>When picking dates with the datepickers, set both dates and clear any active preset button.
const handleDateChange = ([newFrom, newTo]: [Date | null, Date | null]) => {
const fromStartOfDay = newFrom && startOfDay(newFrom);
const toEndOfDay = newTo && endOfDay(newTo);
onChange([fromStartOfDay, toEndOfDay]);
// Clear active preset when user manually picks dates
setActiveButton(undefined);
};Combined code example
Include time
Since react-datepicker doesn't provide a way to pick two different times in
the same datepicker instance, we need two datepickers to be able to pick a range
between two datetime points:
<DatePicker
label="From"
showTimeSelect
selected={from}
selectsStart
startDate={from}
endDate={to}
maxDate={to || undefined}
onChange={(newDate) => {
onChange([newDate, to]);
setActiveButton(undefined);
}}
/>
<DatePicker
label="To"
showTimeSelect
selected={to}
selectsEnd
startDate={from}
endDate={to}
minDate={from || undefined}
onChange={(newDate) => {
onChange([from, newDate]);
setActiveButton(undefined);
}}
/>Otherwise it works a lot like the example above.
Combined code example with time
Placed in a dropdown
function MyDateDropdown() {
const [from, setFrom] = useState<Date | null>(null);
const [to, setTo] = useState<Date | null>(null);
return (
<Dropdown
unstyled
noArrow
content={
<InlineDateRangePreset
from={from}
to={to}
onChange={([newFrom, newTo]) => {
setFrom(newFrom);
setTo(newTo);
}}
/>
}
>
<Button>
{from && to ? (
<FormatDuration start={from} end={to} />
) : (
"Select date range"
)}
</Button>
</Dropdown>
);
}