fix(ui): align dropdowns correctly in RTL layouts (#7522)

This commit is contained in:
白熱
2026-08-15 16:48:35 +08:00
committed by GitHub
parent cade815f9d
commit c528c7fca5
4 changed files with 41 additions and 6 deletions
@@ -295,7 +295,6 @@ export function DefinedName({ disable }: { disable: boolean }) {
univer-box-border univer-size-full univer-appearance-none univer-bg-transparent univer-pl-1.5
univer-pr-5 univer-text-gray-900
focus:univer-outline-none
rtl:univer-pl-5 rtl:univer-pr-1.5
dark:!univer-border-r-gray-700 dark:!univer-text-gray-0
`, borderRightClassName, {
'univer-cursor-not-allowed': disable,
@@ -323,7 +322,6 @@ export function DefinedName({ disable }: { disable: boolean }) {
univer-absolute univer-right-0 univer-top-0 univer-flex univer-h-full univer-cursor-pointer
univer-items-center univer-justify-center univer-px-1 univer-transition-colors univer-duration-200
hover:univer-bg-gray-100
rtl:univer-left-0 rtl:univer-right-auto
dark:!univer-text-gray-0
dark:hover:!univer-bg-gray-800
`, {
@@ -328,11 +328,26 @@ describe('FormulaBar', () => {
it('keeps its toolbar layout LTR when the sheet host is RTL', () => {
currentBed = createFormulaBarTestBed();
const rendered = renderWithDependencies(<FormulaBar disableDefinedName />, currentBed.injector);
const rendered = renderWithDependencies(
<div dir="rtl">
<FormulaBar disableDefinedName />
</div>,
currentBed.injector
);
root = rendered.root;
container = rendered.container;
expect(rendered.container.querySelector('[data-u-comp="formula-bar"]')?.getAttribute('dir')).toBe('ltr');
const formulaBar = rendered.container.querySelector('[data-u-comp="formula-bar"]');
const definedName = formulaBar?.querySelector('[data-u-comp="defined-name"]');
const input = definedName?.querySelector('input');
const dropdownTrigger = definedName?.querySelector('a');
expect(formulaBar?.getAttribute('dir')).toBe('ltr');
expect(input?.classList).toContain('univer-pl-1.5');
expect(input?.classList).toContain('univer-pr-5');
expect(input?.className).not.toContain('rtl:');
expect(dropdownTrigger?.classList).toContain('univer-right-0');
expect(dropdownTrigger?.className).not.toContain('rtl:');
});
it('closes editing through the command service when cancel is clicked', async () => {
@@ -19,6 +19,7 @@ import type { ReactNode } from 'react';
import type { IMenuItem, IValueOption } from '../../../services/menu/menu';
import {
clsx,
ConfigContext,
Dropdown,
DropdownMenu,
Tooltip,
@@ -149,7 +150,8 @@ export const TooltipWrapper = forwardRef<ITooltipWrapperRef, ITooltipProps & { d
});
export function DropdownWrapper(props: Omit<Partial<IDropdownProps>, 'overlay'> & { overlay: ReactNode; align?: 'start' | 'end' | 'center' }) {
const { children, overlay, disabled, align = 'start' } = props;
const { children, overlay, disabled, align } = props;
const { direction } = useContext(ConfigContext);
const { dropdownVisible, setDropdownVisible } = useContext(TooltipWrapperContext);
const triggerRef = useRef<HTMLDivElement>(null);
const overlayRef = useRef<HTMLDivElement>(null);
@@ -181,7 +183,7 @@ export function DropdownWrapper(props: Omit<Partial<IDropdownProps>, 'overlay'>
return (
<Dropdown
align={align}
align={align ?? (direction === 'rtl' ? 'end' : 'start')}
overlay={(
<div ref={overlayRef} className="univer-grid univer-gap-2">
{overlay}
@@ -21,6 +21,7 @@
import type { ComponentType, ReactElement } from 'react';
import { cleanup, fireEvent, render } from '@testing-library/react';
import { ILogService, Injector, LocaleService } from '@univerjs/core';
import { ConfigProvider } from '@univerjs/design';
import { of } from 'rxjs';
import { afterEach, describe, expect, it, vi } from 'vitest';
@@ -134,6 +135,25 @@ describe('DropdownMenuLabel', () => {
});
describe('DropdownWrapper', () => {
it('opens toward the left in RTL layouts', async () => {
const { findByText, getByRole } = render(
<ConfigProvider direction="rtl" mountContainer={document.body}>
<ToolbarDropdownProvider>
<TooltipWrapper dropdownKey="test-dropdown">
<DropdownWrapper overlay={<div>Dropdown content</div>}>
<button type="button">Open dropdown</button>
</DropdownWrapper>
</TooltipWrapper>
</ToolbarDropdownProvider>
</ConfigProvider>
);
fireEvent.click(getByRole('button', { name: 'Open dropdown' }));
const content = (await findByText('Dropdown content')).closest('[data-slot="popover-content"]');
expect(content?.getAttribute('data-align')).toBe('end');
});
it('closes on an outside pointerdown even when no click follows', async () => {
const { findByText, getByRole, queryByText } = render(
<ToolbarDropdownProvider>