From 7d38a0c3657f45e481f028bbec35009b53013283 Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Thu, 14 Sep 2023 18:07:51 -0400 Subject: [PATCH 1/3] fix(popper): add start/end positioning with RTL support, update default --- .../react-core/src/helpers/Popper/Popper.tsx | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/react-core/src/helpers/Popper/Popper.tsx b/packages/react-core/src/helpers/Popper/Popper.tsx index 403e232dfd6..4f642abc471 100644 --- a/packages/react-core/src/helpers/Popper/Popper.tsx +++ b/packages/react-core/src/helpers/Popper/Popper.tsx @@ -45,6 +45,14 @@ const getOppositePlacement = (placement: Placement): any => export const getOpacityTransition = (animationDuration: number) => `opacity ${animationDuration}ms cubic-bezier(.54, 1.5, .38, 1.11)`; +export const getLanguageDirection = (targetElement: HTMLElement) => { + if (!targetElement) { + return 'ltr'; + } + + return getComputedStyle(targetElement).getPropertyValue('direction') as 'ltr' | 'rtl'; +}; + export interface PopperProps { /** * Trigger reference element to which the popper is relatively placed to. @@ -66,7 +74,7 @@ export interface PopperProps { /** popper direction */ direction?: 'up' | 'down'; /** popper position */ - position?: 'right' | 'left' | 'center'; + position?: 'right' | 'left' | 'center' | 'start' | 'end'; /** Instead of direction and position can set the placement of the popper */ placement?: Placement; /** Custom width of the popper. If the value is "trigger", it will set the width to the trigger element's width */ @@ -177,7 +185,7 @@ export const Popper: React.FunctionComponent = ({ trigger, popper, direction = 'down', - position = 'left', + position = 'start', placement, width, minWidth = 'trigger', @@ -226,6 +234,28 @@ export const Popper: React.FunctionComponent = ({ const refOrTrigger = refElement || triggerElement; const showPopper = isVisible || internalIsVisible; + const triggerParent = ((triggerRef as React.RefObject)?.current || triggerElement)?.parentElement; + const languageDirection = getLanguageDirection(triggerParent); + + const internalPosition = React.useMemo<'left' | 'right' | 'center'>(() => { + const fixedPositions = { left: 'left', right: 'right', center: 'center' }; + + const positionMap = { + ltr: { + start: 'left', + end: 'right', + ...fixedPositions + }, + rtl: { + start: 'right', + end: 'left', + ...fixedPositions + } + }; + + return positionMap[languageDirection][position] as 'left' | 'right' | 'center'; + }, [position, languageDirection]); + const onDocumentClickCallback = React.useCallback( (event: MouseEvent) => onDocumentClick(event, refOrTrigger, popperElement), [showPopper, triggerElement, refElement, popperElement, onDocumentClick] @@ -333,15 +363,15 @@ export const Popper: React.FunctionComponent = ({ return placement; } let convertedPlacement = direction === 'up' ? 'top' : 'bottom'; - if (position !== 'center') { - convertedPlacement = `${convertedPlacement}-${position === 'right' ? 'end' : 'start'}`; + if (internalPosition !== 'center') { + convertedPlacement = `${convertedPlacement}-${internalPosition === 'right' ? 'end' : 'start'}`; } return convertedPlacement as Placement; }; - const getPlacementMemo = React.useMemo(getPlacement, [direction, position, placement]); + const getPlacementMemo = React.useMemo(getPlacement, [direction, internalPosition, placement]); const getOppositePlacementMemo = React.useMemo( () => getOppositePlacement(getPlacement()), - [direction, position, placement] + [direction, internalPosition, placement] ); const widthMods: Modifier<'widthMods', {}> = React.useMemo( From a29627b063e13122a8454a4181d3b3e58cefc44f Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Fri, 15 Sep 2023 12:01:44 -0400 Subject: [PATCH 2/3] ensure that getLanguageDirection only returns ltr or rtl --- packages/react-core/src/helpers/Popper/Popper.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/react-core/src/helpers/Popper/Popper.tsx b/packages/react-core/src/helpers/Popper/Popper.tsx index 4f642abc471..b7ba8663470 100644 --- a/packages/react-core/src/helpers/Popper/Popper.tsx +++ b/packages/react-core/src/helpers/Popper/Popper.tsx @@ -46,11 +46,18 @@ export const getOpacityTransition = (animationDuration: number) => `opacity ${animationDuration}ms cubic-bezier(.54, 1.5, .38, 1.11)`; export const getLanguageDirection = (targetElement: HTMLElement) => { - if (!targetElement) { - return 'ltr'; + const defaultDirection = 'ltr'; + let direction = defaultDirection; + + if (targetElement) { + direction = getComputedStyle(targetElement).getPropertyValue('direction'); + } + + if (['ltr', 'rtl'].includes(direction)) { + return direction as 'ltr' | 'rtl'; } - return getComputedStyle(targetElement).getPropertyValue('direction') as 'ltr' | 'rtl'; + return defaultDirection; }; export interface PopperProps { From ec3b15b4d5d569cdf6a7d8ef5e352e5ee1de858f Mon Sep 17 00:00:00 2001 From: Austin Sullivan Date: Mon, 18 Sep 2023 16:02:16 -0400 Subject: [PATCH 3/3] fix(Popover, Tooltip): fix arrow positioning in RTL --- .../src/components/Popover/Popover.tsx | 15 +++++++ .../src/components/Tooltip/Tooltip.tsx | 15 +++++++ .../react-core/src/helpers/Popper/Popper.tsx | 40 +++++++++++++++++-- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/react-core/src/components/Popover/Popover.tsx b/packages/react-core/src/components/Popover/Popover.tsx index 5a9edfecccd..220947ce5dc 100644 --- a/packages/react-core/src/components/Popover/Popover.tsx +++ b/packages/react-core/src/components/Popover/Popover.tsx @@ -321,6 +321,20 @@ export const Popover: React.FunctionComponent = ({ 'right-start': styles.modifiers.rightTop, 'right-end': styles.modifiers.rightBottom }; + const positionModifiersRTL = { + top: styles.modifiers.top, + bottom: styles.modifiers.bottom, + left: styles.modifiers.right, + right: styles.modifiers.left, + 'top-start': styles.modifiers.topRight, + 'top-end': styles.modifiers.topLeft, + 'bottom-start': styles.modifiers.bottomRight, + 'bottom-end': styles.modifiers.bottomLeft, + 'left-start': styles.modifiers.rightTop, + 'left-end': styles.modifiers.rightBottom, + 'right-start': styles.modifiers.leftTop, + 'right-end': styles.modifiers.leftBottom + }; const hasCustomMinWidth = minWidth !== popoverMinWidth.value; const hasCustomMaxWidth = maxWidth !== popoverMaxWidth.value; const onDocumentKeyDown = (event: KeyboardEvent) => { @@ -508,6 +522,7 @@ export const Popover: React.FunctionComponent = ({ onFocus={triggerAction === 'hover' && onFocus} onBlur={triggerAction === 'hover' && onBlur} positionModifiers={positionModifiers} + positionModifiersRTL={positionModifiersRTL} distance={distance} placement={position} onTriggerClick={triggerAction === 'click' && onTriggerClick} diff --git a/packages/react-core/src/components/Tooltip/Tooltip.tsx b/packages/react-core/src/components/Tooltip/Tooltip.tsx index 472d428a8c5..b71b68b3ef0 100644 --- a/packages/react-core/src/components/Tooltip/Tooltip.tsx +++ b/packages/react-core/src/components/Tooltip/Tooltip.tsx @@ -220,6 +220,20 @@ export const Tooltip: React.FunctionComponent = ({ 'right-start': styles.modifiers.rightTop, 'right-end': styles.modifiers.rightBottom }; + const positionModifiersRTL = { + top: styles.modifiers.top, + bottom: styles.modifiers.bottom, + left: styles.modifiers.right, + right: styles.modifiers.left, + 'top-start': styles.modifiers.topRight, + 'top-end': styles.modifiers.topLeft, + 'bottom-start': styles.modifiers.bottomRight, + 'bottom-end': styles.modifiers.bottomLeft, + 'left-start': styles.modifiers.rightTop, + 'left-end': styles.modifiers.rightBottom, + 'right-start': styles.modifiers.leftTop, + 'right-end': styles.modifiers.leftBottom + }; const hasCustomMaxWidth = maxWidth !== tooltipMaxWidth.value; const content = (
= ({ appendTo={appendTo} isVisible={visible} positionModifiers={positionModifiers} + positionModifiersRTL={positionModifiersRTL} distance={distance} placement={position} onMouseEnter={triggerOnMouseenter && show} diff --git a/packages/react-core/src/helpers/Popper/Popper.tsx b/packages/react-core/src/helpers/Popper/Popper.tsx index b7ba8663470..2ac559a877f 100644 --- a/packages/react-core/src/helpers/Popper/Popper.tsx +++ b/packages/react-core/src/helpers/Popper/Popper.tsx @@ -119,6 +119,29 @@ export interface PopperProps { rightStart?: string; rightEnd?: string; }; + /** + * Map class names to positions when the popper is in RTL, for example: + * { + * top: styles.modifiers.top, + * bottom: styles.modifiers.bottom, + * left: styles.modifiers.left, + * right: styles.modifiers.right + * } + */ + positionModifiersRTL?: { + top?: string; + right?: string; + bottom?: string; + left?: string; + topStart?: string; + topEnd?: string; + bottomStart?: string; + bottomEnd?: string; + leftStart?: string; + leftEnd?: string; + rightStart?: string; + rightEnd?: string; + }; /** Distance of the popper to the trigger */ distance?: number; /** Callback function when mouse enters trigger */ @@ -201,6 +224,7 @@ export const Popper: React.FunctionComponent = ({ zIndex = 9999, isVisible = true, positionModifiers, + positionModifiersRTL, distance = 0, onMouseEnter, onMouseLeave, @@ -244,6 +268,14 @@ export const Popper: React.FunctionComponent = ({ const triggerParent = ((triggerRef as React.RefObject)?.current || triggerElement)?.parentElement; const languageDirection = getLanguageDirection(triggerParent); + const internalPositionModifiers = React.useMemo(() => { + if (languageDirection === 'rtl' && positionModifiersRTL) { + return positionModifiersRTL; + } + + return positionModifiers; + }, [positionModifiers, positionModifiersRTL, languageDirection]); + const internalPosition = React.useMemo<'left' | 'right' | 'center'>(() => { const fixedPositions = { left: 'left', right: 'right', center: 'center' }; @@ -512,14 +544,14 @@ export const Popper: React.FunctionComponent = ({ // Depends on the position of the Popper relative to the reference element const modifierFromPopperPosition = () => { if (attributes && attributes.popper && attributes.popper['data-popper-placement']) { - const popperPlacement = attributes.popper['data-popper-placement'] as keyof typeof positionModifiers; - return positionModifiers[popperPlacement]; + const popperPlacement = attributes.popper['data-popper-placement'] as keyof typeof internalPositionModifiers; + return internalPositionModifiers[popperPlacement]; } - return positionModifiers.top; + return internalPositionModifiers.top; }; const options = { - className: css(popper.props && popper.props.className, positionModifiers && modifierFromPopperPosition()), + className: css(popper.props && popper.props.className, internalPositionModifiers && modifierFromPopperPosition()), style: { ...((popper.props && popper.props.style) || {}), ...popperStyles.popper,