Enhanced Checkbox Lists On GitHub Accessibly enhancing checkbox lists with filtering, toggling, select-all, and keyboard shortcuts

Installation and usage

You can grab the module from NPM:

npm install enhanced-checkbox-list

And then import it to be used with your module system:

import EnhancedCheckboxList from 'enhanced-checkbox-list';
new EnhancedCheckboxList(document.getElementById('list'), options);

Or you can grab the code straight from unpkg:

<script src="https://unpkg.com/enhanced-checkbox-list"></script>
<script>
    const list = document.getElementById('list');
    new EnhancedCheckboxList(list, options);
</script>

The EnhancedCheckboxList function takes 2 arguments: the list element / wrapper, and an optional options object.

Examples

1. Default options: filtering, toggling, select all, and shortcuts

Fruits
    <fieldset>
        <legend>Fruits</legend>
        <ul id="list">
            <!-- checkbox list items here -->
        </ul>
    </fieldset>
    
    const list = document.getElementById('list');
    new EnhancedCheckboxList(list);

    When keyboard focus is on a checkbox in the list, you can use the home, end, pageup, and pagedown keys to jump to the first, last, +10th, or -10th checkbox respectively.

    The modifiers used for the pageup and pagedown keys are of course customisable using the pageUpModifier and pageDownModifier options respectively.

    2. Enhanced list with toggling disabled

    Fruits
    <fieldset>
        <legend>Fruits</legend>
        <div id="list">
            <!-- checkboxes and labels here -->
        </div>
    </fieldset>
    
    const list = document.getElementById('list');
    new EnhancedCheckboxList(list, {
        togglable: false,
        itemSelector: ''
    });

    The filter input's value can also be cleared using the Esc key.

    This example has all of the checkboxes and labels directly inside of a <div>, instead of in list items. So we're setting the itemSelector option to an empty string to prevent any unnecessary DOM interrogation. This means that, after filtering, only the checkboxes themselves will be shown or hidden based on the filter value, so we need to use CSS to hide the sibling label.

    3. Togglable and open by default, but not filterable

      <ul id="list" aria-label="Fruits">
          <!-- checkbox list items here -->
      </ul>
      
      const list = document.getElementById('list');
      new EnhancedCheckboxList(list, {
          filterable: false,
          visible: true
      });

      If togglable, you can also use the Esc key anywhere inside the wrapper (other than the filter input) to close the list.

      This example is also using an aria-label attribute on the list which is being used for the toggle button text (instead of being inside a <fieldset> with a <legend>).

      4. Auto-closing panel, with custom button text

      This example only shows the number of checked items in the button if there are some.

        const list = document.getElementById('list');
        new EnhancedCheckboxList(list, {
            togglable: true,
            autoClose: true,
            toggleButtonText: (listLabel, checkedCount) => {
                const labelLower = listLabel.toLowerCase();
                if (checkedCount === 0) {
                    return `Select ${labelLower}`;
                } else if (checkedCount > 0) {
                    return `Chosen ${labelLower} (${checkedCount})`;
                }
            }
        });

        5. All core functionality set to false
        (because that's supported for some reason...)

        This example basically just waps the checkbox list in a container... I'm not sure why you'd use it this way, but you can if you want to.

        Fruits
          const list = document.getElementById('list');
          new EnhancedCheckboxList(list, {
              keyboardShortcuts: false,
              selectAllControl: false,
              filterable: false,
              togglable: false
          });

          HTML requirements

          For proper, accessible checkbox lists, they should be in a <fieldset> with a <legend> to give the list context.

          If your list isn't in a <fieldset> though, the module will group and label the list for screen reader users, as long as a label can be found. When looking for a label for the list, the module attempts the following steps:

          1. Checking if the listLabel option has a value
          2. Check for an aria-label attribute on the list element
          3. Checking for an aria-labelledby attribute on the list element, and looking for an element with that id
          4. Checking if another element labels the list using a for attribute
          5. Travelling up the DOM tree looking for a <fieldset> element; if one is found, its <legend> text will be used

          For example:

          <ul aria-label="Fruits">
              <!-- list items in here -->
          </ul>
          
          <span id="fruits-label" hidden>Fruits</span>
          <ul id="fruits" aria-labelledby="fruits-label">
              <!-- list items in here -->
          </ul>
          
          <span for="fruits" hidden>Fruits</span>
          <ul id="fruits">
              <!-- list items in here -->
          </ul>
          
          <fieldset>
              <legend>Fruits</legend>
              <ul id="fruits">
                  <!-- checkbox list items here -->
              </ul>
          </fieldset>
          

          As well as giving the list context, this label is used for the toggle button text, and as part of a hidden label for the filter input for screen reader users.

          Options

          The full list of available options is as follows:

          Filtering

          filterable: boolean
          Generate an input to allow filtering the list
          Default true
          filterDelay: number
          Delay after typing in the filter input before filtering (to allow for fast typers)
          Default: 300
          itemSelector: string
          Selector to use when doing a .closest() DOM query to determine which element to show/hide after filtering
          Default: `li`

          Toggling

          togglable: boolean
          Allow toggling visibility by generate a toggle button
          Default true
          toggleButtonText: string | ((listLabel: string, checkedCount: number) => string)
          Text to use in the toggle button; can accept a string, or function.
          Default (listLabel, checkedCount) => `${listLabel} (${checkedCount})`
          autoClose: boolean
          If togglable, close when clicking or blurring outside of the container / button
          Default: false
          visible: boolean
          If togglable, determines initial visibility
          Default: false

          Select all

          selectAllControl: boolean
          Include a custom select all control;
          this has to be a custom element due to issues with updating the indeterminate state on native checkboxes in many browsers
          Default: true
          selectAllText: string
          Text for the select all control label
          Default: `Select all`

          Keyboard shortcuts

          keyboardShortcuts: boolean
          If keyboard focus is on a checkbox, allow: home, end, pageup, and pagedown shortcuts to move to first, last, -10, or +10 checkbox respectively;
          and escape key to close the wrapper if togglable
          Default: true
          pageUpModifier: number
          The index modifier for the pageup shortcut;
          e.g. if on the 15th checkbox, use pageup to go to the 5th
          Default: -10
          pageDownModifier: number
          The index modifier for the pagedown shortcut;
          e.g. if on the 5th checkbox, use pagedown to go to the 15th
          Default: 10

          Rendering options

          checkboxSelector: string
          Selector to use when finding the checkboxes in the list
          Default: `input[type="checkbox"], input[type="radio"]`
          tabindex: number | string
          Control the tabindex added to the toggle button and select all control in case your checkbox list sits at a certain point in the page's tabbing order
          Default: `0`
          cssNameSpace: string
          String to prepend to classes for BEM naming
          e.g. "enhanced-checkbox-list__wrapper"
          Default: `enhanced-checkbox-list`
          listLabel: string
          Label for the checkbox list - used for the toggle button text and the label for the filter input;
          if not provided, the module will search for one using aria-label or aria-labelledby attributes, or looking for a parent <fieldset>'s <legend>
          filterClassName: string
          Custom class name to add to the filter text input
          toggleClassName: string
          Custom class name to add to the toggle button
          wrapperClassName: string
          Custom class name to add to the component wrapper

          Screen reader enhancements

          srEscapeToCloseText: string
          Screen reader text added to list wrapper advising shortcut to close the wrapper (if toggling enabled)
          Default: `Esc to close`
          srEscapeToClearText: string
          Screen reader text added to filter field advising shortcut to clear filter value (if filtering enabled)
          Default: `Esc to clear`
          srFilterText: string
          Screen reader text used for label explaining the search/filter input;
          combines with listLabel if available
          e.g. "filter departments"
          Default: `filter`
          srFoundText: string
          Partial screen reader text used in message after filtering e.g. "12 found, 3 selected"
          Default: `found`
          srSelectedText: string
          Partial screen reader text used in message after filtering e.g. "12 found, 3 selected"
          Default: `selected`

          Callbacks

          onFilter: Function
          Callback after a filter has run
          onReady: Function
          Callback once ready
          onShow: Function
          Callback when the list is toggled visible
          onHide: Function
          Callback when the list is toggled hidden

          All component options that accept a function will also have their context (this) set to include the full API (assuming you use an ordinary function expression instead of arrow functions).

          API

          The returned EnhancedCheckboxList instance exposes the following API ((which is also available on the original element's enhancedCheckboxList property):

          list: HTMLElement
          The original list element that the function was called on
          options: EnhancedCheckboxListOptions
          The instance options that were / are being used
          checkboxes: HTMLInputElement[]
          The checkboxes that the instance currently recognises and will affect
          listWrapper: HTMLDivElement
          The wrapping <div> generated by the module
          filterInput: HTMLInputElement
          The filtering input generated by the module
          selectAllCheckbox: HTMLSpanElement
          The generated custom select all element
          show: (): void
          Toggle the checkbox list wrapper visible
          hide: (): void
          Toggle the checkbox list wrapper hidden
          update: (updateCheckboxStorage: boolean = true): void
          Update the toggle button text and select all state
          filter: (value: string, updateScreenReaderText: boolean = false): void
          Filter the checkbox list by a chosen value
          destroy: (clearInternalCache: boolean = false): void
          Destroy the component