refactor: remove base ui i18n dependency

This commit is contained in:
yyh
2026-04-10 20:26:04 +08:00
parent 66183c1f0a
commit c8f9723780
4 changed files with 23 additions and 25 deletions

View File

@ -172,13 +172,13 @@ describe('NumberField wrapper', () => {
// Increment and decrement buttons should preserve accessible naming, icon fallbacks, and spacing variants.
describe('Control buttons', () => {
it('should provide localized aria labels and default icons when labels are not provided', () => {
it('should provide english fallback aria labels and default icons when labels are not provided', () => {
renderNumberField({
controlsProps: {},
})
const increment = screen.getByRole('button', { name: 'common.operation.increment' })
const decrement = screen.getByRole('button', { name: 'common.operation.decrement' })
const increment = screen.getByRole('button', { name: 'Increment value' })
const decrement = screen.getByRole('button', { name: 'Decrement value' })
expect(increment.querySelector('.i-ri-arrow-up-s-line')).toBeInTheDocument()
expect(decrement.querySelector('.i-ri-arrow-down-s-line')).toBeInTheDocument()
@ -217,11 +217,11 @@ describe('NumberField wrapper', () => {
},
})
expect(screen.getByRole('button', { name: 'common.operation.increment' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'common.operation.decrement' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Increment value' })).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Decrement value' })).toBeInTheDocument()
})
it('should rely on aria-labelledby when provided instead of injecting a translated aria-label', () => {
it('should rely on aria-labelledby when provided instead of injecting a fallback aria-label', () => {
render(
<>
<span id="increment-label">Increment from label</span>

View File

@ -4,7 +4,6 @@ import type { VariantProps } from 'class-variance-authority'
import { NumberField as BaseNumberField } from '@base-ui/react/number-field'
import { cva } from 'class-variance-authority'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
export const NumberField = BaseNumberField.Root
@ -188,18 +187,19 @@ type NumberFieldButtonVariantProps = Omit<
export type NumberFieldButtonProps = React.ComponentPropsWithoutRef<typeof BaseNumberField.Increment> & NumberFieldButtonVariantProps
const incrementAriaLabel = 'Increment value'
const decrementAriaLabel = 'Decrement value'
export function NumberFieldIncrement({
className,
children,
size = 'regular',
...props
}: NumberFieldButtonProps) {
const { t } = useTranslation()
return (
<BaseNumberField.Increment
{...props}
aria-label={props['aria-label'] ?? (props['aria-labelledby'] ? undefined : t('operation.increment', { ns: 'common' }))}
aria-label={props['aria-label'] ?? (props['aria-labelledby'] ? undefined : incrementAriaLabel)}
className={cn(numberFieldControlButtonVariants({ size, direction: 'increment' }), className)}
>
{children ?? <span aria-hidden="true" className="i-ri-arrow-up-s-line size-3" />}
@ -213,12 +213,10 @@ export function NumberFieldDecrement({
size = 'regular',
...props
}: NumberFieldButtonProps) {
const { t } = useTranslation()
return (
<BaseNumberField.Decrement
{...props}
aria-label={props['aria-label'] ?? (props['aria-labelledby'] ? undefined : t('operation.decrement', { ns: 'common' }))}
aria-label={props['aria-label'] ?? (props['aria-labelledby'] ? undefined : decrementAriaLabel)}
className={cn(numberFieldControlButtonVariants({ size, direction: 'decrement' }), className)}
>
{children ?? <span aria-hidden="true" className="i-ri-arrow-down-s-line size-3" />}

View File

@ -31,13 +31,13 @@ describe('base/ui/toast', () => {
expect(await screen.findByText('Saved')).toBeInTheDocument()
expect(screen.getByText('Your changes are available now.')).toBeInTheDocument()
const viewport = screen.getByRole('region', { name: 'common.toast.notifications' })
const viewport = screen.getByRole('region', { name: 'Notifications' })
expect(viewport).toHaveAttribute('aria-live', 'polite')
expect(viewport).toHaveClass('z-1101')
expect(viewport.firstElementChild).toHaveClass('top-4')
expect(screen.getByRole('dialog')).not.toHaveClass('outline-hidden')
expect(document.body.querySelector('[aria-hidden="true"].i-ri-checkbox-circle-fill')).toBeInTheDocument()
expect(document.body.querySelector('button[aria-label="common.toast.close"][aria-hidden="true"]')).toBeInTheDocument()
expect(document.body.querySelector('button[aria-label="Close notification"][aria-hidden="true"]')).toBeInTheDocument()
})
// Collapsed stacks should keep multiple toast roots mounted for smooth stack animation.
@ -57,12 +57,12 @@ describe('base/ui/toast', () => {
expect(await screen.findByText('Third toast')).toBeInTheDocument()
expect(screen.getAllByRole('dialog')).toHaveLength(3)
expect(document.body.querySelectorAll('button[aria-label="common.toast.close"][aria-hidden="true"]')).toHaveLength(3)
expect(document.body.querySelectorAll('button[aria-label="Close notification"][aria-hidden="true"]')).toHaveLength(3)
fireEvent.mouseEnter(screen.getByRole('region', { name: 'common.toast.notifications' }))
fireEvent.mouseEnter(screen.getByRole('region', { name: 'Notifications' }))
await waitFor(() => {
expect(document.body.querySelector('button[aria-label="common.toast.close"][aria-hidden="true"]')).not.toBeInTheDocument()
expect(document.body.querySelector('button[aria-label="Close notification"][aria-hidden="true"]')).not.toBeInTheDocument()
})
})
@ -126,9 +126,9 @@ describe('base/ui/toast', () => {
})
})
fireEvent.mouseEnter(screen.getByRole('region', { name: 'common.toast.notifications' }))
fireEvent.mouseEnter(screen.getByRole('region', { name: 'Notifications' }))
const dismissButton = await screen.findByRole('button', { name: 'common.toast.close' })
const dismissButton = await screen.findByRole('button', { name: 'Close notification' })
act(() => {
dismissButton.click()

View File

@ -7,7 +7,6 @@ import type {
} from '@base-ui/react/toast'
import type { ReactNode } from 'react'
import { Toast as BaseToast } from '@base-ui/react/toast'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
type ToastData = Record<string, never>
@ -35,6 +34,9 @@ const TOAST_TONE_STYLES = {
},
} satisfies Record<string, ToastToneStyle>
const toastCloseLabel = 'Close notification'
const toastViewportLabel = 'Notifications'
type ToastType = keyof typeof TOAST_TONE_STYLES
type ToastAddOptions = Omit<ToastManagerAddOptions<ToastData>, 'data' | 'positionerProps' | 'type'> & {
@ -145,7 +147,6 @@ function ToastCard({
}: {
toast: ToastObject<ToastData>
}) {
const { t } = useTranslation('common')
const toastType = getToastType(toastItem.type)
return (
@ -200,7 +201,7 @@ function ToastCard({
</div>
<div className="flex shrink-0 items-center justify-center rounded-md p-0.5">
<BaseToast.Close
aria-label={t('toast.close')}
aria-label={toastCloseLabel}
className={cn(
'flex h-5 w-5 items-center justify-center rounded-md hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
)}
@ -215,12 +216,11 @@ function ToastCard({
}
function ToastViewport() {
const { t } = useTranslation('common')
const { toasts } = BaseToast.useToastManager<ToastData>()
return (
<BaseToast.Viewport
aria-label={t('toast.notifications')}
aria-label={toastViewportLabel}
className={cn(
// During overlay migration, toast must stay above legacy highPriority modals (z-[1100]).
'inset-0 group/toast-viewport pointer-events-none fixed z-1101 overflow-visible',