Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/healthy-pugs-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/live-region-element": minor
---

Add support for delayMs to announcements, along with support for canceling announcements
13 changes: 7 additions & 6 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,7 @@ const config = {
es6: true,
},
rules: {
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
},
],
'no-unused-vars': 'off',
'import/no-unresolved': 'off',
},
overrides: [
Expand All @@ -43,6 +38,12 @@ const config = {
'import/no-namespace': 'off',
'@typescript-eslint/no-namespace': 'off',
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
},
],
},
},
{
Expand Down
69 changes: 51 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,6 @@ It is **essential** that the `live-region` element exists in the initial HTML pa

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.

### Defining `live-region` as a custom element

The `@primer/live-region-element` package provides an entrypoint that you can use to define the `live-region` custom element.

```ts
import '@primer/live-region-element/define`
```

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:

```ts
import {LiveRegionElement} from '@primer/live-region-element'

if (!customElements.get('live-region')) {
customElements.define('live-region', LiveRegionElement)
}
```

### Declarative Shadow DOM

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:
Expand All @@ -90,6 +72,57 @@ The `live-region` custom element includes support for [Declarative Shadow DOM](h

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

### Delaying announcements

Both `announce` and `announceFromElement` provide support for announcing
messages at a later point in time. In the example below, we are waiting five
seconds before announcing the message.

```ts
import {announce} from '@primer/live-region-element'

announce('Example message', {
delayMs: 5000,
})
```

### Canceling announcements

Any announcements made with `announce` and `announceFromElement` may be
cancelled. This may be useful if a delayed announcements has become outdated. To
cancel an announcement, call the return value of either method.

```ts
import {announce} from '@primer/live-region-element'

const cancel = announce('Example message', {
delayMs: 5000,
})

// At some point before five seconds, call:
cancel()
```

If you would like to clear all announcements, like when transitioning between
routes, you can call the `clear()` method on an existing `LiveRegionElement`.

```ts
const liveRegion = document.querySelector('live-region')

// Send example messages
liveRegion.announce('Example polite message', {
delayMs: 1000,
politeness: 'polite',
})
liveRegion.announce('Example polite message', {
delayMs: 1000,
politeness: 'polite',
})

// Clear all pending messages
liveRegion.clear()
```

## 🙌 Contributing

We're always looking for contributors to help us fix bugs, build new features,
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 120 additions & 0 deletions packages/live-region-element/src/MinHeap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import {Ordering, type Compare} from './order'

export class MinHeap<T> {
#compareFn: Compare<T>
#heap: Array<T>

constructor({compareFn}: {compareFn: Compare<T>}) {
this.#compareFn = compareFn
this.#heap = []
}

insert(value: T) {
this.#heap.push(value)
this.#heapifyUp()
}

pop(): T | undefined {
const item = this.#heap[0]

if (this.#heap[this.#heap.length - 1]) {
this.#heap[0] = this.#heap[this.#heap.length - 1]!
this.#heap.pop()
}
this.#heapifyDown()

return item
}

peek(): T | undefined {
return this.#heap[0]
}

delete(value: T): void {
const index = this.#heap.indexOf(value)
if (index === -1) {
return
}

swap(this.#heap, index, this.#heap.length - 1)
this.#heap.pop()
this.#heapifyDown()
}

clear(): void {
this.#heap = []
}

get size(): number {
return this.#heap.length
}

#heapifyDown() {
let index = 0
while (hasLeftChild(index, this.#heap.length)) {
let smallerChildIndex = getLeftChildIndex(index)
if (
hasRightChild(index, this.#heap.length) &&
this.#compareFn(rightChild(this.#heap, index)!, leftChild(this.#heap, index)!) === Ordering.Less
) {
smallerChildIndex = getRightChildIndex(index)
}
if (this.#compareFn(this.#heap[index]!, this.#heap[smallerChildIndex]!) === Ordering.Less) {
break
} else {
swap(this.#heap, index, smallerChildIndex)
}
index = smallerChildIndex
}
}

#heapifyUp() {
let index = this.#heap.length - 1
while (hasParent(index) && this.#compareFn(this.#heap[index]!, parent(this.#heap, index)!) === Ordering.Less) {
swap(this.#heap, index, getParentIndex(index))
index = getParentIndex(index)
}
}
}

function getLeftChildIndex(index: number) {
return 2 * index + 1
}

function getRightChildIndex(index: number) {
return 2 * index + 2
}

function getParentIndex(index: number) {
return Math.floor((index - 1) / 2)
}

function hasLeftChild(index: number, size: number) {
return getLeftChildIndex(index) < size
}

function hasRightChild(index: number, size: number) {
return getRightChildIndex(index) < size
}

function hasParent(index: number) {
return index > 0
}

function leftChild<T>(heap: Array<T>, index: number) {
return heap[getLeftChildIndex(index)]
}

function rightChild<T>(heap: Array<T>, index: number) {
return heap[getRightChildIndex(index)]
}

function parent<T>(heap: Array<T>, index: number) {
return heap[getParentIndex(index)]
}

function swap(heap: Array<unknown>, a: number, b: number) {
const tmp = heap[a]
heap[a] = heap[b]
heap[b] = tmp
}
85 changes: 85 additions & 0 deletions packages/live-region-element/src/__tests__/MinHeap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {describe, test, beforeEach, expect} from 'vitest'
import {MinHeap} from '../MinHeap'
import {Ordering} from '../order'

describe('MinHeap', () => {
let heap: MinHeap<number>

beforeEach(() => {
heap = new MinHeap({compareFn: compare})
})

test('ordering', () => {
heap.insert(3)
heap.insert(1)
heap.insert(2)
expect(heap.pop()).toBe(1)
expect(heap.pop()).toBe(2)
expect(heap.pop()).toBe(3)
})

test('peek()', () => {
heap.insert(3)
expect(heap.peek()).toBe(3)

heap.insert(1)
expect(heap.peek()).toBe(1)

heap.insert(2)
expect(heap.peek()).toBe(1)

heap.pop()
expect(heap.peek()).toBe(2)
})

test('peek() with empty heap', () => {
expect(heap.peek()).not.toBeDefined()
})

test('delete()', () => {
heap.insert(3)
heap.insert(1)
heap.insert(2)
expect(heap.peek()).toBe(1)

heap.delete(1)
expect(heap.peek()).toBe(2)
})

test('delete() with non-existent value', () => {
heap.insert(3)
heap.insert(1)
heap.insert(2)

expect(heap.peek()).toBe(1)
heap.delete(0)
expect(heap.peek()).toBe(1)
})

test('size', () => {
expect(heap.size).toBe(0)

heap.insert(3)
expect(heap.size).toBe(1)

heap.insert(1)
heap.insert(2)
expect(heap.size).toBe(3)

heap.peek()
expect(heap.size).toBe(3)

heap.pop()
expect(heap.size).toBe(2)
})
})

function compare(a: number, b: number) {
if (a < b) {
return Ordering.Less
}
if (a > b) {
return Ordering.Greater
}
return Ordering.Equal
}
Loading