Skip to content

Commit 62d4280

Browse files
refactor: use :dir pseudo selector for RTL detection (#5477)
To be aligned with SAP/ui5-webcomponents#8241
1 parent 68c6fcc commit 62d4280

File tree

5 files changed

+68
-113
lines changed

5 files changed

+68
-113
lines changed

packages/base/src/hooks/useIsRTL.ts

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,30 @@
11
'use client';
22

3-
import { getRTL } from '@ui5/webcomponents-base/dist/config/RTL.js';
3+
import { attachDirectionChange, detachDirectionChange } from '@ui5/webcomponents-base/dist/locale/directionChange.js';
44
import type { RefObject } from 'react';
55
import { useRef, useState } from 'react';
66
import { useIsomorphicLayoutEffect } from '../hooks/index.js';
77

8-
const GLOBAL_DIR_CSS_VAR = '--_ui5_dir';
9-
10-
const detectRTL = <RefType extends HTMLElement>(elementRef: RefObject<RefType>) => {
11-
if (!elementRef.current) {
12-
return getRTL();
13-
}
14-
const doc = window.document;
15-
const dirValues = ['ltr', 'rtl']; // exclude "auto" and "" from all calculations
16-
const locallyAppliedDir = getComputedStyle(elementRef.current).getPropertyValue(GLOBAL_DIR_CSS_VAR);
17-
18-
// In that order, inspect the CSS Var (for modern browsers), the element itself, html and body (for IE fallback)
19-
if (dirValues.includes(locallyAppliedDir)) {
20-
return locallyAppliedDir === 'rtl';
21-
}
22-
if (dirValues.includes(elementRef.current?.dir)) {
23-
return elementRef.current?.dir === 'rtl';
24-
}
25-
if (dirValues.includes(doc.documentElement.dir)) {
26-
return doc.documentElement.dir === 'rtl';
27-
}
28-
if (dirValues.includes(doc.body.dir)) {
29-
return doc.body.dir === 'rtl';
30-
}
31-
32-
// Finally, check the configuration for explicitly set RTL or language-implied RTL
33-
return getRTL();
34-
};
35-
368
const useIsRTL = <RefType extends HTMLElement>(elementRef: RefObject<RefType>): boolean => {
37-
const [isRTL, setRTL] = useState<boolean>(getRTL()); // use config RTL as best guess
9+
const [isRTL, setRTL] = useState(false); // initial value is always LTR (also for SSR)
3810
const isMounted = useRef(false);
11+
3912
useIsomorphicLayoutEffect(() => {
4013
isMounted.current = true;
41-
setRTL(detectRTL(elementRef)); // update immediately while rendering
42-
const targets = [document.documentElement, document.body, elementRef.current].filter(Boolean);
43-
const observer = new MutationObserver((mutations) => {
44-
mutations.forEach((mutation) => {
45-
if (mutation.attributeName === 'dir') {
46-
if (isMounted.current) {
47-
setRTL(detectRTL(elementRef));
48-
}
49-
}
50-
});
51-
});
14+
setRTL(elementRef.current?.matches(':dir(rtl)') ?? false);
5215

53-
targets.forEach((target) => {
54-
// @ts-expect-error: target can never be a faulty value
55-
observer.observe(target, {
56-
attributes: true,
57-
childList: false,
58-
characterData: false,
59-
attributeFilter: ['dir']
60-
});
61-
});
16+
const handleDirectionChange = () => {
17+
if (isMounted.current) {
18+
setRTL(elementRef.current?.matches(':dir(rtl)') ?? false);
19+
}
20+
};
21+
attachDirectionChange(handleDirectionChange);
6222

6323
return () => {
6424
isMounted.current = false;
65-
observer.disconnect();
25+
detachDirectionChange(handleDirectionChange);
6626
};
67-
}, [isMounted, elementRef.current]);
27+
}, []);
6828

6929
return isRTL;
7030
};

