Skip to content

Commit a4daf9b

Browse files
lishichengyanLFDanLusnowystingerdevongovett
authored
Add Support for longPress to MenuTrigger (#2678)
* add support for longpress to MenuTrigger * add basic tests for MenuTrigger's longPress behavior * more tests for MenuTrigger's longPress support * more tests for MenuTrigger's longPress support * make press a default value for trigger * fix lint issues * simple fix * remove isDiabled: false * more tests and simple fixes * fix lint issues * fix test * fix lint issues * fix lint issues * Update packages/dev/test-utils/src/events.ts Co-authored-by: Robert Snow <[email protected]> * Require alt key, and add a11y description * Add hold affordance to action button when long press is enabled * Fix combobox tests Co-authored-by: Daniel Lu <[email protected]> Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Devon Govett <[email protected]> Co-authored-by: Robert Snow <[email protected]>
1 parent bef1b1d commit a4daf9b

File tree

10 files changed

+270
-32
lines changed

10 files changed

+270
-32
lines changed

packages/@react-aria/combobox/test/useComboBox.test.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,16 @@ describe('useComboBox', function () {
3939
let props = {
4040
label: 'test label',
4141
popoverRef: {
42-
current: {}
42+
current: document.createElement('div')
4343
},
4444
buttonRef: {
45-
current: {}
45+
current: document.createElement('button')
4646
},
4747
inputRef: {
48-
current: {
49-
contains: jest.fn(),
50-
focus: jest.fn()
51-
}
48+
current: document.createElement('input')
5249
},
5350
listBoxRef: {
54-
current: {}
51+
current: document.createElement('div')
5552
},
5653
layout: mockLayout
5754
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"longPressMessage": "Long press or press Alt + ArrowDown to open menu"
3+
}

packages/@react-aria/menu/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"@babel/runtime": "^7.6.2",
21+
"@react-aria/i18n": "^3.3.4",
2122
"@react-aria/interactions": "^3.7.0",
2223
"@react-aria/overlays": "^3.7.3",
2324
"@react-aria/selection": "^3.7.0",

packages/@react-aria/menu/src/useMenuTrigger.ts

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@
1212

1313
import {AriaButtonProps} from '@react-types/button';
1414
import {HTMLAttributes, RefObject} from 'react';
15+
// @ts-ignore
16+
import intlMessages from '../intl/*.json';
1517
import {MenuTriggerState} from '@react-stately/menu';
16-
import {useId} from '@react-aria/utils';
18+
import {MenuTriggerType} from '@react-types/menu';
19+
import {mergeProps, useId} from '@react-aria/utils';
20+
import {useLongPress} from '@react-aria/interactions';
21+
import {useMessageFormatter} from '@react-aria/i18n';
1722
import {useOverlayTrigger} from '@react-aria/overlays';
1823

1924
interface MenuTriggerAriaProps {
2025
/** The type of menu that the menu trigger opens. */
2126
type?: 'menu' | 'listbox',
2227
/** Whether menu trigger is disabled. */
23-
isDisabled?: boolean
28+
isDisabled?: boolean,
29+
/** How menu is triggered. */
30+
trigger?: MenuTriggerType
2431
}
2532

2633
interface MenuTriggerAria {
@@ -39,7 +46,8 @@ interface MenuTriggerAria {
3946
export function useMenuTrigger(props: MenuTriggerAriaProps, state: MenuTriggerState, ref: RefObject<HTMLElement>): MenuTriggerAria {
4047
let {
4148
type = 'menu' as MenuTriggerAriaProps['type'],
42-
isDisabled
49+
isDisabled,
50+
trigger = 'press'
4351
} = props;
4452

4553
let menuTriggerId = useId();
@@ -50,11 +58,19 @@ export function useMenuTrigger(props: MenuTriggerAriaProps, state: MenuTriggerSt
5058
return;
5159
}
5260

61+
if (trigger === 'longPress' && !e.altKey) {
62+
return;
63+
}
64+
5365
if (ref && ref.current) {
5466
switch (e.key) {
55-
case 'ArrowDown':
5667
case 'Enter':
5768
case ' ':
69+
if (trigger === 'longPress') {
70+
return;
71+
}
72+
// fallthrough
73+
case 'ArrowDown':
5874
// Stop propagation, unless it would already be handled by useKeyboard.
5975
if (!('continuePropagation' in e)) {
6076
e.stopPropagation();
@@ -73,23 +89,39 @@ export function useMenuTrigger(props: MenuTriggerAriaProps, state: MenuTriggerSt
7389
}
7490
};
7591

92+
let formatMessage = useMessageFormatter(intlMessages);
93+
let {longPressProps} = useLongPress({
94+
accessibilityDescription: formatMessage('longPressMessage'),
95+
onLongPressStart() {
96+
state.close();
97+
},
98+
onLongPress() {
99+
state.open('first');
100+
}
101+
});
102+
103+
let pressProps = {
104+
onPressStart(e) {
105+
// For consistency with native, open the menu on mouse/key down, but touch up.
106+
if (e.pointerType !== 'touch' && e.pointerType !== 'keyboard' && !isDisabled) {
107+
// If opened with a screen reader, auto focus the first item.
108+
// Otherwise, the menu itself will be focused.
109+
state.toggle(e.pointerType === 'virtual' ? 'first' : null);
110+
}
111+
},
112+
onPress(e) {
113+
if (e.pointerType === 'touch' && !isDisabled) {
114+
state.toggle();
115+
}
116+
}
117+
};
118+
119+
triggerProps = mergeProps(triggerProps, trigger === 'press' ? pressProps : longPressProps);
120+
76121
return {
77122
menuTriggerProps: {
78123
...triggerProps,
79124
id: menuTriggerId,
80-
onPressStart(e) {
81-
// For consistency with native, open the menu on mouse/key down, but touch up.
82-
if (e.pointerType !== 'touch' && e.pointerType !== 'keyboard' && !isDisabled) {
83-
// If opened with a screen reader, auto focus the first item.
84-
// Otherwise, the menu itself will be focused.
85-
state.toggle(e.pointerType === 'virtual' ? 'first' : null);
86-
}
87-
},
88-
onPress(e) {
89-
if (e.pointerType === 'touch' && !isDisabled) {
90-
state.toggle();
91-
}
92-
},
93125
onKeyDown
94126
},
95127
menuProps: {

packages/@react-spectrum/button/src/ActionButton.tsx

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

1313
import {classNames, SlotProvider, useFocusableRef, useSlotProps, useStyleProps} from '@react-spectrum/utils';
14+
import CornerTriangle from '@spectrum-icons/ui/CornerTriangle';
1415
import {FocusableRef} from '@react-types/shared';
1516
import {FocusRing} from '@react-aria/focus';
1617
import {mergeProps} from '@react-aria/utils';
@@ -31,6 +32,8 @@ function ActionButton(props: SpectrumActionButtonProps, ref: FocusableRef<HTMLBu
3132
staticColor,
3233
children,
3334
autoFocus,
35+
// @ts-ignore (private)
36+
holdAffordance,
3437
...otherProps
3538
} = props;
3639

@@ -62,6 +65,9 @@ function ActionButton(props: SpectrumActionButtonProps, ref: FocusableRef<HTMLBu
6265
styleProps.className
6366
)
6467
}>
68+
{holdAffordance &&
69+
<CornerTriangle UNSAFE_className={classNames(styles, 'spectrum-ActionButton-hold')} />
70+
}
6571
<SlotProvider
6672
slots={{
6773
icon: {

packages/@react-spectrum/menu/src/MenuTrigger.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {classNames, unwrapDOMRef, useDOMRef, useIsMobileDevice} from '@react-spectrum/utils';
13+
import {classNames, SlotProvider, unwrapDOMRef, useDOMRef, useIsMobileDevice} from '@react-spectrum/utils';
1414
import {DismissButton, useOverlayPosition} from '@react-aria/overlays';
1515
import {DOMRef, DOMRefValue} from '@react-types/shared';
1616
import {FocusScope} from '@react-aria/focus';
@@ -35,13 +35,14 @@ function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef<HTMLElement>)
3535
align = 'start',
3636
shouldFlip = true,
3737
direction = 'bottom',
38-
closeOnSelect
38+
closeOnSelect,
39+
trigger = 'press'
3940
} = props;
4041

4142
let [menuTrigger, menu] = React.Children.toArray(children);
4243
let state = useMenuTriggerState(props);
4344

44-
let {menuTriggerProps, menuProps} = useMenuTrigger({}, state, menuTriggerRef);
45+
let {menuTriggerProps, menuProps} = useMenuTrigger({trigger}, state, menuTriggerRef);
4546

4647
let initialPlacement: Placement;
4748
switch (direction) {
@@ -114,9 +115,11 @@ function MenuTrigger(props: SpectrumMenuTriggerProps, ref: DOMRef<HTMLElement>)
114115

115116
return (
116117
<Fragment>
117-
<PressResponder {...menuTriggerProps} ref={menuTriggerRef} isPressed={state.isOpen}>
118-
{menuTrigger}
119-
</PressResponder>
118+
<SlotProvider slots={{actionButton: {holdAffordance: trigger === 'longPress'}}}>
119+
<PressResponder {...menuTriggerProps} ref={menuTriggerRef} isPressed={state.isOpen}>
120+
{menuTrigger}
121+
</PressResponder>
122+
</SlotProvider>
120123
<MenuContext.Provider value={menuContext}>
121124
{overlay}
122125
</MenuContext.Provider>

packages/@react-spectrum/menu/stories/MenuTrigger.stories.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,10 @@ storiesOf('MenuTrigger', module)
528528
</Menu>
529529
)
530530
)
531+
.add(
532+
'MenuTrigger with trigger="longPress"',
533+
() => render(defaultMenu, {trigger: 'longPress'})
534+
)
531535
.add('controlled isOpen',
532536
() => <ControlledOpeningMenuTrigger />
533537
);

0 commit comments

Comments
 (0)