Skip to content

Commit 0f8aab0

Browse files
committed
feat: allow customazing behavior of pressed state
1 parent 76e8dd1 commit 0f8aab0

File tree

13 files changed

+125
-11
lines changed

13 files changed

+125
-11
lines changed

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,12 @@ export interface ComboBoxProps<T extends object> extends Omit<AriaComboBoxProps<
7777
*/
7878
formValue?: 'text' | 'key',
7979
/** Whether the combo box allows the menu to be open when the collection is empty. */
80-
allowsEmptyCollection?: boolean
80+
allowsEmptyCollection?: boolean,
81+
/**
82+
* Whether the trigger remains pressed when the overlay is open.
83+
* @default true
84+
*/
85+
isTriggerPressedWhenOpen?: boolean
8186
}
8287

8388
export const ComboBoxContext = createContext<ContextValue<ComboBoxProps<any>, HTMLDivElement>>(null);
@@ -123,7 +128,8 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
123128
let {
124129
name,
125130
formValue = 'key',
126-
allowsCustomValue
131+
allowsCustomValue,
132+
isTriggerPressedWhenOpen = true
127133
} = props;
128134
if (allowsCustomValue) {
129135
formValue = 'text';
@@ -207,7 +213,7 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
207213
values={[
208214
[ComboBoxStateContext, state],
209215
[LabelContext, {...labelProps, ref: labelRef}],
210-
[ButtonContext, {...buttonProps, ref: buttonRef, isPressed: state.isOpen}],
216+
[ButtonContext, {...buttonProps, ref: buttonRef, isPressed: isTriggerPressedWhenOpen && state.isOpen}],
211217
[InputContext, {...inputProps, ref: inputRef}],
212218
[OverlayTriggerStateContext, state],
213219
[PopoverContext, {

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,24 @@ export interface DatePickerProps<T extends DateValue> extends Omit<AriaDatePicke
8787
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state.
8888
* @default 'react-aria-DatePicker'
8989
*/
90-
className?: ClassNameOrFunction<DatePickerRenderProps>
90+
className?: ClassNameOrFunction<DatePickerRenderProps>,
91+
/**
92+
* Whether the trigger remains pressed when the overlay is open.
93+
* @default true
94+
*/
95+
isTriggerPressedWhenOpen?: boolean
9196
}
9297
export interface DateRangePickerProps<T extends DateValue> extends Omit<AriaDateRangePickerProps<T>, 'label' | 'description' | 'errorMessage' | 'validationState' | 'validationBehavior'>, Pick<DateRangePickerStateOptions<T>, 'shouldCloseOnSelect'>, RACValidation, RenderProps<DateRangePickerRenderProps>, SlotProps, GlobalDOMAttributes<HTMLDivElement> {
9398
/**
9499
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the element. A function may be provided to compute the class based on component state.
95100
* @default 'react-aria-DateRangePicker'
96101
*/
97-
className?: ClassNameOrFunction<DateRangePickerRenderProps>
102+
className?: ClassNameOrFunction<DateRangePickerRenderProps>,
103+
/**
104+
* Whether the trigger remains pressed when the overlay is open.
105+
* @default true
106+
*/
107+
isTriggerPressedWhenOpen?: boolean
98108
}
99109

100110
export const DatePickerContext = createContext<ContextValue<DatePickerProps<any>, HTMLDivElement>>(null);
@@ -112,6 +122,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function
112122
[props, ref] = useContextProps(props, ref, DatePickerContext);
113123
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
114124
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
125+
let {isTriggerPressedWhenOpen = true} = props;
115126
let state = useDatePickerState({
116127
...props,
117128
validationBehavior
@@ -174,7 +185,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function
174185
[DatePickerStateContext, state],
175186
[GroupContext, {...groupProps, ref: groupRef, isInvalid: state.isInvalid}],
176187
[DateFieldContext, fieldProps],
177-
[ButtonContext, {...buttonProps, isPressed: state.isOpen}],
188+
[ButtonContext, {...buttonProps, isPressed: isTriggerPressedWhenOpen && state.isOpen}],
178189
[LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}],
179190
[CalendarContext, calendarProps],
180191
[OverlayTriggerStateContext, state],
@@ -221,6 +232,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
221232
[props, ref] = useContextProps(props, ref, DateRangePickerContext);
222233
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
223234
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
235+
let {isTriggerPressedWhenOpen = true} = props;
224236
let state = useDateRangePickerState({
225237
...props,
226238
validationBehavior
@@ -283,7 +295,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
283295
values={[
284296
[DateRangePickerStateContext, state],
285297
[GroupContext, {...groupProps, ref: groupRef, isInvalid: state.isInvalid}],
286-
[ButtonContext, {...buttonProps, isPressed: state.isOpen}],
298+
[ButtonContext, {...buttonProps, isPressed: isTriggerPressedWhenOpen && state.isOpen}],
287299
[LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}],
288300
[RangeCalendarContext, calendarProps],
289301
[OverlayTriggerStateContext, state],

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import React, {createContext, ForwardedRef, forwardRef, JSX, ReactNode, useCallb
2222
import {RootMenuTriggerStateContext} from './Menu';
2323

2424
export interface DialogTriggerProps extends OverlayTriggerProps {
25+
/**
26+
* Whether the trigger remains pressed when the overlay is open.
27+
* @default true
28+
*/
29+
isPressedWhenOpen?: boolean,
2530
children: ReactNode
2631
}
2732

@@ -52,6 +57,7 @@ export function DialogTrigger(props: DialogTriggerProps): JSX.Element {
5257

5358
let buttonRef = useRef<HTMLButtonElement>(null);
5459
let {triggerProps, overlayProps} = useOverlayTrigger({type: 'dialog'}, state, buttonRef);
60+
let {isPressedWhenOpen = true} = props;
5561

5662
// Allows popover width to match trigger element
5763
let [buttonWidth, setButtonWidth] = useState<string | null>(null);
@@ -86,7 +92,7 @@ export function DialogTrigger(props: DialogTriggerProps): JSX.Element {
8692
style: {'--trigger-width': buttonWidth} as React.CSSProperties
8793
}]
8894
]}>
89-
<PressResponder {...triggerProps} ref={buttonRef} isPressed={state.isOpen}>
95+
<PressResponder {...triggerProps} ref={buttonRef} isPressed={isPressedWhenOpen && state.isOpen}>
9096
{props.children}
9197
</PressResponder>
9298
</Provider>

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,17 @@ export const RootMenuTriggerStateContext = createContext<RootMenuTriggerState |
6161
const SelectionManagerContext = createContext<SelectionManager | null>(null);
6262

6363
export interface MenuTriggerProps extends BaseMenuTriggerProps {
64+
/**
65+
* Whether the trigger remains pressed when the overlay is open.
66+
* @default true
67+
*/
68+
isPressedWhenOpen?: boolean,
6469
children: ReactNode
6570
}
6671

6772
export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
6873
let state = useMenuTriggerState(props);
74+
let {isPressedWhenOpen = true} = props;
6975
let ref = useRef<HTMLButtonElement>(null);
7076
let {menuTriggerProps, menuProps} = useMenuTrigger({
7177
...props,
@@ -100,7 +106,7 @@ export function MenuTrigger(props: MenuTriggerProps): JSX.Element {
100106
'aria-labelledby': menuProps['aria-labelledby']
101107
}]
102108
]}>
103-
<PressResponder {...menuTriggerProps} ref={ref} isPressed={state.isOpen}>
109+
<PressResponder {...menuTriggerProps} ref={ref} isPressed={isPressedWhenOpen ? state.isOpen : undefined}>
104110
{props.children}
105111
</PressResponder>
106112
</Provider>

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ export interface SelectProps<T extends object = {}, M extends SelectionMode = 's
8686
* Temporary text that occupies the select when it is empty.
8787
* @default 'Select an item' (localized)
8888
*/
89-
placeholder?: string
89+
placeholder?: string,
90+
/**
91+
* Whether the trigger remains pressed when the overlay is open.
92+
* @default true
93+
*/
94+
isTriggerPressedWhenOpen?: boolean
9095
}
9196

9297
export const SelectContext = createContext<ContextValue<SelectProps<any, SelectionMode>, HTMLDivElement>>(null);
@@ -131,6 +136,7 @@ interface SelectInnerProps<T extends object> {
131136
function SelectInner<T extends object>({props, selectRef: ref, collection}: SelectInnerProps<T>) {
132137
let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
133138
let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
139+
let {isTriggerPressedWhenOpen = true} = props;
134140
let state = useSelectState({
135141
...props,
136142
collection,
@@ -201,7 +207,7 @@ function SelectInner<T extends object>({props, selectRef: ref, collection}: Sele
201207
[SelectStateContext, state],
202208
[SelectValueContext, valueProps],
203209
[LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}],
204-
[ButtonContext, {...triggerProps, ref: buttonRef, isPressed: state.isOpen, autoFocus: props.autoFocus}],
210+
[ButtonContext, {...triggerProps, ref: buttonRef, isPressed: isTriggerPressedWhenOpen && state.isOpen, autoFocus: props.autoFocus}],
205211
[OverlayTriggerStateContext, state],
206212
[PopoverContext, {
207213
trigger: 'Select',

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ export default {
4646
validationBehavior: {
4747
control: 'select',
4848
options: ['native', 'aria']
49+
},
50+
isTriggerPressedWhenOpen: {
51+
control: 'boolean'
4952
}
5053
}
5154
} as Meta<typeof DatePicker>;

packages/react-aria-components/stories/Select.stories.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ export default {
3131
selectionMode: {
3232
control: 'radio',
3333
options: ['single', 'multiple']
34+
},
35+
isTriggerPressedWhenOpen: {
36+
control: 'boolean'
3437
}
3538
}
3639
} as Meta<typeof Select>;

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ describe('ComboBox', () => {
109109
expect(button).toHaveAttribute('data-pressed');
110110
});
111111

112+
it('should not apply isPressed state to button when expanded and isTriggerPressedWhenOpen is false', async () => {
113+
let {getByRole} = render(<TestComboBox isTriggerPressedWhenOpen={false} />);
114+
let button = getByRole('button');
115+
116+
expect(button).not.toHaveAttribute('data-pressed');
117+
await user.click(button);
118+
expect(button).not.toHaveAttribute('data-pressed');
119+
});
120+
112121
it('should support filtering sections', async () => {
113122
let tree = render(
114123
<ComboBox>

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

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

110+
it('should not apply isPressed state to button when expanded and isTriggerPressedWhenOpen is false', async () => {
111+
let {getByRole} = render(<TestDatePicker isTriggerPressedWhenOpen={false} />);
112+
let button = getByRole('button');
113+
114+
expect(button).not.toHaveAttribute('data-pressed');
115+
await user.click(button);
116+
expect(button).not.toHaveAttribute('data-pressed');
117+
});
118+
110119
it('should support data-open state', async () => {
111120
let {getByRole} = render(<TestDatePicker />);
112121
let datePicker = document.querySelector('.react-aria-DatePicker');

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ describe('DateRangePicker', () => {
128128
await user.click(button);
129129
expect(button).toHaveAttribute('data-pressed');
130130
});
131+
132+
it('should not apply isPressed state to button when expanded and isTriggerPressedWhenOpen is false', async () => {
133+
let {getByRole} = render(<TestDateRangePicker isTriggerPressedWhenOpen={false} />);
134+
let button = getByRole('button');
135+
136+
expect(button).not.toHaveAttribute('data-pressed');
137+
await user.click(button);
138+
expect(button).not.toHaveAttribute('data-pressed');
139+
});
131140

132141
it('should support data-open state', async () => {
133142
let {getByRole} = render(<TestDateRangePicker />);

0 commit comments

Comments
 (0)