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
import DatePicker from "@intility/bifrost-react-datepicker"; import Box from "@intility/bifrost-react/Box"; import Button from "@intility/bifrost-react/Button"; import FormatDuration from "@intility/bifrost-react/FormatDuration"; import Grid from "@intility/bifrost-react/Grid"; import Inline from "@intility/bifrost-react/Inline"; import { addDays, endOfDay, endOfMonth, endOfWeek, endOfYear, startOfDay, startOfMonth, startOfWeek, startOfYear, } from "date-fns"; import { useState } from "react"; import "@intility/bifrost-react-datepicker/datepicker.css"; export default function InlineDateRangePresetDemo() { const [from, setFrom] = useState<Date | null>(null); const [to, setTo] = useState<Date | null>(null); return ( <InlineDateRangePreset from={from} to={to} onChange={([newFrom, newTo]) => { setFrom(newFrom); setTo(newTo); }} /> ); } 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> ); } function InlineDateRangePreset({ from, to, onChange, }: { from: Date | null; to: Date | null; onChange: (dates: [Date | null, Date | null]) => void; }) { const [activeButton, setActiveButton] = useState< number | string | undefined >(); const setDurationDays = (days: number) => { const newFrom = addDays(new Date(), days * -1); // set end date to "now" if that makes sense for your data // (includes future dates that we want to filter out) const newTo = new Date(); // otherwise, setting no end date might make sense for "last 24 hours" if // dataset has no future dates (like an auto-updating log table) // const newTo = null; onChange([newFrom, newTo]); setActiveButton(days); }; const handleDateChange = ([newFrom, newTo]: [Date | null, Date | null]) => { const fromStartOfDay = newFrom && startOfDay(newFrom); const toEndOfDay = newTo && endOfDay(newTo); onChange([fromStartOfDay, toEndOfDay]); setActiveButton(undefined); }; return ( <Box border radius="s" background style={{ display: "inline-block" }}> <Inline gap={0} style={{ flexWrap: "nowrap" }}> <Box padding={12}> <Grid gap={0}> <PresetButton active={activeButton === "today"} onClick={() => { const fromStartOfDay = startOfDay(new Date()); const toEndOfDay = endOfDay(new Date()); onChange([fromStartOfDay, toEndOfDay]); setActiveButton("today"); }} > Today </PresetButton> <PresetButton active={activeButton === "week"} onClick={() => { const newFrom = startOfWeek(new Date(), { weekStartsOn: 1 }); const newTo = endOfWeek(new Date(), { weekStartsOn: 1 }); onChange([newFrom, newTo]); setActiveButton("week"); }} > This week </PresetButton> <PresetButton active={activeButton === "month"} onClick={() => { const newFrom = startOfMonth(new Date()); const newTo = endOfMonth(new Date()); setActiveButton("month"); onChange([newFrom, newTo]); }} > This month </PresetButton> <PresetButton active={activeButton === "year"} onClick={() => { const newFrom = startOfYear(new Date()); const newTo = endOfYear(new Date()); setActiveButton("year"); onChange([newFrom, newTo]); }} > This year </PresetButton> <PresetButton active={activeButton === 1} onClick={() => setDurationDays(1)} > Last 24 hours </PresetButton> <PresetButton active={activeButton === 7} onClick={() => setDurationDays(7)} > Last 7 days </PresetButton> <PresetButton active={activeButton === 30} onClick={() => setDurationDays(30)} > Last 30 days </PresetButton> <PresetButton active={activeButton === 365} onClick={() => setDurationDays(365)} > Last 365 days </PresetButton> </Grid> </Box> <Box background="base-2" padding radiusTopRight="s" radiusBottomRight="s" borderLeft style={{ alignSelf: "stretch" }} > <Grid gap={16}> <Grid> <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 /> </Grid> <div> <span className="bfc-base-2">Selected: </span> {from && to ? ( <FormatDuration start={from} // if `setDurationDays(N)` sets `newTo = new Date()` end={to} // if `setDurationDays(N)` sets `newTo = null` // end={to ?? new Date()} /> ) : ( "No range selected" )} </div> </Grid> </Box> </Inline> </Box> ); }
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
import DatePicker from "@intility/bifrost-react-datepicker"; import Box from "@intility/bifrost-react/Box"; import Button from "@intility/bifrost-react/Button"; import FormatDuration from "@intility/bifrost-react/FormatDuration"; import Grid from "@intility/bifrost-react/Grid"; import Inline from "@intility/bifrost-react/Inline"; import { addDays, endOfDay, endOfWeek, startOfDay, startOfWeek, } from "date-fns"; import { useState } from "react"; import "@intility/bifrost-react-datepicker/datepicker.css"; export default function InlineDateRangePresetDemo() { const [from, setFrom] = useState<Date | null>(null); const [to, setTo] = useState<Date | null>(null); return ( <InlineDateTimeRangePreset from={from} to={to} onChange={([newFrom, newTo]) => { setFrom(newFrom); setTo(newTo); }} /> ); } 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> ); } function InlineDateTimeRangePreset({ from, to, onChange, }: { from: Date | null; to: Date | null; onChange: (dates: [Date | null, Date | null]) => void; }) { const [activeButton, setActiveButton] = useState< number | string | undefined >(); const setDurationDays = (days: number) => { const newFrom = addDays(new Date(), days * -1); // set end date to "now" if that makes sense for your data // (includes future dates that we want to filter out) const newTo = new Date(); // otherwise, setting no end date might make sense for "last 24 hours" if // dataset has no future dates (like an auto-updating log table) // const newTo = null; onChange([newFrom, newTo]); setActiveButton(days); }; return ( <Box border radius="s" background style={{ display: "inline-block" }}> <Inline gap={0} style={{ flexWrap: "nowrap" }}> <Box padding={12}> <Grid gap={0}> <PresetButton active={activeButton === "today"} onClick={() => { const fromStartOfDay = startOfDay(new Date()); const toEndOfDay = endOfDay(new Date()); onChange([fromStartOfDay, toEndOfDay]); setActiveButton("today"); }} > Today </PresetButton> <PresetButton active={activeButton === "week"} onClick={() => { const newFrom = startOfWeek(new Date(), { weekStartsOn: 1 }); const newTo = endOfWeek(new Date(), { weekStartsOn: 1 }); onChange([newFrom, newTo]); setActiveButton("week"); }} > This week </PresetButton> <PresetButton active={activeButton === 1} onClick={() => setDurationDays(1)} > Last 24 hours </PresetButton> <PresetButton active={activeButton === 7} onClick={() => setDurationDays(7)} > Last 7 days </PresetButton> </Grid> </Box> <Box background="base-2" padding radiusTopRight="s" radiusBottomRight="s" borderLeft style={{ alignSelf: "stretch", display: "grid" }} > <Inline> <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); }} /> </Inline> <Inline align="center" style={{ alignSelf: "end" }}> <Inline.Stretch> <span className="bfc-base-2">Selected: </span> {from && to ? ( <FormatDuration start={from} // if `setDurationDays(N)` sets `newTo = new Date()` end={to} // if `setDurationDays(N)` sets `newTo = null` // end={to ?? new Date()} /> ) : ( "No range selected" )} </Inline.Stretch> <Button small variant="flat" state="neutral" onClick={() => { onChange([null, null]); setActiveButton(undefined); }} disabled={!from && !to} > Clear </Button> </Inline> </Box> </Inline> </Box> ); }
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>
);
}