A11y: announce copy to clipboard action (#310)

* A11y: announce copy to clipboard action * Add style to hide the container * Fixed lint issues Co-authored-by: Seryozha Khachatryan <seryozha95@github.com>

Nurbol Alpysbayev committed Nov 14, 2022 at 13:31 UTC 13877be908a16bf5fe5b831570616376bdc43d4e
2 files changed +70
theme/src/aria-live.js new
+68
@@ -0,0 +1,68 @@
1 +/**
2 + * This is an implementation of the
3 + * aria-live guide:
4 + * https://github.com/github/thehub/blob/main/docs/epd/engineering/dev-practicals/frontend/accessibility/readiness-routine/screenreaders/live-regions-and-screen-reader-announcements.md
5 + *
6 + * The code is heavily borrowed from https://github.com/github/github/blob/master/app/assets/modules/github/aria-live.ts
7 + */
8 +
9 +let container
10 +
11 +if (typeof window !== 'undefined') {
12 + document.addEventListener("DOMContentLoaded", function(event) {
13 + createNoticeContainer()
14 + });
15 + }
16 +
17 + // Announce message update to screen reader.
18 +function announce(message) {
19 + if (!container || !container.isConnected) {
20 + /* This condition is for when the aria-live container no longer exists due to nav methods like turbo drive
21 + which replace the body getting rid of the region. We add the container if it's missing. We then add a delay
22 + to ensure aria-live can work correctly.
23 + Note: This approach is not ideal and should be revisited.
24 + See https://github.com/github/accessibility/issues/1900#issuecomment-1254369320
25 + */
26 + createNoticeContainer()
27 + setTimeout(() => {
28 + setContainerContent(message)
29 + }, 200)
30 + } else {
31 + setContainerContent(message)
32 + }
33 + }
34 +
35 + // Set aria-live container to message.
36 +function setContainerContent(message) {
37 + if (!container) return
38 + if (container.textContent === message) {
39 + /* This is a hack due to the way the aria live API works.
40 + A screen reader will not read a live region again
41 + if the text is the same. Adding a space character tells
42 + the browser that the live region has updated,
43 + which will cause it to read again, but with no audible difference. */
44 + container.textContent = `${message}\u00A0`
45 + } else {
46 + container.textContent = message
47 + }
48 + }
49 +
50 + // Get the global screen reader notice container.
51 +function createNoticeContainer() {
52 + container = document.createElement('div')
53 + container.setAttribute('aria-live', 'polite')
54 + container.style.position = 'absolute'
55 + container.style.width = '1px'
56 + container.style.height = '1px'
57 + container.style.padding = '0'
58 + container.style.overflow = 'hidden'
59 + container.style.clip = 'rect(0, 0, 0, 0)'
60 + container.style.wordWrap = 'normal'
61 + container.style.border = '0'
62 + document.body.append(container)
63 + }
64 +
65 +module.exports = {
66 + announce
67 + }
68 +
\ No newline at end of file
theme/src/components/clipboard-copy.js
+2
@@ -3,6 +3,7 @@ import {CheckIcon, CopyIcon} from '@primer/octicons-react'
3 import styled from 'styled-components'
4 import copy from 'copy-to-clipboard'
5 import React from 'react'
6 +import {announce} from '../aria-live'
7
8 const CopyToClipboard = styled(Button)`
9 &:focus {
@@ -27,6 +28,7 @@ function ClipboardCopy({value}) {
28 onClick={() => {
29 copy(value)
30 setCopied(true)
31 + announce(`Copied to clipboard`)
32 }}
33 >
34 {copied ? <StyledOcticon icon={CheckIcon} color="green.5" /> : <StyledOcticon icon={CopyIcon} color="gray.7" />}