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:
@ -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);
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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 }) =>
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user