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

fix(a11y): multiple a11y fixes #14

Merged
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
5 changes: 4 additions & 1 deletion packages/emoji-mart-data/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"6": "Dark"
},
"a11y": {
"available_emojis": "Available emojis"
"available_emojis": "Available emojis",
"emojis_found_plural": "emojis found",
JanPodmajersky marked this conversation as resolved.
Show resolved Hide resolved
"emoji_found_singular": "emoji found",
"search_input_aria_label": "Search emojis"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export default class Navigation extends PureComponent {
type="button"
class="flex flex-grow flex-center"
role="tab"
disabled={this.props.disabled}
JanPodmajersky marked this conversation as resolved.
Show resolved Hide resolved
tabIndex={selected ? 0 : -1}
onMouseDown={(e) => e.preventDefault()}
onClick={() => {
Expand Down
75 changes: 48 additions & 27 deletions packages/emoji-mart/src/components/Picker/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { Category } from '@slidoapp/emoji-mart-data'
import { Emoji } from '../Emoji'
import { PureInlineComponent } from '../HOCs'
import { Navigation } from '../Navigation'
import ScreenReaderAnnouncement from '../ScreenReaderAnnouncement'

const Performance = {
rowsPerRender: 10,
Expand Down Expand Up @@ -707,6 +708,7 @@ export default class Picker extends Component {
theme={this.state.theme}
dir={this.dir}
unfocused={!!this.state.searchResults}
disabled={!!this.state.searchResults}
position={this.props.navPosition}
onClick={this.handleCategoryClick}
/>
Expand Down Expand Up @@ -856,6 +858,22 @@ export default class Picker extends Component {
)
}

getResultMessage() {
const { searchResults } = this.state
if (searchResults === undefined || searchResults === null) return ''

if (searchResults.length <= 0) {
return I18n.search_no_results_2
} else {
let count = searchResults.flat().length
let translation =
count === 1
? I18n?.a11y?.emoji_found_singular
: I18n?.a11y?.emojis_found_plural
return [count, translation].join(' ')
}
}

renderSearch() {
const renderSkinTone =
this.props.previewPosition == 'none' ||
Expand All @@ -873,9 +891,12 @@ export default class Picker extends Component {
onClick={this.handleSearchClick}
onInput={this.handleSearchInput}
onKeyDown={this.handleEmojisKeyDown}
autoComplete="off"
></input>
<span class="icon loupe flex">{Icons.search.loupe}</span>
aria-label={I18n?.a11y?.search_input_aria_label}
autocomplete="off"
/>
<span aria-hidden="true" class="icon loupe flex">
{Icons.search.loupe}
</span>
{this.state.searchResults && (
<button
title="Clear"
Expand All @@ -889,7 +910,6 @@ export default class Picker extends Component {
</button>
)}
</div>

{renderSkinTone && this.renderSkinToneButton()}
</div>
</div>
Expand All @@ -913,6 +933,11 @@ export default class Picker extends Component {
<div>
{!searchResults.length ? (
<div class={`padding-small align-${this.dir[0]}`}>
{this.props.previewPosition === 'none' && (
<p>
{I18n.search_no_results_1} {I18n.search_no_results_2}
</p>
)}
{this.props.onAddCustomEmoji && (
<a onClick={this.props.onAddCustomEmoji}>{I18n.add_custom}</a>
)}
Expand Down Expand Up @@ -951,6 +976,7 @@ export default class Picker extends Component {
}}
role="listbox"
onKeyDown={this.handleEmojisKeyDown}
aria-label={I18n?.a11y?.available_emojis}
>
{categories.map((category) => {
const { root, rows } = this.refs.categories.get(category.id)
Expand All @@ -960,12 +986,15 @@ export default class Picker extends Component {
<div
data-id={category.target ? category.target.id : category.id}
class="category"
role="group"
aria-labelledby={`${category.id}-heading`}
ref={root}
>
<div
aria-label={categoryName + ' emojis'}
aria-level="5"
role="heading"
id={`${category.id}-heading`}
class={`sticky padding-small align-${this.dir[0]}`}
>
{categoryName}
Expand Down Expand Up @@ -1039,6 +1068,8 @@ export default class Picker extends Component {
return null
}

const ariaSkinToneShown = this.state.showSkins ? 'true' : 'false'

return (
<div
class="flex flex-auto flex-center flex-middle"
Expand All @@ -1052,7 +1083,10 @@ export default class Picker extends Component {
type="button"
ref={this.refs.skinToneButton}
class="skin-tone-button flex flex-auto flex-center flex-middle"
aria-selected={this.state.showSkins ? 'true' : 'false'}
aria-selected={ariaSkinToneShown}
aria-expanded={ariaSkinToneShown}
aria-controls="skin-tone-options"
aria-haspopup="true"
aria-label={`${I18n.skins.choose}, ${I18n.skins[this.state.skin]}`}
title={I18n.skins.choose}
onClick={this.openSkins}
Expand All @@ -1067,24 +1101,6 @@ export default class Picker extends Component {
)
}

JanPodmajersky marked this conversation as resolved.
Show resolved Hide resolved
renderLiveRegion() {
const emoji = this.getEmojiByPos(this.state.pos)
const noSearchResults =
this.state.searchResults == null || this.state.searchResults.length === 0

const contents = emoji
? emoji.name
: noSearchResults
? I18n.search_no_results_2
: ''

return (
<div aria-live="polite" class="sr-only">
{contents}
</div>
)
}

renderSkins() {
const skinToneButton = this.refs.skinToneButton.current
const skinToneButtonRect = skinToneButton.getBoundingClientRect()
Expand Down Expand Up @@ -1117,6 +1133,7 @@ export default class Picker extends Component {
class="menu hidden"
data-position={position.top ? 'top' : 'bottom'}
style={position}
id="skin-tone-options"
>
{[...Array(6).keys()].map((i) => {
const skin = i + 1
Expand Down Expand Up @@ -1166,8 +1183,9 @@ export default class Picker extends Component {
const lineWidth = this.props.perLine * this.props.emojiButtonSize

return (
<section
<form
id="root"
role="search"
class="flex flex-column"
dir={this.dir}
style={{
Expand All @@ -1191,7 +1209,7 @@ export default class Picker extends Component {
width: this.props.dynamicWidth ? '100%' : lineWidth,
height: '100%',
}}
aria-label={I18n.a11y?.available_emojis ?? 'Available emojis'}
role="presentation"
>
{this.props.searchPosition == 'static' && this.renderSearch()}
{this.renderSearchResults()}
Expand All @@ -1202,8 +1220,11 @@ export default class Picker extends Component {
{this.props.navPosition == 'bottom' && this.renderNav()}
{this.props.previewPosition == 'bottom' && this.renderPreview()}
{this.state.showSkins && this.renderSkins()}
{this.renderLiveRegion()}
</section>
<ScreenReaderAnnouncement
level="polite"
text={this.getResultMessage()}
/>
</form>
)
}
}
16 changes: 12 additions & 4 deletions packages/emoji-mart/src/components/Picker/PickerStyles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,15 @@
}

.sr-only {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
white-space: nowrap;
width: 1px;
JanPodmajersky marked this conversation as resolved.
Show resolved Hide resolved
}

a {
Expand Down Expand Up @@ -293,6 +296,11 @@ button {
&:hover { color: var(--color-a) }
}

button:disabled {
color: var(--color-c);
cursor: not-allowed;
}

svg, img {
width: var(--category-icon-size);
height: var(--category-icon-size);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect, useState } from 'preact/hooks'

type Level = 'assertive' | 'polite'

type AnnouncementProps = {
text: string
level: Level
delay: number
timeout: number
}

/**
* Component which will cause a screen reader to announce a message when required.
*/
const ScreenReaderAnnouncement = ({
delay = 1500,
level,
text,
timeout = 2000,
}: AnnouncementProps) => {
const [message, setMessage] = useState('')

useEffect(() => {
let timer = setTimeout(() => {
setMessage(text)

timer = setTimeout(() => {
setMessage('')
}, timeout)
}, delay)

return () => {
clearTimeout(timer)
}
}, [delay, text, timeout])

return (
<div aria-live={level} className="sr-only" aria-atomic="true">
{message}
</div>
)
}

export default ScreenReaderAnnouncement