Skip to content

Commit f5fd94d

Browse files
authored
Add support for controlling focused date in calendar (#2899)
1 parent 3c89231 commit f5fd94d

File tree

11 files changed

+248
-29
lines changed

11 files changed

+248
-29
lines changed

packages/@react-aria/calendar/docs/useCalendar.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,28 @@ import {today} from '@internationalized/date';
382382
<Calendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} />
383383
```
384384

385+
### Controlling the focused date
386+
387+
By default, the selected date is focused when a `Calendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `useCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it.
388+
389+
This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value.
390+
391+
```tsx example
392+
import {CalendarDate} from '@internationalized/date';
393+
394+
function Example() {
395+
let defaultDate = new CalendarDate(2021, 7, 1);
396+
let [focusedDate, setFocusedDate] = React.useState(defaultDate);
397+
398+
return (
399+
<div style={{flexDirection: 'column', alignItems: 'start', gap: 20}}>
400+
<button onClick={() => setFocusedDate(defaultDate)}>Reset focused date</button>
401+
<Calendar focusedValue={focusedDate} onFocusChange={setFocusedDate} />
402+
</div>
403+
);
404+
}
405+
```
406+
385407
### Disabled
386408

387409
The `isDisabled` boolean prop makes the Calendar disabled. Cells cannot be focused or selected.

packages/@react-aria/calendar/docs/useRangeCalendar.mdx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,28 @@ import {today} from '@internationalized/date';
392392
<RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} />
393393
```
394394

395+
### Controlling the focused date
396+
397+
By default, the first selected date is focused when a `RangeCalendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `useRangeCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it.
398+
399+
This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value.
400+
401+
```tsx example
402+
import {CalendarDate} from '@internationalized/date';
403+
404+
function Example() {
405+
let defaultDate = new CalendarDate(2021, 7, 1);
406+
let [focusedDate, setFocusedDate] = React.useState(defaultDate);
407+
408+
return (
409+
<div style={{flexDirection: 'column', alignItems: 'start', gap: 20}}>
410+
<button onClick={() => setFocusedDate(defaultDate)}>Reset focused date</button>
411+
<RangeCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} />
412+
</div>
413+
);
414+
}
415+
```
416+
395417
### Disabled
396418

397419
The `isDisabled` boolean prop makes the RangeCalendar disabled. Cells cannot be focused or selected.

packages/@react-aria/datepicker/src/useDatePicker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ export function useDatePicker<T extends DateValue>(props: AriaDatePickerProps<T>
119119
maxValue: props.maxValue,
120120
isDisabled: props.isDisabled,
121121
isReadOnly: props.isReadOnly,
122-
isDateDisabled: props.isDateDisabled
122+
isDateDisabled: props.isDateDisabled,
123+
defaultFocusedValue: state.dateValue ? undefined : props.placeholderValue
123124
}
124125
};
125126
}

packages/@react-aria/datepicker/src/useDateRangePicker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ export function useDateRangePicker<T extends DateValue>(props: AriaDateRangePick
155155
maxValue: props.maxValue,
156156
isDisabled: props.isDisabled,
157157
isReadOnly: props.isReadOnly,
158-
isDateDisabled: props.isDateDisabled
158+
isDateDisabled: props.isDateDisabled,
159+
defaultFocusedValue: state.dateRange ? undefined : props.placeholderValue
159160
}
160161
};
161162
}

packages/@react-spectrum/calendar/docs/Calendar.mdx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import packageData from '@react-spectrum/calendar/package.json';
1818
```jsx import
1919
import {Calendar} from '@react-spectrum/calendar';
2020
import {Flex} from '@react-spectrum/layout';
21+
import {ActionButton} from '@adobe/react-spectrum';
2122
```
2223

2324
---
@@ -132,6 +133,28 @@ import {today} from '@internationalized/date';
132133
<Calendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} />
133134
```
134135

136+
## Controlling the focused date
137+
138+
By default, the selected date is focused when a `Calendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `Calendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it.
139+
140+
This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value.
141+
142+
```tsx example
143+
import {CalendarDate} from '@internationalized/date';
144+
145+
function Example() {
146+
let defaultDate = new CalendarDate(2021, 7, 1);
147+
let [focusedDate, setFocusedDate] = React.useState(defaultDate);
148+
149+
return (
150+
<Flex direction="column" alignItems="start" gap="size-200">
151+
<ActionButton onPress={() => setFocusedDate(defaultDate)}>Reset focused date</ActionButton>
152+
<Calendar focusedValue={focusedDate} onFocusChange={setFocusedDate} />
153+
</Flex>
154+
);
155+
}
156+
```
157+
135158
## Props
136159

137160
<PropTable component={docs.exports.Calendar} links={docs.links} />
@@ -170,5 +193,7 @@ The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDi
170193
By default, the `Calendar` displays a single month. The `visibleMonths` prop allows displaying up to 3 months at a time.
171194

172195
```tsx example
173-
<Calendar aria-label="Event date" visibleMonths={3} />
196+
<div style={{maxWidth: '100%', overflow: 'auto'}}>
197+
<Calendar aria-label="Event date" visibleMonths={3} />
198+
</div>
174199
```

packages/@react-spectrum/calendar/docs/RangeCalendar.mdx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import packageData from '@react-spectrum/calendar/package.json';
1818
```jsx import
1919
import {RangeCalendar} from '@react-spectrum/calendar';
2020
import {Flex} from '@react-spectrum/layout';
21+
import {ActionButton} from '@adobe/react-spectrum';
2122
```
2223

2324
---
@@ -148,6 +149,28 @@ import {today} from '@internationalized/date';
148149
<RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} />
149150
```
150151

