تعريف متغيرَي command وcommandfor

تاريخ النشر: 7 آذار (مارس) 2025

الأزرار ضرورية لإنشاء تطبيقات ويب ديناميكية. تفتح الأزرار القوائم، ويُستخدَم الزرّ لتفعيل الإجراءات وإيقافها وإرسال النماذج. وهي تشكل أساس التفاعل على الويب. يمكن أن يؤدي جعل الأزرار بسيطة وسهلة الاستخدام إلى بعض الصعوبات العميقة. يمكن أن يواجه المطوّرون الذين يبنون واجهات أمامية صغيرة أو أنظمة مكوّنات حلولًا تصبح أكثر تعقيدًا من اللازم. على الرغم من أنّ الأطر المرجعية مفيدة، يمكن للويب تقديم المزيد من المساعدة.

يقدّم الإصدار 135 من Chrome إمكانات جديدة لتوفير سلوك تعريفي باستخدامسمتَي command وcommandfor الجديدتَين، ما يؤدي إلى تحسين سمتَي popovertargetaction وpopovertarget واستبدالهما. يمكن إضافة هذه السمات الجديدة إلى الأزرار، ما يتيح للمتصفّح معالجة بعض المشاكل الأساسية المتعلّقة بالسلاسة وسهولة الاستخدام، وتوفير وظائف شائعة مدمجة.

الأنماط التقليدية

يمكن أن يشكّل إنشاء سلوكيات الأزرار بدون إطار عمل بعض الصعوبات المثيرة للاهتمام مع تطور رمز الإنتاج. على الرغم من أنّ HTML يوفّر onclick معالِجات ل buttons، غالبًا ما يتم حظر هذه المعالِجات خارج العروض التوضيحية أو الأدلة التعليمية بسبب قواعد Content security policy (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>
  );
}

نمط الأمر و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 على مجموعة من السلوكيات المضمّنة التي يتم ربطها بواجهات برمجة تطبيقات مختلفة للعناصر التفاعلية:

  • 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 للعنصر المستهدَف أو ربط نقرات الأزرار بتغييرات الحالة. يتيح لك ذلك توفير واجهة برمجة تطبيقات ضمن 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 التي يمكنها ضبط أي عنصر على مستوى جذور الظل:

<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>

قد تقدّم الاقتراحات المستقبلية طريقة توضيحية لمشاركة المراجع على مستوى حدود الظل، مثل اقتراح استهداف المراجع.

ما هي الخطوات التالية؟

سنواصل استكشاف إمكانيات توفير أوامر مضمّنة جديدة لتلبية الوظائف الشائعة التي تستخدمها المواقع الإلكترونية. يتم تناول الأفكار المقترَحة في اقتراح واجهة المستخدم المفتوحة. في ما يلي بعض الأفكار التي تم استكشافها من قبل:

  • فتح عناصر <details> وإغلاقها
  • أمر "show-picker" لعنصرَي <input> و<select>، مع الربط بقيمة showPicker()
  • أوامر التشغيل لعنصرَي <video> و<audio>
  • نسخ محتوى النص من العناصر

نرحب بملاحظات المنتدى. إذا كانت لديك اقتراحات، يُرجى عدم التردد في إرسال ملف بشأن مشكلة في نظام تتبُّع مشاكل واجهة المستخدم المفتوحة.

مزيد من المعلومات

يمكنك العثور على مزيد من المعلومات عن command وcommandfor في المواصفات وعلى MDN.