Skip to content

Commit 04d19c9

Browse files
devongovettLFDanLu
andauthored
Add missing DatePicker states (#5158)
* Add data-open state to DatePicker and DateRangePicker * Support hover and focus states in DateSegment --------- Co-authored-by: Daniel Lu <dl1644@gmail.com>
1 parent 3cde0d4 commit 04d19c9

File tree

5 files changed

+81
-11
lines changed

5 files changed

+81
-11
lines changed

packages/react-aria-components/src/DateField.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* OF ANY KIND, either express or implied. See the License for the specific language
1010
* governing permissions and limitations under the License.
1111
*/
12-
import {AriaDateFieldProps, AriaTimeFieldProps, DateValue, mergeProps, TimeValue, useDateField, useDateSegment, useLocale, useTimeField} from 'react-aria';
12+
import {AriaDateFieldProps, AriaTimeFieldProps, DateValue, mergeProps, TimeValue, useDateField, useDateSegment, useFocusRing, useHover, useLocale, useTimeField} from 'react-aria';
1313
import {ContextValue, forwardRefType, Provider, removeDataAttributes, RenderProps, SlotProps, StyleRenderProps, useContextProps, useRenderProps, useSlot} from './utils';
1414
import {createCalendar} from '@internationalized/date';
1515
import {DateFieldState, DateSegmentType, DateSegment as IDateSegment, TimeFieldState, useDateFieldState, useTimeFieldState} from 'react-stately';
@@ -248,6 +248,21 @@ const _DateInput = /*#__PURE__*/ (forwardRef as forwardRefType)(DateInput);
248248
export {_DateInput as DateInput};
249249

250250
export interface DateSegmentRenderProps extends Omit<IDateSegment, 'isEditable'> {
251+
/**
252+
* Whether the segment is currently hovered with a mouse.
253+
* @selector [data-hovered]
254+
*/
255+
isHovered: boolean,
256+
/**
257+
* Whether the segment is focused, either via a mouse or keyboard.
258+
* @selector [data-focused]
259+
*/
260+
isFocused: boolean,
261+
/**
262+
* Whether the segment is keyboard focused.
263+
* @selector [data-focus-visible]
264+
*/
265+
isFocusVisible: boolean,
251266
/**
252267
* Whether the value is a placeholder.
253268
* @selector [data-placeholder]
@@ -280,26 +295,35 @@ function DateSegment({segment, ...otherProps}: DateSegmentProps, ref: ForwardedR
280295
let state = dateFieldState ?? timeFieldState!;
281296
let domRef = useObjectRef(ref);
282297
let {segmentProps} = useDateSegment(segment, state, domRef);
298+
let {focusProps, isFocused, isFocusVisible} = useFocusRing();
299+
let {hoverProps, isHovered} = useHover({isDisabled: state.isDisabled || segment.type === 'literal'});
283300
let renderProps = useRenderProps({
284301
...otherProps,
285302
values: {
286303
...segment,
287304
isReadOnly: !segment.isEditable,
288-
isInvalid: state.isInvalid
305+
isInvalid: state.isInvalid,
306+
isHovered,
307+
isFocused,
308+
isFocusVisible
289309
},
290310
defaultChildren: segment.text,
291311
defaultClassName: 'react-aria-DateSegment'
292312
});
293313

314+
294315
return (
295316
<div
296-
{...mergeProps(filterDOMProps(otherProps as any), segmentProps)}
317+
{...mergeProps(filterDOMProps(otherProps as any), segmentProps, focusProps, hoverProps)}
297318
{...renderProps}
298319
ref={domRef}
299320
data-placeholder={segment.isPlaceholder || undefined}
300321
data-invalid={state.isInvalid || undefined}
301322
data-readonly={!segment.isEditable || undefined}
302-
data-type={segment.type} />
323+
data-type={segment.type}
324+
data-hovered={isHovered || undefined}
325+
data-focused={isFocused || undefined}
326+
data-focus-visible={isFocusVisible || undefined} />
303327
);
304328
}
305329

packages/react-aria-components/src/DatePicker.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export interface DatePickerRenderProps {
4444
* @selector [data-disabled]
4545
*/
4646
isInvalid: boolean,
47+
/**
48+
* Whether the date picker's popover is currently open.
49+
* @selector [data-open]
50+
*/
51+
isOpen: boolean,
4752
/**
4853
* State of the date picker.
4954
*/
@@ -88,7 +93,8 @@ function DatePicker<T extends DateValue>(props: DatePickerProps<T>, ref: Forward
8893
isFocusWithin: isFocused,
8994
isFocusVisible,
9095
isDisabled: props.isDisabled || false,
91-
isInvalid: state.isInvalid
96+
isInvalid: state.isInvalid,
97+
isOpen: state.isOpen
9298
},
9399
defaultClassName: 'react-aria-DatePicker'
94100
});
@@ -124,7 +130,8 @@ function DatePicker<T extends DateValue>(props: DatePickerProps<T>, ref: Forward
124130
data-focus-within={isFocused || undefined}
125131
data-invalid={state.isInvalid || undefined}
126132
data-focus-visible={isFocusVisible || undefined}
127-
data-disabled={props.isDisabled || undefined} />
133+
data-disabled={props.isDisabled || undefined}
134+
data-open={state.isOpen || undefined} />
128135
</Provider>
129136
);
130137
}
@@ -160,7 +167,8 @@ function DateRangePicker<T extends DateValue>(props: DateRangePickerProps<T>, re
160167
isFocusWithin: isFocused,
161168
isFocusVisible,
162169
isDisabled: props.isDisabled || false,
163-
isInvalid: state.isInvalid
170+
isInvalid: state.isInvalid,
171+
isOpen: state.isOpen
164172
},
165173
defaultClassName: 'react-aria-DateRangePicker'
166174
});
@@ -201,7 +209,8 @@ function DateRangePicker<T extends DateValue>(props: DateRangePickerProps<T>, re
201209
data-focus-within={isFocused || undefined}
202210
data-invalid={state.isInvalid || undefined}
203211
data-focus-visible={isFocusVisible || undefined}
204-
data-disabled={props.isDisabled || undefined} />
212+
data-disabled={props.isDisabled || undefined}
213+
data-open={state.isOpen || undefined} />
205214
</Provider>
206215
);
207216
}

