diff --git a/content/copilot/how-tos/copilot-cli/use-copilot-cli/browse-issues-prs-gists.md b/content/copilot/how-tos/copilot-cli/use-copilot-cli/browse-issues-prs-gists.md index c8b62b2d9bd8..954fe7ec867e 100644 --- a/content/copilot/how-tos/copilot-cli/use-copilot-cli/browse-issues-prs-gists.md +++ b/content/copilot/how-tos/copilot-cli/use-copilot-cli/browse-issues-prs-gists.md @@ -13,10 +13,7 @@ docsTeamMetrics: - copilot-cli --- -> [!NOTE] -> The new tabbed interface is currently in {% data variables.release-phases.public_preview %} and is subject to change. - -An interactive {% data variables.copilot.copilot_cli_short %} session has four tabs at the top of the screen: +By default, interactive {% data variables.copilot.copilot_cli_short %} sessions for Git repositories have four tabs at the top of the screen: * **Session**: The regular chat experience where you enter prompts for {% data variables.product.prodname_copilot_short %}. * **Issues**: Open issues in the current repository on {% data variables.product.prodname_dotcom %}. * **Pull requests**: Open pull requests in the current repository on {% data variables.product.prodname_dotcom %}. @@ -28,15 +25,19 @@ The **Issues**, **Pull requests**, and **Gists** tabs let you browse content fro * **Pull an item into your chat** — quickly insert a reference to the selected item into the prompt box so that you can ask {% data variables.product.prodname_copilot_short %} to investigate, fix, comment on, or review it. * **Jump to an item on {% data variables.product.prodname_dotcom_the_website %}** — for example when you want to comment on an issue, merge a pull request, or edit a gist. +> [!NOTE] +> The **Issues** and **Pull requests** tabs are only shown when {% data variables.copilot.copilot_cli_short %} is running inside a {% data variables.product.prodname_dotcom %} repository. In other directories, only the **Session** and **Gists** tabs are shown. + ## Switching between tabs * Press Tab to move to the next tab. * Press Shift+Tab to move to the previous tab. - -Tab switching is paused while another part of the CLI—such as the slash command picker—is observing your keystrokes. +* Use the mouse to click on a tab to switch to it. > [!NOTE] -> The **Issues** and **Pull requests** tabs are only shown when {% data variables.copilot.copilot_cli_short %} is running inside a {% data variables.product.prodname_dotcom %} repository. In other directories, only the **Session** and **Gists** tabs are shown. +> Clicking tabs requires mouse support. This is enabled by default but can be disabled with the `--no-mouse` command-line option. Use the `--mouse=on` option to re-enable mouse support if it has been disabled. + +Tab switching is paused while another part of the CLI—such as the slash command picker—is observing your keystrokes. ## Common keyboard controls @@ -47,6 +48,7 @@ The **Issues**, **Pull requests**, and **Gists** tabs all use the same controls. * Press Enter to display a detailed view of the highlighted item. Press Esc in the details view to return to the list. * Press o to open the highlighted item (or, in the detailed view, the current item) on {% data variables.product.prodname_dotcom_the_website %}. * Press c to insert a reference to the item into the prompt input area and jump back to the **Session** tab. +* Press / (on the **Issues** and **Pull requests** tabs) to search {% data variables.product.prodname_dotcom %} with a custom query. Type a query, press Enter to run it, and Esc to cancel or clear it. For the full set of keypresses you can use, see [Keyboard reference](#keyboard-reference) at the end of this article. @@ -74,6 +76,16 @@ Pressing c inserts a reference to the pull request into the prompt bo #5678 check this out and run tests ``` +## Searching issues and pull requests + +By default, the **Issues** and **Pull requests** tabs show items that involve you. Press a to toggle between this (`involves:@me`) and all open items. + +To run your own search, press /. An inline search box opens where you can type a {% data variables.product.prodname_dotcom %} search query, then press Enter to run it. + +Press Esc to cancel while typing, or to clear an applied search and return to the default list. + +You can use the same set of search qualifiers that are available on {% data variables.product.prodname_dotcom_the_website %}. See [AUTOTITLE](/search-github/searching-on-github/searching-issues-and-pull-requests). + ## Browsing your gists The **Gists** tab lists the gists owned by the {% data variables.product.prodname_dotcom %} account you are signed in to. Both public and secret gists are shown. Unlike the **Issues** and **Pull requests** tabs, the **Gists** tab is not scoped to a repository—it is always available, regardless of where you started the CLI. @@ -103,6 +115,24 @@ The **Issues**, **Pull requests**, and **Gists** tabs are read-only environments https://gist.github.com/USERNAME/GIST-ID delete this ``` +## Customizing the tabs + +You can reorder, hide, or turn off the tabs in your settings file (`~/.copilot/settings.json`) using the `tabs` object: + +```json copy +{ + "tabs": { + "enabled": true, + "sort": ["copilot", "pull-requests", "issues", "gists"], + "hide": ["gists"] + } +} +``` + +* `enabled`: set to `false` to turn off the tabbed interface entirely. +* `sort`: the order in which tabs appear. Use the identifiers `copilot` (the **Session** tab), `issues`, `pull-requests`, and `gists`. Any tabs you omit keep their default order after the ones you list. Unknown identifiers are ignored. +* `hide`: tabs to hide, using the same identifiers. The **Session** tab (`copilot`) cannot be hidden. + ## Keyboard reference The footer hint bar in the **Issues**, **Pull requests**, and **Gists** tabs summarizes the available keys: @@ -116,4 +146,7 @@ The footer hint bar in the **Issues**, **Pull requests**, and **Gists** tabs sum | o | List view or details view | Open the highlighted item on {% data variables.product.prodname_dotcom_the_website %} in your browser. | | c | List view or details view | Insert a reference to the item into the prompt input area and jump back to the **Session** tab. | | a | List view on **Issues** and **Pull requests** tabs | Toggle between showing only items that involve you and showing every open item in the repository. | +| / | List view on **Issues** and **Pull requests** tabs | Open a search box. | +| Enter | Search box | Run the search query. | +| Esc | Search box / applied search | Cancel the search box, or dismiss the search results. | | Esc | Details view | Return to the list view. | diff --git a/data/reusables/copilot/plans/copilot-max-upgrade-only.md b/data/reusables/copilot/plans/copilot-max-upgrade-only.md deleted file mode 100644 index ccbb6ebb7c38..000000000000 --- a/data/reusables/copilot/plans/copilot-max-upgrade-only.md +++ /dev/null @@ -1 +0,0 @@ -**Beginning June 1, 2026**, the new {% data variables.copilot.copilot_max_short %} plan is only available for upgrade to users with existing {% data variables.product.prodname_copilot_short %} plans. diff --git a/data/reusables/projects/sunset_notice_content.md b/data/reusables/projects/sunset_notice_content.md deleted file mode 100644 index 3c0863e80855..000000000000 --- a/data/reusables/projects/sunset_notice_content.md +++ /dev/null @@ -1,3 +0,0 @@ ->{% data variables.product.prodname_projects_v1_caps %} has been retired. You can read more about this change on [{% data variables.product.prodname_blog %}](https://gh.io/projects-classic-sunset-notice). -> ->The new and improved Projects experience is available. For more information, see [AUTOTITLE](/issues/planning-and-tracking-with-projects/learning-about-projects/about-projects). diff --git a/data/tables/copilot/model-multipliers.yml b/data/tables/copilot/model-multipliers.yml deleted file mode 100644 index 363c4adcadea..000000000000 --- a/data/tables/copilot/model-multipliers.yml +++ /dev/null @@ -1,94 +0,0 @@ -# Please keep this list sorted in alphabetical order. -# -# This file defines the model multipliers for paid and free plans. -# It is used to generate the "Model multipliers" table at -# /content/copilot/reference/ai-models/supported-models#moodel-multiplier. -# -# Column keys: -# - name: The model name. -# - multiplier_paid: The multiplier for paid plans. -# - multiplier_free: The multiplier for free plans. - -- name: Claude Haiku 4.5 - multiplier_paid: 0.33 - multiplier_free: 1 - -- name: Claude Opus 4.5 - multiplier_paid: 3 - multiplier_free: Not applicable - -- name: Claude Opus 4.6 - multiplier_paid: 3 - multiplier_free: Not applicable - -- name: Claude Opus 4.6 (fast mode) (preview) - multiplier_paid: 30 - multiplier_free: Not applicable - -- name: Claude Opus 4.7 - multiplier_paid: 15 - multiplier_free: Not applicable - -- name: Claude Opus 4.8 - multiplier_paid: 15 - multiplier_free: Not applicable - -- name: Claude Sonnet 4.5 - multiplier_paid: 1 - multiplier_free: Not applicable - -- name: Claude Sonnet 4.6 - multiplier_paid: 1 - multiplier_free: Not applicable - -- name: Gemini 2.5 Pro - multiplier_paid: 1 - multiplier_free: Not applicable - -- name: Gemini 3 Flash - multiplier_paid: 0.33 - multiplier_free: Not applicable - -- name: Gemini 3.1 Pro - multiplier_paid: 1 - multiplier_free: Not applicable - -- name: Gemini 3.5 Flash - multiplier_paid: 14 - multiplier_free: Not applicable - -- name: GPT-4o - multiplier_paid: 0 - multiplier_free: 1 - -- name: GPT-5 mini - multiplier_paid: 0 - multiplier_free: 1 - -- name: GPT-5.3-Codex - multiplier_paid: 1.0 - multiplier_free: Not applicable - -- name: GPT-5.4 - multiplier_paid: 1.0 - multiplier_free: Not applicable - -- name: GPT-5.4 mini - multiplier_paid: 0.33 - multiplier_free: Not applicable - -- name: GPT-5.4 nano - multiplier_paid: 0.25 - multiplier_free: Not applicable - -- name: GPT-5.5 - multiplier_paid: 7.5 - multiplier_free: Not applicable - -- name: MAI-Code-1-Flash - multiplier_paid: 0.33 - multiplier_free: Not applicable - -- name: Raptor mini - multiplier_paid: 0 - multiplier_free: 1 diff --git a/data/ui.yml b/data/ui.yml index dfe8b85964f0..4eba450206e8 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -293,6 +293,7 @@ secret_scanning: webhooks: action_type_switch_error: There was an error switching webhook action types. action_type: Action type + action_type_selected: "'{{ actionType }}' action selected. {{ description }}" availability: Availability for {{ WebhookName }} webhook_payload_object: Webhook payload object for {{ WebhookName }} webhook_payload_example: Webhook payload example diff --git a/package-lock.json b/package-lock.json index 867414e0f367..9ee4046dadc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,7 @@ "is-svg": "6.0.0", "javascript-stringify": "^2.1.0", "js-cookie": "^3.0.7", - "js-yaml": "^4.1.1", + "js-yaml": "^4.2.0", "liquidjs": "^10.27.0", "lodash": "^4.18.0", "lodash-es": "^4.18.0", @@ -6018,16 +6018,16 @@ } }, "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", - "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.6.tgz", + "integrity": "sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", + "hasown": "^2.0.4", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, @@ -10083,16 +10083,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -10461,9 +10461,10 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -11798,9 +11799,19 @@ } }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" diff --git a/package.json b/package.json index 912507ffee32..c415b9d1ff59 100644 --- a/package.json +++ b/package.json @@ -225,7 +225,7 @@ "is-svg": "6.0.0", "javascript-stringify": "^2.1.0", "js-cookie": "^3.0.7", - "js-yaml": "^4.1.1", + "js-yaml": "^4.2.0", "liquidjs": "^10.27.0", "lodash": "^4.18.0", "lodash-es": "^4.18.0", diff --git a/src/data-directory/lib/data-schemas/tables/copilot/model-multipliers.ts b/src/data-directory/lib/data-schemas/tables/copilot/model-multipliers.ts deleted file mode 100644 index bd89d584236e..000000000000 --- a/src/data-directory/lib/data-schemas/tables/copilot/model-multipliers.ts +++ /dev/null @@ -1,33 +0,0 @@ -// This schema enforces the structure in model-multipliers.yml - -const modelMultipliersSchema = { - type: 'object', - additionalProperties: false, - required: ['models'], - properties: { - models: { - type: 'object', - items: { - type: 'object', - additionalProperties: false, - required: ['name', 'multiplier_paid', 'multiplier_free'], - properties: { - name: { - type: 'string', - lintable: true, - }, - multiplier_paid: { - type: 'string', - lintable: true, - }, - multiplier_free: { - type: 'string', - lintable: true, - }, - }, - }, - }, - }, -} - -export default modelMultipliersSchema diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml index dfe8b85964f0..4eba450206e8 100644 --- a/src/fixtures/fixtures/data/ui.yml +++ b/src/fixtures/fixtures/data/ui.yml @@ -293,6 +293,7 @@ secret_scanning: webhooks: action_type_switch_error: There was an error switching webhook action types. action_type: Action type + action_type_selected: "'{{ actionType }}' action selected. {{ description }}" availability: Availability for {{ WebhookName }} webhook_payload_object: Webhook payload object for {{ WebhookName }} webhook_payload_example: Webhook payload example diff --git a/src/graphql/data/fpt/changelog.json b/src/graphql/data/fpt/changelog.json index 3c9fb775d66e..3054300cda4c 100644 --- a/src/graphql/data/fpt/changelog.json +++ b/src/graphql/data/fpt/changelog.json @@ -1,4 +1,22 @@ [ + { + "schemaChanges": [ + { + "title": "The GraphQL schema includes these changes:", + "changes": [] + } + ], + "previewChanges": [], + "upcomingChanges": [ + { + "title": "The following changes will be made to the schema:", + "changes": [ + "

