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(DateField): improve month segment behavior for invalid months starting with 1 #1560

Draft
wants to merge 8 commits into
base: v2
Choose a base branch
from
Draft
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"docs:build": "pnpm --filter docs docs:build",
"docs:gen": "pnpm --filter docs docs:gen",
"docs:contributors": "pnpm --filter docs docs:contributors",
"prepare": "pnpm simple-git-hooks",
"prepare": "pnpm simple-git-hooks && pnpm build-only",
"test": "pnpm --filter reka-ui test",
"test-update": "pnpm --filter reka-ui test-update",
"lint": "eslint .",
Expand Down
64 changes: 59 additions & 5 deletions packages/core/src/DateField/DateFieldInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
import { Primitive, type PrimitiveProps } from '@/Primitive'
import type { SegmentPart } from '@/shared/date'
import { useDateField } from '@/shared/date/useDateField'
import { injectDateFieldRootContext } from './DateFieldRoot.vue'
import { computed, ref } from 'vue'
import { injectDateFieldRootContext } from './DateFieldRoot.vue'

export interface DateFieldInputProps extends PrimitiveProps {
/** The part of the date to render */
Expand Down Expand Up @@ -35,11 +35,67 @@ const {
readonly: rootContext.readonly,
focusNext: rootContext.focusNext,
modelValue: rootContext.modelValue,
programmaticContinuation: rootContext.programmaticContinuation,
})

const disabled = computed(() => rootContext.disabled.value)
const readonly = computed(() => rootContext.readonly.value)
const isInvalid = computed(() => rootContext.isInvalid.value)

function handleFocusOut(e: FocusEvent) {
if (rootContext.programmaticContinuation.value) {
hasLeftFocus.value = false
}
else {
hasLeftFocus.value = true
}
}

function handleFocusIn(e: FocusEvent) {
rootContext.setFocusedElement(e.target as HTMLElement)
const dayValue = rootContext.segmentValues.value.day
const yearValue = rootContext.segmentValues.value.year

if (rootContext.programmaticContinuation.value) {
if (props.part === 'year' && yearValue) {
// create key event for keyword with rootContext.segmentValues.value.year
const event = new KeyboardEvent('keydown', {
key: yearValue.toString(),
code: `Digit${yearValue}`,
keyCode: 48 + yearValue,
which: 48 + yearValue,
bubbles: true,
cancelable: true,
})

console.log('triggering keydown for year', event)

hasLeftFocus.value = false
handleSegmentKeydown(event)
rootContext.programmaticContinuation.value = false
}
else if (props.part === 'day' && dayValue) {
// create key event for keyword with rootContext.segmentValues.value.day
const event = new KeyboardEvent('keydown', {
key: dayValue.toString(),
code: `Digit${dayValue}`,
keyCode: 48 + dayValue,
which: 48 + dayValue,
bubbles: true,
cancelable: true,
})

console.log('triggering keydown for day', event)

hasLeftFocus.value = false
handleSegmentKeydown(event)
rootContext.programmaticContinuation.value = false
}
}
else {
hasLeftFocus.value = true
}
}
</script>

<template>
Expand All @@ -57,10 +113,8 @@ const isInvalid = computed(() => rootContext.isInvalid.value)
v-on="part !== 'literal' ? {
mousedown: handleSegmentClick,
keydown: handleSegmentKeydown,
focusout: () => { hasLeftFocus = true },
focusin: (e: FocusEvent) => {
rootContext.setFocusedElement(e.target as HTMLElement)
},
focusout: handleFocusOut,
focusin: handleFocusIn,
} : {}"
>
<slot />
Expand Down
18 changes: 13 additions & 5 deletions packages/core/src/DateField/DateFieldRoot.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
<script lang="ts">
import { type DateValue, isEqualDay } from '@internationalized/date'