152+
## Controlling the focused date
153+
154+
By default, the first selected date is focused when a `RangeCalendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `RangeCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it.
155+
156+
This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value.
157+
158+
```tsx example
159+
import {CalendarDate} from '@internationalized/date';
160+
161+
function Example() {
162+
let defaultDate = new CalendarDate(2021, 7, 1);
163+
let [focusedDate, setFocusedDate] = React.useState(defaultDate);
164+
165+
return (
166+
<Flex direction="column" alignItems="start" gap="size-200">
167+
<ActionButton onPress={() => setFocusedDate(defaultDate)}>Reset focused date</ActionButton>
168+
<RangeCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} />
169+
</Flex>
170+
);
171+
}
172+
```
173+
151174
## Props
152175

153176
<PropTable component={docs.exports.RangeCalendar} links={docs.links} />
@@ -186,5 +209,7 @@ The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike
186209
By default, a `RangeCalendar` displays a single month. The `visibleMonths` prop allows displaying up to 3 months at a time.
187210

188211
```tsx example
189-
<RangeCalendar aria-label="Trip dates" visibleMonths={3} />
212+
<div style={{maxWidth: '100%', overflow: 'auto'}}>
213+
<RangeCalendar aria-label="Trip dates" visibleMonths={3} />
214+
</div>
190215
```

packages/@react-spectrum/calendar/stories/Calendar.stories.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import {action} from '@storybook/addon-actions';
14+
import {ActionButton} from '@react-spectrum/button';
1415
import {Calendar} from '../';
1516
import {CalendarDate, CalendarDateTime, getLocalTimeZone, parseZonedDateTime, today, ZonedDateTime} from '@internationalized/date';
1617
import {DateValue} from '@react-types/calendar';
@@ -86,6 +87,14 @@ storiesOf('Date and Time/Calendar', module)
8687
.add(
8788
'minValue, visibleMonths: 3, defaultValue',
8889
() => render({minValue: new CalendarDate(2019, 6, 1), defaultValue: new CalendarDate(2019, 6, 5), visibleMonths: 3})
90+
)
91+
.add(
92+
'defaultFocusedValue',
93+
() => render({defaultFocusedValue: new CalendarDate(2019, 6, 5)})
94+
)
95+
.add(
96+
'focusedValue',
97+
() => <ControlledFocus />
8998
);
9099

91100
function render(props = {}) {
@@ -194,3 +203,13 @@ function CalendarWithZonedTime() {
194203
</Flex>
195204
);
196205
}
206+
207+
function ControlledFocus() {
208+
let [focusedDate, setFocusedDate] = useState(new CalendarDate(2019, 6, 5));
209+
return (
210+
<Flex direction="column" alignItems="start" gap="size-200">
211+
<ActionButton onPress={() => setFocusedDate(new CalendarDate(2019, 6, 5))}>Reset focused date</ActionButton>
212+
<Calendar focusedValue={focusedDate} onFocusChange={setFocusedDate} />
213+
</Flex>
214+
);
215+
}

