Refactor/display input part 2 (#1555)

* Email - Money - Number

* Date
This commit is contained in:
Lucas Bordeau
2023-09-12 20:04:26 +02:00
committed by GitHub
parent 9b495ae2e8
commit 9b5e24105b
33 changed files with 348 additions and 295 deletions

View File

@ -0,0 +1,103 @@
import { useEffect, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
import { Nullable } from '~/types/Nullable';
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
import { DatePicker } from './DatePicker';
const StyledCalendarContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
margin-top: 1px;
position: absolute;
z-index: 1;
`;
const StyledInputContainer = styled.div`
padding: ${({ theme }) => theme.spacing(0)} ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export type DateInputEditProps = {
value: Nullable<Date>;
onEnter: (newDate: Nullable<Date>) => void;
onEscape: (newDate: Nullable<Date>) => void;
onClickOutside: (
event: MouseEvent | TouchEvent,
newDate: Nullable<Date>,
) => void;
hotkeyScope: string;
};
export function DateInput({
value,
hotkeyScope,
onEnter,
onEscape,
onClickOutside,
}: DateInputEditProps) {
const theme = useTheme();
const [internalValue, setInternalValue] = useState(value);
const wrapperRef = useRef(null);
const { refs, floatingStyles } = useFloating({
placement: 'bottom-start',
middleware: [
flip(),
offset({
mainAxis: theme.spacingMultiplicator * 2,
}),
],
});
function handleChange(newDate: Date) {
setInternalValue(newDate);
}
useEffect(() => {
setInternalValue(value);
}, [value]);
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalValue,
onEnter,
onEscape,
onClickOutside,
hotkeyScope,
});
return (
<div ref={wrapperRef}>
<div ref={refs.setReference}>
<StyledInputContainer>
<DateDisplay value={internalValue} />
</StyledInputContainer>
</div>
<div ref={refs.setFloating} style={floatingStyles}>
<StyledCalendarContainer>
<DatePicker
date={internalValue ?? new Date()}
onChange={handleChange}
onMouseSelect={(newDate: Date) => {
onEnter(newDate);
}}
/>
</StyledCalendarContainer>
</div>
</div>
);
}

View File

@ -1,9 +0,0 @@
import { formatToHumanReadableDate } from '~/utils';
type OwnProps = {
value: Date | string | null;
};
export function DateInputDisplay({ value }: OwnProps) {
return <div>{value && formatToHumanReadableDate(value)}</div>;
}

View File

