Skip to content

Commit 1e3126b

Browse files
authored
[šŸ›][Menu]: Fix an issue that caused the menu to have the wrong behavior on RTL directions (#95)
* Update version * Fix alignment * Update api * Remove useDirection hook and add getDirection fn instead * Fix the issue with directions
1 parent af98794 commit 1e3126b

File tree

12 files changed

+68
-107
lines changed

12 files changed

+68
-107
lines changed

ā€Žlib/Menu/BaseMenu.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ type OwnProps = {
1212
keepMounted: boolean;
1313
trapFocus: boolean;
1414
alignment: NonNullable<PopperProps["alignment"]>;
15+
autoPlacement?: PopperProps["autoPlacement"];
1516
activeDescendantId?: string | null;
1617
label: LabelInfo;
1718
onExitTrap?: (event: FocusEvent) => void;
1819
resolveAnchor: NonNullable<MenuProps["resolveAnchor"]>;
19-
computationMiddleware: NonNullable<PopperProps["computationMiddleware"]>;
20+
computationMiddleware?: PopperProps["computationMiddleware"];
2021
};
2122

2223
export type Props = Omit<
@@ -34,6 +35,7 @@ const BaseMenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
3435
alignment,
3536
activeDescendantId,
3637
label,
38+
autoPlacement = true,
3739
onExitTrap,
3840
resolveAnchor,
3941
computationMiddleware,
@@ -76,7 +78,7 @@ const BaseMenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
7678

7779
return (
7880
<Popper
79-
autoPlacement
81+
autoPlacement={autoPlacement}
8082
keepMounted={keepMounted}
8183
open={open}
8284
resolveAnchor={resolveAnchor}

ā€Žlib/Menu/Menu.tsx

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
componentWithForwardedRef,
1818
dispatchDiscreteCustomEvent,
1919
useDeterministicId,
20-
useDirection,
2120
useEventCallback,
2221
useEventListener,
2322
useForkedRefs,
@@ -28,7 +27,7 @@ import { CollapseSubMenuEvent, ExpandSubMenuEvent } from "./constants";
2827
import { MenuContext, type MenuContextValue } from "./context";
2928
import { Root as RootSlot } from "./slots";
3029
import {
31-
createComputationMiddleware,
30+
computationMiddleware,
3231
getAvailableItem,
3332
getCurrentFocusedElement,
3433
getCurrentMenuControllerItem,
@@ -69,7 +68,7 @@ type OwnProps = {
6968
/**
7069
* The menu positioning alignment.
7170
*
72-
* @default "start"
71+
* @default "middle"
7372
*/
7473
alignment?: PopperProps["alignment"];
7574
/**
@@ -120,7 +119,7 @@ const MenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
120119
className: classNameProp,
121120
children: childrenProp,
122121
label,
123-
alignment = "start",
122+
alignment = "middle",
124123
open = false,
125124
keepMounted = false,
126125
resolveAnchor,
@@ -148,8 +147,6 @@ const MenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
148147
const [activeExpandedDescendant, setActiveExpandedDescendant] =
149148
React.useState<HTMLElement | null>(null);
150149

151-
const dir = useDirection(rootRef) ?? "ltr";
152-
153150
const emitActiveElementChange = React.useCallback(
154151
(newActiveItem: HTMLElement | null) => {
155152
setActiveElement(newActiveItem);
@@ -170,11 +167,6 @@ const MenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
170167
onActiveDescendantElementChange: emitActiveElementChange,
171168
});
172169

173-
const computationMiddleware = React.useMemo(
174-
() => createComputationMiddleware(dir, alignment),
175-
[dir, alignment],
176-
);
177-
178170
useOnChange(open, currentOpen => {
179171
if (currentOpen) {
180172
previouslyFocusedElement.current = document.activeElement as HTMLElement;
@@ -444,6 +436,7 @@ const MenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
444436

445437
if (!node) return;
446438
if (!open) return;
439+
447440
if (document.activeElement === node) return;
448441

449442
node.focus();
@@ -468,7 +461,6 @@ const MenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
468461
id,
469462
activeElement,
470463
keepMounted,
471-
alignment,
472464
emitClose,
473465
emitActiveElementChange,
474466
computationMiddleware,
@@ -490,16 +482,16 @@ const MenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
490482
return (
491483
<BaseMenu
492484
{...otherProps}
493-
id={id}
494485
trapFocus
486+
autoPlacement={{ excludeSides: ["left", "right"] }}
487+
id={id}
495488
ref={refCallback}
496489
onKeyDown={handleKeyDown}
497490
onExitTrap={handleExitTrap}
498491
onFocus={handleFocus}
499492
activeDescendantId={getActiveDescendant()}
500493
resolveAnchor={resolveAnchor}
501494
alignment={alignment}
502-
computationMiddleware={computationMiddleware}
503495
keepMounted={keepMounted}
504496
open={open}
505497
label={labelProps}

ā€Žlib/Menu/components/SubMenu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ const SubMenuBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
9898
ref={refCallback}
9999
activeDescendantId={null}
100100
resolveAnchor={resolveAnchor}
101-
alignment={menuCtx.alignment}
101+
alignment="start"
102+
autoPlacement={{ excludeSides: ["bottom", "top"] }}
102103
computationMiddleware={menuCtx.computationMiddleware}
103104
keepMounted={menuCtx.keepMounted}
104105
open={openState}

ā€Žlib/Menu/context.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ type ContextValue = {
55
id: string;
66
activeElement: HTMLElement | null;
77
keepMounted: boolean;
8-
alignment: NonNullable<PopperProps["alignment"]>;
98
emitClose: () => void;
109
emitActiveElementChange: (newActiveElement: HTMLElement | null) => void;
1110
computationMiddleware: NonNullable<PopperProps["computationMiddleware"]>;

ā€Žlib/Menu/utils.ts

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as React from "react";
33
import type { PopperProps } from "../Popper";
44
import { logger } from "../internals";
5-
import { isHTMLElement, useEventCallback } from "../utils";
5+
import { getDirection, isHTMLElement, useEventCallback } from "../utils";
66
import { MenuContext } from "./context";
77

88
export const getListItems = (
@@ -121,36 +121,29 @@ export const getCurrentFocusedElement = (
121121
return { item: null, index: -1 };
122122
};
123123

124-
export const createComputationMiddleware =
125-
(
126-
dir: "ltr" | "rtl",
127-
alignment: NonNullable<PopperProps["alignment"]>,
128-
): NonNullable<PopperProps["computationMiddleware"]> =>
129-
({ overflow, elementRects, placement }) => {
130-
if (dir === "rtl") {
131-
const desiredPlacement: typeof placement =
132-
alignment === "middle" ? "left" : `left-${alignment}`;
133-
134-
const oppOfDesiredPlacement: typeof placement =
135-
alignment === "middle" ? "right" : `right-${alignment}`;
136-
137-
if (placement === desiredPlacement) return { placement };
138-
if (overflow.left + elementRects.popperRect.width <= 0) {
139-
return { placement: desiredPlacement };
140-
} else return { placement: oppOfDesiredPlacement };
141-
} else {
142-
const desiredPlacement: typeof placement =
143-
alignment === "middle" ? "right" : `right-${alignment}`;
144-
145-
const oppOfDesiredPlacement: typeof placement =
146-
alignment === "middle" ? "left" : `left-${alignment}`;
147-
148-
if (placement === desiredPlacement) return { placement };
149-
if (overflow.right + elementRects.popperRect.width <= 0) {
150-
return { placement: desiredPlacement };
151-
} else return { placement: oppOfDesiredPlacement };
152-
}
153-
};
124+
export const computationMiddleware: NonNullable<
125+
PopperProps["computationMiddleware"]
126+
> = ({ overflow, elementRects, placement, elements }) => {
127+
const dir = getDirection(elements.popperElement);
128+
129+
if (dir === "rtl") {
130+
const desiredPlacement: typeof placement = "left-start";
131+
const oppOfDesiredPlacement: typeof placement = "right-start";
132+
133+
if (placement === desiredPlacement) return { placement };
134+
if (overflow.left + elementRects.popperRect.width <= 0) {
135+
return { placement: desiredPlacement };
136+
} else return { placement: oppOfDesiredPlacement };
137+
} else {
138+
const desiredPlacement: typeof placement = "right-start";
139+
const oppOfDesiredPlacement: typeof placement = "left-start";
140+
141+
if (placement === desiredPlacement) return { placement };
142+
if (overflow.right + elementRects.popperRect.width <= 0) {
143+
return { placement: desiredPlacement };
144+
} else return { placement: oppOfDesiredPlacement };
145+
}
146+
};
154147

155148
export const getMenuItem = (event: React.MouseEvent) => {
156149
if (isHTMLElement(event.target)) {

ā€Žlib/Popper/Popper.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type {
99
import {
1010
componentWithForwardedRef,
1111
useDeterministicId,
12-
useDirection,
1312
useForkedRefs,
1413
useIsomorphicLayoutEffect,
1514
useIsomorphicValue,
@@ -134,6 +133,7 @@ type OwnProps = {
134133
/**
135134
* Used to keep mounting when more control is needed.\
136135
* Useful when controlling animation with React animation libraries.
136+
*
137137
* @default false
138138
*/
139139
keepMounted?: boolean;
@@ -170,7 +170,6 @@ const PopperBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
170170

171171
const open = useIsomorphicValue(openProp, false);
172172

173-
const isRtl = useDirection() === "rtl";
174173
const id = useDeterministicId(idProp, "styleless-ui__popper");
175174

176175
const popperRef = React.useRef<HTMLDivElement>(null);
@@ -194,7 +193,6 @@ const PopperBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
194193
autoPlacement,
195194
offset,
196195
strategy,
197-
isRtl,
198196
};
199197

200198
const updatePosition = () => {
@@ -274,6 +272,7 @@ const PopperBase = (props: Props, ref: React.Ref<HTMLDivElement>) => {
274272
className={className}
275273
tabIndex={-1}
276274
data-slot={Slots.Root}
275+
data-placement={placement}
277276
data-open={open ? "" : undefined}
278277
>
279278
{children}

ā€Žlib/Popper/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ export type ComputationResult = Coordinates & { placement: Placement };
5757
export type ComputationConfig = {
5858
placement: Placement;
5959
strategy: Strategy;
60-
isRtl: boolean;
6160
autoPlacement: AutoPlacementMiddleware;
6261
offset: OffsetMiddleware;
6362
computationMiddleware?: ComputationMiddleware;

ā€Žlib/Popper/utils.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
clamp,
55
contains,
66
getBoundingClientRect,
7+
getDirection,
78
getDocumentElement,
89
getNodeName,
910
getOffsetParent,
@@ -58,7 +59,7 @@ const getAlignmentFromPlacement = (
5859
const getAlignmentSidesFromPlacement = (
5960
placement: Placement,
6061
elementRects: ElementRects,
61-
isRtl: boolean,
62+
isRTL: boolean,
6263
): { mainSide: Side; crossSide: Side } => {
6364
const alignment = getAlignmentFromPlacement(placement);
6465
const mainAxis = getMainAxisFromPlacement(placement);
@@ -75,7 +76,7 @@ const getAlignmentSidesFromPlacement = (
7576

7677
let mainSide: Side =
7778
mainAxis === "x"
78-
? alignment === (isRtl ? "end" : "start")
79+
? alignment === (isRTL ? "end" : "start")
7980
? "right"
8081
: "left"
8182
: alignment === "start"
@@ -434,9 +435,10 @@ const calcCoordinatesFromPlacement = (args: {
434435
elements: Elements;
435436
elementRects: ElementRects;
436437
strategy: Strategy;
437-
isRtl: boolean;
438+
isRTL: boolean;
438439
}): Coordinates => {
439-
const { placement, offset, elementRects, elements, strategy, isRtl } = args;
440+
const { placement, offset, elementRects, elements, strategy, isRTL } = args;
441+
440442
const { anchorRect, popperRect } = elementRects;
441443

442444
const commonX = anchorRect.x + (anchorRect.width - popperRect.width) / 2;
@@ -473,10 +475,10 @@ const calcCoordinatesFromPlacement = (args: {
473475

474476
switch (alignment) {
475477
case "start":
476-
coordinates[mainAxis] -= commonAlign * (isRtl && isVertical ? -1 : 1);
478+
coordinates[mainAxis] -= commonAlign * (isRTL && isVertical ? -1 : 1);
477479
break;
478480
case "end":
479-
coordinates[mainAxis] += commonAlign * (isRtl && isVertical ? -1 : 1);
481+
coordinates[mainAxis] += commonAlign * (isRTL && isVertical ? -1 : 1);
480482
break;
481483
default:
482484
}
@@ -513,7 +515,7 @@ const calcCoordinatesFromPlacement = (args: {
513515
let crossAxisCoef = 1;
514516

515517
if (alignment === "end") crossAxisCoef = -1;
516-
if (isRtl && isVertical) crossAxisCoef *= -1;
518+
if (isRTL && isVertical) crossAxisCoef *= -1;
517519

518520
let mainAxisOffset = 0;
519521
let crossAxisOffset = 0;
@@ -550,11 +552,11 @@ export const suppressViewportOverflow = (
550552
args: {
551553
placement: Placement;
552554
elementRects: ElementRects;
553-
isRtl: boolean;
555+
isRTL: boolean;
554556
overflow: ComputationMiddlewareArgs["overflow"];
555557
},
556558
) => {
557-
const { overflow, placement, elementRects, isRtl } = args;
559+
const { overflow, placement, elementRects, isRTL: isRTL } = args;
558560
const alignment = getAlignmentFromPlacement(placement);
559561

560562
const _getOppositeAlignment = (placement: Placement) =>
@@ -601,7 +603,7 @@ export const suppressViewportOverflow = (
601603
const { mainSide, crossSide } = getAlignmentSidesFromPlacement(
602604
currentPlacement,
603605
elementRects,
604-
isRtl,
606+
isRTL,
605607
);
606608

607609
const currentOverflows = [
@@ -659,7 +661,6 @@ export const computePosition = (
659661
): ComputationResult => {
660662
const {
661663
strategy,
662-
isRtl,
663664
autoPlacement,
664665
offset,
665666
placement: initialPlacement,
@@ -672,8 +673,10 @@ export const computePosition = (
672673
const elements: Elements = { anchorElement, popperElement };
673674
const elementRects = getElementRects(elements, strategy);
674675

676+
const isRTL = getDirection(elements.popperElement) === "rtl";
677+
675678
let { x, y } = calcCoordinatesFromPlacement({
676-
isRtl,
679+
isRTL,
677680
offset,
678681
strategy,
679682
elements,
@@ -710,7 +713,7 @@ export const computePosition = (
710713
elementRects,
711714
placement,
712715
overflow,
713-
isRtl,
716+
isRTL,
714717
});
715718
}
716719

@@ -731,7 +734,7 @@ export const computePosition = (
731734
placement = result.placement ?? placement;
732735

733736
const coords = calcCoordinatesFromPlacement({
734-
isRtl,
737+
isRTL,
735738
offset,
736739
strategy,
737740
elements,

ā€Žlib/utils/get-direction.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { getWindow } from "./dom";
2+
3+
type Direction = "rtl" | "ltr";
4+
5+
const getDirection = (element: HTMLElement) => {
6+
const context = getWindow(element);
7+
8+
return context.getComputedStyle(element).direction as Direction;
9+
};
10+
11+
export default getDirection;

0 commit comments

Comments
Ā (0)