Skip to content

Commit

Permalink
feat: implement placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Feb 8, 2025
1 parent 8b25ec0 commit 73ccfcf
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 30 deletions.
10 changes: 5 additions & 5 deletions packages/core/src/useCalendar/useCalendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { computed, InjectionKey, MaybeRefOrGetter, nextTick, provide, ref, Ref,
import { Temporal } from '@js-temporal/polyfill';
import { CalendarDay, CalendarIdentifier } from './types';
import { hasKeyCode, normalizeProps, useUniqId, withRefCapture } from '../utils/common';
import { Reactivify } from '../types';
import { Maybe, Reactivify } from '../types';
import { useDateFormatter, useLocale } from '../i18n';
import { WeekInfo } from '../i18n/getWeekInfo';
import { FieldTypePrefixes } from '../constants';
Expand Down Expand Up @@ -55,21 +55,21 @@ export interface CalendarProps {
/**
* The minimum date to use for the calendar.
*/
minDate?: Temporal.ZonedDateTime;
minDate?: Maybe<Temporal.ZonedDateTime>;

/**
* The maximum date to use for the calendar.
*/
maxDate?: Temporal.ZonedDateTime;
maxDate?: Maybe<Temporal.ZonedDateTime>;
}

interface CalendarContext {
locale: Ref<string>;
weekInfo: Ref<WeekInfo>;
calendar: Ref<CalendarIdentifier>;
selectedDate: MaybeRefOrGetter<Temporal.ZonedDateTime>;
getMinDate: () => Temporal.ZonedDateTime | undefined;
getMaxDate: () => Temporal.ZonedDateTime | undefined;
getMinDate: () => Maybe<Temporal.ZonedDateTime>;
getMaxDate: () => Maybe<Temporal.ZonedDateTime>;
getFocusedDate: () => Temporal.ZonedDateTime;
setDay: (date: Temporal.ZonedDateTime) => void;
setFocusedDay: (date: Temporal.ZonedDateTime) => void;
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/useDateTimeField/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ export function isEditableSegmentType(type: DateTimeSegmentType) {
return !['era', 'timeZoneName', 'literal'].includes(type);
}

export function isOptionalSegmentType(type: DateTimeSegmentType) {
const optionalTypes: DateTimeSegmentType[] = ['dayPeriod', 'weekday', 'era'];

return optionalTypes.includes(type);
}

export function segmentTypeToDurationLike(type: DateTimeSegmentType): keyof Temporal.DurationLike | undefined {
const map: Partial<Record<DateTimeSegmentType, keyof Temporal.DurationLike>> = {
year: 'years',
Expand All @@ -19,3 +25,18 @@ export function segmentTypeToDurationLike(type: DateTimeSegmentType): keyof Temp

return map[type];
}

export function getSegmentTypePlaceholder(type: DateTimeSegmentType) {
const map: Partial<Record<DateTimeSegmentType, string>> = {
year: 'YYYY',
month: 'MM',
day: 'DD',
hour: 'HH',
minute: 'mm',
second: 'ss',
dayPeriod: 'AM',
weekday: 'ddd',
};

return map[type];
}
34 changes: 34 additions & 0 deletions packages/core/src/useDateTimeField/temporalPartial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Temporal } from '@js-temporal/polyfill';
import { DateTimeSegmentType, TemporalPartial } from './types';
import { CalendarIdentifier } from '../useCalendar';
import { isObject } from '../../../shared/src';

export function createTemporalPartial(calendar: CalendarIdentifier, timeZone: string) {
const zonedDateTime = Temporal.Now.zonedDateTime(calendar, timeZone);
zonedDateTime['~fw_temporal_partial'] = {};

Check failure on line 8 in packages/core/src/useDateTimeField/temporalPartial.ts

View workflow job for this annotation

GitHub Actions / typecheck

Element implicitly has an 'any' type because expression of type '"~fw_temporal_partial"' can't be used to index type 'ZonedDateTime'.

return zonedDateTime as TemporalPartial;
}

export function toTemporalPartial(
value: Temporal.ZonedDateTime | TemporalPartial,
setParts?: DateTimeSegmentType[],
): TemporalPartial {
const clone = Temporal.ZonedDateTime.from(value);
clone['~fw_temporal_partial'] = isTemporalPartial(value) ? value['~fw_temporal_partial'] : {};

Check failure on line 18 in packages/core/src/useDateTimeField/temporalPartial.ts

View workflow job for this annotation

GitHub Actions / typecheck

Element implicitly has an 'any' type because expression of type '"~fw_temporal_partial"' can't be used to index type 'ZonedDateTime'.
if (setParts) {
setParts.forEach(part => {
clone['~fw_temporal_partial'][part] = true;

Check failure on line 21 in packages/core/src/useDateTimeField/temporalPartial.ts

View workflow job for this annotation

GitHub Actions / typecheck

Element implicitly has an 'any' type because expression of type '"~fw_temporal_partial"' can't be used to index type 'ZonedDateTime'.
});
}

return clone as TemporalPartial;
}

export function isTemporalPartial(value: Temporal.ZonedDateTime): value is TemporalPartial {
return isObject(value['~fw_temporal_partial']);

Check failure on line 29 in packages/core/src/useDateTimeField/temporalPartial.ts

View workflow job for this annotation

GitHub Actions / typecheck

Element implicitly has an 'any' type because expression of type '"~fw_temporal_partial"' can't be used to index type 'ZonedDateTime'.
}

export function isTemporalPartSet(value: TemporalPartial, part: DateTimeSegmentType): boolean {
return part in value['~fw_temporal_partial'] && value['~fw_temporal_partial'][part] === true;
}
7 changes: 7 additions & 0 deletions packages/core/src/useDateTimeField/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ export type TemporalValue =
| Temporal.ZonedDateTime;

export type DateValue = Date | TemporalValue;

export type TemporalPartial = Temporal.ZonedDateTime & {
[`~fw_temporal_partial`]: {
[key: string]: boolean | undefined;
};
[`~fw_temporal_full_partial`]?: true;
};
2 changes: 1 addition & 1 deletion packages/core/src/useDateTimeField/useDateTimeField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function useDateTimeField(_props: Reactivify<DateTimeFieldProps, 'schema'
const field = useFormField<Maybe<Date>>({
path: props.name,
disabled: props.disabled,
initialValue: toValue(props.modelValue) ?? toValue(props.value) ?? new Date(),
initialValue: toValue(props.modelValue) ?? toValue(props.value),
schema: props.schema,
});

Expand Down
18 changes: 13 additions & 5 deletions packages/core/src/useDateTimeField/useDateTimeSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
throw new Error('DateTimeSegmentGroup is not provided');
}

const { increment, decrement, setValue, getMetadata, onDone, parser } = segmentGroup.useDateSegmentRegistration({
id,
getElem: () => segmentEl.value,
getType: () => toValue(props.type),
});
const { increment, decrement, setValue, getMetadata, onDone, parser, clear } =
segmentGroup.useDateSegmentRegistration({
id,
getElem: () => segmentEl.value,
getType: () => toValue(props.type),
});

const isNumeric = computed(() => parser.isValidNumberPart(toValue(props.value)));

Expand Down Expand Up @@ -124,6 +125,13 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
}
return;
}

