Skip to content

Commit

Permalink
feat: support editable input | min & max dates | dd-mm-yyyy format in…
Browse files Browse the repository at this point in the history
… CalendarInput (#1504)

* feat: support min & max dates

* chore: add story CalendarWithEditiableInput

* chore: refactor code

* fix: improve calendar component for independent use

* fix: lint warnings

* refactor: use calendar inner components directly

to avoid double calls to multi-calendar library

* fix: pass isValid to calendarProps

* refactor: extract calendar table into shared component

* chore: rename passed props

* fix: label in storybook

* fix: lint issue

---------

Co-authored-by: Mozafar Haider <mozafar@dhis2.org>
alaa-yahia and kabaros authored Aug 1, 2024
1 parent 6341a01 commit 99a78f5
Showing 8 changed files with 252 additions and 113 deletions.
2 changes: 1 addition & 1 deletion components/calendar/package.json
Original file line number Diff line number Diff line change
@@ -38,7 +38,7 @@
"@dhis2-ui/input": "9.9.0-alpha.1",
"@dhis2-ui/layer": "9.9.0-alpha.1",
"@dhis2-ui/popper": "9.9.0-alpha.1",
"@dhis2/multi-calendar-dates": "^1.1.1",
"@dhis2/multi-calendar-dates": "v1.0.0-alpha.26",
"@dhis2/prop-types": "^3.1.2",
"@dhis2/ui-constants": "9.9.0-alpha.1",
"@dhis2/ui-icons": "9.9.0-alpha.1",
101 changes: 66 additions & 35 deletions components/calendar/src/calendar-input/calendar-input.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Button } from '@dhis2-ui/button'
import { Card } from '@dhis2-ui/card'
import { InputField, InputFieldProps } from '@dhis2-ui/input'
import { InputField } from '@dhis2-ui/input'
import { Layer } from '@dhis2-ui/layer'
import { Popper } from '@dhis2-ui/popper'
import {
useDatePicker,
useResolvedDirection,
} from '@dhis2/multi-calendar-dates'
import cx from 'classnames'
import React, { useRef, useState } from 'react'
import { Calendar, CalendarProps } from '../calendar/calendar.js'
import React, { useRef, useState, useMemo } from 'react'
import { CalendarContainer } from '../calendar/calendar-container.js'
import { CalendarProps } from '../calendar/calendar.js'
import i18n from '../locales/index.js'