import type { Ref } from 'vue'
import { type Matcher, hasTime, isBefore } from '@/date'
import type { PrimitiveProps } from '@/Primitive'
import { type Formatter, createContext, isNullish, useDateFormatter, useDirection, useKbd, useLocale } from '@/shared'
import {
type Granularity,
type HourCycle,
type SegmentPart,
type SegmentValueObj,
createContent,
getDefaultDate,
getSegmentElements,
initializeSegmentValues,
isSegmentNavigationKey,
syncSegmentValues,
} from '@/shared/date'
import { type Matcher, hasTime, isBefore } from '@/date'
import { createContent, getSegmentElements, initializeSegmentValues, isSegmentNavigationKey, syncSegmentValues } from '@/shared/date'
import type { Direction, FormFieldProps } from '@/shared/types'
import type { Ref } from 'vue'

type DateFieldRootContext = {
locale: Ref<string>
Expand All @@ -30,6 +34,7 @@ type DateFieldRootContext = {
elements: Ref<Set<HTMLElement>>
focusNext: () => void
setFocusedElement: (el: HTMLElement) => void
programmaticContinuation: Ref<boolean>
}

export interface DateFieldRootProps extends PrimitiveProps, FormFieldProps {
Expand Down Expand Up @@ -77,10 +82,10 @@ export const [injectDateFieldRootContext, provideDateFieldRootContext]
</script>

<script setup lang="ts">
import { computed, nextTick, onMounted, ref, toRefs, watch } from 'vue'
import { Primitive, usePrimitiveElement } from '@/Primitive'
import { useVModel } from '@vueuse/core'
import { VisuallyHidden } from '@/VisuallyHidden'
import { useVModel } from '@vueuse/core'
import { computed, nextTick, onMounted, ref, toRefs, watch } from 'vue'

defineOptions({
inheritAttrs: false,
Expand Down Expand Up @@ -243,6 +248,8 @@ function setFocusedElement(el: HTMLElement) {
currentFocusedElement.value = el
}

const programmaticContinuation = ref(false)

provideDateFieldRootContext({
isDateUnavailable: propsIsDateUnavailable.value,
locale,
Expand All @@ -260,6 +267,7 @@ provideDateFieldRootContext({
focusNext() {
nextFocusableSegment.value?.focus()
},
programmaticContinuation,
})

defineExpose({
Expand Down
93 changes: 73 additions & 20 deletions packages/core/src/shared/date/useDateField.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type Formatter, useKbd } from '@/shared'
import type { AnyExceptLiteral, HourCycle, SegmentPart, SegmentValueObj } from './types'
import { getDaysInMonth, toDate } from '@/date'
import { type Formatter, useKbd } from '@/shared'
import type { CalendarDateTime, CycleTimeOptions, DateFields, DateValue, TimeFields } from '@internationalized/date'
import { type Ref, computed } from 'vue'
import { isAcceptableSegmentKey, isNumberString, isSegmentNavigationKey } from './segment'
import type { AnyExceptLiteral, HourCycle, SegmentPart, SegmentValueObj } from './types'

type MinuteSecondIncrementProps = {
e: KeyboardEvent
Expand Down Expand Up @@ -278,6 +278,7 @@ export type UseDateFieldProps = {
part: SegmentPart
modelValue: Ref<DateValue | undefined>
focusNext: () => void
programmaticContinuation: Ref<boolean>
}

export function useDateField(props: UseDateFieldProps) {
Expand Down Expand Up @@ -334,11 +335,15 @@ export function useDateField(props: UseDateFieldProps) {
* `prev` value so that we can start the segment over again
* when the user types a number.
*/
if (props.hasLeftFocus.value) {
if (props.hasLeftFocus.value && !props.programmaticContinuation.value) {
props.hasLeftFocus.value = false
prev = null
}

if (props.programmaticContinuation.value) {
props.programmaticContinuation.value = false
}

if (prev === null) {
/**
* If the user types a 0 as the first number, we want
Expand Down Expand Up @@ -384,19 +389,56 @@ export function useDateField(props: UseDateFieldProps) {
* month, then we will reset the segment as if the user had pressed the
* backspace key and then typed the number.
*/

if (digits === 2 || total > max) {
/**
* As we're doing elsewhere, we're checking if the number is greater
* than the max start digit (0-3 in most months), and if so, we're
* going to move to the next segment.
*/
/**
* If we're updating months (max === 12) and user types a number
* that starts with 1 but would result in an invalid month (13-19),
* we keep the 1 as the month value and use the second digit
* as the initial value for the next segment (day)
*/
if (max === 12 && prev === 1 && total > max) {
console.log('enter 1')
props.programmaticContinuation.value = true

return {
moveToNext: true,
value: prev,
nextSegmentInitialValue: num,
}
}
if (max === 28 || max === 29 || max === 30 || max === 31) {
console.log({
prev,
total,
})

if (prev === 3 && total > max) {
props.programmaticContinuation.value = true

return {
moveToNext: true,
value: prev,
nextSegmentInitialValue: num,
}
}
}

/**
* As we're doing elsewhere, we're checking if the number is greater
* than the max start digit (0-3 in most months), and if so, we're
* going to move to the next segment.
*/
if (num > maxStart || total > max) {
// move to next
console.log('enter 3')

// move to next
moveToNext = true
}
return { value: num, moveToNext }
}

console.log('enter 4')

// move to next
moveToNext = true
return { value: total, moveToNext }
Expand Down Expand Up @@ -570,11 +612,15 @@ export function useDateField(props: UseDateFieldProps) {
* when the user types a number.
*/
// probably not implement, kind of weird
if (props.hasLeftFocus.value) {
if (props.hasLeftFocus.value && !props.programmaticContinuation.value) {
props.hasLeftFocus.value = false
prev = null
}

if (props.programmaticContinuation.value) {
props.programmaticContinuation.value = false
}

if (prev === null)
return { value: num === 0 ? 1 : num, moveToNext }

Expand Down Expand Up @@ -609,21 +655,23 @@ export function useDateField(props: UseDateFieldProps) {
props.segmentValues.value.day = dateTimeValueIncrementation({ e, part: 'day', dateRef: props.placeholder.value, prevValue })
return
}

if (isNumberString(e.key)) {
const num = Number.parseInt(e.key)
const segmentMonthValue = props.segmentValues.value.month

const daysInMonth = segmentMonthValue
? getDaysInMonth(props.placeholder.value.set({ month: segmentMonthValue }))
: getDaysInMonth(props.placeholder.value)

const { value, moveToNext } = updateDayOrMonth(daysInMonth, num, prevValue)

const { value, moveToNext, nextSegmentInitialValue } = updateDayOrMonth(daysInMonth, num, props.programmaticContinuation.value ? null : prevValue)
props.segmentValues.value.day = value

if (moveToNext)
if (nextSegmentInitialValue) {
props.segmentValues.value.year = nextSegmentInitialValue
}

if (moveToNext) {
props.focusNext()
}
}

if (e.key === kbd.BACKSPACE) {
Expand All @@ -645,10 +693,14 @@ export function useDateField(props: UseDateFieldProps) {

if (isNumberString(e.key)) {
const num = Number.parseInt(e.key)
const { value, moveToNext } = updateDayOrMonth(12, num, prevValue)
const { value, moveToNext, nextSegmentInitialValue } = updateDayOrMonth(12, num, prevValue)

props.segmentValues.value.month = value

if (nextSegmentInitialValue) {
props.segmentValues.value.day = nextSegmentInitialValue
}

if (moveToNext)
props.focusNext()
}
Expand All @@ -672,9 +724,10 @@ export function useDateField(props: UseDateFieldProps) {

if (isNumberString(e.key)) {
const num = Number.parseInt(e.key)
const { value, moveToNext } = updateYear(num, prevValue)

props.segmentValues.value.year = value
const { value, moveToNext } = updateYear(num, props.programmaticContinuation.value ? null : prevValue)
if (!props.programmaticContinuation.value) {
props.segmentValues.value.year = value
}

if (moveToNext)
props.focusNext()
Expand Down