Skip to content

Commit c9fe395

Browse files
authored
[LG-5504] feat(input-box): Add segment focus behavior (#3322)
* refactor(date-picker): extract reusable logic from DateInputSegment, wip * refactor(date-picker): extract reusable logic from DateInputSegment, wip * refactor(date-picker): enhance InputSegment and DateInputSegment with improved type handling and event management * refactor(date-picker): update InputSegment types and enhance DateInputSegment with additional props * refactor(date-picker): WIP enhance InputBox and DateInput components with improved type handling and segment management * refactor(date-picker): integrate InputBox into DateInputBox for improved segment management and type handling * refactor(date-picker): clean up DateInputBox and enhance InputBox types for better segment management * refactor(date-picker): enhance DatePicker components with improved type handling and segment management * refactor(date-picker): enhance InputSegment and DateInputSegment with new props for step handling and rollover management * refactor(input-box): move utils into input-box * refactor(input-box): move utils into input-box * refactor(date-picker): integrate InputBox and InputSegment into DatePicker components for improved segment management and type handling * refactor(date-picker): migrate utility functions to InputBox and enhance segment validation logic for improved date handling * refactor(date-picker): improve type safety in DateInputSegment and clean up InputBox component * refactor(input-box): update exports in utils to include new segment validation and formatting functions * refactor(input-box): add devDependencies for palette and enhance InputSegment and InputBox tests for better coverage * refactor(input-box): simplify styling logic in InputBox and InputSegment components by introducing utility functions * refactor(input-box): enhance type definitions in InputBox and InputSegment components for improved clarity and documentation * refactor(input-box): improve documentation for InputBox and InputSegment types with clearer examples * refactor(input-box): update documentation for segmentRefs and segmentRules in InputBox types for better clarity * refactor(input-box, date-picker): streamline value formatting by updating getValueFormatter to accept segment-specific character counts * refactor(input-box, date-picker): update utils to allow zero values * refactor(input-box, date-picker): introduce shouldSkipValidation flag for year segment and enhance validation logic * refactor(input-box): enhance renderSegment return type for improved type safety and clarity * refactor(input-box): improve InputBox tests to verify segment rendering and props validation * refactor(input-box): remove unused getLgIds utility and clean up InputSegment props * refactor(input-box, date-picker): rename segmentObj to segmentEnum for consistency and clarity across components * refactor(input-box, date-picker): update type annotations and enhance tests for InputBox and InputSegment components * refactor(input-box): update README and improve InputBox documentation for clarity * feat(input-box): adds input-box package and utils * refactor(input-box): consolidate InputBox stories into a single file and enhance control parameters * refactor(date-picker): remove input-box dependency and streamline date segment handling * feat(date-picker): integrate input-box for date segment handling and enhance validation logic * refactor(date-picker, input-box): implement context for segment management and streamline props handling in DateInput components * refactor(input-box): update InputBox and InputSegment components to use context for segment rendering and streamline prop handling * refactor(input-box): clarify type handling in InputBoxContext with detailed comments on type assertions * refactor(input-box): enhance type handling in InputBox and InputSegment components for improved clarity and consistency * refactor(input-box): update InputSegment and InputBox components to utilize context for segment values and enhance prop handling * refactor(date-picker): implement DateInputBoxContext for improved state management and enhance DateInputSegment integration * refactor(date-picker, input-box): enhance DateInput components by integrating context for segment management and improving prop handling * refactor(date-picker, input-box): improve DateInputSegment tests and stories by enhancing prop handling and integrating context for segment management * refactor(date-picker): reorganize DateInputBox context imports and remove unused DateInputBoxContext file * docs(input-box): expand README with detailed component descriptions, features, and usage examples for InputBox, InputBoxContext, and InputSegment * docs(input-box): update README to reflect changes in component structure and props for InputBox and InputSegment * refactor(input-box): rename `shouldRollover` to `shouldWrap` for clarity and update related documentation and tests * refactor(date-picker, input-box): reorganize imports and enhance prop handling in DateInputBox and InputBox components for improved clarity and functionality * test(input-box): add comprehensive tests for segment navigation and rendering behavior in InputBox component * fix(input-box, date-picker): address validation and formatting issues in InputBox and DateInputBox components; enhance tests for edge cases and input behavior * refactor(input-box, date-picker): remove defaultMin prop and enhance validation logic for segment inputs; update tests for improved coverage and clarity * refactor(input-box): standardize parameter naming from `allowsZero` to `allowZero` across components and utility functions for consistency * refactor(input-box): enhance createExplicitSegmentValidator documentation by adding parameter descriptions and examples for improved clarity * test(input-box): enhance mouse and keyboard interaction tests for segment focus behavior in InputBox component * test(input-box): add tests for Up and Down arrow key interactions to maintain focus in InputBox segments * refactor(input-box, date-picker): streamline InputBoxContext structure and enhance type definitions; update InputBox and InputSegment components for improved clarity and functionality * docs(input-box): update README.md to enhance installation instructions, usage examples, and component prop descriptions for better clarity and usability * feat(input-box): enhance InputBox and InputSegment components with new features and documentation. * feat(input-box): add '@leafygreen-ui/a11y' as a dependency in pnpm-lock.yaml * fix(input-box): fix lint errors * feat(input-box): set default size for InputBox in stories and refactor InputSegment styles for improved class handling * refactor(date-picker): simplify ProviderWrapper in DateInputSegment stories for better segment handling * feat(input-box): implement InputBoxContext and InputBoxProvider with associated types and tests * remove segement files * feat(input-box): implement InputSegment component with styles, tests, and stories * feat(input-box): add @leafygreen-ui/a11y dependency and update InputSegment component references * refactor(input-box): update createExplicitSegmentValidator tests to use object parameter format for improved clarity and consistency * test(input-box): refactor InputBoxContext tests for improved readability by destructuring context values * refactor(input-box): update InputBoxContext types to extend SharedInputBoxTypes and remove deprecated InputSegment types * fix(input-box): correct comment formatting in testutils.mocks.ts for clarity * feat(input-box): add InputSegment component for modular input handling * feat(input-box): add placeholder for InputSegment types * refactor(input-box): move InputSegmentChangeEventHandler import to shared types for better organization * refactor(input-box): rename min and max props to minSegmentValue and maxSegmentValue for consistency * refactor(input-box): simplify placeholder logic in InputSegment stories using defaultPlaceholderMock * refactor(input-box): update InputSegment styles to use dynamic theme styles and improve organization * feat(input-box): extend InputSegmentProps to include hours, minutes, and seconds segments * refactor(input-box): rename onChange and onBlur props in InputSegment to improve clarity * refactor(input-box): rename shouldSkipValidation prop to shouldValidate for clarity and consistency * refactor(input-box): reorganize imports in testutils for better clarity and structure * refactor(input-box): remove deprecated InputSegment types and update imports in InputBoxContext * refactor(input-box): update InputSegmentChangeEventHandler import to use type alias from shared.types * refactor(input-box): enhance InputSegment types and documentation, adding isInputSegment utility and improving component descriptions * refactor(input-box): streamline InputSegment exports by removing unused types * test(input-box): add accessibility test for InputSegment to ensure no violations when tooltip is closed * refactor(input-box): update InputSegment to remove size prop and enhance type definitions for better clarity * refactor(input-box): enhance InputSegment types by adding onChange and onBlur event handlers with detailed documentation * refactor(input-box): update InputSegment types to extend from 'div' and include additional props for improved functionality * refactor(input-box): simplify SharedInputBoxTypes by removing redundant properties and enhancing type clarity * refactor(input-box): remove InputBoxContext and related tests to streamline input box functionality * wip * refactor(input-box): simplify InputSegment types by removing Value generic and updating related components for improved clarity * refactor(input-box): update InputSegment and InputBox types to include value prop and streamline segment handling * refactor(date-picker): streamline DateInput components by simplifying props and enhancing context usage * refactor(input-box): update InputSegment types to include value prop and remove unused segmentRefs and segments properties for improved clarity * refactor(input-box): remove unused Size import from InputBox.spec.tsx for cleaner code * refactor(input-box): enhance InputBox and InputSegment documentation, update props for clarity, and streamline type exports * testing * refactor(input-box): remove unused dependencies and update InputSegment types for consistency * update lock file * testing * testing build * testing build * test(input-segment): add test for resetting value with complete zeros and update InputSegment story with segmentEnum * refactor(input-box): update separator literal styles to use new token-based approach * fix(input-segment): add missing line to check for number input handling * refactor(input-segment): update comments and variable name for clarity in digit input handling * refactor(input-box): update comment to reflect correct component responsible for increment/decrement logic * refactor(input-segment): utilize isSingleDigit utility for digit input handling * refactor(input-box): enhance documentation for InputBox component to clarify functionality and usage * refactor(input-box): integrate size prop into InputBox and InputSegment components for enhanced customization * refactor(input-box): migrate Size import to shared.types for consistent usage across components * refactor(input-box): enhance InputBox and InputSegment tests with segmentRefs integration for improved focus handling * feat(input-box): add comprehensive mocks for date and time segments in testutils for enhanced testing capabilities * feat(input-box): integrate lodash for utility functions and enhance InputBox stories with date and time segment examples * refactor(input-box): remove unused props from InputBox stories and testutils for cleaner code * feat(input-box): enhance InputBox functionality with click handling and focus management for segments * fix(input-box): ensure segments prop is required and handle error logging in InputBox component * refactor(date-input): remove unused charsPerSegment prop and update related references to charsCount for consistency * refactor(date-input): remove onKeyDown prop from DateInputBox and update DateInputBoxProviderProps to extend PropsWithChildren for better type handling * fix(date-input): import DateType and add comment about allowing any 4-digit year value in custom validation for date segments * refactor(date-picker, input-box): standardize usage of charsCount instead of charsPerSegment across components and tests for consistency * refactor(input-box): simplify InputBox tests and remove unused click handling logic for improved clarity * refactor(input-box): update exports and clean up InputBox component by removing unused props for better maintainability * refactor(date-input): simplify DateInputBox by removing unused props for cleaner implementation * fix(date-picker): correct parameter name from disableSnapshots to disableSnapshot in DatePicker stories and pass size prop to InputBox * remove isSingleDigit utility and its associated tests * test(date-picker): ensure real timers are used after tests in keyboard and mouse interaction specs * feat(date-picker): integrate focusAndSelectSegment utility for improved segment handling in DatePickerInput * fix(date-picker): clarify comment regarding focusAndSelectSegment implementation in DatePickerInput * rename files * refactor(date-picker): remove unused getFirstEmptySegment utility function * refactor(input-box): remove unused getFirstEmptySegment and getSegmentToFocus utilities * refactor(input-box): remove getSegmentToFocus utility and its associated tests * test(input-box): enhance focusAndSelectSegment tests for improved coverage and edge case handling * feat(input-box): enhance InputSegment focus handling with data-focus attribute support in stories and styles * fix(input-box): enhance focus handling in InputSegment styles by adding support for data-focus attribute
1 parent 92fd3e3 commit c9fe395

File tree

14 files changed

+585
-148
lines changed

14 files changed

+585
-148
lines changed

packages/date-picker/src/DatePicker/DatePickerInput/DatePickerInput.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import React, {
88
import isNull from 'lodash/isNull';
99

1010
import { isInvalidDateObject, isSameUTCDay } from '@leafygreen-ui/date-utils';
11-
import { isElementInputSegment } from '@leafygreen-ui/input-box';
11+
import {
12+
focusAndSelectSegment,
13+
isElementInputSegment,
14+
} from '@leafygreen-ui/input-box';
1215
import { createSyntheticEvent, keyMap } from '@leafygreen-ui/lib';
1316

1417
import {
@@ -20,7 +23,6 @@ import { DateInputSegmentChangeEventHandler } from '../../shared/components/Date
2023
import { useSharedDatePickerContext } from '../../shared/context';
2124
import { getFormattedDateStringFromSegments } from '../../shared/utils';
2225
import { useDatePickerContext } from '../DatePickerContext';
23-
import { getSegmentToFocus } from '../utils/getSegmentToFocus';
2426

2527
import { DatePickerInputProps } from './DatePickerInput.types';
2628

@@ -77,14 +79,17 @@ export const DatePickerInput = forwardRef<HTMLDivElement, DatePickerInputProps>(
7779
if (!disabled) {
7880
openMenu(e);
7981
const { target } = e;
80-
const segmentToFocus = getSegmentToFocus({
82+
83+
/**
84+
* Focus and select the appropriate segment.
85+
*
86+
* This is done here instead of `InputBox` because this component has padding that needs to be accounted for on click.
87+
*/
88+
focusAndSelectSegment({
8189
target,
8290
formatParts,
8391
segmentRefs,
8492
});
85-
86-
segmentToFocus?.focus();
87-
segmentToFocus?.select();
8893
}
8994
};
9095

packages/date-picker/src/DatePicker/utils/getSegmentToFocus/getSegmentToFocus.spec.ts

Lines changed: 0 additions & 96 deletions
This file was deleted.

packages/date-picker/src/shared/utils/getFirstEmptySegment/index.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

packages/date-picker/src/shared/utils/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export { doesSomeSegmentExist } from './doesSomeSegmentExist';
22
export { doSegmentsFormValidDate } from './doSegmentsFormValidDate';
33
export { getAutoComplete } from './getAutoComplete';
4-
export { getFirstEmptySegment } from './getFirstEmptySegment';
54
export { getFormatParts, getFormatter } from './getFormatParts';
65
export {
76
getFormattedDateString,

packages/input-box/src/InputSegment/InputSegment.stories.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ const meta: StoryMetaType<typeof InputSegment, InputSegmentStoryProps> = {
7171
segment: ['day', 'year'],
7272
size: Object.values(Size),
7373
value: ['', '2', '02', '0', '00', '2025', '0000'],
74+
// @ts-expect-error - data-focus is not a valid prop for InputSegment
75+
'data-focus': [false, true],
7476
},
7577
excludeCombinations: [
7678
{
@@ -81,6 +83,13 @@ const meta: StoryMetaType<typeof InputSegment, InputSegmentStoryProps> = {
8183
value: ['2025', '0000'],
8284
segment: ['day'],
8385
},
86+
[
87+
// @ts-expect-error - data-focus is not a valid prop for InputSegment
88+
'data-focus',
89+
{
90+
value: ['02', '0', '00', '2025', '0000'],
91+
},
92+
],
8493
],
8594
decorator: (StoryFn, context) => (
8695
<LeafyGreenProvider darkMode={context?.args.darkMode}>

packages/input-box/src/InputSegment/InputSegment.styles.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ export const baseStyles = css`
2828
-moz-appearance: textfield; /* Firefox */
2929
appearance: textfield;
3030
31-
&:focus {
31+
&:focus,
32+
&[data-focus='true'] {
3233
outline: none;
3334
}
3435
`;
@@ -44,7 +45,8 @@ export const getSegmentThemeStyles = (theme: Theme) => {
4445
]};
4546
}
4647
47-
&:focus {
48+
&:focus,
49+
&[data-focus='true'] {
4850
background-color: ${color[theme].background[Variant.Primary][
4951
InteractionState.Focus
5052
]};

packages/input-box/src/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ export {
99
export {
1010
createExplicitSegmentValidator,
1111
type ExplicitSegmentRule,
12+
focusAndSelectSegment,
13+
getSegmentToFocus,
14+
getValueFormatter,
1215
isElementInputSegment,
13-
isValidValueForSegment,
14-
} from './utils';
15-
export { getValueFormatter } from './utils/getValueFormatter/getValueFormatter';
16-
export {
1716
isValidSegmentName,
1817
isValidSegmentValue,
19-
} from './utils/isValidSegment/isValidSegment';
18+
isValidValueForSegment,
19+
} from './utils';
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { createRef } from 'react';
2+
3+
import { SegmentRefsType } from '../getSegmentToFocus/getSegmentToFocus';
4+
5+
import { focusAndSelectSegment } from './focusAndSelectSegment';
6+
7+
describe('packages/input-box/utils/focusAndSelectSegment', () => {
8+
const formatParts: Array<Intl.DateTimeFormatPart> = [
9+
{ type: 'year', value: '' },
10+
{ type: 'literal', value: '-' },
11+
{ type: 'month', value: '' },
12+
{ type: 'literal', value: '-' },
13+
{ type: 'day', value: '' },
14+
];
15+
16+
test('focuses and selects the target segment when target is a segment', () => {
17+
const target = document.createElement('input');
18+
const focusSpy = jest.spyOn(target, 'focus');
19+
const selectSpy = jest.spyOn(target, 'select');
20+
21+
const segmentRefs: SegmentRefsType = {
22+
year: createRef(),
23+
month: createRef(),
24+
day: { current: target },
25+
};
26+
27+
focusAndSelectSegment({
28+
target,
29+
formatParts,
30+
segmentRefs,
31+
});
32+
33+
expect(focusSpy).toHaveBeenCalledTimes(1);
34+
expect(selectSpy).toHaveBeenCalledTimes(1);
35+
});
36+
37+
test('focuses and selects the first empty segment when target is not a segment', () => {
38+
const yearSegment = document.createElement('input');
39+
const monthSegment = document.createElement('input');
40+
monthSegment.value = '12';
41+
const daySegment = document.createElement('input');
42+
daySegment.value = '26';
43+
44+
const yearFocusSpy = jest.spyOn(yearSegment, 'focus');
45+
const yearSelectSpy = jest.spyOn(yearSegment, 'select');
46+
47+
const target = document.createElement('div');
48+
49+
const segmentRefs: SegmentRefsType = {
50+
year: { current: yearSegment },
51+
month: { current: monthSegment },
52+
day: { current: daySegment },
53+
};
54+
55+
focusAndSelectSegment({
56+
target,
57+
formatParts,
58+
segmentRefs,
59+
});
60+
61+
expect(yearFocusSpy).toHaveBeenCalledTimes(1);
62+
expect(yearSelectSpy).toHaveBeenCalledTimes(1);
63+
});
64+
65+
test('focuses and selects the last filled segment when target is not a segment', () => {
66+
const yearSegment = document.createElement('input');
67+
yearSegment.value = '1993';
68+
const monthSegment = document.createElement('input');
69+
monthSegment.value = '12';
70+
const daySegment = document.createElement('input');
71+
daySegment.value = '26';
72+
73+
const dayFocusSpy = jest.spyOn(daySegment, 'focus');
74+
const daySelectSpy = jest.spyOn(daySegment, 'select');
75+
76+
const target = document.createElement('div');
77+
78+
const segmentRefs: SegmentRefsType = {
79+
year: { current: yearSegment },
80+
month: { current: monthSegment },
81+
day: { current: daySegment },
82+
};
83+
84+
focusAndSelectSegment({
85+
target,
86+
formatParts,
87+
segmentRefs,
88+
});
89+
90+
expect(dayFocusSpy).toHaveBeenCalledTimes(1);
91+
expect(daySelectSpy).toHaveBeenCalledTimes(1);
92+
});
93+
94+
test('Does not focus or select any segment when target is undefined', () => {
95+
const yearSegment = document.createElement('input');
96+
const monthSegment = document.createElement('input');
97+
const daySegment = document.createElement('input');
98+
99+
const yearFocusSpy = jest.spyOn(yearSegment, 'focus');
100+
const yearSelectSpy = jest.spyOn(yearSegment, 'select');
101+
102+
const monthFocusSpy = jest.spyOn(monthSegment, 'focus');
103+
const monthSelectSpy = jest.spyOn(monthSegment, 'select');
104+
const dayFocusSpy = jest.spyOn(daySegment, 'focus');
105+
const daySelectSpy = jest.spyOn(daySegment, 'select');
106+
107+
const segmentRefs: SegmentRefsType = {
108+
year: { current: yearSegment },
109+
month: { current: monthSegment },
110+
day: { current: daySegment },
111+
};
112+
113+
focusAndSelectSegment({
114+
target: undefined,
115+
formatParts,
116+
segmentRefs,
117+
});
118+
119+
expect(yearFocusSpy).not.toHaveBeenCalled();
120+
expect(yearSelectSpy).not.toHaveBeenCalled();
121+
expect(monthFocusSpy).not.toHaveBeenCalled();
122+
expect(monthSelectSpy).not.toHaveBeenCalled();
123+
expect(dayFocusSpy).not.toHaveBeenCalled();
124+
expect(daySelectSpy).not.toHaveBeenCalled();
125+
});
126+
127+
test('Does not focus or select any segment when formatParts is undefined', () => {
128+
const yearSegment = document.createElement('input');
129+
const monthSegment = document.createElement('input');
130+
const daySegment = document.createElement('input');
131+
132+
const yearFocusSpy = jest.spyOn(yearSegment, 'focus');
133+
const yearSelectSpy = jest.spyOn(yearSegment, 'select');
134+
135+
const monthFocusSpy = jest.spyOn(monthSegment, 'focus');
136+
const monthSelectSpy = jest.spyOn(monthSegment, 'select');
137+
const dayFocusSpy = jest.spyOn(daySegment, 'focus');
138+
const daySelectSpy = jest.spyOn(daySegment, 'select');
139+
140+
const target = document.createElement('div');
141+
142+
const segmentRefs: SegmentRefsType = {
143+
year: { current: yearSegment },
144+
month: { current: monthSegment },
145+
day: { current: daySegment },
146+
};
147+
148+
focusAndSelectSegment({
149+
target,
150+
formatParts: undefined,
151+
segmentRefs,
152+
});
153+
154+
expect(yearFocusSpy).not.toHaveBeenCalled();
155+
expect(yearSelectSpy).not.toHaveBeenCalled();
156+
expect(monthFocusSpy).not.toHaveBeenCalled();
157+
expect(monthSelectSpy).not.toHaveBeenCalled();
158+
expect(dayFocusSpy).not.toHaveBeenCalled();
159+
expect(daySelectSpy).not.toHaveBeenCalled();
160+
});
161+
});

0 commit comments

Comments
 (0)