Skip to content
Open
2 changes: 2 additions & 0 deletions modules/react/menu/lib/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export const useMenuItemFocus = createElemPropsHook(useMenuModel)(
(model, ref, elemProps: {'data-id': string} = {'data-id': ''}) => {
const {localRef, elementRef} = useLocalRef(ref as React.Ref<HTMLElement>);
const id = elemProps['data-id'];

// focus on the item with the cursor
React.useLayoutEffect(() => {
if (model.state.mode === 'single') {
Expand All @@ -196,6 +197,7 @@ export const useMenuItemFocus = createElemPropsHook(useMenuModel)(
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, localRef, model.state.cursorId, model.state.mode]);

return {
ref: elementRef,
className: isCursor(model.state, elemProps['data-id']) ? 'focus' : undefined,
Expand Down
52 changes: 46 additions & 6 deletions modules/react/popup/lib/Popper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,25 @@ export type PopperOptions = Options;
export const defaultFallbackPlacements: Placement[] = ['top', 'right', 'bottom', 'left'];

import {usePopupStack} from './hooks';
import {useLocalRef} from '@workday/canvas-kit-react/common';
import {isElementRTL, useLocalRef} from '@workday/canvas-kit-react/common';
import {fallbackPlacementsModifier} from './fallbackPlacements';

/**
* Flips a placement for RTL layouts. Popper.js doesn't automatically flip
* placements based on dir attribute, so we need to do it manually. In RTL:
* - `left` ↔ `right`
* - `-start` ↔ `-end`
*/
const flipPlacementForRTL = (placement: Placement): Placement => {
return placement
.replace(/left/g, '__LEFT__')
.replace(/right/g, 'left')
.replace(/__LEFT__/g, 'right')
.replace(/-start/g, '__START__')
.replace(/-end/g, '-start')
.replace(/__START__/g, '-end') as Placement;
};

export interface PopperProps {
/**
* The reference element used to position the Popper. Popper content will try to follow the
Expand Down Expand Up @@ -172,16 +188,25 @@ const OpenPopper = React.forwardRef<HTMLDivElement, PopperProps>(
return undefined;
}

// Check RTL from the popup container (stackRef) to flip placements.
// Popper.js doesn't automatically flip left/right placements in RTL mode.
// We use stackRef because it has the dir attribute set by usePopupStack.
const isRTL = stackRef.current ? isElementRTL(stackRef.current) : false;
const rtlAwarePlacement = isRTL ? flipPlacementForRTL(popperPlacement) : popperPlacement;
const rtlAwareFallbackPlacements = isRTL
? fallbackPlacements.map(flipPlacementForRTL)
: fallbackPlacements;

Comment on lines +191 to +199
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RTL placement flipping is new behavior in Popper, but there are existing unit tests for Popper behavior. Please add test coverage to assert that in an RTL container the passed placement and fallbackPlacements are flipped as expected (including *-start/*-end), and that the render-prop placement reflects the RTL-aware placement.

Copilot uses AI. Check for mistakes.
if (stackRef.current) {
const instance = createPopper(anchorEl, stackRef.current, {
placement: popperPlacement,
placement: rtlAwarePlacement,
...popperOptions,
modifiers: [
placementModifier,
{
...fallbackPlacementsModifier,
options: {
fallbackPlacements,
fallbackPlacements: rtlAwareFallbackPlacements,
},
},
...(popperOptions.modifiers || []),
Expand All @@ -204,23 +229,38 @@ const OpenPopper = React.forwardRef<HTMLDivElement, PopperProps>(
React.useLayoutEffect(() => {
// Only update options if this is _not_ the first render
if (!firstRender.current) {
// Check RTL from the popup container to flip placements.
const popperElement = localRef.current?.state?.elements?.popper;
const isRTL = popperElement ? isElementRTL(popperElement) : false;
const rtlAwarePlacement = isRTL ? flipPlacementForRTL(popperPlacement) : popperPlacement;
const rtlAwareFallbackPlacements = isRTL
? fallbackPlacements.map(flipPlacementForRTL)
: fallbackPlacements;

localRef.current?.setOptions({
placement: popperPlacement,
placement: rtlAwarePlacement,
...popperOptions,
modifiers: [
placementModifier,
{
...fallbackPlacementsModifier,
options: {
fallbackPlacements,
fallbackPlacements: rtlAwareFallbackPlacements,
},
},
...(popperOptions.modifiers || []),
],
});
}
firstRender.current = false;
}, [popperOptions, popperPlacement, fallbackPlacements, placementModifier, localRef]);
}, [
popperOptions,
popperPlacement,
fallbackPlacements,
anchorElement,
placementModifier,
localRef,
]);

const contents = <>{isRenderProp(children) ? children({placement}) : children}</>;

Expand Down
22 changes: 12 additions & 10 deletions modules/react/popup/stories/visual-testing/Popup.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,18 @@ export const PopupRTL = {
});
return (
<CanvasProvider dir="rtl">
<Popup model={model}>
<Popup.Target style={{display: 'none'}}></Popup.Target>
<Popup.Popper>
<Popup.Card style={{animation: 'none'}} width={300}>
<Popup.CloseIcon aria-label="" />
<Popup.Heading>למחוק פריט</Popup.Heading>
<Popup.Body>האם ברצונך למחוק פריט זה</Popup.Body>
</Popup.Card>
</Popup.Popper>
</Popup>
<div style={{display: 'flex', justifyContent: 'flex-start'}}>
<Popup model={model}>
<Popup.Target style={{visibility: 'hidden'}}></Popup.Target>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when the target is hidden, it knows how to correctly position itself.

<Popup.Popper>
<Popup.Card style={{animation: 'none'}} width={300}>
<Popup.CloseIcon aria-label="" />
<Popup.Heading>למחוק פריט</Popup.Heading>
<Popup.Body>האם ברצונך למחוק פריט זה</Popup.Body>
</Popup.Card>
</Popup.Popper>
</Popup>
</div>
</CanvasProvider>
);
},
Expand Down
Loading