Skip to content
This repository was archived by the owner on Sep 20, 2024. It is now read-only.

Commit 2bef655

Browse files
committed
feat: create live region component
1 parent 362b362 commit 2bef655

File tree

15 files changed

+371
-64
lines changed

15 files changed

+371
-64
lines changed

@types/components.d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*
77
* This is a generated file. Do not edit it's contents.
88
*
9-
* This file was generated on 2023-02-26T18:55:26.214Z
9+
* This file was generated on 2023-02-26T19:46:48.080Z
1010
*/
1111

1212
import { ChakraProps, chakra } from "@chakra-ui/vue-system"
@@ -155,9 +155,18 @@ declare module "@vue/runtime-core" {
155155
CStack: typeof import("@chakra-ui/vue-next")["CStack"]
156156
CStackDivider: typeof import("@chakra-ui/vue-next")["CStackDivider"]
157157
CStackItem: typeof import("@chakra-ui/vue-next")["CStackItem"]
158+
CTable: typeof import("@chakra-ui/vue-next")["CTable"]
159+
CTableCaption: typeof import("@chakra-ui/vue-next")["CTableCaption"]
160+
CTableContainer: typeof import("@chakra-ui/vue-next")["CTableContainer"]
161+
CTbody: typeof import("@chakra-ui/vue-next")["CTbody"]
162+
CTd: typeof import("@chakra-ui/vue-next")["CTd"]
158163
CText: typeof import("@chakra-ui/vue-next")["CText"]
164+
CTfoot: typeof import("@chakra-ui/vue-next")["CTfoot"]
165+
CTh: typeof import("@chakra-ui/vue-next")["CTh"]
166+
CThead: typeof import("@chakra-ui/vue-next")["CThead"]
159167
CThemeProvider: typeof import("@chakra-ui/vue-next")["CThemeProvider"]
160168
CToastContainer: typeof import("@chakra-ui/vue-next")["CToastContainer"]
169+
CTr: typeof import("@chakra-ui/vue-next")["CTr"]
161170
CUnorderedList: typeof import("@chakra-ui/vue-next")["CUnorderedList"]
162171
CVStack: typeof import("@chakra-ui/vue-next")["CVStack"]
163172
CVisuallyHidden: typeof import("@chakra-ui/vue-next")["CVisuallyHidden"]

components.d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*
77
* This is a generated file. Do not edit it's contents.
88
*
9-
* This file was generated on 2023-02-26T18:55:26.214Z
9+
* This file was generated on 2023-02-26T19:46:48.080Z
1010
*/
1111

1212
import { ChakraProps, chakra } from "@chakra-ui/vue-system"
@@ -155,9 +155,18 @@ declare module "@vue/runtime-core" {
155155
CStack: typeof import("@chakra-ui/vue-next")["CStack"]
156156
CStackDivider: typeof import("@chakra-ui/vue-next")["CStackDivider"]
157157
CStackItem: typeof import("@chakra-ui/vue-next")["CStackItem"]
158+
CTable: typeof import("@chakra-ui/vue-next")["CTable"]
159+
CTableCaption: typeof import("@chakra-ui/vue-next")["CTableCaption"]
160+
CTableContainer: typeof import("@chakra-ui/vue-next")["CTableContainer"]
161+
CTbody: typeof import("@chakra-ui/vue-next")["CTbody"]
162+
CTd: typeof import("@chakra-ui/vue-next")["CTd"]
158163
CText: typeof import("@chakra-ui/vue-next")["CText"]
164+
CTfoot: typeof import("@chakra-ui/vue-next")["CTfoot"]
165+
CTh: typeof import("@chakra-ui/vue-next")["CTh"]
166+
CThead: typeof import("@chakra-ui/vue-next")["CThead"]
159167
CThemeProvider: typeof import("@chakra-ui/vue-next")["CThemeProvider"]
160168
CToastContainer: typeof import("@chakra-ui/vue-next")["CToastContainer"]
169+
CTr: typeof import("@chakra-ui/vue-next")["CTr"]
161170
CUnorderedList: typeof import("@chakra-ui/vue-next")["CUnorderedList"]
162171
CVStack: typeof import("@chakra-ui/vue-next")["CVStack"]
163172
CVisuallyHidden: typeof import("@chakra-ui/vue-next")["CVisuallyHidden"]

packages/c-live-region/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# `@chakra-ui/c-live-region`
2+
3+
Creates a hidden live region with dynamic content based on triggered events to be read out by the screen reader on change of the content
4+
5+
## Installation
6+
7+
```sh
8+
# with pnpm
9+
pnpm add @chakra-ui/c-live-region
10+
# or with Yarn
11+
yarn i @chakra-ui/c-live-region
12+
# or with npm
13+
npm i @chakra-ui/c-live-region
14+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<button @click="region.speak('Filtering categories was successful')">
3+
Click Me
4+
</button>
5+
</template>
6+
<script setup lang="ts">
7+
import { useLiveRegion } from "../src/use-live-region"
8+
const region = useLiveRegion()
9+
</script>

packages/c-live-region/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./src"

packages/c-live-region/package.json

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "@chakra-ui/c-live-region",
3+
"description": "Chakra UI Vue | Creates a hidden live region with dynamic content based on triggered events to be read out by the screen reader on change of the content component",
4+
"version": "0.0.0-next.0",
5+
"author": "Jonathan Bakebwa <[email protected]>",
6+
"homepage": "https://github.com/chakra-ui/chakra-ui-vue-next#readme",
7+
"license": "MIT",
8+
"main": "dist/index.js",
9+
"module": "dist/index.mjs",
10+
"typings": "dist/index.d.ts",
11+
"files": [
12+
"dist"
13+
],
14+
"exports": {
15+
".": {
16+
"require": "./dist/index.js",
17+
"default": "./dist/index.mjs"
18+
}
19+
},
20+
"repository": {
21+
"type": "git",
22+
"url": "git+https://github.com/chakra-ui/chakra-ui-vue-next.git"
23+
},
24+
"bugs": {
25+
"url": "https://github.com/chakra-ui/chakra-ui-vue-next/issues"
26+
},
27+
"sideEffects": false,
28+
"scripts": {
29+
"clean": "rimraf dist .turbo",
30+
"build": "tsup && pnpm build:types",
31+
"build:fast": "tsup",
32+
"build:types": "tsup src --dts-only",
33+
"types:check": "tsc --noEmit",
34+
"dev": "tsup --watch"
35+
},
36+
"dependencies": {
37+
"@chakra-ui/vue-system": "workspace:*"
38+
},
39+
"devDependencies": {
40+
"vue": "^3.2.37"
41+
},
42+
"peerDependencies": {
43+
"vue": "^3.1.4"
44+
},
45+
"publishConfig": {
46+
"access": "public"
47+
}
48+
}

packages/c-live-region/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { LiveRegion } from "./live-region"
2+
3+
export type { LiveRegionOptions } from "./live-region"
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { HTMLAttributes } from "vue"
2+
3+
function isDom() {
4+
return Boolean(globalThis?.document)
5+
}
6+
const isBrowser = isDom()
7+
export interface LiveRegionOptions {
8+
/**
9+
* A unique id for the created live region element
10+
*/
11+
id?: string
12+
/**
13+
* Used to mark a part of the page as "live" so that updates will
14+
* be communicated to users by screen readers.
15+
*
16+
* - If set to `polite`: tells assistive technology to alert the user
17+
* to this change when it has finished whatever it is currently doing
18+
*
19+
* - If set to `assertive`: tells assistive technology to interrupt whatever
20+
* it is doing and alert the user to this change immediately
21+
*
22+
* @default "polite".
23+
*/
24+
"aria-live"?: "polite" | "assertive"
25+
/**
26+
* The desired value of the role attribute
27+
* @default "status"
28+
*/
29+
role?: "status" | "alert" | "log"
30+
/**
31+
* Indicates what types of changes should be presented to the user.
32+
* @default "all"
33+
*/
34+
"aria-relevant"?: HTMLAttributes["aria-relevant"]
35+
/**
36+
* Indicates whether the entire region should be
37+
* considered as a whole when communicating updates
38+
*
39+
* @default true
40+
*/
41+
"aria-atomic"?: HTMLAttributes["aria-atomic"]
42+
/**
43+
* The node to append the live region node to
44+
*/
45+
parentNode?: HTMLElement
46+
}
47+
48+
export class LiveRegion {
49+
region: HTMLElement | null
50+
options: Required<LiveRegionOptions>
51+
parentNode: HTMLElement
52+
53+
constructor(options?: LiveRegionOptions) {
54+
this.options = getOptions(options) as any
55+
this.region = getRegion(this.options)
56+
this.parentNode = this.options.parentNode
57+
if (this.region) {
58+
this.parentNode.appendChild(this.region)
59+
}
60+
}
61+
62+
/**
63+
* Message provided to the region to be read out by the Screen Reader.
64+
*
65+
* Message can be supplied on trigger of some event (i.e. button click)
66+
*/
67+
public speak(message: string) {
68+
this.clear()
69+
if (this.region) {
70+
this.region.innerText = message
71+
}
72+
}
73+
74+
/**
75+
* Removes the region.
76+
*/
77+
public destroy() {
78+
if (this.region) {
79+
this.region.parentNode?.removeChild(this.region)
80+
}
81+
}
82+
83+
/**
84+
* Clears the inner text of the region
85+
*/
86+
public clear() {
87+
if (this.region) {
88+
this.region.innerText = ""
89+
}
90+
}
91+
}
92+
93+
function getOptions(options?: LiveRegionOptions) {
94+
const defaultOptions: LiveRegionOptions = {
95+
"aria-live": "polite",
96+
"aria-atomic": "true",
97+
"aria-relevant": "all",
98+
role: "status",
99+
id: "chakra-a11y-live-region",
100+
parentNode: isBrowser ? document.body : undefined,
101+
}
102+
if (options) {
103+
return Object.assign(defaultOptions, options)
104+
}
105+
return defaultOptions
106+
}
107+
108+
function getRegion(options: Required<LiveRegionOptions>) {
109+
let region = isBrowser ? document.getElementById(options.id) : null
110+
111+
if (region) return region
112+
113+
if (isBrowser) {
114+
region = document.createElement("div")
115+
setup(region, options)
116+
}
117+
118+
return region
119+
}
120+
121+
function setup(region: HTMLElement, options: Required<LiveRegionOptions>) {
122+
region.id = options.id || "chakra-live-region"
123+
region.className = "__chakra-live-region"
124+
region.setAttribute("aria-live", options["aria-live"])
125+
region.setAttribute("role", options.role)
126+
region.setAttribute("aria-relevant", options["aria-relevant"])
127+
region.setAttribute("aria-atomic", String(options["aria-atomic"]))
128+
Object.assign(region.style, {
129+
border: "0px",
130+
clip: "rect(0px, 0px, 0px, 0px)",
131+
height: "1px",
132+
width: "1px",
133+
margin: "-1px",
134+
padding: "0px",
135+
overflow: "hidden",
136+
whiteSpace: "nowrap",
137+
position: "absolute",
138+
})
139+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { reactive, watchEffect } from "vue"
2+
import { LiveRegion, LiveRegionOptions } from "./live-region"
3+
4+
/**
5+
* Creates a hidden live region with dynamic content based on triggered events
6+
* to be read out by the screen reader on change of the content.
7+
*/
8+
export function useLiveRegion(options?: LiveRegionOptions) {
9+
const liveRegion = new LiveRegion(options)
10+
11+
watchEffect((cleanup) => cleanup(() => liveRegion.destroy()))
12+
return liveRegion
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { render } from "../../test-utils/src"
2+
import { CLiveRegion } from "../src"
3+
4+
it("should render properly", () => {
5+
const { asFragment } = render(CLiveRegion)
6+
expect(asFragment()).toMatchSnapshot()
7+
})

0 commit comments

Comments
 (0)