เผยแพร่: 7 มีนาคม 2025
ปุ่มเป็นองค์ประกอบสําคัญในการสร้างเว็บแอปพลิเคชันแบบไดนามิก ปุ่มเปิดเมนู เปิดตัวดำเนินการ และส่งแบบฟอร์ม ภาษาเหล่านี้เป็นรากฐานของการโต้ตอบบนเว็บ การทำปุ่มให้เรียบง่ายและเข้าถึงได้อาจทำให้เกิดปัญหาบางอย่างที่คาดไม่ถึง นักพัฒนาซอฟต์แวร์ที่สร้างไมโครเฟรนเทจหรือระบบคอมโพเนนต์อาจพบโซลูชันที่ซับซ้อนเกินความจำเป็น แม้ว่าเฟรมเวิร์กจะมีประโยชน์ แต่เว็บก็ทําได้มากกว่า
Chrome 135 เปิดตัวความสามารถใหม่ในการระบุลักษณะการทำงานแบบประกาศด้วยแอตทริบิวต์ command
และ commandfor
ใหม่ ซึ่งจะช่วยปรับปรุงและแทนที่แอตทริบิวต์ popovertargetaction
และ popovertarget
คุณเพิ่มแอตทริบิวต์ใหม่เหล่านี้ลงในปุ่มได้ ซึ่งจะช่วยให้เบราว์เซอร์จัดการปัญหาหลักๆ บางประการเกี่ยวกับความเรียบง่ายและการช่วยเหลือพิเศษ รวมถึงให้ฟังก์ชันการทำงานทั่วไปในตัว
ลวดลายดั้งเดิม
การสร้างลักษณะการทํางานของปุ่มโดยไม่ใช้เฟรมเวิร์กอาจก่อให้เกิดปัญหาที่น่าสนใจเมื่อโค้ดเวอร์ชันที่ใช้งานจริงพัฒนาขึ้น แม้ว่า HTML จะมีตัวแฮนเดิล onclick
สำหรับปุ่ม แต่ระบบมักไม่อนุญาตให้ใช้ตัวแฮนเดิลเหล่านี้นอกการสาธิตหรือบทแนะนำเนื่องจากกฎนโยบายความปลอดภัยของเนื้อหา (CSP) แม้ว่าเหตุการณ์เหล่านี้จะส่งในองค์ประกอบปุ่ม แต่โดยทั่วไปแล้วปุ่มจะวางไว้ในหน้าเว็บเพื่อควบคุมองค์ประกอบอื่นๆ ซึ่งต้องใช้โค้ดเพื่อควบคุมองค์ประกอบ 2 รายการพร้อมกัน นอกจากนี้ คุณต้องตรวจสอบว่าผู้ใช้เทคโนโลยีความช่วยเหลือพิเศษเข้าถึงการโต้ตอบนี้ได้ ซึ่งมักจะทําให้โค้ดมีลักษณะดังนี้
<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>
ระบบการออกแบบหรือไลบรารีบางระบบอาจดำเนินการเพิ่มเติมอีกขั้นด้วยการจัดเตรียม Wrapper ไว้รอบๆ องค์ประกอบปุ่มที่รวมการเปลี่ยนแปลงสถานะ ซึ่งจะแยกการเปลี่ยนแปลงสถานะออกจากคอมโพเนนต์ทริกเกอร์ โดยแลกกับความยืดหยุ่นเล็กน้อยเพื่อปรับปรุงการยศาสตร์
import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
return (
<MenuTrigger>
<button>Open Menu</button>
</MenuTrigger>
<MenuContent>{children}</MenuContent>
);
}
รูปแบบคำสั่งและ commandfor
เมื่อใช้แอตทริบิวต์ command
และ commandfor
ตอนนี้ปุ่มจะดำเนินการกับองค์ประกอบอื่นๆ ได้แบบประกาศ ซึ่งจะเพิ่มประสิทธิภาพของเฟรมเวิร์กโดยไม่ลดทอนความยืดหยุ่น ปุ่ม commandfor
จะใช้รหัส ซึ่งคล้ายกับแอตทริบิวต์ for
ส่วน command
จะยอมรับค่าในตัว ซึ่งช่วยให้ใช้แนวทางที่นําไปใช้ได้จริงและใช้งานง่ายยิ่งขึ้น
ตัวอย่าง: ปุ่มเปิดเมนูที่มีคําสั่งและ commandfor
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"
ไปยังองค์ประกอบเป้าหมาย (เช่นเดียวกับคำสั่งในตัว) แต่จะไม่ดำเนินการตามตรรกะเพิ่มเติมเหมือนค่าในตัว การดำเนินการนี้ช่วยให้คุณสร้างคอมโพเนนต์ที่ตอบสนองต่อปุ่มได้หลายวิธีโดยไม่ต้องระบุคอมโพเนนต์ Wrapper, เรียกใช้ 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 เงา ในกรณีเหล่านี้ คุณสามารถใช้ JavaScript API เพื่อตั้งค่าพร็อพเพอร์ตี้ .commandForElement
ซึ่งสามารถตั้งค่าองค์ประกอบใดก็ได้ใน Shadow Root
<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>
ข้อเสนอในอนาคตอาจระบุวิธีแชร์ข้อมูลอ้างอิงข้ามขอบเขตเงา เช่น ข้อเสนอเป้าหมายอ้างอิง
ขั้นตอนถัดไปคือ
เราจะยังคงสำรวจความเป็นไปได้สำหรับคำสั่งในตัวใหม่ๆ เพื่อครอบคลุมฟังก์ชันการทำงานทั่วไปที่เว็บไซต์ใช้ แนวคิดที่เสนอมีอยู่ในข้อเสนอ UI แบบเปิด ตัวอย่างแนวคิดที่เราได้ลองใช้แล้ว
- การเปิดและปิดองค์ประกอบ
<details>
- คําสั่ง
"show-picker"
สําหรับองค์ประกอบ<input>
และ<select>
ซึ่งแมปกับshowPicker()
- คำสั่งการเล่นสำหรับองค์ประกอบ
<video>
และ<audio>
- การคัดลอกเนื้อหาข้อความจากองค์ประกอบ
เรายินดีรับฟังความคิดเห็นจากชุมชน หากมีข้อเสนอแนะ โปรดแจ้งปัญหาในเครื่องมือติดตามปัญหา UI แบบเปิด
ดูข้อมูลเพิ่มเติม
ดูข้อมูลเพิ่มเติมเกี่ยวกับ command
และ commandfor
ในข้อกำหนดและใน MDN