diff --git a/packages/@react-spectrum/s2/src/Card.tsx b/packages/@react-spectrum/s2/src/Card.tsx index e90087ffc4e..e30599d1019 100644 --- a/packages/@react-spectrum/s2/src/Card.tsx +++ b/packages/@react-spectrum/s2/src/Card.tsx @@ -29,10 +29,14 @@ import {ImageContext} from './Image'; import {ImageCoordinator} from './ImageCoordinator'; import {inertValue} from 'react-aria/private/utils/inertValue'; import {Link} from 'react-aria-components/Link'; +import {mergeProps} from 'react-aria/mergeProps'; import {mergeStyles} from '../style/runtime'; import {pressScale} from './pressScale'; import {SkeletonContext, SkeletonWrapper, useIsSkeleton} from './Skeleton'; import {useDOMRef} from './useDOMRef'; +import {useFocusRing} from 'react-aria/useFocusRing'; +import {useHover} from 'react-aria/useHover'; +import {usePress} from 'react-aria/usePress'; import {useSpectrumContextProps} from './useSpectrumContextProps'; interface CardRenderProps { @@ -121,10 +125,12 @@ let card = style({ contain: 'layout', disableTapHighlight: true, userSelect: { - isCardView: 'none' + isCardView: 'none', + isInteractive: 'none' }, cursor: { - isLink: 'pointer' + isLink: 'pointer', + isInteractive: 'pointer' }, width: { size: { @@ -396,9 +402,51 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef { + onPress?.(e); + onAction?.(); + }, + onPressStart, + onPressEnd, + onPressChange, + onPressUp, + isDisabled: isDisabled || !isInteractiveStandalone + }); + let {hoverProps, isHovered: isInteractiveHovered} = useHover({ + isDisabled: isDisabled || !isInteractiveStandalone + }); + let {focusProps, isFocusVisible: isInteractiveFocusVisible} = useFocusRing(); + let children = ( + + {children} + + + ); + } + return (
{ + let user; + beforeAll(() => { + jest.useFakeTimers(); + user = userEvent.setup({delay: null, pointerMap}); + }); + + afterEach(() => { + jest.clearAllMocks(); + act(() => jest.runAllTimers()); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('renders as a plain div when no press callbacks are provided', () => { + let {getByText} = render( + + Static Card + + ); + let el = getByText('Static Card').closest('[class]')!.parentElement!; + expect(el.tagName).toBe('DIV'); + expect(el).not.toHaveAttribute('role'); + expect(el).not.toHaveAttribute('tabindex'); + }); + + it('renders as role=button and fires onPress when onPress is provided', async () => { + let onPress = jest.fn(); + let {getByRole} = render( + + Interactive Card + + ); + + let card = getByRole('button'); + expect(card).toBeInTheDocument(); + expect(card).toHaveAttribute('tabindex', '0'); + + await user.click(card); + expect(onPress).toHaveBeenCalledTimes(1); + }); + + it('fires onAction when onAction is provided', async () => { + let onAction = jest.fn(); + let {getByRole} = render( + + Action Card + + ); + + let card = getByRole('button'); + await user.click(card); + expect(onAction).toHaveBeenCalledTimes(1); + }); + + it('fires both onPress and onAction when both are provided', async () => { + let onPress = jest.fn(); + let onAction = jest.fn(); + let {getByRole} = render( + + Both Callbacks Card + + ); + + let card = getByRole('button'); + await user.click(card); + expect(onPress).toHaveBeenCalledTimes(1); + expect(onAction).toHaveBeenCalledTimes(1); + }); + + it('fires onPressStart and onPressEnd when provided', async () => { + let onPressStart = jest.fn(); + let onPressEnd = jest.fn(); + let {getByRole} = render( + + Press Events Card + + ); + + let card = getByRole('button'); + await user.click(card); + expect(onPressStart).toHaveBeenCalledTimes(1); + expect(onPressEnd).toHaveBeenCalledTimes(1); + }); + + it('does not fire press callbacks when disabled', async () => { + let onPress = jest.fn(); + let {getByRole} = render( + + Disabled Card + + ); + + let card = getByRole('button'); + expect(card).not.toHaveAttribute('tabindex'); + expect(card).toHaveAttribute('aria-disabled', 'true'); + + await user.click(card); + expect(onPress).not.toHaveBeenCalled(); + }); + + it('does not expose role=button when no press callbacks are provided', () => { + let {queryByRole} = render( + + Static Card + + ); + expect(queryByRole('button')).toBeNull(); + }); + + it('has pointer cursor and no user-select when interactive', async () => { + let onPress = jest.fn(); + let {getByRole} = render( + + Interactive Card + + ); + let card = getByRole('button'); + expect(card).toHaveStyle({cursor: 'pointer', userSelect: 'none'}); + }); +});