diff --git a/packages/@react-aria/overlays/src/calculatePosition.ts b/packages/@react-aria/overlays/src/calculatePosition.ts index 160de612c72..e9696c666e0 100644 --- a/packages/@react-aria/overlays/src/calculatePosition.ts +++ b/packages/@react-aria/overlays/src/calculatePosition.ts @@ -110,7 +110,11 @@ function getContainerDimensions(containerNode: Element): Dimensions { let scroll: Position = {}; let isPinchZoomedIn = (visualViewport?.scale ?? 1) > 1; - if (containerNode.tagName === 'BODY') { + // In the case where the container is `html` or `body` and the container doesn't have something like `position: relative`, + // then position absolute will be positioned relative to the viewport, also known as the `initial containing block`. + // That's why we use the visual viewport instead. + + if (containerNode.tagName === 'BODY' || containerNode.tagName === 'HTML') { let documentElement = document.documentElement; totalWidth = documentElement.clientWidth; totalHeight = documentElement.clientHeight; @@ -179,10 +183,13 @@ function getDelta( let boundarySize = boundaryDimensions[AXIS_SIZE[axis]]; // Calculate the edges of the boundary (accomodating for the boundary padding) and the edges of the overlay. // Note that these values are with respect to the visual viewport (aka 0,0 is the top left of the viewport) - let boundaryStartEdge = boundaryDimensions.scroll[AXIS[axis]] + padding; - let boundaryEndEdge = boundarySize + boundaryDimensions.scroll[AXIS[axis]] - padding; - let startEdgeOffset = offset - containerScroll + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]]; - let endEdgeOffset = offset - containerScroll + size + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]]; + + let boundaryStartEdge = containerOffsetWithBoundary[axis] + boundaryDimensions.scroll[AXIS[axis]] + padding; + let boundaryEndEdge = containerOffsetWithBoundary[axis] + boundaryDimensions.scroll[AXIS[axis]] + boundarySize - padding; + // transformed value of the left edge of the overlay + let startEdgeOffset = offset - containerScroll + boundaryDimensions.scroll[AXIS[axis]] + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]]; + // transformed value of the right edge of the overlay + let endEdgeOffset = offset - containerScroll + size + boundaryDimensions.scroll[AXIS[axis]] + containerOffsetWithBoundary[axis] - boundaryDimensions[AXIS[axis]]; // If any of the overlay edges falls outside of the boundary, shift the overlay the required amount to align one of the overlay's // edges with the closest boundary edge. @@ -234,7 +241,8 @@ function computePosition( containerOffsetWithBoundary: Offset, isContainerPositioned: boolean, arrowSize: number, - arrowBoundaryOffset: number + arrowBoundaryOffset: number, + containerDimensions: Dimensions ) { let {placement, crossPlacement, axis, crossAxis, size, crossSize} = placementInfo; let position: Position = {}; @@ -255,9 +263,9 @@ function computePosition( position[crossAxis]! += crossOffset; - // overlay top overlapping arrow with button bottom + // overlay top or left overlapping arrow with button bottom or right const minPosition = childOffset[crossAxis] - overlaySize[crossSize] + arrowSize + arrowBoundaryOffset; - // overlay bottom overlapping arrow with button top + // overlay bottom or right overlapping arrow with button top or left const maxPosition = childOffset[crossAxis] + childOffset[crossSize] - arrowSize - arrowBoundaryOffset; position[crossAxis] = clamp(position[crossAxis]!, minPosition, maxPosition); @@ -266,8 +274,8 @@ function computePosition( // If the container is positioned (non-static), then we use the container's actual // height, as `bottom` will be relative to this height. But if the container is static, // then it can only be the `document.body`, and `bottom` will be relative to _its_ - // container, which should be as large as boundaryDimensions. - const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : boundaryDimensions[TOTAL_SIZE[size]]); + // container. + const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary[size] : containerDimensions[TOTAL_SIZE[size]]); position[FLIPPED_DIRECTION[axis]] = Math.floor(containerHeight - childOffset[axis] + offset); } else { position[axis] = Math.floor(childOffset[axis] + childOffset[size] + offset); @@ -283,42 +291,55 @@ function getMaxHeight( margins: Position, padding: number, overlayHeight: number, - heightGrowthDirection: HeightGrowthDirection + heightGrowthDirection: HeightGrowthDirection, + containerDimensions: Dimensions ) { - const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : boundaryDimensions[TOTAL_SIZE.height]); - // For cases where position is set via "bottom" instead of "top", we need to calculate the true overlay top with respect to the boundary. Reverse calculate this with the same method - // used in computePosition. - let overlayTop = position.top != null ? containerOffsetWithBoundary.top + position.top : containerOffsetWithBoundary.top + (containerHeight - (position.bottom ?? 0) - overlayHeight); + const containerHeight = (isContainerPositioned ? containerOffsetWithBoundary.height : containerDimensions[TOTAL_SIZE.height]); + // For cases where position is set via "bottom" instead of "top", we need to calculate the true overlay top + // with respect to the container. + let overlayTop = position.top != null ? position.top : (containerHeight - (position.bottom ?? 0) - overlayHeight); let maxHeight = heightGrowthDirection !== 'top' ? // We want the distance between the top of the overlay to the bottom of the boundary Math.max(0, - (boundaryDimensions.height + boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the bottom of the boundary + (boundaryDimensions.height + boundaryDimensions.top) // this is the bottom of the boundary - overlayTop // this is the top of the overlay - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding ) // We want the distance between the bottom of the overlay to the top of the boundary : Math.max(0, (overlayTop + overlayHeight) // this is the bottom of the overlay - - (boundaryDimensions.top + (boundaryDimensions.scroll.top ?? 0)) // this is the top of the boundary + - boundaryDimensions.top // this is the top of the boundary - ((margins.top ?? 0) + (margins.bottom ?? 0) + padding) // save additional space for margin and padding ); return Math.min(boundaryDimensions.height - (padding * 2), maxHeight); } function getAvailableSpace( - boundaryDimensions: Dimensions, + boundaryDimensions: Dimensions, // boundary containerOffsetWithBoundary: Offset, - childOffset: Offset, - margins: Position, - padding: number, + childOffset: Offset, // trigger + margins: Position, // overlay + padding: number, // overlay <-> boundary placementInfo: ParsedPlacement ) { let {placement, axis, size} = placementInfo; if (placement === axis) { - return Math.max(0, childOffset[axis] - boundaryDimensions[axis] - (boundaryDimensions.scroll[axis] ?? 0) + containerOffsetWithBoundary[axis] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding); + return Math.max(0, + childOffset[axis] // trigger start + - boundaryDimensions[axis] // boundary start + - (margins[axis] ?? 0) // margins usually for arrows or other decorations + - margins[FLIPPED_DIRECTION[axis]] + - padding); // padding between overlay and boundary } - return Math.max(0, boundaryDimensions[size] + boundaryDimensions[axis] + boundaryDimensions.scroll[axis] - containerOffsetWithBoundary[axis] - childOffset[axis] - childOffset[size] - (margins[axis] ?? 0) - margins[FLIPPED_DIRECTION[axis]] - padding); + return Math.max(0, + boundaryDimensions[size] + + boundaryDimensions[axis] + - childOffset[axis] + - childOffset[size] + - (margins[axis] ?? 0) + - margins[FLIPPED_DIRECTION[axis]] + - padding); } export function calculatePositionInternal( @@ -341,7 +362,7 @@ export function calculatePositionInternal( ): PositionResult { let placementInfo = parsePlacement(placementInput); let {size, crossAxis, crossSize, placement, crossPlacement} = placementInfo; - let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset); + let position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset, containerDimensions); let normalizedOffset = offset; let space = getAvailableSpace( boundaryDimensions, @@ -355,7 +376,8 @@ export function calculatePositionInternal( // Check if the scroll size of the overlay is greater than the available space to determine if we need to flip if (flip && scrollSize[size] > space) { let flippedPlacementInfo = parsePlacement(`${FLIPPED_DIRECTION[placement]} ${crossPlacement}` as Placement); - let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset); + let flippedPosition = computePosition(childOffset, boundaryDimensions, overlaySize, flippedPlacementInfo, offset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset, containerDimensions); + let flippedSpace = getAvailableSpace( boundaryDimensions, containerOffsetWithBoundary, @@ -400,7 +422,8 @@ export function calculatePositionInternal( margins, padding, overlaySize.height, - heightGrowthDirection + heightGrowthDirection, + containerDimensions ); if (userSetMaxHeight && userSetMaxHeight < maxHeight) { @@ -409,7 +432,7 @@ export function calculatePositionInternal( overlaySize.height = Math.min(overlaySize.height, maxHeight); - position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset); + position = computePosition(childOffset, boundaryDimensions, overlaySize, placementInfo, normalizedOffset, crossOffset, containerOffsetWithBoundary, isContainerPositioned, arrowSize, arrowBoundaryOffset, containerDimensions); delta = getDelta(crossAxis, position[crossAxis]!, overlaySize[crossSize], boundaryDimensions, containerDimensions, padding, containerOffsetWithBoundary); position[crossAxis]! += delta; @@ -507,7 +530,7 @@ export function calculatePosition(opts: PositionOpts): PositionResult { // If the container is the HTML element wrapping the body element, the retrieved scrollTop/scrollLeft will be equal to the // body element's scroll. Set the container's scroll values to 0 since the overlay's edge position value in getDelta don't then need to be further offset // by the container scroll since they are essentially the same containing element and thus in the same coordinate system - let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container, false) : getPosition(container, boundaryElement, false); + let containerOffsetWithBoundary: Offset = boundaryElement.tagName === 'BODY' ? getOffset(container, false) : getPosition(boundaryElement, container, false); if (container.tagName === 'HTML' && boundaryElement.tagName === 'BODY') { containerDimensions.scroll.top = 0; containerDimensions.scroll.left = 0; @@ -536,7 +559,7 @@ export function calculatePosition(opts: PositionOpts): PositionResult { export function getRect(node: Element, ignoreScale: boolean) { let {top, left, width, height} = node.getBoundingClientRect(); - // Use offsetWidth and offsetHeight if this is an HTML element, so that + // Use offsetWidth and offsetHeight if this is an HTML element, so that // the size is not affected by scale transforms. if (ignoreScale && node instanceof node.ownerDocument.defaultView!.HTMLElement) { width = node.offsetWidth; diff --git a/packages/@react-aria/overlays/test/calculatePosition.test.ts b/packages/@react-aria/overlays/test/calculatePosition.test.ts index 96907043448..5eea87941b5 100644 --- a/packages/@react-aria/overlays/test/calculatePosition.test.ts +++ b/packages/@react-aria/overlays/test/calculatePosition.test.ts @@ -138,13 +138,16 @@ describe('calculatePosition', function () { }; const container = createElementWithDimensions('div', containerDimensions); + Object.assign(container.style, { + position: 'relative' + }); const target = createElementWithDimensions('div', targetDimension); const overlay = createElementWithDimensions('div', overlaySize, margins); const parentElement = document.createElement('div'); parentElement.appendChild(container); parentElement.appendChild(target); - parentElement.appendChild(overlay); + container.appendChild(overlay); document.documentElement.appendChild(parentElement); diff --git a/packages/@react-aria/overlays/test/useOverlayPosition.test.tsx b/packages/@react-aria/overlays/test/useOverlayPosition.test.tsx index c81c34a94c1..f7f64969d66 100644 --- a/packages/@react-aria/overlays/test/useOverlayPosition.test.tsx +++ b/packages/@react-aria/overlays/test/useOverlayPosition.test.tsx @@ -89,7 +89,7 @@ describe('useOverlayPosition', function () { position: absolute; z-index: 100000; left: 12px; - bottom: 518px; + bottom: 350px; max-height: 238px; `); @@ -281,7 +281,7 @@ describe('useOverlayPosition with positioned container', () => { z-index: 100000; left: 12px; top: 200px; - max-height: 406px; + max-height: 556px; `); expect(overlay).toHaveTextContent('placement: bottom'); @@ -303,7 +303,7 @@ describe('useOverlayPosition with positioned container', () => { z-index: 100000; left: 12px; bottom: 300px; - max-height: 238px; + max-height: 88px; `); expect(overlay).toHaveTextContent('placement: top'); diff --git a/packages/@react-spectrum/menu/chromatic/Submenu.stories.tsx b/packages/@react-spectrum/menu/chromatic/Submenu.stories.tsx index 096f91f23a3..74ec34977c0 100644 --- a/packages/@react-spectrum/menu/chromatic/Submenu.stories.tsx +++ b/packages/@react-spectrum/menu/chromatic/Submenu.stories.tsx @@ -143,6 +143,9 @@ Default.play = async ({canvasElement}) => { let body = canvasElement.ownerDocument.body; let menu = await within(body).getByRole('menu'); let menuItems = within(menu).getAllByRole('menuitem'); + + // clean up any previous click state + await userEvent.click(document.body); await userEvent.hover(menuItems[0]); let submenuTrigger = await within(body).findByText('Baseline'); await userEvent.hover(submenuTrigger); @@ -166,6 +169,9 @@ Mobile.play = async ({canvasElement}) => { let body = canvasElement.ownerDocument.body; let menu = await within(body).getByRole('menu'); let menuItems = within(menu).getAllByRole('menuitem'); + + // clean up any previous click state + await userEvent.click(document.body); await userEvent.click(menuItems[0]); await within(body).findByText('Baseline'); }; diff --git a/packages/@react-spectrum/s2/src/ActionButton.tsx b/packages/@react-spectrum/s2/src/ActionButton.tsx index d944e2fa2c5..09f390349d3 100644 --- a/packages/@react-spectrum/s2/src/ActionButton.tsx +++ b/packages/@react-spectrum/s2/src/ActionButton.tsx @@ -19,12 +19,18 @@ import {control, getAllowedOverrides, staticColor, StyleProps} from './style-uti import {createContext, forwardRef, ReactNode, useContext} from 'react'; import {FocusableRef, FocusableRefValue, GlobalDOMAttributes} from '@react-types/shared'; import {IconContext} from './Icon'; +import {ImageContext} from './Image'; +// @ts-ignore +import intlMessages from '../intl/*.json'; import {NotificationBadgeContext} from './NotificationBadge'; import {pressScale} from './pressScale'; +import {ProgressCircle} from './ProgressCircle'; import {SkeletonContext} from './Skeleton'; import {Text, TextContext} from './Content'; import {useFocusableRef} from '@react-spectrum/utils'; import {useFormProps} from './Form'; +import {useLocalizedStringFormatter} from '@react-aria/i18n'; +import {usePendingState} from './Button'; import {useSpectrumContextProps} from './useSpectrumContextProps'; export interface ActionButtonStyleProps { @@ -53,7 +59,7 @@ interface ActionGroupItemStyleProps { isJustified?: boolean } -export interface ActionButtonProps extends Omit, StyleProps, ActionButtonStyleProps { +export interface ActionButtonProps extends Omit, StyleProps, ActionButtonStyleProps { /** The content to display in the ActionButton. */ children: ReactNode } @@ -67,6 +73,7 @@ export const btnStyles = style) { [props, ref] = useSpectrumContextProps(props, ref, ActionButtonContext); props = useFormProps(props as any); + let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); + let {isPending = false} = props; let domRef = useFocusableRef(ref); let overlayTriggerState = useContext(OverlayTriggerStateContext); let ctx = useSlottedContext(ActionButtonGroupContext); @@ -262,21 +271,21 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton orientation = 'horizontal', staticColor = props.staticColor, isQuiet = props.isQuiet, - size = props.size || 'M', - isDisabled = props.isDisabled + size = props.size || 'M' } = ctx || {}; + let {isProgressVisible} = usePendingState(isPending); return ( (props.UNSAFE_className || '') + btnStyles({ ...renderProps, // Retain hover styles when an overlay is open. isHovered: renderProps.isHovered || overlayTriggerState?.isOpen || false, + isDisabled: renderProps.isDisabled || isProgressVisible, staticColor, isStaticColor: !!staticColor, size, @@ -286,34 +295,92 @@ export const ActionButton = forwardRef(function ActionButton(props: ActionButton orientation, isInGroup }, props.styles)}> - - {typeof props.children === 'string' ? {props.children} : props.children} - + {({isDisabled}) => ( + <> + + {typeof props.children === 'string' ? {props.children} : props.children} + {isPending && +
+ +
+ } +
+ + )}
); }); diff --git a/packages/@react-spectrum/s2/src/Button.tsx b/packages/@react-spectrum/s2/src/Button.tsx index 3c2afe3bb7a..d7d28f8652d 100644 --- a/packages/@react-spectrum/s2/src/Button.tsx +++ b/packages/@react-spectrum/s2/src/Button.tsx @@ -292,6 +292,24 @@ const gradient = style({ } }); +export function usePendingState(isPending: boolean) { + let [isProgressVisible, setIsProgressVisible] = useState(false); + useEffect(() => { + let timeout: ReturnType; + if (isPending) { + timeout = setTimeout(() => { + setIsProgressVisible(true); + }, 1000); + } else { + setIsProgressVisible(false); + } + return () => { + clearTimeout(timeout); + }; + }, [isPending]); + return {isProgressVisible}; +} + /** * Buttons allow users to perform an action. * They have multiple styles for various needs, and are ideal for calling attention to @@ -302,7 +320,7 @@ export const Button = forwardRef(function Button(props: ButtonProps, ref: Focusa props = useFormProps(props); let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-spectrum/s2'); let { - isPending, + isPending = false, variant = 'primary', fillStyle = 'fill', size = 'M', @@ -311,24 +329,7 @@ export const Button = forwardRef(function Button(props: ButtonProps, ref: Focusa let domRef = useFocusableRef(ref); let overlayTriggerState = useContext(OverlayTriggerStateContext); - let [isProgressVisible, setIsProgressVisible] = useState(false); - useEffect(() => { - let timeout: ReturnType; - - if (isPending) { - // Start timer when isPending is set to true. - timeout = setTimeout(() => { - setIsProgressVisible(true); - }, 1000); - } else { - // Exit loading state when isPending is set to false. */ - setIsProgressVisible(false); - } - return () => { - // Clean up on unmount or when user removes isPending prop before entering loading state. - clearTimeout(timeout); - }; - }, [isPending]); + let {isProgressVisible} = usePendingState(isPending); return ( extends StyleProps, Omit, keyof GlobalDOMAttributes | 'layout' | 'dragAndDropHooks' | 'dependencies' | 'renderEmptyState' | 'children' | 'onAction' | 'shouldFocusOnHover' | 'selectionBehavior' | 'shouldSelectOnPressUp' | 'shouldFocusWrap' | 'style' | 'className'> { @@ -293,7 +294,7 @@ const gridStyles = style<{orientation?: Orientation}>({ */ export function SelectBox(props: SelectBoxProps): ReactNode { let {children, isDisabled: individualDisabled = false, UNSAFE_style, UNSAFE_className, styles, ...otherProps} = props; - + let { orientation = 'vertical', isDisabled: groupDisabled = false @@ -301,6 +302,7 @@ export function SelectBox(props: SelectBoxProps): ReactNode { const isDisabled = individualDisabled || groupDisabled; const ref = useRef(null); + let {isFocusVisible} = useFocusVisible(); return ( (UNSAFE_className || '') + selectBoxStyles({ ...renderProps, + isFocusVisible: isFocusVisible && renderProps.isFocused, orientation }, styles)} style={pressScale(ref, UNSAFE_style)} @@ -315,7 +318,7 @@ export function SelectBox(props: SelectBoxProps): ReactNode { {({isSelected, isDisabled, isHovered}) => { return ( <> -
-
)} @@ -382,22 +385,17 @@ export const SelectBoxGroup = /*#__PURE__*/ forwardRef(function SelectBoxGroup { - const contextValue = { - orientation, - isDisabled - }; - return contextValue; - }, - [orientation, isDisabled] - ); + const selectBoxContextValue = useMemo(() => ({ + orientation, + isDisabled + }), [orientation, isDisabled]); return ( diff --git a/packages/@react-spectrum/s2/src/TableView.tsx b/packages/@react-spectrum/s2/src/TableView.tsx index 4ee43b7698d..6057c98d6e6 100644 --- a/packages/@react-spectrum/s2/src/TableView.tsx +++ b/packages/@react-spectrum/s2/src/TableView.tsx @@ -458,7 +458,8 @@ const cellFocus = { outlineOffset: -2, outlineWidth: 2, outlineColor: 'focus-ring', - borderRadius: '[6px]' + borderRadius: '[6px]', + pointerEvents: 'none' } as const; function CellFocusRing() { @@ -1036,8 +1037,8 @@ export const Cell = forwardRef(function Cell(props: CellProps, ref: DOMRef {({isFocusVisible}) => ( <> - {isFocusVisible && } {children} + {isFocusVisible && } )} diff --git a/packages/@react-spectrum/s2/test/SelectBoxGroup.test.tsx b/packages/@react-spectrum/s2/test/SelectBoxGroup.test.tsx index de029ab0b41..8fd18a33e3e 100644 --- a/packages/@react-spectrum/s2/test/SelectBoxGroup.test.tsx +++ b/packages/@react-spectrum/s2/test/SelectBoxGroup.test.tsx @@ -153,11 +153,7 @@ describe('SelectBoxGroup', () => { it('handles uncontrolled keyboard selection', async () => { render(); - const listbox = screen.getByRole('listbox'); - - await act(async () => { - listbox.focus(); - }); + await user.tab(); await act(async () => { await user.keyboard(' '); @@ -703,13 +699,10 @@ describe('SelectBoxGroup', () => { it('supports space key selection in uncontrolled mode', async () => { render(); - const listbox = screen.getByRole('listbox'); const option1 = screen.getByRole('option', {name: 'Option 1'}); - await act(async () => { - listbox.focus(); - await user.keyboard(' '); - }); + await user.tab(); + await user.keyboard(' '); expect(option1).toHaveAttribute('aria-selected', 'true'); }); @@ -730,10 +723,7 @@ describe('SelectBoxGroup', () => { ); - const listbox = screen.getByRole('listbox'); - await act(async () => { - listbox.focus(); - }); + await user.tab(); await user.keyboard('{ArrowDown}'); diff --git a/packages/react-aria-components/src/Button.tsx b/packages/react-aria-components/src/Button.tsx index cbe771d9730..1b1f0ad06bf 100644 --- a/packages/react-aria-components/src/Button.tsx +++ b/packages/react-aria-components/src/Button.tsx @@ -85,10 +85,10 @@ export const ButtonContext = createContext) { [props, ref] = useContextProps(props, ref, ButtonContext); - props = disablePendingProps(props); let ctx = props as ButtonContextValue; let {isPending} = ctx; let {buttonProps, isPressed} = useButton(props, ref); + buttonProps = useDisableInteractions(buttonProps, isPending); let {focusProps, isFocused, isFocusVisible} = useFocusRing(props); let {hoverProps, isHovered} = useHover({ ...props, @@ -161,18 +161,16 @@ export const Button = /*#__PURE__*/ createHideableComponent(function Button(prop ); }); -function disablePendingProps(props) { +function useDisableInteractions(props, isPending) { // Don't allow interaction while isPending is true - if (props.isPending) { - props.onPress = undefined; - props.onPressStart = undefined; - props.onPressEnd = undefined; - props.onPressChange = undefined; - props.onPressUp = undefined; - props.onKeyDown = undefined; - props.onKeyUp = undefined; - props.onClick = undefined; + if (isPending) { + for (const key in props) { + if (key.startsWith('on') && !(key.includes('Focus') || key.includes('Blur'))) { + props[key] = undefined; + } + } props.href = undefined; + props.target = undefined; } return props; } diff --git a/packages/react-aria-components/stories/Popover.stories.tsx b/packages/react-aria-components/stories/Popover.stories.tsx index 39440664563..6f4ba5e19cd 100644 --- a/packages/react-aria-components/stories/Popover.stories.tsx +++ b/packages/react-aria-components/stories/Popover.stories.tsx @@ -465,3 +465,56 @@ export const PopoverTriggerWidthExample: PopoverStory = () => ( ); + +function ScrollingBoundaryContainerExample(args) { + let [boundaryElem, setBoundaryElem] = useState(null); + return ( +
+
+ + + + + Should match the width of the trigger button + + + +
+
+ ); +} + +export const ScrollingBoundaryContainer: StoryObj = { + render: (args) => , + args: { + containerPadding: 0, + placement: 'bottom' + }, + argTypes: { + containerPadding: { + control: { + type: 'range', + min: 0, + max: 100 + } + }, + hideArrow: { + table: { + disable: true + } + }, + animation: { + table: { + disable: true + } + } + } +}; diff --git a/packages/react-aria-components/stories/Tree.stories.tsx b/packages/react-aria-components/stories/Tree.stories.tsx index 6a005514922..f086ed99b07 100644 --- a/packages/react-aria-components/stories/Tree.stories.tsx +++ b/packages/react-aria-components/stories/Tree.stories.tsx @@ -1247,7 +1247,6 @@ function generateTreeData(): Array { } const treeData = generateTreeData(); -console.log(`Total items: ${totalItems}`); function HugeVirtualizedTreeRender(args: TreeProps): JSX.Element { let [expandedKeys, setExpandedKeys] = useState(new Set()); diff --git a/packages/react-aria-components/test/Button.test.js b/packages/react-aria-components/test/Button.test.js index f9a44115122..3e95d797c58 100644 --- a/packages/react-aria-components/test/Button.test.js +++ b/packages/react-aria-components/test/Button.test.js @@ -11,7 +11,7 @@ */ import {act, pointerMap, render} from '@react-spectrum/test-utils-internal'; -import {Button, ButtonContext, ProgressBar, Text} from '../'; +import {Button, ButtonContext, Dialog, DialogTrigger, Heading, Modal, ProgressBar, Text} from '../'; import React, {useState} from 'react'; import userEvent from '@testing-library/user-event'; @@ -364,4 +364,35 @@ describe('Button', () => { await user.keyboard('{Enter}'); expect(onSubmitSpy).not.toHaveBeenCalled(); }); + + it('disables press when in pending state for context', async function () { + let onFocusSpy = jest.fn(); + let onBlurSpy = jest.fn(); + let {getByRole, queryByRole} = render( + + + + + {({close}) => ( + <> + Alert + + + )} + + + + ); + + let button = getByRole('button'); + await user.click(button); + expect(onFocusSpy).toHaveBeenCalled(); + expect(onBlurSpy).not.toHaveBeenCalled(); + + let dialog = queryByRole('alertdialog'); + expect(dialog).toBeNull(); + + await user.click(document.body); + expect(onBlurSpy).toHaveBeenCalled(); + }); });