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

feat(date): initialize the known segments if they have one possible value #145

Merged
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
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
Loading