רכיבי HowTo – תיבת סימון של מדריך

<howto-checkbox> מייצג אפשרות בוליאנית בטופס. הסוג הנפוץ ביותר של תיבות סימון הוא סוג כפול המאפשר למשתמש לעבור בין שתי אפשרויות - הן מסומנות ולא מסומנות.

הרכיב מנסה להחיל את המאפיינים role="checkbox" ו-tabindex="0" על עצמו בפעם הראשונה שהוא נוצר. המאפיין role עוזר לטכנולוגיות מסייעות, כמו קורא מסך, לומר למשתמש איזה סוג של אמצעי בקרה זה. המאפיין tabindex מוסיף את הרכיב לסדר הטאבים, כך שניתן להתמקד בו באמצעות המקלדת ולהפעיל אותו. למידע נוסף על שני הנושאים האלה, אפשר לעיין במאמרים מה אפשר לעשות באמצעות ARIA? ושימוש ב-tabindex.

כשתיבת הסימון מסומנת, המערכת מוסיפה מאפיין בוליאני checked ומגדירה את checked התואם ל-true. בנוסף, הרכיב מגדיר את המאפיין aria-checked לערך "true" או "false", בהתאם למצב שלו. לחיצה על תיבת הסימון בעזרת העכבר או מקש הרווח משנה את המצבים האלה.

בתיבת הסימון יש תמיכה גם במצב disabled. אם המאפיין disabled מוגדר כ-true או שהמאפיין disabled מיושם, התיבה מסמנת את aria-disabled="true", מסירה את המאפיין tabindex ומחזירה את המיקוד למסמך אם התיבה היא activeElement הנוכחי.

תיבת הסימון מוצמדת לרכיב howto-label כדי לוודא שיש לה שם נגיש.

חומרי עזר

הדגמה (דמו)

הצגת הדגמה פעילה ב-GitHub

דוגמה לשימוש

<style>
  howto-checkbox {
    vertical-align: middle;
  }
  howto-label {
    vertical-align: middle;
    display: inline-block;
    font-weight: bold;
    font-family: sans-serif;
    font-size: 20px;
    margin-left: 8px;
  }
</style>

<howto-checkbox id="join-checkbox"></howto-checkbox>
<howto-label for="join-checkbox">Join Newsletter</howto-label>

קוד

