Skip to content

Commit 689b686

Browse files
authored
feat(live-region-element): add support for delayMs (#29)
* chore: check-in work * feat(live-region-element): add support for delayMs * chore: add changeset * chore(lint): update eslint warnings
1 parent d91f87d commit 689b686

File tree

10 files changed

+493
-47
lines changed

10 files changed

+493
-47
lines changed

.changeset/healthy-pugs-wash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@primer/live-region-element": minor
3+
---
4+
5+
Add support for delayMs to announcements, along with support for canceling announcements

.eslintrc.cjs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,7 @@ const config = {
2727
es6: true,
2828
},
2929
rules: {
30-
'no-unused-vars': [
31-
'error',
32-
{
33-
argsIgnorePattern: '^_',
34-
},
35-
],
30+
'no-unused-vars': 'off',
3631
'import/no-unresolved': 'off',
3732
},
3833
overrides: [
@@ -43,6 +38,12 @@ const config = {
4338
'import/no-namespace': 'off',
4439
'@typescript-eslint/no-namespace': 'off',
4540
'@typescript-eslint/array-type': 'off',
41+
'@typescript-eslint/no-unused-vars': [
42+
'error',
43+
{
44+
argsIgnorePattern: '^_',
45+
},
46+
],
4647
},
4748
},
4849
{

README.md

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,6 @@ It is **essential** that the `live-region` element exists in the initial HTML pa
4747

4848
To do so, include `<live-region></live-region>` in your HTML and make sure that [the custom element has been defined](#defining-live-region-as-a-custom-element). Follow the [Declarative shadow DOM](#declarative-shadow-dom) section below if you would like to include this in your HTML.
4949

50-
### Defining `live-region` as a custom element
51-
52-
The `@primer/live-region-element` package provides an entrypoint that you can use to define the `live-region` custom element.
53-
54-
```ts
55-
import '@primer/live-region-element/define`
56-
```
57-
58-
If you prefer to define the custom element directly, import `LiveRegionElement` directly from the package and use that to define the `live-region` element. For example:
59-
60-
```ts
61-
import {LiveRegionElement} from '@primer/live-region-element'
62-
63-
if (!customElements.get('live-region')) {
64-
customElements.define('live-region', LiveRegionElement)
65-
}
66-
```
67-
6850
### Declarative Shadow DOM
6951

7052
The `live-region` custom element includes support for [Declarative Shadow DOM](https://developer.chrome.com/docs/css-ui/declarative-shadow-dom) and you can leverage this feature by using the following snippet:
@@ -90,6 +72,57 @@ The `live-region` custom element includes support for [Declarative Shadow DOM](h
9072

9173
In addition, a `templateContent` export is available through the package which can be used alongside `<template shadowrootmode="open">` to support this feature.
9274

75+
### Delaying announcements
76+
77+
Both `announce` and `announceFromElement` provide support for announcing
78+
messages at a later point in time. In the example below, we are waiting five
79+
seconds before announcing the message.
80+
81+
```ts
82+
import {announce} from '@primer/live-region-element'
83+
84+
announce('Example message', {
85+
delayMs: 5000,
86+
})
87+
```
88+
89+
### Canceling announcements
90+
91+
Any announcements made with `announce` and `announceFromElement` may be
92+
cancelled. This may be useful if a delayed announcements has become outdated. To
93+
cancel an announcement, call the return value of either method.
94+
95+
```ts
96+
import {announce} from '@primer/live-region-element'
97+
98+
const cancel = announce('Example message', {
99+
delayMs: 5000,
100+
})
101+
102+
// At some point before five seconds, call:
103+
cancel()
104+
```
105+
106+
If you would like to clear all announcements, like when transitioning between
107+
routes, you can call the `clear()` method on an existing `LiveRegionElement`.
108+
109+
```ts
110+
const liveRegion = document.querySelector('live-region')
111+
112+
// Send example messages
113+
liveRegion.announce('Example polite message', {
114+
delayMs: 1000,
115+
politeness: 'polite',
116+
})
117+
liveRegion.announce('Example polite message', {
118+
delayMs: 1000,
119+
politeness: 'polite',
120+
})
121+
122+
// Clear all pending messages
123+
liveRegion.clear()
124+
```
125+
93126
## 🙌 Contributing
94127

95128
We're always looking for contributors to help us fix bugs, build new features,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import {Ordering, type Compare} from './order'
2+
3+
export class MinHeap<T> {
4+
#compareFn: Compare<T>
5+
#heap: Array<T>
6+
7+
constructor({compareFn}: {compareFn: Compare<T>}) {
8+
this.#compareFn = compareFn
9+
this.#heap = []
10+
}
11+
12+
insert(value: T) {
13+
this.#heap.push(value)
14+
this.#heapifyUp()
15+
}
16+
17+
pop(): T | undefined {
18+
const item = this.#heap[0]
19+
20+
if (this.#heap[this.#heap.length - 1]) {
21+
this.#heap[0] = this.#heap[this.#heap.length - 1]!
22+
this.#heap.pop()
23+
}
24+
this.#heapifyDown()
25+
26+
return item
27+
}
28+
29+
peek(): T | undefined {
30+
return this.#heap[0]
31+
}
32+
33+
delete(value: T): void {
34+
const index = this.#heap.indexOf(value)
35+
if (index === -1) {
36+
return
37+
}
38+
39+
swap(this.#heap, index, this.#heap.length - 1)
40+
this.#heap.pop()
41+
this.#heapifyDown()
42+
}
43+
44+
clear(): void {
45+
this.#heap = []
46+
}
47+
48+
get size(): number {
49+
return this.#heap.length
50+
}
51+
52+
#heapifyDown() {
53+
let index = 0
54+
while (hasLeftChild(index, this.#heap.length)) {
55+
let smallerChildIndex = getLeftChildIndex(index)
56+
if (
57+
hasRightChild(index, this.#heap.length) &&
58+
this.#compareFn(rightChild(this.#heap, index)!, leftChild(this.#heap, index)!) === Ordering.Less
59+
) {
60+
smallerChildIndex = getRightChildIndex(index)
61+
}
62+
if (this.#compareFn(this.#heap[index]!, this.#heap[smallerChildIndex]!) === Ordering.Less) {
63+
break
64+
} else {
65+
swap(this.#heap, index, smallerChildIndex)
66+
}
67+
index = smallerChildIndex
68+
}
69+
}
70+
71+
#heapifyUp() {
72+
let index = this.#heap.length - 1
73+
while (hasParent(index) && this.#compareFn(this.#heap[index]!, parent(this.#heap, index)!) === Ordering.Less) {
74+
swap(this.#heap, index, getParentIndex(index))
75+
index = getParentIndex(index)
76+
}
77+
}
78+
}
79+
80+
function getLeftChildIndex(index: number) {
81+
return 2 * index + 1
82+
}
83+
84+
function getRightChildIndex(index: number) {
85+
return 2 * index + 2
86+
}
87+
88+
function getParentIndex(index: number) {
89+
return Math.floor((index - 1) / 2)
90+
}
91+
92+
function hasLeftChild(index: number, size: number) {
93+
return getLeftChildIndex(index) < size
94+
}
95+
96+
function hasRightChild(index: number, size: number) {
97+
return getRightChildIndex(index) < size
98+
}
99+
100+
function hasParent(index: number) {
101+
return index > 0
102+
}
103+
104+
function leftChild<T>(heap: Array<T>, index: number) {
105+
return heap[getLeftChildIndex(index)]
106+
}
107+
108+
function rightChild<T>(heap: Array<T>, index: number) {
109+
return heap[getRightChildIndex(index)]
110+
}
111+
112+
function parent<T>(heap: Array<T>, index: number) {
113+
return heap[getParentIndex(index)]
114+
}
115+
116+
function swap(heap: Array<unknown>, a: number, b: number) {
117+
const tmp = heap[a]
118+
heap[a] = heap[b]
119+
heap[b] = tmp
120+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {describe, test, beforeEach, expect} from 'vitest'
2+
import {MinHeap} from '../MinHeap'
3+
import {Ordering} from '../order'
4+
5+
describe('MinHeap', () => {
6+
let heap: MinHeap<number>
7+
8+
beforeEach(() => {
9+
heap = new MinHeap({compareFn: compare})
10+
})
11+
12+
test('ordering', () => {
13+
heap.insert(3)
14+
heap.insert(1)
15+
heap.insert(2)
16+
expect(heap.pop()).toBe(1)
17+
expect(heap.pop()).toBe(2)
18+
expect(heap.pop()).toBe(3)
19+
})
20+
21+
test('peek()', () => {
22+
heap.insert(3)
23+
expect(heap.peek()).toBe(3)
24+
25+
heap.insert(1)
26+
expect(heap.peek()).toBe(1)
27+
28+
heap.insert(2)
29+
expect(heap.peek()).toBe(1)
30+
31+
heap.pop()
32+
expect(heap.peek()).toBe(2)
33+
})
34+
35+
test('peek() with empty heap', () => {
36+
expect(heap.peek()).not.toBeDefined()
37+
})
38+
39+
test('delete()', () => {
40+
heap.insert(3)
41+
heap.insert(1)
42+
heap.insert(2)
43+
expect(heap.peek()).toBe(1)
44+
45+
heap.delete(1)
46+
expect(heap.peek()).toBe(2)
47+
})
48+
49+
test('delete() with non-existent value', () => {
50+
heap.insert(3)
51+
heap.insert(1)
52+
heap.insert(2)
53+
54+
expect(heap.peek()).toBe(1)
55+
heap.delete(0)
56+
expect(heap.peek()).toBe(1)
57+
})
58+
59+
test('size', () => {
60+
expect(heap.size).toBe(0)
61+
62+
heap.insert(3)
63+
expect(heap.size).toBe(1)
64+
65+
heap.insert(1)
66+
heap.insert(2)
67+
expect(heap.size).toBe(3)
68+
69+
heap.peek()
70+
expect(heap.size).toBe(3)
71+
72+
heap.pop()
73+
expect(heap.size).toBe(2)
74+
})
75+
})
76+
77+
function compare(a: number, b: number) {
78+
if (a < b) {
79+
return Ordering.Less
80+
}
81+
if (a > b) {
82+
return Ordering.Greater
83+
}
84+
return Ordering.Equal
85+
}

0 commit comments

Comments
 (0)