Опубликовано: 7 марта 2025 г.
Кнопки необходимы для создания динамических веб-приложений. Кнопки открывают меню, переключают действия и отправляют формы. Они обеспечивают основу интерактивности в сети. Создание простых и доступных кнопок может привести к некоторым неожиданным проблемам. Разработчики, создающие микроинтерфейсы или компонентные системы, могут столкнуться с решениями, которые становятся более сложными, чем необходимо. Хотя фреймворки и помогают, Интернет может сделать здесь больше.
В Chrome 135 представлены новые возможности для обеспечения декларативного поведения с помощью новых атрибутов command
и commandfor
, а также улучшения и замены атрибутов popovertargetaction
и popovertarget
. Эти новые атрибуты можно добавлять к кнопкам, позволяя браузеру решать некоторые основные проблемы, связанные с простотой и доступностью, а также обеспечивать встроенную общую функциональность.
Традиционные узоры
Создание поведения кнопок без фреймворка может создать некоторые интересные проблемы по мере развития производственного кода. Хотя HTML предлагает обработчики onclick
для кнопок, они часто запрещены за пределами демонстраций или учебных пособий из-за правил политики безопасности контента (CSP). Хотя эти события отправляются на элементы кнопок, кнопки обычно размещаются на странице для управления другими элементами, требующими кода для управления двумя элементами одновременно. Вам также необходимо обеспечить доступность этого взаимодействия для пользователей вспомогательных технологий. Это часто приводит к тому, что код выглядит примерно так:
<div class="menu-wrapper">
<button class="menu-opener" aria-expanded="false">
Open Menu
</button>
<div popover class="menu-content">
<!-- ... -->
</div>
</div>
<script type="module">
document.addEventListener('click', e => {
const button = e.target;
if (button.matches('.menu-opener')) {
const menu = button
.closest('.menu-wrapper')
.querySelector('.menu-content');
if (menu) {
button.setAttribute('aria-expanded', 'true');
menu.showPopover();
menu.addEventListener('toggle', e => {
// reset back to aria-expanded=false on close
if (e.newState == 'closed') {
button.setAttribute('aria-expanded', 'false');
}
}, {once: true})
}
}
});
</script>
Этот подход может быть немного хрупким, а фреймворки направлены на улучшение эргономики. Распространенный шаблон в таких платформах, как React, может включать сопоставление щелчка с изменением состояния:
function MyMenu({ children }) {
const [isOpen, setIsOpen] = useState(false);
const open = useCallback(() => setIsOpen(true), []);
const handleToggle = useCallback((e) => {
// popovers have light dismiss which influences our state
setIsOpen(e.newState === 'open')
}, []);
const popoverRef = useRef(null);
useEffect(() => {
if (popoverRef.current) {
if (isOpen) {
popoverRef.current.showPopover();
} else {
popoverRef.current.hidePopover();
}
}
}, [popoverRef, isOpen]);
return (
<>
<button onClick={open} aria-expanded={isOpen}>
Open Menu
</button>
<div popover onToggle={handleToggle} ref={popoverRef}>
{children}
</div>
</>
);
}
Многие другие фреймворки также стремятся обеспечить аналогичную эргономику, например, в AlpineJS это можно записать так:
<div x-data="{open: false}">
<button @click="open = !open; $refs.popover.showPopover()" :aria-expanded="open">
Open Menu
</button>
<div popover x-ref="popover" @toggle="open = $event.newState === 'open'">
<!-- ... -->
</div>
</div>
Написание этого в Svelte может выглядеть примерно так:
<script>
let popover;
let open = false;
function togglePopover() {
open ? popover.hidePopover() : popover.showPopover();
open = !open;
}
</script>
<button on:click={togglePopover} aria-expanded={open}>
Open Menu
</button>
<div bind:this={popover} popover>
<!-- ... -->
</div>
Некоторые системы или библиотеки дизайна могут пойти еще дальше, предоставляя оболочки вокруг элементов кнопок, инкапсулирующие изменения состояния. Это абстрагирует изменения состояния за компонентом триггера, жертвуя небольшой гибкостью для улучшения эргономики:
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
Команда и команда для шаблона
Благодаря атрибутам command
и commandfor
кнопок теперь можно декларативно выполнять действия над другими элементами, обеспечивая эргономику структуры без ущерба для гибкости. Кнопка commandfor
принимает идентификатор, аналогичный атрибуту for
, а command
принимает встроенные значения, что обеспечивает более переносимый и интуитивно понятный подход.
Пример: кнопка открытия меню с командами и командами.
Следующий HTML-код устанавливает декларативные отношения между кнопкой и меню, что позволяет браузеру обрабатывать логику и доступность за вас. Нет необходимости управлять aria-expanded
или добавлять какой-либо дополнительный JavaScript.
<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
<!-- ... -->
</div>
Сравнение command
и commandfor
с popovertargetaction
и popovertarget
Если вы раньше использовали popover
, возможно, вы знакомы с атрибутами popovertarget
и popovertargetaction
. Они работают аналогично commandfor
и command
соответственно, за исключением того, что они специфичны для всплывающих окон. Атрибуты command
и commandfor
полностью заменяют эти старые атрибуты. Новые атрибуты поддерживают все, что делали старые атрибуты, а также добавляют новые возможности.
Встроенные команды
Атрибут command
имеет набор встроенных вариантов поведения, которые соответствуют различным API для интерактивных элементов:
-
show-popover
: Сопоставляется сel.showPopover()
. -
hide-popover
: Сопоставляется сel.hidePopover()
. -
toggle-popover
: Сопоставляется сel.togglePopover()
. -
show-modal
: Сопоставляется сdialogEl.showModal()
. -
close
: Сопоставляется сdialogEl.close()
.
Эти команды сопоставляются со своими аналогами в JavaScript, а также оптимизируют доступность (например, предоставление aria-details
и aria-expanded
), управление фокусом и многое другое.
Пример: диалоговое окно подтверждения с command
и commandfor
<button commandfor="confirm-dialog" command="show-modal">
Delete Record
</button>
<dialog id="confirm-dialog">
<header>
<h1>Delete Record?</h1>
<button commandfor="confirm-dialog" command="close" aria-label="Close" value="close">
<img role="none" src="/close-icon.svg">
</button>
</header>
<p>Are you sure? This action cannot be undone</p>
<footer>
<button commandfor="confirm-dialog" command="close" value="cancel">
Cancel
</button>
<button commandfor="confirm-dialog" command="close" value="delete">
Delete
</button>
</footer>
</dialog>
Нажатие кнопки «Удалить запись» откроет диалоговое окно в модальном режиме, а нажатие кнопок «Закрыть» , «Отмена » или «Удалить » закроет диалоговое окно, а также отправит в диалоговое окно событие "close"
, которое имеет свойство returnValue
, соответствующее значению кнопки. Это уменьшает потребность в JavaScript, ограничиваясь одним прослушивателем событий в диалоговом окне, чтобы определить, что делать дальше:
dialog.addEventListener("close", (event) => {
if (event.target.returnValue == "cancel") {
console.log("cancel was clicked");
} else if (event.target.returnValue == "close") {
console.log("close was clicked");
} else if (event.target.returnValue == "delete") {
console.log("delete was clicked");
}
});
Пользовательские команды
В дополнение к встроенным командам вы можете определять собственные команды, используя префикс --
. Пользовательские команды отправляют событие "command"
целевому элементу (так же, как встроенные команды), но в противном случае никогда не будут выполнять какую-либо дополнительную логику, как это делают встроенные значения. Это дает гибкость при создании компонентов, которые могут реагировать на кнопки по-разному, без необходимости предоставления компонентов-оболочек, перемещения по DOM для целевого элемента или сопоставления щелчков кнопок с изменениями состояния. Это позволяет вам предоставить API в HTML для ваших компонентов:
<button commandfor="the-image" command="--rotate-landscape">
Landscape
</button>
<button commandfor="the-image" command="--rotate-portrait">
Portrait
</button>
<img id="the-image" src="photo.jpg">
<script type="module">
const image = document.getElementById("the-image");
image.addEventListener("command", (event) => {
if ( event.command == "--rotate-landscape" ) {
image.style.rotate = "-90deg"
} else if ( event.command == "--rotate-portrait" ) {
image.style.rotate = "0deg"
}
});
</script>
Команды в ShadowDOM
Поскольку атрибут commandfor
принимает идентификатор, существуют ограничения на пересечение теневого DOM. В этих случаях вы можете использовать API JavaScript для установки свойства .commandForElement
, которое может устанавливать любой элемент в теневых корнях:
<my-element>
<template shadowrootmode=open>
<button command="show-popover">Show popover</button>
<slot></slot>
</template>
<div popover><!-- ... --></div>
</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
const popover = this.querySelector('[popover]');
// The commandForElement can set cross-shadow root elements.
this.shadowRoot.querySelector('button').commandForElement = popover;
}
});
</script>
Будущие предложения могут предоставить декларативный способ обмена ссылками через теневые границы, например, « Эталонное целевое предложение» .
Что дальше?
Мы продолжим изучать возможности новых встроенных команд, чтобы охватить общие функции, используемые веб-сайтами. Предлагаемые идеи описаны в Open UI Proposal . Некоторые идеи уже рассмотрены:
- Открытие и закрытие элементов
<details>
. - Команда
"show-picker"
для элементов<input>
и<select>
, сопоставленная сshowPicker()
. - Команды воспроизведения для элементов
<video>
и<audio>
. - Копирование текстового контента из элементов.
Мы приветствуем мнение сообщества — если у вас есть предложения, не стесняйтесь сообщать о проблеме в Open UI Issue Tracker .
Узнать больше
Дополнительную информацию о command
и commandfor
можно найти в спецификации и на MDN .