From e4f7794795a9806fe3531d9b1c15cd139ab9791b Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Wed, 6 Nov 2024 13:44:13 +0800 Subject: [PATCH] Add RTL support --- .../ToggleButtonGroupRtl.js | 67 +++++++ .../ToggleButtonGroupRtl.tsx | 67 +++++++ .../toggle-button-group/styles.module.css | 20 +- .../toggle-button-group.mdx | 4 +- .../toggle-button/styles.module.css | 2 +- .../Item/ToggleButtonGroupItem.test.tsx | 3 +- .../Item/ToggleButtonGroupItem.tsx | 3 +- .../Root/ToggleButtonGroupRoot.test.tsx | 175 ++++++++++++++---- .../Root/ToggleButtonGroupRoot.tsx | 3 +- 9 files changed, 302 insertions(+), 42 deletions(-) create mode 100644 docs/data/components/toggle-button-group/ToggleButtonGroupRtl.js create mode 100644 docs/data/components/toggle-button-group/ToggleButtonGroupRtl.tsx diff --git a/docs/data/components/toggle-button-group/ToggleButtonGroupRtl.js b/docs/data/components/toggle-button-group/ToggleButtonGroupRtl.js new file mode 100644 index 000000000..1f96c61b3 --- /dev/null +++ b/docs/data/components/toggle-button-group/ToggleButtonGroupRtl.js @@ -0,0 +1,67 @@ +'use client'; +import * as React from 'react'; +import { ToggleButtonGroup } from '@base_ui/react/ToggleButtonGroup'; +import classes from './styles.module.css'; + +export default function ToggleButtonGroupRtl() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/docs/data/components/toggle-button-group/ToggleButtonGroupRtl.tsx b/docs/data/components/toggle-button-group/ToggleButtonGroupRtl.tsx new file mode 100644 index 000000000..1f96c61b3 --- /dev/null +++ b/docs/data/components/toggle-button-group/ToggleButtonGroupRtl.tsx @@ -0,0 +1,67 @@ +'use client'; +import * as React from 'react'; +import { ToggleButtonGroup } from '@base_ui/react/ToggleButtonGroup'; +import classes from './styles.module.css'; + +export default function ToggleButtonGroupRtl() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/docs/data/components/toggle-button-group/styles.module.css b/docs/data/components/toggle-button-group/styles.module.css index 965b79590..49d9ac564 100644 --- a/docs/data/components/toggle-button-group/styles.module.css +++ b/docs/data/components/toggle-button-group/styles.module.css @@ -4,8 +4,10 @@ .button { --size: 2.5rem; - --corner: 0.4rem; + --corner-radius: 0.4rem; --border-color: var(--gray-outline-2); + --border-radius-visual-right: 0 var(--corner-radius) var(--corner-radius) 0; + --border-radius-visual-left: var(--corner-radius) 0 0 var(--corner-radius); display: flex; flex-flow: row nowrap; @@ -19,16 +21,24 @@ } .button:first-child { - border-radius: var(--corner) 0 0 var(--corner); + border-radius: var(--border-radius-visual-left); border-right-color: transparent; } .button:last-child { - border-radius: 0 var(--corner) var(--corner) 0; + border-radius: var(--border-radius-visual-right); border-left-color: transparent; } -.button:hover { +[dir='rtl'] .button:first-child { + border-radius: var(--border-radius-visual-right); +} + +[dir='rtl'] .button:last-child { + border-radius: var(--border-radius-visual-left); +} + +.button:not(:disabled):hover { background-color: var(--gray-surface-1); outline: 1px solid var(--gray-500); outline-offset: -1px; @@ -38,7 +48,7 @@ } .button:focus-visible { - outline: 2px solid var(--gray-900); + outline: 2px solid var(--color-violet); z-index: 1; } diff --git a/docs/data/components/toggle-button-group/toggle-button-group.mdx b/docs/data/components/toggle-button-group/toggle-button-group.mdx index 3b49138d4..226bd1c88 100644 --- a/docs/data/components/toggle-button-group/toggle-button-group.mdx +++ b/docs/data/components/toggle-button-group/toggle-button-group.mdx @@ -57,7 +57,7 @@ When uncontrolled, use the `defaultValue` prop to set the initial state of the g When controlled, pass the `value` and `onValueChange` props to `ToggleButtonGroup.Root`: ```tsx -const [value, setValue] = React.useState(['italics']); +const [value, setValue] = React.useState(['align-center']); return ( @@ -114,6 +114,8 @@ Use the `direction` prop to configure a RTL toggle button group: {/* toggle buttons */} ``` + + ## Accessibility - The `Root` component, and every `Item` must be given must be given an accessible name with `aria-label` or `aria-labelledby`. diff --git a/docs/data/components/toggle-button/styles.module.css b/docs/data/components/toggle-button/styles.module.css index 13cf86e1d..1c18fed1c 100644 --- a/docs/data/components/toggle-button/styles.module.css +++ b/docs/data/components/toggle-button/styles.module.css @@ -20,7 +20,7 @@ } .button:focus-visible { - outline: 2px solid var(--gray-900); + outline: 2px solid var(--color-violet); } .icon { diff --git a/packages/mui-base/src/ToggleButtonGroup/Item/ToggleButtonGroupItem.test.tsx b/packages/mui-base/src/ToggleButtonGroup/Item/ToggleButtonGroupItem.test.tsx index 843d71837..beb639a8a 100644 --- a/packages/mui-base/src/ToggleButtonGroup/Item/ToggleButtonGroupItem.test.tsx +++ b/packages/mui-base/src/ToggleButtonGroup/Item/ToggleButtonGroupItem.test.tsx @@ -5,6 +5,7 @@ import { act } from '@mui/internal-test-utils'; import { ToggleButtonGroup } from '@base_ui/react/ToggleButtonGroup'; import { createRenderer, describeConformance } from '#test-utils'; import { NOOP } from '../../utils/noop'; +import { CompositeRoot } from '../../Composite/Root/CompositeRoot'; import { ToggleButtonGroupRootContext } from '../Root/ToggleButtonGroupRootContext'; const contextValue: ToggleButtonGroupRootContext = { @@ -21,7 +22,7 @@ describe('', () => { render: (node) => render( - {node} + {node} , ), })); diff --git a/packages/mui-base/src/ToggleButtonGroup/Item/ToggleButtonGroupItem.tsx b/packages/mui-base/src/ToggleButtonGroup/Item/ToggleButtonGroupItem.tsx index 4a3efc58e..338ef851b 100644 --- a/packages/mui-base/src/ToggleButtonGroup/Item/ToggleButtonGroupItem.tsx +++ b/packages/mui-base/src/ToggleButtonGroup/Item/ToggleButtonGroupItem.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { BaseUIComponentProps } from '../../utils/types'; +import { CompositeItem } from '../../Composite/Item/CompositeItem'; import { useToggleButtonGroupRootContext } from '../Root/ToggleButtonGroupRootContext'; import { useToggleButtonGroupItem } from './useToggleButtonGroupItem'; @@ -67,7 +68,7 @@ const ToggleButtonGroupItem = React.forwardRef(function ToggleButtonGroupItem( extraProps: otherProps, }); - return renderElement(); + return ; }); export { ToggleButtonGroupItem }; diff --git a/packages/mui-base/src/ToggleButtonGroup/Root/ToggleButtonGroupRoot.test.tsx b/packages/mui-base/src/ToggleButtonGroup/Root/ToggleButtonGroupRoot.test.tsx index f3b28e49a..1a99fadc2 100644 --- a/packages/mui-base/src/ToggleButtonGroup/Root/ToggleButtonGroupRoot.test.tsx +++ b/packages/mui-base/src/ToggleButtonGroup/Root/ToggleButtonGroupRoot.test.tsx @@ -1,10 +1,12 @@ import * as React from 'react'; import { expect } from 'chai'; import { spy } from 'sinon'; -import { act, flushMicrotasks } from '@mui/internal-test-utils'; +import { act, describeSkipIf, flushMicrotasks } from '@mui/internal-test-utils'; import { ToggleButtonGroup } from '@base_ui/react/ToggleButtonGroup'; import { createRenderer, describeConformance } from '#test-utils'; +const isJSDOM = /jsdom/.test(window.navigator.userAgent); + describe('', () => { const { render } = createRenderer(); @@ -21,7 +23,7 @@ describe('', () => { describe('uncontrolled', () => { it('pressed state', async function test(t = {}) { - if (/jsdom/.test(window.navigator.userAgent)) { + if (isJSDOM) { // @ts-expect-error to support mocha and vitest // eslint-disable-next-line @typescript-eslint/no-unused-expressions this?.skip?.() || t?.skip(); @@ -197,6 +199,117 @@ describe('', () => { }); }); + describeSkipIf(isJSDOM)('keyboard interactions', () => { + it('ltr', async () => { + const { getAllByRole, user } = await render( + + + + , + ); + + const [button1, button2] = getAllByRole('button'); + + await user.keyboard('[Tab]'); + + expect(button1).to.have.attribute('data-active'); + expect(button1).to.have.attribute('tabindex', '0'); + expect(button1).toHaveFocus(); + + await user.keyboard('[ArrowRight]'); + + expect(button2).to.have.attribute('data-active'); + expect(button2).to.have.attribute('tabindex', '0'); + expect(button2).toHaveFocus(); + + await user.keyboard('[ArrowRight]'); + + expect(button1).to.have.attribute('data-active'); + expect(button1).to.have.attribute('tabindex', '0'); + expect(button1).toHaveFocus(); + + await user.keyboard('[ArrowDown]'); + + expect(button2).to.have.attribute('data-active'); + expect(button2).to.have.attribute('tabindex', '0'); + expect(button2).toHaveFocus(); + + await user.keyboard('[ArrowDown]'); + + expect(button1).to.have.attribute('data-active'); + expect(button1).to.have.attribute('tabindex', '0'); + expect(button1).toHaveFocus(); + }); + + it('rtl', async () => { + const { getAllByRole, user } = await render( + + + + , + ); + + const [button1, button2] = getAllByRole('button'); + + await user.keyboard('[Tab]'); + + expect(button1).to.have.attribute('data-active'); + expect(button1).to.have.attribute('tabindex', '0'); + expect(button1).toHaveFocus(); + + await user.keyboard('[ArrowLeft]'); + + expect(button2).to.have.attribute('data-active'); + expect(button2).to.have.attribute('tabindex', '0'); + expect(button2).toHaveFocus(); + + await user.keyboard('[ArrowLeft]'); + + expect(button1).to.have.attribute('data-active'); + expect(button1).to.have.attribute('tabindex', '0'); + expect(button1).toHaveFocus(); + + await user.keyboard('[ArrowDown]'); + + expect(button2).to.have.attribute('data-active'); + expect(button2).to.have.attribute('tabindex', '0'); + expect(button2).toHaveFocus(); + + await user.keyboard('[ArrowDown]'); + + expect(button1).to.have.attribute('data-active'); + expect(button1).to.have.attribute('tabindex', '0'); + expect(button1).toHaveFocus(); + }); + + ['Enter', 'Space'].forEach((key) => { + it(`key: ${key} toggles the pressed state`, async () => { + const { getAllByRole, user } = await render( + + + + , + ); + + const [button1] = getAllByRole('button'); + + expect(button1).to.have.attribute('aria-pressed', 'false'); + + await act(async () => { + button1.focus(); + }); + + await user.keyboard(`[${key}]`); + + expect(button1).to.have.attribute('aria-pressed', 'true'); + + await user.keyboard(`[${key}]`); + + expect(button1).to.have.attribute('aria-pressed', 'false'); + }); + }); + }); + describe('prop: onValueChange', () => { it('fires when an Item is clicked', async () => { const onValueChange = spy(); @@ -223,46 +336,44 @@ describe('', () => { expect(onValueChange.args[1][0]).to.deep.equal(['two']); }); - describe('keypresses', () => { - ['Enter', 'Space'].forEach((key) => { - it(`fires when when the ${key} is pressed`, async function test(t = {}) { - if (/jsdom/.test(window.navigator.userAgent)) { - // @ts-expect-error to support mocha and vitest - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - this?.skip?.() || t?.skip(); - } + ['Enter', 'Space'].forEach((key) => { + it(`fires when when the ${key} is pressed`, async function test(t = {}) { + if (isJSDOM) { + // @ts-expect-error to support mocha and vitest + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this?.skip?.() || t?.skip(); + } - const onValueChange = spy(); + const onValueChange = spy(); - const { getAllByRole, user } = await render( - - - - , - ); + const { getAllByRole, user } = await render( + + + + , + ); - const [button1, button2] = getAllByRole('button'); + const [button1, button2] = getAllByRole('button'); - expect(onValueChange.callCount).to.equal(0); + expect(onValueChange.callCount).to.equal(0); - await act(async () => { - button1.focus(); - }); + await act(async () => { + button1.focus(); + }); - await user.keyboard(`[${key}]`); + await user.keyboard(`[${key}]`); - expect(onValueChange.callCount).to.equal(1); - expect(onValueChange.args[0][0]).to.deep.equal(['one']); + expect(onValueChange.callCount).to.equal(1); + expect(onValueChange.args[0][0]).to.deep.equal(['one']); - await act(async () => { - button2.focus(); - }); + await act(async () => { + button2.focus(); + }); - await user.keyboard(`[${key}]`); + await user.keyboard(`[${key}]`); - expect(onValueChange.callCount).to.equal(2); - expect(onValueChange.args[1][0]).to.deep.equal(['two']); - }); + expect(onValueChange.callCount).to.equal(2); + expect(onValueChange.args[1][0]).to.deep.equal(['two']); }); }); }); diff --git a/packages/mui-base/src/ToggleButtonGroup/Root/ToggleButtonGroupRoot.tsx b/packages/mui-base/src/ToggleButtonGroup/Root/ToggleButtonGroupRoot.tsx index 21fa7b590..cae2cba70 100644 --- a/packages/mui-base/src/ToggleButtonGroup/Root/ToggleButtonGroupRoot.tsx +++ b/packages/mui-base/src/ToggleButtonGroup/Root/ToggleButtonGroupRoot.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useComponentRenderer } from '../../utils/useComponentRenderer'; import type { BaseUIComponentProps } from '../../utils/types'; +import { CompositeRoot } from '../../Composite/Root/CompositeRoot'; import { useToggleButtonGroupRoot, type UseToggleButtonGroupRoot, @@ -86,7 +87,7 @@ const ToggleButtonGroupRoot = React.forwardRef(function ToggleButtonGroupRoot( return ( - {renderElement()} + ); });