Drawer#

Drawer — это боковая панель для предоставления какой-либо информации, не требующей постоянного отображения на странице.

import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@v-uik/drawer'

Свойство

Описание

Значение по умолчанию

1

classes

JSS-классы для стилизации Partial<Record<"root" / "content" / "backdrop" / "nonModalContainer", string>>

-

2

container

HTML-элемент или функция, возвращающая HTML-элемент, в который отрисовываются дочерние элементы `HTMLElement

(() => HTMLElement)`

3

placement

Расположение относительно границ экрана: bottom, top, left, right

right

4

open

Показать/скрыть элемент (boolean)

false

5

height

Высота элемента (string / number)

6

width

Ширина элемента (string / number)

-

7

contentProps

Свойства HTML-элемента панели (Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "slot" / "style" / "title" / ... 251 more ... / "onTransitionEndCapture"> & { ...; })

8

backdrop

Показать/скрыть затемнение фона (boolean)

true

9

bodyScrollLock

Флаг блокировки скролла страницы (boolean)

true

10

backdropProps

HTML-аттрибуты элемента фона окна (HTMLAttributes<HTMLDivElement> & { ref?: ((instance: HTMLDivElement / null) => void) / RefObject<HTMLDivElement> / null; })

11

disableEscapePressHandler

Отключить срабатывание обработчика onClose при нажатии клавиши Esc (boolean)

12

disableBackdropClickHandler

Отключить срабатывание обработчика onClose при клике за пределами панели (boolean)

13

onClose

Обработчик закрытия окна ((event: KeyboardEvent<HTMLDivElement> / MouseEvent<HTMLButtonElement, MouseEvent> / MouseEvent<HTMLDivElement, MouseEvent>) => void)

DrawerHeader#

Компонент DrawerHeader используется для отображения заголовка боковой панели.

Свойство

Описание

Значение по умолчанию

1

classes

JSS-классы для стилизации Partial<Record<"title" / "root" / "divider" / "closeButton" / "withCloseButton" / "subtitle", string>>

-

2

onClose

Обработчик нажатия кнопки закрытия ((event: MouseEvent<HTMLButtonElement, MouseEvent>) => void)

3

subtitle

Подзаголовок модального окна (ReactNode)

4

titleProps

Свойства компонента заголовка (TextProps)

5

subtitleProps

Свойства компонента подзаголовка (TextProps)

6

showCloseButton

Отображать ли кнопку закрытия (boolean)

true

7

closeButtonProps

Свойства кнопки закрытия (ButtonProps)

8

dividerProps

Свойства элемента-разделителя HTMLAttributes

DrawerBody#

Компонент DrawerBody используется для отображения содержимого боковой панели.

DrawerFooter#

Компонент DrawerFooter используется для отображения футера боковой панели.

Свойство

Описание

Значение по умолчанию

1

dividerProps

Свойства элемента-разделителя HTMLAttributes<HTMLHRElement>

-

Базовый пример#

import * as React from 'react'
import { Button } from '@v-uik/button'
import { Text } from '@v-uik/typography'
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@v-uik/drawer'

export const BasicDrawer = (): JSX.Element => {
  const [open, setOpen] = React.useState(false)

  const handleClose = () => setOpen(false)

  return (
    <>
      <Button onClick={() => setOpen(!open)}>показать drawer</Button>
      <Drawer open={open} onClose={handleClose}>
        <DrawerHeader
          subtitle="Подзаголовок"
          closeButtonProps={{
            'aria-label': 'Close drawer',
          }}
          onClose={handleClose}
        >
          Заголовок
        </DrawerHeader>
        <DrawerBody>
          <Text>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
            beatae consectetur dolore doloremque doloribus earum enim, ex
            exercitationem facere natus nisi nostrum repellat repudiandae rerum,
            sit tenetur velit voluptate.
          </Text>
        </DrawerBody>
        <DrawerFooter>
          <Button kind="outlined" onClick={handleClose}>
            Закрыть
          </Button>
        </DrawerFooter>
      </Drawer>
    </>
  )
}

Варианты расположения на странице#

С помощью свойства position можно выбрать, с какой стороны экрана будет появляться боковая панель: сверху, снизу, справа или слева.

import * as React from 'react'
import { Button } from '@v-uik/button'
import { Text } from '@v-uik/typography'
import {
  Drawer,
  DrawerPlacementType,
  DrawerHeader,
  DrawerBody,
  DrawerFooter,
} from '@v-uik/drawer'

