Skip to content
This repository was archived by the owner on Sep 20, 2024. It is now read-only.
Closed
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
11 changes: 11 additions & 0 deletions packages/c-live-region/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# @chakra-ui/c-live-region

Live region

## Installation

```sh
yarn add @chakra-ui/c-live-region
# or
npm i @chakra-ui/c-live-region
```
10 changes: 10 additions & 0 deletions packages/c-live-region/examples/base-live-region.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<template>
<button @click="region.speak('Filtering categories was successful')">
Click Me
</button>
</template>
<script setup lang="ts">
import { useLiveRegion } from "../src/use-live-region"

const region = useLiveRegion()
</script>
1 change: 1 addition & 0 deletions packages/c-live-region/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./src"
39 changes: 39 additions & 0 deletions packages/c-live-region/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@chakra-ui/c-live-region",
"description": "Chakra UI Vue | Live region component",
"version": "0.0.0-alpha.0",
"main": "dist/chakra-ui-c-live-region.cjs.js",
"module": "dist/chakra-ui-c-live-region.esm.js",
"author": "Jonathan Bakebwa <codebender828@gmail.com>",
"homepage": "https://github.com/chakra-ui/chakra-ui-vue-next#readme",
"license": "MIT",
"files": [
"dist"
],
"exports": {
".": {
"require": "./dist/chakra-ui-c-live-region.cjs.js",
"default": "./dist/chakra-ui-c-live-region.esm.js"
}
},
"repository": "https://github.com/chakra-ui/chakra-ui-vue-next/tree/master/packages/c-live-region",
"bugs": {
"url": "https://github.com/chakra-ui/chakra-ui-vue-next/issues"
},
"sideEffects": false,
"scripts": {
"clean": "rimraf dist"
},
"dependencies": {
"@chakra-ui/vue-system": "0.1.0-alpha.10"
},
"devDependencies": {
"vue": "^3.2.37"
},
"peerDependencies": {
"vue": "^3.1.4"
},
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/c-live-region/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./live-region"
139 changes: 139 additions & 0 deletions packages/c-live-region/src/live-region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { HTMLAttributes } from "vue"

function isDom() {
return Boolean(globalThis?.document)
}
const isBrowser = isDom()
export interface LiveRegionOptions {
/**
* A unique id for the created live region element
*/
id?: string
/**
* Used to mark a part of the page as "live" so that updates will
* be communicated to users by screen readers.
*
* - If set to `polite`: tells assistive technology to alert the user
* to this change when it has finished whatever it is currently doing
*
* - If set to `assertive`: tells assistive technology to interrupt whatever
* it is doing and alert the user to this change immediately
*
* @default "polite".
*/
"aria-live"?: "polite" | "assertive"
/**
* The desired value of the role attribute
* @default "status"
*/
role?: "status" | "alert" | "log"
/**
* Indicates what types of changes should be presented to the user.
* @default "all"
*/
"aria-relevant"?: HTMLAttributes["aria-relevant"]
/**
* Indicates whether the entire region should be
* considered as a whole when communicating updates
*
* @default true
*/
"aria-atomic"?: HTMLAttributes["aria-atomic"]
/**
* The node to append the live region node to
*/
parentNode?: HTMLElement
}

export class LiveRegion {
region: HTMLElement | null
options: Required<LiveRegionOptions>
parentNode: HTMLElement

constructor(options?: LiveRegionOptions) {
this.options = getOptions(options) as any
this.region = getRegion(this.options)
this.parentNode = this.options.parentNode
if (this.region) {
this.parentNode.appendChild(this.region)
}
}

/**
* Message provided to the region to be read out by the Screen Reader.
*
* Message can be supplied on trigger of some event (i.e. button click)
*/
public speak(message: string) {
this.clear()
if (this.region) {
this.region.innerText = message
}
}

/**
* Removes the region.
*/
public destroy() {
if (this.region) {
this.region.parentNode?.removeChild(this.region)
}
}

/**
* Clears the inner text of the region
*/
public clear() {
if (this.region) {
this.region.innerText = ""
}
}
}
Comment on lines +48 to +91
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @TylerAPfledderer

Can't this class be a component instead? It seem that it only creates/appends attributes to certain elements, which seems better as a component to me.

Also, using methods such as removeChild should only be done when there is no other solution, otherwise it's a bit of an anti-pattern in Vue. So i'd say that if we have the choice let's create a component.


function getOptions(options?: LiveRegionOptions) {
const defaultOptions: LiveRegionOptions = {
"aria-live": "polite",
"aria-atomic": "true",
"aria-relevant": "all",
role: "status",
id: "chakra-a11y-live-region",
parentNode: isBrowser ? document.body : undefined,
}
if (options) {
return Object.assign(defaultOptions, options)
}
return defaultOptions
}

function getRegion(options: Required<LiveRegionOptions>) {
let region = isBrowser ? document.getElementById(options.id) : null

if (region) return region

if (isBrowser) {
region = document.createElement("div")
setup(region, options)
}

return region
}

function setup(region: HTMLElement, options: Required<LiveRegionOptions>) {
region.id = options.id || "chakra-live-region"
region.className = "__chakra-live-region"
region.setAttribute("aria-live", options["aria-live"])
region.setAttribute("role", options.role)
region.setAttribute("aria-relevant", options["aria-relevant"])
region.setAttribute("aria-atomic", String(options["aria-atomic"]))
Object.assign(region.style, {
border: "0px",
clip: "rect(0px, 0px, 0px, 0px)",
height: "1px",
width: "1px",
margin: "-1px",
padding: "0px",
overflow: "hidden",
whiteSpace: "nowrap",
position: "absolute",
})
}
Comment on lines +121 to +139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems better to me to pass a Ref instead of an HTMLElement might be better for reactivity.

