diff --git a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx index c68789da3d..fc68e525f1 100644 --- a/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx +++ b/packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx @@ -8,7 +8,10 @@ import React, { import isNull from 'lodash/isNull'; import { isInvalidDateObject, isSameUTCDay } from '@leafygreen-ui/date-utils'; -import { isElementInputSegment } from '@leafygreen-ui/input-box'; +import { + focusAndSelectSegment, + isElementInputSegment, +} from '@leafygreen-ui/input-box'; import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib'; import { @@ -20,7 +23,6 @@ import { DateInputSegmentChangeEventHandler } from '../../shared/components/Date import { useSharedDatePickerContext } from '../../shared/context'; import { getFormattedDateStringFromSegments } from '../../shared/utils'; import { useDatePickerContext } from '../DatePickerContext'; -import { getSegmentToFocus } from '../utils/getSegmentToFocus'; import { DatePickerInputProps } from './DatePickerInput.types'; @@ -77,14 +79,17 @@ export const DatePickerInput = forwardRef( if (!disabled) { openMenu(e); const { target } = e; - const segmentToFocus = getSegmentToFocus({ + + /** + * Focus and select the appropriate segment. + * + * This is done here instead of `InputBox` because this component has padding that needs to be accounted for on click. + */ + focusAndSelectSegment({ target, formatParts, segmentRefs, }); - - segmentToFocus?.focus(); - segmentToFocus?.select(); } }; diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts b/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts deleted file mode 100644 index 3327e4031f..0000000000 --- a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { createRef } from 'react'; - -import { SupportedLocales } from '@leafygreen-ui/date-utils'; - -import { SegmentRefs } from '../../../shared/hooks'; -import { getFormatParts } from '../../../shared/utils'; - -import { getSegmentToFocus } from '.'; - -describe('packages/date-picker/utils/getSegmentToFocus', () => { - const formatParts = getFormatParts(SupportedLocales.ISO_8601); - - test('if target is a segment, return target', () => { - const target = document.createElement('input'); - - const segmentRefs: SegmentRefs = { - year: createRef(), - month: createRef(), - day: { current: target }, - }; - - const segment = getSegmentToFocus({ - target, - formatParts, - segmentRefs, - }); - - expect(segment).toBe(target); - }); - - test('if all inputs are filled, return the last input', () => { - const target = document.createElement('div'); - - const yearEl = document.createElement('input'); - yearEl.value = '1993'; - yearEl.id = 'year'; - const monthEl = document.createElement('input'); - monthEl.value = '12'; - monthEl.id = 'month'; - const dayEl = document.createElement('input'); - dayEl.value = '26'; - dayEl.id = 'day'; - - const segmentRefs: SegmentRefs = { - year: { current: yearEl }, - month: { current: monthEl }, - day: { current: dayEl }, - }; - - const segment = getSegmentToFocus({ - target, - formatParts, - segmentRefs, - }); - - expect(segment).toBe(dayEl); - }); - - test('if first input is filled, return second input', () => { - const target = document.createElement('div'); - - const yearEl = document.createElement('input'); - yearEl.value = '1993'; - yearEl.id = 'year'; - const monthEl = document.createElement('input'); - monthEl.id = 'month'; - const dayEl = document.createElement('input'); - dayEl.id = 'day'; - - const segmentRefs: SegmentRefs = { - year: { current: yearEl }, - month: { current: monthEl }, - day: { current: dayEl }, - }; - - const segment = getSegmentToFocus({ - target, - formatParts, - segmentRefs, - }); - - expect(segment).toBe(monthEl); - }); - - test('returns undefined for undefined input', () => { - const segment = getSegmentToFocus({ - // @ts-expect-error - target: undefined, - formatParts: undefined, - // @ts-expect-error - segmentRefs: undefined, - }); - - expect(segment).toBeUndefined(); - }); -}); diff --git a/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts b/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts deleted file mode 100644 index 93830e4748..0000000000 --- a/packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SharedDatePickerContextProps } from '../../context'; -import { SegmentRefs } from '../../hooks'; -import { DateSegment } from '../../types'; - -/** - * - * @returns The first empty date segment for the given format - */ -export const getFirstEmptySegment = ({ - formatParts, - segmentRefs, -}: { - formatParts: Required['formatParts']; - segmentRefs: SegmentRefs; -}) => { - // if 1+ are empty, focus the first empty one - const formatSegments = formatParts.filter(part => part.type !== 'literal'); - const emptySegmentKeys = formatSegments - .map(p => p.type) - .filter(type => { - const element = segmentRefs[type as DateSegment]; - return !element?.current?.value; - }); - const firstEmptySegmentKey = emptySegmentKeys[0] as DateSegment; - const firstEmptySegmentRef = segmentRefs[firstEmptySegmentKey]; - return firstEmptySegmentRef.current; -}; diff --git a/packages/date-picker/src/shared/utils/index.ts b/packages/date-picker/src/shared/utils/index.ts index cc082f9041..ffd76e430b 100644 --- a/packages/date-picker/src/shared/utils/index.ts +++ b/packages/date-picker/src/shared/utils/index.ts @@ -1,7 +1,6 @@ export { doesSomeSegmentExist } from './doesSomeSegmentExist'; export { doSegmentsFormValidDate } from './doSegmentsFormValidDate'; export { getAutoComplete } from './getAutoComplete'; -export { getFirstEmptySegment } from './getFirstEmptySegment'; export { getFormatParts, getFormatter } from './getFormatParts'; export { getFormattedDateString, diff --git a/packages/input-box/src/InputSegment/InputSegment.stories.tsx b/packages/input-box/src/InputSegment/InputSegment.stories.tsx index 0166d5b7f7..ca615d5e8b 100644 --- a/packages/input-box/src/InputSegment/InputSegment.stories.tsx +++ b/packages/input-box/src/InputSegment/InputSegment.stories.tsx @@ -71,6 +71,8 @@ const meta: StoryMetaType = { segment: ['day', 'year'], size: Object.values(Size), value: ['', '2', '02', '0', '00', '2025', '0000'], + // @ts-expect-error - data-focus is not a valid prop for InputSegment + 'data-focus': [false, true], }, excludeCombinations: [ { @@ -81,6 +83,13 @@ const meta: StoryMetaType = { value: ['2025', '0000'], segment: ['day'], }, + [ + // @ts-expect-error - data-focus is not a valid prop for InputSegment + 'data-focus', + { + value: ['02', '0', '00', '2025', '0000'], + }, + ], ], decorator: (StoryFn, context) => ( diff --git a/packages/input-box/src/InputSegment/InputSegment.styles.ts b/packages/input-box/src/InputSegment/InputSegment.styles.ts index 39f4f3da64..9ddd44b5e3 100644 --- a/packages/input-box/src/InputSegment/InputSegment.styles.ts +++ b/packages/input-box/src/InputSegment/InputSegment.styles.ts @@ -28,7 +28,8 @@ export const baseStyles = css` -moz-appearance: textfield; /* Firefox */ appearance: textfield; - &:focus { + &:focus, + &[data-focus='true'] { outline: none; } `; @@ -44,7 +45,8 @@ export const getSegmentThemeStyles = (theme: Theme) => { ]}; } - &:focus { + &:focus, + &[data-focus='true'] { background-color: ${color[theme].background[Variant.Primary][ InteractionState.Focus ]}; diff --git a/packages/input-box/src/index.ts b/packages/input-box/src/index.ts index 35515140c6..e3f6014d7f 100644 --- a/packages/input-box/src/index.ts +++ b/packages/input-box/src/index.ts @@ -9,11 +9,11 @@ export { export { createExplicitSegmentValidator, type ExplicitSegmentRule, + focusAndSelectSegment, + getSegmentToFocus, + getValueFormatter, isElementInputSegment, - isValidValueForSegment, -} from './utils'; -export { getValueFormatter } from './utils/getValueFormatter/getValueFormatter'; -export { isValidSegmentName, isValidSegmentValue, -} from './utils/isValidSegment/isValidSegment'; + isValidValueForSegment, +} from './utils'; diff --git a/packages/input-box/src/utils/focusAndSelectSegment/focusAndSelectSegment.spec.tsx b/packages/input-box/src/utils/focusAndSelectSegment/focusAndSelectSegment.spec.tsx new file mode 100644 index 0000000000..017725a918 --- /dev/null +++ b/packages/input-box/src/utils/focusAndSelectSegment/focusAndSelectSegment.spec.tsx @@ -0,0 +1,161 @@ +import { createRef } from 'react'; + +import { SegmentRefsType } from '../getSegmentToFocus/getSegmentToFocus'; + +import { focusAndSelectSegment } from './focusAndSelectSegment'; + +describe('packages/input-box/utils/focusAndSelectSegment', () => { + const formatParts: Array = [ + { type: 'year', value: '' }, + { type: 'literal', value: '-' }, + { type: 'month', value: '' }, + { type: 'literal', value: '-' }, + { type: 'day', value: '' }, + ]; + + test('focuses and selects the target segment when target is a segment', () => { + const target = document.createElement('input'); + const focusSpy = jest.spyOn(target, 'focus'); + const selectSpy = jest.spyOn(target, 'select'); + + const segmentRefs: SegmentRefsType = { + year: createRef(), + month: createRef(), + day: { current: target }, + }; + + focusAndSelectSegment({ + target, + formatParts, + segmentRefs, + }); + + expect(focusSpy).toHaveBeenCalledTimes(1); + expect(selectSpy).toHaveBeenCalledTimes(1); + }); + + test('focuses and selects the first empty segment when target is not a segment', () => { + const yearSegment = document.createElement('input'); + const monthSegment = document.createElement('input'); + monthSegment.value = '12'; + const daySegment = document.createElement('input'); + daySegment.value = '26'; + + const yearFocusSpy = jest.spyOn(yearSegment, 'focus'); + const yearSelectSpy = jest.spyOn(yearSegment, 'select'); + + const target = document.createElement('div'); + + const segmentRefs: SegmentRefsType = { + year: { current: yearSegment }, + month: { current: monthSegment }, + day: { current: daySegment }, + }; + + focusAndSelectSegment({ + target, + formatParts, + segmentRefs, + }); + + expect(yearFocusSpy).toHaveBeenCalledTimes(1); + expect(yearSelectSpy).toHaveBeenCalledTimes(1); + }); + + test('focuses and selects the last filled segment when target is not a segment', () => { + const yearSegment = document.createElement('input'); + yearSegment.value = '1993'; + const monthSegment = document.createElement('input'); + monthSegment.value = '12'; + const daySegment = document.createElement('input'); + daySegment.value = '26'; + + const dayFocusSpy = jest.spyOn(daySegment, 'focus'); + const daySelectSpy = jest.spyOn(daySegment, 'select'); + + const target = document.createElement('div'); + + const segmentRefs: SegmentRefsType = { + year: { current: yearSegment }, + month: { current: monthSegment }, + day: { current: daySegment }, + }; + + focusAndSelectSegment({ + target, + formatParts, + segmentRefs, + }); + + expect(dayFocusSpy).toHaveBeenCalledTimes(1); + expect(daySelectSpy).toHaveBeenCalledTimes(1); + }); + + test('Does not focus or select any segment when target is undefined', () => { + const yearSegment = document.createElement('input'); + const monthSegment = document.createElement('input'); + const daySegment = document.createElement('input'); + + const yearFocusSpy = jest.spyOn(yearSegment, 'focus'); + const yearSelectSpy = jest.spyOn(yearSegment, 'select'); + + const monthFocusSpy = jest.spyOn(monthSegment, 'focus'); + const monthSelectSpy = jest.spyOn(monthSegment, 'select'); + const dayFocusSpy = jest.spyOn(daySegment, 'focus'); + const daySelectSpy = jest.spyOn(daySegment, 'select'); + + const segmentRefs: SegmentRefsType = { + year: { current: yearSegment }, + month: { current: monthSegment }, + day: { current: daySegment }, + }; + + focusAndSelectSegment({ + target: undefined, + formatParts, + segmentRefs, + }); + + expect(yearFocusSpy).not.toHaveBeenCalled(); + expect(yearSelectSpy).not.toHaveBeenCalled(); + expect(monthFocusSpy).not.toHaveBeenCalled(); + expect(monthSelectSpy).not.toHaveBeenCalled(); + expect(dayFocusSpy).not.toHaveBeenCalled(); + expect(daySelectSpy).not.toHaveBeenCalled(); + }); + + test('Does not focus or select any segment when formatParts is undefined', () => { + const yearSegment = document.createElement('input'); + const monthSegment = document.createElement('input'); + const daySegment = document.createElement('input'); + + const yearFocusSpy = jest.spyOn(yearSegment, 'focus'); + const yearSelectSpy = jest.spyOn(yearSegment, 'select'); + + const monthFocusSpy = jest.spyOn(monthSegment, 'focus'); + const monthSelectSpy = jest.spyOn(monthSegment, 'select'); + const dayFocusSpy = jest.spyOn(daySegment, 'focus'); + const daySelectSpy = jest.spyOn(daySegment, 'select'); + + const target = document.createElement('div'); + + const segmentRefs: SegmentRefsType = { + year: { current: yearSegment }, + month: { current: monthSegment }, + day: { current: daySegment }, + }; + + focusAndSelectSegment({ + target, + formatParts: undefined, + segmentRefs, + }); + + expect(yearFocusSpy).not.toHaveBeenCalled(); + expect(yearSelectSpy).not.toHaveBeenCalled(); + expect(monthFocusSpy).not.toHaveBeenCalled(); + expect(monthSelectSpy).not.toHaveBeenCalled(); + expect(dayFocusSpy).not.toHaveBeenCalled(); + expect(daySelectSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/input-box/src/utils/focusAndSelectSegment/focusAndSelectSegment.ts b/packages/input-box/src/utils/focusAndSelectSegment/focusAndSelectSegment.ts new file mode 100644 index 0000000000..52b8ec7978 --- /dev/null +++ b/packages/input-box/src/utils/focusAndSelectSegment/focusAndSelectSegment.ts @@ -0,0 +1,43 @@ +import { + getSegmentToFocus, + GetSegmentToFocusProps, + SegmentRefsType, +} from '../getSegmentToFocus/getSegmentToFocus'; + +interface FocusAndSelectSegmentProps + extends GetSegmentToFocusProps {} + +/** + * Helper function that focuses and selects the appropriate input segment + * given an event target and segment refs. + * + * This is a convenience wrapper around `getSegmentToFocus` that also + * calls `.focus()` and `.select()` on the returned element. + * + * @param target - The target element from the event + * @param formatParts - The format parts of the date/time + * @param segmentRefs - The segment refs + * + * @example + * const handleInputClick = (e: MouseEvent) => { + * focusAndSelectSegment({ + * target: e.target, + * formatParts, + * segmentRefs, + * }); + * }; + */ +export const focusAndSelectSegment = ({ + target, + formatParts, + segmentRefs, +}: FocusAndSelectSegmentProps): void => { + const segmentToFocus = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + segmentToFocus?.focus(); + segmentToFocus?.select(); +}; diff --git a/packages/input-box/src/utils/getFirstEmptySegment/getFirstEmptySegment.spec.ts b/packages/input-box/src/utils/getFirstEmptySegment/getFirstEmptySegment.spec.ts new file mode 100644 index 0000000000..cae3572304 --- /dev/null +++ b/packages/input-box/src/utils/getFirstEmptySegment/getFirstEmptySegment.spec.ts @@ -0,0 +1,115 @@ +import { getFirstEmptySegment } from './getFirstEmptySegment'; + +describe('packages/input-box/utils/getFirstEmptySegment', () => { + const formatParts: Array = [ + { type: 'year', value: '' }, + { type: 'literal', value: '-' }, + { type: 'month', value: '' }, + { type: 'literal', value: '-' }, + { type: 'day', value: '' }, + ]; + + test('returns first segment when all segments are empty', () => { + const yearEl = document.createElement('input'); + const monthEl = document.createElement('input'); + const dayEl = document.createElement('input'); + + const segmentRefs = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getFirstEmptySegment({ + formatParts, + segmentRefs, + }); + + expect(segment).toBe(yearEl); + }); + + test('returns first segment when the other segments are filled', () => { + const yearEl = document.createElement('input'); + const monthEl = document.createElement('input'); + monthEl.value = '12'; + const dayEl = document.createElement('input'); + dayEl.value = '26'; + + const segmentRefs = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getFirstEmptySegment({ + formatParts, + segmentRefs, + }); + + expect(segment).toBe(yearEl); + }); + + test('returns second segment when first segment is filled', () => { + const yearEl = document.createElement('input'); + yearEl.value = '1993'; + const monthEl = document.createElement('input'); + const dayEl = document.createElement('input'); + dayEl.value = '26'; + + const segmentRefs = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getFirstEmptySegment({ + formatParts, + segmentRefs, + }); + + expect(segment).toBe(monthEl); + }); + + test('returns third segment when first two segments are filled', () => { + const yearEl = document.createElement('input'); + yearEl.value = '1993'; + const monthEl = document.createElement('input'); + monthEl.value = '12'; + const dayEl = document.createElement('input'); + + const segmentRefs = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getFirstEmptySegment({ + formatParts, + segmentRefs, + }); + + expect(segment).toBe(dayEl); + }); + + test('returns null when all segments are filled', () => { + const yearEl = document.createElement('input'); + yearEl.value = '1993'; + const monthEl = document.createElement('input'); + monthEl.value = '12'; + const dayEl = document.createElement('input'); + dayEl.value = '26'; + + const segmentRefs = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getFirstEmptySegment({ + formatParts, + segmentRefs, + }); + + expect(segment).toBeNull(); + }); +}); diff --git a/packages/input-box/src/utils/getFirstEmptySegment/getFirstEmptySegment.ts b/packages/input-box/src/utils/getFirstEmptySegment/getFirstEmptySegment.ts new file mode 100644 index 0000000000..33d312db47 --- /dev/null +++ b/packages/input-box/src/utils/getFirstEmptySegment/getFirstEmptySegment.ts @@ -0,0 +1,43 @@ +interface GetFirstEmptySegmentProps< + SegmentRefs extends Record>, +> { + formatParts: Array; + segmentRefs: SegmentRefs; +} +/** + * @param formatParts - The format parts of the date + * @param segmentRefs - The segment refs + * + * @returns The first empty date segment input for the given format + * + * @example + * const formatParts = [ + * { type: 'year', value: '' }, + * { type: 'month', value: '' }, + * { type: 'day', value: '' }, + * ]; + * const segmentRefs = { + * year: { current: yearRef }, + * month: { current: monthRef }, + * day: { current: dayRef }, + * }; + * getFirstEmptySegment({ formatParts, segmentRefs }); // yearRef.current + */ +export const getFirstEmptySegment = < + SegmentRefs extends Record>, +>({ + formatParts, + segmentRefs, +}: GetFirstEmptySegmentProps): HTMLInputElement | null => { + // if 1+ are empty, focus the first empty one + const formatSegments = formatParts.filter(part => part.type !== 'literal'); + const emptySegmentKeys = formatSegments + .map(p => p.type) + .filter(type => { + const element = segmentRefs[type as keyof SegmentRefs]; + return !element?.current?.value; + }); + const firstEmptySegmentKey = emptySegmentKeys[0] as keyof SegmentRefs; + const firstEmptySegmentRef = segmentRefs[firstEmptySegmentKey]; + return firstEmptySegmentRef?.current ?? null; +}; diff --git a/packages/input-box/src/utils/getSegmentToFocus/getSegmentToFocus.spec.tsx b/packages/input-box/src/utils/getSegmentToFocus/getSegmentToFocus.spec.tsx new file mode 100644 index 0000000000..91e0fc08c3 --- /dev/null +++ b/packages/input-box/src/utils/getSegmentToFocus/getSegmentToFocus.spec.tsx @@ -0,0 +1,160 @@ +import { createRef } from 'react'; + +import { getSegmentToFocus, SegmentRefsType } from './getSegmentToFocus'; + +describe('packages/date-picker/utils/getSegmentToFocus', () => { + const formatParts: Array = [ + { type: 'year', value: '' }, + { type: 'literal', value: '-' }, + { type: 'month', value: '' }, + { type: 'literal', value: '-' }, + { type: 'day', value: '' }, + ]; + + test('if target is a segment, return target', () => { + const target = document.createElement('input'); + + const segmentRefs: SegmentRefsType = { + year: createRef(), + month: createRef(), + day: { current: target }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(target); + }); + + test('if all inputs are filled, return the last input', () => { + const target = document.createElement('div'); + + const yearEl = document.createElement('input'); + yearEl.value = '1993'; + const monthEl = document.createElement('input'); + monthEl.value = '12'; + const dayEl = document.createElement('input'); + dayEl.value = '26'; + + const segmentRefs: SegmentRefsType = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(dayEl); + }); + + test('if all inputs are empty, return the first input', () => { + const target = document.createElement('div'); + + const yearEl = document.createElement('input'); + const monthEl = document.createElement('input'); + const dayEl = document.createElement('input'); + + const segmentRefs: SegmentRefsType = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(yearEl); + }); + + test('if first input is filled, and the second is empty, return second input', () => { + const target = document.createElement('div'); + + const yearEl = document.createElement('input'); + yearEl.value = '1993'; + const monthEl = document.createElement('input'); + const dayEl = document.createElement('input'); + dayEl.value = '26'; + + const segmentRefs: SegmentRefsType = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(monthEl); + }); + + test('if first and second inputs are filled, and the third is empty, return third input', () => { + const target = document.createElement('div'); + + const yearEl = document.createElement('input'); + yearEl.value = '1993'; + const monthEl = document.createElement('input'); + monthEl.value = '12'; + const dayEl = document.createElement('input'); + + const segmentRefs: SegmentRefsType = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(dayEl); + }); + + test('if the first input is empty, and the second and third are filled, return first input', () => { + const target = document.createElement('div'); + + const yearEl = document.createElement('input'); + const monthEl = document.createElement('input'); + monthEl.value = '12'; + const dayEl = document.createElement('input'); + dayEl.value = '26'; + + const segmentRefs: SegmentRefsType = { + year: { current: yearEl }, + month: { current: monthEl }, + day: { current: dayEl }, + }; + + const segment = getSegmentToFocus({ + target, + formatParts, + segmentRefs, + }); + + expect(segment).toBe(yearEl); + }); + + test('returns undefined for undefined input', () => { + const segment = getSegmentToFocus({ + target: undefined, + formatParts: undefined, + segmentRefs: undefined, + }); + + expect(segment).toBeUndefined(); + }); +}); diff --git a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts b/packages/input-box/src/utils/getSegmentToFocus/getSegmentToFocus.ts similarity index 56% rename from packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts rename to packages/input-box/src/utils/getSegmentToFocus/getSegmentToFocus.ts index aafbed88f6..b3489954d1 100644 --- a/packages/date-picker/src/DatePicker/utils/getSegmentToFocus/index.ts +++ b/packages/input-box/src/utils/getSegmentToFocus/getSegmentToFocus.ts @@ -1,15 +1,14 @@ import isUndefined from 'lodash/isUndefined'; import last from 'lodash/last'; -import { SharedDatePickerContextProps } from '../../../shared/context'; -import { SegmentRefs } from '../../../shared/hooks'; -import { DateSegment } from '../../../shared/types'; -import { getFirstEmptySegment } from '../../../shared/utils'; +import { getFirstEmptySegment } from '../getFirstEmptySegment/getFirstEmptySegment'; -interface GetSegmentToFocusProps { - target: EventTarget; - formatParts: SharedDatePickerContextProps['formatParts']; - segmentRefs: SegmentRefs; +export type SegmentRefsType = Record>; + +export interface GetSegmentToFocusProps { + target?: EventTarget; + formatParts?: Array; + segmentRefs?: SegmentRefs; } /** @@ -19,12 +18,33 @@ interface GetSegmentToFocusProps { * 1) if the target was a segment, focus that segment * 2) otherwise, if all segments are filled, focus the last one * 3) but, if some segments are empty, focus the first empty one + * + * @param target - The target element + * @param formatParts - The format parts of the date + * @param segmentRefs - The segment refs + * @returns The segment to focus + * @example + * const target = document.querySelector('input[data-segment="day"]'); + * const formatParts = [ + * { type: 'year', value: '' }, + * { type: 'month', value: '' }, + * { type: 'day', value: '' }, + * ]; + * const segmentRefs = { + * year: { current: yearRef }, + * month: { current: monthRef }, + * day: { current: dayRef }, + * }; + * getSegmentToFocus({ target, formatParts, segmentRefs }); // yearRef.current */ -export const getSegmentToFocus = ({ +export const getSegmentToFocus = ({ target, formatParts, segmentRefs, -}: GetSegmentToFocusProps): HTMLInputElement | undefined | null => { +}: GetSegmentToFocusProps): + | HTMLInputElement + | undefined + | null => { if ( isUndefined(target) || isUndefined(formatParts) || @@ -49,7 +69,7 @@ export const getSegmentToFocus = ({ if (allSegmentsFilled) { // if all are filled, focus the last one, const lastSegmentPart = last(formatSegments) as Intl.DateTimeFormatPart; - const keyOfLastSegment = lastSegmentPart.type as DateSegment; + const keyOfLastSegment = lastSegmentPart.type as keyof SegmentRefs; // this should be one of the keys of the segmentRefs const lastSegmentRef = segmentRefs[keyOfLastSegment]; return lastSegmentRef.current; } else { diff --git a/packages/input-box/src/utils/index.ts b/packages/input-box/src/utils/index.ts index 7aab16f5d5..7271078e93 100644 --- a/packages/input-box/src/utils/index.ts +++ b/packages/input-box/src/utils/index.ts @@ -2,12 +2,15 @@ export { createExplicitSegmentValidator, ExplicitSegmentRule, } from './createExplicitSegmentValidator/createExplicitSegmentValidator'; +export { focusAndSelectSegment } from './focusAndSelectSegment/focusAndSelectSegment'; +export { getFirstEmptySegment } from './getFirstEmptySegment/getFirstEmptySegment'; export { getNewSegmentValueFromArrowKeyPress } from './getNewSegmentValueFromArrowKeyPress/getNewSegmentValueFromArrowKeyPress'; export { getNewSegmentValueFromInputValue } from './getNewSegmentValueFromInputValue/getNewSegmentValueFromInputValue'; export { getRelativeSegment, getRelativeSegmentRef, } from './getRelativeSegment/getRelativeSegment'; +export { getSegmentToFocus } from './getSegmentToFocus/getSegmentToFocus'; export { getValueFormatter } from './getValueFormatter/getValueFormatter'; export { isElementInputSegment } from './isElementInputSegment/isElementInputSegment'; export { isSingleDigitKey } from './isSingleDigitKey/isSingleDigitKey';