(function() {

הגדרת קודי מפתחות כדי לעזור בטיפול באירועים של מקלדת.

  const KEYCODE = {
    SPACE: 32,
  };

העתקת תוכן מרכיב <template> משפרת את הביצועים בהשוואה לשימוש ב-innerHTML, כי היא מונעת עלויות נוספות של ניתוח HTML.

  const template = document.createElement('template');

  template.innerHTML = `
    <style>
      :host {
        display: inline-block;
        background: url('../images/unchecked-checkbox.svg') no-repeat;
        background-size: contain;
        width: 24px;
        height: 24px;
      }
      :host([hidden]) {
        display: none;
      }
      :host([checked]) {
        background: url('../images/checked-checkbox.svg') no-repeat;
        background-size: contain;
      }
      :host([disabled]) {
        background:
          url('../images/unchecked-checkbox-disabled.svg') no-repeat;
        background-size: contain;
      }
      :host([checked][disabled]) {
        background:
          url('../images/checked-checkbox-disabled.svg') no-repeat;
        background-size: contain;
      }
    </style>
  `;


  class HowToCheckbox extends HTMLElement {
    static get observedAttributes() {
      return ['checked', 'disabled'];
    }

ה-constructor של הרכיב מופעל בכל פעם שיוצרים מכונה חדשה. אפשר ליצור את המופעים על ידי ניתוח HTML, קריאה ל-document.createElement('howto-checkbox') או קריאה ל-new HowToCheckbox();‏. ה-constructor הוא מקום טוב ליצירת DOM בצל, אבל כדאי להימנע משינוי מאפיינים או צאצאים של DOM בהיר, כי יכול להיות שהם עדיין לא יהיו זמינים.

    constructor() {
      super();
      this.attachShadow({mode: 'open'});
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

האירוע connectedCallback() מופעל כשהרכיב מוכנס ל-DOM. זהו מקום טוב להגדיר בו את role, tabindex והמצב הפנימי, ולהתקין פונקציות event listener להתקנה.

    connectedCallback() {
      if (!this.hasAttribute('role'))
        this.setAttribute('role', 'checkbox');
      if (!this.hasAttribute('tabindex'))
        this.setAttribute('tabindex', 0);

משתמש יכול להגדיר מאפיין למופיע של רכיב לפני שהאב טיפוס שלו מחובר לכיתה הזו. השיטה _upgradeProperty() תבדוק אם יש מאפייני מופעים ותריץ אותם דרך הגדרות ה-set של הכיתה המתאימה. פרטים נוספים זמינים בקטע מאפיינים מושהה.

      this._upgradeProperty('checked');
      this._upgradeProperty('disabled');

      this.addEventListener('keyup', this._onKeyUp);
      this.addEventListener('click', this._onClick);
    }

    _upgradeProperty(prop) {
      if (this.hasOwnProperty(prop)) {
        let value = this[prop];
        delete this[prop];
        this[prop] = value;
      }
    }

האירוע disconnectedCallback() מופעל כשהרכיב מוסר מה-DOM. זהו מקום טוב לבצע פעולות ניקוי כמו שחרור הפניות והסרת מאזינים לאירועים.

    disconnectedCallback() {
      this.removeEventListener('keyup', this._onKeyUp);
      this.removeEventListener('click', this._onClick);
    }

הנכסים והמאפיינים התואמים שלהם צריכים לשקף זה את זה. ה-setter של המאפיין checked מטפל בערכים נכונים/לא נכונים ומשקף אותם במצב של המאפיין. פרטים נוספים זמינים בקטע הימנעות מחדירה חוזרת.

    set checked(value) {
      const isChecked = Boolean(value);
      if (isChecked)
        this.setAttribute('checked', '');
      else
        this.removeAttribute('checked');
    }

    get checked() {
      return this.hasAttribute('checked');
    }

    set disabled(value) {
      const isDisabled = Boolean(value);
      if (isDisabled)
        this.setAttribute('disabled', '');
      else
        this.removeAttribute('disabled');
    }

    get disabled() {
      return this.hasAttribute('disabled');
    }

הפונקציה attributeChangedCallback() תופעל בכל שינוי של אחד מהמאפיינים במערך החשיפות (edAttributes). זהו מקום טוב לטיפול בתופעות לוואי, כמו הגדרת מאפייני ARIA.

    attributeChangedCallback(name, oldValue, newValue) {
      const hasValue = newValue !== null;
      switch (name) {
        case 'checked':
          this.setAttribute('aria-checked', hasValue);
          break;
        case 'disabled':
          this.setAttribute('aria-disabled', hasValue);

המאפיין tabindex לא מספק דרך להסיר באופן מלא את יכולת המיקוד מאלמנט. עדיין אפשר להתמקד ברכיבים עם tabindex=-1 באמצעות העכבר או קריאה לרכיב focus(). כדי לוודא שרכיב מסוים מושבת ואי אפשר להתמקד בו, צריך להסיר את המאפיין tabindex.

          if (hasValue) {
            this.removeAttribute('tabindex');

אם המיקוד נמצא כרגע ברכיב הזה, מבטלים את המיקוד על ידי קריאה ל-method‏ HTMLElement.blur()

            this.blur();
          } else {
            this.setAttribute('tabindex', '0');
          }
          break;
      }
    }

    _onKeyUp(event) {

לא מטפלים במקשי קיצור עם שינוי (modifier) שבדרך כלל משמשים לטכנולוגיה מסייעת.

      if (event.altKey)
        return;

      switch (event.keyCode) {
        case KEYCODE.SPACE:
          event.preventDefault();
          this._toggleChecked();
          break;

המערכת מתעלמת מכל הקשה אחרת על מקש, ומועברת חזרה לדפדפן.

        default:
          return;
      }
    }

    _onClick(event) {
      this._toggleChecked();
    }

_toggleChecked() קורא למגדיר המאומת ומשנה את המצב שלו. _toggleChecked() נגרמה רק על ידי פעולת משתמש, ולכן היא גם תשלח אירוע שינוי. האירוע הזה עובר 'בועה' כדי לחקות את ההתנהגות המקורית של <input type=checkbox>.

    _toggleChecked() {
      if (this.disabled)
        return;
      this.checked = !this.checked;
      this.dispatchEvent(new CustomEvent('change', {
        detail: {
          checked: this.checked,
        },
        bubbles: true,
      }));
    }
  }

  customElements.define('howto-checkbox', HowToCheckbox);
})();