From 34c5c2bb823196785d4a70219588d17128302d3f Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 13 May 2026 16:11:34 -0700 Subject: [PATCH 1/5] add action button hold affordance + menu --- .../@react-spectrum/s2/src/ActionButton.tsx | 48 ++++++++++++++++- packages/@react-spectrum/s2/src/Menu.tsx | 51 ++++++++++++------- .../s2/stories/Menu.stories.tsx | 24 ++++++++- 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/packages/@react-spectrum/s2/src/ActionButton.tsx b/packages/@react-spectrum/s2/src/ActionButton.tsx index 0f5b6245add..a80d4839ec0 100644 --- a/packages/@react-spectrum/s2/src/ActionButton.tsx +++ b/packages/@react-spectrum/s2/src/ActionButton.tsx @@ -12,7 +12,14 @@ import {ActionButtonGroupContext} from './ActionButtonGroup'; import {AvatarContext} from './Avatar'; -import {baseColor, focusRing, fontRelative, lightDark, style} from '../style' with {type: 'macro'}; +import { + baseColor, + focusRing, + fontRelative, + lightDark, + space, + style +} from '../style' with {type: 'macro'}; import {ButtonProps, ButtonRenderProps, Button as RACButton} from 'react-aria-components/Button'; import {centerBaseline} from './CenterBaseline'; import {ContextValue, Provider, useSlottedContext} from 'react-aria-components/slots'; @@ -22,12 +29,13 @@ import { staticColor, StyleProps } from './style-utils' with {type: 'macro'}; +import CornerTriangle from '../ui-icons/CornerTriangle'; import {createContext, forwardRef, ReactNode, useContext} from 'react'; import {FocusableRef, FocusableRefValue, GlobalDOMAttributes} from '@react-types/shared'; import {IconContext} from './Icon'; import {ImageContext} from './Image'; -import intlMessages from '../intl/*.json'; // @ts-ignore +import intlMessages from '../intl/*.json'; import {NotificationBadgeContext} from './NotificationBadge'; import {OverlayTriggerStateContext} from 'react-aria-components/Dialog'; import {pressScale} from './pressScale'; @@ -36,6 +44,7 @@ import {SkeletonContext} from './Skeleton'; import {Text, TextContext} from './Content'; import {useFocusableRef} from './useDOMRef'; import {useFormProps} from './Form'; +import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; import {usePendingState} from './Button'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -54,6 +63,8 @@ export interface ActionButtonStyleProps { * style](https://spectrum.adobe.com/page/action-button/#Quiet). */ isQuiet?: boolean; + /** @private */ + holdAffordance?: boolean; } interface ToggleButtonStyleProps { @@ -340,6 +351,8 @@ export const ActionButton = forwardRef(function ActionButton( } = ctx || {}; let {isProgressVisible} = usePendingState(isPending); + let {holdAffordance} = props; + let {direction} = useLocale(); return ( )} + {holdAffordance && ( + + )} )} diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 01a17b56cfd..0a07dbda111 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -76,6 +76,8 @@ import {useGlobalListeners} from 'react-aria/private/utils/useGlobalListeners'; import {useId} from 'react-aria/useId'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; +import {ActionButtonContext} from './ActionButton'; +import {ToggleButtonContext} from './ToggleButton'; import {useSpectrumContextProps} from './useSpectrumContextProps'; // viewbox on LinkOut is super weird just because i copied the icon from designs... // need to strip id's from icons @@ -99,6 +101,12 @@ export interface MenuTriggerProps extends AriaMenuTriggerProps { * @default true */ shouldFlip?: boolean; + /** + * How the menu is triggered. + * + * @default 'press' + */ + trigger?: 'press' | 'longPress'; } export interface MenuProps @@ -736,7 +744,7 @@ function MenuTrigger(props: MenuTriggerProps): ReactNode { ); }; - let {align = 'start', direction = 'bottom', shouldFlip} = props; + let {align = 'start', direction = 'bottom', shouldFlip, trigger = 'press'} = props; let placement: Placement; switch (direction) { case 'left': @@ -750,25 +758,32 @@ function MenuTrigger(props: MenuTriggerProps): ReactNode { default: placement = `${direction} ${align}` as Placement; } + let holdAffordance = trigger === 'longPress'; return ( - - - - - - {props.children} - - - - - + + + + + + + {props.children} + + + + + + ); } diff --git a/packages/@react-spectrum/s2/stories/Menu.stories.tsx b/packages/@react-spectrum/s2/stories/Menu.stories.tsx index 7ca50c777f9..18a5be6ccd5 100644 --- a/packages/@react-spectrum/s2/stories/Menu.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Menu.stories.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {ActionButton} from '../src/ActionButton'; import AlignLeft from '../s2wf-icons/S2_Icon_TextAlignLeft_20_N.svg'; import AlignMiddle from '../s2wf-icons/S2_Icon_TextAlignCenter_20_N.svg'; import AlignRight from '../s2wf-icons/S2_Icon_TextAlignRight_20_N.svg'; @@ -19,9 +20,7 @@ import {categorizeArgTypes, getActionArgs} from './utils'; import ClockPendingIcon from '../s2wf-icons/S2_Icon_ClockPending_20_N.svg'; import {CombinedMenu} from '../src/Menu'; import CommentTextIcon from '../s2wf-icons/S2_Icon_CommentText_20_N.svg'; - import CommunityIcon from '../s2wf-icons/S2_Icon_Community_20_N.svg'; - import {Content, Footer, Header, Heading, Keyboard, Text} from '../src/Content'; import {ContextualHelpPopover} from '../src/ContextualHelp'; import Copy from '../s2wf-icons/S2_Icon_Copy_20_N.svg'; @@ -48,6 +47,7 @@ import Paste from '../s2wf-icons/S2_Icon_Paste_20_N.svg'; import {ReactElement, useState} from 'react'; import {Selection} from '@react-types/shared'; import TextIcon from '../s2wf-icons/S2_Icon_Text_20_N.svg'; +import {ToggleButton} from '../src/ToggleButton'; import Underline from '../s2wf-icons/S2_Icon_TextUnderline_20_N.svg'; const events = ['onAction', 'onClose', 'onOpenChange', 'onScroll', 'onSelectionChange']; @@ -398,3 +398,23 @@ export const UnavailableMenuItem: Story = { ); } }; + +export const HoldAffordance: Story = { + render: args => ( +
+ + + + Action Button Cut + + + Cut + + +
+ ) +}; From d924bb873a95e58ebfdfcdcb8339630d399b18fa Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 13 May 2026 16:15:56 -0700 Subject: [PATCH 2/5] add toggle button support --- .../@react-spectrum/s2/src/ToggleButton.tsx | 37 ++++++++++++++++++- .../s2/stories/Menu.stories.tsx | 9 +++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/@react-spectrum/s2/src/ToggleButton.tsx b/packages/@react-spectrum/s2/src/ToggleButton.tsx index 9e90ffc3713..281f450eee8 100644 --- a/packages/@react-spectrum/s2/src/ToggleButton.tsx +++ b/packages/@react-spectrum/s2/src/ToggleButton.tsx @@ -13,9 +13,10 @@ import {ActionButtonStyleProps, btnStyles} from './ActionButton'; import {centerBaseline} from './CenterBaseline'; import {ContextValue, Provider, useSlottedContext} from 'react-aria-components/slots'; +import CornerTriangle from '../ui-icons/CornerTriangle'; import {createContext, forwardRef, ReactNode} from 'react'; import {FocusableRef, FocusableRefValue, GlobalDOMAttributes} from '@react-types/shared'; -import {fontRelative, style} from '../style' with {type: 'macro'}; +import {fontRelative, space, style} from '../style' with {type: 'macro'}; import {IconContext} from './Icon'; import {pressScale} from './pressScale'; import { @@ -28,6 +29,7 @@ import {Text, TextContext} from './Content'; import {ToggleButtonGroupContext} from './ToggleButtonGroup'; import {useFocusableRef} from './useDOMRef'; import {useFormProps} from './Form'; +import {useLocale} from 'react-aria/I18nProvider'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface ToggleButtonProps @@ -84,6 +86,8 @@ export const ToggleButton = forwardRef(function ToggleButton( size = props.size || 'M', isDisabled = props.isDisabled } = ctx || {}; + let {holdAffordance} = props; + let {direction} = useLocale(); return ( {typeof props.children === 'string' ? {props.children} : props.children} + {holdAffordance && ( + + )} ); }); diff --git a/packages/@react-spectrum/s2/stories/Menu.stories.tsx b/packages/@react-spectrum/s2/stories/Menu.stories.tsx index 18a5be6ccd5..fa4251e6972 100644 --- a/packages/@react-spectrum/s2/stories/Menu.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Menu.stories.tsx @@ -415,6 +415,15 @@ export const HoldAffordance: Story = { Cut + + + + Toggle Button Cut + + + Cut + + ) }; From 880afd863b5f47c8af599eafe2f0c02eddf0bb48 Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 13 May 2026 16:32:14 -0700 Subject: [PATCH 3/5] chromatic stories --- .../s2/chromatic/ActionButton.stories.tsx | 34 +++++++++++++++++++ .../s2/chromatic/ToggleButton.stories.tsx | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/packages/@react-spectrum/s2/chromatic/ActionButton.stories.tsx b/packages/@react-spectrum/s2/chromatic/ActionButton.stories.tsx index 6e9c5f2be8e..f4ce1a02420 100644 --- a/packages/@react-spectrum/s2/chromatic/ActionButton.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ActionButton.stories.tsx @@ -15,6 +15,7 @@ import {ActionButton, ActionButtonProps} from '../src/ActionButton'; import {Avatar} from '../src/Avatar'; import BellIcon from '../s2wf-icons/S2_Icon_Bell_20_N.svg'; import CommentIcon from '../s2wf-icons/S2_Icon_Comment_20_N.svg'; +import Cut from '../s2wf-icons/S2_Icon_Cut_20_N.svg'; import {Fonts, NotificationBadges, UnsafeClassName} from '../stories/ActionButton.stories'; import {generatePowerset} from '@react-spectrum/story-utils'; import type {Meta, StoryObj} from '@storybook/react'; @@ -133,6 +134,39 @@ export const AvatarOnly: ActionButtonStory = { export {Fonts, UnsafeClassName, NotificationBadges}; +const sizes = ['XS', 'S', 'M', 'L', 'XL'] as const; + +export const HoldAffordance: ActionButtonStory = { + render: () => ( +
+ {sizes.map(size => ( +
+ + + + + Cut + + + + Cut + + + + + + Cut + + + + Cut + +
+ ))} +
+ ) +}; + export const NotificationBadgesCustomWidth: ActionButtonStory = { render: args => (
diff --git a/packages/@react-spectrum/s2/chromatic/ToggleButton.stories.tsx b/packages/@react-spectrum/s2/chromatic/ToggleButton.stories.tsx index 952a5def7fe..6daf766b4a3 100644 --- a/packages/@react-spectrum/s2/chromatic/ToggleButton.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ToggleButton.stories.tsx @@ -16,6 +16,7 @@ import { StaticColorDecorator, StaticColorProvider } from '../stories/utils'; +import Cut from '../s2wf-icons/S2_Icon_Cut_20_N.svg'; import {generatePowerset} from '@react-spectrum/story-utils'; import type {Meta, StoryObj} from '@storybook/react'; import NewIcon from '../s2wf-icons/S2_Icon_New_20_N.svg'; @@ -128,3 +129,36 @@ export const Truncate: StoryObj = {
) }; + +const sizes = ['XS', 'S', 'M', 'L', 'XL'] as const; + +export const HoldAffordance: StoryObj = { + render: () => ( +
+ {sizes.map(size => ( +
+ + + + + Cut + + + + Cut + + + + + + Cut + + + + Cut + +
+ ))} +
+ ) +}; From 899e2671cf58a48cccbd21dd428eb34cbc67dada Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Wed, 13 May 2026 17:11:41 -0700 Subject: [PATCH 4/5] missed the fact that trigger already existed as a prop... --- packages/@react-spectrum/s2/src/Menu.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 0a07dbda111..34585c51a1b 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -10,6 +10,7 @@ * governing permissions and limitations under the License. */ +import {ActionButtonContext} from './ActionButton'; import { Menu as AriaMenu, MenuItem as AriaMenuItem, @@ -23,7 +24,6 @@ import { SubmenuTriggerProps as AriaSubmenuTriggerProps, MenuItemRenderProps } from 'react-aria-components/Menu'; - import { baseColor, centerPadding, @@ -61,23 +61,22 @@ import {edgeToText} from '../style/spectrum-theme' with {type: 'macro'}; import {forwardRefType} from './types'; import {HeaderContext, HeadingContext, KeyboardContext, Text, TextContext} from './Content'; import {IconContext} from './Icon'; -import {ImageContext} from './Image'; -import InfoCircleIcon from '../s2wf-icons/S2_Icon_InfoCircle_20_N.svg'; // chevron right removed?? +import {ImageContext} from './Image'; // chevron right removed?? +import InfoCircleIcon from '../s2wf-icons/S2_Icon_InfoCircle_20_N.svg'; import {InPopoverContext, Popover, PopoverContext} from './Popover'; -import intlMessages from '../intl/*.json'; // @ts-ignore +import intlMessages from '../intl/*.json'; import LinkOutIcon from '../ui-icons/LinkOut'; import {mergeStyles} from '../style/runtime'; import {Placement} from 'react-aria/useOverlayPosition'; import {PressResponder} from 'react-aria/private/interactions/PressResponder'; import {pressScale} from './pressScale'; import {Separator, SeparatorProps} from 'react-aria-components/Separator'; +import {ToggleButtonContext} from './ToggleButton'; import {useGlobalListeners} from 'react-aria/private/utils/useGlobalListeners'; import {useId} from 'react-aria/useId'; import {useLocale} from 'react-aria/I18nProvider'; import {useLocalizedStringFormatter} from 'react-aria/useLocalizedStringFormatter'; -import {ActionButtonContext} from './ActionButton'; -import {ToggleButtonContext} from './ToggleButton'; import {useSpectrumContextProps} from './useSpectrumContextProps'; // viewbox on LinkOut is super weird just because i copied the icon from designs... // need to strip id's from icons @@ -101,12 +100,6 @@ export interface MenuTriggerProps extends AriaMenuTriggerProps { * @default true */ shouldFlip?: boolean; - /** - * How the menu is triggered. - * - * @default 'press' - */ - trigger?: 'press' | 'longPress'; } export interface MenuProps From 20baef5c94eb4c797683a21575ec75d1b33e330c Mon Sep 17 00:00:00 2001 From: Daniel Lu Date: Thu, 14 May 2026 10:55:54 -0700 Subject: [PATCH 5/5] add tests and review comments --- .../s2/chromatic/ActionButton.stories.tsx | 57 +++++++-------- .../s2/chromatic/ToggleButton.stories.tsx | 56 ++++++++------- .../@react-spectrum/s2/src/ActionButton.tsx | 13 ++-- .../@react-spectrum/s2/src/ToggleButton.tsx | 10 +-- .../s2/stories/Menu.stories.tsx | 26 ++++--- .../@react-spectrum/s2/test/Menu.test.tsx | 70 +++++++++++++++++-- 6 files changed, 153 insertions(+), 79 deletions(-) diff --git a/packages/@react-spectrum/s2/chromatic/ActionButton.stories.tsx b/packages/@react-spectrum/s2/chromatic/ActionButton.stories.tsx index f4ce1a02420..fa75a34da09 100644 --- a/packages/@react-spectrum/s2/chromatic/ActionButton.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ActionButton.stories.tsx @@ -10,8 +10,7 @@ * governing permissions and limitations under the License. */ -import {ActionButton, ActionButtonProps} from '../src/ActionButton'; - +import {ActionButton, ActionButtonContext, ActionButtonProps} from '../src/ActionButton'; import {Avatar} from '../src/Avatar'; import BellIcon from '../s2wf-icons/S2_Icon_Bell_20_N.svg'; import CommentIcon from '../s2wf-icons/S2_Icon_Comment_20_N.svg'; @@ -138,32 +137,34 @@ const sizes = ['XS', 'S', 'M', 'L', 'XL'] as const; export const HoldAffordance: ActionButtonStory = { render: () => ( -
- {sizes.map(size => ( -
- - - - - Cut - - - - Cut - - - - - - Cut - - - - Cut - -
- ))} -
+ +
+ {sizes.map(size => ( +
+ + + + + Cut + + + + Cut + + + + + + Cut + + + + Cut + +
+ ))} +
+
) }; diff --git a/packages/@react-spectrum/s2/chromatic/ToggleButton.stories.tsx b/packages/@react-spectrum/s2/chromatic/ToggleButton.stories.tsx index 6daf766b4a3..0064f0bcb80 100644 --- a/packages/@react-spectrum/s2/chromatic/ToggleButton.stories.tsx +++ b/packages/@react-spectrum/s2/chromatic/ToggleButton.stories.tsx @@ -24,7 +24,7 @@ import {ReactElement} from 'react'; import {shortName} from './utils'; import {style} from '../style' with {type: 'macro'}; import {Text} from '../src/Content'; -import {ToggleButton, ToggleButtonProps} from '../src/ToggleButton'; +import {ToggleButton, ToggleButtonContext, ToggleButtonProps} from '../src/ToggleButton'; const events = ['onPress', 'onPressChange', 'onPressEnd', 'onPressStart', 'onPressUp', 'onChange']; @@ -134,31 +134,33 @@ const sizes = ['XS', 'S', 'M', 'L', 'XL'] as const; export const HoldAffordance: StoryObj = { render: () => ( -
- {sizes.map(size => ( -
- - - - - Cut - - - - Cut - - - - - - Cut - - - - Cut - -
- ))} -
+ +
+ {sizes.map(size => ( +
+ + + + + Cut + + + + Cut + + + + + + Cut + + + + Cut + +
+ ))} +
+
) }; diff --git a/packages/@react-spectrum/s2/src/ActionButton.tsx b/packages/@react-spectrum/s2/src/ActionButton.tsx index a80d4839ec0..dc8176a3958 100644 --- a/packages/@react-spectrum/s2/src/ActionButton.tsx +++ b/packages/@react-spectrum/s2/src/ActionButton.tsx @@ -63,8 +63,6 @@ export interface ActionButtonStyleProps { * style](https://spectrum.adobe.com/page/action-button/#Quiet). */ isQuiet?: boolean; - /** @private */ - holdAffordance?: boolean; } interface ToggleButtonStyleProps { @@ -318,10 +316,12 @@ const avatarSize: Record, number> = XL: 26 } as const; +interface ActionButtonContextProps extends Partial { + holdAffordance?: boolean; +} + export const ActionButtonContext = - createContext, FocusableRefValue>>( - null - ); + createContext>>(null); /** * ActionButtons allow users to perform an action. They're used for similar, task-based options @@ -335,7 +335,7 @@ export const ActionButton = forwardRef(function ActionButton( [props, ref] = useSpectrumContextProps(props, ref, ActionButtonContext); props = useFormProps(props as any); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); - let {isPending = false} = props; + let {isPending = false, holdAffordance} = props as ActionButtonContextProps; let domRef = useFocusableRef(ref); let overlayTriggerState = useContext(OverlayTriggerStateContext); let ctx = useSlottedContext(ActionButtonGroupContext); @@ -351,7 +351,6 @@ export const ActionButton = forwardRef(function ActionButton( } = ctx || {}; let {isProgressVisible} = usePendingState(isPending); - let {holdAffordance} = props; let {direction} = useLocale(); return ( diff --git a/packages/@react-spectrum/s2/src/ToggleButton.tsx b/packages/@react-spectrum/s2/src/ToggleButton.tsx index 281f450eee8..b9bc5bfaf2f 100644 --- a/packages/@react-spectrum/s2/src/ToggleButton.tsx +++ b/packages/@react-spectrum/s2/src/ToggleButton.tsx @@ -58,10 +58,12 @@ export interface ToggleButtonProps isEmphasized?: boolean; } +interface ToggleButtonContextProps extends Partial { + holdAffordance?: boolean; +} + export const ToggleButtonContext = - createContext, FocusableRefValue>>( - null - ); + createContext>>(null); /** * ToggleButtons allow users to toggle a selection on or off, for example @@ -86,7 +88,7 @@ export const ToggleButton = forwardRef(function ToggleButton( size = props.size || 'M', isDisabled = props.isDisabled } = ctx || {}; - let {holdAffordance} = props; + let {holdAffordance} = props as ToggleButtonContextProps; let {direction} = useLocale(); return ( diff --git a/packages/@react-spectrum/s2/stories/Menu.stories.tsx b/packages/@react-spectrum/s2/stories/Menu.stories.tsx index fa4251e6972..d80f78d5d8d 100644 --- a/packages/@react-spectrum/s2/stories/Menu.stories.tsx +++ b/packages/@react-spectrum/s2/stories/Menu.stories.tsx @@ -24,6 +24,8 @@ import CommunityIcon from '../s2wf-icons/S2_Icon_Community_20_N.svg'; import {Content, Footer, Header, Heading, Keyboard, Text} from '../src/Content'; import {ContextualHelpPopover} from '../src/ContextualHelp'; import Copy from '../s2wf-icons/S2_Icon_Copy_20_N.svg'; +import Crop from '../s2wf-icons/S2_Icon_Crop_20_N.svg'; +import CropRotate from '../s2wf-icons/S2_Icon_CropRotate_20_N.svg'; import Cut from '../s2wf-icons/S2_Icon_Cut_20_N.svg'; import DeviceDesktopIcon from '../s2wf-icons/S2_Icon_DeviceDesktop_20_N.svg'; import DeviceTabletIcon from '../s2wf-icons/S2_Icon_DeviceTablet_20_N.svg'; @@ -46,6 +48,7 @@ import NewIcon from '../s2wf-icons/S2_Icon_New_20_N.svg'; import Paste from '../s2wf-icons/S2_Icon_Paste_20_N.svg'; import {ReactElement, useState} from 'react'; import {Selection} from '@react-types/shared'; +import StampClone from '../s2wf-icons/S2_Icon_StampClone_20_N.svg'; import TextIcon from '../s2wf-icons/S2_Icon_Text_20_N.svg'; import {ToggleButton} from '../src/ToggleButton'; import Underline from '../s2wf-icons/S2_Icon_TextUnderline_20_N.svg'; @@ -407,21 +410,26 @@ export const HoldAffordance: Story = { gap: 8 }}> - - - Action Button Cut - + Copy - Cut + Copy as plain text + Copy as rich text + Copy URL - - - Toggle Button Cut + + - Cut + + + Crop Rotate + + + + Clone Stamp + diff --git a/packages/@react-spectrum/s2/test/Menu.test.tsx b/packages/@react-spectrum/s2/test/Menu.test.tsx index 688d3fd46af..1d6a4706bad 100644 --- a/packages/@react-spectrum/s2/test/Menu.test.tsx +++ b/packages/@react-spectrum/s2/test/Menu.test.tsx @@ -10,12 +10,19 @@ * governing permissions and limitations under the License. */ +import { + act, + installPointerEvent, + pointerMap, + render, + User +} from '@react-spectrum/test-utils-internal'; +import {ActionButton} from '../src/ActionButton'; import {AriaMenuTests} from '../../../react-aria-components/test/AriaMenu.test-util'; import {Button} from '../src/Button'; import {Collection} from 'react-aria/Collection'; import {Content, Header, Heading} from '../src/Content'; import {ContextualHelpPopover} from '../src/ContextualHelp'; - import { Menu, MenuItem, @@ -24,11 +31,9 @@ import { SubmenuTrigger, UnavailableMenuItemTrigger } from '../src/Menu'; - -import {pointerMap} from '@react-aria/test-utils'; import React from 'react'; -import {render} from '@react-spectrum/test-utils-internal'; import {Selection} from '@react-types/shared'; +import {ToggleButton} from '../src/ToggleButton'; import userEvent from '@testing-library/user-event'; // better to accept items from the test? or just have the test have a requirement that you render a certain-ish structure? @@ -140,6 +145,63 @@ describe('Menu unavailable', () => { }); }); +describe('long press support', function () { + let testUtilUser = new User({advanceTimer: jest.advanceTimersByTime}); + let user; + installPointerEvent(); + + beforeAll(function () { + user = userEvent.setup({delay: null, pointerMap}); + jest.useFakeTimers(); + }); + + afterEach(() => { + act(() => jest.runAllTimers()); + jest.clearAllMocks(); + }); + + it('should open the menu on longPress (ActionButton)', async function () { + let {getByRole} = render( + + Menu Button + + Cut + + + ); + + let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')}); + await user.click(menuTester.trigger); + act(() => { + jest.runAllTimers(); + }); + expect(menuTester.menu).toBeFalsy(); + await menuTester.open({needsLongPress: true}); + expect(menuTester.menu).toBeTruthy(); + }); + + it('should open the menu on longPress (ToggleButton)', async function () { + let {getByRole} = render( + + Menu Button + + Cut + + + ); + + let menuTester = testUtilUser.createTester('Menu', {root: getByRole('button')}); + await user.click(menuTester.trigger); + act(() => { + jest.runAllTimers(); + }); + expect(menuTester.menu).toBeFalsy(); + expect(menuTester.trigger).toHaveAttribute('data-selected', 'true'); + await menuTester.open({needsLongPress: true}); + expect(menuTester.menu).toBeTruthy(); + }); +}); + AriaMenuTests({ prefix: 'spectrum2-static', renderers: {