Skip to content

Commit a77030a

Browse files
authored
Refactor internal contexts to expose state publicly (#5052)
1 parent c031433 commit a77030a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1177
-444
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {AriaDateFieldProps as AriaDateFieldPropsBase, AriaTimeFieldProps, DateValue, TimeValue} from '@react-types/datepicker';
1414
import {createFocusManager, FocusManager} from '@react-aria/focus';
1515
import {DateFieldState, TimeFieldState} from '@react-stately/datepicker';
16-
import {DOMAttributes, KeyboardEvent} from '@react-types/shared';
16+
import {DOMAttributes, GroupDOMAttributes, KeyboardEvent} from '@react-types/shared';
1717
import {filterDOMProps, mergeProps, useDescription, useFormReset} from '@react-aria/utils';
1818
import {FocusEvent, InputHTMLAttributes, RefObject, useEffect, useMemo, useRef} from 'react';
1919
// @ts-ignore
@@ -33,7 +33,7 @@ export interface DateFieldAria {
3333
/** Props for the field's visible label element, if any. */
3434
labelProps: DOMAttributes,
3535
/** Props for the field grouping element. */
36-
fieldProps: DOMAttributes,
36+
fieldProps: GroupDOMAttributes,
3737
/** Props for the hidden input element for HTML form submission. */
3838
inputProps: InputHTMLAttributes<HTMLInputElement>,
3939
/** Props for the description element, if any. */
@@ -110,14 +110,14 @@ export function useDateField<T extends DateValue>(props: AriaDateFieldOptions<T>
110110
// rather than role="group". Since the date picker/date range picker already has a role="group"
111111
// with a label and description, and the segments are already labeled by this as well, this
112112
// avoids very verbose duplicate announcements.
113-
let fieldDOMProps: DOMAttributes;
113+
let fieldDOMProps: GroupDOMAttributes;
114114
if (props[roleSymbol] === 'presentation') {
115115
fieldDOMProps = {
116116
role: 'presentation'
117117
};
118118
} else {
119119
fieldDOMProps = mergeProps(fieldProps, {
120-
role: 'group',
120+
role: 'group' as const,
121121
'aria-disabled': props.isDisabled || undefined,
122122
'aria-describedby': describedBy
123123
});

packages/@react-aria/numberfield/src/useNumberField.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt
297297
...focusWithinProps,
298298
role: 'group',
299299
'aria-disabled': isDisabled,
300-
'aria-invalid': validationState === 'invalid' ? 'true' : undefined
300+
'aria-invalid': isInvalid || validationState === 'invalid' ? 'true' : undefined
301301
},
302302
labelProps,
303303
inputProps,

packages/@react-stately/datepicker/src/useDatePickerState.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export interface DatePickerStateOptions<T extends DateValue> extends DatePickerP
2828

2929
export interface DatePickerState extends OverlayTriggerState {
3030
/** The currently selected date. */
31-
value: DateValue,
31+
value: DateValue | null,
3232
/** Sets the selected date. */
33-
setValue(value: DateValue): void,
33+
setValue(value: DateValue | null): void,
3434
/**
3535
* The date portion of the value. This may be set prior to `value` if the user has
3636
* selected a date but has not yet selected a time.

packages/@react-stately/datepicker/src/useDateRangePickerState.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,21 @@ export interface DateRangePickerStateOptions<T extends DateValue = DateValue> ex
2929
type TimeRange = RangeValue<TimeValue>;
3030
export interface DateRangePickerState extends OverlayTriggerState {
3131
/** The currently selected date range. */
32-
value: DateRange,
32+
value: DateRange | null,
3333
/** Sets the selected date range. */
34-
setValue(value: DateRange): void,
34+
setValue(value: DateRange | null): void,
3535
/**
3636
* The date portion of the selected range. This may be set prior to `value` if the user has
3737
* selected a date range but has not yet selected a time range.
3838
*/
39-
dateRange: DateRange,
39+
dateRange: DateRange | null,
4040
/** Sets the date portion of the selected range. */
4141
setDateRange(value: DateRange): void,
4242
/**
4343
* The time portion of the selected range. This may be set prior to `value` if the user has
4444
* selected a time range but has not yet selected a date range.
4545
*/
46-
timeRange: TimeRange,
46+
timeRange: TimeRange | null,
4747
/** Sets the time portion of the selected range. */
4848
setTimeRange(value: TimeRange): void,
4949
/** Sets the date portion of either the start or end of the selected range. */
@@ -90,7 +90,7 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
9090
let value = controlledValue || placeholderValue;
9191

9292
let setValue = (value: DateRange) => {
93-
setPlaceholderValue(value);
93+
setPlaceholderValue(value || {start: null, end: null});
9494
if (value?.start && value.end) {
9595
setControlledValue(value);
9696
} else {

packages/@react-stately/list/src/useSingleSelectListState.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface SingleSelectListState<T> extends ListState<T> {
2727
readonly selectedKey: Key,
2828

2929
/** Sets the selected key. */
30-
setSelectedKey(key: Key): void,
30+
setSelectedKey(key: Key | null): void,
3131

3232
/** The value of the currently selected item. */
3333
readonly selectedItem: Node<T>

packages/@react-stately/selection/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface SingleSelectionState extends FocusState {
3232
/** The currently selected key in the collection. */
3333
readonly selectedKey: Key,
3434
/** Sets the selected key in the collection. */
35-
setSelectedKey(key: Key): void
35+
setSelectedKey(key: Key | null): void
3636
}
3737

3838
export interface MultipleSelectionState extends FocusState {

packages/react-aria-components/docs/Calendar.mdx

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default Layout;
1212

1313
import docs from 'docs:react-aria-components';
1414
import i18nDocs from 'docs:@internationalized/date';
15+
import ariaDocs from 'docs:@react-aria/calendar';
1516
import statelyDocs from 'docs:@react-stately/calendar';
1617
import {PropTable, HeaderInfo, TypeLink, PageDescription, StateTable, ContextTable} from '@react-spectrum/docs';
1718
import styles from '@react-spectrum/docs/src/docs.css';
@@ -767,7 +768,7 @@ interface PresetProps {
767768
768769
function Preset({date, children}: PresetProps) {
769770
/*- begin highlight -*/
770-
let context = useSlottedContext(CalendarContext);
771+
let context = useSlottedContext(CalendarContext)!;
771772
/*- end highlight -*/
772773
let onPress = () => {
773774
context.onFocusChange(date);
@@ -848,20 +849,53 @@ Now you can use `MyCustomHeading` within a `Calendar`, in place of the builtin R
848849
</Calendar>
849850
```
850851

852+
### State
853+
854+
Calendar provides a <TypeLink links={statelyDocs.links} type={statelyDocs.exports.CalendarState} /> object to its children via `CalendarStateContext`. This can be used to access and manipulate the calendar's state.
855+
856+
This example shows a `CalendarValue` component that can be placed within a `Calendar` to display the currently selected date as a formatted string.
857+
858+
```tsx example
859+
import {CalendarStateContext} from 'react-aria-components';
860+
import {useDateFormatter} from 'react-aria';
861+
862+
function CalendarValue() {
863+
/*- begin highlight -*/
864+
let state = React.useContext(CalendarStateContext)!;
865+
/*- end highlight -*/
866+
let date = state.value?.toDate(getLocalTimeZone());
867+
let formatted = date ? useDateFormatter().format(date) : 'None';
868+
return <small>Selected date: {formatted}</small>;
869+
}
870+
871+
<Calendar>
872+
<header>
873+
<Button slot="previous">◀</Button>
874+
<Heading />
875+
<Button slot="next">▶</Button>
876+
</header>
877+
<CalendarGrid>
878+
{date => <CalendarCell date={date} />}
879+
</CalendarGrid>
880+
{/*- begin highlight -*/}
881+
<CalendarValue />
882+
{/*- end highlight -*/}
883+
</Calendar>
884+
```
885+
851886
### Hooks
852887

853-
If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See [useCalendar](useCalendar.html) for more details. This example uses the `useCalendarGrid` hook to build a single week calendar view.
888+
If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See [useCalendar](useCalendar.html) for more details.
889+
890+
This example uses the <TypeLink links={ariaDocs.links} type={ariaDocs.exports.useCalendarGrid} /> hook to build a single week calendar view.
854891

855892
```tsx example render=false export=true
856893
import type {CalendarGridProps} from 'react-aria-components';
857-
import type {CalendarState} from 'react-stately';
894+
import {CalendarStateContext} from 'react-aria-components';
858895
import {useCalendarGrid} from 'react-aria';
859896
860-
interface WeekCalendarGridProps extends CalendarGridProps {
861-
state: CalendarState
862-
}
863-
864-
function WeekCalendarGrid({state, ...props}: WeekCalendarGridProps) {
897+
function WeekCalendarGrid(props: CalendarGridProps) {
898+
let state = React.useContext(CalendarStateContext)!;
865899
let {gridProps} = useCalendarGrid(props, state);
866900
867901
return (
@@ -880,14 +914,12 @@ function WeekCalendarGrid({state, ...props}: WeekCalendarGridProps) {
880914

881915
```tsx example
882916
<Calendar visibleDuration={{weeks: 1}} defaultValue={today(getLocalTimeZone())}>
883-
{({state}) => (
884-
<div className="week">
885-
<Heading />
886-
<Button slot="previous">◀</Button>
887-
<WeekCalendarGrid state={state} />
888-
<Button slot="next">▶</Button>
889-
</div>
890-
)}
917+
<div className="week">
918+
<Heading />
919+
<Button slot="previous">◀</Button>
920+
<WeekCalendarGrid />
921+
<Button slot="next">▶</Button>
922+
</div>
891923
</Calendar>
892924
```
893925

packages/react-aria-components/docs/CheckboxGroup.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,31 @@ Now you can use `MyCustomLabel` within a `CheckboxGroup`, in place of the builti
648648
</CheckboxGroup>
649649
```
650650

651+
### State
652+
653+
CheckboxGroup provides a <TypeLink links={statelyDocs.links} type={statelyDocs.exports.CheckboxGroupState} /> object to its children via `CheckboxGroupStateContext`. This can be used to access and manipulate the checkbox group's state.
654+
655+
This example shows a `SelectionCount` component that can be placed within a `CheckboxGroup` to display the number of selected items.
656+
657+
```tsx example
658+
import {CheckboxGroupStateContext} from 'react-aria-components';
659+
660+
function SelectionCount() {
661+
/*- begin highlight -*/
662+
let state = React.useContext(CheckboxGroupStateContext)!;
663+
/*- end highlight -*/
664+
return <small>{state.value.length} items selected.</small>;
665+
}
666+
667+
<MyCheckboxGroup label="Sandwich condiments">
668+
<MyCheckbox value="lettuce">Lettuce</MyCheckbox>
669+
<MyCheckbox value="tomato">Tomato</MyCheckbox>
670+
<MyCheckbox value="onion">Onion</MyCheckbox>
671+
<MyCheckbox value="sprouts">Sprouts</MyCheckbox>
672+
<SelectionCount />
673+
</MyCheckboxGroup>
674+
```
675+
651676
### Hooks
652677

653678
If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See [useCheckboxGroup](useCheckboxGroup.html) for more details.

packages/react-aria-components/docs/ComboBox.mdx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,86 @@ Now you can use `MyCustomLabel` within a `ComboBox`, in place of the builtin Rea
13191319
</ComboBox>
13201320
```
13211321

1322+
### State
1323+
1324+
ComboBox provides an <TypeLink links={statelyDocs.links} type={statelyDocs.exports.ComboBoxState} /> object to its children via `ComboBoxStateContext`. This can be used to access and manipulate the combo box's state.
1325+
1326+
This example shows a `ComboBoxClearButton` component that can be placed within a `ComboBox` to allow the user to clear the selected value.
1327+
1328+
```tsx example
1329+
import {ComboBoxStateContext} from 'react-aria-components';
1330+
1331+
function ComboBoxClearButton() {
1332+
/*- begin highlight -*/
1333+
let state = React.useContext(ComboBoxStateContext);
1334+
/*- end highlight -*/
1335+
return (
1336+
<Button
1337+
// Don't inherit default Button behavior from ComboBox.
1338+
slot={null}
1339+
className="clear-button"
1340+
aria-label="Clear"
1341+
onPress={() => state?.setSelectedKey(null)}>
1342+
1343+
</Button>
1344+
);
1345+
}
1346+
1347+
<ComboBox defaultSelectedKey="cat">
1348+
<Label>Favorite Animal</Label>
1349+
<div>
1350+
<Input />
1351+
{/*- begin highlight -*/}
1352+
<ComboBoxClearButton />
1353+
{/*- end highlight -*/}
1354+
<Button>▼</Button>
1355+
</div>
1356+
<Popover>
1357+
<ListBox>
1358+
<Item id="cat">Cat</Item>
1359+
<Item id="dog">Dog</Item>
1360+
<Item id="kangaroo">Kangaroo</Item>
1361+
</ListBox>
1362+
</Popover>
1363+
</ComboBox>
1364+
```
1365+
1366+
<details>
1367+
<summary style={{fontWeight: 'bold'}}><ChevronRight size="S" /> Show CSS</summary>
1368+
1369+
```css
1370+
.clear-button {
1371+
width: 1.143rem;
1372+
height: 1.143rem;
1373+
border-radius: 1.143rem;
1374+
margin-left: -3.143rem;
1375+
font-size: 0.857rem;
1376+
line-height: 0.857rem;
1377+
vertical-align: middle;
1378+
text-align: center;
1379+
background: gray;
1380+
color: white;
1381+
border: none;
1382+
padding: 0;
1383+
outline: none;
1384+
1385+
&[data-pressed] {
1386+
background: dimgray;
1387+
}
1388+
1389+
&[data-focus-visible] {
1390+
outline: 2px solid slateblue;
1391+
outline-offset: 2px;
1392+
}
1393+
1394+
+ .react-aria-Button {
1395+
margin-left: 4px;
1396+
}
1397+
}
1398+
```
1399+
1400+
</details>
1401+
13221402
### Hooks
13231403

13241404
If you need to customize things even further, such as accessing internal state, intercepting events, or customizing the DOM structure, you can drop down to the lower level Hook-based API. See [useComboBox](useComboBox.html) for more details.

packages/react-aria-components/docs/DateField.mdx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,37 @@ Now you can use `MyCustomLabel` within a `DateField`, in place of the builtin Re
718718
</DateField>
719719
```
720720

721+
### State
722+
723+
DateField provides a <TypeLink links={statelyDocs.links} type={statelyDocs.exports.DateFieldState} /> object to its children via `DateFieldStateContext`. This can be used to access and manipulate the date field's state.
724+
725+
This example shows a `DateFormat` component that can be placed within a `DateField` to display the expected date format.
726+
727+
```tsx example
728+
import {DateFieldStateContext} from 'react-aria-components';
729+
import {useLocale} from 'react-aria';
730+
731+
function DateFormat() {
732+
/*- begin highlight -*/
733+
let state = React.useContext(DateFieldStateContext)!;
734+
/*- end highlight -*/
735+
let {locale} = useLocale();
736+
let displayNames = new Intl.DisplayNames(locale, {type: 'dateTimeField'});
737+
let format = state.segments.map(segment => segment.type === 'literal' ? segment.text : displayNames.of(segment.type)).join(' ');
738+
return <small>{format}</small>;
739+
}
740+
741+
<DateField defaultValue={today(getLocalTimeZone())}>
742+
<Label>Date</Label>
743+
<DateInput>
744+
{segment => <DateSegment segment={segment} />}
745+
</DateInput>
746+
{/*- begin highlight -*/}
747+
<DateFormat />
748+
{/*- end highlight -*/}
749+
</DateField>
750+
```
751+
721752
### Hooks
722753

723754
If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See [useDateField](useDateField.html) for more details.

0 commit comments

Comments
 (0)