Skip to content

Commit 2a891c1

Browse files
committed
Extract PlainDateSegments into component
1 parent 737e1b8 commit 2a891c1

File tree

4 files changed

+142
-70
lines changed

4 files changed

+142
-70
lines changed

packages/circuit-ui/components/DateInput/DateInput.tsx

Lines changed: 17 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ import { applyMultipleRefs } from '../../util/refs.js';
6060
import { changeInputValue } from '../../util/input-value.js';
6161

6262
import { Dialog } from './components/Dialog.js';
63-
import { DateSegment } from './components/DateSegment.js';
63+
import { PlainDateSegments } from './components/PlainDateSegments.js';
6464
import { usePlainDateState } from './hooks/usePlainDateState.js';
6565
import { useSegmentFocus } from './hooks/useSegmentFocus.js';
66-
import { getCalendarButtonLabel, getDateSegments } from './DateInputService.js';
66+
import { getCalendarButtonLabel, getDateParts } from './DateInputService.js';
6767
import classes from './DateInput.module.css';
6868
import { translations } from './translations/index.js';
6969

@@ -315,7 +315,7 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
315315

316316
const dialogStyles = isMobile ? mobileStyles : floatingStyles;
317317

318-
const segments = getDateSegments(locale);
318+
const parts = getDateParts(locale);
319319
const calendarButtonLabel = getCalendarButtonLabel(
320320
openCalendarButtonLabel,
321321
state.date,
@@ -371,68 +371,20 @@ export const DateInput = forwardRef<HTMLInputElement, DateInputProps>(
371371
readOnly && classes.readonly,
372372
)}
373373
>
374-
{segments.map((segment, index) => {
375-
const segmentProps = {
376-
required,
377-
invalid,
378-
disabled,
379-
readOnly,
380-
focus,
381-
// Only the first segment should be associated with the validation hint to reduce verbosity.
382-
'aria-describedby': index === 0 ? descriptionIds : undefined,
383-
};
384-
switch (segment.type) {
385-
case 'year':
386-
return (
387-
<DateSegment
388-
key={segment.type}
389-
aria-label={yearInputLabel}
390-
autoComplete={
391-
autoComplete === 'bday' ? 'bday-year' : undefined
392-
}
393-
{...segmentProps}
394-
{...state.props.year}
395-
/>
396-
);
397-
case 'month':
398-
return (
399-
<DateSegment
400-
key={segment.type}
401-
aria-label={monthInputLabel}
402-
autoComplete={
403-
autoComplete === 'bday' ? 'bday-month' : undefined
404-
}
405-
{...segmentProps}
406-
{...state.props.month}
407-
/>
408-
);
409-
case 'day':
410-
return (
411-
<DateSegment
412-
key={segment.type}
413-
aria-label={dayInputLabel}
414-
autoComplete={
415-
autoComplete === 'bday' ? 'bday-day' : undefined
416-
}
417-
{...segmentProps}
418-
{...state.props.day}
419-
/>
420-
);
421-
case 'literal':
422-
return (
423-
<div
424-
// biome-ignore lint/suspicious/noArrayIndexKey: The order of the literals is static
425-
key={segment.type + index}
426-
className={classes.literal}
427-
aria-hidden="true"
428-
>
429-
{segment.value}
430-
</div>
431-
);
432-
default:
433-
return null;
434-
}
435-
})}
374+
<PlainDateSegments
375+
parts={parts}
376+
state={state}
377+
focus={focus}
378+
yearInputLabel={yearInputLabel}
379+
monthInputLabel={monthInputLabel}
380+
dayInputLabel={dayInputLabel}
381+
aria-describedby={descriptionIds}
382+
required={required}
383+
invalid={invalid}
384+
disabled={disabled}
385+
readOnly={readOnly}
386+
autoComplete={autoComplete}
387+
/>
436388
</div>
437389
<IconButton
438390
ref={calendarButtonRef}

packages/circuit-ui/components/DateInput/DateInputService.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@
1616
import { describe, expect, it } from 'vitest';
1717
import { Temporal } from 'temporal-polyfill';
1818

19-
import { getCalendarButtonLabel, getDateSegments } from './DateInputService.js';
19+
import { getCalendarButtonLabel, getDateParts } from './DateInputService.js';
2020

