@jupyter/chat 0.25.0-alpha.0 → 0.25.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import React, { act } from 'react';
|
|
6
|
+
import { createRoot } from 'react-dom/client';
|
|
7
|
+
// React 18 asks test environments to declare themselves, otherwise every
|
|
8
|
+
// `act` call warns.
|
|
9
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
import { WritingIndicator } from '../components/writing-indicator';
|
|
11
|
+
const alice = { username: 'a', name: 'Alice', display_name: 'Alice' };
|
|
12
|
+
const writer = (user, typingIndicator) => ({ user, typingIndicator });
|
|
13
|
+
describe('WritingIndicator accessibility', () => {
|
|
14
|
+
let container;
|
|
15
|
+
let root;
|
|
16
|
+
const render = (writers) => {
|
|
17
|
+
act(() => {
|
|
18
|
+
root.render(React.createElement(WritingIndicator, { writers: writers }));
|
|
19
|
+
});
|
|
20
|
+
return container.querySelector('.jp-chat-writers');
|
|
21
|
+
};
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
container = document.createElement('div');
|
|
24
|
+
document.body.appendChild(container);
|
|
25
|
+
root = createRoot(container);
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
act(() => root.unmount());
|
|
29
|
+
container.remove();
|
|
30
|
+
});
|
|
31
|
+
it('is a polite live region, so a reply on its way is announced', () => {
|
|
32
|
+
// The indicator is the only signal that someone is replying. Without a
|
|
33
|
+
// live region it is visible text that assistive technology never speaks.
|
|
34
|
+
const el = render([]);
|
|
35
|
+
expect(el.getAttribute('role')).toBe('status');
|
|
36
|
+
expect(el.getAttribute('aria-live')).toBe('polite');
|
|
37
|
+
});
|
|
38
|
+
it('reads the whole phrase rather than a fragment of it', () => {
|
|
39
|
+
// Without aria-atomic, a change to part of the text can be announced on
|
|
40
|
+
// its own, so a reader hears a bare name with no context.
|
|
41
|
+
expect(render([]).getAttribute('aria-atomic')).toBe('true');
|
|
42
|
+
});
|
|
43
|
+
it('announces who is writing', () => {
|
|
44
|
+
expect(render([writer(alice)]).textContent).toContain('Alice is typing');
|
|
45
|
+
});
|
|
46
|
+
it('announces a custom indicator, not just a generic one', () => {
|
|
47
|
+
const el = render([writer(alice, 'is running `ripgrep`')]);
|
|
48
|
+
expect(el.textContent).toContain('Alice is running `ripgrep`');
|
|
49
|
+
});
|
|
50
|
+
it('says nothing when nobody is writing', () => {
|
|
51
|
+
var _a;
|
|
52
|
+
// The container is always rendered to reserve space, and holds a
|
|
53
|
+
// non-breaking space as a placeholder. That placeholder must not be
|
|
54
|
+
// announced as if it were a message.
|
|
55
|
+
const text = ((_a = render([]).textContent) !== null && _a !== void 0 ? _a : '').replace(/ /g, '').trim();
|
|
56
|
+
expect(text).toBe('');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -57,7 +57,17 @@ export function WritingIndicator(props) {
|
|
|
57
57
|
const trans = useTranslator();
|
|
58
58
|
// Always render the container to reserve space, even if no writers
|
|
59
59
|
const writersText = writers.length > 0 ? formatWritersText(writers, trans) : '';
|
|
60
|
-
return (React.createElement(Box, { className: WRITERS_ELEMENT_CLASSNAME,
|
|
60
|
+
return (React.createElement(Box, { className: WRITERS_ELEMENT_CLASSNAME,
|
|
61
|
+
// The indicator already says something useful, "Alice is typing..." or
|
|
62
|
+
// "Jupyternaut is running `ripgrep`", but only on screen. Announcing it
|
|
63
|
+
// politely means a screen reader user learns a reply is coming without
|
|
64
|
+
// being interrupted mid-sentence.
|
|
65
|
+
//
|
|
66
|
+
// The region is the container rather than the text, so it is present in
|
|
67
|
+
// the accessibility tree before a writer appears and the change is
|
|
68
|
+
// announced. `aria-atomic` keeps the phrase together: without it a name
|
|
69
|
+
// change alone can be read out on its own, stripped of its context.
|
|
70
|
+
role: "status", "aria-live": "polite", "aria-atomic": "true", sx: {
|
|
61
71
|
...props.sx,
|
|
62
72
|
minHeight: '16px'
|
|
63
73
|
} },
|
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Jupyter Development Team.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { act } from 'react';
|
|
7
|
+
import { createRoot, Root } from 'react-dom/client';
|
|
8
|
+
|
|
9
|
+
// React 18 asks test environments to declare themselves, otherwise every
|
|
10
|
+
// `act` call warns.
|
|
11
|
+
(
|
|
12
|
+
globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }
|
|
13
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
|
+
|
|
15
|
+
import { WritingIndicator } from '../components/writing-indicator';
|
|
16
|
+
import { IChatModel } from '../model';
|
|
17
|
+
import { IUser } from '../types';
|
|
18
|
+
|
|
19
|
+
const alice: IUser = { username: 'a', name: 'Alice', display_name: 'Alice' };
|
|
20
|
+
|
|
21
|
+
const writer = (user: IUser, typingIndicator?: string): IChatModel.IWriter =>
|
|
22
|
+
({ user, typingIndicator }) as IChatModel.IWriter;
|
|
23
|
+
|
|
24
|
+
describe('WritingIndicator accessibility', () => {
|
|
25
|
+
let container: HTMLDivElement;
|
|
26
|
+
let root: Root;
|
|
27
|
+
|
|
28
|
+
const render = (writers: IChatModel.IWriter[]) => {
|
|
29
|
+
act(() => {
|
|
30
|
+
root.render(<WritingIndicator writers={writers} />);
|
|
31
|
+
});
|
|
32
|
+
return container.querySelector('.jp-chat-writers') as HTMLElement;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
container = document.createElement('div');
|
|
37
|
+
document.body.appendChild(container);
|
|
38
|
+
root = createRoot(container);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
act(() => root.unmount());
|
|
43
|
+
container.remove();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('is a polite live region, so a reply on its way is announced', () => {
|
|
47
|
+
// The indicator is the only signal that someone is replying. Without a
|
|
48
|
+
// live region it is visible text that assistive technology never speaks.
|
|
49
|
+
const el = render([]);
|
|
50
|
+
expect(el.getAttribute('role')).toBe('status');
|
|
51
|
+
expect(el.getAttribute('aria-live')).toBe('polite');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('reads the whole phrase rather than a fragment of it', () => {
|
|
55
|
+
// Without aria-atomic, a change to part of the text can be announced on
|
|
56
|
+
// its own, so a reader hears a bare name with no context.
|
|
57
|
+
expect(render([]).getAttribute('aria-atomic')).toBe('true');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('announces who is writing', () => {
|
|
61
|
+
expect(render([writer(alice)]).textContent).toContain('Alice is typing');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('announces a custom indicator, not just a generic one', () => {
|
|
65
|
+
const el = render([writer(alice, 'is running `ripgrep`')]);
|
|
66
|
+
expect(el.textContent).toContain('Alice is running `ripgrep`');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('says nothing when nobody is writing', () => {
|
|
70
|
+
// The container is always rendered to reserve space, and holds a
|
|
71
|
+
// non-breaking space as a placeholder. That placeholder must not be
|
|
72
|
+
// announced as if it were a message.
|
|
73
|
+
const text = (render([]).textContent ?? '').replace(/ /g, '').trim();
|
|
74
|
+
expect(text).toBe('');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -92,6 +92,18 @@ export function WritingIndicator(
|
|
|
92
92
|
return (
|
|
93
93
|
<Box
|
|
94
94
|
className={WRITERS_ELEMENT_CLASSNAME}
|
|
95
|
+
// The indicator already says something useful, "Alice is typing..." or
|
|
96
|
+
// "Jupyternaut is running `ripgrep`", but only on screen. Announcing it
|
|
97
|
+
// politely means a screen reader user learns a reply is coming without
|
|
98
|
+
// being interrupted mid-sentence.
|
|
99
|
+
//
|
|
100
|
+
// The region is the container rather than the text, so it is present in
|
|
101
|
+
// the accessibility tree before a writer appears and the change is
|
|
102
|
+
// announced. `aria-atomic` keeps the phrase together: without it a name
|
|
103
|
+
// change alone can be read out on its own, stripped of its context.
|
|
104
|
+
role="status"
|
|
105
|
+
aria-live="polite"
|
|
106
|
+
aria-atomic="true"
|
|
95
107
|
sx={{
|
|
96
108
|
...props.sx,
|
|
97
109
|
minHeight: '16px'
|