diff --git a/.changeset/dialog-accessibility.md b/.changeset/dialog-accessibility.md new file mode 100644 index 00000000..9ebe0f48 --- /dev/null +++ b/.changeset/dialog-accessibility.md @@ -0,0 +1,5 @@ +--- +'ondocs': patch +--- + +The dialog works better on a phone and with assistive technology. On a touch screen, which has no Escape key, a Close button takes the place of the esc hint, buttons are 44 px tall, and citations have a larger area to tap. When a thumb, a sent comment, Stop or Retry goes away as it is pressed, focus moves on to "Add a comment", "New question" or the input, rather than falling to the dialog. Each source's name now starts with its number and whether the answer cites it, and the cited sources have their number filled. An error is announced once, the input is named once and is expanded only while its list shows, and the questions in a thread are headings. Numbered steps with code or a blank line between them keep their numbers: 1, 2, 3 rather than 1, 1, 1. In forced colors (Windows High Contrast), the selected suggestion is outlined, the cited sources keep their filled number, and the navbar button keeps a border. New labels: `close`, `source`, `sourceCited` and `sourceNotCited`. diff --git a/examples/docusaurus/docs/ask-dialog.md b/examples/docusaurus/docs/ask-dialog.md index f3ea5ba5..bf523306 100644 --- a/examples/docusaurus/docs/ask-dialog.md +++ b/examples/docusaurus/docs/ask-dialog.md @@ -186,6 +186,7 @@ The endpoint's own words, its "I don't know" answer, its rate limit and budget m | `list` | Suggestions | | `stop` | Stop | | `escapeKey` | esc | +| `close` | Close | | `footer` | Answers come from this site and can be wrong. Check the sources. | | `footerNoSources` | Answers come from this site and can be wrong. | | `footerAsk` | ask | @@ -201,6 +202,9 @@ The endpoint's own words, its "I don't know" answer, its rate limit and budget m | `retry` | Retry | | `sources` | Sources | | `citation` | Source {n}: {title} | +| `source` | Source {n}: | +| `sourceCited` | Source {n}, cited: | +| `sourceNotCited` | Source {n}, not cited: | | `newQuestion` | New question | | `helpfulQuestion` | Was this helpful? | | `helpful` | Yes, it helped | @@ -233,11 +237,11 @@ Import `ondocs/react/styles.css` for the default theme. With `theme="auto"`, the ## Accessibility -Focus moves into the dialog when it opens and returns to where it was when it closes. The answer region is `aria-live="polite"` and `aria-busy` while text streams, and a separate status region announces when the answer is ready. All animation is turned off under `prefers-reduced-motion`. The test suite runs axe on the dialog before and after it answers, and checks that every text color in the light and dark themes keeps a contrast of at least 4.5:1. +Focus moves into the dialog when it opens and returns to where it was when it closes. A touch screen has no Escape key, so there a Close button takes the place of the esc hint in the input row. When a control goes away as it is pressed, focus moves on rather than falling to the dialog: from a thumb to "Add a comment", from a sent comment to "New question", and from Stop or Retry pressed with the keyboard to the input. The answer region is `aria-live="polite"` and `aria-busy` while text streams, and a separate status region announces when the answer is ready; an error is announced once, by its alert. Each source's name starts with its number, which its citations name, and whether the answer cites it (the `source`, `sourceCited` and `sourceNotCited` labels), and questions in a thread are headings. On a touch screen, buttons are 44 px tall and citations have a larger area to tap. In forced colors (Windows High Contrast), the selected suggestion is outlined and the cited sources keep their filled number. All animation is turned off under `prefers-reduced-motion`. The test suite runs axe on the dialog before and after it answers, and checks that every text color in the light and dark themes keeps a contrast of at least 4.5:1. ## While an answer streams -Sources arrive before the first word, and the answer renders as it streams, batched to one update per animation frame. Closing the dialog, by Escape, a click outside or its parent, stops the answer in flight, so the model is not left generating for nobody. When an answer stops at the model's output limit, the dialog says it may be incomplete. Once an answer is complete, the sources it does not cite are dimmed, and an answer that cites none shows no sources. +Sources arrive before the first word, and the answer renders as it streams, batched to one update per animation frame. Closing the dialog, by Escape, its Close button, a click outside or its parent, stops the answer in flight, so the model is not left generating for nobody. When an answer stops at the model's output limit, the dialog says it may be incomplete. Once an answer is complete, the sources it cites have their number filled and the others are dimmed, and an answer that cites none shows no sources. ## The useAsk hook diff --git a/examples/nextjs/content/docs/ask-dialog.md b/examples/nextjs/content/docs/ask-dialog.md index 106ebe9b..c2391e51 100644 --- a/examples/nextjs/content/docs/ask-dialog.md +++ b/examples/nextjs/content/docs/ask-dialog.md @@ -188,6 +188,7 @@ The endpoint's own words, its "I don't know" answer, its rate limit and budget m | `list` | Suggestions | | `stop` | Stop | | `escapeKey` | esc | +| `close` | Close | | `footer` | Answers come from this site and can be wrong. Check the sources. | | `footerNoSources` | Answers come from this site and can be wrong. | | `footerAsk` | ask | @@ -203,6 +204,9 @@ The endpoint's own words, its "I don't know" answer, its rate limit and budget m | `retry` | Retry | | `sources` | Sources | | `citation` | Source {n}: {title} | +| `source` | Source {n}: | +| `sourceCited` | Source {n}, cited: | +| `sourceNotCited` | Source {n}, not cited: | | `newQuestion` | New question | | `helpfulQuestion` | Was this helpful? | | `helpful` | Yes, it helped | @@ -235,11 +239,11 @@ Import `ondocs/react/styles.css` for the default theme. With `theme="auto"`, the ## Accessibility -Focus moves into the dialog when it opens and returns to where it was when it closes. The answer region is `aria-live="polite"` and `aria-busy` while text streams, and a separate status region announces when the answer is ready. All animation is turned off under `prefers-reduced-motion`. The test suite runs axe on the dialog before and after it answers, and checks that every text color in the light and dark themes keeps a contrast of at least 4.5:1. +Focus moves into the dialog when it opens and returns to where it was when it closes. A touch screen has no Escape key, so there a Close button takes the place of the esc hint in the input row. When a control goes away as it is pressed, focus moves on rather than falling to the dialog: from a thumb to "Add a comment", from a sent comment to "New question", and from Stop or Retry pressed with the keyboard to the input. The answer region is `aria-live="polite"` and `aria-busy` while text streams, and a separate status region announces when the answer is ready; an error is announced once, by its alert. Each source's name starts with its number, which its citations name, and whether the answer cites it (the `source`, `sourceCited` and `sourceNotCited` labels), and questions in a thread are headings. On a touch screen, buttons are 44 px tall and citations have a larger area to tap. In forced colors (Windows High Contrast), the selected suggestion is outlined and the cited sources keep their filled number. All animation is turned off under `prefers-reduced-motion`. The test suite runs axe on the dialog before and after it answers, and checks that every text color in the light and dark themes keeps a contrast of at least 4.5:1. ## While an answer streams -Sources arrive before the first word, and the answer renders as it streams, batched to one update per animation frame. Closing the dialog, by Escape, a click outside or its parent, stops the answer in flight, so the model is not left generating for nobody. When an answer stops at the model's output limit, the dialog says it may be incomplete. Once an answer is complete, the sources it does not cite are dimmed, and an answer that cites none shows no sources. +Sources arrive before the first word, and the answer renders as it streams, batched to one update per animation frame. Closing the dialog, by Escape, its Close button, a click outside or its parent, stops the answer in flight, so the model is not left generating for nobody. When an answer stops at the model's output limit, the dialog says it may be incomplete. Once an answer is complete, the sources it cites have their number filled and the others are dimmed, and an answer that cites none shows no sources. ## The useAsk hook diff --git a/examples/starlight/src/content/docs/reference/ask-dialog.md b/examples/starlight/src/content/docs/reference/ask-dialog.md index 13c0dbfa..e379305f 100644 --- a/examples/starlight/src/content/docs/reference/ask-dialog.md +++ b/examples/starlight/src/content/docs/reference/ask-dialog.md @@ -188,6 +188,7 @@ The endpoint's own words, its "I don't know" answer, its rate limit and budget m | `list` | Suggestions | | `stop` | Stop | | `escapeKey` | esc | +| `close` | Close | | `footer` | Answers come from this site and can be wrong. Check the sources. | | `footerNoSources` | Answers come from this site and can be wrong. | | `footerAsk` | ask | @@ -203,6 +204,9 @@ The endpoint's own words, its "I don't know" answer, its rate limit and budget m | `retry` | Retry | | `sources` | Sources | | `citation` | Source {n}: {title} | +| `source` | Source {n}: | +| `sourceCited` | Source {n}, cited: | +| `sourceNotCited` | Source {n}, not cited: | | `newQuestion` | New question | | `helpfulQuestion` | Was this helpful? | | `helpful` | Yes, it helped | @@ -235,11 +239,11 @@ Import `ondocs/react/styles.css` for the default theme. With `theme="auto"`, the ## Accessibility -Focus moves into the dialog when it opens and returns to where it was when it closes. The answer region is `aria-live="polite"` and `aria-busy` while text streams, and a separate status region announces when the answer is ready. All animation is turned off under `prefers-reduced-motion`. The test suite runs axe on the dialog before and after it answers, and checks that every text color in the light and dark themes keeps a contrast of at least 4.5:1. +Focus moves into the dialog when it opens and returns to where it was when it closes. A touch screen has no Escape key, so there a Close button takes the place of the esc hint in the input row. When a control goes away as it is pressed, focus moves on rather than falling to the dialog: from a thumb to "Add a comment", from a sent comment to "New question", and from Stop or Retry pressed with the keyboard to the input. The answer region is `aria-live="polite"` and `aria-busy` while text streams, and a separate status region announces when the answer is ready; an error is announced once, by its alert. Each source's name starts with its number, which its citations name, and whether the answer cites it (the `source`, `sourceCited` and `sourceNotCited` labels), and questions in a thread are headings. On a touch screen, buttons are 44 px tall and citations have a larger area to tap. In forced colors (Windows High Contrast), the selected suggestion is outlined and the cited sources keep their filled number. All animation is turned off under `prefers-reduced-motion`. The test suite runs axe on the dialog before and after it answers, and checks that every text color in the light and dark themes keeps a contrast of at least 4.5:1. ## While an answer streams -Sources arrive before the first word, and the answer renders as it streams, batched to one update per animation frame. Closing the dialog, by Escape, a click outside or its parent, stops the answer in flight, so the model is not left generating for nobody. When an answer stops at the model's output limit, the dialog says it may be incomplete. Once an answer is complete, the sources it does not cite are dimmed, and an answer that cites none shows no sources. +Sources arrive before the first word, and the answer renders as it streams, batched to one update per animation frame. Closing the dialog, by Escape, its Close button, a click outside or its parent, stops the answer in flight, so the model is not left generating for nobody. When an answer stops at the model's output limit, the dialog says it may be incomplete. Once an answer is complete, the sources it cites have their number filled and the others are dimmed, and an answer that cites none shows no sources. ## The useAsk hook diff --git a/examples/vitepress/docs/ask-dialog.md b/examples/vitepress/docs/ask-dialog.md index 1dd446fe..644eb2d0 100644 --- a/examples/vitepress/docs/ask-dialog.md +++ b/examples/vitepress/docs/ask-dialog.md @@ -188,6 +188,7 @@ The endpoint's own words, its "I don't know" answer, its rate limit and budget m | `list` | Suggestions | | `stop` | Stop | | `escapeKey` | esc | +| `close` | Close | | `footer` | Answers come from this site and can be wrong. Check the sources. | | `footerNoSources` | Answers come from this site and can be wrong. | | `footerAsk` | ask | @@ -203,6 +204,9 @@ The endpoint's own words, its "I don't know" answer, its rate limit and budget m | `retry` | Retry | | `sources` | Sources | | `citation` | Source {n}: {title} | +| `source` | Source {n}: | +| `sourceCited` | Source {n}, cited: | +| `sourceNotCited` | Source {n}, not cited: | | `newQuestion` | New question | | `helpfulQuestion` | Was this helpful? | | `helpful` | Yes, it helped | @@ -235,11 +239,11 @@ Import `ondocs/react/styles.css` for the default theme. With `theme="auto"`, the ## Accessibility -Focus moves into the dialog when it opens and returns to where it was when it closes. The answer region is `aria-live="polite"` and `aria-busy` while text streams, and a separate status region announces when the answer is ready. All animation is turned off under `prefers-reduced-motion`. The test suite runs axe on the dialog before and after it answers, and checks that every text color in the light and dark themes keeps a contrast of at least 4.5:1. +Focus moves into the dialog when it opens and returns to where it was when it closes. A touch screen has no Escape key, so there a Close button takes the place of the esc hint in the input row. When a control goes away as it is pressed, focus moves on rather than falling to the dialog: from a thumb to "Add a comment", from a sent comment to "New question", and from Stop or Retry pressed with the keyboard to the input. The answer region is `aria-live="polite"` and `aria-busy` while text streams, and a separate status region announces when the answer is ready; an error is announced once, by its alert. Each source's name starts with its number, which its citations name, and whether the answer cites it (the `source`, `sourceCited` and `sourceNotCited` labels), and questions in a thread are headings. On a touch screen, buttons are 44 px tall and citations have a larger area to tap. In forced colors (Windows High Contrast), the selected suggestion is outlined and the cited sources keep their filled number. All animation is turned off under `prefers-reduced-motion`. The test suite runs axe on the dialog before and after it answers, and checks that every text color in the light and dark themes keeps a contrast of at least 4.5:1. ## While an answer streams -Sources arrive before the first word, and the answer renders as it streams, batched to one update per animation frame. Closing the dialog, by Escape, a click outside or its parent, stops the answer in flight, so the model is not left generating for nobody. When an answer stops at the model's output limit, the dialog says it may be incomplete. Once an answer is complete, the sources it does not cite are dimmed, and an answer that cites none shows no sources. +Sources arrive before the first word, and the answer renders as it streams, batched to one update per animation frame. Closing the dialog, by Escape, its Close button, a click outside or its parent, stops the answer in flight, so the model is not left generating for nobody. When an answer stops at the model's output limit, the dialog says it may be incomplete. Once an answer is complete, the sources it cites have their number filled and the others are dimmed, and an answer that cites none shows no sources. ## The useAsk hook diff --git a/scripts/js-weight.mjs b/scripts/js-weight.mjs index 431252eb..74a638d4 100644 --- a/scripts/js-weight.mjs +++ b/scripts/js-weight.mjs @@ -44,7 +44,7 @@ const SITES = { // site loads its shared code in one chunk, not two, and 173.8 KB, the benchmarks page's // figure. The budget leaves about 4 KB of room, as Starlight's does. budget: 182, - // The dialog, Radix and cmdk, on the site's React: 27.2 KB. + // The dialog, Radix and cmdk, on the site's React: 27.7 KB. open: 31, }, starlight: { @@ -54,7 +54,7 @@ const SITES = { // Starlight's own 33.7 KB, with the demo's banner and question box, and ondocs's 2.4 KB: // 36.1 KB. budget: 40, - // ondocs/embed/standalone's dialog, with its own React: 92.6 KB. + // ondocs/embed/standalone's dialog, with its own React: 93.1 KB. open: 97, }, vitepress: { @@ -64,7 +64,7 @@ const SITES = { // VitePress's own 58.7 KB, with the demo's notice and question box, and ondocs's 2.9 KB: // 61.6 KB. budget: 64, - // ondocs/embed/standalone's dialog, with its own React: 95.6 KB. + // ondocs/embed/standalone's dialog, with its own React: 96.1 KB. open: 100, }, next: { @@ -75,9 +75,11 @@ const SITES = { // before the shared header's theme switch and phone menu, 171.4 KB with them, 178 KB with the // docs shell (On this page, the docs bar, page actions, tabs), and 174.5 KB once ondocs's // package.json, which client code reached through the site's constants, left the bundle. + // 180.5 KB with the dialog's styles for touch screens, forced colors and cited sources: the + // site inlines ondocs/react/styles.css in its page payload, so every rule counts here. launcher: false, - budget: 180, - // The dialog, Radix and cmdk, on the site's React: 27.3 KB. + budget: 182, + // The dialog, Radix and cmdk, on the site's React: 27.8 KB. open: 31, }, embed: { @@ -86,7 +88,7 @@ const SITES = { key: 'i', // dist/embed.global.js alone: the button, the shortcut and their styles. budget: 4, - // dist/embed-dialog.global.js: the dialog, React and the dialog's styles, 97.8 KB. + // dist/embed-dialog.global.js: the dialog, React and the dialog's styles, 98.5 KB. open: 102, }, }; diff --git a/src/docusaurus/launcher.css b/src/docusaurus/launcher.css index 0e6e1a18..e1f8e019 100644 --- a/src/docusaurus/launcher.css +++ b/src/docusaurus/launcher.css @@ -109,7 +109,11 @@ -webkit-mask: var(--ask-button-icon) center / contain no-repeat; mask: var(--ask-button-icon) center / contain no-repeat; } +/* Forced colors drop the fill: a border keeps it a button rather than bare text. */ @media (forced-colors: active) { + .ondocs-button { + border: 1px solid ButtonText; + } .ondocs-button::before { background: ButtonText; forced-color-adjust: none; diff --git a/src/embed/button.css b/src/embed/button.css index 41c2dc22..3a99fadf 100644 --- a/src/embed/button.css +++ b/src/embed/button.css @@ -70,7 +70,11 @@ -webkit-mask: var(--ask-button-icon) center / contain no-repeat; mask: var(--ask-button-icon) center / contain no-repeat; } +/* Forced colors drop the fill: a border keeps it a button rather than bare text. */ @media (forced-colors: active) { + .ondocs-button { + border: 1px solid ButtonText; + } .ondocs-button::before { background: ButtonText; forced-color-adjust: none; diff --git a/src/react/answer.tsx b/src/react/answer.tsx index ff10d2a7..1f5bb883 100644 --- a/src/react/answer.tsx +++ b/src/react/answer.tsx @@ -188,14 +188,16 @@ export function AskAnswer({ } const BULLET = /^[-*+]\s+/; -const NUMBERED = /^\d+[.)]\s+/; +const NUMBERED = /^(\d+)[.)]\s+/; type LineKind = 'ul' | 'ol' | 'p'; const kindOf = (line: string): LineKind => BULLET.test(line) ? 'ul' : NUMBERED.test(line) ? 'ol' : 'p'; /** * Paragraphs and lists. Within a paragraph block, consecutive lines of one kind are grouped, so - * "To install:" directly followed by "1. …" lines renders as a paragraph and then a list. + * "To install:" directly followed by "1. …" lines renders as a paragraph and then a list. A + * numbered list starts at its first item's number: steps with code or a blank line between them + * come as lists of their own, and read 1, 2, 3 rather than 1, 1, 1. */ function prose( text: string, @@ -228,7 +230,16 @@ function prose( } const marker = run.kind === 'ul' ? BULLET : NUMBERED; const items = run.lines.map((line, i) =>