-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
46 Date and time picker inputs (#77)
* input error text * initial DatetimeInput component * user form field attrs * calendar icon * datetimeinput internal event logic * profile dateOfBirth * datetime time format * date format * datetime disabled * clean up * format datetime * DateInput component * tests * DatetimeInput refactored * format IonDatetime value * tests * tests * docs
- Loading branch information
Showing
13 changed files
with
486 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,5 @@ export const profileFixture1: Profile = { | |
name: 'Test User', | ||
email: '[email protected]', | ||
bio: 'My name is Test User.', | ||
dateOfBirth: '2002-05-07', | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
ion-input.ls-date-input { | ||
input { | ||
cursor: pointer; | ||
} | ||
} | ||
|
||
ion-modal.ls-date-modal { | ||
--height: fit-content; | ||
--width: fit-content; | ||
|
||
--border-radius: 0.5rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import { ComponentPropsWithoutRef, useMemo, useState } from 'react'; | ||
import { ModalCustomEvent } from '@ionic/core'; | ||
import { DatetimeCustomEvent, IonButton, IonDatetime, IonInput, IonModal } from '@ionic/react'; | ||
import { useField } from 'formik'; | ||
import classNames from 'classnames'; | ||
import dayjs from 'dayjs'; | ||
|
||
import './DateInput.scss'; | ||
import { PropsWithTestId } from '../types'; | ||
import Icon, { IconName } from '../Icon/Icon'; | ||
|
||
/** | ||
* Default `IonDatetime` `formatOptions` for the date. May be overridden by | ||
* supplying a `formatOptions` property. Controls how the date is displayed | ||
* in the form input. | ||
* @see {@link IonDatetime} | ||
*/ | ||
const DEFAULT_FORMAT_DATE: Intl.DateTimeFormatOptions = { | ||
month: 'short', | ||
day: 'numeric', | ||
year: 'numeric', | ||
}; | ||
|
||
/** | ||
* `DateValue` describes the possible types of an `IonDatetime` whose `presentation` | ||
* is 'date'. | ||
*/ | ||
type DateValue = string | null; | ||
|
||
/** | ||
* Properties for the `DateInput` component. | ||
* @see {@link PropsWithTestId} | ||
* @see {@link IonDatetime} | ||
* @see {@link IonInput} | ||
* @see {@link IonModal} | ||
*/ | ||
interface DateInputProps | ||
extends PropsWithTestId, | ||
Pick<ComponentPropsWithoutRef<typeof IonInput>, 'label' | 'labelPlacement'>, | ||
Pick<ComponentPropsWithoutRef<typeof IonModal>, 'onIonModalDidDismiss'>, | ||
Omit<ComponentPropsWithoutRef<typeof IonDatetime>, 'multiple' | 'name' | 'presentation'>, | ||
Required<Pick<ComponentPropsWithoutRef<typeof IonDatetime>, 'name'>> {} | ||
|
||
/** | ||
* The `DateInput` component renders an `IonDatetime` which is integrated with | ||
* Formik. The form field value is displayed in an `IonInput`. When that input | ||
* is clicked, an `IonDatetime` is presented within an `IonModal`. | ||
* | ||
* Use this component when you need to collect a date value within a form. The | ||
* date value will be set as an ISO8601 date, e.g. YYYY-MM-DD | ||
* | ||
* @param {DateInputProps} props - Component properties. | ||
* @returns {JSX.Element} JSX | ||
*/ | ||
const DateInput = ({ | ||
className, | ||
label, | ||
labelPlacement, | ||
onIonModalDidDismiss, | ||
testid = 'input-date', | ||
...datetimeProps | ||
}: DateInputProps): JSX.Element => { | ||
const [field, meta, helpers] = useField<DateValue>(datetimeProps.name); | ||
const [isOpen, setIsOpen] = useState<boolean>(false); | ||
|
||
// populate error text only if the field has been touched and has an error | ||
const errorText: string | undefined = meta.touched ? meta.error : undefined; | ||
|
||
/** | ||
* Handle change events emitted by `IonDatetime`. | ||
* @param {DatetimeCustomEvent} e - The event. | ||
*/ | ||
const onChange = async (e: DatetimeCustomEvent): Promise<void> => { | ||
const value = e.detail.value as DateValue; | ||
if (value) { | ||
const isoDate = dayjs(value).format('YYYY-MM-DD'); | ||
await helpers.setValue(isoDate, true); | ||
} else { | ||
await helpers.setValue(null, true); | ||
} | ||
datetimeProps.onIonChange?.(e); | ||
}; | ||
|
||
/** | ||
* Handle 'did dismiss' events emitted by `IonModal`. | ||
*/ | ||
const onDidDismiss = async (e: ModalCustomEvent): Promise<void> => { | ||
await helpers.setTouched(true, true); | ||
setIsOpen(false); | ||
onIonModalDidDismiss?.(e); | ||
}; | ||
|
||
// format the value to display in the IonInput | ||
// use UTC so that local timezone offset does not change the date | ||
const inputValue = useMemo(() => { | ||
if (field.value) { | ||
const dateOptions: Intl.DateTimeFormatOptions = { | ||
...(datetimeProps.formatOptions?.date ?? DEFAULT_FORMAT_DATE), | ||
timeZone: 'UTC', | ||
}; | ||
|
||
return Intl.DateTimeFormat(undefined, dateOptions).format(new Date(field.value)); | ||
} else { | ||
return ''; | ||
} | ||
}, [datetimeProps.formatOptions, field.value]); | ||
|
||
// format the value for the IonDatetime. it must be a local ISO date or null/undefined | ||
const datetimeValue = useMemo(() => { | ||
return field.value ? dayjs(field.value).format('YYYY-MM-DD[T]HH:mm') : null; | ||
}, [field.value]); | ||
|
||
return ( | ||
<> | ||
<IonInput | ||
className={classNames( | ||
'ls-date-input', | ||
className, | ||
{ 'ion-touched': meta.touched }, | ||
{ 'ion-invalid': meta.error }, | ||
{ 'ion-valid': meta.touched && !meta.error }, | ||
)} | ||
data-testid={testid} | ||
disabled={datetimeProps.disabled} | ||
errorText={errorText} | ||
label={label} | ||
labelPlacement={labelPlacement} | ||
onFocus={() => setIsOpen(true)} | ||
readonly | ||
value={inputValue} | ||
> | ||
<IonButton | ||
aria-hidden="true" | ||
data-testid={`${testid}-button-calendar`} | ||
disabled={datetimeProps.disabled} | ||
fill="clear" | ||
onClick={() => setIsOpen(true)} | ||
slot="end" | ||
> | ||
<Icon icon={IconName.Calendar} /> | ||
</IonButton> | ||
</IonInput> | ||
|
||
<IonModal | ||
className="ls-date-modal" | ||
data-testid={`${testid}-modal`} | ||
isOpen={isOpen} | ||
onIonModalDidDismiss={onDidDismiss} | ||
> | ||
<IonDatetime | ||
{...datetimeProps} | ||
data-testid={`${testid}-datetime`} | ||
multiple={false} | ||
onIonChange={onChange} | ||
presentation="date" | ||
value={datetimeValue} | ||
></IonDatetime> | ||
</IonModal> | ||
</> | ||
); | ||
}; | ||
|
||
export default DateInput; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
ion-input.ls-datetime-input { | ||
input { | ||
cursor: pointer; | ||
} | ||
} | ||
|
||
ion-modal.ls-datetime-modal { | ||
--height: fit-content; | ||
--width: fit-content; | ||
|
||
--border-radius: 0.5rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { ComponentPropsWithoutRef, useMemo, useState } from 'react'; | ||
import { ModalCustomEvent } from '@ionic/core'; | ||
import { DatetimeCustomEvent, IonButton, IonDatetime, IonInput, IonModal } from '@ionic/react'; | ||
import { useField } from 'formik'; | ||
import classNames from 'classnames'; | ||
import dayjs from 'dayjs'; | ||
|
||
import './DatetimeInput.scss'; | ||
import { PropsWithTestId } from '../types'; | ||
import Icon, { IconName } from '../Icon/Icon'; | ||
|
||
/** | ||
* Default `IonDatetime` `formatOptions` for the date. May be overridden by | ||
* supplying a `formatOptions` property. Controls how the date is displayed | ||
* in the form input. | ||
* @see {@link IonDatetime} | ||
*/ | ||
const DEFAULT_FORMAT_DATE: Intl.DateTimeFormatOptions = { | ||
month: 'short', | ||
day: 'numeric', | ||
year: 'numeric', | ||
}; | ||
|
||
/** | ||
* Default `IonDatetime` `formatOptions` for the time. May be overridden by | ||
* supplying a `formatOptions` property. Controls how the time is displayed | ||
* in the form input. | ||
* @see {@link IonDatetime} | ||
*/ | ||
const DEFAULT_FORMAT_TIME: Intl.DateTimeFormatOptions = { | ||
hour: 'numeric', | ||
minute: '2-digit', | ||
}; | ||
|
||
/** | ||
* `DatetimeValue` describes the possible types of an `IonDatetime` whose `presentation` | ||
* is 'date-time'. | ||
*/ | ||
type DatetimeValue = string | null; | ||
|
||
/** | ||
* Properties for the `DatetimeInput` component. | ||
* @see {@link PropsWithTestId} | ||
* @see {@link IonDatetime} | ||
* @see {@link IonInput} | ||
* @see {@link IonModal} | ||
*/ | ||
interface DatetimeInputProps | ||
extends PropsWithTestId, | ||
Pick<ComponentPropsWithoutRef<typeof IonInput>, 'label' | 'labelPlacement'>, | ||
Pick<ComponentPropsWithoutRef<typeof IonModal>, 'onIonModalDidDismiss'>, | ||
Omit<ComponentPropsWithoutRef<typeof IonDatetime>, 'multiple' | 'name' | 'presentation'>, | ||
Required<Pick<ComponentPropsWithoutRef<typeof IonDatetime>, 'name'>> {} | ||
|
||
/** | ||
* The `DatetimeInput` component renders an `IonDatetime` which is integrated with | ||
* Formik. The form field value is displayed in an `IonInput`. When that input | ||
* is clicked, an `IonDatetime` is presented within an `IonModal`. | ||
* | ||
* Use this component when you need to collect a date and time, a timestamp, | ||
* value within a form. The value will be set as an ISO8601 timestamp. | ||
* | ||
* @param {DateInputProps} props - Component properties. | ||
* @returns {JSX.Element} JSX | ||
*/ | ||
const DatetimeInput = ({ | ||
className, | ||
label, | ||
labelPlacement, | ||
onIonModalDidDismiss, | ||
testid = 'input-datetime', | ||
...datetimeProps | ||
}: DatetimeInputProps): JSX.Element => { | ||
const [field, meta, helpers] = useField<DatetimeValue>(datetimeProps.name); | ||
const [isOpen, setIsOpen] = useState<boolean>(false); | ||
|
||
// populate error text only if the field has been touched and has an error | ||
const errorText: string | undefined = meta.touched ? meta.error : undefined; | ||
|
||
/** | ||
* Handle change events emitted by `IonDatetime`. | ||
* @param {DatetimeCustomEvent} e - The event. | ||
*/ | ||
const onChange = async (e: DatetimeCustomEvent): Promise<void> => { | ||
const value = e.detail.value as DatetimeValue; | ||
if (value) { | ||
const isoDate = dayjs(value).toISOString(); | ||
await helpers.setValue(isoDate, true); | ||
} else { | ||
await helpers.setValue(null, true); | ||
} | ||
datetimeProps.onIonChange?.(e); | ||
}; | ||
|
||
/** | ||
* Handle 'did dismiss' events emitted by `IonModal`. | ||
*/ | ||
const onDidDismiss = async (e: ModalCustomEvent): Promise<void> => { | ||
await helpers.setTouched(true, true); | ||
setIsOpen(false); | ||
onIonModalDidDismiss?.(e); | ||
}; | ||
|
||
// format the value to display in the IonInput | ||
const inputValue = useMemo(() => { | ||
if (field.value) { | ||
const date = new Intl.DateTimeFormat( | ||
undefined, | ||
datetimeProps.formatOptions?.date ?? DEFAULT_FORMAT_DATE, | ||
).format(new Date(field.value)); | ||
const time = new Intl.DateTimeFormat( | ||
undefined, | ||
datetimeProps.formatOptions?.time ?? DEFAULT_FORMAT_TIME, | ||
).format(new Date(field.value)); | ||
|
||
return `${date} ${time}`; | ||
} else { | ||
return ''; | ||
} | ||
}, [datetimeProps.formatOptions, field.value]); | ||
|
||
// format the value for the IonDatetime. it must be a local ISO date or null/undefined | ||
const datetimeValue = useMemo(() => { | ||
return field.value ? dayjs(field.value).format('YYYY-MM-DD[T]HH:mm') : null; | ||
}, [field.value]); | ||
|
||
return ( | ||
<> | ||
<IonInput | ||
className={classNames( | ||
'ls-datetime-input', | ||
className, | ||
{ 'ion-touched': meta.touched }, | ||
{ 'ion-invalid': meta.error }, | ||
{ 'ion-valid': meta.touched && !meta.error }, | ||
)} | ||
data-testid={testid} | ||
disabled={datetimeProps.disabled} | ||
errorText={errorText} | ||
label={label} | ||
labelPlacement={labelPlacement} | ||
onFocus={() => setIsOpen(true)} | ||
readonly | ||
value={inputValue} | ||
> | ||
<IonButton | ||
aria-hidden="true" | ||
data-testid={`${testid}-button-calendar`} | ||
disabled={datetimeProps.disabled} | ||
fill="clear" | ||
onClick={() => setIsOpen(true)} | ||
slot="end" | ||
> | ||
<Icon icon={IconName.Calendar} /> | ||
</IonButton> | ||
</IonInput> | ||
|
||
<IonModal | ||
className="ls-datetime-modal" | ||
data-testid={`${testid}-modal`} | ||
isOpen={isOpen} | ||
onIonModalDidDismiss={onDidDismiss} | ||
> | ||
<IonDatetime | ||
{...datetimeProps} | ||
data-testid={`${testid}-datetime`} | ||
multiple={false} | ||
onIonChange={onChange} | ||
presentation="date-time" | ||
value={datetimeValue} | ||
></IonDatetime> | ||
</IonModal> | ||
</> | ||
); | ||
}; | ||
|
||
export default DatetimeInput; |
Oops, something went wrong.