Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update time field with AM/PM #8831

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/client/src/components/form/FormFieldGenerator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ const GeneratedInputField = React.memo<GeneratedInputFieldProps>(
<InputField {...inputFieldProps}>
<TimeField
{...inputProps}
use12HourFormat={fieldDefinition.use12HourFormat}
ignorePlaceHolder={fieldDefinition.ignorePlaceHolder}
onChange={onChangeGroupInput}
value={value as string}
Expand Down
2 changes: 2 additions & 0 deletions packages/client/src/forms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,7 @@ export interface ILoaderButton extends IFormFieldBase {
interface ITimeFormFIeld extends IFormFieldBase {
type: typeof TIME
ignorePlaceHolder?: boolean
use12HourFormat?: boolean
}

export interface ISignatureFormField extends IFormFieldBase {
Expand Down Expand Up @@ -1249,6 +1250,7 @@ interface I18nHeading3Field extends Ii18nFormFieldBase {
interface Ii18nTimeFormField extends Ii18nFormFieldBase {
type: typeof TIME
ignorePlaceHolder?: boolean
use12HourFormat?: boolean
}

interface Ii18nSignatureField extends Ii18nFormFieldBase {
Expand Down
124 changes: 88 additions & 36 deletions packages/components/src/TimeField/TimeField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import * as React from 'react'
import styled from 'styled-components'
import { ITextInputProps, TextInput } from '../TextInput/TextInput'
import { useIntl } from 'react-intl'
import { ISelectProps, Select } from '../Select/Select'

export interface IProps {
id: string
Expand All @@ -20,6 +22,7 @@ export interface IProps {
notice?: string
value?: string
ignorePlaceHolder?: boolean
use12HourFormat?: boolean
onChange: (dateString: string) => void
}

Expand All @@ -35,10 +38,18 @@ const Container = styled.div`
`

export type ITimeFieldProps = IProps &
Omit<ITextInputProps, 'onChange' | 'value'>

function getFormattedValue(time: { hh: string; mm: string }) {
return `${time.hh.padStart(2, '0')}:${time.mm.padStart(2, '0')}`
Omit<ITextInputProps, 'onChange' | 'value'> &
Omit<ISelectProps, 'onChange' | 'value'>

function getFormattedValue(
time: { hh: string; mm: string },
use12HourFormat: boolean,
amPm: string | null
) {
const formattedHours = time.hh.padStart(2, '0')
return use12HourFormat
? `${formattedHours}:${time.mm.padStart(2, '0')} ${amPm}`
: `${formattedHours}:${time.mm.padStart(2, '0')}`
}

function isValidMinutes(minutes: string) {
Expand All @@ -52,15 +63,16 @@ function isValidMinutes(minutes: string) {
return parsed >= 0 && parsed <= 59
}

function isValidHours(hours: string) {
if (hours.length !== 2) {
return false
}
function isValidHours(hours: string, use12HourFormat: boolean) {
if (hours.length !== 2) return false

const parsed = Number(hours)
if (isNaN(parsed)) {
return false
}
return parsed >= 0 && parsed <= 23

if (isNaN(parsed)) return false

return use12HourFormat
? parsed >= 1 && parsed <= 12
: parsed >= 0 && parsed <= 23
}

export function TimeField(props: ITimeFieldProps) {
Expand All @@ -70,6 +82,7 @@ export function TimeField(props: ITimeFieldProps) {
focusInput,
notice,
ignorePlaceHolder,
use12HourFormat = false,
onChange,
...otherProps
} = props
Expand All @@ -79,37 +92,45 @@ export function TimeField(props: ITimeFieldProps) {
mm: ''
})

const [amPm, setAmPm] = React.useState<string>(use12HourFormat ? 'AM' : '') // Default to AM for 12-hour format

React.useEffect(() => {
function getInitialState(time: string): IState {
const dateSegmentVals = time.split(':')
return {
hh: dateSegmentVals[0],
mm: dateSegmentVals[1]
}
const parts = time.split(/[:\s]/)

const [hh, mm, meridiem] = parts.length === 3 ? parts : [...parts, null]

if (use12HourFormat && meridiem) setAmPm(meridiem)

return { hh: hh || '', mm: mm || '' }
}

const isValidTime = (time: string) => {
const parts = time.split(':')
const cleanTime = time.replace(/\s?(AM|PM)$/i, '')
const parts = cleanTime.split(':')

if (parts.length !== 2) {
return false
}
if (parts.length !== 2) return false

return isValidHours(parts[0]) && isValidMinutes(parts[1])
return isValidHours(parts[0], use12HourFormat) && isValidMinutes(parts[1])
}

if (props.value && isValidTime(props.value)) {
setState(getInitialState(props.value))
}
}, [props.value])
}, [props.value, use12HourFormat])

const hh = React.useRef<HTMLInputElement>(null)
const mm = React.useRef<HTMLInputElement>(null)
const intl = useIntl()

function change(event: React.ChangeEvent<HTMLInputElement>) {
const val = event.target.value
if (event.target.id.includes('hh')) {
if (Number(val) < 0 || Number(val) > 23) return
if (use12HourFormat) {
if (val === '00' || Number(val) < 0 || Number(val) > 12) return
} else {
if (Number(val) < 0 || Number(val) > 23) return
}
if (val.length === 2 && mm?.current !== null) {
mm.current.focus()
}
Expand All @@ -122,20 +143,21 @@ export function TimeField(props: ITimeFieldProps) {

function padStart(part: 'hh' | 'mm') {
return (event: React.FocusEvent<HTMLInputElement>) => {
const val = event.target.value
if (part === 'hh') {
setState((state) => ({ ...state, hh: val.padStart(2, '0') }))
} else if (part === 'mm') {
setState((state) => ({ ...state, mm: val.padStart(2, '0') }))
let val = event.target.value
if (part === 'hh' && use12HourFormat && (!val || val === '0')) {
val = '01'
}
const paddedValue = val.padStart(2, '0')

setState((state) => ({ ...state, [part]: paddedValue }))
}
}

React.useEffect(() => {
if (isValidHours(state.hh) && isValidMinutes(state.mm)) {
onChange(getFormattedValue(state))
if (isValidHours(state.hh, use12HourFormat) && isValidMinutes(state.mm)) {
onChange(getFormattedValue(state, use12HourFormat, amPm))
}
}, [state, onChange])
}, [state, amPm, onChange, use12HourFormat])

return (
<Container id={id}>
Expand All @@ -149,8 +171,8 @@ export function TimeField(props: ITimeFieldProps) {
focusInput={focusInput}
type="number"
placeholder={ignorePlaceHolder ? '' : 'hh'}
min={1}
max={31}
min={use12HourFormat ? 1 : 0}
max={use12HourFormat ? 12 : 23}
value={state.hh}
onChange={change}
onBlur={padStart('hh')}
Expand All @@ -168,15 +190,45 @@ export function TimeField(props: ITimeFieldProps) {
focusInput={focusInput}
type="number"
placeholder={ignorePlaceHolder ? '' : 'mm'}
min={1}
max={31}
min={0}
max={59}
value={state.mm}
onChange={change}
onBlur={padStart('mm')}
onWheel={(event: React.WheelEvent<HTMLInputElement>) => {
event.currentTarget.blur()
}}
/>
{use12HourFormat && (
<Select
{...props}
id={`${id}-amPm`}
error={Boolean(meta && meta.error)}
touched={meta && meta.touched}
focusInput={focusInput}
placeholder={ignorePlaceHolder ? '' : 'mm'}
options={[
{
label: intl.formatMessage({
id: 'timeField.meridiem.am',
defaultMessage: 'AM',
description: 'Option label: AM'
}),
value: 'AM'
},
{
label: intl.formatMessage({
id: 'timeField.meridiem.pm',
defaultMessage: 'PM',
description: 'Option label: PM'
}),
value: 'PM'
}
]}
value={amPm}
onChange={(value: string) => setAmPm(value)}
/>
)}
</Container>
)
}