Skip to content

feat(core): add eslint-plugin-jsx-a11y and fix accessibility violations#930

Open
jensblond wants to merge 4 commits intomainfrom
feat/a11y-linting
Open

feat(core): add eslint-plugin-jsx-a11y and fix accessibility violations#930
jensblond wants to merge 4 commits intomainfrom
feat/a11y-linting

Conversation

@jensblond
Copy link

  • Add eslint-plugin-jsx-a11y with recommended rules to ESLint config
  • Fix cat-select: add tabIndex, aria-expanded, aria-controls, role="option", keyboard handlers
  • Fix cat-tag: add aria-expanded and aria-controls to combobox input
  • Fix cat-radio: remove incorrect role="radio" from label element
  • Fix cat-date-inline: remove redundant onClick from label (htmlFor handles focus)
  • Fix cat-input: add role="presentation" to wrapper div
  • Fix cat-i18n-registry: remove unused variable in catch block

This enables static a11y linting as part of the standard lint workflow.

- Add eslint-plugin-jsx-a11y with recommended rules to ESLint config
- Fix cat-select: add tabIndex, aria-expanded, aria-controls, role="option", keyboard handlers
- Fix cat-tag: add aria-expanded and aria-controls to combobox input
- Fix cat-radio: remove incorrect role="radio" from label element
- Fix cat-date-inline: remove redundant onClick from label (htmlFor handles focus)
- Fix cat-input: add role="presentation" to wrapper div
- Fix cat-i18n-registry: remove unused variable in catch block

This enables static a11y linting as part of the standard lint workflow.
- Add axe-core dependency for runtime a11y checks
- Create reusable checkA11y() helper that injects axe-core into Stencil E2E pages
- Add formatViolations() helper for readable test output
- Disable document-level rules by default (title, lang, landmarks) for component testing
- Add a11y test examples to cat-button component

Usage:
  import { checkA11y, formatViolations } from '../../utils/a11y-test';
  const violations = await checkA11y(page);
  expect(violations).toEqual([]);
'input-disabled': this.disabled,
'input-invalid': this.invalid
}}
role="presentation"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

role="presentation" removes semantic from the element. Since div doesn't have any semantic role="presentation" does nothing. It may trick eslint but it may confuse developers, I recommend replace it with eslint disable line

ref={el => (this.trigger = el)}
id={this.id}
role="combobox"
tabIndex={0}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that tabIndex is assigned programmatically in onClick and in onKeyDown, I'm not sure if this don't break the current implementation, if you are not sure - easier to add eslint-disable

aria-required={this.required ? 'true' : false}
aria-activedescendant={this.activeDescendant}
onClick={e => this.onClick(e)}
onKeyDown={e => e.key === 'Enter' && this.onClick(e as unknown as MouseEvent)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is no need in this, key handler is already working, you can also double check here https://haiilo.github.io/catalyst/ (I guess because of tabindex)

@@ -637,11 +637,13 @@ export class CatSelect {
ref={el => (this.trigger = el)}
id={this.id}
role="combobox"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to you changes, but I think we should not have role="combobox" on the wrapper, but only on the input

role="listbox"
aria-multiselectable={this.multiple}
aria-setsize={this.state.totalElements}
aria-label={this.label || 'Options'}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably use i18n instead of hardcoded strings

'select-option-active': this.state.activeOptionIndex === i
}}
role="option"
aria-selected={false}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

li element above already has role option and the correct implementation of aria-selected

aria-selected={false}
onFocus={() => this.input?.focus()}
onClick={() => (isTagOption ? this.createTag(item.render.label) : this.select(item))}
onKeyDown={e =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed, enter keydown is already working

role="combobox"
ref={el => (this.input = el as HTMLInputElement)}
aria-expanded={false}
aria-controls={`tags-${this.id}-list`}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead we should probably remove combobox role, it's not a combobox

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants