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

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

הרכיב מנסה להחיל את המאפיינים role="checkbox" ו-tabindex="0" על עצמו בפעם הראשונה שהוא נוצר. המאפיין role עוזר לטכנולוגיות מסייעות, כמו קורא מסך, לומר למשתמש איזה סוג של אמצעי בקרה זה. המאפיין tabindex מוסיף את הרכיב לסדר הקלדת Tab, כך שניתן להתמקד בו באמצעות המקלדת ולהפעיל אותו. למידע נוסף על שני הנושאים האלה, אפשר לעיין במאמרים מה אפשר לעשות באמצעות 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 הראשוני ולהתקין מאזינים לאירועים.

    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() נקראת כשאחד מהמאפיינים במערך observedAttributes משתנה. זהו מקום טוב לטיפול בתופעות לוואי, כמו הגדרת מאפייני 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() נגרם רק כתוצאה מפעולת משתמש, הוא גם ישלח אירוע שינוי. האירוע הזה עובר דרך ה-bubbles כדי לחקות את ההתנהגות המקורית של <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);
})();