Skip to content

Commit 92aa6f4

Browse files
rothsandrosnowystingerreidbarber
authored
feat: allow customizing behavior of pressed state (#8971)
* feat: allow customizing behavior of pressed state * feat: use new prop in S2 components * fix daterangepicker * rename prop * whitespace --------- Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Reid Barber <[email protected]>
1 parent a0cc05b commit 92aa6f4

20 files changed

+184
-91
lines changed

packages/@react-spectrum/s2/src/ComboBox.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export interface ComboboxStyleProps {
7979
size?: 'S' | 'M' | 'L' | 'XL'
8080
}
8181
export interface ComboBoxProps<T extends object> extends
82-
Omit<AriaComboBoxProps<T>, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection' | keyof GlobalDOMAttributes>,
82+
Omit<AriaComboBoxProps<T>, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
8383
ComboboxStyleProps,
8484
StyleProps,
8585
SpectrumLabelableProps,
@@ -354,6 +354,7 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co
354354
return (
355355
<AriaComboBox
356356
{...comboBoxProps}
357+
isTriggerUpWhenOpen
357358
allowsEmptyCollection
358359
style={UNSAFE_style}
359360
className={UNSAFE_className + style(field(), getAllowedOverrides())({
@@ -643,9 +644,6 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps<any
643644
)}
644645
<Button
645646
ref={buttonRef}
646-
// Prevent press scale from sticking while ComboBox is open.
647-
// @ts-ignore
648-
isPressed={false}
649647
style={renderProps => pressScale(buttonRef)(renderProps)}
650648
className={renderProps => inputButton({
651649
...renderProps,

packages/@react-spectrum/s2/src/DatePicker.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps';
4141

4242

4343
export interface DatePickerProps<T extends DateValue> extends
44-
Omit<AriaDatePickerProps<T>, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>,
44+
Omit<AriaDatePickerProps<T>, 'children' | 'className' | 'style' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
4545
Pick<CalendarProps<T>, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>,
4646
Pick<PopoverProps, 'shouldFlip'>,
4747
StyleProps,
@@ -155,6 +155,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function
155155
ref={ref}
156156
isRequired={isRequired}
157157
{...dateFieldProps}
158+
isTriggerUpWhenOpen
158159
style={UNSAFE_style}
159160
className={(UNSAFE_className || '') + style(field(), getAllowedOverrides())({
160161
isInForm: !!formContext,
@@ -277,9 +278,6 @@ export function CalendarButton(props: {isOpen: boolean, size: 'S' | 'M' | 'L' |
277278
return (
278279
<Button
279280
ref={buttonRef}
280-
// Prevent press scale from sticking while DatePicker is open.
281-
// @ts-ignore
282-
isPressed={false}
283281
onFocusChange={setButtonHasFocus}
284282
style={pressScale(buttonRef)}
285283
className={renderProps => inputButton({

packages/@react-spectrum/s2/src/DateRangePicker.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps';
3333

3434

3535
export interface DateRangePickerProps<T extends DateValue> extends
36-
Omit<AriaDateRangePickerProps<T>, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>,
36+
Omit<AriaDateRangePickerProps<T>, 'children' | 'className' | 'style' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
3737
Pick<RangeCalendarProps<T>, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>,
3838
Pick<PopoverProps, 'shouldFlip'>,
3939
StyleProps,
@@ -89,6 +89,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
8989
ref={ref}
9090
isRequired={isRequired}
9191
{...dateFieldProps}
92+
isTriggerUpWhenOpen
9293
style={UNSAFE_style}
9394
className={(UNSAFE_className || '') + style(field(), getAllowedOverrides())({
9495
isInForm: !!formContext,

packages/@react-spectrum/s2/src/DialogTrigger.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@
1111
*/
1212

1313
import {DialogTrigger as AriaDialogTrigger, DialogTriggerProps as AriaDialogTriggerProps} from 'react-aria-components';
14-
import {PressResponder} from '@react-aria/interactions';
1514
import {ReactNode} from 'react';
1615

17-
export interface DialogTriggerProps extends AriaDialogTriggerProps {}
16+
export type DialogTriggerProps = Omit<AriaDialogTriggerProps, 'isTriggerUpWhenOpen'>;
1817

1918
/**
2019
* DialogTrigger serves as a wrapper around a Dialog and its associated trigger, linking the Dialog's
@@ -23,12 +22,6 @@ export interface DialogTriggerProps extends AriaDialogTriggerProps {}
2322
*/
2423
export function DialogTrigger(props: DialogTriggerProps): ReactNode {
2524
return (
26-
<AriaDialogTrigger {...props}>
27-
{/* RAC sets isPressed via PressResponder when the dialog is open.
28-
We don't want press scaling to appear to get "stuck", so override this. */}
29-
<PressResponder isPressed={false}>
30-
{props.children}
31-
</PressResponder>
32-
</AriaDialogTrigger>
25+
<AriaDialogTrigger {...props} isTriggerUpWhenOpen />
3326
);
3427
}

packages/@react-spectrum/s2/src/Menu.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps';
5353
// viewbox on LinkOut is super weird just because i copied the icon from designs...
5454
// need to strip id's from icons
5555

56-
export interface MenuTriggerProps extends AriaMenuTriggerProps {
56+
export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, 'isTriggerUpWhenOpen'> {
5757
/**
5858
* Alignment of the menu relative to the trigger.
5959
*
@@ -547,8 +547,6 @@ export function MenuItem(props: MenuItemProps): ReactNode {
547547
* linking the Menu's open state with the trigger's press state.
548548
*/
549549
function MenuTrigger(props: MenuTriggerProps): ReactNode {
550-
// RAC sets isPressed via PressResponder when the menu is open.
551-
// We don't want press scaling to appear to get "stuck", so override this.
552550
// For mouse interactions, menus open on press start. When the popover underlay appears
553551
// it covers the trigger button, causing onPressEnd to fire immediately and no press scaling
554552
// to occur. We override this by listening for pointerup on the document ourselves.

packages/@react-spectrum/s2/src/Picker.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export interface PickerStyleProps {
9898

9999
type SelectionMode = 'single' | 'multiple';
100100
export interface PickerProps<T extends object, M extends SelectionMode = 'single'> extends
101-
Omit<AriaSelectProps<T, M>, 'children' | 'style' | 'className' | 'allowsEmptyCollection' | keyof GlobalDOMAttributes>,
101+
Omit<AriaSelectProps<T, M>, 'children' | 'style' | 'className' | 'allowsEmptyCollection' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
102102
PickerStyleProps,
103103
StyleProps,
104104
SpectrumLabelableProps,
@@ -351,6 +351,7 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
351351
return (
352352
<AriaSelect
353353
{...pickerProps}
354+
isTriggerUpWhenOpen
354355
aria-describedby={spinnerId}
355356
placeholder={placeholder}
356357
style={UNSAFE_style}
@@ -522,9 +523,6 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj
522523
<Button
523524
ref={buttonRef}
524525
style={renderProps => pressScale(buttonRef)(renderProps)}
525-
// Prevent press scale from sticking while Picker is open.
526-
// @ts-ignore
527-
isPressed={false}
528526
className={renderProps => inputButton({
529527
...renderProps,
530528
size: size,

packages/@react-spectrum/s2/src/TabsPicker.tsx

Lines changed: 73 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,24 @@ import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'};
4141
import {
4242
FieldLabel
4343
} from './Field';
44-
import {FocusableRef, FocusableRefValue, SpectrumLabelableProps} from '@react-types/shared';
44+
import {FocusableRef, FocusableRefValue, PressEvent, SpectrumLabelableProps} from '@react-types/shared';
4545
import {forwardRefType} from './types';
4646
import {HeaderContext, HeadingContext, Text, TextContext} from './Content';
4747
import {IconContext} from './Icon';
4848
import {Placement, useLocale} from 'react-aria';
4949
import {Popover} from './Popover';
50+
import {PressResponder} from '@react-aria/interactions';
5051
import {pressScale} from './pressScale';
5152
import {raw} from '../style/style-macro' with {type: 'macro'};
52-
import React, {createContext, forwardRef, ReactNode, useContext, useRef} from 'react';
53+
import React, {createContext, forwardRef, ReactNode, useContext, useRef, useState} from 'react';
5354
import {useFocusableRef} from '@react-spectrum/utils';
5455
import {useFormProps} from './Form';
56+
import {useGlobalListeners} from '@react-aria/utils';
5557
import {useSpectrumContextProps} from './useSpectrumContextProps';
5658
export interface PickerStyleProps {
5759
}
5860
export interface PickerProps<T extends object> extends
59-
Omit<AriaSelectProps<T>, 'children' | 'style' | 'className' | 'placeholder'>,
61+
Omit<AriaSelectProps<T>, 'children' | 'style' | 'className' | 'placeholder' | 'isTriggerUpWhenOpen'>,
6062
PickerStyleProps,
6163
StyleProps,
6264
SpectrumLabelableProps,
@@ -182,70 +184,85 @@ function Picker<T extends object>(props: PickerProps<T>, ref: FocusableRef<HTMLB
182184
let {direction: dir} = useLocale();
183185
let RTLFlipOffset = dir === 'rtl' ? -1 : 1;
184186

187+
// For mouse interactions, pickers open on press start. When the popover underlay appears
188+
// it covers the trigger button, causing onPressEnd to fire immediately and no press scaling
189+
// to occur. We override this by listening for pointerup on the document ourselves.
190+
let [isPressed, setPressed] = useState(false);
191+
let {addGlobalListener} = useGlobalListeners();
192+
let onPressStart = (e: PressEvent) => {
193+
if (e.pointerType !== 'mouse') {
194+
return;
195+
}
196+
setPressed(true);
197+
addGlobalListener(document, 'pointerup', () => {
198+
setPressed(false);
199+
}, {once: true, capture: true});
200+
};
201+
185202
return (
186203
<div>
187204
<AriaSelect
188205
{...pickerProps}
206+
isTriggerUpWhenOpen
189207
className=""
190208
aria-labelledby={`${labelBehavior === 'hide' ? valueId : ''} ${ariaLabelledby}`}>
191209
{({isOpen}) => (
192210
<>
193211
<FieldLabel isQuiet={isQuiet} />
194-
<Button
195-
ref={domRef}
196-
style={renderProps => pressScale(domRef)(renderProps)}
197-
// Prevent press scale from sticking while Picker is open.
198-
// @ts-ignore
199-
isPressed={false}
200-
className={renderProps => inputButton({
201-
...renderProps,
202-
size: 'M',
203-
isOpen,
204-
isQuiet,
205-
density
206-
})}>
207-
<SelectValue className={valueStyles + ' ' + raw('&> * {display: none;}')}>
208-
{({defaultChildren}) => {
209-
return (
210-
<Provider
211-
values={[
212-
[IconContext, {
213-
slots: {
214-
icon: {
215-
render: centerBaseline({slot: 'icon', styles: iconCenterWrapper({labelBehavior})}),
216-
styles: icon
212+
<PressResponder onPressStart={onPressStart} isPressed={isPressed}>
213+
<Button
214+
ref={domRef}
215+
style={renderProps => pressScale(domRef)(renderProps)}
216+
className={renderProps => inputButton({
217+
...renderProps,
218+
size: 'M',
219+
isOpen,
220+
isQuiet,
221+
density
222+
})}>
223+
<SelectValue className={valueStyles + ' ' + raw('&> * {display: none;}')}>
224+
{({defaultChildren}) => {
225+
return (
226+
<Provider
227+
values={[
228+
[IconContext, {
229+
slots: {
230+
icon: {
231+
render: centerBaseline({slot: 'icon', styles: iconCenterWrapper({labelBehavior})}),
232+
styles: icon
233+
}
217234
}
218-
}
219-
}],
220-
[TextContext, {
221-
slots: {
222-
// Default slot is useful when converting other collections to PickerItems.
223-
[DEFAULT_SLOT]: {
224-
id: valueId,
225-
styles: style({
226-
display: {
227-
default: 'block',
228-
labelBehavior: {
229-
hide: 'none'
230-
}
231-
},
232-
flexGrow: 1,
233-
truncate: true
234-
})({labelBehavior})
235+
}],
236+
[TextContext, {
237+
slots: {
238+
// Default slot is useful when converting other collections to PickerItems.
239+
[DEFAULT_SLOT]: {
240+
id: valueId,
241+
styles: style({
242+
display: {
243+
default: 'block',
244+
labelBehavior: {
245+
hide: 'none'
246+
}
247+
},
248+
flexGrow: 1,
249+
truncate: true
250+
})({labelBehavior})
251+
}
235252
}
236-
}
237-
}],
238-
[InsideSelectValueContext, true]
239-
]}>
240-
{defaultChildren}
241-
</Provider>
242-
);
243-
}}
244-
</SelectValue>
245-
<ChevronIcon
246-
size={size}
247-
className={iconStyles} />
248-
</Button>
253+
}],
254+
[InsideSelectValueContext, true]
255+
]}>
256+
{defaultChildren}
257+
</Provider>
258+
);
259+
}}
260+
</SelectValue>
261+
<ChevronIcon
262+
size={size}
263+
className={iconStyles} />
264+
</Button>
265+
</PressResponder>
249266
<Popover
250267
hideArrow
251268
offset={menuOffset}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ 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+
/** Whether the trigger is up when the overlay is open. */
82+
isTriggerUpWhenOpen?: boolean
8183
}
8284

8385
export const ComboBoxContext = createContext<ContextValue<ComboBoxProps<any>, HTMLDivElement>>(null);
@@ -207,7 +209,7 @@ function ComboBoxInner<T extends object>({props, collection, comboBoxRef: ref}:
207209
values={[
208210
[ComboBoxStateContext, state],
209211
[LabelContext, {...labelProps, ref: labelRef}],
210-
[ButtonContext, {...buttonProps, ref: buttonRef, isPressed: state.isOpen}],
212+
[ButtonContext, {...buttonProps, ref: buttonRef, isPressed: !props.isTriggerUpWhenOpen && state.isOpen}],
211213
[InputContext, {...inputProps, ref: inputRef}],
212214
[OverlayTriggerStateContext, state],
213215
[PopoverContext, {

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,18 @@ 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+
/** Whether the trigger is up when the overlay is open. */
92+
isTriggerUpWhenOpen?: boolean
9193
}
9294
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> {
9395
/**
9496
* 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.
9597
* @default 'react-aria-DateRangePicker'
9698
*/
97-
className?: ClassNameOrFunction<DateRangePickerRenderProps>
99+
className?: ClassNameOrFunction<DateRangePickerRenderProps>,
100+
/** Whether the trigger is up when the overlay is open. */
101+
isTriggerUpWhenOpen?: boolean
98102
}
99103

100104
export const DatePickerContext = createContext<ContextValue<DatePickerProps<any>, HTMLDivElement>>(null);
@@ -174,7 +178,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function
174178
[DatePickerStateContext, state],
175179
[GroupContext, {...groupProps, ref: groupRef, isInvalid: state.isInvalid}],
176180
[DateFieldContext, fieldProps],
177-
[ButtonContext, {...buttonProps, isPressed: state.isOpen}],
181+
[ButtonContext, {...buttonProps, isPressed: !props.isTriggerUpWhenOpen && state.isOpen}],
178182
[LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}],
179183
[CalendarContext, calendarProps],
180184
[OverlayTriggerStateContext, state],
@@ -283,7 +287,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
283287
values={[
284288
[DateRangePickerStateContext, state],
285289
[GroupContext, {...groupProps, ref: groupRef, isInvalid: state.isInvalid}],
286-
[ButtonContext, {...buttonProps, isPressed: state.isOpen}],
290+
[ButtonContext, {...buttonProps, isPressed: !props.isTriggerUpWhenOpen && state.isOpen}],
287291
[LabelContext, {...labelProps, ref: labelRef, elementType: 'span'}],
288292
[RangeCalendarContext, calendarProps],
289293
[OverlayTriggerStateContext, state],

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

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

2424
export interface DialogTriggerProps extends OverlayTriggerProps {
25+
/** Whether the trigger is up when the overlay is open. */
26+
isTriggerUpWhenOpen?: boolean,
2527
children: ReactNode
2628
}
2729

@@ -86,7 +88,7 @@ export function DialogTrigger(props: DialogTriggerProps): JSX.Element {
8688
style: {'--trigger-width': buttonWidth} as React.CSSProperties
8789
}]
8890
]}>
89-
<PressResponder {...triggerProps} ref={buttonRef} isPressed={state.isOpen}>
91+
<PressResponder {...triggerProps} ref={buttonRef} isPressed={!props.isTriggerUpWhenOpen && state.isOpen}>
9092
{props.children}
9193
</PressResponder>
9294
</Provider>

0 commit comments

Comments
 (0)