Also, this setup function also looks like it can also just be a component

14 changes: 14 additions & 0 deletions packages/c-live-region/src/use-live-region.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { reactive, watchEffect } from "vue"
import { LiveRegion, LiveRegionOptions } from "./live-region"

/**
* 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.
*/
export function useLiveRegion(options?: LiveRegionOptions) {
const liveRegion = reactive(() => new LiveRegion(options))

watchEffect((cleanup) => cleanup(() => liveRegion().destroy()))

return liveRegion()
}
Comment on lines +8 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about it, seem better to have a CLiveRegion component instead of a hook imo.

All the options passed can basically be props and the rest of the code would be much simpler and more aligned with the Vue philosophy.
It will also allow to remove some of the code. I think that eventhough we need to be as close to the React package, there are some things that need to be done differently due to the different philosophy between React and Vue.

What is your opinion about this @codebender828 , @TylerAPfledderer ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Shyrro @codebender828 for context and my understanding of LiveRegion, it is a class because the hook triggers an instance which renders an element out of view of the visual user; a hidden portal of sorts specific to the use of a acreen reader. Then you pull methods (events) from the instance through the hook, to apply to what triggers are used in a given component.

The developer can certainly access the other data to make their own reason, but it did not seem to be intended by default for someone to have to make an element just for a screen reader to readout a simple message on change from some event.

I would not mind converting to a component, with composables to house the props that can be exported separately, but then the question would be how this new component is to be implemented in the environment.

I'm primarily thinking about how the speak method is currently executed versus an alternative approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh if you want to access methods of the components or other data you can expose them through the expose method of your component. They'll then be available through template refs

And for the fact the the component is outside the screen it doesn't really matter if you wrap it in a Teleport or CPortal

It just feels the "Vue way" so to say. Also, when manipulating the DOM directly through document.createElement etc, basically nothing is tracked so you're instance doesn't have any reactivity whatsoever.

I might be wrong about this since this is a special hook, but it just feels more natural in Vue to have in this case a logical component instead of a hook.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood! I'll take a look at expose.

I'll play around with it, because you could probably argue the same case against direct DOM manipulation in React here with the reactivity, so maybe I can ask one of the React folks on the reason for this approach. 🤔

Better for me to know at least so I can get a better idea on differences! 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if u need any more clarification on what i'm saying or any more explanation on this approach.

We can even discuss it in a call, might be easier ☺️

54 changes: 54 additions & 0 deletions packages/c-live-region/tests/c-live-region.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render } from "@chakra-ui/vue-test-utils"
import { LiveRegion, LiveRegionOptions } from "../index"

const renderDiv = () =>
render({
template: `<div/>`,
})

describe("LiveRegion", () => {
it("creates a container and has the proper aria and role attributes", () => {
renderDiv()

new LiveRegion()
const region = document.getElementById("chakra-a11y-live-region")

expect(region).toHaveAttribute("aria-atomic", "true")
expect(region).toHaveAttribute("aria-live", "polite")
expect(region).toHaveAttribute("aria-relevant", "all")
expect(region).toHaveAttribute("role", "status")
})

it("creates a container using the provided options", () => {
renderDiv()

const options: LiveRegionOptions = {
id: "some-id",
role: "alert",
"aria-live": "assertive",
"aria-relevant": "removals",
"aria-atomic": "false",
}

// eslint-disable-next-line no-new
new LiveRegion(options)
const region = document.getElementById("some-id")

expect(region).toHaveAttribute("aria-atomic", "false")
expect(region).toHaveAttribute("aria-live", "assertive")
expect(region).toHaveAttribute("aria-relevant", "removals")
expect(region).toHaveAttribute("role", "alert")
})

it("can 'speak' by setting its text content", () => {
renderDiv()

const liveRegion = new LiveRegion()
const region = document.getElementById(
"chakra-a11y-live-region"
) as HTMLElement

liveRegion.speak("Hello World")
expect(region.innerText).toEqual("Hello World")
})
})
4 changes: 4 additions & 0 deletions packages/c-live-region/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "./index.tsx", "./index.ts"]
}
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@chakra-ui/c-form-control": "0.0.0-alpha.5",
"@chakra-ui/c-icon": "1.0.0-alpha.10",
"@chakra-ui/c-input": "0.0.0-alpha.5",
"@chakra-ui/c-live-region": "0.0.0-alpha.0",
"@chakra-ui/c-modal": "1.1.0-alpha.10",
"@chakra-ui/c-motion": "0.1.0-alpha.9",
"@chakra-ui/c-pin-input": "0.0.0-alpha.0",
Expand All @@ -47,7 +48,7 @@
"@chakra-ui/c-theme-provider": "1.0.0-alpha.10",
"@chakra-ui/c-visually-hidden": "1.0.0-alpha.10",
"@chakra-ui/styled-system": "^2.2.2",
"@chakra-ui/utils": "^2.0.3",
"@chakra-ui/utils": "2.0.11",
"@chakra-ui/vue-a11y": "0.1.0-alpha.9",
"@chakra-ui/vue-composables": "0.1.0-alpha.9",
"@chakra-ui/vue-layout": "0.1.0-alpha.11",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export * from "@chakra-ui/c-input"

// L
export * from "@chakra-ui/vue-layout"
export * from "@chakra-ui/c-live-region"

// M
export * from "@chakra-ui/c-modal"
Expand Down