2121
describe('DateInputService', () => {
22-
describe('getDateSegments', () => {
22+
describe('getDateParts', () => {
2323
it.each([
2424
// locale, year, month, day
2525
['en-US', [4, 0, 2]],
2626
['de-DE', [4, 2, 0]],
2727
['pt-BR', [4, 2, 0]],
2828
])('should order the segments for the %s locale', (locale, indices) => {
29-
const actual = getDateSegments(locale);
29+
const actual = getDateParts(locale);
3030
const year = actual.findIndex(({ type }) => type === 'year');
3131
const month = actual.findIndex(({ type }) => type === 'month');
3232
const day = actual.findIndex(({ type }) => type === 'day');
@@ -39,7 +39,7 @@ describe('DateInputService', () => {
3939
['de-DE', '.'],
4040
['pt-BR', '/'],
4141
])('should return the literal for the %s locale', (locale, literal) => {
42-
const actual = getDateSegments(locale);
42+
const actual = getDateParts(locale);
4343
const literalSegment = actual.find(({ type }) => type === 'literal');
4444
expect(literalSegment?.value).toBe(literal);
4545
});

packages/circuit-ui/components/DateInput/DateInputService.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,26 @@ import type { Locale } from '../../util/i18n.js';
2020

2121
const TEST_VALUE = new Temporal.PlainDate(2024, 3, 8);
2222

23-
export function getDateSegments(locale?: Locale) {
23+
export type DatePart =
24+
| { type: 'literal'; value: string }
25+
| {
26+
type:
27+
| 'day'
28+
| 'dayPeriod'
29+
| 'era'
30+
| 'hour'
31+
| 'minute'
32+
| 'month'
33+
| 'second'
34+
| 'timeZoneName'
35+
| 'weekday'
36+
| 'year'
37+
| 'unknown'
38+
| 'date';
39+
value?: never;
40+
};
41+
42+
export function getDateParts(locale?: Locale): DatePart[] {
2443
const parts = formatDateTimeToParts(TEST_VALUE, locale);
2544
return parts.map(({ type, value }) =>
2645
type === 'literal' ? { type, value } : { type },
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Copyright 2024, SumUp Ltd.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
import type { DatePart } from '../DateInputService.js';
17+
import type { PlainDateState } from '../hooks/usePlainDateState.js';
18+
import classes from '../DateInput.module.css';
19+
20+
import { DateSegment, type DateSegmentProps } from './DateSegment.js';
21+
22+
export interface PlainDateSegmentsProps
23+
extends Pick<
24+
DateSegmentProps,
25+
| 'focus'
26+
| 'required'
27+
| 'invalid'
28+
| 'disabled'
29+
| 'readOnly'
30+
| 'aria-describedby'
31+
> {
32+
parts: DatePart[];
33+
state: PlainDateState;
34+
yearInputLabel: string;
35+
monthInputLabel: string;
36+
dayInputLabel: string;
37+
autoComplete?: 'bday';
38+
}
39+
40+
export function PlainDateSegments({
41+
parts,
42+
state,
43+
yearInputLabel,
44+
monthInputLabel,
45+
dayInputLabel,
46+
'aria-describedby': descriptionId,
47+
autoComplete,
48+
...props
49+
}: PlainDateSegmentsProps) {
50+
return parts.map((part, index) => {
51+
switch (part.type) {
52+
case 'year':
53+
return (
54+
<DateSegment
55+
key={part.type}
56+
aria-label={yearInputLabel}
57+
// Only associate the first segment with the validation hint to reduce verbosity
58+
aria-describedby={index === 0 ? descriptionId : undefined}
59+
autoComplete={autoComplete === 'bday' ? 'bday-year' : undefined}
60+
{...props}
61+
{...state.props.year}
62+
/>
63+
);
64+
case 'month':
65+
return (
66+
<DateSegment
67+
key={part.type}
68+
aria-label={monthInputLabel}
69+
aria-describedby={index === 0 ? descriptionId : undefined}
70+
autoComplete={autoComplete === 'bday' ? 'bday-month' : undefined}
71+
{...props}
72+
{...state.props.month}
73+
/>
74+
);
75+
case 'day':
76+
return (
77+
<DateSegment
78+
key={part.type}
79+
aria-label={dayInputLabel}
80+
aria-describedby={index === 0 ? descriptionId : undefined}
81+
autoComplete={autoComplete === 'bday' ? 'bday-day' : undefined}
82+
{...props}
83+
{...state.props.day}
84+
/>
85+
);
86+
case 'literal':
87+
return (
88+
<div
89+
// biome-ignore lint/suspicious/noArrayIndexKey: The order of the literals is static
90+
key={part.type + index}
91+
className={classes.literal}
92+
aria-hidden="true"
93+
>
94+
{part.value}
95+
</div>
96+
);
97+
default:
98+
return null;
99+
}
100+
});
101+
}

0 commit comments

Comments
 (0)