103
front/src/modules/ui/input/components/DateInput.tsx
Normal file
103
front/src/modules/ui/input/components/DateInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>;
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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') },
|
||||
|
||||
@ -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 = {};
|
||||
|
||||
Reference in New Issue
Block a user