Skip to content

Commit

Permalink
fix: better numeric segment check and added keyboard hints
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Feb 15, 2025
1 parent 95adb10 commit 7a29be0
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 18 deletions.
13 changes: 13 additions & 0 deletions packages/core/src/useDateTimeField/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,16 @@ export function getSegmentTypePlaceholder(type: DateTimeSegmentType) {

return map[type];
}

export function isNumericByDefault(type: DateTimeSegmentType) {
const map: Partial<Record<DateTimeSegmentType, boolean>> = {
year: true,
month: true,
day: true,
hour: true,
minute: true,
second: true,
};

return map[type] ?? false;
}
50 changes: 39 additions & 11 deletions packages/core/src/useDateTimeField/useDateTimeSegment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ interface DateTimeSegmentDomProps {
contenteditable: string | undefined;
'aria-disabled': boolean | undefined;
'data-segment-type': DateTimeSegmentType;
style?: CSSProperties;
style: CSSProperties;
'aria-label'?: string;
spellcheck?: boolean;
inputmode?: string;
autocorrect?: string;
enterkeyhint?: string;
}

export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
Expand All @@ -44,14 +49,23 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
throw new Error('DateTimeSegmentGroup is not provided');
}

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

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

let currentInput = '';

Expand All @@ -67,7 +81,7 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
}

blockEvent(evt);
if (!isNumeric.value && !isPlaceholder()) {
if (!isNumeric()) {
return;
}

Expand Down Expand Up @@ -110,6 +124,12 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
currentInput = '';
},
onKeydown(evt: KeyboardEvent) {
if (hasKeyCode(evt, 'Enter')) {
blockEvent(evt);
focusNext();
return;
}

if (hasKeyCode(evt, 'ArrowUp')) {
blockEvent(evt);
if (!isNonEditable()) {
Expand Down Expand Up @@ -142,11 +162,19 @@ export function useDateTimeSegment(_props: Reactivify<DateTimeSegmentProps>) {
contenteditable: isNonEditable() ? undefined : 'plaintext-only',
'aria-disabled': toValue(props.disabled),
'data-segment-type': toValue(props.type),
'aria-label': isNonEditable() ? undefined : toValue(props.type),
autocorrect: isNonEditable() ? undefined : 'off',
spellcheck: isNonEditable() ? undefined : false,
enterkeyhint: isNonEditable() ? undefined : isLast() ? 'done' : 'next',
inputmode: isNonEditable() ? undefined : isNumeric() ? 'numeric' : 'none',
...handlers,
style: {
caretColor: 'transparent',
},
};

if (isNonEditable()) {
domProps.style = { pointerEvents: 'none' };
domProps.style.pointerEvents = 'none';
}

return withRefCapture(domProps, segmentEl);
Expand Down
31 changes: 26 additions & 5 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,
isNumericByDefault,
isOptionalSegmentType,
segmentTypeToDurationLike,
} from './constants';
Expand All @@ -29,8 +30,10 @@ export interface DateTimeSegmentGroupContext {
getMetadata(): { min: number | null; max: number | null; maxLength: number | null };
onDone(): void;
clear(): void;
isPlaceholder(): boolean;
isNumeric(): boolean;
onTouched(): void;
isLast(): boolean;
focusNext(): void;
};
}

Expand Down Expand Up @@ -192,11 +195,27 @@ export function useDateTimeSegmentGroup({
onValueChange(next);
}

function isPlaceholder() {
function isNumeric() {
const type = segment.getType();
const date = toValue(temporalValue);
const options = toValue(formatOptions);
const optionFormat = options?.[type];

Check failure on line 201 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 'DateTimeSegmentType' can't be used to index type 'DateTimeFormatOptions'.
if (!optionFormat) {
return isNumericByDefault(type);
}

return optionFormat === 'numeric' || optionFormat === '2-digit';
}

function isLast() {
return renderedSegments.value.at(-1)?.id === segment.id;
}

function focusNext() {
if (isLast()) {
return;
}

return isTemporalPartial(date) && !isTemporalPartSet(date, type);
focusNextSegment();
}

return {
Expand All @@ -208,8 +227,10 @@ export function useDateTimeSegmentGroup({
getMetadata,
onDone: onSegmentDone,
clear,
isPlaceholder,
onTouched,
isLast,
focusNext,
isNumeric,
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const maxDate = new Date('2025-01-31');
</script>

<template>
<div class="flex flex-col w-1/2 gap-4">
<div class="flex flex-col gap-4">
<DateField
name="date"
label="Date"
Expand Down

0 comments on commit 7a29be0

Please sign in to comment.