export const PositionsExample = (): JSX.Element => {
  const [opened, setOpened] = React.useState<{
    [key in DrawerPlacementType]: boolean
  }>({
    left: false,
    right: false,
    top: false,
    bottom: false,
  })

  return (
    <>
      {(['left', 'right', 'top', 'bottom'] as const).map((placement) => {
        const toggle = (val: boolean) =>
          setOpened({
            ...opened,
            [placement]: val,
          })

        const handleClose = () => toggle(false)

        return (
          <React.Fragment key={placement}>
            <Button style={{ marginRight: 16 }} onClick={() => toggle(true)}>
              {placement}
            </Button>
            <Drawer
              open={opened[placement]}
              placement={placement}
              onClose={handleClose}
            >
              <DrawerHeader showCloseButton={false}>{placement}</DrawerHeader>

              <DrawerBody>
                <Text>
                  Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ad
                  enim hic, inventore nihil quas soluta veritatis! Autem,
                  blanditiis, consectetur dolorum ea in laudantium non, omnis
                  perferendis porro praesentium quaerat sint.
                </Text>
              </DrawerBody>

              <DrawerFooter>
                <Button kind="outlined" onClick={handleClose}>
                  Закрыть
                </Button>
                <Button onClick={handleClose}>Подтвердить</Button>
              </DrawerFooter>
            </Drawer>
          </React.Fragment>
        )
      })}
    </>
  )
}

Пример без блокировки страницы#

Установив свойству backdrop значение false, можно отключить затемнение фона. При указании свойства bodyScrollLock = false отключается блокировка скролла страницы. Заметьте, что компонент будет помещен в текущее место в DOM-дереве, тогда как вариант по умолчанию отрисовывает панель в конце документа.

import * as React from 'react'
import { Button } from '@v-uik/button'
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@v-uik/drawer'
import { Text } from '@v-uik/typography'

export const NonModalExample = (): JSX.Element => {
  const [open, setOpen] = React.useState(false)

  const handleClose = () => setOpen(false)

  return (
    <>
      <Button onClick={() => setOpen(!open)}>показать drawer</Button>
      <Drawer
        backdrop={false}
        bodyScrollLock={false}
        open={open}
        onClose={handleClose}
      >
        <DrawerHeader onClose={handleClose}>Заголовок</DrawerHeader>
        <DrawerBody>
          <Text>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
            beatae consectetur dolore doloremque doloribus earum enim, ex
            exercitationem facere natus nisi nostrum repellat repudiandae rerum,
            sit tenetur velit voluptate.
          </Text>
        </DrawerBody>
        <DrawerFooter>
          <Button kind="outlined" onClick={handleClose}>
            Закрыть
          </Button>
        </DrawerFooter>
      </Drawer>
    </>
  )
}

Панель для пользовательского элемента#

С помощью свойства container можно отобразить боковую панель внутри любого элемента DOM-дерева.

import * as React from 'react'
import { Table, ColumnProps, RecordDataSource } from '@v-uik/table'
import { Drawer, DrawerHeader, DrawerBody } from '@v-uik/drawer'
import { Link } from '@v-uik/link'
import { Text } from '@v-uik/typography'

type DataSource = RecordDataSource<{
  name: string
  role: string
  email: string
  phone: string
  city: string
}>

const dataSource: DataSource[] = [
  {
    key: 1,
    name: 'Иван Иванов',
    role: 'разработчик',
    email: 'ivan@example.ru',
    phone: '+79991110000',
    city: 'Москва',
  },
  {
    key: 2,
    name: 'Петр Петров',
    role: 'дизайнер',
    email: 'petr@example.ru',
    phone: '+79876543210',
    city: 'Санкт-Петербург',
  },
  {
    key: 3,
    name: 'Николай Николаев',
    role: 'менеджер',
    email: 'nikolay@example.ru',
    phone: '+79991234567',
    city: 'Москва',
  },
]