packages/main/src/components/ActionSheet/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { isPhone } from '@ui5/webcomponents-base/dist/Device.js';
4-
import { useI18nBundle, useIsRTL, useSyncRef } from '@ui5/webcomponents-react-base';
4+
import { useI18nBundle, useSyncRef } from '@ui5/webcomponents-react-base';
55
import { clsx } from 'clsx';
66
import type { ReactElement } from 'react';
77
import React, { forwardRef, useReducer, useRef } from 'react';
@@ -164,7 +164,6 @@ const ActionSheet = forwardRef<ResponsivePopoverDomRef, ActionSheetPropTypes>((p
164164
const childrenToRender = flattenFragments(children);
165165
const childrenArrayLength = childrenToRender.length;
166166
const childrenLength = isPhone() && showCancelButton ? childrenArrayLength + 1 : childrenArrayLength;
167-
const isRtl = useIsRTL(popoverRef);
168167

169168
const canRenderPortal = useCanRenderPortal();
170169
if (!canRenderPortal) {
@@ -210,6 +209,7 @@ const ActionSheet = forwardRef<ResponsivePopoverDomRef, ActionSheetPropTypes>((p
210209

211210
const handleKeyDown = (e) => {
212211
const currentIndex = parseInt(e.target.dataset.actionBtnIndex);
212+
const isRtl = actionBtnsRef.current?.matches(':dir(rtl)');
213213
switch (e.key) {
214214
case 'ArrowDown':
215215
case isRtl ? 'ArrowLeft' : 'ArrowRight':

packages/main/src/components/Splitter/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ const Splitter = forwardRef<HTMLDivElement, SplitterPropTypes>((props, ref) => {
178178
const { vertical } = props;
179179
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
180180
const [componentRef, localRef] = useSyncRef<HTMLDivElement>(ref);
181-
const isRtl = useIsRTL({ current: localRef.current?.parentElement });
181+
const isRtl = useIsRTL(localRef);
182182
const start = useRef(null);
183183
const classes = useStyles();
184184

packages/main/src/components/SplitterLayout/SplitterLayout.cy.tsx

Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,57 @@
1+
import { cypressPassThroughTestsFactory } from '@/cypress/support/utils';
12
import { useState } from 'react';
23
import type { SplitterLayoutPropTypes } from '../..';
3-
import { SplitterElement, SplitterLayout, Label, Button } from '../..';
4-
import { cypressPassThroughTestsFactory } from '@/cypress/support/utils';
4+
import { Button, Label, SplitterElement, SplitterLayout } from '../..';
55

6-
describe('SplitterLayout', () => {
7-
it('Splitter Move & Reset', () => {
8-
const TestComp = ({ vertical, dir }: { vertical: SplitterLayoutPropTypes['vertical']; dir: string }) => {
9-
const [mount, setMount] = useState(false);
10-
const [dep, setDep] = useState(false);
11-
return (
12-
<SplitterLayout
13-
dir={dir}
14-
vertical={vertical}
15-
style={{
16-
width: '100vw',
17-
height: '100vh'
18-
}}
19-
options={{ resetOnSizeChange: true, resetOnChildrenChange: true, resetOnCustomDepsChange: [dep] }}
20-
>
21-
<SplitterElement size="70%" data-testid="se1">
22-
<Label>Left</Label>
23-
<Button onClick={() => setMount(true)}>Add child</Button>
24-
<Button onClick={() => setDep(true)}>Trigger dep</Button>
25-
</SplitterElement>
26-
<SplitterElement size={mount ? '25%' : '30%'} data-testid="se2">
27-
<Label>Right</Label>
28-
</SplitterElement>
29-
{mount && (
30-
<SplitterElement size="5%" data-testid="se3">
31-
Additional Child
32-
</SplitterElement>
33-
)}
34-
</SplitterLayout>
35-
);
36-
};
6+
function TestComp({ vertical, dir }: { vertical: SplitterLayoutPropTypes['vertical']; dir: string }) {
7+
const [mount, setMount] = useState(false);
8+
const [dep, setDep] = useState(false);
9+
return (
10+
<SplitterLayout
11+
dir={dir}
12+
vertical={vertical}
13+
style={{
14+
width: '100vw',
15+
height: '100vh'
16+
}}
17+
options={{ resetOnSizeChange: true, resetOnChildrenChange: true, resetOnCustomDepsChange: [dep] }}
18+
>
19+
<SplitterElement size="70%" data-testid="se1">
20+
<Label>Left</Label>
21+
<Button onClick={() => setMount(true)}>Add child</Button>
22+
<Button onClick={() => setDep(true)}>Trigger dep</Button>
23+
</SplitterElement>
24+
<SplitterElement size={mount ? '25%' : '30%'} data-testid="se2">
25+
<Label>Right</Label>
26+
</SplitterElement>
27+
{mount && (
28+
<SplitterElement size="5%" data-testid="se3">
29+
Additional Child
30+
</SplitterElement>
31+
)}
32+
</SplitterLayout>
33+
);
34+
}
3735

38-
function moveSpacer(dir, vertical) {
39-
cy.findAllByRole('separator').eq(0).click();
40-
cy.wait(50);
41-
const rtlSafeLeft = `Arrow${dir === 'rtl' && !vertical ? 'Right' : 'Left'}`;
42-
const rtlSafeUp = `Arrow${dir === 'rtl' && !vertical ? 'Down' : 'Up'}`;
43-
for (let i = 0; i < 5; i++) {
44-
cy.findAllByRole('separator').eq(0).trigger('keydown', { code: rtlSafeLeft, force: true });
45-
cy.findAllByRole('separator').eq(0).trigger('keyup', { code: rtlSafeLeft, force: true });
46-
cy.wait(50);
47-
cy.findAllByRole('separator').eq(0).trigger('keydown', { code: rtlSafeUp, force: true });
48-
cy.findAllByRole('separator').eq(0).trigger('keyup', { code: rtlSafeUp, force: true });
49-
cy.wait(50);
50-
}
51-
}
52-
['ltr', 'rtl'].forEach((dir) => {
53-
[false, true].forEach((vertical) => {
36+
function moveSpacer(dir: string, vertical: boolean) {
37+
cy.findAllByRole('separator').eq(0).click();
38+
cy.wait(50);
39+
const rtlSafeLeft = `Arrow${dir === 'rtl' && !vertical ? 'Right' : 'Left'}`;
40+
const rtlSafeUp = `Arrow${dir === 'rtl' && !vertical ? 'Down' : 'Up'}`;
41+
for (let i = 0; i < 5; i++) {
42+
cy.findAllByRole('separator').eq(0).trigger('keydown', { code: rtlSafeLeft, force: true });
43+
cy.findAllByRole('separator').eq(0).trigger('keyup', { code: rtlSafeLeft, force: true });
44+
cy.wait(50);
45+
cy.findAllByRole('separator').eq(0).trigger('keydown', { code: rtlSafeUp, force: true });
46+
cy.findAllByRole('separator').eq(0).trigger('keyup', { code: rtlSafeUp, force: true });
47+
cy.wait(50);
48+
}
49+
}
50+
51+
describe('SplitterLayout', () => {
52+
['ltr', 'rtl'].forEach((dir) => {
53+
[false, true].forEach((vertical) => {
54+
it(`Splitter Move & Reset - ${dir} - vertical: ${vertical}`, () => {
5455
cy.viewport(2000, 2000);
5556
cy.mount(<TestComp vertical={vertical} dir={dir} />);
5657

packages/main/src/components/Toolbar/index.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
'use client';
22

3-
import {
4-
debounce,
5-
useI18nBundle,
6-
useIsomorphicLayoutEffect,
7-
useIsRTL,
8-
useSyncRef
9-
} from '@ui5/webcomponents-react-base';
3+
import { debounce, useI18nBundle, useIsomorphicLayoutEffect, useSyncRef } from '@ui5/webcomponents-react-base';
104
import { clsx } from 'clsx';
115
import type { ElementType, HTMLAttributes, ReactElement, ReactNode, Ref, RefObject } from 'react';
126
import React, {
@@ -181,7 +175,6 @@ const Toolbar = forwardRef<HTMLDivElement, ToolbarPropTypes>((props, ref) => {
181175
const overflowContentRef = useRef(null);
182176
const overflowBtnRef = useRef(null);
183177
const [minWidth, setMinWidth] = useState('0');
184-
const isRtl = useIsRTL(outerContainer);
185178

186179
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
187180
const showMoreText = i18nBundle.getText(SHOW_MORE);
@@ -231,6 +224,7 @@ const Toolbar = forwardRef<HTMLDivElement, ToolbarPropTypes>((props, ref) => {
231224
const lastElement = contentRef.current.children[numberOfAlwaysVisibleItems - 1];
232225
const debouncedObserverFn = debounce(() => {
233226
const spacerWidth = getSpacerWidths(lastElement);
227+
const isRtl = outerContainer.current?.matches(':dir(rtl)');
234228
if (isRtl) {
235229
setMinWidth(
236230
`${lastElement.offsetParent.offsetWidth - lastElement.offsetLeft + OVERFLOW_BUTTON_WIDTH - spacerWidth}px`
@@ -251,7 +245,7 @@ const Toolbar = forwardRef<HTMLDivElement, ToolbarPropTypes>((props, ref) => {
251245
debouncedObserverFn.cancel();
252246
lastElementResizeObserver?.disconnect();
253247
};
254-
}, [numberOfAlwaysVisibleItems, overflowNeeded, isRtl]);
248+
}, [numberOfAlwaysVisibleItems, overflowNeeded]);
255249

256250
const requestAnimationFrameRef = useRef<undefined | number>();
257251
const calculateVisibleItems = useCallback(() => {

0 commit comments

Comments
 (0)