[w3c/webcomponents] accessibility considerations: states and behaviors (#567)

# The problem

Currently, there is no way to replicate certain accessible properties of HTML elements with custom elements. The following are examples of things that cannot be replicated that this proposal is hoping to fix:

* Default `tabindex` attribute
* Default `role` attribute
* Default `aria-*` attributes
* Focusability (think of `input`)
* Disableability (think of `button`)
* Non‐pointer clickability (think of pressing the space bar with a button focused)

# The solution: the `accessibility` property

The `customElements.define` options object could have an `accessibility` property, containing an “accessibility object”.

## Default attributes

The “accessibility object” would contain a `default`  property. The `default` property would take an object whose keys are attribute names, and values are default values for those attributes. Valid attribute names are `tabindex`, `role` and `aria-*`. That is, consider the following:

```Javascript
window.customElements.define("my-button", MyButton,
{
 accessibility:
 {
  default:
  {
   tabindex: "1",
   role: "button",
   "aria-enabled": "true"
  }
 }
});
```

The code makes `my-button` act more like a button.

## States and behaviors

The “accessibility object” would contain a `behaviors` property. The `behaviors` property would take an object whose property names indicate the name of behaviors. Each “behavior property” would take an object whose property names indicate the names of states that that behavior exposes. Each “state property” would take an “accessibility object”. That is, consider the following code:

```Javascript
window.customElements.define("my-button", MyButton,
{
 accessibility:
 {
  default:
  {
   role: "button"
  }
  behaviors:
  {
   disableable:
   {
    enabled:
    {
     default:
     {
      "aria-enabled": "true"
     }
     behaviors:
     {
      clickable:
      {
      }
     }
    }
    disabled:
    {
     default:
     {
      "aria-enabled": "false"
     }
    }
   }
  }
 }
});
```

Here, the behavior is `disableable`. The states it exposes are “enabled” and “disabled”. Whenever the element is in the “enabled” state, it also uses the “clickable” behavior.

The built‐in behaviors would be the following:

* disableable
 * enabled
 * disabled
* focusable
 * focused
 * unfocused
* clickable (extends focusable)
 * focused
 * unfocused
* checkable
 * checked
 * unchecked
 * indeterminate
* checkbox (extends checkable)
 * checked
 * unchecked
 * indeterminate
* radio (extends checkable)
 * checked
 * unchecked
 * indeterminate

### Custom states and behaviors

Built‐in states and behaviors should be reproducible by custom, user‐created states and behaviors.

Creating a custom behavior would be done by calling `customElements.defineBehavior`. Consider the following code:

```Javascript
customElements.defineBehavior("my-disableable",
{
 states:
 {
  disabled:
  {
   "matches": "[disabled]",
   details:
   {
    default:
    {
     "aria-disabled": "true"
    }
   }
  },
  enabled:
  {
   "matches": ":not([disabled])",
   details:
   {
    default:
    {
     "aria-disabled": "false"
    }
   }
  }
 }
}

customElements.defineBehavior("my-focusable",
{
 default:
 {
  "tabindex": "1"
 }
 
 states:
 {
  focused:
  {
   "matches": ":focus"
  },
  unfocused:
  {
   "matches": ":not(:focus)"
  }
 }
});

customElements.defineBehavior("my-clickable",
{
 extends: {"my-focusable": {}},
 constructor()
 {
  this.element.addEventListener("keyup", ()=>
  {
   let code = event.code;
   if(code === "Space" || code === "Enter" || code === "NumpadEnter")
   {
    this.element.click();
   }
  });
 }
});
```

The options object for `defineBehavior` would take a “behavior options object”.

The “behavior options object”’s `default` property would act similarly to the “accessibility object”’s default property, except that it would affect the element the behavior is applied to.

The “behavior options object”’s `states` property would take an object whose keys are state names, each `states` object’s keys’ corresponding object’s `matches` key would take a CSS selector. Whenever an element that uses the behavior matches that selector, the state is applied to the element. Each `states` object’s keys’ corresponding object’s `details` property would take a “behavior options object”.

The “behavior options object”’s `extends` property would take an object. Each key of this object would be the name of a pre‐existing behavior. Each key of each of this object’s values would be the name of a property that that pre‐existing behavior exposes; each corresponding value would take a “behavior options object”.

Any of a `constructor`, an `connectedCallback`, an `atrributeChangedCallback` or a `disconnectedCallback` property of the “behavior options object” would take a function that’d be called immediately after (or immediately before) the corresponding life‐cycle callback function of the element the behavior is applied to. Within these functions, the `this` object would be a special “behavior object”. This “behavior object” exposes an `element` property (returns the element the behavior is associated with), and a `setState(string, boolean|null)` method, which sets a state for that behavior to true or false; `null` would mean “use the defined `matches` string for that state”.

As a complete example, here the definition of a behavior that would make an element act like a button:

```Javascript
window.customElements.defineBehavior("button-like",
{
 extends:
 {
  disableable:
  {
   enabled:
   {
    extends:
    {
     clickable: {}
    }
   }
  }
 },
 default:
 {
  role: "button"
 }
 constructor()
 {
  let element = this.element;
  this.addEventListener("click", event=>
  {
   if(this.disabled)
   {
    event.preventDefault();
    event.stopPropagation();
   }
  });
 }
});
```

### Details

It’s important to note that each object passed to `customElements.defineBehavior`  and `window.customElements.define` would be evaluated once and converted into a “flat object”. That is, an object that has no getter and whose all properties are also flat objects.

### CSS pseudo‐classes

A thing that would be unreproducible from built‐in behaviors would be CSS pseudo‐classes. That is, if an element has the `disableable` built‐in behavior, it can be targeted by the `:disabled` and `:enabled` pseudo‐class. Such behavior would not be reproducible by a `my‐disableable` behaviors.

An exception would be `focusable`, which would merely set the default value of `tabindex` to `1`, meaning that the `:focus` pseudo‐class can be very easily reproduced by custom behaviors.

However, it would be possible to select custom states from CSS by using the `:state()` functional pseudo‐class. The syntax would be either `state(state)` which would match the `state` state from any behavior, or `state(behavior:state)` which would match only the `state` state from the `behavior` behavior.

With this, in HTML, we’d have `:disabled` being the equivalent to `:state(disableable|disabled)`, `:enabled` being the equivalent to `:state(disableable|enabled)`, and etc.

# Why?

I think this would be great to allow people to create elements that behave very similarly to (or the same as) built‐in elements. It would, besides making the platform feel more regular, also provide authors with a powerful tool to help them write more accessible web applications and documents.

----------

Any ideas are welcome!


-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/w3c/webcomponents/issues/567

Received on Wednesday, 14 September 2016 14:52:28 UTC