From c28d8fd91ae6571a6548e7a9bbe7d53f9f7135df Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 30 Jul 2024 14:19:26 -0400 Subject: [PATCH] (feat) O3-3505: Datepicker improvements --- packages/framework/esm-framework/docs/API.md | 4 +- .../docs/interfaces/OpenmrsDatePickerProps.md | 53 +++-- .../src/datepicker/datepicker.module.scss | 1 - .../esm-styleguide/src/datepicker/index.tsx | 212 ++++++++++++++++-- 4 files changed, 230 insertions(+), 40 deletions(-) diff --git a/packages/framework/esm-framework/docs/API.md b/packages/framework/esm-framework/docs/API.md index f009405bb..10b7c697e 100644 --- a/packages/framework/esm-framework/docs/API.md +++ b/packages/framework/esm-framework/docs/API.md @@ -529,7 +529,7 @@ A type for any of the acceptable date formats #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:58](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L58) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:73](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L73) ___ @@ -1559,7 +1559,7 @@ A date picker component to select a single date. Based on React Aria, but styled #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:251](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L251) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:402](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L402) ___ diff --git a/packages/framework/esm-framework/docs/interfaces/OpenmrsDatePickerProps.md b/packages/framework/esm-framework/docs/interfaces/OpenmrsDatePickerProps.md index 7fce86b13..7f0c8b4f0 100644 --- a/packages/framework/esm-framework/docs/interfaces/OpenmrsDatePickerProps.md +++ b/packages/framework/esm-framework/docs/interfaces/OpenmrsDatePickerProps.md @@ -6,7 +6,7 @@ Properties for the OpenmrsDatePicker ## Hierarchy -- `Omit`<`DatePickerProps`<`CalendarDate`\>, ``"className"`` \| ``"defaultValue"`` \| ``"value"``\> +- `Omit`<`DatePickerProps`<`CalendarDate`\>, ``"className"`` \| ``"onChange"`` \| ``"defaultValue"`` \| ``"value"``\> ↳ **`OpenmrsDatePickerProps`** @@ -58,6 +58,7 @@ Properties for the OpenmrsDatePicker - [isDateUnavailable](OpenmrsDatePickerProps.md#isdateunavailable) - [onBlur](OpenmrsDatePickerProps.md#onblur) - [onChange](OpenmrsDatePickerProps.md#onchange) +- [onChangeRaw](OpenmrsDatePickerProps.md#onchangeraw) - [onFocus](OpenmrsDatePickerProps.md#onfocus) - [onFocusChange](OpenmrsDatePickerProps.md#onfocuschange) - [onKeyDown](OpenmrsDatePickerProps.md#onkeydown) @@ -171,7 +172,7 @@ Any CSS classes to add to the outer div of the date picker #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:76](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L76) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:91](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L91) ___ @@ -199,7 +200,7 @@ The default value (uncontrolled) #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:78](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L78) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:93](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L93) ___ @@ -277,7 +278,7 @@ Whether the input value is invalid. #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:80](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L80) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:95](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L95) ___ @@ -289,7 +290,7 @@ Text to show if the input is invalid e.g. an error message #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:82](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L82) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:97](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L97) ___ @@ -383,7 +384,7 @@ The label for this DatePicker element #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:87](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L87) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:102](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L102) ___ @@ -395,7 +396,7 @@ The label for this DatePicker element. #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:89](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L89) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:104](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L104) ___ @@ -407,7 +408,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:91](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L91) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:106](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L106) ___ @@ -419,7 +420,7 @@ The latest date it is possible to select #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:93](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L93) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:108](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L108) ___ @@ -447,7 +448,7 @@ The earliest date it is possible to select #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:95](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L95) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:110](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L110) ___ @@ -525,7 +526,7 @@ ___ #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:99](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L99) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:118](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L118) ___ @@ -572,7 +573,7 @@ Specifies the size of the input. Currently supports either `sm`, `md`, or `lg` a #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:97](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L97) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:116](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L116) ___ @@ -637,7 +638,7 @@ The value (controlled) #### Defined in -[packages/framework/esm-styleguide/src/datepicker/index.tsx:101](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L101) +[packages/framework/esm-styleguide/src/datepicker/index.tsx:120](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L120) ## Methods @@ -703,19 +704,37 @@ Handler that is called when the value changes. | Name | Type | | :------ | :------ | -| `value` | `C` | +| `value` | `undefined` \| ``null`` \| `Date` | #### Returns `void` -#### Inherited from +#### Defined in + +[packages/framework/esm-styleguide/src/datepicker/index.tsx:112](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L112) + +___ + +### onChangeRaw + +▸ `Optional` **onChangeRaw**(`value`): `void` + +Handler that is called when the value changes. Note that this provides types from @internationalized/date. + +#### Parameters -Omit.onChange +| Name | Type | +| :------ | :------ | +| `value` | ``null`` \| `DateValue` | + +#### Returns + +`void` #### Defined in -node_modules/@react-types/shared/src/inputs.d.ts:71 +[packages/framework/esm-styleguide/src/datepicker/index.tsx:114](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/datepicker/index.tsx#L114) ___ diff --git a/packages/framework/esm-styleguide/src/datepicker/datepicker.module.scss b/packages/framework/esm-styleguide/src/datepicker/datepicker.module.scss index aa7705006..06a164040 100644 --- a/packages/framework/esm-styleguide/src/datepicker/datepicker.module.scss +++ b/packages/framework/esm-styleguide/src/datepicker/datepicker.module.scss @@ -123,7 +123,6 @@ } &[data-placeholder] { - caret-color: var(--cds-text-primary); color: theme.$text-disabled; } } diff --git a/packages/framework/esm-styleguide/src/datepicker/index.tsx b/packages/framework/esm-styleguide/src/datepicker/index.tsx index 491f094ed..d7159c82c 100644 --- a/packages/framework/esm-styleguide/src/datepicker/index.tsx +++ b/packages/framework/esm-styleguide/src/datepicker/index.tsx @@ -1,10 +1,13 @@ import React, { - cloneElement, - forwardRef, + type CSSProperties, + type ForwardedRef, type HTMLAttributes, type PropsWithChildren, type RefObject, type ReactElement, + type ReactNode, + cloneElement, + forwardRef, useMemo, useContext, useCallback, @@ -21,7 +24,19 @@ import { toCalendar, today, } from '@internationalized/date'; -import { I18nProvider, type DateValue, useLocale, useDateField } from 'react-aria'; +import { type AriaLabelingProps, type DOMProps } from '@react-types/shared'; +import { filterDOMProps } from '@react-aria/utils'; +import { + I18nProvider, + type DateValue, + mergeProps, + useLocale, + useDateField, + useDateSegment, + useFocusRing, + useHover, + useObjectRef, +} from 'react-aria'; import { useDateFieldState } from 'react-stately'; import { Button, @@ -30,24 +45,24 @@ import { CalendarCell, CalendarStateContext, DateFieldContext, + DateFieldStateContext, type DateInputProps, DatePicker, type DatePickerProps, DatePickerStateContext, - DateSegment, + type DateSegmentProps, Dialog, + FieldError, Group, + GroupContext, Input, + InputContext, Label, NumberField, Popover, + Provider, RangeCalendarStateContext, useContextProps, - DateFieldStateContext, - InputContext, - Provider, - GroupContext, - FieldError, } from 'react-aria-components'; import dayjs, { type Dayjs } from 'dayjs'; import { formatDate, getDefaultCalendar, getLocale } from '@openmrs/esm-utils'; @@ -71,7 +86,7 @@ export type DateInputValue = */ export interface OpenmrsDatePickerProps // omits here for features we have custom implementations of - extends Omit, 'className' | 'defaultValue' | 'value'> { + extends Omit, 'className' | 'onChange' | 'defaultValue' | 'value'> { /** Any CSS classes to add to the outer div of the date picker */ className?: Argument; /** The default value (uncontrolled) */ @@ -93,6 +108,10 @@ export interface OpenmrsDatePickerProps maxDate?: DateInputValue; /** The earliest date it is possible to select */ minDate?: DateInputValue; + /** Handler that is called when the value changes. */ + onChange?: (value: Date | null | undefined) => void; + /** Handler that is called when the value changes. Note that this provides types from @internationalized/date. */ + onChangeRaw?: (value: DateValue | null) => void; /** Specifies the size of the input. Currently supports either `sm`, `md`, or `lg` as an option */ size?: 'sm' | 'md' | 'lg'; /** 'true' to use the short version. */ @@ -110,9 +129,28 @@ const defaultProps: OpenmrsDatePickerProps = { * Function to convert relatively arbitrary date values into a React Aria `DateValue`, * normally a `CalendarDate`, which represents a date without time or timezone. */ -function dateToInternationalizedDate(date: DateInputValue, calendar: CalendarType | undefined): DateValue | undefined { - if (!date) { - return undefined; +function dateToInternationalizedDate( + date: DateInputValue, + calendar: CalendarType | undefined, + allowNull: true, +): DateValue | null | undefined; +function dateToInternationalizedDate( + date: DateInputValue, + calendar: CalendarType | undefined, + allowNull: false, +): DateValue | undefined; +function dateToInternationalizedDate(date: DateInputValue, calendar: CalendarType | undefined): DateValue | undefined; +function dateToInternationalizedDate( + date: DateInputValue, + calendar: CalendarType | undefined, + allowNull: boolean = false, +) { + if (typeof date === 'undefined' || date === null) { + return allowNull ? date : undefined; + } + + if (typeof date === 'string' && !date) { + return allowNull ? null : undefined; } if (date instanceof CalendarDate || date instanceof CalendarDateTime || date instanceof ZonedDateTime) { @@ -125,6 +163,13 @@ function dateToInternationalizedDate(date: DateInputValue, calendar: CalendarTyp } } +function internationalizedDateToDate(date: DateValue): Date | undefined { + if (!date) { + return undefined; + } + return date.toDate(getLocalTimeZone()); +} + function getYearAsNumber(date: Date, intlLocale: Intl.Locale) { return parseInt( formatDate(date, { @@ -228,7 +273,9 @@ const DatePickerInput = forwardRef(function Date slot={props.slot || undefined} className={props.className ?? 'react-aria-DateInput'} isInvalid={state.isInvalid} - onClick={() => datePickerState.setOpen(!datePickerState.isOpen)} + onClick={() => { + datePickerState.setOpen(!datePickerState.isOpen); + }} > {state.segments.map((segment, i) => cloneElement(props.children(segment), { key: i }))} @@ -237,6 +284,110 @@ const DatePickerInput = forwardRef(function Date ); }); +interface RenderPropsHookOptions extends DOMProps, AriaLabelingProps { + children?: ReactNode | ((values: T & { defaultChildren: ReactNode | undefined }) => ReactNode); + values: T; + defaultChildren?: ReactNode; + defaultClassName?: string; + defaultStyle?: CSSProperties; + className?: string | ((values: T & { defaultClassName: string | undefined }) => string); + style?: CSSProperties | ((values: T & { defaultStyle: CSSProperties }) => CSSProperties); +} + +function useRenderProps(props: RenderPropsHookOptions) { + const { + className, + style, + children, + defaultClassName = undefined, + defaultChildren = undefined, + defaultStyle, + values, + } = props; + + return useMemo(() => { + let computedClassName: string | undefined; + let computedStyle: React.CSSProperties | undefined; + let computedChildren: React.ReactNode | undefined; + + if (typeof className === 'function') { + computedClassName = className({ ...values, defaultClassName }); + } else { + computedClassName = className; + } + + if (typeof style === 'function') { + computedStyle = style({ ...values, defaultStyle: defaultStyle || {} }); + } else { + computedStyle = style; + } + + if (typeof children === 'function') { + computedChildren = children({ ...values, defaultChildren }); + } else if (children == null) { + computedChildren = defaultChildren; + } else { + computedChildren = children; + } + + return { + className: computedClassName ?? defaultClassName, + style: computedStyle || defaultStyle ? { ...defaultStyle, ...computedStyle } : undefined, + children: computedChildren ?? defaultChildren, + 'data-rac': '', + }; + }, [className, style, children, defaultClassName, defaultChildren, defaultStyle, values]); +} + +const DateSegment = forwardRef(function DateSegment( + { segment, ...otherProps }: DateSegmentProps, + ref: ForwardedRef, +) { + const state = useContext(DateFieldStateContext); + const domRef = useObjectRef(ref); + const { segmentProps } = useDateSegment(segment, state, domRef); + const { focusProps, isFocused, isFocusVisible } = useFocusRing(); + const { hoverProps, isHovered } = useHover({ + ...otherProps, + isDisabled: state.isDisabled || segment.type === 'literal', + }); + const renderProps = useRenderProps({ + ...otherProps, + values: { + ...segment, + isReadOnly: !segment.isEditable, + isInvalid: state.isInvalid, + isDisabled: state.isDisabled, + isHovered, + isFocused, + isFocusVisible, + }, + defaultChildren: segment.text, + }); + + return ( +
[0]), + segmentProps, + focusProps, + hoverProps, + )} + {...renderProps} + ref={domRef} + data-placeholder={segment.isPlaceholder || undefined} + data-invalid={state.isInvalid || undefined} + data-readonly={!segment.isEditable || undefined} + data-disabled={state.isDisabled || undefined} + data-type={segment.type} + data-hovered={isHovered || undefined} + data-focused={isFocused || undefined} + data-focus-visible={isFocusVisible || undefined} + onClick={(event) => event.stopPropagation()} + /> + ); +}); + function DatePickerLabel({ labelText }: Pick) { if (labelText === null || typeof labelText === 'undefined' || typeof labelText === 'boolean') { return null; @@ -261,30 +412,49 @@ export const OpenmrsDatePicker = forwardRef { + let locale = getLocale(); + let intlLocale = new Intl.Locale(locale); + + if (intlLocale.language === 'en' && !intlLocale.region) { + locale = 'en-GB'; + intlLocale = new Intl.Locale(locale); + } + + return [locale, intlLocale]; + }, []); const calendar = useMemo(() => { const cal = getDefaultCalendar(locale); + intlLocale = new Intl.Locale(intlLocale.toString(), { calendar: cal }); return typeof cal !== 'undefined' ? createCalendar(cal) : undefined; }, [locale]); - const localeWithCalendar = useMemo( - () => (typeof calendar === 'undefined' ? locale : `${locale}-u-ca-${calendar.identifier}`), - [calendar, locale], - ); + const localeWithCalendar = useMemo(() => intlLocale.toString(), [intlLocale]); const defaultValue = useMemo(() => dateToInternationalizedDate(rawDefaultValue, calendar), [rawDefaultValue]); - const value = useMemo(() => dateToInternationalizedDate(rawValue, calendar), [rawValue]); + const value = useMemo(() => dateToInternationalizedDate(rawValue, calendar, true), [rawValue]); const maxDate = useMemo(() => dateToInternationalizedDate(rawMaxDate, calendar), [rawMaxDate]); const minDate = useMemo(() => dateToInternationalizedDate(rawMinDate, calendar), [rawMinDate]); const isInvalid = useMemo(() => invalid ?? isInvalidRaw, [invalid, isInvalidRaw]); const today_ = calendar ? toCalendar(today(getLocalTimeZone()), calendar) : today(getLocalTimeZone()); + const onChange = useMemo(() => { + if (onChangeRaw && rawOnChange) { + console.error( + 'An OpenmrsDatePicker component was created with both onChange and onChangeRaw handlers defined. Only onChangeRaw will be used.', + ); + } + return onChangeRaw ? onChangeRaw : (value: DateValue) => rawOnChange?.(internationalizedDateToDate(value)); + }, [onChangeRaw, rawOnChange]); + return (
@@ -298,7 +468,9 @@ export const OpenmrsDatePicker = forwardRef