Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[A11Y] Accessibility improvements for the Search component #3017

Merged
merged 2 commits into from
Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions js/src/forum/components/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,18 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone

const searchLabel = extractText(app.translator.trans('core.forum.header.search_placeholder'));

const isActive = !!currentSearch;
const shouldShowResults = !!(!this.loadingSources && this.state.getValue() && this.hasFocus);
const shouldShowClearButton = !!(!this.loadingSources && this.state.getValue());

return (
<div
role="search"
className={classList({
Search: true,
aria-label={app.translator.trans('core.forum.header.search_role_label')}
className={classList('Search', {
open: this.state.getValue() && this.hasFocus,
focused: this.hasFocus,
active: !!currentSearch,
active: isActive,
loading: !!this.loadingSources,
})}
>
Expand All @@ -124,18 +128,23 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
onfocus={() => (this.hasFocus = true)}
onblur={() => (this.hasFocus = false)}
/>
{this.loadingSources ? (
<LoadingIndicator size="small" display="inline" containerClassName="Button Button--icon Button--link" />
) : currentSearch ? (
<button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>
{!!this.loadingSources && <LoadingIndicator size="small" display="inline" containerClassName="Button Button--icon Button--link" />}
{shouldShowClearButton && (
<button
className="Search-clear Button Button--icon Button--link"
onclick={this.clear.bind(this)}
aria-label={app.translator.trans('core.forum.header.search_clear_button_accessible_label')}
>
{icon('fas fa-times-circle')}
</button>
) : (
''
)}
</div>
<ul className="Dropdown-menu Search-results">
{this.state.getValue() && this.hasFocus ? this.sources.map((source) => source.view(this.state.getValue())) : ''}
<ul
className="Dropdown-menu Search-results"
aria-hidden={!shouldShowResults || undefined}
aria-live={shouldShowResults ? 'polite' : undefined}
>
{shouldShowResults && this.sources.map((source) => source.view(this.state.getValue()))}
</ul>
</div>
);
Expand Down Expand Up @@ -173,7 +182,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone

this.$('.Search-results')
.on('mousedown', (e) => e.preventDefault())
.on('click', () => this.$('input').blur())
.on('click', () => this.$('input').trigger('blur'))

// Whenever the mouse is hovered over a search result, highlight it.
.on('mouseenter', '> li:not(.Dropdown-header)', function () {
Expand Down Expand Up @@ -222,7 +231,7 @@ export default class Search<T extends SearchAttrs = SearchAttrs> extends Compone
.on('focus', function () {
$(this)
.one('mouseup', (e) => e.preventDefault())
.select();
.trigger('select');
});

this.updateMaxHeightHandler = this.updateMaxHeight.bind(this);
Expand Down
11 changes: 11 additions & 0 deletions less/common/Search.less
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
.Search {
position: relative;

&-clear {
// It looks very weird due to the padding given to the button..
&:focus {
outline: none;
}

// ...so we display the ring around the icon inside the button, with an offset
.add-keyboard-focus-ring-nearby("> *");
.add-keyboard-focus-ring-nearby-offset("> *", 4px);
}
}
@media @tablet-up {
.Search {
Expand Down
68 changes: 68 additions & 0 deletions less/common/mixins/accessibility.less
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,38 @@
}
}

/**
* This mixin allows support for a custom element nearby the focused one
* to have a focus style applied to it
*
* For example...
*
*? button { .add-keyboard-focus-ring-nearby("+ .myOtherElement") }
* becomes
*? button:-moz-focusring + .myOtherElement { <styles> }
*? button:focus-within + .myOtherElement { <styles> }
*/
.add-keyboard-focus-ring-nearby(@nearbySelector) {
@realNearbySelector: ~"@{nearbySelector}";

// We need to declare these separately, otherwise
// browsers will ignore `:focus-visible` as they
// don't understand `:-moz-focusring`

// These are the keyboard-only versions of :focus
&:-moz-focusring {
@{realNearbySelector} {
#private.__focus-ring-styles();
}
}

&:focus-visible {
@{realNearbySelector} {
#private.__focus-ring-styles();
}
}
}

/**
* Allows an offset to be supplied for an a11y
* outline.
Expand Down Expand Up @@ -98,3 +130,39 @@
.offset();
}
}

/**
* This mixin allows support for a custom element nearby the focused one
* to have a focus style applied to it
*
* For example...
*
*? button { .add-keyboard-focus-ring-nearby("+ .myOtherElement") }
* becomes
*? button:-moz-focusring + .myOtherElement { <styles> }
*? button:focus-within + .myOtherElement { <styles> }
*/
.add-keyboard-focus-ring-nearby-offset(@nearbySelector, @offset) {
@realNearbySelector: ~"@{nearbySelector}";

.offset() {
outline-offset: @offset;
}

// We need to declare these separately, otherwise
// browsers will ignore `:focus-visible` as they
// don't understand `:-moz-focusring`

// These are the keyboard-only versions of :focus
&:-moz-focusring {
@{realNearbySelector} {
.offset();
}
}

&:focus-visible {
@{realNearbySelector} {
.offset();
}
}
}
2 changes: 2 additions & 0 deletions locale/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ core:
log_in_link: => core.ref.log_in
log_out_button: => core.ref.log_out
profile_button: Profile
search_clear_button_accessible_label: Clear search query
search_placeholder: Search Forum
search_role_label: Search Forum
session_dropdown_accessible_label: Toggle session options dropdown menu
settings_button: => core.ref.settings
sign_up_link: => core.ref.sign_up
Expand Down