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> ); }