const offsetModifier = {
@@ -16,7 +21,7 @@ const offsetModifier = {
}

export const CalendarInput = ({
onDateSelect,
onDateSelect: parentOnDateSelect,
calendar,
date,
dir,
@@ -27,45 +32,67 @@ export const CalendarInput = ({
width,
cellSize,
clearable,
minDate,
maxDate,
format, // todo: props and types for format and validation
strictValidation,
...rest
} = {}) => {
const ref = useRef()
const [open, setOpen] = useState(false)

const calendarProps = React.useMemo(() => {
const onDateSelectWrapper = (selectedDate) => {
setOpen(false)
onDateSelect?.(selectedDate)
}
return {
onDateSelect: onDateSelectWrapper,
const useDatePickerOptions = useMemo(
() => ({
calendar,
date,
dir,
locale,
timeZone, // todo: we probably shouldn't have had timezone here in the first place
numberingSystem,
weekDayFormat,
timeZone,
width,
cellSize,
}
}, [
calendar,
cellSize,
}),
[calendar, locale, numberingSystem, timeZone, weekDayFormat]
)

const pickerResults = useDatePicker({
onDateSelect: (result) => {
setOpen(false)
parentOnDateSelect?.(result)
},
date,
dir,
locale,
numberingSystem,
onDateSelect,
timeZone,
weekDayFormat,
width,
])
minDate: minDate,
maxDate: maxDate,
strictValidation: strictValidation,
format: format,
options: useDatePickerOptions,
})

const handleChange = (e) => {
parentOnDateSelect?.({ calendarDateString: e.value })
}

const onFocus = () => {
setOpen(true)
}

const languageDirection = useResolvedDirection(dir, locale)

const calendarProps = useMemo(() => {
return {
date,
width,
cellSize,
isValid: pickerResults.isValid,
calendarWeekDays: pickerResults.calendarWeekDays,
weekDayLabels: pickerResults.weekDayLabels,
currMonth: pickerResults.currMonth,
currYear: pickerResults.currYear,
nextMonth: pickerResults.nextMonth,
nextYear: pickerResults.nextYear,
prevMonth: pickerResults.prevMonth,
prevYear: pickerResults.prevYear,
languageDirection,
}
}, [cellSize, date, pickerResults, width, languageDirection])

return (
<>
<div className="calendar-input-wrapper" ref={ref}>
@@ -75,13 +102,17 @@ export const CalendarInput = ({
type="text"
onFocus={onFocus}
value={date}
onChange={handleChange}
validationText={
pickerResults.errorMessage ||
pickerResults.warningMessage
}
error={!!pickerResults.errorMessage}
warning={!!pickerResults.warningMessage}
/>
{clearable && (
<div
className={cx('calendar-clear-button', {
// ToDo: this is a workaround to show the clear button in the correct place when an icon is shown.
// Long-term, we should abstract and share the logic multi-select uses for the input-wrapper
// https://dhis2.atlassian.net/browse/DHIS2-14848
'with-icon':
rest.valid ||
rest.error ||
@@ -94,7 +125,9 @@ export const CalendarInput = ({
dataTest="calendar-clear-button"
secondary
small
onClick={() => calendarProps.onDateSelect(null)}
onClick={() => {
parentOnDateSelect?.(null)
}}
type="button"
>
{i18n.t('Clear')}
@@ -114,7 +147,7 @@ export const CalendarInput = ({
modifiers={[offsetModifier]}
>
<Card>
<Calendar {...calendarProps} date={date} />
<CalendarContainer {...calendarProps} />
</Card>
</Popper>
</Layer>
@@ -130,7 +163,6 @@ export const CalendarInput = ({
inset-inline-end: 6px;
inset-block-start: 27px;
}
.calendar-clear-button.with-icon {
inset-inline-end: 36px;
}
@@ -148,5 +180,4 @@ CalendarInput.defaultProps = {
}
CalendarInput.propTypes = {
...CalendarProps,
...InputFieldProps,
}
92 changes: 92 additions & 0 deletions components/calendar/src/calendar/calendar-container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { colors } from '@dhis2/ui-constants'
import PropTypes from 'prop-types'
import React, { useMemo } from 'react'
import { CalendarTable, CalendarTableProps } from './calendar-table.js'
import {
NavigationContainer,
NavigationContainerProps,
} from './navigation-container.js'

const wrapperBorderColor = colors.grey300
const backgroundColor = 'none'

export const CalendarContainer = ({
date,
width,
cellSize,
calendarWeekDays,
weekDayLabels,
currMonth,
currYear,
nextMonth,
nextYear,
prevMonth,
prevYear,
languageDirection,
}) => {
const navigationProps = useMemo(() => {
return {
currMonth,
currYear,
nextMonth,
nextYear,
prevMonth,
prevYear,
languageDirection,
}
}, [
currMonth,
currYear,
languageDirection,
nextMonth,
nextYear,
prevMonth,
prevYear,
])
return (
<div>
<div
className="calendar-wrapper"
dir={languageDirection}
data-test="calendar"
>
<NavigationContainer {...navigationProps} />
<CalendarTable
selectedDate={date}
calendarWeekDays={calendarWeekDays}
weekDayLabels={weekDayLabels}
cellSize={cellSize}
width={width}
/>
</div>
<style jsx>{`
.calendar-wrapper {
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
background-color: ${backgroundColor};
display: flex;
flex-direction: column;
border: 1px solid ${wrapperBorderColor};
border-radius: 3px;
min-width: ${width};
width: max-content;
box-shadow: 0px 4px 6px -2px #2129340d;
box-shadow: 0px 10px 15px -3px #2129341a;
}
`}</style>
</div>
)
}

CalendarContainer.defaultProps = {
cellSize: '32px',
width: '240px',
}

CalendarContainer.propTypes = {
/** the currently selected date using an iso-like format YYYY-MM-DD, in the calendar system provided (not iso8601) */
date: PropTypes.string,
...CalendarTableProps,
...NavigationContainerProps,
}
4 changes: 3 additions & 1 deletion components/calendar/src/calendar/calendar-table.js
Original file line number Diff line number Diff line change
@@ -48,7 +48,7 @@ export const CalendarTable = ({
</div>
)

CalendarTable.propTypes = {
export const CalendarTableProps = {
calendarWeekDays: PropTypes.arrayOf(
PropTypes.arrayOf(
PropTypes.shape({
@@ -70,3 +70,5 @@ CalendarTable.propTypes = {
weekDayLabels: PropTypes.arrayOf(PropTypes.string),
width: PropTypes.string,
}

CalendarTable.propTypes = CalendarTableProps
69 changes: 27 additions & 42 deletions components/calendar/src/calendar/calendar.js
Original file line number Diff line number Diff line change
@@ -2,11 +2,9 @@ import {
useDatePicker,
useResolvedDirection,
} from '@dhis2/multi-calendar-dates'
import { colors } from '@dhis2/ui-constants'
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import { CalendarTable } from './calendar-table.js'
import { NavigationContainer } from './navigation-container.js'
import React, { useMemo, useState } from 'react'
import { CalendarContainer } from './calendar-container.js'

export const Calendar = ({
onDateSelect,
@@ -20,9 +18,6 @@ export const Calendar = ({
width,
cellSize,
}) => {
const wrapperBorderColor = colors.grey300
const backgroundColor = 'none'

const [selectedDateString, setSelectedDateString] = useState(date)
const languageDirection = useResolvedDirection(dir, locale)

@@ -34,7 +29,7 @@ export const Calendar = ({
weekDayFormat,
}

const pickerOptions = useDatePicker({
const pickerResults = useDatePicker({
onDateSelect: (result) => {
const { calendarDateString } = result
setSelectedDateString(calendarDateString)
@@ -44,43 +39,33 @@ export const Calendar = ({
options,
})

const { calendarWeekDays, weekDayLabels } = pickerOptions
const calendarProps = useMemo(() => {
return {
date,
dir,
locale,
width,
cellSize,
// minDate,
// maxDate,
// validation, // todo: clarify how we use validation props (and format) in Calendar (not CalendarInput)
// format,
isValid: pickerResults.isValid,
calendarWeekDays: pickerResults.calendarWeekDays,
weekDayLabels: pickerResults.weekDayLabels,
currMonth: pickerResults.currMonth,
currYear: pickerResults.currYear,
nextMonth: pickerResults.nextMonth,
nextYear: pickerResults.nextYear,
prevMonth: pickerResults.prevMonth,
prevYear: pickerResults.prevYear,
languageDirection,
}
}, [cellSize, date, dir, locale, pickerResults, width, languageDirection])

return (
<div>
<div
className="calendar-wrapper"
dir={languageDirection}
data-test="calendar"
>
<NavigationContainer
pickerOptions={pickerOptions}
languageDirection={languageDirection}
/>
<CalendarTable
selectedDate={selectedDateString}
calendarWeekDays={calendarWeekDays}
weekDayLabels={weekDayLabels}
cellSize={cellSize}
width={width}
/>
</div>
<style jsx>{`
.calendar-wrapper {
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
background-color: ${backgroundColor};
display: flex;
flex-direction: column;
border: 1px solid ${wrapperBorderColor};
border-radius: 3px;
min-width: ${width};
width: max-content;
box-shadow: 0px 4px 6px -2px #2129340d;
box-shadow: 0px 10px 15px -3px #2129341a;
}
`}</style>
<CalendarContainer {...calendarProps} />
</div>
)
}
Loading

0 comments on commit 99a78f5

Please sign in to comment.