export const ContainerExample = (): JSX.Element => {
  const [tableMounted, setTableMounted] = React.useState(false)

  const tableRef = React.useRef<HTMLDivElement>(null)

  const [currentProfile, setCurrentProfile] = React.useState<DataSource>()

  React.useEffect(() => {
    setTableMounted(true)
  }, [setTableMounted])

  const columns: ColumnProps<DataSource>[] = [
    {
      key: 'name',
      dataIndex: 'name',
      title: 'Имя',
    },
    {
      key: 'role',
      dataIndex: 'role',
      title: 'Роль',
    },
    {
      key: 'profile',
      dataIndex: 'profile',
      renderCellContent: ({ originClassName, row }) => {
        return (
          <div className={originClassName}>
            <Link
              tabIndex={0}
              onKeyDown={(event: React.KeyboardEvent<HTMLAnchorElement>) => {
                if (event.key === ' ' || event.key === 'Enter') {
                  event.preventDefault()
                  setCurrentProfile(row)
                }
              }}
              onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
                event.preventDefault()
                setCurrentProfile(row)
              }}
            >
              доп. информация
            </Link>
          </div>
        )
      },
    },
  ]

  const handleClose = () => setCurrentProfile(undefined)

  const open = !!currentProfile

  return (
    <>
      <Table
        ref={tableRef}
        style={{
          position: 'relative',
          overflowX: 'hidden',
        }}
        columns={columns}
        dataSource={dataSource}
      />

      {tableMounted && (
        <Drawer
          style={{
            position: 'absolute',
          }}
          backdrop={false}
          bodyScrollLock={false}
          container={tableRef.current ?? undefined}
          open={open}
          onClose={handleClose}
        >
          <DrawerHeader
            subtitle={currentProfile?.role}
            dividerProps={{ style: { display: 'none' } }}
            onClose={handleClose}
          >
            {currentProfile?.name}
          </DrawerHeader>

          <DrawerBody style={{ flexDirection: 'column' }}>
            <Text kind="body1">{currentProfile?.email}</Text>
            <Text kind="body1">{currentProfile?.phone}</Text>
          </DrawerBody>
        </Drawer>
      )}
    </>
  )
}

Панель для пользовательского элемента со скроллом#

Для того чтобы использовать Drawer на скроллящемся элементе, рекомендуется обернуть его каким-либо элементом-контейнером, и уже его передавать свойству container.

import * as React from 'react'
import { Table, ColumnProps, RecordDataSource } from '@v-uik/table'
import { Drawer, DrawerHeader, DrawerBody } from '@v-uik/drawer'
import { Link } from '@v-uik/link'
import { Text } from '@v-uik/typography'

type DataSource = RecordDataSource<{
  name: string
  role: string
  email: string
  phone: string
  city: string
}>

const dataSource: DataSource[] = [
  {
    key: 1,
    name: 'Иван Иванов',
    role: 'разработчик',
    email: 'ivan@example.ru',
    phone: '+79991110000',
    city: 'Москва',
  },
  {
    key: 2,
    name: 'Петр Петров',
    role: 'дизайнер',
    email: 'petr@example.ru',
    phone: '+79876543210',
    city: 'Санкт-Петербург',
  },
  {
    key: 3,
    name: 'Николай Николаев',
    role: 'менеджер',
    email: 'nikolay@example.ru',
    phone: '+79991234567',
    city: 'Москва',
  },
  {
    key: 4,
    name: 'Федор Федоров',
    role: 'разработчик',
    email: 'fedor@example.ru',
    phone: '+79999999999',
    city: 'Казань',
  },
  {
    key: 5,
    name: 'Егор Егоров',
    role: 'разработчик',
    email: 'egor@example.ru',
    phone: '+71234567890',
    city: 'Нижний Новгород',
  },
]

export const ContainerOverflowExample = (): JSX.Element => {
  const [containerMounted, setContainerMounted] = React.useState(false)

  const containerRef = React.useRef<HTMLDivElement>(null)

  const [currentProfile, setCurrentProfile] = React.useState<DataSource>()

  React.useEffect(() => {
    setContainerMounted(true)
  }, [setContainerMounted])

  const columns: ColumnProps<DataSource>[] = [
    {
      key: 'name',
      dataIndex: 'name',
      title: 'Имя',
    },
    {
      key: 'role',
      dataIndex: 'role',
      title: 'Роль',
    },
    {
      key: 'profile',
      dataIndex: 'profile',
      renderCellContent: ({ originClassName, row }) => {
        return (
          <div className={originClassName}>
            <Link
              tabIndex={0}
              onKeyDown={(event: React.KeyboardEvent<HTMLAnchorElement>) => {
                if (event.key === ' ' || event.key === 'Enter') {
                  event.preventDefault()
                  setCurrentProfile(row)
                }
              }}
              onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
                event.preventDefault()
                setCurrentProfile(row)
              }}
            >
              доп. информация
            </Link>
          </div>
        )
      },
    },
  ]

  const handleClose = () => setCurrentProfile(undefined)

  const open = !!currentProfile

  return (
    <div
      ref={containerRef}
      style={{
        position: 'relative',
        overflowX: 'hidden',
      }}
    >
      <Table height={200} columns={columns} dataSource={dataSource} />

      {containerMounted && (
        <Drawer
          style={{
            position: 'absolute',
          }}
          backdrop={false}
          bodyScrollLock={false}
          container={containerRef.current ?? undefined}
          open={open}
          onClose={handleClose}
        >
          <DrawerHeader
            subtitle={currentProfile?.role}
            dividerProps={{ style: { display: 'none' } }}
            onClose={handleClose}
          >
            {currentProfile?.name}
          </DrawerHeader>

          <DrawerBody style={{ flexDirection: 'column' }}>
            <Text kind="body1">{currentProfile?.email}</Text>
            <Text kind="body1">{currentProfile?.phone}</Text>
          </DrawerBody>
        </Drawer>
      )}
    </div>
  )
}

