Перетаскиваемое окно с ресайзом на React
Окно, которое тащится за заголовок и растягивается за любой край, выглядит как вечер с обработчиками mousemove. Вечер превращается в неделю, как только всплывают детали. Окно должно подниматься наверх по клику, упираться в минимальный размер, не уезжать за экран и переживать касания. Потом оно должно прилипать к половине экрана, слушаться клавиатуры и помнить, где стояло. wmkit берёт всё это на себя, а разметку оставляет вам.
Тащите за заголовок, тяните за любой край или угол, бросьте окно к левому или правому краю области — оно прилипнет к половине. Двойной клик по заголовку разворачивает окно. Стрелки двигают окно в фокусе.
Установка
npm i @surdeddd/wmkitПоддерживаются React 18 и 19. У адаптера нет собственных зависимостей.
Компонент целиком
import { useDesktop, useWindowManager, useWmState, useWmWindowRef } from '@surdeddd/wmkit/react'
import type { DesktopBinder, WindowState } from '@surdeddd/wmkit'
import '@surdeddd/wmkit/themes/glass.css'
function Window({ binder, win }: { binder: DesktopBinder; win: WindowState }) {
const ref = useWmWindowRef(binder, win.id)
return (
<section ref={ref}>
<header data-wm-drag>
<span data-wm-title>{win.title}</span>
<span data-wm-controls>
<button data-wm-minimize aria-label="Свернуть" />
<button data-wm-maximize aria-label="Развернуть" />
<button data-wm-close aria-label="Закрыть" />
</span>
</header>
<div data-wm-content>Здесь всё, что рисует React.</div>
</section>
)
}
export function Desktop() {
const wm = useWindowManager()
const { ref, binder } = useDesktop(wm)
const state = useWmState(wm)
return (
<div ref={ref} style={{ position: 'relative', height: '100vh' }}>
<button onClick={() => wm.open({ title: 'Заметки', width: 360, height: 240, minWidth: 220 })}>
Новое окно
</button>
{Object.values(state.windows).map((win) => (
<Window key={win.id} binder={binder} win={win} />
))}
</div>
)
}data-wm-drag отмечает ручку перетаскивания, в data-wm-title попадает заголовок, три кнопки работают по одному атрибуту. Ручки ресайза со всех восьми сторон добавляются сами. Перебирайте state.windows, а не state.order: order — это порядок стопки, он меняется на каждый клик, а перестановка DOM-узлов посреди клика клик теряет. Ключи state.windows идут в порядке открытия, пока id окон не являются просто числами (почему).
Что достаётся без единой строки
- Перетаскивание и ресайз с захватом указателя и одной записью стилей на кадр, так что указатель не теряется ни над iframe, ни за пределами окна.
- Ограничения:
minWidth,minHeight,maxWidth,maxHeightиaspectRatioсоблюдаются при любом движении, включая ресайз с клавиатуры и снэп. - Фокус и стопка: нажатие поднимает окно, фокус переходит в него,
Tabне выходит из модального окна,F6перебирает окна. - Снэп к половинам, четвертям и третям с живым превью; если потянуть прилипшее окно, оно отрывается прямо под курсором.
- Клавиатура: стрелки двигают, Shift+стрелки меняют размер, Ctrl/⌘+Alt+стрелки прилепляют к краю, Ctrl/⌘+Alt+PageDown/PageUp переключают вкладки, Ctrl/⌘+Z отменяет последнее действие.
- Скринридеры слышат, что произошло («Notes maximized», «Notes snapped to left»), через один live-регион, и все фразы можно заменить своими.
- Отмена и повтор: всё перетаскивание — один шаг истории.
Читать и менять положение
Состояние окна — обычные данные. Читайте его хуком, меняйте через менеджер:
import { useWmWindow } from '@surdeddd/wmkit/react'
function Position({ wm }) {
const notes = useWmWindow(wm, 'notes')
if (!notes) return null
return (
<p>
{Math.round(notes.bounds.x)}, {Math.round(notes.bounds.y)}
<button onClick={() => wm.move('notes', 0, 0)}>в левый верхний угол</button>
<button onClick={() => wm.snap('notes', 'right')}>правая половина</button>
</p>
)
}useWmWindow перерисовывает компонент, только когда меняется именно это окно, поэтому панель с координатами не тянет за собой перерисовку всей страницы во время перетаскивания.
Запомнить раскладку
import { persist } from '@surdeddd/wmkit/persist'
persist(wm, { key: 'my-app-windows' })Положение, размеры, стадии и порядок стопки сохраняются в localStorage и возвращаются при следующем визите.
Next.js и серверный рендер
Менеджер — это данные, он работает и на сервере. DOM-часть подключается в эффекте, поэтому в App Router компонент с рабочим столом помечается 'use client'. Больше ничего менять не нужно.
Чем это отличается от react-rnd и react-draggable
react-draggable двигает элемент, react-rnd добавляет ему ручки ресайза, и обе делают ровно это и делают хорошо. Стопка, фокус, снэп, сворачивание и разворачивание, доступ с клавиатуры и сохранение остаются на вас, а когда окон на экране несколько, основная работа как раз в этом. wmkit — оконный менеджер, и движение в нём — самая маленькая часть. Если нужна одна коробка, которая двигается, react-draggable легче. Если вы делаете окна, начинайте отсюда.
Дальше
- Адаптеры — все хуки React и их контракты
- Темы — шестнадцать тем или собственные стили по атрибутам
data-wm-* - Рабочий стол Windows 95 на React — таскбар, кнопка «Пуск» и тема retro