Модальное окно

Модальное окно — это элемент popover. Любая кнопка, чей popovertarget совпадает с id окна, открывает его — и нигде никакого скрипта, включая затемнение, закрытие по клику снаружи, Escape и работу с фокусом: всем этим владеет браузер.

Поэтому id обязателен, и поэтому без него компонент бросает ошибку: id — это и есть вся проводка.

Простое окно

Каждое модальное окно на этой странице по-настоящему открывается — попробуйте.

fragment(
  button({ popovertarget: 'demo-basic' }, 'Открыть окно'),
  modal({ id: 'demo-basic', title: 'Пересобрать сайт?' },
    'Это запустит sitelo build и заново опубликует dist/.',
  ),
)

С подвалом

Кнопка закрытия — это любая кнопка, указывающая на тот же id с popovertargetaction="hide".

fragment(
  button({ color: 'danger', popovertarget: 'demo-confirm' }, 'Удалить страницу…'),
  modal({
    id: 'demo-confirm',
    title: 'Удалить эту страницу?',
    footer: stack({ direction: 'row', gap: 'sm' },
      button({
        variant: 'ghost',
        color: 'neutral',
        popovertarget: 'demo-confirm',
        popovertargetaction: 'hide',
      }, 'Отмена'),
      button({ color: 'danger' }, 'Удалить'),
    ),
  }, 'Это не отменить. Сгенерированный HTML исчезнет при следующей сборке.'),
)

Размеры

fragment(
  stack({ direction: 'row', gap: 'sm', wrap: true },
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-sm' }, 'Маленькое'),
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-md' }, 'Среднее'),
    button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-lg' }, 'Большое'),
  ),
  modal({ id: 'demo-sm', size: 'sm', title: 'Маленькое' }, 'size: sm — около 24rem.'),
  modal({ id: 'demo-md', title: 'Среднее' }, 'Значение по умолчанию — около 32rem.'),
  modal({ id: 'demo-lg', size: 'lg', title: 'Большое' }, 'size: lg — около 48rem.'),
)

Формы внутри окна

fragment(
  button({ variant: 'soft', popovertarget: 'demo-form' }, 'Новая страница…'),
  modal({
    id: 'demo-form',
    title: 'Новая страница',
    footer: stack({ direction: 'row', gap: 'sm' },
      button({ variant: 'ghost', color: 'neutral', popovertarget: 'demo-form', popovertargetaction: 'hide' }, 'Отмена'),
      button({ type: 'submit' }, 'Создать'),
    ),
  },
    stack({ gap: 'md' },
      textField({ label: 'Заголовок', name: 'modal-title', placeholder: 'О проекте' }),
      selectField({ label: 'Расширение', name: 'modal-ext', options: ['.ht.js', '.ht.ts', '.ht.jsx'] }),
    ),
  ),
)

Без кнопки закрытия

closable: false убирает × в углу. Escape и клик снаружи всё равно закрывают окно: popover нельзя сделать по-настоящему блокирующим, да и это обычно правильное поведение.

fragment(
  button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-bare' }, 'Без кнопки закрытия'),
  modal({ id: 'demo-bare', title: 'Нажмите Escape', closable: false },
    'Или кликните в любом месте за пределами этого диалога.',
  ),
)

Длинное содержимое

Тело прокручивается; шапка и подвал остаются на месте.

fragment(
  button({ variant: 'outline', color: 'neutral', popovertarget: 'demo-long' }, 'Длинное окно'),
  modal({
    id: 'demo-long',
    title: 'Заметки о выпуске',
    footer: button({ popovertarget: 'demo-long', popovertargetaction: 'hide' }, 'Закрыть'),
  },
    stack({ gap: 'md' },
      ...Array.from({ length: 12 }, (unused, index) =>
        text({ variant: 'small', tone: 'muted' }, 'Изменение ' + (index + 1) + ' — что-то починили.'),
      ),
    ),
  ),
)

Прокрутка фона

Страница за открытым окном не прокручивается. Это единственное, что popover API оставляет на вас, и здесь оно сделано на CSS — без скрипта и без всякой инициализации. Передайте lockScroll: false, чтобы фон прокручивался как обычно.

Поддержка браузерами

Popover API есть во всех современных браузерах. В слишком старом, который о нём не знает, окно отрисуется прямо в потоке страницы, а не поверх неё: видимое и рабочее, просто не наложенное сверху. Ничего не пропадает.

Пропсы

ПропТипПо умолчаниюОписание
idstring—Обязателен. То, на что указывает popovertarget кнопки-триггера.
titleChild—Заголовок и доступное имя диалога.
size'sm' | 'md' | 'lg''md'Максимальная ширина.
footerChild—Нижний ряд на собственной подкрашенной полосе.
closablebooleantrueПоказывать × в шапке.
closeLabelstring'Close'Доступное имя этой кнопки.
lockScrollbooleantrueНе давать странице за окном прокручиваться, пока оно открыто.

closeButton({ target }) рисует этот × отдельно — для шапки, которую вы собираете сами.