Skip to content

Commit

Permalink
fixes for #2637
Browse files Browse the repository at this point in the history
  • Loading branch information
bjosttveit committed Oct 25, 2024
1 parent 9fabcb0 commit 0e95d53
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 142 deletions.
2 changes: 1 addition & 1 deletion src/layout/Datepicker/DatePickerCalendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const DatePickerCalendar = ({
}}
locale={currentLocale}
today={new Date()}
month={selectedDate}
defaultMonth={selectedDate}
disabled={[{ before: minDate, after: maxDate }]}
weekStartsOn={1}
mode='single'
Expand Down
121 changes: 48 additions & 73 deletions src/layout/Datepicker/DatePickerInput.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import React, { forwardRef, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { PatternFormat } from 'react-number-format';
import type { RefObject } from 'react';

import { Button, Textfield } from '@digdir/designsystemet-react';
import { CalendarIcon } from '@navikt/aksel-icons';
import { Textfield } from '@digdir/designsystemet-react';
import { format, isValid } from 'date-fns';

import { useLanguage } from 'src/features/language/useLanguage';
import styles from 'src/layout/Datepicker/Calendar.module.css';
import { getSaveFormattedDateString, strictParseFormat, strictParseISO } from 'src/utils/dateHelpers';
import { getFormatPattern } from 'src/utils/formatDateLocale';
Expand All @@ -17,78 +14,56 @@ export interface DatePickerInputProps {
timeStamp: boolean;
value?: string;
onValueChange?: (value: string) => void;
onClick?: () => void;
isDialogOpen?: boolean;
readOnly?: boolean;
}

export const DatePickerInput = forwardRef(
(
{ id, value, datepickerFormat, timeStamp, onValueChange, isDialogOpen, readOnly, onClick }: DatePickerInputProps,
ref: RefObject<HTMLButtonElement>,
) => {
const formatPattern = getFormatPattern(datepickerFormat);
const dateValue = strictParseISO(value);
const formattedDateValue = dateValue ? format(dateValue, datepickerFormat) : value;
const [inputValue, setInputValue] = useState(formattedDateValue ?? '');
export function DatePickerInput({
id,
value,
datepickerFormat,
timeStamp,
onValueChange,
readOnly,
}: DatePickerInputProps) {
const formatPattern = getFormatPattern(datepickerFormat);
const dateValue = strictParseISO(value);
const formattedDateValue = dateValue ? format(dateValue, datepickerFormat) : value;
const [inputValue, setInputValue] = useState(formattedDateValue ?? '');

useEffect(() => {
setInputValue(formattedDateValue ?? '');
}, [formattedDateValue]);
useEffect(() => {
setInputValue(formattedDateValue ?? '');
}, [formattedDateValue]);

const saveValue = (e: React.ChangeEvent<HTMLInputElement>) => {
const stringValue = e.target.value;
const date = strictParseFormat(stringValue, datepickerFormat);
const valueToSave = getSaveFormattedDateString(date, timeStamp) ?? stringValue;
onValueChange && onValueChange(valueToSave);
};
const saveValue = (e: React.ChangeEvent<HTMLInputElement>) => {
const stringValue = e.target.value;
const date = strictParseFormat(stringValue, datepickerFormat);
const valueToSave = getSaveFormattedDateString(date, timeStamp) ?? stringValue;
onValueChange && onValueChange(valueToSave);
};

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const stringValue = e.target.value;
setInputValue(stringValue);
// If the date is valid, save immediately
if (stringValue.length == 0 || isValid(strictParseFormat(stringValue, datepickerFormat))) {
saveValue(e);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const stringValue = e.target.value;
setInputValue(stringValue);
// If the date is valid, save immediately
if (stringValue.length == 0 || isValid(strictParseFormat(stringValue, datepickerFormat))) {
saveValue(e);
}
};

const { langAsString } = useLanguage();

return (
<div className={styles.calendarInputWrapper}>
<PatternFormat
format={formatPattern}
customInput={Textfield}
mask='_'
className={styles.calendarInput}
type='text'
id={id}
value={inputValue}
placeholder={datepickerFormat.toUpperCase()}
onChange={handleChange}
onBlur={saveValue}
readOnly={readOnly}
aria-readonly={readOnly}
/>
<Button
id={`${id}-button`}
variant='tertiary'
icon={true}
aria-controls='dialog'
aria-haspopup='dialog'
onClick={onClick}
aria-expanded={isDialogOpen}
aria-label={langAsString('date_picker.aria_label_icon')}
ref={ref}
disabled={readOnly}
color='first'
size='small'
>
<CalendarIcon title={langAsString('date_picker.aria_label_icon')} />
</Button>
</div>
);
},
);

DatePickerInput.displayName = 'DatePickerInput';
return (
<PatternFormat
format={formatPattern}
customInput={Textfield}
mask='_'
className={styles.calendarInput}
type='text'
id={id}
value={inputValue}
placeholder={datepickerFormat.toUpperCase()}
onChange={handleChange}
onBlur={saveValue}
readOnly={readOnly}
aria-readonly={readOnly}
/>
);
}
99 changes: 40 additions & 59 deletions src/layout/Datepicker/DatepickerComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useRef, useState } from 'react';
import type { ReactNode } from 'react';
import React, { useState } from 'react';

