تاریخ انتشار: 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
مقادیر داخلی را می پذیرد و رویکرد قابل حمل و بصری تر را امکان پذیر می کند.
مثال: یک دکمه منوی باز با دستور و commandfor
HTML زیر روابطی را بین دکمه و منو تنظیم می کند که به مرورگر اجازه می دهد منطق و دسترسی را برای شما مدیریت کند. نیازی به مدیریت aria-expanded
یا افزودن جاوا اسکریپت اضافی وجود ندارد.
<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()
.
این دستورات به همتایان خود در جاوا اسکریپت نگاشت می شوند، در حالی که دسترسی را ساده می کنند (مانند ارائه 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>
با کلیک بر روی دکمه Delete Record، دیالوگ به صورت مدال باز می شود، در حالی که با کلیک بر روی دکمه های Close ، Cancel یا Delete ، دیالوگ بسته می شود و همچنین یک رویداد "close"
در گفتگو ارسال می شود، که دارای ویژگی returnValue
مطابق با مقدار دکمه است. این امر نیاز به جاوا اسکریپت را فراتر از یک شنونده رویداد در گفتگو کاهش می دهد تا مشخص شود چه کاری باید انجام شود:
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
یک ID می گیرد، محدودیت هایی در مورد عبور از سایه DOM وجود دارد. در این موارد می توانید از JavaScript API برای تنظیم ویژگی .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 بیابید.