packages/react-aria-components/test/DateField.test.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212

1313
import {CalendarDate} from '@internationalized/date';
1414
import {DateField, DateFieldContext, DateInput, DateSegment, Label, Text} from '../';
15-
import {pointerMap, render, within} from '@react-spectrum/test-utils';
15+
import {installPointerEvent, pointerMap, render, within} from '@react-spectrum/test-utils';
1616
import React from 'react';
1717
import userEvent from '@testing-library/user-event';
1818

1919
describe('DateField', () => {
20+
installPointerEvent();
21+
2022
let user;
2123
beforeAll(() => {
2224
user = userEvent.setup({delay: null, pointerMap});
@@ -96,7 +98,7 @@ describe('DateField', () => {
9698
<DateField>
9799
<Label>Birth date</Label>
98100
<DateInput className={({isHovered}) => isHovered ? 'hover' : ''}>
99-
{segment => <DateSegment segment={segment} />}
101+
{segment => <DateSegment segment={segment} className={({isHovered}) => isHovered ? 'hover' : ''} />}
100102
</DateInput>
101103
</DateField>
102104
);
@@ -112,14 +114,23 @@ describe('DateField', () => {
112114
await user.unhover(group);
113115
expect(group).not.toHaveAttribute('data-hovered');
114116
expect(group).not.toHaveClass('hover');
117+
118+
let segments = within(group).getAllByRole('spinbutton');
119+
await user.hover(segments[0]);
120+
expect(segments[0]).toHaveAttribute('data-hovered', 'true');
121+
expect(segments[0]).toHaveClass('hover');
122+
123+
await user.unhover(segments[0]);
124+
expect(segments[0]).not.toHaveAttribute('data-hovered', 'true');
125+
expect(segments[0]).not.toHaveClass('hover');
115126
});
116127

117128
it('should support focus visible state', async () => {
118129
let {getByRole} = render(
119130
<DateField>
120131
<Label>Birth date</Label>
121132
<DateInput className={({isFocusVisible}) => isFocusVisible ? 'focus' : ''}>
122-
{segment => <DateSegment segment={segment} />}
133+
{segment => <DateSegment segment={segment} className={({isFocusVisible}) => isFocusVisible ? 'focus' : ''} />}
123134
</DateInput>
124135
</DateField>
125136
);
@@ -133,9 +144,15 @@ describe('DateField', () => {
133144
expect(group).toHaveAttribute('data-focus-visible', 'true');
134145
expect(group).toHaveClass('focus');
135146

147+
let segments = within(group).getAllByRole('spinbutton');
148+
expect(segments[0]).toHaveAttribute('data-focus-visible', 'true');
149+
expect(segments[0]).toHaveClass('focus');
150+
136151
await user.tab({shift: true});
137152
expect(group).not.toHaveAttribute('data-focus-visible');
138153
expect(group).not.toHaveClass('focus');
154+
expect(segments[0]).not.toHaveAttribute('data-focus-visible', 'true');
155+
expect(segments[0]).not.toHaveClass('focus');
139156
});
140157

141158
it('should support disabled state', () => {

packages/react-aria-components/test/DatePicker.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,16 @@ describe('DatePicker', () => {
106106
expect(button).toHaveAttribute('data-pressed');
107107
});
108108

109+
it('should support data-open state', async () => {
110+
let {getByRole} = render(<TestDatePicker />);
111+
let datePicker = document.querySelector('.react-aria-DatePicker');
112+
let button = getByRole('button');
113+
114+
expect(datePicker).not.toHaveAttribute('data-open');
115+
await user.click(button);
116+
expect(datePicker).toHaveAttribute('data-open');
117+
});
118+
109119
it('should support render props', () => {
110120
let {getByRole} = render(
111121
<DatePicker minValue={new CalendarDate(2023, 1, 1)} defaultValue={new CalendarDate(2020, 2, 3)}>

packages/react-aria-components/test/DateRangePicker.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ describe('DateRangePicker', () => {
112112
expect(button).toHaveAttribute('data-pressed');
113113
});
114114

115+
it('should support data-open state', async () => {
116+
let {getByRole} = render(<TestDateRangePicker />);
117+
let datePicker = document.querySelector('.react-aria-DateRangePicker');
118+
let button = getByRole('button');
119+
120+
expect(datePicker).not.toHaveAttribute('data-open');
121+
await user.click(button);
122+
expect(datePicker).toHaveAttribute('data-open');
123+
});
124+
115125
it('should support render props', () => {
116126
let {getByRole} = render(
117127
<DateRangePicker defaultValue={{start: new CalendarDate(2023, 1, 10), end: new CalendarDate(2023, 1, 1)}}>

0 commit comments

Comments
 (0)