import { Modal, Popover } from '@digdir/designsystemet-react';
import { Button } from '@digdir/designsystemet-react';
import { Grid } from '@material-ui/core';
import { CalendarIcon } from '@navikt/aksel-icons';
import { formatDate, isValid as isValidDate } from 'date-fns';

import { useDataModelBindings } from 'src/features/formData/useDataModelBindings';
Expand All @@ -12,6 +12,7 @@ import { useIsMobile } from 'src/hooks/useDeviceWidths';
import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper';
import styles from 'src/layout/Datepicker/Calendar.module.css';
import { DatePickerCalendar } from 'src/layout/Datepicker/DatePickerCalendar';
import { DatePickerDialog } from 'src/layout/Datepicker/DatepickerDialog';
import { DatePickerInput } from 'src/layout/Datepicker/DatePickerInput';
import { getDateConstraint, getDateFormat, getSaveFormattedDateString, strictParseISO } from 'src/utils/dateHelpers';
import { getDatepickerFormat } from 'src/utils/formatDateLocale';
Expand All @@ -26,9 +27,7 @@ export function DatepickerComponent({ node }: IDatepickerProps) {
const { langAsString } = useLanguage();
const languageLocale = useCurrentLanguage();
const { minDate, maxDate, format, timeStamp = true, readOnly, required, id, dataModelBindings } = useNodeItem(node);

const [isDialogOpen, setIsDialogOpen] = useState(false);
const modalRef = useRef<HTMLDialogElement>(null);

const calculatedMinDate = getDateConstraint(minDate, 'min');
const calculatedMaxDate = getDateConstraint(maxDate, 'max');
Expand All @@ -44,51 +43,13 @@ export function DatepickerComponent({ node }: IDatepickerProps) {
if (date && isValidDate(date)) {
setValue('simpleBinding', getSaveFormattedDateString(date, timeStamp));
}
modalRef.current?.close();
setIsDialogOpen(false);
};

const handleInputValueChange = (isoDateString: string) => {
setValue('simpleBinding', isoDateString);
};

const renderModal = (trigger: ReactNode, content: ReactNode) =>
isMobile ? (
<>
{trigger}
<Modal
role='dialog'
ref={modalRef}
onInteractOutside={() => modalRef.current?.close()}
style={{ width: 'fit-content', minWidth: 'fit-content' }}
>
<Modal.Content>{content}</Modal.Content>
</Modal>
</>
) : (
<Popover
portal={false}
open={isDialogOpen}
onClose={() => setIsDialogOpen(false)}
size='lg'
placement='top'
>
<Popover.Trigger
onClick={() => setIsDialogOpen(!isDialogOpen)}
asChild={true}
>
{trigger}
</Popover.Trigger>
<Popover.Content
className={styles.calendarWrapper}
aria-modal
autoFocus={true}
>
{content}
</Popover.Content>
</Popover>
);

return (
<ComponentStructureWrapper
node={node}
Expand All @@ -100,29 +61,49 @@ export function DatepickerComponent({ node }: IDatepickerProps) {
item
xs={12}
>
{renderModal(
<div className={styles.calendarInputWrapper}>
<DatePickerInput
id={id}
value={value}
isDialogOpen={isMobile ? modalRef.current?.open : isDialogOpen}
datepickerFormat={dateFormat}
timeStamp={timeStamp}
onValueChange={handleInputValueChange}
onClick={() => (isMobile ? modalRef.current?.showModal() : setIsDialogOpen(!isDialogOpen))}
readOnly={readOnly}
/>,
<DatePickerCalendar
id={id}
locale={languageLocale}
selectedDate={dayPickerDate}
isOpen={isDialogOpen}
onSelect={handleDayPickerSelect}
minDate={calculatedMinDate}
maxDate={calculatedMaxDate}
required={required}
autoFocus={isMobile}
/>,
)}
/>
<DatePickerDialog
isDialogOpen={isDialogOpen}
setIsDialogOpen={setIsDialogOpen}
trigger={
<Button
id={`${id}-button`}
variant='tertiary'
icon={true}
aria-controls='dialog'
aria-haspopup='dialog'
onClick={() => setIsDialogOpen(!isDialogOpen)}
aria-expanded={isDialogOpen}
aria-label={langAsString('date_picker.aria_label_icon')}
disabled={readOnly}
color='first'
size='small'
>
<CalendarIcon title={langAsString('date_picker.aria_label_icon')} />
</Button>
}
>
<DatePickerCalendar
id={id}
locale={languageLocale}
selectedDate={dayPickerDate}
isOpen={isDialogOpen}
onSelect={handleDayPickerSelect}
minDate={calculatedMinDate}
maxDate={calculatedMaxDate}
required={required}
autoFocus={isMobile}
/>
</DatePickerDialog>
</div>
</Grid>
<span className={`${styles.formatText} no-visual-testing`}>
{langAsString('date_picker.format_text', [formatDate(new Date(), dateFormat)])}
Expand Down
61 changes: 61 additions & 0 deletions src/layout/Datepicker/DatepickerDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useEffect, useRef } from 'react';
import type { PropsWithChildren, ReactNode } from 'react';

import { Modal, Popover } from '@digdir/designsystemet-react';

import { useIsMobile } from 'src/hooks/useDeviceWidths';
import styles from 'src/layout/Datepicker/Calendar.module.css';

export function DatePickerDialog({
children,
trigger,
isDialogOpen,
setIsDialogOpen,
}: PropsWithChildren<{ trigger: ReactNode; isDialogOpen: boolean; setIsDialogOpen: (open: boolean) => void }>) {
const isMobile = useIsMobile();
const modalRef = useRef<HTMLDialogElement>(null);

useEffect(() => {
isDialogOpen && modalRef.current?.showModal();
!isDialogOpen && modalRef.current?.close();
}, [isMobile, isDialogOpen]);

if (isMobile) {
return (
<>
{trigger}
<Modal
role='dialog'
ref={modalRef}
onInteractOutside={() => setIsDialogOpen(false)}
style={{ width: 'fit-content', minWidth: 'fit-content' }}
>
<Modal.Content>{children}</Modal.Content>
</Modal>
</>
);
}
return (
<Popover
portal={true}
open={isDialogOpen}
onClose={() => setIsDialogOpen(false)}
size='lg'
placement='top'
>
<Popover.Trigger
onClick={() => setIsDialogOpen(!isDialogOpen)}
asChild={true}
>
{trigger}
</Popover.Trigger>
<Popover.Content
className={styles.calendarWrapper}
aria-modal
autoFocus={true}
>
{children}
</Popover.Content>
</Popover>
);
}
Loading

0 comments on commit 0e95d53

Please sign in to comment.