From a764650845b6c14a7ac84740e5ffd02817256554 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sun, 2 Mar 2025 12:06:01 +0200 Subject: [PATCH] feat(date): initialize the known segments if they have one possible value (#145) * feat: initialize the known segments if they have one possible value * feat: lock non editable segments * fix: start initial values with zero time components * fix: types * chore: added changeset --- .changeset/sour-rings-listen.md | 5 +++ .../core/src/useDateTimeField/constants.ts | 19 ++++++++- .../src/useDateTimeField/temporalPartial.ts | 40 ++++++++++++++++++- .../src/useDateTimeField/useDateTimeField.ts | 9 ++++- .../useDateTimeField/useDateTimeSegment.ts | 14 +++++-- .../useDateTimeSegmentGroup.ts | 15 +++++++ .../src/useDateTimeField/useTemporalStore.ts | 10 ++++- 7 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 .changeset/sour-rings-listen.md diff --git a/.changeset/sour-rings-listen.md b/.changeset/sour-rings-listen.md new file mode 100644 index 00000000..f24606bc --- /dev/null +++ b/.changeset/sour-rings-listen.md @@ -0,0 +1,5 @@ +--- +'@formwerk/core': patch +--- + +fix: initialize known date segments and disable locked ones diff --git a/packages/core/src/useDateTimeField/constants.ts b/packages/core/src/useDateTimeField/constants.ts index 8457289e..daf9d729 100644 --- a/packages/core/src/useDateTimeField/constants.ts +++ b/packages/core/src/useDateTimeField/constants.ts @@ -1,5 +1,5 @@ import { DateTimeSegmentType } from './types'; -import type { DateTimeDuration } from '@internationalized/date'; +import type { DateTimeDuration, ZonedDateTime } from '@internationalized/date'; export function isEditableSegmentType(type: DateTimeSegmentType) { return !['era', 'timeZoneName', 'literal'].includes(type); @@ -53,3 +53,20 @@ export function isNumericByDefault(type: DateTimeSegmentType) { return map[type] ?? false; } + +type EditableSegmentType = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'; + +export function getOrderedSegmentTypes(): EditableSegmentType[] { + return ['year', 'month', 'day', 'hour', 'minute', 'second']; +} + +export function isEqualPart(min: ZonedDateTime, max: ZonedDateTime, part: DateTimeSegmentType) { + const editablePart = part as EditableSegmentType; + const parts = getOrderedSegmentTypes(); + const idx = parts.indexOf(editablePart); + if (idx === -1) { + return false; + } + + return parts.slice(0, idx).every(p => min[p] === max[p]) && min[editablePart] === max[editablePart]; +} diff --git a/packages/core/src/useDateTimeField/temporalPartial.ts b/packages/core/src/useDateTimeField/temporalPartial.ts index e4564326..ee779b86 100644 --- a/packages/core/src/useDateTimeField/temporalPartial.ts +++ b/packages/core/src/useDateTimeField/temporalPartial.ts @@ -1,9 +1,45 @@ import { DateTimeSegmentType, TemporalPartial } from './types'; import { isObject } from '../../../shared/src'; import { Calendar, ZonedDateTime, now, toCalendar } from '@internationalized/date'; +import { Maybe } from '../types'; +import { getOrderedSegmentTypes, isEqualPart } from './constants'; -export function createTemporalPartial(calendar: Calendar, timeZone: string) { - const zonedDateTime = toCalendar(now(timeZone), calendar) as TemporalPartial; +export function createTemporalPartial( + calendar: Calendar, + timeZone: string, + min?: Maybe, + max?: Maybe, +) { + if (min && max) { + // Get the middle of the min and max + const diff = Math.round(max.compare(min) / 2); + const zonedDateTime = min + .add({ + milliseconds: diff, + }) + .set({ + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }) as TemporalPartial; + zonedDateTime['~fw_temporal_partial'] = {}; + + const parts = getOrderedSegmentTypes(); + // If min and max parts are the same, then all parts are set, but we have to check previous parts for every part. + parts.forEach(part => { + zonedDateTime['~fw_temporal_partial'][part] = isEqualPart(min, max, part); + }); + + return zonedDateTime; + } + + const zonedDateTime = toCalendar(now(timeZone), calendar).set({ + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + }) as TemporalPartial; zonedDateTime['~fw_temporal_partial'] = {}; return zonedDateTime; diff --git a/packages/core/src/useDateTimeField/useDateTimeField.ts b/packages/core/src/useDateTimeField/useDateTimeField.ts index 30bb5e6d..06fdae63 100644 --- a/packages/core/src/useDateTimeField/useDateTimeField.ts +++ b/packages/core/src/useDateTimeField/useDateTimeField.ts @@ -111,6 +111,9 @@ export function useDateTimeField(_props: Reactivify fromDateToCalendarZonedDateTime(toValue(props.min), calendar.value, timeZone.value)); + const max = computed(() => fromDateToCalendarZonedDateTime(toValue(props.max), calendar.value, timeZone.value)); + const temporalValue = useTemporalStore({ calendar: calendar, timeZone: timeZone, @@ -119,6 +122,8 @@ export function useDateTimeField(_props: Reactivify field.fieldValue.value, set: value => field.setValue(value), }, + min, + max, }); function onValueChange(value: ZonedDateTime) { @@ -135,8 +140,8 @@ export function useDateTimeField(_props: Reactivify field.setTouched(true), - min: computed(() => fromDateToCalendarZonedDateTime(toValue(props.min), calendar.value, timeZone.value)), - max: computed(() => fromDateToCalendarZonedDateTime(toValue(props.max), calendar.value, timeZone.value)), + min, + max, }); const { labelProps, labelledByProps } = useLabel({ diff --git a/packages/core/src/useDateTimeField/useDateTimeSegment.ts b/packages/core/src/useDateTimeField/useDateTimeSegment.ts index 85ec850b..582dcf76 100644 --- a/packages/core/src/useDateTimeField/useDateTimeSegment.ts +++ b/packages/core/src/useDateTimeField/useDateTimeSegment.ts @@ -57,7 +57,6 @@ export function useDateTimeSegment(_props: Reactivify) { const segmentEl = shallowRef(); const segmentGroup = inject(DateTimeSegmentGroupKey, null); const isDisabled = createDisabledContext(props.disabled); - const isNonEditable = () => isDisabled.value || !isEditableSegmentType(toValue(props.type)); if (!segmentGroup) { throw new Error('DateTimeSegmentGroup is not provided'); @@ -75,6 +74,7 @@ export function useDateTimeSegment(_props: Reactivify) { isLast, focusNext, isNumeric, + isLockedByRange, } = segmentGroup.useDateSegmentRegistration({ id, getElem: () => segmentEl.value, @@ -83,13 +83,19 @@ export function useDateTimeSegment(_props: Reactivify) { let currentInput = ''; + function isNonEditable() { + return ( + !isEditableSegmentType(toValue(props.type)) || isDisabled.value || toValue(props.readonly) || isLockedByRange() + ); + } + const handlers = { onFocus() { // Reset the current input when the segment is focused currentInput = ''; }, onBeforeinput(evt: InputEvent) { - if (toValue(props.readonly) || isDisabled.value) { + if (isNonEditable()) { blockEvent(evt); return; } @@ -143,7 +149,7 @@ export function useDateTimeSegment(_props: Reactivify) { currentInput = ''; }, onKeydown(evt: KeyboardEvent) { - if (toValue(props.readonly) || isDisabled.value) { + if (isNonEditable()) { return; } @@ -185,7 +191,7 @@ export function useDateTimeSegment(_props: Reactivify) { id, tabindex: isNonEditable() ? -1 : 0, contenteditable: isNonEditable() ? undefined : ceValue, - 'aria-disabled': isDisabled.value, + 'aria-disabled': isNonEditable(), 'data-segment-type': toValue(props.type), 'aria-label': isNonEditable() ? undefined : toValue(props.type), 'aria-readonly': toValue(props.readonly) ? true : undefined, diff --git a/packages/core/src/useDateTimeField/useDateTimeSegmentGroup.ts b/packages/core/src/useDateTimeField/useDateTimeSegmentGroup.ts index 313bf94e..464c3531 100644 --- a/packages/core/src/useDateTimeField/useDateTimeSegmentGroup.ts +++ b/packages/core/src/useDateTimeField/useDateTimeSegmentGroup.ts @@ -7,6 +7,7 @@ import { useEventListener } from '../helpers/useEventListener'; import { getSegmentTypePlaceholder, isEditableSegmentType, + isEqualPart, isNumericByDefault, isOptionalSegmentType, segmentTypeToDurationLike, @@ -34,6 +35,7 @@ export interface DateTimeSegmentGroupContext { onTouched(): void; isLast(): boolean; focusNext(): void; + isLockedByRange(): boolean; }; } @@ -166,6 +168,18 @@ export function useDateTimeSegmentGroup({ onValueChange(withAllPartsSet(date)); } + function isLockedByRange() { + const type = segment.getType(); + const minDate = toValue(min); + const maxDate = toValue(max); + // Can't be locked when either bound is open. + if (!minDate || !maxDate) { + return false; + } + + return isEqualPart(minDate, maxDate, type); + } + function getMetadata() { const type = segment.getType(); const date = toValue(temporalValue); @@ -252,6 +266,7 @@ export function useDateTimeSegmentGroup({ isLast, focusNext, isNumeric, + isLockedByRange, }; } diff --git a/packages/core/src/useDateTimeField/useTemporalStore.ts b/packages/core/src/useDateTimeField/useTemporalStore.ts index 1f66156e..ddc5d2b9 100644 --- a/packages/core/src/useDateTimeField/useTemporalStore.ts +++ b/packages/core/src/useDateTimeField/useTemporalStore.ts @@ -14,13 +14,21 @@ interface TemporalValueStoreInit { timeZone: MaybeRefOrGetter; calendar: MaybeRefOrGetter; allowPartial?: boolean; + min?: MaybeRefOrGetter>; + max?: MaybeRefOrGetter>; } export function useTemporalStore(init: TemporalValueStoreInit) { const model = init.model; + function normalizeNullish(value: Maybe): ZonedDateTime | TemporalPartial { if (isNullOrUndefined(value)) { - return createTemporalPartial(toValue(init.calendar), toValue(init.timeZone)); + return createTemporalPartial( + toValue(init.calendar), + toValue(init.timeZone), + toValue(init.min), + toValue(init.max), + ); } return value;