@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, sx: {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter/chat",
3
- "version": "0.25.0-alpha.0",
3
+ "version": "0.25.0-alpha.2",
4
4
  "description": "A package that provides UI components that can be used to create a chat in a Jupyterlab extension.",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -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'