Многоуровневая панель#

Немного стилизовав компоненты Drawer, можно добиться эффекта вложенности панелей.

import * as React from 'react'
import { Button } from '@v-uik/button'
import { Text } from '@v-uik/typography'
import { Drawer, DrawerHeader, DrawerBody, DrawerFooter } from '@v-uik/drawer'

const contentProps_1 = {
  style: {
    transition: 'transform 250ms ease-out',
    transform: 'translateX(-24px)',
  },
}

const contentProps_2 = {
  style: {
    transition: 'transform 250ms ease-out',
    transform: 'translateX(-48px)',
  },
}

const backdropProps = {
  style: {
    opacity: 0,
  },
}

export const MultiLevelExample = (): JSX.Element => {
  const [openedCount, setOpenedCount] = React.useState(0)

  const handleClose = () => setOpenedCount(openedCount - 1)

  const openNext = () => setOpenedCount(openedCount + 1)

  return (
    <>
      <Button onClick={() => setOpenedCount(1)}>показать drawer</Button>
      <Drawer
        open={openedCount > 0}
        backdropProps={openedCount > 1 ? backdropProps : undefined}
        contentProps={
          openedCount === 2
            ? contentProps_1
            : openedCount === 3
            ? contentProps_2
            : undefined
        }
        onClose={handleClose}
      >
        <DrawerHeader onClose={handleClose}>Первый</DrawerHeader>
        <DrawerBody>
          <Text>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
            beatae consectetur dolore doloremque doloribus earum enim, ex
            exercitationem facere natus nisi nostrum repellat repudiandae rerum,
            sit tenetur velit voluptate.
          </Text>
        </DrawerBody>
        <DrawerFooter>
          <Button kind="outlined" onClick={handleClose}>
            Закрыть
          </Button>
          <Button onClick={openNext}>Далее</Button>
        </DrawerFooter>
      </Drawer>

      <Drawer
        open={openedCount > 1}
        backdropProps={openedCount > 2 ? backdropProps : undefined}
        contentProps={openedCount === 3 ? contentProps_1 : undefined}
        onClose={handleClose}
      >
        <DrawerHeader onClose={handleClose}>Второй</DrawerHeader>
        <DrawerBody>
          <Text>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
            beatae consectetur dolore doloremque doloribus earum enim, ex
            exercitationem facere natus nisi nostrum repellat repudiandae rerum,
            sit tenetur velit voluptate.
          </Text>
        </DrawerBody>
        <DrawerFooter>
          <Button kind="outlined" onClick={handleClose}>
            Закрыть
          </Button>
          <Button onClick={openNext}>Далее</Button>
        </DrawerFooter>
      </Drawer>

      <Drawer open={openedCount > 2} onClose={handleClose}>
        <DrawerHeader onClose={handleClose}>Третий</DrawerHeader>
        <DrawerBody>
          <Text>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab animi
            beatae consectetur dolore doloremque doloribus earum enim, ex
            exercitationem facere natus nisi nostrum repellat repudiandae rerum,
            sit tenetur velit voluptate.
          </Text>
        </DrawerBody>
        <DrawerFooter>
          <Button kind="outlined" onClick={handleClose}>
            Закрыть
          </Button>
        </DrawerFooter>
      </Drawer>
    </>
  )
}

Доступность#

Компонент проставляет необходимые ARIA-атрибуты (role, aria-modal) по умолчанию. Для варианта с затемнением фона можно это изменить с помощью свойства contentProps. При открытии меню фокус выставляется на скрытый статичный элемент, от которого затем можно переходить к последующим фокусируемым элементам. На текущий момент нет фокуса для какого-либо видимого элемента, так как рекомендации по работе с фокусом сильно зависят от содержимого компонента (семантика, скролл), поэтому при необходимости установите фокус на нужном элементе самостоятельно.