wagtail icon indicating copy to clipboard operation
wagtail copied to clipboard

StreamField FieldBlock / widgets labels’ missing semantic association with inputs

Open thibaudcolas opened this issue 2 years ago • 2 comments

Raised by @lb- in https://github.com/wagtail/wagtail/issues/10515#issuecomment-1666252098

Not sure if I should flag as a standalone issue but for the labels do not appear to be rendering for FieldBlock/widgets correctly. Discovered while reviewing #10705

I have not dug into it further, but no label element appears to render at all.

Screenshot 2023-08-05 at 8 32 36 am Screenshot 2023-08-05 at 8 27 41 am

Potential solution -> client/src/components/StreamField/blocks/BaseSequenceBlock.js

            <h2 class="w-panel__heading w-panel__heading--label" aria-level="3" id="${headingId}" data-panel-heading>
              <span data-panel-heading-text class="c-sf-block__title"></span>
-              <span class="c-sf-block__type">${blockTypeLabel}</span>
+              <label class="c-sf-block__type" for="??">${blockTypeLabel}</label>

We will then need to patch in the id when bound with the provided idForLabel or similar.

This seems to work in Firefox but not sure on a h2/h3 with label inside, also, not sure this will work for all contexts.

thibaudcolas avatar Nov 03 '23 09:11 thibaudcolas

Looks like this is a bit more tricky tricky than first thought, I had called out idForLabel but the PR submitted did not account for this. Here is a copy of my notes from the PR.

We may continue looking at setting the label element but we will need to consider a few more cases;

  1. Some blocks come with their own labels, e.g. those with more than one inputs, we do not want to break those.
  2. The idForLabel is only known after we render and may not exist (depending on block implementation) so we need to consider cases where it's not going to be set. Hence we may not always want this header element to render a label if we will never set for on it.
  3. We cannot just 'find' an input in the rendered DOM either, that will be very prone to error.

As such, some refined approaches that we could take.

A. Determine what the input id is after the block renders

  • We could do this with const idForLabel = this.block.idForLabel;
  • We can access this once the block renders this.block = this.blockDef.render(
  • However, we need to account for cases where the idForLabel may not be present, hence we would not always want to render a label in the heading.
  • So, basically, we would check if we have an idForLabel, then probably change the inner span of the data-panel-heading to a label.
  • This is getting pretty fragile and prone to error so maybe it's not a good approach, but may be the best option, as long as we consider edge cases.

            <h2 class="w-panel__heading w-panel__heading--label" aria-level="3" id="${headingId}" data-panel-heading>
              <span>
                <span data-panel-heading-text class="c-sf-block__title"></span>
                <span class="c-sf-block__type">${blockTypeLabel}</span>
              </span>
              ${
                blockDef.meta.required
                  ? '<span class="w-required-mark" data-panel-required>*</span>'
                  : ''
              }
            </h2>

  // ...
    this.block = this.blockDef.render(
      blockElement,
      this.prefix + '-value',
      initialState,
      undefined,
      capabilities,
    );



    const idForLabel = this.block.idForLabel;
    if (idForLabel) {
      const innerHeadingLabel = dom
        .find('[data-panel-heading]')
        .find(':first-child');
      console.log({ idForLabel, innerHeadingLabel });
      // innerHeadingLabel.type = 'label'; // this wont work but we need to set the type to label
      // alternatively we always render an outer label but only set for if we know an ID
      innerHeadingLabel.attr('for', idForLabel);
    }

  // ...

I do not really recommend the approach above but it's a start of what we may have to do if we go down this path.

B. Try to pass aria-labelledby to the input that gets rendered.

  • Since we have a known headingId and also a way to pass in options.attributes into blocks we could pass in something like aria-labelledby: headingId and add that to the rendered input.
  • The downside of this approach though as that IF there is already a label provided by the block we do not want to do that as the proper label should take precedence but if we supply aria-labelledby it will. https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-labelledby
  • We also need to account for cases where aria-labelledby may already exist on the DOM element.

lb- avatar Nov 07 '23 22:11 lb-

@lb- I am trying this approach:First check if there is no label or aria-labelledby attribute in the block and if not there pass the aria-labelledby attribute to the block

if(!this.block.hasAttribute("label") || !this.block.hasAttribute("aria-labelledby")){
      const innerHeadingLabel = dom
        .find('[data-panel-heading]')
        .find(':first-child');
      innerHeadingLabel.attr('aria-labelledby',`block-${this.id}-heading`)
    }

Based upon your analysis I found this as a good option.I request you to please suggest me upon this

jgyfutub avatar Nov 10 '23 17:11 jgyfutub