@ -1,64 +0,0 @@
import { forwardRef } from 'react';
import styled from '@emotion/styled';
import { formatToHumanReadableDate } from '~/utils';
import DatePicker from './DatePicker';
type StyledCalendarContainerProps = {
editModeHorizontalAlign?: 'left' | 'right';
};
const StyledInputContainer = styled.div`
color: ${({ theme }) => theme.font.color.primary};
display: flex;
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledCalendarContainer = styled.div<StyledCalendarContainerProps>`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
margin-top: 1px;
position: absolute;
z-index: 1;
`;
type DivProps = React.HTMLProps<HTMLDivElement>;
const DateDisplay = forwardRef<HTMLDivElement, DivProps>(
({ value, onClick }, ref) => (
<StyledInputContainer onClick={onClick} ref={ref}>
{value && formatToHumanReadableDate(new Date(value as string))}
</StyledInputContainer>
),
);
type DatePickerContainerProps = {
children: React.ReactNode;
};
const DatePickerContainer = ({ children }: DatePickerContainerProps) => {
return <StyledCalendarContainer>{children}</StyledCalendarContainer>;
};
export type DateInputEditProps = {
value: Date | null | undefined;
onChange: (newDate: Date) => void;
};
export function DateInputEdit({ onChange, value }: DateInputEditProps) {
return (
<DatePicker
date={value ?? new Date()}
onChangeHandler={onChange}
customInput={<DateDisplay />}
customCalendarContainer={DatePickerContainer}
/>
);
}

View File

@ -1,18 +1,11 @@
import React, { forwardRef, ReactElement, useState } from 'react';
import ReactDatePicker, { CalendarContainerProps } from 'react-datepicker';
import React from 'react';
import ReactDatePicker from 'react-datepicker';
import styled from '@emotion/styled';
import { overlayBackground } from '@/ui/theme/constants/effects';
import 'react-datepicker/dist/react-datepicker.css';
export type DatePickerProps = {
date: Date;
onChangeHandler: (date: Date) => void;
customInput?: ReactElement;
customCalendarContainer?(props: CalendarContainerProps): React.ReactNode;
};
const StyledContainer = styled.div`
& .react-datepicker {
border-color: ${({ theme }) => theme.border.color.light};
@ -39,6 +32,10 @@ const StyledContainer = styled.div`
display: none;
}
& .react-datepicker-wrapper {
display: none;
}
// Header
& .react-datepicker__header {
@ -223,47 +220,32 @@ const StyledContainer = styled.div`
}
`;
function DatePicker({
date,
onChangeHandler,
customInput,
customCalendarContainer,
}: DatePickerProps) {
const [startDate, setStartDate] = useState(date);
type DivProps = React.HTMLProps<HTMLDivElement>;
const DefaultDateDisplay = forwardRef<HTMLDivElement, DivProps>(
({ value, onClick }, ref) => (
<div onClick={onClick} ref={ref}>
{value &&
new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(new Date(value as string))}
</div>
),
);
export type DatePickerProps = {
date: Date;
onMouseSelect?: (date: Date) => void;
onChange?: (date: Date) => void;
};
export function DatePicker({ date, onChange, onMouseSelect }: DatePickerProps) {
return (
<StyledContainer>
<ReactDatePicker
open={true}
selected={startDate}
selected={date}
showMonthDropdown
showYearDropdown
onChange={(date: Date) => {
setStartDate(date);
onChangeHandler(date);
onChange={() => {
// We need to use onSelect here but onChange is almost redundant with onSelect but is required
}}
customInput={<></>}
onSelect={(date: Date, event) => {
if (event?.type === 'click') {
onMouseSelect?.(date);
} else {
onChange?.(date);
}
}}
customInput={customInput ? customInput : <DefaultDateDisplay />}
calendarContainer={
customCalendarContainer ? customCalendarContainer : undefined
}
/>
</StyledContainer>
);
}
export default DatePicker;

View File

@ -1,27 +0,0 @@
import { MouseEvent } from 'react';
import { ContactLink } from '@/ui/link/components/ContactLink';
function validateEmail(email: string) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email.trim());
}
type OwnProps = {
value: string | null;
};
export function EmailInputDisplay({ value }: OwnProps) {
return value && validateEmail(value) ? (
<ContactLink
href={`mailto:${value}`}
onClick={(event: MouseEvent<HTMLElement>) => {
event.stopPropagation();
}}
>
{value}
</ContactLink>
) : (
<ContactLink href="#">{value}</ContactLink>
);
}

View File

@ -4,14 +4,13 @@ import { userEvent, within } from '@storybook/testing-library';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import DatePicker from '../DatePicker';
import { DatePicker } from '../DatePicker';
const meta: Meta<typeof DatePicker> = {
title: 'UI/Input/DatePicker',
component: DatePicker,
decorators: [ComponentDecorator],
argTypes: {
customInput: { control: false },
date: { control: 'date' },
},
args: { date: new Date('January 1, 2023 00:00:00') },

View File

@ -2,11 +2,11 @@ import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { EmailInputDisplay } from '../EmailInputDisplay';
import { EmailDisplay } from '../../../content-display/components/EmailDisplay';
const meta: Meta = {
title: 'UI/Input/EmailInputDisplay',
component: EmailInputDisplay,
component: EmailDisplay,
decorators: [ComponentWithRouterDecorator],
args: {
value: 'mustajab.ikram@google.com',
@ -15,6 +15,6 @@ const meta: Meta = {
export default meta;
type Story = StoryObj<typeof EmailInputDisplay>;
type Story = StoryObj<typeof EmailDisplay>;
export const Default: Story = {};