if (hasKeyCode(evt, 'Backspace') || hasKeyCode(evt, 'Delete')) {
blockEvent(evt);
if (!isNonEditable()) {
clear();
}
}
},
};

Expand Down
110 changes: 100 additions & 10 deletions packages/core/src/useDateTimeField/useDateTimeSegmentGroup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { InjectionKey, MaybeRefOrGetter, provide, ref, toValue, Ref, onBeforeUnmount, computed } from 'vue';
import { Temporal, Intl as TemporalIntl } from '@js-temporal/polyfill';
import { DateTimeSegmentType } from './types';
import { DateTimeSegmentType, TemporalPartial } from './types';
import { hasKeyCode } from '../utils/common';
import { blockEvent } from '../utils/events';
import { Direction, Maybe } from '../types';
import { useEventListener } from '../helpers/useEventListener';
import { isEditableSegmentType, segmentTypeToDurationLike } from './constants';
import {
getSegmentTypePlaceholder,
isEditableSegmentType,
isOptionalSegmentType,
segmentTypeToDurationLike,
} from './constants';
import { NumberParserContext, useNumberParser } from '../i18n';
import { isTemporalPartial, isTemporalPartSet, toTemporalPartial } from './temporalPartial';

export interface DateTimeSegmentRegistration {
id: string;
Expand All @@ -22,6 +28,7 @@ export interface DateTimeSegmentGroupContext {
setValue(value: number): void;
getMetadata(): { min: number | null; max: number | null; maxLength: number | null };
onDone(): void;
clear(): void;
};
}

