CleverCloud / clever-components

Collection of Web Components by Clever Cloud
https://www.clever-cloud.com/doc/clever-components/
Apache License 2.0
221 stars 20 forks source link

How to transmit aria attributes to inner elements #849

Closed pdesoyres-cc closed 1 year ago

pdesoyres-cc commented 1 year ago

Context

When designing the cc-popover component, we used a cc-button to trigger the floating content display. Making this pattern accessible requires the use of an aria-expanded attribute.

Our first thought was just to put this attribute on the <cc-button> element but after looking closer, we discovered that this is not the right way to do it. The element receiving the aria-expanded must be the focused element (corresponding to document.activeElement). In our case, the <cc-button> element is not the active element (even if it has got a focus() method). In fact, the active element is the <button> element inside the cc-button component.

We need something more to make the cc-popover accessible: We need to allow the cc-button to transmit some aria attributes to its inner <button> element. There is a related issue: #699

Solutions

When facing this kind of problem, we need to look a bit further. We need to think of a solution that fits the need of transmitting some aria attributes from a component into an inner element.

After a discussion with Florian, we are able to propose 3 solutions that we will need to discuss with the whole team.

In each solution below, we will consider as an example, that we want to add two aria attributes to the inner <button> element of the cc-button component. Please consider that the solution needs to apply to any other component and to any other aria attributes.

Solution 1: Use aria-X property

This solution consists of adding new reactive properties to the cc-button component. It uses the same aria prefix as the native HTML.

export class CcButton extends LitElement {
  static get properties () {
    return {
      ariaExpanded: { type: Boolean, attribute: 'aria-expanded' },
      ariaPressed: { type: Boolean, attribute: 'aria-pressed' },
    };
  }

  constructor () {
    /** @type {boolean|null} */
    this.ariaExpanded = null;

    /** @type {boolean|null} */
    this.ariaPressed = null;
  }

  render () {
    return html`<button
        aria-expanded=${ifDefined(this.ariaExpanded)}
        aria-pressed=${ifDefined(this.ariaPressed)}
    ></button>`;
  }
<cc-button aria-expanded aria-pressed></cc-button>

Solution 2: Use accessible-X property

This solution is very close to the previous one. The only difference is that it uses a different prefix: accessible. Note that, this prefix is already used on the cc-button and cc-icon components for the accessible-name property which is transmitted as an aria-label attribute to an inner element.

export class CcButton extends LitElement {
  static get properties () {
    return {
      accessibleExpanded: { type: Boolean, attribute: 'accessible-expanded' },
      accessiblePressed: { type: Boolean, attribute: 'accessible-pressed' },
    };
  }

  constructor () {
    /** @type {boolean|null} */
    this.accessibleExpanded = null;

    /** @type {boolean|null} */
    this.accessiblePressed = null;
  }

  render () {
    return html`<button
        aria-expanded=${ifDefined(this.accessibleExpanded)}
        aria-pressed=${ifDefined(this.accessiblePressed)}
    ></button>`;
  }
<cc-button accessible-expanded accessible-pressed></cc-button>

Solution 3: Use aria object property

This solution proposes one single aria Object property where users can specify multiple aria attributes at once. Note that the homemade aria lit directive helps a lot!

import { aria } from './aria-directive.js';

/**
 * @typedef {import('./cc-button.types.js').CcButtonAria} CcButtonAria
 */

export class CcButton extends LitElement {
  static get properties () {
    return {
      aria { type: Object },
    };
  }

  constructor () {
    /** @type {CcButtonAria|null} */
    this.aria = null;
  }

  render () {
    return html`<button
        ${aria(this.aria)}
    ></button>`;
  }
export interface CcButtonAria {
    expanded: boolean|null;
    pressed: boolean|null;
}
<cc-button aria='{"expanded": true, "pressed": true}'></cc-button>
Directive helping us transmit aria attributes ```javascript import { nothing } from 'lit'; import { directive, Directive } from 'lit/directive.js'; class AriaDirective extends Directive { render (value) { return nothing; } update (part, [aria]) { if (aria == null) { return; } Object.entries(aria).forEach(([a, v]) => { if (typeof v === 'boolean') { if (v) { part.element.setAttribute(`aria-${a}`, ''); } else { part.element.removeAttribute(`aria-${a}`); } } else { part.element.setAttribute(`aria-${a}`, `${v}`); } }); } } export const aria = directive(AriaDirective); ```

Pros & Cons

Pros Cons
solution 1
  • reuse the well-known ARIA spec
  • properties can be reflected to attributes
  • aria attributes land in the web component element. This may clash with the ARIA spec
solution 2
  • no clash with ARIA spec
  • properties can be reflected to attributes
-
solution 3
  • very little boilerplate with the aria directive
  • not HTML friendly
  • property cannot be reflected to attribute
florian-sanders-cc commented 1 year ago

Thanks for this awesome issue :wink:

I would go with option 2 because I like to manipulate this stuff through HTML attributes, especially for atomic components. I'm not against option 3. I think option 1 is too risky.

So for me 2 > 3 > 1.

About the solution 2: I remember suggesting accessible- as prefix but I'm not sure it's the best idea cause it sounds weird accessible-expanded / accessible-selected etc.

If you have better ideas, please go for it :rocket:

pdesoyres-cc commented 1 year ago

@florian-sanders-cc what do you think of cc-aria- prefix?

florian-sanders-cc commented 1 year ago

@pdesoyres-cc I think it's better than my suggestion but I'm not completely sold, prefixing our own component attributes with cc- feels redundant (but it also makes sense here?).

Anyway, it's a good suggestion :+1:

Galimede commented 1 year ago

Same as @florian-sanders-cc 2 > 3 > 1 :grimacing:

hsablonniere commented 1 year ago

I vote for solution 2 with accessible- or a11y- prefix.

pdesoyres-cc commented 1 year ago

I'll go for a11y- prefix