packages/@react-spectrum/calendar/test/CalendarBase.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,66 @@ describe('CalendarBase', () => {
281281
let gridCells = getAllByRole('gridcell').filter(cell => cell.getAttribute('aria-disabled') !== 'true');
282282
expect(gridCells.length).toBe(21);
283283
});
284+
285+
it.each`
286+
Name | Calendar
287+
${'v3 Calendar'} | ${Calendar}
288+
${'v3 RangeCalendar'} | ${RangeCalendar}
289+
`('$Name should support defaultFocusedValue', ({Calendar}) => {
290+
let onFocusChange = jest.fn();
291+
let {getByRole} = render(<Calendar defaultFocusedValue={new CalendarDate(2019, 6, 5)} autoFocus onFocusChange={onFocusChange} />);
292+
293+
let grid = getByRole('grid');
294+
expect(grid).toHaveAttribute('aria-label', 'June 2019');
295+
expect(document.activeElement.getAttribute('aria-label').startsWith('Wednesday, June 5, 2019')).toBe(true);
296+
297+
fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'});
298+
fireEvent.keyUp(document.activeElement, {key: 'ArrowRight'});
299+
expect(document.activeElement.getAttribute('aria-label').startsWith('Thursday, June 6, 2019')).toBe(true);
300+
expect(onFocusChange).toHaveBeenCalledWith(new CalendarDate(2019, 6, 6));
301+
});
302+
303+
it.each`
304+
Name | Calendar
305+
${'v3 Calendar'} | ${Calendar}
306+
${'v3 RangeCalendar'} | ${RangeCalendar}
307+
`('$Name should support controlled focusedValue', ({Calendar}) => {
308+
let onFocusChange = jest.fn();
309+
let {getByRole} = render(<Calendar focusedValue={new CalendarDate(2019, 6, 5)} autoFocus onFocusChange={onFocusChange} />);
310+
311+
let grid = getByRole('grid');
312+
expect(grid).toHaveAttribute('aria-label', 'June 2019');
313+
expect(document.activeElement.getAttribute('aria-label').startsWith('Wednesday, June 5, 2019')).toBe(true);
314+
315+
fireEvent.keyDown(document.activeElement, {key: 'ArrowRight'});
316+
fireEvent.keyUp(document.activeElement, {key: 'ArrowRight'});
317+
expect(document.activeElement.getAttribute('aria-label').startsWith('Wednesday, June 5, 2019')).toBe(true);
318+
expect(onFocusChange).toHaveBeenCalledWith(new CalendarDate(2019, 6, 6));
319+
});
320+
321+
it.each`
322+
Name | Calendar
323+
${'v3 Calendar'} | ${Calendar}
324+
${'v3 RangeCalendar'} | ${RangeCalendar}
325+
`('$Name should constrain defaultFocusedValue', ({Calendar}) => {
326+
let {getByRole} = render(<Calendar defaultFocusedValue={new CalendarDate(2019, 6, 5)} minValue={new CalendarDate(2019, 7, 5)} autoFocus />);
327+
328+
let grid = getByRole('grid');
329+
expect(grid).toHaveAttribute('aria-label', 'July 2019');
330+
expect(document.activeElement.getAttribute('aria-label').startsWith('Friday, July 5, 2019')).toBe(true);
331+
});
332+
333+
it.each`
334+
Name | Calendar
335+
${'v3 Calendar'} | ${Calendar}
336+
${'v3 RangeCalendar'} | ${RangeCalendar}
337+
`('$Name should constrain focusedValue', ({Calendar}) => {
338+
let {getByRole} = render(<Calendar focusedValue={new CalendarDate(2019, 6, 5)} minValue={new CalendarDate(2019, 7, 5)} autoFocus />);
339+
340+
let grid = getByRole('grid');
341+
expect(grid).toHaveAttribute('aria-label', 'July 2019');
342+
expect(document.activeElement.getAttribute('aria-label').startsWith('Friday, July 5, 2019')).toBe(true);
343+
});
284344
});
285345

286346
describe('labeling', () => {

packages/@react-spectrum/datepicker/test/DatePickerBase.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,36 @@ describe('DatePickerBase', function () {
156156
expect(tz).not.toHaveAttribute('contenteditable');
157157
}
158158
});
159+
160+
it.each`
161+
Name | Component
162+
${'DatePicker'} | ${DatePicker}
163+
${'DateRangePicker'} | ${DateRangePicker}
164+
`('$Name should focus placeholderValue in calendar', ({Component}) => {
165+
let {getByRole} = render(<Component label="Date" placeholderValue={new CalendarDate(2019, 6, 5)} />);
166+
167+
let button = getByRole('button');
168+
triggerPress(button);
169+
170+
let grid = getByRole('grid');
171+
expect(grid).toHaveAttribute('aria-label', 'June 2019');
172+
expect(document.activeElement.getAttribute('aria-label').startsWith('Wednesday, June 5, 2019')).toBe(true);
173+
});
174+
175+
it.each`
176+
Name | Component | props
177+
${'DatePicker'} | ${DatePicker} | ${{defaultValue: new CalendarDate(2019, 7, 5)}}
178+
${'DateRangePicker'} | ${DateRangePicker} | ${{defaultValue: {start: new CalendarDate(2019, 7, 5), end: new CalendarDate(2019, 7, 10)}}}
179+
`('$Name should focus selected date over placeholderValue', ({Component, props}) => {
180+
let {getByRole} = render(<Component label="Date" {...props} placeholderValue={new CalendarDate(2019, 6, 5)} />);
181+
182+
let button = getByRole('button');
183+
triggerPress(button);
184+
185+
let grid = getByRole('grid');
186+
expect(grid).toHaveAttribute('aria-label', 'July 2019');
187+
expect(document.activeElement.getAttribute('aria-label').startsWith('Friday, July 5, 2019')).toBe(true);
188+
});
159189
});
160190

161191
describe('calendar popover', function () {

0 commit comments

Comments
 (0)