On member CopilotAgentTask.type:type will be removed. Use codingAgentFilter and codingAgentTypeFilter instead. Effective 2026-10-01.

" + ] + } + ], + "date": "2026-06-23" + }, { "schemaChanges": [ { diff --git a/src/graphql/data/fpt/graphql_upcoming_changes.public.yml b/src/graphql/data/fpt/graphql_upcoming_changes.public.yml index 32cdfd9aba68..f6347e0e72b7 100644 --- a/src/graphql/data/fpt/graphql_upcoming_changes.public.yml +++ b/src/graphql/data/fpt/graphql_upcoming_changes.public.yml @@ -7998,6 +7998,14 @@ upcoming_changes: date: '2026-07-01T00:00:00+00:00' criticality: breaking owner: peter-evans + - location: CopilotAgentTask.type + description: + '`type` will be removed. Use `codingAgentFilter` and `codingAgentTypeFilter` + instead.' + reason: '`type` will be removed.' + date: '2026-10-01T00:00:00+00:00' + criticality: breaking + owner: github/copilot-mission-control - location: User.viewerRelevantRepositories description: '`viewerRelevantRepositories` will be removed. Use viewerCopilotChatRepositorySuggestions diff --git a/src/graphql/data/fpt/upcoming-changes.json b/src/graphql/data/fpt/upcoming-changes.json index 4404479ddde5..8968e57a98ff 100644 --- a/src/graphql/data/fpt/upcoming-changes.json +++ b/src/graphql/data/fpt/upcoming-changes.json @@ -25,6 +25,14 @@ "date": "2026-10-01", "criticality": "breaking", "owner": "github/client-apps-platform" + }, + { + "location": "CopilotAgentTask.type", + "description": "

