From ce876d97c7e765700472d680b02c7b1b9ef4b769 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 16 Jan 2024 09:09:03 +0100 Subject: [PATCH 01/10] chore(deps): add temporal polyfill --- components/calendar/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/components/calendar/package.json b/components/calendar/package.json index 4017e1f700..2357a2ad68 100644 --- a/components/calendar/package.json +++ b/components/calendar/package.json @@ -42,6 +42,7 @@ "@dhis2/prop-types": "^3.1.2", "@dhis2/ui-constants": "9.2.0", "@dhis2/ui-icons": "9.2.0", + "@js-temporal/polyfill": "^0.4.2", "classnames": "^2.3.1", "prop-types": "^15.7.2" }, From f046c7049ba7bc590d9a8371f3dfdfcc1f8bddbd Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 16 Jan 2024 09:09:40 +0100 Subject: [PATCH 02/10] refactor: use index.js to expose calendar input --- components/calendar/src/calendar-input/index.js | 1 + components/calendar/src/index.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 components/calendar/src/calendar-input/index.js diff --git a/components/calendar/src/calendar-input/index.js b/components/calendar/src/calendar-input/index.js new file mode 100644 index 0000000000..c9563f4503 --- /dev/null +++ b/components/calendar/src/calendar-input/index.js @@ -0,0 +1 @@ +export { CalendarInput } from './calendar-input.js' diff --git a/components/calendar/src/index.js b/components/calendar/src/index.js index 252dc77ae4..2f73cbb02f 100644 --- a/components/calendar/src/index.js +++ b/components/calendar/src/index.js @@ -1,2 +1,2 @@ export { Calendar } from './calendar/calendar.js' -export { CalendarInput } from './calendar-input/calendar-input.js' +export { CalendarInput } from './calendar-input/index.js' From be25896cfeb28bf2d24b1505c8eb919b799f2ff8 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 16 Jan 2024 09:10:54 +0100 Subject: [PATCH 03/10] feat(calendar input): make input editable --- .../src/calendar-input/calendar-input.js | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index a2753110b2..641ccc8e3b 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -3,8 +3,9 @@ import { Card } from '@dhis2-ui/card' import { InputField, InputFieldProps } from '@dhis2-ui/input' import { Layer } from '@dhis2-ui/layer' import { Popper } from '@dhis2-ui/popper' +import { Temporal } from '@js-temporal/polyfill' import cx from 'classnames' -import React, { useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { Calendar, CalendarProps } from '../calendar/calendar.js' import i18n from '../locales/index.js' @@ -15,6 +16,10 @@ const offsetModifier = { }, } +export const validateInput = (input) => { + return /^\d{4}([/-])\d{2}\1\d{2}$/.test(input) +} + export const CalendarInput = ({ onDateSelect, calendar, @@ -31,12 +36,31 @@ export const CalendarInput = ({ } = {}) => { const ref = useRef() const [open, setOpen] = useState(false) + const [tempInputValue, setTempInputValue] = useState(date) + + useEffect(() => { + const isValidInputDate = validateInput(tempInputValue) + if (isValidInputDate && date !== tempInputValue) { + const [year, month, day] = tempInputValue.split('-') + const nextDate = Temporal.PlainDate.from({ + calendar, + year, + month, + day, + }) + onDateSelect({ + calendarDate: nextDate, + calendarDateString: tempInputValue, + }) + } + }, [calendar, date, tempInputValue, onDateSelect]) const calendarProps = React.useMemo(() => { const onDateSelectWrapper = (selectedDate) => { setOpen(false) - onDateSelect?.(selectedDate) + setTempInputValue(selectedDate.calendarDateString) } + return { onDateSelect: onDateSelectWrapper, calendar, @@ -56,7 +80,6 @@ export const CalendarInput = ({ dir, locale, numberingSystem, - onDateSelect, timeZone, weekDayFormat, width, @@ -74,7 +97,8 @@ export const CalendarInput = ({ {...rest} type="text" onFocus={onFocus} - value={date} + value={tempInputValue} + onChange={({ value }) => setTempInputValue(value)} /> {clearable && (
Date: Tue, 16 Jan 2024 09:11:16 +0100 Subject: [PATCH 04/10] test(calendar input): cover input validation helper --- .../src/calendar-input/calendar-input.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 components/calendar/src/calendar-input/calendar-input.test.js diff --git a/components/calendar/src/calendar-input/calendar-input.test.js b/components/calendar/src/calendar-input/calendar-input.test.js new file mode 100644 index 0000000000..7d8658678f --- /dev/null +++ b/components/calendar/src/calendar-input/calendar-input.test.js @@ -0,0 +1,19 @@ +import { validateInput } from './calendar-input.js' + +describe('validateInput', () => { + it('should return true when the input is valid with - as delimiter', () => { + expect(validateInput('2024-01-15')).toBe(true) + }) + + it('should return true when the input is valid with / as delimiter', () => { + expect(validateInput('2024/01/15')).toBe(true) + }) + + it('should return false when the input different delimiters', () => { + expect(validateInput('2024/01-15')).toBe(false) + }) + + it('should return false when the input is incorrect', () => { + expect(validateInput('202-01-15')).toBe(false) + }) +}) From 49a5490f67bcacf1bf6e0b41d5c22685fbb7f143 Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Tue, 16 Jan 2024 13:05:40 +0100 Subject: [PATCH 05/10] refactor(calendar input): use setter function instead of useEffect --- .../src/calendar-input/calendar-input.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index 641ccc8e3b..637fe837c5 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -36,24 +36,27 @@ export const CalendarInput = ({ } = {}) => { const ref = useRef() const [open, setOpen] = useState(false) - const [tempInputValue, setTempInputValue] = useState(date) + const [tempInputValue, _setTempInputValue] = useState(date) - useEffect(() => { - const isValidInputDate = validateInput(tempInputValue) - if (isValidInputDate && date !== tempInputValue) { - const [year, month, day] = tempInputValue.split('-') + const setTempInputValue = (nextTempValue) => { + _setTempInputValue(nextTempValue) + const isValidInputDate = validateInput(nextTempValue) + + if (isValidInputDate) { + const [year, month, day] = nextTempValue.split('-') const nextDate = Temporal.PlainDate.from({ calendar, year, month, day, }) + onDateSelect({ calendarDate: nextDate, - calendarDateString: tempInputValue, + calendarDateString: nextTempValue, }) } - }, [calendar, date, tempInputValue, onDateSelect]) + } const calendarProps = React.useMemo(() => { const onDateSelectWrapper = (selectedDate) => { From 74b2ce1329fe0e8a0859f2fd7e258d0a5096a5bc Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Thu, 25 Jan 2024 16:13:11 +0800 Subject: [PATCH 06/10] fix: provide zdt when changing the date through typing --- .../src/calendar-input/calendar-input.js | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index 637fe837c5..ec89673283 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -3,9 +3,9 @@ import { Card } from '@dhis2-ui/card' import { InputField, InputFieldProps } from '@dhis2-ui/input' import { Layer } from '@dhis2-ui/layer' import { Popper } from '@dhis2-ui/popper' -import { Temporal } from '@js-temporal/polyfill' +import { useDatePicker } from '@dhis2/multi-calendar-dates' import cx from 'classnames' -import React, { useEffect, useRef, useState } from 'react' +import React, { useCallback, useRef, useState } from 'react' import { Calendar, CalendarProps } from '../calendar/calendar.js' import i18n from '../locales/index.js' @@ -16,8 +16,21 @@ const offsetModifier = { }, } -export const validateInput = (input) => { - return /^\d{4}([/-])\d{2}\1\d{2}$/.test(input) +export function validateInput(input) { + return /^\d{4}([/-]?)\d{2}\1\d{2}$/.test(input) +} + +function searchCalendarWeekDays(date, calendarWeekDays) { + for (let i = 0; i < calendarWeekDays.length; ++i) { + const days = calendarWeekDays[i] + const temporalDate = days.find(({ calendarDate }) => calendarDate === date) + + if (temporalDate) { + return temporalDate.zdt + } + } + + return null } export const CalendarInput = ({ From 6203ab3c0651ad389fb774966b39aa87e1517e4f Mon Sep 17 00:00:00 2001 From: Jan-Gerke Salomon Date: Thu, 25 Jan 2024 17:09:47 +0800 Subject: [PATCH 07/10] feat: add icon-button that opens the picker widget --- .../src/calendar-input/calendar-input.js | 120 ++++++++++-------- .../src/stories/calendar-input.stories.js | 1 + 2 files changed, 65 insertions(+), 56 deletions(-) diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index ec89673283..106b19e0ac 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -4,6 +4,7 @@ import { InputField, InputFieldProps } from '@dhis2-ui/input' import { Layer } from '@dhis2-ui/layer' import { Popper } from '@dhis2-ui/popper' import { useDatePicker } from '@dhis2/multi-calendar-dates' +import { IconCheckmark24 } from '@dhis2/ui-icons' import cx from 'classnames' import React, { useCallback, useRef, useState } from 'react' import { Calendar, CalendarProps } from '../calendar/calendar.js' @@ -50,72 +51,45 @@ export const CalendarInput = ({ const ref = useRef() const [open, setOpen] = useState(false) const [tempInputValue, _setTempInputValue] = useState(date) - - const setTempInputValue = (nextTempValue) => { + const currentValidDate = validateInput(tempInputValue) + ? tempInputValue + : date + const { calendarWeekDays } = useDatePicker({ + date: currentValidDate, + options: { calendar }, + }) + + const setTempInputValue = useCallback((nextTempValue) => { _setTempInputValue(nextTempValue) const isValidInputDate = validateInput(nextTempValue) if (isValidInputDate) { - const [year, month, day] = nextTempValue.split('-') - const nextDate = Temporal.PlainDate.from({ - calendar, - year, - month, - day, - }) - + const calendarDate = searchCalendarWeekDays(nextTempValue, calendarWeekDays) onDateSelect({ - calendarDate: nextDate, + calendarDate, calendarDateString: nextTempValue, }) } - } + }, [onDateSelect, calendarWeekDays]) - const calendarProps = React.useMemo(() => { - const onDateSelectWrapper = (selectedDate) => { - setOpen(false) - setTempInputValue(selectedDate.calendarDateString) - } - - return { - onDateSelect: onDateSelectWrapper, - calendar, - date, - dir, - locale, - numberingSystem, - weekDayFormat, - timeZone, - width, - cellSize, - } - }, [ - calendar, - cellSize, - date, - dir, - locale, - numberingSystem, - timeZone, - weekDayFormat, - width, - ]) - - const onFocus = () => { - setOpen(true) - } + const onDateSelectWrapper = useCallback((selectedDate) => { + setOpen(false) + setTempInputValue(selectedDate?.calendarDateString || '') + }, [setTempInputValue]) return ( <>
- setTempInputValue(value)} - /> +
+ setTempInputValue(value)} + /> +
+ {clearable && (
calendarProps.onDateSelect(null)} + onClick={() => { + onDateSelectWrapper(null) + }} type="button" > {i18n.t('Clear')}
)} + +
+
{open && ( - + @@ -164,19 +158,33 @@ export const CalendarInput = ({ {` .calendar-input-wrapper { position: relative; + display: flex; + gap: 8px; + } + + .input { + flex-grow: 1; } + .calendar-clear-button { position: absolute; - inset-inline-end: 6px; + inset-inline-end: 51px; inset-block-start: 27px; } .calendar-clear-button.with-icon { inset-inline-end: 36px; + } + .calendar-clear-button.with-dense-wrapper { inset-block-start: 23px; } + + .open-calendar-widget { + padding-top: 22px; + flex-shrink: 1; + } `} diff --git a/components/calendar/src/stories/calendar-input.stories.js b/components/calendar/src/stories/calendar-input.stories.js index 13cc451670..801b67014a 100644 --- a/components/calendar/src/stories/calendar-input.stories.js +++ b/components/calendar/src/stories/calendar-input.stories.js @@ -27,6 +27,7 @@ const buildCalendar = () => ( Date: Thu, 29 Feb 2024 11:21:25 +0800 Subject: [PATCH 08/10] feat(calendar input): implement date validation --- components/calendar/package.json | 2 +- .../src/calendar-input/calendar-input.js | 73 ++++++++++++++----- yarn.lock | 8 +- 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/components/calendar/package.json b/components/calendar/package.json index 2357a2ad68..3fd60520bb 100644 --- a/components/calendar/package.json +++ b/components/calendar/package.json @@ -38,7 +38,7 @@ "@dhis2-ui/input": "9.2.0", "@dhis2-ui/layer": "9.2.0", "@dhis2-ui/popper": "9.2.0", - "@dhis2/multi-calendar-dates": "1.0.2", + "@dhis2/multi-calendar-dates": "v1.0.0-alpha.23", "@dhis2/prop-types": "^3.1.2", "@dhis2/ui-constants": "9.2.0", "@dhis2/ui-icons": "9.2.0", diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index 106b19e0ac..ec3995e650 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -3,9 +3,10 @@ import { Card } from '@dhis2-ui/card' import { InputField, InputFieldProps } from '@dhis2-ui/input' import { Layer } from '@dhis2-ui/layer' import { Popper } from '@dhis2-ui/popper' -import { useDatePicker } from '@dhis2/multi-calendar-dates' +import { useDatePicker, validateDateString } from '@dhis2/multi-calendar-dates' import { IconCheckmark24 } from '@dhis2/ui-icons' import cx from 'classnames' +import PropTypes from 'prop-types' import React, { useCallback, useRef, useState } from 'react' import { Calendar, CalendarProps } from '../calendar/calendar.js' import i18n from '../locales/index.js' @@ -17,10 +18,6 @@ const offsetModifier = { }, } -export function validateInput(input) { - return /^\d{4}([/-]?)\d{2}\1\d{2}$/.test(input) -} - function searchCalendarWeekDays(date, calendarWeekDays) { for (let i = 0; i < calendarWeekDays.length; ++i) { const days = calendarWeekDays[i] @@ -46,12 +43,26 @@ export const CalendarInput = ({ width, cellSize, clearable, + onError, ...rest } = {}) => { const ref = useRef() const [open, setOpen] = useState(false) + const [invalidMessage, _setInvalidMessage] = useState('') + const setInvalidMessage = useCallback((nextInvalidMessage) => { + _setInvalidMessage((prevInvalidMessage) => { + const isUpdate = prevInvalidMessage !== nextInvalidMessage + + if (isUpdate) { + onError?.(nextInvalidMessage) + } + + return nextInvalidMessage + }) + }, [onError]) + const [tempInputValue, _setTempInputValue] = useState(date) - const currentValidDate = validateInput(tempInputValue) + const currentValidDate = validateDateString(tempInputValue) ? tempInputValue : date const { calendarWeekDays } = useDatePicker({ @@ -61,22 +72,40 @@ export const CalendarInput = ({ const setTempInputValue = useCallback((nextTempValue) => { _setTempInputValue(nextTempValue) - const isValidInputDate = validateInput(nextTempValue) - - if (isValidInputDate) { - const calendarDate = searchCalendarWeekDays(nextTempValue, calendarWeekDays) - onDateSelect({ - calendarDate, - calendarDateString: nextTempValue, - }) - } - }, [onDateSelect, calendarWeekDays]) + let nextInvalidMessage + + try { + nextInvalidMessage = validateDateString(nextTempValue) + } catch (e) { + console.error(e) + } + + if (!nextTempValue) { + onDateSelect({ + calendarDate: null, + calendarDateString: '', + }) + } else if (!nextInvalidMessage) { + setInvalidMessage('') + const calendarDate = searchCalendarWeekDays(nextTempValue, calendarWeekDays) + onDateSelect({ + calendarDate, + calendarDateString: nextTempValue, + }) + } else { + setInvalidMessage(nextInvalidMessage) + } + }, [onDateSelect, calendarWeekDays, setInvalidMessage]) const onDateSelectWrapper = useCallback((selectedDate) => { setOpen(false) setTempInputValue(selectedDate?.calendarDateString || '') }, [setTempInputValue]) + const showInvalidMessage = !!tempInputValue && !!invalidMessage + const warning = rest.warning || (!rest.required && showInvalidMessage) + const error = rest.error || (rest.required && !!showInvalidMessage) + return ( <>
@@ -87,6 +116,9 @@ export const CalendarInput = ({ type="text" value={tempInputValue} onChange={({ value }) => setTempInputValue(value)} + warning={warning || (!rest.required && showInvalidMessage)} + error={error || (rest.required && !!showInvalidMessage)} + helpText={rest.helpText || (tempInputValue && invalidMessage)} />
@@ -98,8 +130,8 @@ export const CalendarInput = ({ // https://dhis2.atlassian.net/browse/DHIS2-14848 'with-icon': rest.valid || - rest.error || - rest.warning || + error || + warning || rest.loading, 'with-dense-wrapper': rest.dense, })} @@ -173,8 +205,7 @@ export const CalendarInput = ({ } .calendar-clear-button.with-icon { - inset-inline-end: 36px; - + inset-inline-end: 81px; } .calendar-clear-button.with-dense-wrapper { @@ -194,7 +225,9 @@ export const CalendarInput = ({ CalendarInput.defaultProps = { dataTest: 'dhis2-uiwidgets-calendar-inputfield', } + CalendarInput.propTypes = { ...CalendarProps, ...InputFieldProps, + onError: PropTypes.func, } diff --git a/yarn.lock b/yarn.lock index 0dac4c4cb7..4e962468e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2586,10 +2586,10 @@ i18next "^10.3" moment "^2.24.0" -"@dhis2/multi-calendar-dates@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-1.0.2.tgz#e54dc85e512aba93fceef3004e67e199077f3ba8" - integrity sha512-oQZ7PFMwHFpt4ygDN9DmAeYO3g07L7AHJW6diZ37mzpkEF/DyMafhsZHnJWNlTH5HDp8nYuO3EjBiM7fZN6C0g== +"@dhis2/multi-calendar-dates@v1.0.0-alpha.23": + version "1.0.0-alpha.23" + resolved "https://registry.yarnpkg.com/@dhis2/multi-calendar-dates/-/multi-calendar-dates-1.0.0-alpha.23.tgz#7219ab28263b61e92b16b0a79e33049253d2baa5" + integrity sha512-E4+doG1tLGi81Nsty0CYLfQbPJg2P15AF5Znp+22IW5EuMUnuK/9Y64D5qiGGzlGoTHnqsr3/Hz62DUTIXqwXQ== dependencies: "@js-temporal/polyfill" "^0.4.2" classnames "^2.3.2" From 9925bfd855b0ec76f2d68a1e9c715ae5732ce0fc Mon Sep 17 00:00:00 2001 From: Mohammer5 Date: Mon, 4 Mar 2024 18:31:45 +0800 Subject: [PATCH 09/10] fix: split editable and standard calendar input to avoid breaking change --- .../calendar-input/calendar-input-editable.js | 186 ++++++++++++++ .../src/calendar-input/calendar-input.js | 236 ++++-------------- .../src/calendar-input/calendar-widget.js | 66 +++++ .../calendar/src/calendar-input/index.js | 1 + .../src/calendar-input/input-clear-button.js | 71 ++++++ .../features/supports_nepali_calendar.feature | 6 +- .../supports_nepali_calendar.js | 19 ++ .../calendar-input-editable.stories.js | 117 +++++++++ components/calendar/types/index.d.ts | 6 + 9 files changed, 517 insertions(+), 191 deletions(-) create mode 100644 components/calendar/src/calendar-input/calendar-input-editable.js create mode 100644 components/calendar/src/calendar-input/calendar-widget.js create mode 100644 components/calendar/src/calendar-input/input-clear-button.js create mode 100644 components/calendar/src/stories/calendar-input-editable.stories.js diff --git a/components/calendar/src/calendar-input/calendar-input-editable.js b/components/calendar/src/calendar-input/calendar-input-editable.js new file mode 100644 index 0000000000..9ca6b9492b --- /dev/null +++ b/components/calendar/src/calendar-input/calendar-input-editable.js @@ -0,0 +1,186 @@ +import { Button } from '@dhis2-ui/button' +import { InputField, InputFieldProps } from '@dhis2-ui/input' +import { useDatePicker, validateDateString } from '@dhis2/multi-calendar-dates' +import { IconCheckmark24 } from '@dhis2/ui-icons' +import PropTypes from 'prop-types' +import React, { useCallback, useRef, useState } from 'react' +import { CalendarProps } from '../calendar/calendar.js' +import { CalendarWidget } from './calendar-widget.js' +import { InputClearButton } from './input-clear-button.js' + +function searchCalendarWeekDays(date, calendarWeekDays) { + for (let i = 0; i < calendarWeekDays.length; ++i) { + const days = calendarWeekDays[i] + const temporalDate = days.find(({ calendarDate }) => calendarDate === date) + + if (temporalDate) { + return temporalDate.zdt + } + } + + return null +} + +export const CalendarInputEditable = ({ + onDateSelect, + calendar, + cellSize, + clearable, + date, + dir, + locale, + numberingSystem, + weekDayFormat, + timeZone, + width, + onError, + ...rest +} = {}) => { + const ref = useRef() + const [open, setOpen] = useState(false) + const [invalidMessage, _setInvalidMessage] = useState('') + const setInvalidMessage = useCallback((nextInvalidMessage) => { + _setInvalidMessage((prevInvalidMessage) => { + const isUpdate = prevInvalidMessage !== nextInvalidMessage + + if (isUpdate) { + onError?.(nextInvalidMessage) + } + + return nextInvalidMessage + }) + }, [onError]) + + const [tempInputValue, _setTempInputValue] = useState(date) + const currentValidDate = validateDateString(tempInputValue) + ? tempInputValue + : date + const { calendarWeekDays } = useDatePicker({ + date: currentValidDate, + options: { calendar }, + }) + + const setTempInputValue = useCallback((nextTempValue) => { + _setTempInputValue(nextTempValue) + let nextInvalidMessage + + try { + nextInvalidMessage = validateDateString(nextTempValue) + } catch (e) { + console.error(e) + } + + if (!nextTempValue) { + onDateSelect({ + calendarDate: null, + calendarDateString: '', + }) + } else if (!nextInvalidMessage) { + setInvalidMessage('') + const calendarDate = searchCalendarWeekDays(nextTempValue, calendarWeekDays) + onDateSelect({ + calendarDate, + calendarDateString: nextTempValue, + }) + } else { + setInvalidMessage(nextInvalidMessage) + } + }, [onDateSelect, calendarWeekDays, setInvalidMessage]) + + const onDateSelectWrapper = useCallback((selectedDate) => { + setOpen(false) + setTempInputValue(selectedDate?.calendarDateString || '') + }, [setTempInputValue]) + + const showInvalidMessage = !!tempInputValue && !!invalidMessage + const warning = rest.warning || (!rest.required && showInvalidMessage) + const error = rest.error || (rest.required && !!showInvalidMessage) + + return ( + <> +
+
+ setTempInputValue(value)} + warning={warning || (!rest.required && showInvalidMessage)} + error={error || (rest.required && !!showInvalidMessage)} + helpText={rest.helpText || (tempInputValue && invalidMessage)} + /> +
+ + {clearable && ( + { + setOpen(false) + onDateSelect?.(selectedDate) + }} + /> + )} + +
+
+
+ + {open && ( + setOpen(false)} + onDateSelect={onDateSelectWrapper} + calendar={calendar} + date={date} + dir={dir} + locale={locale} + numberingSystem={numberingSystem} + weekDayFormat={weekDayFormat} + timeZone={timeZone} + width={width} + cellSize={cellSize} + /> + )} + + + + ) +} + +CalendarInputEditable.defaultProps = { + dataTest: 'dhis2-uiwidgets-calendar-inputfield', +} + +CalendarInputEditable.propTypes = { + ...CalendarProps, + ...InputFieldProps, + onError: PropTypes.func, +} diff --git a/components/calendar/src/calendar-input/calendar-input.js b/components/calendar/src/calendar-input/calendar-input.js index ec3995e650..9b7ea52078 100644 --- a/components/calendar/src/calendar-input/calendar-input.js +++ b/components/calendar/src/calendar-input/calendar-input.js @@ -1,35 +1,8 @@ -import { Button } from '@dhis2-ui/button' -import { Card } from '@dhis2-ui/card' import { InputField, InputFieldProps } from '@dhis2-ui/input' -import { Layer } from '@dhis2-ui/layer' -import { Popper } from '@dhis2-ui/popper' -import { useDatePicker, validateDateString } from '@dhis2/multi-calendar-dates' -import { IconCheckmark24 } from '@dhis2/ui-icons' -import cx from 'classnames' -import PropTypes from 'prop-types' -import React, { useCallback, useRef, useState } from 'react' -import { Calendar, CalendarProps } from '../calendar/calendar.js' -import i18n from '../locales/index.js' - -const offsetModifier = { - name: 'offset', - options: { - offset: [0, 2], - }, -} - -function searchCalendarWeekDays(date, calendarWeekDays) { - for (let i = 0; i < calendarWeekDays.length; ++i) { - const days = calendarWeekDays[i] - const temporalDate = days.find(({ calendarDate }) => calendarDate === date) - - if (temporalDate) { - return temporalDate.zdt - } - } - - return null -} +import React, { useRef, useState } from 'react' +import { CalendarProps } from '../calendar/calendar.js' +import { CalendarWidget } from './calendar-widget.js' +import { InputClearButton } from './input-clear-button.js' export const CalendarInput = ({ onDateSelect, @@ -43,181 +16,65 @@ export const CalendarInput = ({ width, cellSize, clearable, - onError, ...rest } = {}) => { const ref = useRef() const [open, setOpen] = useState(false) - const [invalidMessage, _setInvalidMessage] = useState('') - const setInvalidMessage = useCallback((nextInvalidMessage) => { - _setInvalidMessage((prevInvalidMessage) => { - const isUpdate = prevInvalidMessage !== nextInvalidMessage - - if (isUpdate) { - onError?.(nextInvalidMessage) - } - - return nextInvalidMessage - }) - }, [onError]) - - const [tempInputValue, _setTempInputValue] = useState(date) - const currentValidDate = validateDateString(tempInputValue) - ? tempInputValue - : date - const { calendarWeekDays } = useDatePicker({ - date: currentValidDate, - options: { calendar }, - }) - - const setTempInputValue = useCallback((nextTempValue) => { - _setTempInputValue(nextTempValue) - let nextInvalidMessage - - try { - nextInvalidMessage = validateDateString(nextTempValue) - } catch (e) { - console.error(e) - } - - if (!nextTempValue) { - onDateSelect({ - calendarDate: null, - calendarDateString: '', - }) - } else if (!nextInvalidMessage) { - setInvalidMessage('') - const calendarDate = searchCalendarWeekDays(nextTempValue, calendarWeekDays) - onDateSelect({ - calendarDate, - calendarDateString: nextTempValue, - }) - } else { - setInvalidMessage(nextInvalidMessage) - } - }, [onDateSelect, calendarWeekDays, setInvalidMessage]) - - const onDateSelectWrapper = useCallback((selectedDate) => { - setOpen(false) - setTempInputValue(selectedDate?.calendarDateString || '') - }, [setTempInputValue]) - - const showInvalidMessage = !!tempInputValue && !!invalidMessage - const warning = rest.warning || (!rest.required && showInvalidMessage) - const error = rest.error || (rest.required && !!showInvalidMessage) + const onFocus = () => setOpen(true) return ( <>
-
- setTempInputValue(value)} - warning={warning || (!rest.required && showInvalidMessage)} - error={error || (rest.required && !!showInvalidMessage)} - helpText={rest.helpText || (tempInputValue && invalidMessage)} - /> -
+ {clearable && ( -
- -
- )} - -
-
+ )}
+ {open && ( - { + setOpen(false)} + onDateSelect={(selectedDate) => { setOpen(false) + onDateSelect?.(selectedDate) }} - > - - - - - - + calendar={calendar} + date={date} + dir={dir} + locale={locale} + numberingSystem={numberingSystem} + weekDayFormat={weekDayFormat} + timeZone={timeZone} + width={width} + cellSize={cellSize} + /> )} - + ) } @@ -229,5 +86,4 @@ CalendarInput.defaultProps = { CalendarInput.propTypes = { ...CalendarProps, ...InputFieldProps, - onError: PropTypes.func, } diff --git a/components/calendar/src/calendar-input/calendar-widget.js b/components/calendar/src/calendar-input/calendar-widget.js new file mode 100644 index 0000000000..09bbca5953 --- /dev/null +++ b/components/calendar/src/calendar-input/calendar-widget.js @@ -0,0 +1,66 @@ +import { Card } from '@dhis2-ui/card' +import { Layer } from '@dhis2-ui/layer' +import { Popper } from '@dhis2-ui/popper' +import PropTypes from 'prop-types' +import React, { forwardRef } from 'react' +import { Calendar } from '../calendar/calendar.js' + +const offsetModifier = { + name: 'offset', + options: { + offset: [0, 2], + }, +} + +export const CalendarWidget = forwardRef(function CalendarWidget({ + onClose, + onDateSelect, + calendar, + date, + dir, + locale, + numberingSystem, + weekDayFormat, + timeZone, + width, + cellSize, +}, ref) { + return ( + onClose}> + + + + + + + ) +}) + +CalendarWidget.propTypes = { + calendar: PropTypes.any.isRequired, + onClose: PropTypes.func.isRequired, + onDateSelect: PropTypes.func.isRequired, + cellSize: PropTypes.string, + date: PropTypes.string, + dir: PropTypes.oneOf(['ltr', 'rtl']), + locale: PropTypes.string, + numberingSystem: PropTypes.string, + timeZone: PropTypes.string, + weekDayFormat: PropTypes.oneOf(['narrow', 'short', 'long']), + width: PropTypes.string, +} diff --git a/components/calendar/src/calendar-input/index.js b/components/calendar/src/calendar-input/index.js index c9563f4503..25acb981e9 100644 --- a/components/calendar/src/calendar-input/index.js +++ b/components/calendar/src/calendar-input/index.js @@ -1 +1,2 @@ export { CalendarInput } from './calendar-input.js' +export { CalendarInputEditable } from './calendar-input-editable.js' diff --git a/components/calendar/src/calendar-input/input-clear-button.js b/components/calendar/src/calendar-input/input-clear-button.js new file mode 100644 index 0000000000..610051dc9e --- /dev/null +++ b/components/calendar/src/calendar-input/input-clear-button.js @@ -0,0 +1,71 @@ +import { Button } from '@dhis2-ui/button' +import cx from 'classnames' +import PropTypes from 'prop-types' +import React from 'react' +import i18n from '../locales/index.js' + +export function InputClearButton({ + dense, + error, + insetBlockStartButton, + insetInlineEndButton, + insetInlineEndIcon, + loading, + valid, + warning, + onDateSelect, +}) { + return ( +
+ + + +
+ ) +} + +InputClearButton.propTypes = { + insetBlockStartButton: PropTypes.string.isRequired, + insetInlineEndButton: PropTypes.string.isRequired, + insetInlineEndIcon: PropTypes.string.isRequired, + onDateSelect: PropTypes.func.isRequired, + dense: PropTypes.bool, + error: PropTypes.bool, + loading: PropTypes.bool, + valid: PropTypes.bool, + warning: PropTypes.bool, +} diff --git a/components/calendar/src/features/supports_nepali_calendar.feature b/components/calendar/src/features/supports_nepali_calendar.feature index addc3a0945..eb797b0dc5 100644 --- a/components/calendar/src/features/supports_nepali_calendar.feature +++ b/components/calendar/src/features/supports_nepali_calendar.feature @@ -9,6 +9,10 @@ Feature: The Calendar renders in Nepali calendar system Given a nepali calendar in "nepali" is rendered Then we should be able to select a day + Scenario: Type a day in the Nepali calendar in Nepali + Given a nepali calendar in "nepali" is rendered + Then we should be able to enter a day + Scenario: Display a Nepali calendar in English Given a nepali calendar in "english" is rendered Then nepali days should be rendered in "english" @@ -16,4 +20,4 @@ Feature: The Calendar renders in Nepali calendar system Scenario: Select a day in the Nepali calendar in English Given a nepali calendar in "english" is rendered - Then we should be able to select a day \ No newline at end of file + Then we should be able to select a day diff --git a/components/calendar/src/features/supports_nepali_calendar/supports_nepali_calendar.js b/components/calendar/src/features/supports_nepali_calendar/supports_nepali_calendar.js index db2b1316f0..aab9720569 100644 --- a/components/calendar/src/features/supports_nepali_calendar/supports_nepali_calendar.js +++ b/components/calendar/src/features/supports_nepali_calendar/supports_nepali_calendar.js @@ -53,3 +53,22 @@ Then('we should be able to select a day', () => { '13 October 2021' ) }) + +Then('we should be able to enter a day', () => { + const nepaliDate = '2078-06-27' + cy.get(`[data-test="${nepaliDate}"]`).click() + + cy.get('[data-test="dhis2-uiwidgets-calendar-inputfield"] input').should( + 'have.value', + nepaliDate + ) + + cy.get('[data-test="storybook-calendar-result"]').should( + 'have.text', + nepaliDate + ) + cy.get('[data-test="storybook-calendar-result-iso"]').should( + 'have.text', + '13 October 2021' + ) +}) diff --git a/components/calendar/src/stories/calendar-input-editable.stories.js b/components/calendar/src/stories/calendar-input-editable.stories.js new file mode 100644 index 0000000000..abd782820a --- /dev/null +++ b/components/calendar/src/stories/calendar-input-editable.stories.js @@ -0,0 +1,117 @@ +import React, { useState } from 'react' +import { CalendarInputEditable } from '../calendar-input/calendar-input-editable.js' +import { CalendarStoryWrapper } from './calendar-story-wrapper.js' + +const subtitle = `[Experimental] Calendar Input is a wrapper around Calendar displaying an input that triggers the calendar` +const description = ` +Use a CalendarInputEditable where there is a need to let the user input a date. + +Note that it requires a parent, like [Box](../?path=/docs/layout-box--default), to define its size. + +\`\`\`js +import { CalendarInputEditable } from '@dhis2/ui' +\`\`\` +` + +export default { + title: 'CalendarInputEditable', + component: CalendarInputEditable, + parameters: { + componentSubtitle: subtitle, + docs: { description: { component: description } }, + }, +} + +const buildCalendar = + ({ date, locale, calendar }) => + () => + ( + + ) + +export const EthiopicWithAmharic = buildCalendar({ + calendar: 'ethiopic', + locale: 'am-ET', + date: '2014-02-03', // 13 Oct 2021 +}) + +export const EthiopicWithEnglish = buildCalendar({ + calendar: 'ethiopian', // using "ethiopian" rather than the correct "ethiopic" to immitate DHIS2 calendar types + locale: 'en-ET', + date: '2014-02-03', // 13 Oct 2021 +}) + +export const NepaliWithNepali = buildCalendar({ + calendar: 'nepali', + locale: 'ne-NP', + date: '2078-06-27', // 13 Oct 2021 +}) + +export const NepaliWithEnglish = buildCalendar({ + calendar: 'nepali', + locale: 'en-NP', + date: '2078-06-27', // 13 Oct 2021 +}) + +export const GregorianWithEnglish = buildCalendar({ + calendar: 'gregorian', + locale: 'en-CA', + date: '2021-10-13', +}) + +export const GregorianWithArabic = buildCalendar({ + calendar: 'gregorian', + locale: 'ar-SD', + date: '2021-10-13', +}) + +export const IslamicWithArabic = () => { + return ( +
+ +
+ ) +} + +export const CalendarWithClearButton = ({ + calendar = 'gregory', + date: initialDate = null, +}) => { + const [date, setDate] = useState(initialDate) + return ( + <> + { + setDate(date?.calendarDateString) + }} + clearable + /> +
+ value: + + {date ?? 'undefined'} + +
+ + ) +} diff --git a/components/calendar/types/index.d.ts b/components/calendar/types/index.d.ts index 62bb95c843..efd78ff2db 100644 --- a/components/calendar/types/index.d.ts +++ b/components/calendar/types/index.d.ts @@ -59,3 +59,9 @@ export type CalendarInputProps = Omit< CalendarProps export const CalendarInput: React.FC + +export type CalendarInputEditableProps = CalendarInputProps & { + onError?: (nextInvalidMessage: string) => void, +} + +export const CalendarInputEditable: React.FC From 0ad8363d3fa8d0aa6ed8dbc8e02147a58f829d56 Mon Sep 17 00:00:00 2001 From: Mohammer5 Date: Mon, 4 Mar 2024 18:50:40 +0800 Subject: [PATCH 10/10] fix(calendar input editable): pass "calendar" to "validateDateString" --- .../calendar-input/calendar-input-editable.js | 112 +++++++++++------- 1 file changed, 66 insertions(+), 46 deletions(-) diff --git a/components/calendar/src/calendar-input/calendar-input-editable.js b/components/calendar/src/calendar-input/calendar-input-editable.js index 9ca6b9492b..00fcd66616 100644 --- a/components/calendar/src/calendar-input/calendar-input-editable.js +++ b/components/calendar/src/calendar-input/calendar-input-editable.js @@ -11,7 +11,9 @@ import { InputClearButton } from './input-clear-button.js' function searchCalendarWeekDays(date, calendarWeekDays) { for (let i = 0; i < calendarWeekDays.length; ++i) { const days = calendarWeekDays[i] - const temporalDate = days.find(({ calendarDate }) => calendarDate === date) + const temporalDate = days.find( + ({ calendarDate }) => calendarDate === date + ) if (temporalDate) { return temporalDate.zdt @@ -39,20 +41,23 @@ export const CalendarInputEditable = ({ const ref = useRef() const [open, setOpen] = useState(false) const [invalidMessage, _setInvalidMessage] = useState('') - const setInvalidMessage = useCallback((nextInvalidMessage) => { - _setInvalidMessage((prevInvalidMessage) => { - const isUpdate = prevInvalidMessage !== nextInvalidMessage - - if (isUpdate) { - onError?.(nextInvalidMessage) - } - - return nextInvalidMessage - }) - }, [onError]) + const setInvalidMessage = useCallback( + (nextInvalidMessage) => { + _setInvalidMessage((prevInvalidMessage) => { + const isUpdate = prevInvalidMessage !== nextInvalidMessage + + if (isUpdate) { + onError?.(nextInvalidMessage) + } + + return nextInvalidMessage + }) + }, + [onError] + ) const [tempInputValue, _setTempInputValue] = useState(date) - const currentValidDate = validateDateString(tempInputValue) + const currentValidDate = validateDateString(tempInputValue, { calendar }) ? tempInputValue : date const { calendarWeekDays } = useDatePicker({ @@ -60,37 +65,48 @@ export const CalendarInputEditable = ({ options: { calendar }, }) - const setTempInputValue = useCallback((nextTempValue) => { - _setTempInputValue(nextTempValue) - let nextInvalidMessage - - try { - nextInvalidMessage = validateDateString(nextTempValue) - } catch (e) { - console.error(e) - } - - if (!nextTempValue) { - onDateSelect({ - calendarDate: null, - calendarDateString: '', - }) - } else if (!nextInvalidMessage) { - setInvalidMessage('') - const calendarDate = searchCalendarWeekDays(nextTempValue, calendarWeekDays) - onDateSelect({ - calendarDate, - calendarDateString: nextTempValue, - }) - } else { - setInvalidMessage(nextInvalidMessage) - } - }, [onDateSelect, calendarWeekDays, setInvalidMessage]) - - const onDateSelectWrapper = useCallback((selectedDate) => { - setOpen(false) - setTempInputValue(selectedDate?.calendarDateString || '') - }, [setTempInputValue]) + const setTempInputValue = useCallback( + (nextTempValue) => { + _setTempInputValue(nextTempValue) + let nextInvalidMessage + + try { + nextInvalidMessage = validateDateString(nextTempValue, { + calendar, + }) + } catch (e) { + console.error(e) + } + + if (!nextTempValue) { + onDateSelect({ + calendarDate: null, + calendarDateString: '', + }) + } else if (!nextInvalidMessage) { + setInvalidMessage('') + const calendarDate = searchCalendarWeekDays( + nextTempValue, + calendarWeekDays + ) + onDateSelect({ + calendarDate, + calendarDateString: nextTempValue, + }) + } else { + setInvalidMessage(nextInvalidMessage) + } + }, + [onDateSelect, calendarWeekDays, setInvalidMessage, calendar] + ) + + const onDateSelectWrapper = useCallback( + (selectedDate) => { + setOpen(false) + setTempInputValue(selectedDate?.calendarDateString || '') + }, + [setTempInputValue] + ) const showInvalidMessage = !!tempInputValue && !!invalidMessage const warning = rest.warning || (!rest.required && showInvalidMessage) @@ -106,9 +122,13 @@ export const CalendarInputEditable = ({ type="text" value={tempInputValue} onChange={({ value }) => setTempInputValue(value)} - warning={warning || (!rest.required && showInvalidMessage)} + warning={ + warning || (!rest.required && showInvalidMessage) + } error={error || (rest.required && !!showInvalidMessage)} - helpText={rest.helpText || (tempInputValue && invalidMessage)} + helpText={ + rest.helpText || (tempInputValue && invalidMessage) + } />