[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
6 changed files with 251 additions and 287 deletions
@@ -1,7 +1,7 @@
"use client";
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 { X } from "lucide-react";
@@ -31,6 +31,8 @@ const defaultValues: TFormValues = {
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 }) => {
const { handleSubmit, watch, control } = useForm<TFormValues>({
defaultValues,
@@ -97,6 +99,10 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
const date2Value = getDate(watch("date2"));
return (
<DayPicker
classNames={{
root: `${defaultClassNames.root} border border-custom-border-200 p-3 rounded-md`,
}}
captionLayout="dropdown"
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date) => {
@@ -105,7 +111,6 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
}}
mode="single"
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"));
return (
<DayPicker
classNames={{
root: `${defaultClassNames.root} border border-custom-border-200 p-3 rounded-md`,
}}
captionLayout="dropdown"
selected={dateValue}
defaultMonth={dateValue}
onSelect={(date) => {
@@ -127,7 +136,6 @@ export const DateFilterModal: React.FC<Props> = ({ title, handleClose, isOpen, o
}}
mode="single"
disabled={date1Value ? [{ before: date1Value }] : undefined}
className="border border-custom-border-200 p-3 rounded-md"
/>
);
}}
+8 -3
View File
@@ -2,7 +2,7 @@
import React, { useEffect, useRef, useState } from "react";
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 { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react";
import { Combobox } from "@headlessui/react";
@@ -52,6 +52,8 @@ type Props = {
renderPlaceholder?: boolean;
};
const defaultClassNames = getDefaultClassNames();
export const DateRangeDropdown: React.FC<Props> = (props) => {
const {
applyButtonText = "Apply changes",
@@ -198,12 +200,14 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
{isOpen && (
<Combobox.Options className="fixed z-10" static>
<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}
style={styles.popper}
{...attributes.popper}
>
<DayPicker
captionLayout="dropdown"
classNames={{ root: `${defaultClassNames.root} p-3 rounded-md` }}
selected={dateRange}
onSelect={(val) => {
// if both the dates are not required, immediately call onSelect
@@ -216,7 +220,8 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
mode="range"
disabled={disabledDays}
showOutsideDays
initialFocus
autoFocus
fixedWeeks
footer={
bothRequired && (
<div className="grid grid-cols-2 items-center gap-3.5 pt-6 relative">
+8 -3
View File
@@ -1,5 +1,5 @@
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 { usePopper } from "react-popper";
import { CalendarDays, X } from "lucide-react";
@@ -33,6 +33,8 @@ type Props = TDropdownProps & {
renderByDefault?: boolean;
};
const defaultClassNames = getDefaultClassNames();
export const DateDropdown: React.FC<Props> = (props) => {
const {
buttonClassName = "",
@@ -166,7 +168,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
<Combobox.Options data-prevent-outside-click static>
<div
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
)}
ref={setPopperElement}
@@ -174,15 +176,18 @@ export const DateDropdown: React.FC<Props> = (props) => {
{...attributes.popper}
>
<DayPicker
captionLayout="dropdown"
classNames={{ root: `${defaultClassNames.root} p-3 rounded-md` }}
selected={getDate(value)}
defaultMonth={getDate(value)}
onSelect={(date) => {
dropdownOnChange(date ?? null);
}}
showOutsideDays
initialFocus
autoFocus
disabled={disabledDays}
mode="single"
fixedWeeks
/>
</div>
</Combobox.Options>,
@@ -1,7 +1,7 @@
"use client";
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";
// ui
import { Button } from "@plane/ui";
@@ -18,6 +18,8 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
// states
const [date, setDate] = useState(value || new Date());
const defaultClassNames = getDefaultClassNames();
return (
<Transition.Root show={isOpen} as={Fragment}>
<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">
<div className="flex h-full w-full flex-col gap-y-1">
<DayPicker
captionLayout="dropdown"
classNames={{root: `${defaultClassNames.root} rounded-md border border-custom-border-200 p-3`}}
selected={date ? new Date(date) : undefined}
defaultMonth={date ? new Date(date) : undefined}
onSelect={(date) => {
@@ -53,7 +57,6 @@ export const InboxIssueSnoozeModal: FC<InboxIssueSnoozeModalProps> = (props) =>
setDate(date);
}}
mode="single"
className="rounded-md border border-custom-border-200 p-3"
disabled={[
{
before: new Date(),
+1 -1
View File
@@ -55,7 +55,7 @@
"posthog-js": "^1.131.3",
"react": "^18.3.1",
"react-color": "^2.19.3",
"react-day-picker": "^8.10.0",
"react-day-picker": "^9.5.0",
"react-dom": "^18.3.1",
"react-dropzone": "^14.2.3",
"react-hook-form": "7.51.5",
+218 -275
View File
@@ -1,9 +1,9 @@
.rdp {
.rdp-root {
font-size: 12px;
--rdp-cell-size: 40px;
/* Size of the day cells. */
--rdp-caption-font-size: 1.15rem;
--rdp-caption-font-size: 1rem;
/* Font size for the caption labels. */
--rdp-caption-navigation-size: 1.25rem;
/* Font size for the caption labels. */
@@ -21,260 +21,16 @@
background: transparent;
}
/* Hide elements for devices that are not screen readers */
.rdp-vhidden {
.rdp-root {
position: relative; /* Required to position the nav. */
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-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 {
.rdp-day_button {
display: flex;
overflow: hidden;
align-items: center;
@@ -285,14 +41,58 @@
height: var(--rdp-cell-size);
margin: 0;
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;
}
.rdp-day_today:not(.rdp-day_outside)::after {
.rdp-today:not(.rdp-outside)::after {
content: "";
position: absolute;
left: 50%;
@@ -304,31 +104,173 @@
transform: translate(-50%, 0);
}
.rdp-day_selected,
.rdp-day_selected:focus-visible,
.rdp-day_selected:hover {
.rdp-selected .rdp-day_button:focus-visible,
.rdp-selected .rdp-day_button:hover {
color: var(--rdp-selected-color);
opacity: 1;
background-color: var(--rdp-accent-color);
}
.rdp-day_outside:not(.rdp-day_selected) {
opacity: 0.5;
.rdp-weekday {
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;
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),
td:has(.rdp-day_range_middle),
td:has(.rdp-day_range_end) {
.rdp-dropdown:not([data-disabled="true"]) {
&:focus-visible + .rdp-caption_label {
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;
}
td:has(.rdp-day_range_start)::before,
td:has(.rdp-day_range_middle)::before,
td:has(.rdp-day_range_end)::before {
.rdp-range_start::before,
.rdp-range_middle::before,
.rdp-range_end::before {
content: "";
position: absolute;
background-color: var(--rdp-background-color);
@@ -336,33 +278,34 @@ td:has(.rdp-day_range_end)::before {
height: 100%;
width: 50%;
transform: translate(0, -50%);
z-index: -1;
}
td:has(.rdp-day_range_start)::before {
.rdp-range_start::before {
left: 50%;
}
td:has(.rdp-day_range_middle)::before {
.rdp-range_middle::before {
left: 50%;
width: 100%;
transform: translate(-50%, -50%);
}
td:has(.rdp-day_range_end)::before {
.rdp-range_end::before {
right: 50%;
}
td:has(.rdp-day_range_start.rdp-day_range_end)::before {
.rdp-range_start.rdp-range_end::before {
display: none;
}
.rdp-day_range_middle {
.rdp-range_middle .rdp-day_button {
background-color: transparent;
color: inherit;
}
.rdp-day_range_middle:hover,
.rdp-day_range_middle:focus-visible {
background-color: var(--rdp-background-color) !important;
color: inherit !important;
.rdp-day.rdp-range_middle .rdp-day_button:hover,
.rdp-day.rdp-range_middle .rdp-day_button:focus-visible {
background-color: var(--rdp-background-color);
color: inherit;
}