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" />}