type will be removed. Use codingAgentFilter and codingAgentTypeFilter instead.

", + "reason": "

type will be removed.

", + "date": "2026-10-01", + "criticality": "breaking", + "owner": "github/copilot-mission-control" } ], "2026-07-01": [ diff --git a/src/graphql/data/ghec/graphql_upcoming_changes.public.yml b/src/graphql/data/ghec/graphql_upcoming_changes.public.yml index 32cdfd9aba68..f6347e0e72b7 100644 --- a/src/graphql/data/ghec/graphql_upcoming_changes.public.yml +++ b/src/graphql/data/ghec/graphql_upcoming_changes.public.yml @@ -7998,6 +7998,14 @@ upcoming_changes: date: '2026-07-01T00:00:00+00:00' criticality: breaking owner: peter-evans + - location: CopilotAgentTask.type + description: + '`type` will be removed. Use `codingAgentFilter` and `codingAgentTypeFilter` + instead.' + reason: '`type` will be removed.' + date: '2026-10-01T00:00:00+00:00' + criticality: breaking + owner: github/copilot-mission-control - location: User.viewerRelevantRepositories description: '`viewerRelevantRepositories` will be removed. Use viewerCopilotChatRepositorySuggestions diff --git a/src/graphql/data/ghec/upcoming-changes.json b/src/graphql/data/ghec/upcoming-changes.json index 4404479ddde5..8968e57a98ff 100644 --- a/src/graphql/data/ghec/upcoming-changes.json +++ b/src/graphql/data/ghec/upcoming-changes.json @@ -25,6 +25,14 @@ "date": "2026-10-01", "criticality": "breaking", "owner": "github/client-apps-platform" + }, + { + "location": "CopilotAgentTask.type", + "description": "

