[WEB-3237, 3238] dev: date picker enhancements (#6470)

* [WEB-3238] dev: datepicker with month and year selection dropdowns (#6391)

* feat: react-day-picker upgrade and caption dropdowns

* style fixes

* style: css and autofocus improved

* fix: fixed weeks for datepicker to ensure static height

---------

Co-authored-by: Vineet K <55555696+vineetk13@users.noreply.github.com>
This commit is contained in:
Aaryan Khandelwal 2025-01-28 16:15:18 +05:30 committed by GitHub
parent f32635a6a8
commit 88b4d32220
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 251 additions and 287 deletions

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { Fragment } from "react"; import { Fragment } from "react";
import { DayPicker } from "react-day-picker"; import { DayPicker, getDefaultClassNames } from "react-day-picker";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { X } from "lucide-react"; import { X } from "lucide-react";
@ -31,6 +31,8 @@ const defaultValues: TFormValues = {
date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()), date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()),
}; };
const defaultClassNames = getDefaultClassNames();
export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, onSelect }) => { export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, onSelect }) => {
const { handleSubmit, watch, control } = useForm<TFormValues>({ const { handleSubmit, watch, control } = useForm<TFormValues>({
defaultValues, defaultValues,
@ -97,6 +99,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
const date2Value = getDate(watch("date2")); const date2Value = getDate(watch("date2"));
return ( return (
<DayPicker <DayPicker
classNames={{
root: `${defaultClassNames.root} border border-custom-border-200 p-3 rounded-md`,
}}
captionLayout="dropdown"
selected={dateValue} selected={dateValue}
defaultMonth={dateValue} defaultMonth={dateValue}
onSelect={(date) => { onSelect={(date) => {
@ -105,7 +111,6 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
}} }}
mode="single" mode="single"
disabled={date2Value ? [{ after: date2Value }] : undefined} disabled={date2Value ? [{ after: date2Value }] : undefined}
className="border border-custom-border-200 p-3 rounded-md"
/> />
); );
}} }}
@ -119,6 +124,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
const date1Value = getDate(watch("date1")); const date1Value = getDate(watch("date1"));
return ( return (
<DayPicker <DayPicker
classNames={{
root: `${defaultClassNames.root} border border-custom-border-200 p-3 rounded-md`,
}}
captionLayout="dropdown"
selected={dateValue} selected={dateValue}
defaultMonth={dateValue} defaultMonth={dateValue}
onSelect={(date) => { onSelect={(date) => {
@ -127,7 +136,6 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
}} }}
mode="single" mode="single"
disabled={date1Value ? [{ before: date1Value }] : undefined} disabled={date1Value ? [{ before: date1Value }] : undefined}
className="border border-custom-border-200 p-3 rounded-md"
/> />
); );
}} }}

View file