Expand All @@ -31,7 +38,7 @@ export interface DateTimeSegmentGroupProps {
formatter: Ref<TemporalIntl.DateTimeFormat>;
locale: MaybeRefOrGetter<string | undefined>;
formatOptions: MaybeRefOrGetter<Maybe<Intl.DateTimeFormatOptions>>;
temporalValue: MaybeRefOrGetter<Temporal.ZonedDateTime>;
temporalValue: MaybeRefOrGetter<Temporal.ZonedDateTime | TemporalPartial>;
direction?: MaybeRefOrGetter<Direction>;
controlEl: Ref<HTMLElement | undefined>;
onValueChange: (value: Temporal.ZonedDateTime) => void;
Expand All @@ -58,10 +65,52 @@ export function useDateTimeSegmentGroup({

const segments = computed(() => {
const date = toValue(temporalValue);
const parts = formatter.value.formatToParts(date.toPlainDateTime()) as {
type: DateTimeSegmentType;
value: string;
}[];

if (isTemporalPartial(date)) {
for (const part of parts) {
if (!isEditableSegmentType(part.type)) {
continue;
}

if (!isTemporalPartSet(date, part.type)) {
part.value = getSegmentTypePlaceholder(part.type) ?? part.value;
}
}
}

return formatter.value.formatToParts(date.toPlainDateTime()) as { type: DateTimeSegmentType; value: string }[];
return parts;
});

function getRequiredParts() {
return segments.value
.filter(part => {
return isEditableSegmentType(part.type) || isOptionalSegmentType(part.type);
})
.map(part => part.type);
}

function isAllPartsSet(value: TemporalPartial) {
return segments.value.every(part => {
if (!isEditableSegmentType(part.type) || isOptionalSegmentType(part.type)) {
return true;
}

return isTemporalPartSet(value, part.type);
});
}

function withAllPartsSet(value: Temporal.ZonedDateTime) {
if (isTemporalPartial(value) && isAllPartsSet(value)) {
return Temporal.ZonedDateTime.from(value); // clones the value and drops the partial flag
}

return value;
}

function onSegmentDone() {
focusNextSegment();
}
Expand All @@ -76,21 +125,21 @@ export function useDateTimeSegmentGroup({
const type = segment.getType();
const date = addToPart(type, 1);

onValueChange(date);
onValueChange(withAllPartsSet(date));
}

function decrement() {
const type = segment.getType();
const date = addToPart(type, -1);

onValueChange(date);
onValueChange(withAllPartsSet(date));
}

function setValue(value: number) {
const type = segment.getType();
const date = setPart(type, value);

onValueChange(date);
onValueChange(withAllPartsSet(date));
}

function getMetadata() {
Expand Down Expand Up @@ -130,6 +179,15 @@ export function useDateTimeSegmentGroup({
};
}

function clear() {
const type = segment.getType();
const date = toValue(temporalValue);
const next = toTemporalPartial(date, !isTemporalPartial(date) ? getRequiredParts() : []);
next['~fw_temporal_partial'][type] = false;

onValueChange(next);
}

return {
increment,
decrement,
Expand All @@ -138,6 +196,7 @@ export function useDateTimeSegmentGroup({
onSegmentDone,
getMetadata,
onDone: onSegmentDone,
clear,
};
}

Expand Down Expand Up @@ -217,7 +276,7 @@ export function useDateTimeSegmentGroup({
}

interface ArithmeticInit {
currentDate: MaybeRefOrGetter<Temporal.ZonedDateTime>;
currentDate: MaybeRefOrGetter<Temporal.ZonedDateTime | TemporalPartial>;
}

function useDateArithmetic({ currentDate }: ArithmeticInit) {
Expand All @@ -231,14 +290,22 @@ function useDateArithmetic({ currentDate }: ArithmeticInit) {
return date;
}

return date.with({
const newDate = date.with({
[part]: value,
});

if (isTemporalPartial(date)) {
newDate['~fw_temporal_partial'] = {

Check failure on line 298 in packages/core/src/useDateTimeField/useDateTimeSegmentGroup.ts

View workflow job for this annotation

GitHub Actions / typecheck

Element implicitly has an 'any' type because expression of type '"~fw_temporal_partial"' can't be used to index type 'ZonedDateTime'.
...date['~fw_temporal_partial'],
[part]: true,
};
}

return newDate;
}

function addToPart(part: DateTimeSegmentType, diff: number) {
const date = toValue(currentDate);

if (!isEditableSegmentType(part)) {
return date;
}
Expand All @@ -252,6 +319,29 @@ function useDateArithmetic({ currentDate }: ArithmeticInit) {
return date;
}

if (isTemporalPartial(date)) {
let newDate: Temporal.ZonedDateTime | TemporalPartial = date;
if (isTemporalPartSet(date, part)) {
newDate = date.add({
[durationPart]: diff,
});
} else {
newDate =
part === 'dayPeriod'
? date
: date.with({
[part]: part === 'year' ? date.year : 1,
});
}

newDate['~fw_temporal_partial'] = {

Check failure on line 337 in packages/core/src/useDateTimeField/useDateTimeSegmentGroup.ts

View workflow job for this annotation

GitHub Actions / typecheck

Element implicitly has an 'any' type because expression of type '"~fw_temporal_partial"' can't be used to index type 'ZonedDateTime'.
...date['~fw_temporal_partial'],
[part]: true,
};

return newDate;
}

// Preserves the day, month, and year when adding to the part so it doesn't overflow.
const day = date.day;
const month = date.month;
Expand Down
Loading

0 comments on commit 73ccfcf

Please sign in to comment.