Fixed date picker UI that was too overloaded (#5039)

Date picker UI was off because of the recent refactor with new field
types Date and DateTime. We had to allow the date picker to edit both.

In this PR we come back to the previous design and we only use the input
to modify time.

Also we use our Select component instead of the ones from the library
`react-datepicker`

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Lucas Bordeau
2024-04-23 18:45:32 +02:00
committed by GitHub
parent 2dc89b8f1d
commit 444e97fa3e
7 changed files with 66 additions and 211 deletions

View File

@ -126,9 +126,9 @@ type Story = StoryObj<typeof DateFieldInputWithContext>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const div = await canvas.findByText('February - 2022');
const div = await canvas.findByText('February');
await expect(div.innerText).toContain('February - 2022');
await expect(div.innerText).toContain('February');
},
};
@ -138,7 +138,7 @@ export const ClickOutside: Story = {
await expect(clickOutsideJestFn).toHaveBeenCalledTimes(0);
await canvas.findByText('February - 2022');
await canvas.findByText('February');
const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div');
await userEvent.click(emptyDiv);
@ -151,7 +151,7 @@ export const Escape: Story = {
await expect(escapeJestFn).toHaveBeenCalledTimes(0);
const canvas = within(canvasElement);
await canvas.findByText('February - 2022');
await canvas.findByText('February');
await userEvent.keyboard('{escape}');
await expect(escapeJestFn).toHaveBeenCalledTimes(1);
@ -163,7 +163,7 @@ export const Enter: Story = {
await expect(enterJestFn).toHaveBeenCalledTimes(0);
const canvas = within(canvasElement);
await canvas.findByText('February - 2022');
await canvas.findByText('February');
await userEvent.keyboard('{enter}');
await expect(enterJestFn).toHaveBeenCalledTimes(1);

View File

@ -2,12 +2,12 @@ import { useRef, useState } from 'react';
import styled from '@emotion/styled';
import { Nullable } from 'twenty-ui';
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
import {
InternalDatePicker,
MONTH_AND_YEAR_DROPDOWN_ID,
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
} from '@/ui/input/components/internal/date/components/MonthAndYearDropdown';
} from '@/ui/input/components/internal/date/components/InternalDatePicker';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';

View File

@ -62,6 +62,7 @@ const StyledButton = styled.button<
white-space: nowrap;
width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
min-width: ${({ size }) => (size === 'small' ? '24px' : '32px')};
&:hover {
background: ${({ theme, disabled }) =>

View File

@ -6,13 +6,7 @@ import { IconCalendarX, IconChevronLeft, IconChevronRight } from 'twenty-ui';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
import {
MONTH_AND_YEAR_DROPDOWN_ID,
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
MonthAndYearDropdown,
} from '@/ui/input/components/internal/date/components/MonthAndYearDropdown';
import { TimeInput } from '@/ui/input/components/internal/date/components/TimeInput';
import { Select } from '@/ui/input/components/Select';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
@ -21,6 +15,32 @@ import { isDefined } from '~/utils/isDefined';
import 'react-datepicker/dist/react-datepicker.css';
export const months = [
{ label: 'January', value: 0 },
{ label: 'February', value: 1 },
{ label: 'March', value: 2 },
{ label: 'April', value: 3 },
{ label: 'May', value: 4 },
{ label: 'June', value: 5 },
{ label: 'July', value: 6 },
{ label: 'August', value: 7 },
{ label: 'September', value: 8 },
{ label: 'October', value: 9 },
{ label: 'November', value: 10 },
{ label: 'December', value: 11 },
];
export const years = Array.from(
{ length: 200 },
(_, i) => new Date().getFullYear() + 5 - i,
).map((year) => ({ label: year.toString(), value: year }));
export const MONTH_AND_YEAR_DROPDOWN_ID = 'date-picker-month-and-year-dropdown';
export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID =
'date-picker-month-and-year-dropdown-month-select';
export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
'date-picker-month-and-year-dropdown-year-select';
const StyledContainer = styled.div`
& .react-datepicker {
border-color: ${({ theme }) => theme.border.color.light};
@ -252,10 +272,10 @@ const StyledContainer = styled.div`
`;
const StyledButtonContainer = styled(StyledHoverableMenuItemBase)`
width: auto;
height: ${({ theme }) => theme.spacing(8)};
padding: 0 ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(4)};
margin: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)};
width: auto;
`;
const StyledButton = styled(MenuItemLeftContent)`
@ -273,13 +293,6 @@ const StyledCustomDatePickerHeader = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledMonthText = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-family: ${({ theme }) => theme.font.family};
font-size: ${({ theme }) => theme.font.size.md};
padding: ${({ theme }) => theme.spacing(2)};
`;
export type InternalDatePickerProps = {
date: Date;
onMouseSelect?: (date: Date | null) => void;
@ -305,9 +318,6 @@ export const InternalDatePicker = ({
}: InternalDatePickerProps) => {
const internalDate = date ?? new Date();
const monthLabel = DateTime.fromJSDate(internalDate).toFormat('LLLL');
const yearLabel = DateTime.fromJSDate(internalDate).toFormat('yyyy');
const { closeDropdown } = useDropdown(MONTH_AND_YEAR_DROPDOWN_ID);
const { closeDropdown: closeDropdownMonthSelect } = useDropdown(
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
@ -357,6 +367,18 @@ export const InternalDatePicker = ({
}
};
const handleChangeMonth = (month: number) => {
const newDate = new Date(date);
newDate.setMonth(month);
onChange?.(newDate);
};
const handleChangeYear = (year: number) => {
const newDate = new Date(date);
newDate.setFullYear(year);
onChange?.(newDate);
};
return (
<StyledContainer onKeyDown={handleKeyDown}>
<div className={clearable ? 'clearable ' : ''}>
@ -393,10 +415,22 @@ export const InternalDatePicker = ({
onChange={onChange}
/>
<StyledCustomDatePickerHeader>
{isDateTimeInput && (
<TimeInput date={internalDate} onChange={onChange} />
)}
<MonthAndYearDropdown date={internalDate} onChange={onChange} />
<Select
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
options={months}
disableBlur
onChange={handleChangeMonth}
value={date.getMonth()}
fullWidth
/>
<Select
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
onChange={handleChangeYear}
value={date.getFullYear()}
options={years}
disableBlur
fullWidth
/>
<LightIconButton
Icon={IconChevronLeft}
onClick={() => decreaseMonth()}
@ -410,9 +444,6 @@ export const InternalDatePicker = ({
disabled={nextMonthButtonDisabled}
/>
</StyledCustomDatePickerHeader>
<StyledMonthText>
{monthLabel} - {yearLabel}
</StyledMonthText>
</>
)}
onSelect={(date: Date, event) => {

View File

@ -1,91 +0,0 @@
import { IconCalendarDue } from 'twenty-ui';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { Select } from '@/ui/input/components/Select';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
type MonthAndYearDropdownProps = {
date: Date;
onChange?: (newDate: Date) => void;
};
const months = [
{ label: 'January', value: 0 },
{ label: 'February', value: 1 },
{ label: 'March', value: 2 },
{ label: 'April', value: 3 },
{ label: 'May', value: 4 },
{ label: 'June', value: 5 },
{ label: 'July', value: 6 },
{ label: 'August', value: 7 },
{ label: 'September', value: 8 },
{ label: 'October', value: 9 },
{ label: 'November', value: 10 },
{ label: 'December', value: 11 },
];
const years = Array.from(
{ length: 200 },
(_, i) => new Date().getFullYear() + 5 - i,
).map((year) => ({ label: year.toString(), value: year }));
export const MONTH_AND_YEAR_DROPDOWN_ID = 'date-picker-month-and-year-dropdown';
export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID =
'date-picker-month-and-year-dropdown-month-select';
export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID =
'date-picker-month-and-year-dropdown-year-select';
export const MonthAndYearDropdown = ({
date,
onChange,
}: MonthAndYearDropdownProps) => {
const handleChangeMonth = (month: number) => {
const newDate = new Date(date);
newDate.setMonth(month);
onChange?.(newDate);
};
const handleChangeYear = (year: number) => {
const newDate = new Date(date);
newDate.setFullYear(year);
onChange?.(newDate);
};
return (
<Dropdown
dropdownId={MONTH_AND_YEAR_DROPDOWN_ID}
dropdownHotkeyScope={{
scope: MONTH_AND_YEAR_DROPDOWN_ID,
}}
dropdownPlacement="bottom-start"
clickableComponent={
<LightIconButton
testId="month-and-year-dropdown"
Icon={IconCalendarDue}
size="medium"
/>
}
dropdownComponents={
<DropdownMenuItemsContainer>
<Select
dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID}
options={months}
fullWidth
disableBlur
onChange={handleChangeMonth}
value={date.getMonth()}
/>
<Select
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
onChange={handleChangeYear}
value={date.getFullYear()}
options={years}
fullWidth
disableBlur
/>
</DropdownMenuItemsContainer>
}
/>
);
};

View File

@ -1,80 +0,0 @@
import { useEffect } from 'react';
import { useIMask } from 'react-imask';
import styled from '@emotion/styled';
import { DateTime } from 'luxon';
import { IconClockHour8 } from 'twenty-ui';
import { TIME_BLOCKS } from '@/ui/input/components/internal/date/constants/TimeBlocks';
import { TIME_MASK } from '@/ui/input/components/internal/date/constants/TimeMask';
const StyledIconClock = styled(IconClockHour8)`
position: absolute;
`;
const StyledTimeInputContainer = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
margin-right: 0;
padding: 0 ${({ theme }) => theme.spacing(2)};
text-align: left;
width: 136px;
height: 32px;
gap: ${({ theme }) => theme.spacing(1)};
z-index: 10;
`;
const StyledTimeInput = styled.input`
background: transparent;
border: none;
color: ${({ theme }) => theme.font.color.primary};
outline: none;
font-weight: 500;
font-size: ${({ theme }) => theme.font.size.md};
margin-left: ${({ theme }) => theme.spacing(5)};
`;
type TimeInputProps = {
onChange?: (date: Date) => void;
date: Date;
};
export const TimeInput = ({ date, onChange }: TimeInputProps) => {
const handleComplete = (value: string) => {
const [hours, minutes] = value.split(':');
const newDate = new Date(date);
newDate.setHours(parseInt(hours, 10));
newDate.setMinutes(parseInt(minutes, 10));
onChange?.(newDate);
};
const { ref, setValue } = useIMask(
{
mask: TIME_MASK,
blocks: TIME_BLOCKS,
lazy: false,
},
{
onComplete: handleComplete,
},
);
useEffect(() => {
const formattedDate = DateTime.fromJSDate(date).toFormat('HH:mm');
setValue(formattedDate);
}, [date, setValue]);
return (
<StyledTimeInputContainer>
<StyledIconClock size={16} />
<StyledTimeInput type="text" ref={ref as any} />
</StyledTimeInputContainer>
);
};

View File

@ -23,12 +23,6 @@ export const WithOpenMonthSelect: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const monthAndYearButton = await canvas.findByTestId(
'month-and-year-dropdown',
);
await userEvent.click(monthAndYearButton);
const monthSelect = await canvas.findByText('January');
await userEvent.click(monthSelect);