auto-complete-element icon indicating copy to clipboard operation
auto-complete-element copied to clipboard

[Localization] Support an optional object of Localized phrases for Screen Reader announcements

Open inkblotty opened this issue 3 years ago • 12 comments
trafficstars

Context

Currently, all the screen reader announcements for this component are in English and have very generic wording such as "Results hidden". You may want to use more specific verbiage for your component use case, and/or support multiple languages.

Acceptance Criteria

  • [x] Extend the autocomplete API to allow the developer to pass in localized phrases for every Screen Reader announcement
  • [ ] If one phrase isn't passed in, fallback to the default already defined (We've got a lot of feedback in the verbiage here.)
  • [ ] Create unit tests to ensure these phrases work correctly

inkblotty avatar Dec 10 '21 18:12 inkblotty

Hello, how to move forward with this? Do you recommend any way on how to proceed? I can collaborate

brunoprietog avatar Aug 04 '22 20:08 brunoprietog

👋 Hey @brunoprietog -- Feel free to pick this up and assign yourself if you like. You can cc me and the other contributors on any PRs / drafts. Would be awesome to see this come together.

inkblotty avatar Aug 05 '22 16:08 inkblotty

Great. Do you have any proposal of how this screen reader ads API could be? With data-something attributes or with divs that have some specific id.

brunoprietog avatar Aug 05 '22 16:08 brunoprietog

@brunoprietog:

The current screen-reader feedback div is identified as ${this.results_id}-feedback ID on this line.

I think the majority of the work would need to be done within this.updateFeedbackForScreenReaders to interpret the phrases based on some JSON blob maybe? We could pass it into the auto-complete-element using a stringified data attribute. We could see how that approach performs. What do you think @keithamus?

inkblotty avatar Aug 05 '22 17:08 inkblotty

updateFeedbackForScreenReaders could emit an event with a mutable value which it then uses to read place into the feedback element. So for example:

class FeedbackEvent extends AutocompleteEvent {
  constructor(public text: string) {
    super('auto-complete-feedback', { bubbles: true, cancelable: true })
  }
}
  updateFeedbackForScreenReaders(inputString: string): void {
    const event = new FeedbackEvent(inputString)
    if (!this.dispatchEvent(event)) return
    setTimeout(() => {
      if (this.feedback) {
        this.feedback.textContent = event.text
      }
    }, SCREEN_READER_DELAY)
  }

This will then allow for consumers to override the default text:

el.addEventListener('auto-complete-feedback', (event) => {
  if (json.length === 0) {
    event.text = my_i18n('No users')
  } else {
    event.text = my_i18n(event.text)
  }
})

keithamus avatar Aug 05 '22 17:08 keithamus

@keithamus - I know we have a delay on the screen reader feedback, but I'm concerned that triggering an event based on the feedback changing would announce the default phrase AND the localized phrase. We'd have to test carefully.

inkblotty avatar Aug 12 '22 14:08 inkblotty

Events are synchronous, so as long as it is fired just before the announcement is triggered, then whatever phrase is in event.text will be read.

keithamus avatar Aug 12 '22 14:08 keithamus

@keithamus -- The aria-live attribute will ensure every change to the element's text is announced though. I would anticipate this won't be the right solution.

inkblotty avatar Aug 12 '22 15:08 inkblotty

The above implementation is correct, and will only update the aria-live region once per call of the function.

Event propagation happens as a synchronous step, and the text part of the event is just a reference, it will not update the aria-live region upon setting. We use event propagation to allow for consumers to set a new text value, and then use that to update the aria-live region.

So currently the code does:

  • Something has changed so call updateFeedbackForScreenReaders(input)
  • Set the aria live container's text content to the input

Or to put it in pseudo code:

updateFeedbackForScreenReaders(input) {
  setAriaLiveContainerText(input)
}

The new behaviour I'm proposing is

  • Something has changed so call updateFeedbackForScreenReaders(input)
  • Create an event containing the input.
  • Emit the event, letting everyone know that a screenreader announcement is about to happen.
  • If the events input has changed, that is the new input to use.
  • Set the aria live container's text content to the input

Or to put it in pseudo code:

updateFeedbackForScreenReaders(input) {
  let event = {text: input}
  emitEvent(event)
  if (event.text !== input) input = event.text
  setAriaLiveContainerText(input)
}

keithamus avatar Aug 12 '22 15:08 keithamus

Ah @keithamus - Thank you! This makes more sense. It's definitely worth trying out the way you've built it here.

inkblotty avatar Aug 12 '22 15:08 inkblotty

Hello @keithamus, thanks for the suggestion. However, the event to emit would have the text that updateFeedbackForScreenReaders method received. Wouldn't it be better for the event to emit a key? This key would be used to search I18n for the translated text according to the key sent. Relying on the text is very susceptible to change and is not always the same, for example when announcing the number of results found.

What do you think about defining the feedback texts in data attributes in the html? This would make it easier to send the text of the various feedbacks directly through the server.

brunoprietog avatar Sep 28 '22 07:09 brunoprietog

It can emit more than just the text, but the text acts as a default and a pointer to replace it with something else. It would be straightforward for us to introduce more complex data in the event, for example here's one that takes a key, the default string and arbitrary per-event data:

class FeedbackEvent extends AutocompleteEvent {
  constructor(public key: string, public text: string, public data: Record<string, unknown>) {
    super('auto-complete-feedback', { bubbles: true, cancelable: true })
  }
}

// Somewhere in the code
const resultCount = 3
this.dispatchEvent(new FeedbackEvent('NEW_RESULTS', `${resultCount} new results`, { resultCount }))

This can then be replaced with userland code like so:

addEventListener('auto-complete-feedback', event => {
  if (event.key == 'NEW_RESULTS') {
    if (event.data.resultCount === 0) {
      event.text = my_i18n({ key: 'MY_KEY_NO_NEW_RESULTS' })
    } else {
      event.text = my_i18n({ key: 'MY_KEY_SOME_NEW_RESULTS' }, event.data)
    }
  }
})

keithamus avatar Sep 28 '22 15:09 keithamus