Skip to content

Commit

Permalink
feat(date): initialize the known segments if they have one possible v…
Browse files Browse the repository at this point in the history
…alue (#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
  • Loading branch information
logaretm authored Mar 2, 2025
1 parent e969bb7 commit a764650
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-rings-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@formwerk/core': patch
---

fix: initialize known date segments and disable locked ones
19 changes: 18 additions & 1 deletion packages/core/src/useDateTimeField/constants.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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];
}
40 changes: 38 additions & 2 deletions packages/core/src/useDateTimeField/temporalPartial.ts
Original file line number Diff line number Diff line change
@@ -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<ZonedDateTime>,
max?: Maybe<ZonedDateTime>,
) {
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;
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/useDateTimeField/useDateTimeField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export function useDateTimeField(_props: Reactivify<DateTimeFieldProps, 'schema'

useInputValidity({ field });

const min = computed(() => 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,
Expand All @@ -119,6 +122,8 @@ export function useDateTimeField(_props: Reactivify<DateTimeFieldProps, 'schema'
get: () => field.fieldValue.value,
set: value => field.setValue(value),
},
min,
max,
});

function onValueChange(value: ZonedDateTime) {
Expand All @@ -135,8 +140,8 @@ export function useDateTimeField(_props: Reactivify<DateTimeFieldProps, 'schema'
readonly: props.readonly,
onValueChange,
onTouched: () => 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({
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/useDateTimeField/useDateTimeSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
const segmentEl = shallowRef<HTMLSpanElement>();
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');
Expand All @@ -75,6 +74,7 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
isLast,
focusNext,
isNumeric,
isLockedByRange,
} = segmentGroup.useDateSegmentRegistration({
id,
getElem: () => segmentEl.value,
Expand All @@ -83,13 +83,19 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {

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;
}
Expand Down Expand Up @@ -143,7 +149,7 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
currentInput = '';
},
onKeydown(evt: KeyboardEvent) {
if (toValue(props.readonly) || isDisabled.value) {
if (isNonEditable()) {
return;
}

Expand Down Expand Up @@ -185,7 +191,7 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
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,
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/useDateTimeField/useDateTimeSegmentGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useEventListener } from '../helpers/useEventListener';
import {
getSegmentTypePlaceholder,
isEditableSegmentType,
isEqualPart,
isNumericByDefault,
isOptionalSegmentType,
segmentTypeToDurationLike,
Expand Down Expand Up @@ -34,6 +35,7 @@ export interface DateTimeSegmentGroupContext {
onTouched(): void;
isLast(): boolean;
focusNext(): void;
isLockedByRange(): boolean;
};
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -252,6 +266,7 @@ export function useDateTimeSegmentGroup({
isLast,
focusNext,
isNumeric,
isLockedByRange,
};
}

Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/useDateTimeField/useTemporalStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ interface TemporalValueStoreInit {
timeZone: MaybeRefOrGetter<string>;
calendar: MaybeRefOrGetter<Calendar>;
allowPartial?: boolean;
min?: MaybeRefOrGetter<Maybe<ZonedDateTime>>;
max?: MaybeRefOrGetter<Maybe<ZonedDateTime>>;
}

export function useTemporalStore(init: TemporalValueStoreInit) {
const model = init.model;

function normalizeNullish(value: Maybe<ZonedDateTime>): 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;
Expand Down

0 comments on commit a764650

Please sign in to comment.