Skip to content

Commit b349702

Browse files
reidbarberktabors
andauthored
RAC: Expose additional states (#4616)
* initial fix * add more render props * add more states and data attributes * fix tabs * fix tests * fix lint * lint again * fix typescript * fix ts strict issues * fix typescript * revert breadcrumbs * remove data-focus-within * fix searchfield render prop type * add more render props to datefield/timefield * add more data attributes to datepicker * add data attributes to datepicker * Adding invalid icon to RAC DatePicker and DateRangePicker (#4675) * Adding invalid icon to RAC DatePicker and DateRangePicker * removing code added by IDE autocomplete * fix ToggleButton render props * add isDisabled to select * add more states to SearchField * add more render props to RadioGroup * add focus states to Tabs * add hover state to slider track * fix select type * formatting * mergeProps * add validationState to TextField * remove hover states from non-interactive elements * remove isHover from RangeCalendar/ DateRangePicker * lint * lint * add subset of state to Select/Tabs/ComboBox * fix typo * add missing jsdoc comments --------- Co-authored-by: Kyle Taborski <[email protected]>
1 parent e783f0b commit b349702

26 files changed

+687
-145
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ import {DatePicker, Label, Group, Popover, Dialog, Calendar, CalendarGrid, Calen
113113
font-size: 12px;
114114
color: var(--text-color-invalid);
115115
}
116+
117+
&[data-validation-state=invalid] {
118+
.react-aria-DateInput:after {
119+
content: '🚫' / '';
120+
content: '🚫';
121+
alt: ' ';
122+
flex: 1;
123+
text-align: end;
124+
}
125+
}
116126
}
117127

118128
.react-aria-DateInput {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,17 @@ import {DateRangePicker, Label, Group, Popover, Dialog, RangeCalendar, CalendarG
153153
font-size: 12px;
154154
color: var(--text-color-invalid);
155155
}
156+
157+
&[data-validation-state=invalid] {
158+
[slot=end]:after {
159+
content: '🚫' / '';
160+
content: '🚫';
161+
alt: ' ';
162+
flex: 1;
163+
text-align: end;
164+
margin-right: -2rem;
165+
}
166+
}
156167
}
157168

158169
.react-aria-DateInput {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,10 +355,10 @@ function MySlider<T extends number | number[]>({label, thumbLabels, ...props}: M
355355
<Slider {...props}>
356356
<Label>{label}</Label>
357357
<SliderOutput>
358-
{state => state.values.map((_, i) => state.getThumbValueLabel(i)).join('')}
358+
{({state}) => state.values.map((_, i) => state.getThumbValueLabel(i)).join('')}
359359
</SliderOutput>
360360
<SliderTrack>
361-
{state => state.values.map((_, i) => (
361+
{({state}) => state.values.map((_, i) => (
362362
<SliderThumb key={i} index={i} aria-label={thumbLabels?.[i]} />
363363
))}
364364
</SliderTrack>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ function Button(props: ButtonProps, ref: ForwardedRef<HTMLButtonElement>) {
9696
slot={props.slot}
9797
data-pressed={ctx.isPressed || isPressed || undefined}
9898
data-hovered={isHovered || undefined}
99+
data-focused={isFocused || undefined}
99100
data-focus-visible={isFocusVisible || undefined} />
100101
);
101102
}

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

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,57 @@
1212
import {CalendarProps as BaseCalendarProps, RangeCalendarProps as BaseRangeCalendarProps, DateValue, mergeProps, useCalendar, useCalendarCell, useCalendarGrid, useFocusRing, useHover, useLocale, useRangeCalendar, VisuallyHidden} from 'react-aria';
1313
import {ButtonContext} from './Button';
1414
import {CalendarDate, createCalendar, DateDuration, endOfMonth, getWeeksInMonth, isSameDay, isSameMonth} from '@internationalized/date';
15-
import {CalendarState, RangeCalendarState, useCalendarState, useRangeCalendarState} from 'react-stately';
15+
import {CalendarState, RangeCalendarState, useCalendarState, useRangeCalendarState, ValidationState} from 'react-stately';
1616
import {ContextValue, DOMProps, forwardRefType, Provider, RenderProps, SlotProps, StyleProps, useContextProps, useRenderProps} from './utils';
1717
import {DOMAttributes, FocusableElement} from '@react-types/shared';
1818
import {filterDOMProps, useObjectRef} from '@react-aria/utils';
1919
import {HeadingContext} from './Heading';
2020
import React, {createContext, ForwardedRef, forwardRef, ReactElement, useContext} from 'react';
2121
import {TextContext} from './Text';
2222

23-
export interface CalendarProps<T extends DateValue> extends Omit<BaseCalendarProps<T>, 'errorMessage'>, RenderProps<CalendarState>, SlotProps {
23+
export interface CalendarRenderProps {
24+
/**
25+
* Whether an element within the calendar is focused, either via a mouse or keyboard.
26+
* @selector :focus-within
27+
*/
28+
isFocusWithin: boolean,
29+
/**
30+
* Whether an element within the calendar is keyboard focused.
31+
* @selector [data-focus-visible]
32+
*/
33+
isFocusVisible: boolean,
34+
/**
35+
* Whether the calendar is disabled.
36+
* @selector [data-disabled]
37+
*/
38+
isDisabled: boolean,
39+
/**
40+
* State of the calendar.
41+
*/
42+
state: CalendarState,
43+
/**
44+
* Validation state of the date field.
45+
* @selector [data-validation-state]
46+
*/
47+
validationState: ValidationState
48+
}
49+
50+
export interface RangeCalendarRenderProps extends Omit<CalendarRenderProps, 'state'> {
51+
/**
52+
* State of the range calendar.
53+
*/
54+
state: RangeCalendarState
55+
}
56+
57+
export interface CalendarProps<T extends DateValue> extends Omit<BaseCalendarProps<T>, 'errorMessage'>, RenderProps<CalendarRenderProps>, SlotProps {
2458
/**
2559
* The amount of days that will be displayed at once. This affects how pagination works.
2660
* @default {months: 1}
2761
*/
2862
visibleDuration?: DateDuration
2963
}
3064

31-
export interface RangeCalendarProps<T extends DateValue> extends Omit<BaseRangeCalendarProps<T>, 'errorMessage'>, RenderProps<RangeCalendarState>, SlotProps {
65+
export interface RangeCalendarProps<T extends DateValue> extends Omit<BaseRangeCalendarProps<T>, 'errorMessage'>, RenderProps<RangeCalendarRenderProps>, SlotProps {
3266
/**
3367
* The amount of days that will be displayed at once. This affects how pagination works.
3468
* @default {months: 1}
@@ -49,16 +83,31 @@ function Calendar<T extends DateValue>(props: CalendarProps<T>, ref: ForwardedRe
4983
createCalendar
5084
});
5185

86+
let {focusProps, isFocused, isFocusVisible} = useFocusRing({within: true});
5287
let {calendarProps, prevButtonProps, nextButtonProps, errorMessageProps, title} = useCalendar(props, state);
5388

5489
let renderProps = useRenderProps({
5590
...props,
56-
values: state,
91+
values: {
92+
state,
93+
isFocusWithin: isFocused,
94+
isFocusVisible,
95+
isDisabled: props.isDisabled || false,
96+
validationState: state.validationState
97+
},
5798
defaultClassName: 'react-aria-Calendar'
5899
});
59100

60101
return (
61-
<div {...renderProps} {...calendarProps} ref={ref} slot={props.slot}>
102+
<div
103+
{...focusProps}
104+
{...renderProps}
105+
{...calendarProps}
106+
ref={ref}
107+
slot={props.slot}
108+
data-focus-visible={isFocusVisible || undefined}
109+
data-disabled={props.isDisabled || undefined}
110+
data-validation-state={state.validationState || undefined}>
62111
<Provider
63112
values={[
64113
[ButtonContext, {
@@ -114,6 +163,7 @@ function RangeCalendar<T extends DateValue>(props: RangeCalendarProps<T>, ref: F
114163
createCalendar
115164
});
116165

166+
let {focusProps, isFocused, isFocusVisible} = useFocusRing({within: true});
117167
let {calendarProps, prevButtonProps, nextButtonProps, errorMessageProps, title} = useRangeCalendar(
118168
props,
119169
state,
@@ -122,12 +172,26 @@ function RangeCalendar<T extends DateValue>(props: RangeCalendarProps<T>, ref: F
122172

123173
let renderProps = useRenderProps({
124174
...props,
125-
values: state,
175+
values: {
176+
state,
177+
isFocusWithin: isFocused,
178+
isFocusVisible,
179+
isDisabled: props.isDisabled || false,
180+
validationState: state.validationState
181+
},
126182
defaultClassName: 'react-aria-RangeCalendar'
127183
});
128184

129185
return (
130-
<div {...renderProps} {...calendarProps} ref={ref} slot={props.slot}>
186+
<div
187+
{...focusProps}
188+
{...renderProps}
189+
{...calendarProps}
190+
ref={ref}
191+
slot={props.slot}
192+
data-focus-visible={isFocusVisible || undefined}
193+
data-disabled={props.isDisabled || undefined}
194+
data-validation-state={state.validationState || undefined}>
131195
<Provider
132196
values={[
133197
[ButtonContext, {
@@ -183,7 +247,7 @@ export interface CalendarCellRenderProps {
183247
* Whether the cell is currently hovered with a mouse.
184248
* @selector [data-hovered]
185249
*/
186-
isHovered: boolean,
250+
isHovered: boolean,
187251
/**
188252
* Whether the cell is currently being pressed.
189253
* @selector [data-pressed]
@@ -203,7 +267,7 @@ export interface CalendarCellRenderProps {
203267
* Whether the cell is the last date in a range selection.
204268
* @selector [data-selection-end]
205269
*/
206-
isSelectionEnd: boolean,
270+
isSelectionEnd: boolean,
207271
/**
208272
* Whether the cell is focused.
209273
* @selector :focus

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,21 @@ export interface CheckboxGroupRenderProps {
4040
* The validation state of the checkbox group.
4141
* @selector [data-validation-state="invalid" | "valid"]
4242
*/
43-
validationState: ValidationState
43+
validationState: ValidationState,
44+
/**
45+
* Whether an element within the checkbox group is focused, either via a mouse or keyboard.
46+
* @selector :focus-within
47+
*/
48+
isFocusWithin: boolean,
49+
/**
50+
* Whether an element within the checkbox group is keyboard focused.
51+
* @selector [data-focus-visible]
52+
*/
53+
isFocusVisible: boolean,
54+
/**
55+
* State of the checkbox group.
56+
*/
57+
state: CheckboxGroupState
4458
}
4559

4660
export interface CheckboxRenderProps {
@@ -103,6 +117,7 @@ function CheckboxGroup(props: CheckboxGroupProps, ref: ForwardedRef<HTMLDivEleme
103117
[props, ref] = useContextProps(props, ref, CheckboxGroupContext);
104118
let state = useCheckboxGroupState(props);
105119
let [labelRef, label] = useSlot();
120+
let {isFocused, isFocusVisible, focusProps} = useFocusRing({within: true});
106121
let {groupProps, labelProps, descriptionProps, errorMessageProps} = useCheckboxGroup({
107122
...props,
108123
label
@@ -114,20 +129,26 @@ function CheckboxGroup(props: CheckboxGroupProps, ref: ForwardedRef<HTMLDivEleme
114129
isDisabled: state.isDisabled,
115130
isReadOnly: state.isReadOnly,
116131
isRequired: props.isRequired || false,
117-
validationState: state.validationState
132+
validationState: state.validationState,
133+
isFocusWithin: isFocused,
134+
isFocusVisible,
135+
state
118136
},
119137
defaultClassName: 'react-aria-CheckboxGroup'
120138
});
121139

122140
return (
123141
<div
142+
{...focusProps}
124143
{...groupProps}
125144
{...renderProps}
126145
ref={ref}
127146
slot={props.slot}
128147
data-readonly={state.isReadOnly || undefined}
129148
data-required={props.isRequired || undefined}
130-
data-validation-state={state.validationState || undefined}>
149+
data-validation-state={state.validationState || undefined}
150+
data-disabled={props.isDisabled || undefined}
151+
data-focus-visible={isFocusVisible || undefined}>
131152
<Provider
132153
values={[
133154
[InternalCheckboxGroupContext, state],

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
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 {AriaComboBoxProps, useComboBox, useFilter} from 'react-aria';
12+
import {AriaComboBoxProps, useComboBox, useFilter, useFocusRing} from 'react-aria';
1313
import {ButtonContext} from './Button';
14+
import {ComboBoxState, useComboBoxState} from 'react-stately';
1415
import {ContextValue, forwardRefType, Provider, RenderProps, slotCallbackSymbol, SlotProps, useContextProps, useRenderProps, useSlot} from './utils';
1516
import {filterDOMProps, useResizeObserver} from '@react-aria/utils';
1617
import {InputContext} from './Input';
@@ -20,19 +21,27 @@ import {PopoverContext} from './Popover';
2021
import React, {createContext, ForwardedRef, forwardRef, useCallback, useMemo, useRef, useState} from 'react';
2122
import {TextContext} from './Text';
2223
import {useCollection} from './Collection';
23-
import {useComboBoxState} from 'react-stately';
2424

2525
export interface ComboBoxRenderProps {
2626
/**
2727
* Whether the combobox is focused, either via a mouse or keyboard.
2828
* @selector [data-focused]
2929
*/
3030
isFocused: boolean,
31+
/**
32+
* Whether the combobox is keyboard focused.
33+
* @selector [data-focus-visible]
34+
*/
35+
isFocusVisible: boolean,
3136
/**
3237
* Whether the combobox is currently open.
3338
* @selector [data-open]
3439
*/
35-
isOpen: boolean
40+
isOpen: boolean,
41+
/**
42+
* State of the combobox.
43+
*/
44+
state: Omit<ComboBoxState<unknown>, 'children' | 'setOpen' | 'toggle' | 'open' | 'close' | 'selectionManager' | 'setSelectedKey' | 'setFocused' | 'collection' | 'commit' | 'revert'>
3645
}
3746

3847
export interface ComboBoxProps<T extends object> extends Omit<AriaComboBoxProps<T>, 'children' | 'placeholder' | 'name' | 'label' | 'description' | 'errorMessage'>, RenderProps<ComboBoxRenderProps>, SlotProps {
@@ -60,8 +69,24 @@ function ComboBox<T extends object>(props: ComboBoxProps<T>, ref: ForwardedRef<H
6069
collection
6170
});
6271

72+
let {isFocusVisible, focusProps} = useFocusRing({within: true});
73+
6374
// Only expose a subset of state to renderProps function to avoid infinite render loop
64-
let renderPropsState = useMemo(() => ({isOpen: state.isOpen, isFocused: state.isFocused}), [state.isOpen, state.isFocused]);
75+
let renderPropsState = useMemo(() => ({
76+
isOpen: state.isOpen,
77+
isFocused: state.isFocused,
78+
isFocusVisible,
79+
state: {
80+
focusStrategy: state.focusStrategy,
81+
isOpen: state.isOpen,
82+
selectedKey: state.selectedKey,
83+
disabledKeys: state.disabledKeys,
84+
isFocused: state.isFocused,
85+
selectedItem: state.selectedItem,
86+
inputValue: state.inputValue,
87+
setInputValue: state.setInputValue
88+
}
89+
}), [state.isOpen, state.isFocused, state.focusStrategy, state.selectedKey, state.disabledKeys, state.selectedItem, state.inputValue, state.setInputValue, isFocusVisible]);
6590
let buttonRef = useRef<HTMLButtonElement>(null);
6691
let inputRef = useRef<HTMLInputElement>(null);
6792
let listBoxRef = useRef<HTMLDivElement>(null);
@@ -136,9 +161,11 @@ function ComboBox<T extends object>(props: ComboBoxProps<T>, ref: ForwardedRef<H
136161
<div
137162
{...DOMProps}
138163
{...renderProps}
164+
{...focusProps}
139165
ref={ref}
140166
slot={props.slot}
141167
data-focused={state.isFocused || undefined}
168+
data-focus-visible={isFocusVisible || undefined}
142169
data-open={state.isOpen || undefined} />
143170
{portal}
144171
</Provider>

0 commit comments

Comments
 (0)