Переопределение стандартного поведения

Radix UI предоставляет низкоуровневые, доступные компоненты для React, которые можно использовать как строительные блоки для сложных интерфейсов. В основе всех компонентов лежит контролируемое состояние и набор событий, что позволяет переопределять стандартное поведение без нарушения доступности и логики компонента.

Контролируемое и неконтролируемое состояние

Большинство компонентов Radix UI поддерживают контролируемый и неконтролируемый режим.

  • Неконтролируемый режим: компонент управляет состоянием самостоятельно через внутренние механизмы. Например, Accordion открывает и закрывает панели без участия внешнего состояния.
  • Контролируемый режим: состояние компонента управляется извне через пропсы value и колбэки onValueChange. Это позволяет внедрять собственную логику при изменении состояния.

Пример контролируемого Accordion:

import * as Accordion from '@radix-ui/react-accordion';
import { useState } from 'react';

export default function CustomAccordion() {
  const [value, setValue] = useState('item-1');

  return (
    <Accordion.Root value={value} onValueCha nge={setValue}>
      <Accordion.Item value="item-1">
        <Accordion.Header>
          <Accordion.Trigger>Пункт 1</Accordion.Trigger>
        </Accordion.Header>
        <Accordion.Content>Содержимое 1</Accordion.Content>
      </Accordion.Item>
      <Accordion.Item value="item-2">
        <Accordion.Header>
          <Accordion.Trigger>Пункт 2</Accordion.Trigger>
        </Accordion.Header>
        <Accordion.Content>Содержимое 2</Accordion.Content>
      </Accordion.Item>
    </Accordion.Root>
  );
}

В этом примере можно полностью контролировать, какая панель открыта, переопределяя поведение через setValue.


Переопределение событий

Radix UI компоненты используют event forwarding, позволяя подключать свои обработчики к стандартным событиям.

  • Пропсы onClick, onKeyDown, onPointerDown и другие можно использовать одновременно с внутренней логикой Radix.
  • Чтобы предотвратить стандартное поведение, применяется event.preventDefault() и event.stopPropagation(), но нужно учитывать, что Radix часто использует useControllableState и собственные обработчики, которые могут срабатывать после вашего кода.

Пример кастомного закрытия Dialog по нажатию на кнопку с дополнительной логикой:

import * as Dialog from '@radix-ui/react-dialog';

function CustomDialog() {
  const [open, setOpen] = useState(false);

  const handleClose = () => {
    console.log('Диалог закрыт');
    setOpen(false);
  };

  return (
    <Dialog.Root open={open} onOpenCha nge={setOpen}>
      <Dialog.Trigger>Открыть диалог</Dialog.Trigger>
      <Dialog.Content>
        <Dialog.Title>Заголовок</Dialog.Title>
        <Dialog.Description>Описание диалога</Dialog.Description>
        <button onCl ick={handleClose}>Закрыть с логикой</button>
      </Dialog.Content>
    </Dialog.Root>
  );
}

Здесь стандартное поведение Dialog по закрытию через Trigger остаётся доступным, но добавляется пользовательская логика при закрытии через отдельную кнопку.


Замена внутренних элементов через asChild

Radix UI компоненты предоставляют проп asChild, позволяющий использовать любой HTML-элемент или кастомный компонент вместо стандартного рендера. Это особенно полезно для переопределения визуального поведения и поведения событий.

Пример с Button:

import * as DropdownMenu from '@radix-ui/react-dropdown-menu';

export default function CustomDropdown() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <a href="#" onCl ick={(e) => e.preventDefault()}>Меню</a>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item onSel ect={() => console.log('Выбран пункт 1')}>Пункт 1</DropdownMenu.Item>
        <DropdownMenu.Item onSel ect={() => console.log('Выбран пункт 2')}>Пункт 2</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu.Root>
  );
}

Использование asChild позволяет интегрировать компоненты Radix в существующий дизайн, сохраняя при этом логику доступности и управление состоянием.


Кастомизация поведения через onOpenChange и onValueChange

Radix UI предоставляет централизованные колбэки для отслеживания состояния:

  • onOpenChange – используется в компонентах Dialog, Popover, DropdownMenu для отслеживания открытия/закрытия.
  • onValueChange – применяется в Tabs, Accordion, RadioGroup для управления выбором.

Эти колбэки позволяют внедрять:

  • Анимации при открытии/закрытии
  • Валидацию перед сменой состояния
  • Логику синхронизации с глобальным состоянием, например Redux или Zustand

Пример динамической блокировки переключения вкладок:

import * as Tabs from '@radix-ui/react-tabs';
import { useState } from 'react';

export default function ControlledTabs() {
  const [tab, setTab] = useState('tab1');

  const handleTabChange = (newTab) => {
    if (newTab !== 'tab2') {
      setTab(newTab);
    } else {
      alert('Вкладка 2 недоступна');
    }
  };

  return (
    <Tabs.Root value={tab} onValueCha nge={handleTabChange}>
      <Tabs.List>
        <Tabs.Trigger value="tab1">Вкладка 1</Tabs.Trigger>
        <Tabs.Trigger value="tab2">Вкладка 2</Tabs.Trigger>
      </Tabs.List>
      <Tabs.Content value="tab1">Контент 1</Tabs.Content>
      <Tabs.Content value="tab2">Контент 2</Tabs.Content>
    </Tabs.Root>
  );
}

Снижение конфликтов с пользовательским поведением

При переопределении стандартного поведения необходимо учитывать:

  1. События мыши и клавиатуры – Radix обрабатывает их для доступности, поэтому полное подавление может нарушить ARIA-совместимость.
  2. Анимации и фокус – компоненты управляют фокусом при открытии и закрытии. Любое вмешательство должно учитывать сохранение видимости фокуса.
  3. Контролируемое состояние – предпочтительно использовать value и колбэки вместо прямого манипулирования DOM.

Использование слотов и контекстов

Radix UI активно применяет контексты и слоты для передачи логики внутренним компонентам:

  • Slots позволяют передавать элементы в заранее определённые позиции, сохраняя внутреннюю логику компонента.
  • Контексты используют для управления группами элементов (Accordion, Tabs, RadioGroup) без передачи состояния через пропсы вниз по дереву.

Это позволяет переопределять отдельные части поведения, например, добавлять кастомные обработчики на конкретные элементы, сохраняя при этом согласованность состояния всего компонента.


Эти подходы делают Radix UI гибкой платформой для построения сложных интерфейсов, где можно полностью контролировать пользовательский опыт, не нарушая встроенных стандартов доступности и логики компонентов.