type will be removed. Use codingAgentFilter and codingAgentTypeFilter instead.

", + "reason": "

type will be removed.

", + "date": "2026-10-01", + "criticality": "breaking", + "owner": "github/copilot-mission-control" } ], "2026-07-01": [ diff --git a/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml b/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml index d5537cf9feb8..75f7acf17aa8 100644 --- a/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml +++ b/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml @@ -296,7 +296,7 @@ isPublic: false isPrivateWithGhas: true hasPushProtection: false - hasValidityCheck: false + hasValidityCheck: true hasExtendedMetadata: false base64Supported: false isduplicate: false @@ -306,7 +306,7 @@ isPublic: false isPrivateWithGhas: true hasPushProtection: true - hasValidityCheck: false + hasValidityCheck: true hasExtendedMetadata: false base64Supported: false isduplicate: true @@ -2760,7 +2760,7 @@ isPublic: true isPrivateWithGhas: false hasPushProtection: false - hasValidityCheck: false + hasValidityCheck: true hasExtendedMetadata: false base64Supported: false isduplicate: false @@ -3200,7 +3200,7 @@ isPublic: true isPrivateWithGhas: true hasPushProtection: true - hasValidityCheck: false + hasValidityCheck: true hasExtendedMetadata: false base64Supported: false isduplicate: false diff --git a/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml b/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml index d5537cf9feb8..75f7acf17aa8 100644 --- a/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml +++ b/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml @@ -296,7 +296,7 @@ isPublic: false isPrivateWithGhas: true hasPushProtection: false - hasValidityCheck: false + hasValidityCheck: true hasExtendedMetadata: false base64Supported: false isduplicate: false @@ -306,7 +306,7 @@ isPublic: false isPrivateWithGhas: true hasPushProtection: true - hasValidityCheck: false + hasValidityCheck: true hasExtendedMetadata: false base64Supported: false isduplicate: true @@ -2760,7 +2760,7 @@ isPublic: true isPrivateWithGhas: false hasPushProtection: false - hasValidityCheck: false + hasValidityCheck: true hasExtendedMetadata: false base64Supported: false isduplicate: false @@ -3200,7 +3200,7 @@ isPublic: true isPrivateWithGhas: true hasPushProtection: true - hasValidityCheck: false + hasValidityCheck: true hasExtendedMetadata: false base64Supported: false isduplicate: false diff --git a/src/webhooks/components/Webhook.tsx b/src/webhooks/components/Webhook.tsx index cf5b8965ac13..66d5863e57dc 100644 --- a/src/webhooks/components/Webhook.tsx +++ b/src/webhooks/components/Webhook.tsx @@ -1,7 +1,6 @@ import { ActionList, ActionMenu, Flash } from '@primer/react' -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback } from 'react' import useSWR from 'swr' -import { useRouter } from 'next/router' import { slug } from 'github-slugger' import cx from 'classnames' import { announce } from '@primer/live-region-element' @@ -33,7 +32,6 @@ export function Webhook({ webhook }: Props) { // Get version for requests to switch webhook action type const version = useVersion() const { t, tObject } = useTranslation('webhooks') - const router = useRouter() // Get more user friendly language for the different availability options in // the webhook schema (we can't change it directly in the schema). Note that @@ -48,6 +46,9 @@ export function Webhook({ webhook }: Props) { // The index of the selected action type so we can highlight which one is selected // in the action type dropdown const [selectedActionTypeIndex, setSelectedActionTypeIndex] = useState(0) + // Tracks whether we need to announce once data loads (first interaction only, + // before SWR cache is populated). + const [pendingAnnouncement, setPendingAnnouncement] = useState('') const webhookSlug = slug(webhook.data.category) const webhookFetchUrl = `/api/webhooks/v1?${new URLSearchParams({ @@ -55,6 +56,18 @@ export function Webhook({ webhook }: Props) { version: version.currentVersion, })}` + // fires when the webhook action type changes or someone clicks on a nested + // body param for the first time. In either case, we now have all the data + // for a webhook (i.e. all the data for each action type and all of their + // nested parameters) + const { data, error } = useSWR( + clickedBodyParameterName || selectedWebhookActionType ? webhookFetchUrl : null, + webhookFetcher, + { + revalidateOnFocus: false, + }, + ) + // When you load the page we want to support linking to a specific webhook type // so this effect sets the webhook type if it's provided in the URL e.g.: // @@ -72,6 +85,20 @@ export function Webhook({ webhook }: Props) { } }, []) + // Build a plain-text announcement from the webhook action data. + const buildAnnouncement = useCallback( + (type: string, actionData: { descriptionHtml: string }) => { + const tempEl = document.createElement('div') + tempEl.innerHTML = actionData.descriptionHtml + const description = tempEl.textContent?.trim() || '' + return t('action_type_selected') + .replace('{{ actionType }}', type) + .replace('{{ description }}', description) + .trim() + }, + [t], + ) + // callback for the action type dropdown -- sets the action type to the given // type, index is the index of the selected type so we can highlight it as // selected. @@ -86,26 +113,26 @@ export function Webhook({ webhook }: Props) { setSelectedWebhookActionType(type) setSelectedActionTypeIndex(index) - // Announce the newly selected action type to screen readers so users - // relying on AT know the page content has changed. - announce(`${t('action_type')}: ${type}`, { politeness: 'polite' }) - - const { asPath, locale } = router - let [pathRoot, pathQuery = ''] = asPath.split('?') - const params = new URLSearchParams(pathQuery) - - if (pathRoot.includes('#')) { - pathRoot = pathRoot.split('#')[0] + // If SWR data is already cached, announce immediately. Otherwise, flag + // the type so the effect can announce once data arrives. + if (data && data[type]) { + // Use setTimeout so the announcement fires after the ActionMenu closes + // and VoiceOver finishes reading the button. Compute message eagerly to + // avoid stale closures if data changes before the timeout fires. + const message = buildAnnouncement(type, data[type]) + setTimeout(() => { + announce(message, { politeness: 'assertive' }) + }, 150) + } else { + setPendingAnnouncement(type) } - params.set('actionType', type) - router.push( - { pathname: `/${locale}${pathRoot}`, query: params.toString(), hash: webhookSlug }, - undefined, - { - shallow: true, - }, - ) + // Update the URL without triggering Next.js router navigation, which causes + // VoiceOver to re-read the page title and swallow live-region announcements. + const url = new URL(location.href) + url.searchParams.set('actionType', type) + url.hash = webhookSlug + window.history.replaceState(window.history.state, '', url.toString()) } // callback to trigger useSWR() hook after a nested property is clicked @@ -113,21 +140,22 @@ export function Webhook({ webhook }: Props) { setClickedBodyParameterName(target.closest('details')?.dataset.nestedParamId) } - // fires when the webhook action type changes or someone clicks on a nested - // body param for the first time. In either case, we now have all the data - // for a webhook (i.e. all the data for each action type and all of their - // nested parameters) - const { data, error } = useSWR( - clickedBodyParameterName || selectedWebhookActionType ? webhookFetchUrl : null, - webhookFetcher, - { - revalidateOnFocus: false, - }, - ) - const currentWebhookActionType = selectedWebhookActionType || webhook.data.action const currentWebhookAction = (data && data[currentWebhookActionType]) || webhook.data + // Announce content changes when data arrives for the first time (before SWR + // cache is populated). Subsequent changes are announced directly in the handler. + useEffect(() => { + if (!pendingAnnouncement || !data || !data[pendingAnnouncement]) return + const type = pendingAnnouncement + setPendingAnnouncement('') + + const message = buildAnnouncement(type, data[type]) + setTimeout(() => { + announce(message, { politeness: 'assertive' }) + }, 150) + }, [data, pendingAnnouncement, buildAnnouncement]) + return (