@ -2,7 +2,7 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Placement } from "@popperjs/core"; import { Placement } from "@popperjs/core";
import { DateRange, DayPicker, Matcher } from "react-day-picker"; import { DateRange, DayPicker, Matcher, getDefaultClassNames } from "react-day-picker";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react"; import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react";
import { Combobox } from "@headlessui/react"; import { Combobox } from "@headlessui/react";
@ -52,6 +52,8 @@ type Props = {
renderPlaceholder?: boolean; renderPlaceholder?: boolean;
}; };
const defaultClassNames = getDefaultClassNames();
export const DateRangeDropdown: React.FC<Props> = (props) => { export const DateRangeDropdown: React.FC<Props> = (props) => {
const { const {
applyButtonText = "Apply changes", applyButtonText = "Apply changes",
@ -198,12 +200,14 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
{isOpen && ( {isOpen && (
<Combobox.Options className="fixed z-10" static> <Combobox.Options className="fixed z-10" static>
<div <div
className="my-1 bg-custom-background-100 shadow-custom-shadow-rg rounded-md overflow-hidden p-3" className="my-1 bg-custom-background-100 shadow-custom-shadow-rg overflow-hidden"
ref={setPopperElement} ref={setPopperElement}
style={styles.popper} style={styles.popper}
{...attributes.popper} {...attributes.popper}
> >
<DayPicker <DayPicker
captionLayout="dropdown"
classNames={{ root: `${defaultClassNames.root} p-3 rounded-md` }}
selected={dateRange} selected={dateRange}
onSelect={(val) => { onSelect={(val) => {
// if both the dates are not required, immediately call onSelect // if both the dates are not required, immediately call onSelect
@ -216,7 +220,8 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
mode="range" mode="range"
disabled={disabledDays} disabled={disabledDays}
showOutsideDays showOutsideDays
initialFocus autoFocus
fixedWeeks
footer={ footer={
bothRequired && ( bothRequired && (
<div className="grid grid-cols-2 items-center gap-3.5 pt-6 relative"> <div className="grid grid-cols-2 items-center gap-3.5 pt-6 relative">

View file

@ -1,5 +1,5 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { DayPicker, Matcher } from "react-day-picker"; import { DayPicker, Matcher, getDefaultClassNames } from "react-day-picker";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { usePopper } from "react-popper"; import { usePopper } from "react-popper";
import { CalendarDays, X } from "lucide-react"; import { CalendarDays, X } from "lucide-react";
@ -33,6 +33,8 @@ type Props = TDropdownProps & {
renderByDefault?: boolean; renderByDefault?: boolean;
}; };
const defaultClassNames = getDefaultClassNames();
export const DateDropdown: React.FC<Props> = (props) => { export const DateDropdown: React.FC<Props> = (props) => {
const { const {
buttonClassName = "", buttonClassName = "",
@ -166,7 +168,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
<Combobox.Options data-prevent-outside-click static> <Combobox.Options data-prevent-outside-click static>
<div <div
className={cn( className={cn(
"my-1 bg-custom-background-100 shadow-custom-shadow-rg rounded-md overflow-hidden p-3 z-20", "my-1 bg-custom-background-100 shadow-custom-shadow-rg overflow-hidden z-20",
optionsClassName optionsClassName
)} )}
ref={setPopperElement} ref={setPopperElement}
@ -174,15 +176,18 @@ export const DateDropdown: React.FC<Props> = (props) => {
{...attributes.popper} {...attributes.popper}
> >
<DayPicker <DayPicker
captionLayout="dropdown"
classNames={{ root: `${defaultClassNames.root} p-3 rounded-md` }}
selected={getDate(value)} selected={getDate(value)}
defaultMonth={getDate(value)} defaultMonth={getDate(value)}
onSelect={(date) => { onSelect={(date) => {
dropdownOnChange(date ?? null); dropdownOnChange(date ?? null);
}} }}
showOutsideDays showOutsideDays
initialFocus autoFocus
disabled={disabledDays} disabled={disabledDays}
mode="single" mode="single"
fixedWeeks
/> />
</div> </div>
</Combobox.Options>, </Combobox.Options>,

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { FC, Fragment, useState } from "react"; import { FC, Fragment, useState } from "react";
import { DayPicker } from "react-day-picker"; import { DayPicker, getDefaultClassNames } from "react-day-picker";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
// ui // ui
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
@ -18,6 +18,8 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
// states // states
const [date, setDate] = useState(value || new Date()); const [date, setDate] = useState(value || new Date());
const defaultClassNames = getDefaultClassNames();
return ( return (
<Transition.Root show={isOpen} as={Fragment}> <Transition.Root show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}> <Dialog as="div" className="relative z-20" onClose={handleClose}>
@ -46,6 +48,8 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
<Dialog.Panel className="relative flex transform rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> <Dialog.Panel className="relative flex transform rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<div className="flex h-full w-full flex-col gap-y-1"> <div className="flex h-full w-full flex-col gap-y-1">
<DayPicker <DayPicker
captionLayout="dropdown"
classNames={{root: `${defaultClassNames.root} rounded-md border border-custom-border-200 p-3`}}
selected={date ? new Date(date) : undefined} selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined} defaultMonth={date ? new Date(date) : undefined}
onSelect={(date) => { onSelect={(date) => {
@ -53,7 +57,6 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
setDate(date); setDate(date);
}} }}
mode="single" mode="single"
className="rounded-md border border-custom-border-200 p-3"
disabled={[ disabled={[
{ {
before: new Date(), before: new Date(),

View file

@ -55,7 +55,7 @@
"posthog-js": "^1.131.3", "posthog-js": "^1.131.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-day-picker": "^8.10.0", "react-day-picker": "^9.5.0",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-hook-form": "7.51.5", "react-hook-form": "7.51.5",

View file

@ -1,9 +1,9 @@
.rdp { .rdp-root {
font-size: 12px; font-size: 12px;
--rdp-cell-size: 40px; --rdp-cell-size: 40px;
/* Size of the day cells. */ /* Size of the day cells. */
--rdp-caption-font-size: 1.15rem; --rdp-caption-font-size: 1rem;
/* Font size for the caption labels. */ /* Font size for the caption labels. */
--rdp-caption-navigation-size: 1.25rem; --rdp-caption-navigation-size: 1.25rem;
/* Font size for the caption labels. */ /* Font size for the caption labels. */
@ -21,260 +21,16 @@
background: transparent; background: transparent;
} }
/* Hide elements for devices that are not screen readers */ .rdp-root {
.rdp-vhidden { position: relative; /* Required to position the nav. */
box-sizing: border-box; box-sizing: border-box;
padding: 0;
margin: 0;
background: transparent;
border: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
position: absolute !important;
top: 0;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
overflow: hidden !important;
clip: rect(1px, 1px, 1px, 1px) !important;
border: 0 !important;
}
/* Buttons */
.rdp-button_reset {
appearance: none;
position: relative;
margin: 0;
padding: 0;
cursor: default;
color: inherit;
background: none;
font: inherit;
-moz-appearance: none;
-webkit-appearance: none;
}
.rdp-button_reset:focus-visible {
/* Make sure to reset outline only when :focus-visible is supported */
outline: none;
}
.rdp-button {
border: 2px solid transparent;
}
.rdp-button[disabled]:not(.rdp-day_selected) {
opacity: 0.25;
}
.rdp-button:not([disabled]) {
cursor: pointer;
}
.rdp-button:focus-visible:not([disabled]):not(.rdp-day_selected) {
color: inherit;
background-color: var(--rdp-background-color);
}
.rdp-button:focus-visible:not([disabled]).rdp-day_selected:not(.rdp-day_range_middle) {
outline: var(--rdp-outline);
outline-offset: 2px;
background-color: var(--rdp-dark-background-color);
outline-width: thin;
}
.rdp-button:hover:not([disabled]).rdp-day_selected {
background-color: var(--rdp-dark-background-color);
}
.rdp-button:hover:not([disabled]):not(.rdp-day_selected) {
background-color: var(--rdp-background-color);
}
.rdp-months {
display: flex;
}
.rdp-month {
margin: 0 1em;
}
.rdp-month:first-child {
margin-left: 0;
}
.rdp-month:last-child {
margin-right: 0;
}
.rdp-table {
margin: 0;
max-width: calc(var(--rdp-cell-size) * 7);
border-collapse: collapse;
}
.rdp-with_weeknumber .rdp-table {
max-width: calc(var(--rdp-cell-size) * 8);
border-collapse: collapse;
}
.rdp-caption {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0;
text-align: left;
}
.rdp-multiple_months .rdp-caption {
position: relative;
display: block;
text-align: center;
}
.rdp-caption_dropdowns {
position: relative;
display: inline-flex;
}
.rdp-caption_label {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
margin: 0;
padding: 0 0.25em;
white-space: nowrap;
color: currentColor;
border: 0;
border: 2px solid transparent;
font-family: inherit;
font-size: var(--rdp-caption-font-size);
font-weight: 600;
}
.rdp-nav {
white-space: nowrap;
}
.rdp-multiple_months .rdp-caption_start .rdp-nav {
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.rdp-multiple_months .rdp-caption_end .rdp-nav {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
}
.rdp-nav_button {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--rdp-caption-navigation-size);
height: var(--rdp-caption-navigation-size);
padding: 0.25em;
border-radius: 2px;
}
.rdp-nav_button:hover,
.rdp-nav_button:focus-visible {
background-color: rgba(var(--color-background-80)) !important;
} }
/* ---------- */ /* ---------- */
/* Dropdowns */ /* Day Buttons */
/* ---------- */ /* ----------- */
.rdp-dropdown_year, .rdp-day_button {
.rdp-dropdown_month {
position: relative;
display: inline-flex;
align-items: center;
}
.rdp-dropdown {
appearance: none;
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
cursor: inherit;
opacity: 0;
border: none;
background-color: transparent;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.rdp-dropdown[disabled] {
opacity: unset;
color: unset;
}
.rdp-dropdown:focus-visible:not([disabled]) + .rdp-caption_label {
background-color: var(--rdp-background-color);
border: var(--rdp-outline);
border-radius: 6px;
}
.rdp-dropdown_icon {
margin: 0 0 0 5px;
}
.rdp-head {
border: 0;
}
.rdp-head_row,
.rdp-row {
height: 100%;
}
.rdp-head_cell {
vertical-align: middle;
font-size: 0.75em;
font-weight: 700;
text-align: center;
height: 100%;
height: var(--rdp-cell-size);
padding: 0;
text-transform: uppercase;
}
.rdp-tbody {
border: 0;
}
.rdp-tfoot {
margin: 0.5em;
}
.rdp-cell {
width: var(--rdp-cell-size);
height: 100%;
height: var(--rdp-cell-size);
padding: 0;
text-align: center;
}
.rdp-weeknumber {
font-size: 0.75em;
}
.rdp-weeknumber,
.rdp-day {
display: flex; display: flex;
overflow: hidden; overflow: hidden;
align-items: center; align-items: center;
@ -285,14 +41,58 @@
height: var(--rdp-cell-size); height: var(--rdp-cell-size);
margin: 0; margin: 0;
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 100%; border-radius: 50%;
} }
.rdp-day_today:not(.rdp-day_outside) { .rdp-day.rdp-outside:not(.rdp-selected) .rdp-day_button {
opacity: 0.5;
}
.rdp-day.rdp-disabled:not(.rdp-selected) .rdp-day_button {
opacity: 0.25;
}
.rdp-day:not(.rdp-disabled) .rdp-day_button {
cursor: pointer;
}
.rdp-day:not(.rdp-selected, .rdp-disabled) .rdp-day_button:focus-visible {
color: inherit;
background-color: var(--rdp-background-color);
}
.rdp-selected:not(.rdp-range_middle, .rdp-disabled) .rdp-day_button:focus-visible {
outline: var(--rdp-outline);
outline-offset: 2px;
background-color: var(--rdp-dark-background-color);
outline-width: thin;
}
.rdp-day:not(.rdp-disabled) .rdp-day_button:hover {
background-color: var(--rdp-background-color);
}
.rdp-selected .rdp-day_button {
background-color: var(--rdp-accent-color);
border-radius: 50%;
color: var(--rdp-selected-color);
z-index: 1;
}
.rdp-selected .rdp-day_button:hover:not(.rdp-disabled) {
background-color: var(--rdp-dark-background-color);
}
.rdp-week {
margin: 0;
padding: 0;
}
.rdp-today:not(.rdp-outside) {
position: relative; position: relative;
} }
.rdp-day_today:not(.rdp-day_outside)::after { .rdp-today:not(.rdp-outside)::after {
content: ""; content: "";
position: absolute; position: absolute;
left: 50%; left: 50%;
@ -304,31 +104,173 @@
transform: translate(-50%, 0); transform: translate(-50%, 0);
} }
.rdp-day_selected, .rdp-selected .rdp-day_button:focus-visible,
.rdp-day_selected:focus-visible, .rdp-selected .rdp-day_button:hover {
.rdp-day_selected:hover {
color: var(--rdp-selected-color); color: var(--rdp-selected-color);
opacity: 1; opacity: 1;
background-color: var(--rdp-accent-color); background-color: var(--rdp-accent-color);
} }
.rdp-day_outside:not(.rdp-day_selected) { .rdp-weekday {
opacity: 0.5; vertical-align: middle;
font-weight: 700;
text-align: center;
font-size: 0.75em;
height: var(--rdp-cell-size);
padding: 0;
text-transform: uppercase;
} }
.rdp-day_selected:focus-visible { /* ---------- */
/* Top Nav */
/* ---------- */
.rdp-nav {
box-sizing: border-box;
position: absolute;
padding: inherit;
top: 1.2em;
right: 1em;
display: flex;
align-items: center;
}
.rdp-button_next,
.rdp-button_previous {
border: none;
background: none;
padding: 0;
margin: 0;
cursor: pointer;
font: inherit;
-moz-appearance: none;
-webkit-appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
appearance: none;
width: var(--rdp-caption-navigation-size);
height: var(--rdp-caption-navigation-size);
padding: 0.25em;
border-radius: 2px;
}
.rdp-chevron {
fill: rgba(var(--color-text-200));
height: 0.75rem;
width: 0.75rem;
}
.rdp-button_next:hover,
.rdp-button_previous:hover,
.rdp-button_next:focus-visible,
.rdp-button_previous:focus-visible {
background-color: rgba(var(--color-background-80)) !important;
}
/* ---------- */
/* Dropdowns */
/* ---------- */
.rdp-dropdowns {
position: relative;
/* width: 100%; */
display: inline-flex;
align-items: center;
}
.rdp-dropdown {
appearance: none;
--webkit-appearance: none;
--moz-appearance: none;
position: absolute;
z-index: 2;
top: 0;
bottom: 0;
left: 0;
width: 100%;
margin: 0;
padding: 0;
opacity: 0;
border: none;
font-family: inherit;
font-size: 1rem;
line-height: inherit;
cursor: pointer;
background: transparent;
&:hover {
background-color: rgba(var(--color-background-80)) !important;
}
}
.rdp-dropdown_root {
margin: 0;
position: relative;
display: inline-flex;
align-items: center;
}
.rdp-months_dropdown {
text-transform: capitalize;
}
.rdp-dropdown[data-disabled="true"] {
opacity: unset;
color: unset;
}
.rdp-caption_label {
z-index: 1; z-index: 1;
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin: 0;
padding: 0 0.25em;
white-space: nowrap;
color: currentColor;
border: 0;
border: 2px solid transparent;
font-family: inherit;
font-size: var(--rdp-caption-font-size);
font-weight: 600;
background: transparent;
border-radius: 4px;
} }
td:has(.rdp-day_range_start), .rdp-dropdown:not([data-disabled="true"]) {
td:has(.rdp-day_range_middle), &:focus-visible + .rdp-caption_label {
td:has(.rdp-day_range_end) { border: var(--rdp-outline);
border-radius: 6px;
}
&:hover {
& + .rdp-caption_label {
background-color: rgba(var(--color-background-80)) !important;
}
}
}
.rdp-dropdown_icon {
margin: 0 0 0 5px;
}
/* --------------- */
/* Range selection */
/* --------------- */
.rdp-range_start,
.rdp-range_middle,
.rdp-range_end {
position: relative; position: relative;
} }
td:has(.rdp-day_range_start)::before, .rdp-range_start::before,
td:has(.rdp-day_range_middle)::before, .rdp-range_middle::before,
td:has(.rdp-day_range_end)::before { .rdp-range_end::before {
content: ""; content: "";
position: absolute; position: absolute;
background-color: var(--rdp-background-color); background-color: var(--rdp-background-color);
@ -336,33 +278,34 @@ td:has(.rdp-day_range_end)::before {
height: 100%; height: 100%;
width: 50%; width: 50%;
transform: translate(0, -50%); transform: translate(0, -50%);
z-index: -1;
} }
td:has(.rdp-day_range_start)::before { .rdp-range_start::before {
left: 50%; left: 50%;
} }
td:has(.rdp-day_range_middle)::before { .rdp-range_middle::before {
left: 50%; left: 50%;
width: 100%; width: 100%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
td:has(.rdp-day_range_end)::before { .rdp-range_end::before {
right: 50%; right: 50%;
} }
td:has(.rdp-day_range_start.rdp-day_range_end)::before { .rdp-range_start.rdp-range_end::before {
display: none; display: none;
} }
.rdp-day_range_middle { .rdp-range_middle .rdp-day_button {
background-color: transparent; background-color: transparent;
color: inherit; color: inherit;
} }
.rdp-day_range_middle:hover, .rdp-day.rdp-range_middle .rdp-day_button:hover,
.rdp-day_range_middle:focus-visible { .rdp-day.rdp-range_middle .rdp-day_button:focus-visible {
background-color: var(--rdp-background-color) !important; background-color: var(--rdp-background-color);
color: inherit !important; color: inherit;
} }