diff --git a/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts index ce9fb68544361..bf44a0cec4296 100644 --- a/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts +++ b/src/plugins/embeddable/public/lib/embeddables/compatibility/legacy_embeddable_to_api.ts @@ -144,8 +144,8 @@ export const legacyEmbeddableToApi = ( (embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown'; const isImage = embeddable.type === 'image'; - const isNavigation = embeddable.type === 'navigation'; - return !isInputControl && !isMarkdown && !isImage && !isNavigation; + const isLinks = embeddable.type === 'links'; + return !isInputControl && !isMarkdown && !isImage && !isLinks; }; return { diff --git a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx index a6ef1c7c4b72d..b6bbe8fa5f68b 100644 --- a/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx +++ b/src/plugins/links/public/components/dashboard_link/dashboard_link_component.tsx @@ -189,6 +189,7 @@ export const DashboardLinkComponent = ({ label={linkLabel} external={link.options?.openInNewTab} data-test-subj={error ? `${id}--error` : `${id}`} + aria-current={link.destination === parentDashboardId} /> ); }; diff --git a/src/plugins/links/public/components/editor/links_editor.test.tsx b/src/plugins/links/public/components/editor/links_editor.test.tsx index c9c1c0eff183d..b7c85099cf516 100644 --- a/src/plugins/links/public/components/editor/links_editor.test.tsx +++ b/src/plugins/links/public/components/editor/links_editor.test.tsx @@ -39,6 +39,7 @@ describe('LinksEditor', () => { onAddToDashboard: jest.fn(), onClose: jest.fn(), isByReference: false, + flyoutId: 'test-id', }; const someLinks: Link[] = [ diff --git a/src/plugins/links/public/components/editor/links_editor.tsx b/src/plugins/links/public/components/editor/links_editor.tsx index 12e48e5aa463f..0b9b21d7b6147 100644 --- a/src/plugins/links/public/components/editor/links_editor.tsx +++ b/src/plugins/links/public/components/editor/links_editor.tsx @@ -38,7 +38,7 @@ import { LINKS_HORIZONTAL_LAYOUT, LINKS_VERTICAL_LAYOUT, } from '../../../common/content_management'; -import { memoizedGetOrderedLinkList } from '../../editor/links_editor_tools'; +import { focusMainFlyout, memoizedGetOrderedLinkList } from '../../editor/links_editor_tools'; import { openLinkEditorFlyout } from '../../editor/open_link_editor_flyout'; import { LinksLayoutInfo } from '../../embeddable/types'; import { coreServices } from '../../services/kibana_services'; @@ -71,6 +71,7 @@ const LinksEditor = ({ initialLayout, parentDashboard, isByReference, + flyoutId, }: { onSaveToLibrary: (newLinks: Link[], newLayout: LinksLayoutType) => Promise; onAddToDashboard: (newLinks: Link[], newLayout: LinksLayoutType) => void; @@ -79,6 +80,7 @@ const LinksEditor = ({ initialLayout?: LinksLayoutType; parentDashboard?: DashboardContainer; isByReference: boolean; + flyoutId: string; // used to manage the focus of this flyout after individual link editor flyout is closed }) => { const toasts = coreServices.notifications.toasts; const isMounted = useMountedState(); @@ -120,6 +122,7 @@ const LinksEditor = ({ const newLink = await openLinkEditorFlyout({ parentDashboard, link: linkToEdit, + mainFlyoutId: flyoutId, ref: editLinkFlyoutRef, }); if (newLink) { @@ -137,7 +140,7 @@ const LinksEditor = ({ } } }, - [editLinkFlyoutRef, orderedLinks, parentDashboard] + [editLinkFlyoutRef, orderedLinks, parentDashboard, flyoutId] ); const hasZeroLinks = useMemo(() => { @@ -151,8 +154,9 @@ const LinksEditor = ({ return link.id !== linkId; }) ); + focusMainFlyout(flyoutId); }, - [orderedLinks] + [orderedLinks, flyoutId] ); return ( @@ -172,7 +176,12 @@ const LinksEditor = ({ {/* The EuiBadge needs an empty title to prevent the default tooltip */} - + {LinksStrings.editor.panelEditor.getTechnicalPreviewLabel()} diff --git a/src/plugins/links/public/components/editor/links_editor_single_link.tsx b/src/plugins/links/public/components/editor/links_editor_single_link.tsx index e6f7bc12ee6a4..e13913f1e349d 100644 --- a/src/plugins/links/public/components/editor/links_editor_single_link.tsx +++ b/src/plugins/links/public/components/editor/links_editor_single_link.tsx @@ -160,7 +160,7 @@ export const LinksEditorSingleLink = ({ size="xs" iconType="pencil" onClick={editLink} - aria-label={LinksStrings.editor.getEditLinkTitle()} + aria-label={LinksStrings.editor.getEditLinkTitle(linkLabel)} data-test-subj="panelEditorLink--editBtn" /> @@ -170,7 +170,7 @@ export const LinksEditorSingleLink = ({ - i18n.translate('links.editor.editLinkTitle', { - defaultMessage: 'Edit link', + getEditLinkTitle: (label?: string) => + i18n.translate('links.editor.editLinkTitle.hasLabel', { + defaultMessage: 'Edit {label} link', + values: { label: label ?? '' }, }), - getDeleteLinkTitle: () => + getDeleteLinkTitle: (label?: string) => i18n.translate('links.editor.deleteLinkTitle', { - defaultMessage: 'Delete link', + defaultMessage: 'Delete {label} link', + values: { label: label ?? '' }, }), getCancelButtonLabel: () => i18n.translate('links.editor.cancelButtonLabel', { diff --git a/src/plugins/links/public/editor/links_editor_tools.tsx b/src/plugins/links/public/editor/links_editor_tools.tsx index 780ef5fd21679..543d08a48ea91 100644 --- a/src/plugins/links/public/editor/links_editor_tools.tsx +++ b/src/plugins/links/public/editor/links_editor_tools.tsx @@ -28,3 +28,14 @@ export const memoizedGetOrderedLinkList = memoize( return links; } ); + +/** + * Return focus to the main flyout div to align with a11y standards + * @param flyoutId ID of the main flyout div element + */ +export const focusMainFlyout = (flyoutId: string) => { + const flyoutElement = document.getElementById(flyoutId); + if (flyoutElement) { + flyoutElement.focus(); + } +}; diff --git a/src/plugins/links/public/editor/open_editor_flyout.tsx b/src/plugins/links/public/editor/open_editor_flyout.tsx index 4ae7582f644c7..83dffe8e55f23 100644 --- a/src/plugins/links/public/editor/open_editor_flyout.tsx +++ b/src/plugins/links/public/editor/open_editor_flyout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { skip, take } from 'rxjs/operators'; +import { v4 as uuidv4 } from 'uuid'; import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui'; import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; @@ -58,6 +59,8 @@ export async function openEditorFlyout( } return new Promise((resolve, reject) => { + const flyoutId = `linksEditorFlyout-${uuidv4()}`; + const closeEditorFlyout = (editorFlyout: OverlayRef) => { if (overlayTracker) { overlayTracker.clearOverlays(); @@ -124,6 +127,7 @@ export async function openEditorFlyout( const editorFlyout = coreServices.overlays.openFlyout( toMountPoint( ; } @@ -34,6 +36,7 @@ export type UnorderedLink = Omit; export async function openLinkEditorFlyout({ ref, link, + mainFlyoutId, // used to manage the focus of this flyout after inidividual link editor flyout is closed parentDashboard, }: LinksEditorProps): Promise { const unmountFlyout = async () => { @@ -44,6 +47,7 @@ export async function openLinkEditorFlyout({ // wait for close animation before unmounting setTimeout(() => { if (ref.current) ReactDOM.unmountComponentAtNode(ref.current); + focusMainFlyout(mainFlyoutId); }, 180); }); }; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3fa3cb4f321fd..2598405f9e40f 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -4771,8 +4771,6 @@ "links.description": "Utiliser des liens pour accéder aux tableaux de bord et aux sites web couramment utilisés.", "links.editor.addButtonLabel": "Ajouter un lien", "links.editor.cancelButtonLabel": "Fermer", - "links.editor.deleteLinkTitle": "Supprimer le lien", - "links.editor.editLinkTitle": "Modifier le lien", "links.editor.horizontalLayout": "Horizontal", "links.editor.unableToSaveToastTitle": "Erreur lors de l'enregistrement du Panneau de liens", "links.editor.updateButtonLabel": "Mettre à jour le lien", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4d69d3a85067c..2243248e76da1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4786,8 +4786,6 @@ "links.description": "リンクを使用して、よく使用されるダッシュボードや Web サイトに移動します。", "links.editor.addButtonLabel": "リンクを追加", "links.editor.cancelButtonLabel": "閉じる", - "links.editor.deleteLinkTitle": "リンクを削除", - "links.editor.editLinkTitle": "リンクを編集", "links.editor.horizontalLayout": "横", "links.editor.unableToSaveToastTitle": "リンクパネルの保存エラー", "links.editor.updateButtonLabel": "リンクを更新", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 03116ed4c3c7e..a0f4b527e0671 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4879,8 +4879,6 @@ "links.description": "使用链接以导航到常用仪表板和网站。", "links.editor.addButtonLabel": "添加链接", "links.editor.cancelButtonLabel": "关闭", - "links.editor.deleteLinkTitle": "删除链接", - "links.editor.editLinkTitle": "编辑链接", "links.editor.horizontalLayout": "水平", "links.editor.unableToSaveToastTitle": "保存链接面板时出错", "links.editor.updateButtonLabel": "更新链接", diff --git a/x-pack/test/accessibility/apps/group1/dashboard_links.ts b/x-pack/test/accessibility/apps/group1/dashboard_links.ts new file mode 100644 index 0000000000000..5e5d00dd34b94 --- /dev/null +++ b/x-pack/test/accessibility/apps/group1/dashboard_links.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const a11y = getService('a11y'); + const deployment = getService('deployment'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const { common, dashboard, home, dashboardLinks } = getPageObjects([ + 'common', + 'dashboard', + 'home', + 'dashboardLinks', + ]); + + const DASHBOARD_NAME = 'Test Links Panel A11y'; + + describe('Dashboard links a11y tests', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await home.addSampleDataSet('flights'); + await common.navigateToApp('dashboard'); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + await dashboard.saveDashboard(DASHBOARD_NAME, { exitFromEditMode: false }); + }); + + after(async () => { + await dashboard.clickQuickSave(); + await common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await home.removeSampleDataSet('flights'); + await kibanaServer.savedObjects.cleanStandardList(); + }); + + it('Empty links editor flyout', async () => { + await dashboardAddPanel.clickEditorMenuButton(); + await dashboardAddPanel.clickAddNewEmbeddableLink('links'); + await a11y.testAppSnapshot(); + }); + + it('Add dashboard link flyout', async () => { + await testSubjects.click('links--panelEditor--addLinkBtn'); + await testSubjects.exists('links--linkEditor--flyout'); + await a11y.testAppSnapshot(); + }); + + it('Add external link flyout', async () => { + const radioOption = await testSubjects.find('links--linkEditor--externalLink--radioBtn'); + const label = await radioOption.findByCssSelector('label[for="externalLink"]'); + await label.click(); + await a11y.testAppSnapshot(); + await dashboardLinks.clickLinkEditorCloseButton(); + }); + + it('Non-empty links editor flyout', async () => { + await dashboardLinks.addDashboardLink('[Flights] Global Flight Dashboard'); + await dashboardLinks.addDashboardLink(DASHBOARD_NAME); + await dashboardLinks.addExternalLink(`${deployment.getHostPort()}/app/bar`); + await a11y.testAppSnapshot(); + }); + + it('Links panel', async () => { + await dashboardLinks.toggleSaveByReference(false); + await dashboardLinks.clickPanelEditorSaveButton(); + await testSubjects.existOrFail('links--component'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/apps/group1/index.ts b/x-pack/test/accessibility/apps/group1/index.ts index d8cd2e76c42dd..7b6a6d615d300 100644 --- a/x-pack/test/accessibility/apps/group1/index.ts +++ b/x-pack/test/accessibility/apps/group1/index.ts @@ -19,8 +19,9 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./uptime')); loadTestFile(require.resolve('./spaces')); loadTestFile(require.resolve('./advanced_settings')); - loadTestFile(require.resolve('./dashboard_panel_options')); loadTestFile(require.resolve('./dashboard_controls')); + loadTestFile(require.resolve('./dashboard_links')); + loadTestFile(require.resolve('./dashboard_panel_options')); loadTestFile(require.resolve('./users')); loadTestFile(require.resolve('./roles')); loadTestFile(require.resolve('./ingest_node_pipelines'));