From 1b0c6aaed36757ed3b0f251f4fc3c47016ded1b1 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Wed, 15 Jul 2026 15:49:45 -0700 Subject: [PATCH] chore(a11y): enable jsx-a11y/click-events-have-key-events as error (#42009) Co-authored-by: Claude Opus 4.8 --- superset-frontend/oxlint.json | 2 +- .../ActionButton/ActionButton.test.tsx | 14 ++++- .../src/components/ActionButton/index.tsx | 2 + .../src/components/ModalTrigger/index.tsx | 13 +++- .../src/components/PopoverSection/index.tsx | 9 ++- .../utils/handleKeyboardActivation.test.ts | 62 +++++++++++++++++++ .../src/utils/handleKeyboardActivation.ts | 48 ++++++++++++++ .../superset-ui-core/src/utils/index.ts | 1 + .../AgGridTable/components/CustomHeader.tsx | 40 ++++++++++-- .../src/react-pivottable/TableRenderers.tsx | 16 +++-- superset-frontend/spec/helpers/shim.tsx | 2 +- .../TableExploreTree/TreeNodeRenderer.tsx | 11 ++++ .../src/components/Chart/Chart.tsx | 8 ++- .../components/Chart/DrillBy/DrillByModal.tsx | 19 +++++- .../Chart/DrillBy/DrillBySubmenu.tsx | 1 + .../DatasourceEditor/DatasourceEditor.tsx | 2 + .../components/ErrorMessage/ErrorAlert.tsx | 9 ++- .../src/components/ListView/ListView.test.tsx | 28 ++++++++- .../src/components/ListView/ListView.tsx | 9 +++ .../components/URLShortLinkButton/index.tsx | 1 + .../components/gridComponents/Tab/Tab.tsx | 4 ++ .../FilterConfigurationLink/index.tsx | 2 + .../FilterTitleContainer.tsx | 2 + .../FiltersConfigModal/ItemTitleContainer.tsx | 2 + .../src/explore/components/ControlHeader.tsx | 2 + .../DataTablesPane/DataTablesPane.tsx | 12 +++- .../components/DatasourcePanel/index.tsx | 10 ++- .../components/ExploreChartPanel/index.tsx | 11 +++- .../components/ExploreViewContainer/index.tsx | 3 + .../ColumnConfigControl.tsx | 4 ++ .../ColumnSelectPopover.tsx | 10 +++ .../DndColumnSelectPopoverTitle.tsx | 2 + .../LayerConfigsControl/LayerTreeItem.tsx | 3 + .../AdhocMetricEditPopover/index.tsx | 11 +++- .../AdhocMetricEditPopoverTitle.tsx | 2 + .../controls/VizTypeControl/index.tsx | 12 +++- .../alerts/components/NotificationMethod.tsx | 6 ++ .../src/features/charts/ChartCard.tsx | 11 +++- .../src/features/dashboards/DashboardCard.tsx | 13 +++- .../AddDataset/DatasetPanel/DatasetPanel.tsx | 59 ++++++++++-------- .../src/pages/ChartList/index.tsx | 4 ++ .../src/pages/DashboardList/index.tsx | 4 ++ .../src/pages/DatabaseList/index.tsx | 11 ++++ .../src/pages/DatasetList/index.tsx | 15 +++++ .../src/pages/RowLevelSecurityList/index.tsx | 4 +- superset-frontend/src/pages/Tags/index.tsx | 8 ++- .../src/pages/TaskList/index.tsx | 4 ++ 47 files changed, 468 insertions(+), 60 deletions(-) create mode 100644 superset-frontend/packages/superset-ui-core/src/utils/handleKeyboardActivation.test.ts create mode 100644 superset-frontend/packages/superset-ui-core/src/utils/handleKeyboardActivation.ts diff --git a/superset-frontend/oxlint.json b/superset-frontend/oxlint.json index 1516d96689c..876a0d9cab8 100644 --- a/superset-frontend/oxlint.json +++ b/superset-frontend/oxlint.json @@ -201,7 +201,7 @@ "jsx-a11y/aria-role": ["error", { "ignoreNonDOM": false }], "jsx-a11y/aria-unsupported-elements": "error", "jsx-a11y/autocomplete-valid": "error", - "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/click-events-have-key-events": "error", "jsx-a11y/control-has-associated-label": "error", "jsx-a11y/heading-has-content": "error", "jsx-a11y/html-has-lang": "error", diff --git a/superset-frontend/packages/superset-ui-core/src/components/ActionButton/ActionButton.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/ActionButton/ActionButton.test.tsx index 123d5349505..06c6584d592 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ActionButton/ActionButton.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ActionButton/ActionButton.test.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { render, screen, userEvent } from '@superset-ui/core/spec'; +import { render, screen, userEvent, fireEvent } from '@superset-ui/core/spec'; import { Icons } from '@superset-ui/core/components/Icons'; import { ActionButton } from '.'; @@ -45,6 +45,18 @@ test('calls onClick when clicked', async () => { expect(onClick).toHaveBeenCalledTimes(1); }); +test('calls onClick when activated with the keyboard', () => { + const onClick = jest.fn(); + render(); + + const button = screen.getByRole('button'); + fireEvent.keyDown(button, { key: 'Enter' }); + expect(onClick).toHaveBeenCalledTimes(1); + + fireEvent.keyDown(button, { key: ' ' }); + expect(onClick).toHaveBeenCalledTimes(2); +}); + test('renders with tooltip when tooltip prop is provided', async () => { const tooltipText = 'This is a tooltip'; render(); diff --git a/superset-frontend/packages/superset-ui-core/src/components/ActionButton/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/ActionButton/index.tsx index 336b8ca3d48..3e56efbc265 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ActionButton/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ActionButton/index.tsx @@ -17,6 +17,7 @@ * under the License. */ +import { handleKeyboardActivation } from '../../utils'; import type { ReactElement, ReactNode } from 'react'; import { Tooltip, type TooltipPlacement } from '@superset-ui/core/components'; import { css, useTheme } from '@apache-superset/core/theme'; @@ -55,6 +56,7 @@ export const ActionButton = ({ className="action-button" data-test={label} onClick={onClick} + onKeyDown={onClick ? handleKeyboardActivation(onClick) : undefined} > {icon} diff --git a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx index 4f477437027..2e94712f513 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx @@ -16,12 +16,13 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '../../utils'; import { forwardRef, ForwardedRef, useState, ReactNode, - MouseEvent, + SyntheticEvent, } from 'react'; import { Button } from '../Button'; @@ -96,7 +97,7 @@ export const ModalTrigger = forwardRef( onExit?.(); }; - const open = (e: MouseEvent) => { + const open = (e: SyntheticEvent) => { e.preventDefault(); beforeOpen?.(); setShowModal(true); @@ -126,7 +127,13 @@ export const ModalTrigger = forwardRef( )} {!isButton && ( -
+
{triggerNode}
)} diff --git a/superset-frontend/packages/superset-ui-core/src/components/PopoverSection/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/PopoverSection/index.tsx index 39009eba76a..284bc60c824 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/PopoverSection/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/PopoverSection/index.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { MouseEventHandler, ReactNode } from 'react'; +import { handleKeyboardActivation } from '../../utils'; +import { ReactNode, SyntheticEvent } from 'react'; import { css, useTheme } from '@apache-superset/core/theme'; import { Icons } from '@superset-ui/core/components/Icons'; import { Tooltip } from '../Tooltip'; @@ -24,7 +25,10 @@ import { Tooltip } from '../Tooltip'; export interface PopoverSectionProps { title: string; isSelected?: boolean; - onSelect?: MouseEventHandler; + // `SyntheticEvent` (rather than `MouseEventHandler`) so the same callback + // can be reused as the keyboard-activation handler via + // `handleKeyboardActivation`, which invokes it with a `KeyboardEvent`. + onSelect?: (event: SyntheticEvent) => void; info?: string; children?: ReactNode; } @@ -48,6 +52,7 @@ export default function PopoverSection({ role="button" tabIndex={0} onClick={onSelect} + onKeyDown={onSelect ? handleKeyboardActivation(onSelect) : undefined} css={css` display: flex; align-items: center; diff --git a/superset-frontend/packages/superset-ui-core/src/utils/handleKeyboardActivation.test.ts b/superset-frontend/packages/superset-ui-core/src/utils/handleKeyboardActivation.test.ts new file mode 100644 index 00000000000..79d0cc6b13b --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/utils/handleKeyboardActivation.test.ts @@ -0,0 +1,62 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { KeyboardEvent } from 'react'; +import { handleKeyboardActivation } from './handleKeyboardActivation'; + +const makeEvent = (key: string, repeat = false) => { + const preventDefault = jest.fn(); + return { + event: { key, repeat, preventDefault } as unknown as KeyboardEvent, + preventDefault, + }; +}; + +test('invokes the callback and prevents default on Enter', () => { + const callback = jest.fn(); + const { event, preventDefault } = makeEvent('Enter'); + handleKeyboardActivation(callback)(event); + expect(callback).toHaveBeenCalledWith(event); + expect(preventDefault).toHaveBeenCalled(); +}); + +test('invokes the callback and prevents default on Space', () => { + const callback = jest.fn(); + const { event, preventDefault } = makeEvent(' '); + handleKeyboardActivation(callback)(event); + expect(callback).toHaveBeenCalledWith(event); + expect(preventDefault).toHaveBeenCalled(); +}); + +test('ignores other keys', () => { + const callback = jest.fn(); + const { event, preventDefault } = makeEvent('a'); + handleKeyboardActivation(callback)(event); + expect(callback).not.toHaveBeenCalled(); + expect(preventDefault).not.toHaveBeenCalled(); +}); + +test('ignores auto-repeat keydown events fired while a key is held', () => { + const callback = jest.fn(); + const { event, preventDefault } = makeEvent('Enter', true); + handleKeyboardActivation(callback)(event); + expect(callback).not.toHaveBeenCalled(); + // preventDefault still fires on the repeat event itself, matching the + // non-repeat Enter case above. + expect(preventDefault).toHaveBeenCalled(); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/utils/handleKeyboardActivation.ts b/superset-frontend/packages/superset-ui-core/src/utils/handleKeyboardActivation.ts new file mode 100644 index 00000000000..d77fa6f58f8 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/utils/handleKeyboardActivation.ts @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { KeyboardEvent } from 'react'; + +/** + * Builds an `onKeyDown` handler that invokes `callback` when the user presses + * Enter or Space, mirroring a click for keyboard users. Pair it with an + * element's `onClick` so `role="button"` (or similar) controls are operable + * from the keyboard, satisfying `jsx-a11y/click-events-have-key-events`. + * + *
+ */ +export function handleKeyboardActivation( + callback: (event: KeyboardEvent) => void, +) { + return (event: KeyboardEvent) => { + if (event.key === 'Enter' || event.key === ' ') { + // Prevent the page from scrolling on Space and stop any duplicate + // default activation on Enter. + event.preventDefault(); + // Ignore auto-repeat keydown events fired while the key is held, so + // a long press activates the callback once, matching a mouse click. + if (event.repeat) { + return; + } + callback(event); + } + }; +} + +export default handleKeyboardActivation; diff --git a/superset-frontend/packages/superset-ui-core/src/utils/index.ts b/superset-frontend/packages/superset-ui-core/src/utils/index.ts index f947bdfd57e..797c7ab561f 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/index.ts @@ -27,6 +27,7 @@ export { default as makeSingleton } from './makeSingleton'; export { default as promiseTimeout } from './promiseTimeout'; export { default as removeDuplicates } from './removeDuplicates'; export { default as withLabel } from './withLabel'; +export { handleKeyboardActivation } from './handleKeyboardActivation'; export { lruCache } from './lruCache'; export { getSelectedText } from './getSelectedText'; export * from './featureFlags'; diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/components/CustomHeader.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/components/CustomHeader.tsx index cb6e58e5491..e5147585cae 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/components/CustomHeader.tsx +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/components/CustomHeader.tsx @@ -19,7 +19,8 @@ * under the License. */ -import { useRef, useState, useEffect } from 'react'; +import { handleKeyboardActivation } from '@superset-ui/core'; +import { useRef, useState, useEffect, SyntheticEvent } from 'react'; import { t } from '@apache-superset/core/translation'; import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons'; import { Column } from '@superset-ui/core/components/ThemedAgGridReact'; @@ -186,7 +187,10 @@ const CustomHeader: React.FC = ({ return undefined; }, [lastFilteredColumn, colId, lastFilteredInputPosition]); - const handleMenuClick = (e: React.MouseEvent) => { + // `SyntheticEvent` (rather than `MouseEvent`) so this callback can also be + // used as the keyboard-activation handler via `handleKeyboardActivation`, + // which invokes it with a `KeyboardEvent`. + const handleMenuClick = (e: SyntheticEvent) => { e.stopPropagation(); setMenuVisible(!isMenuVisible); }; @@ -201,17 +205,35 @@ const CustomHeader: React.FC = ({ const menuContent = ( {shouldShowAsc && ( -
applySort('asc')} className="menu-item"> +
applySort('asc')} + onKeyDown={handleKeyboardActivation(() => applySort('asc'))} + className="menu-item" + > {t('Sort Ascending')}
)} {shouldShowDesc && ( -
applySort('desc')} className="menu-item"> +
applySort('desc')} + onKeyDown={handleKeyboardActivation(() => applySort('desc'))} + className="menu-item" + > {t('Sort Descending')}
)} {currentSort && currentSort?.colId === colId && ( -
+
{t('Clear Sort')}
)} @@ -247,7 +269,13 @@ const CustomHeader: React.FC = ({ isOpen={isMenuVisible} onClose={() => setMenuVisible(false)} > -
+
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx index d3e58763827..1d635803498 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx @@ -20,13 +20,14 @@ import { ReactNode, MouseEvent, + SyntheticEvent, useState, useCallback, useRef, useMemo, useEffect, } from 'react'; -import { safeHtmlSpan } from '@superset-ui/core'; +import { safeHtmlSpan, handleKeyboardActivation } from '@superset-ui/core'; import { t } from '@apache-superset/core/translation'; import { supersetTheme } from '@apache-superset/core/theme'; import PropTypes from 'prop-types'; @@ -154,7 +155,10 @@ function displayCell(value: unknown, allowRenderHtml?: boolean): ReactNode { function displayHeaderCell( needToggle: boolean, ArrowIcon: ReactNode, - onArrowClick: ((e: MouseEvent) => void) | null, + // `SyntheticEvent` (rather than `MouseEvent`) so this callback can also be + // used as the keyboard-activation handler via `handleKeyboardActivation`, + // which invokes it with a `KeyboardEvent`. + onArrowClick: ((e: SyntheticEvent) => void) | null, value: unknown, namesMapping: Record, allowRenderHtml?: boolean, @@ -172,6 +176,9 @@ function displayHeaderCell( tabIndex={0} className="toggle" onClick={onArrowClick || undefined} + onKeyDown={ + onArrowClick ? handleKeyboardActivation(onArrowClick) : undefined + } > {ArrowIcon} @@ -462,7 +469,7 @@ export function TableRenderer(props: TableRendererProps) { ); const toggleRowKey = useCallback( - (flatRowKey: string) => (e: MouseEvent) => { + (flatRowKey: string) => (e: SyntheticEvent) => { e.stopPropagation(); setCollapsedRows(state => ({ ...state, @@ -473,7 +480,7 @@ export function TableRenderer(props: TableRendererProps) { ); const toggleColKey = useCallback( - (flatColKey: string) => (e: MouseEvent) => { + (flatColKey: string) => (e: SyntheticEvent) => { e.stopPropagation(); setCollapsedCols(state => ({ ...state, @@ -1046,6 +1053,7 @@ export function TableRenderer(props: TableRendererProps) { onClick={e => { e.stopPropagation(); }} + onKeyDown={e => e.stopPropagation()} aria-label={ activeSortColumn === i ? `Sorted by ${columnName} ${sortingOrder[i] === 'asc' ? 'ascending' : 'descending'}` diff --git a/superset-frontend/spec/helpers/shim.tsx b/superset-frontend/spec/helpers/shim.tsx index 0bff723e4ca..b1c243357b9 100644 --- a/superset-frontend/spec/helpers/shim.tsx +++ b/superset-frontend/spec/helpers/shim.tsx @@ -119,7 +119,7 @@ jest.mock('@superset-ui/core/components/Icons/AsyncIcon', () => ({ const label = ariaLabel || fileName?.replace(/_/g, '-').toLowerCase() || ''; return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions + // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events = ({ e.stopPropagation()} + onKeyDown={e => e.stopPropagation()} > = ({ node.toggle(); } }} + onKeyDown={handleKeyboardActivation(() => { + if (node.isLeaf) { + node.select(); + } else { + node.toggle(); + } + })} > = ({
e.stopPropagation()} + onKeyDown={e => e.stopPropagation()} > {pinnedSchemas.has(schema) && (
@@ -306,6 +316,7 @@ const TreeNodeRenderer: React.FC = ({
e.stopPropagation()} + onKeyDown={e => e.stopPropagation()} > {isPinned && (
diff --git a/superset-frontend/src/components/Chart/Chart.tsx b/superset-frontend/src/components/Chart/Chart.tsx index 1519837c747..2fcdeb6c7f4 100644 --- a/superset-frontend/src/components/Chart/Chart.tsx +++ b/superset-frontend/src/components/Chart/Chart.tsx @@ -30,6 +30,7 @@ import { type FilterState, type JsonObject, type AgGridChartState, + handleKeyboardActivation, } from '@superset-ui/core'; import { styled } from '@apache-superset/core/theme'; import type { ChartState, Datasource, ChartStatus } from 'src/explore/types'; @@ -465,7 +466,12 @@ function Chart({ {t( 'Click on "Create chart" button in the control panel on the left to preview a visualization or', )}{' '} - + onQuery?.())} + > {t('click here')} . diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx index 57149ee55e0..dd6f747f2c5 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx @@ -17,7 +17,14 @@ * under the License. */ -import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { + SyntheticEvent, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; import { t } from '@apache-superset/core/translation'; import { BinaryQueryObjectFilterClause, @@ -28,6 +35,7 @@ import { isDefined, ContextMenuFilters, AdhocFilter, + handleKeyboardActivation, } from '@superset-ui/core'; import { Alert } from '@apache-superset/core/components'; import { css, useTheme } from '@apache-superset/core/theme'; @@ -554,6 +562,12 @@ export default function DrillByModal({ items={breadcrumbItems} itemRender={(route, _, routes, paths) => { const isLastElement = routes.indexOf(route) === routes.length - 1; + // `route.onClick` is typed by antd as a `MouseEventHandler`, but + // the underlying handler ignores its argument, so it's safe to + // broaden it to an optional `SyntheticEvent` callback here to + // reuse it as the keyboard-activation handler below. + const onRouteClick = route.onClick as + ((event?: SyntheticEvent) => void) | undefined; return isLastElement ? ( {route.title} @@ -564,7 +578,8 @@ export default function DrillByModal({ data-test="drill-by-breadcrumb-item" role="button" tabIndex={0} - onClick={route.onClick} + onClick={onRouteClick} + onKeyDown={handleKeyboardActivation(() => onRouteClick?.())} css={css` cursor: pointer; `} diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx index d8053a8d600..d331cf528e0 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.tsx @@ -217,6 +217,7 @@ export const DrillBySubmenu = ({ role="menu" tabIndex={0} data-test="drill-by-submenu" + onKeyDown={e => e.stopPropagation()} css={css` width: 220px; max-width: 220px; diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx index d5a9034a023..ddb0f6d45dd 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx @@ -36,6 +36,7 @@ import { SupersetClient, getClientErrorObject, getExtensionsRegistry, + handleKeyboardActivation, } from '@superset-ui/core'; import { GenericDataType } from '@apache-superset/core/common'; import { t } from '@apache-superset/core/translation'; @@ -1760,6 +1761,7 @@ function DatasourceEditor({ role="button" tabIndex={0} onClick={onChangeEditMode} + onKeyDown={handleKeyboardActivation(onChangeEditMode)} > {isEditMode ? ( = ({ role="button" tabIndex={0} onClick={toggleDescription} + onKeyDown={handleKeyboardActivation(toggleDescription)} style={{ textDecoration: 'underline', cursor: 'pointer' }} > {isDescriptionVisible ? t('See less') : t('See more')} @@ -127,7 +129,12 @@ export const ErrorAlert: React.FC = ({ return ( <> - setShowModal(true)} tabIndex={0}> + setShowModal(true)} + onKeyDown={handleKeyboardActivation(() => setShowModal(true))} + tabIndex={0} + > {renderTrigger()} diff --git a/superset-frontend/src/components/ListView/ListView.test.tsx b/superset-frontend/src/components/ListView/ListView.test.tsx index e2d0dec4bfc..2b10a1245d1 100644 --- a/superset-frontend/src/components/ListView/ListView.test.tsx +++ b/superset-frontend/src/components/ListView/ListView.test.tsx @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { render, screen, within, waitFor } from 'spec/helpers/testing-library'; +import { + render, + screen, + within, + waitFor, + fireEvent, +} from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import { QueryParamProvider } from 'use-query-params'; import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; @@ -359,4 +365,24 @@ describe('ListView', () => { expect(mockedPropsComprehensive.fetchData).toHaveBeenCalled(); }); + + test('switches view mode via keyboard activation of the toggle buttons', () => { + const { container } = factory({ + renderCard: jest.fn(), + data: [], + count: 0, + initialSort: [{ id: 'something' }], + }); + + const [cardToggle, tableToggle] = + container.querySelectorAll('.toggle-button'); + + expect(screen.getByTestId('empty-state')).toHaveClass('card'); + + fireEvent.keyDown(tableToggle, { key: 'Enter' }); + expect(screen.getByTestId('empty-state')).toHaveClass('table'); + + fireEvent.keyDown(cardToggle, { key: ' ' }); + expect(screen.getByTestId('empty-state')).toHaveClass('card'); + }); }); diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index a88b2ebb736..f8129b50009 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { t } from '@apache-superset/core/translation'; import { Alert } from '@apache-superset/core/components'; import { styled } from '@apache-superset/core/theme'; @@ -255,6 +256,7 @@ const ViewModeToggle = ({ e.currentTarget.blur(); setMode('card'); }} + onKeyDown={handleKeyboardActivation(() => setMode('card'))} className={cx('toggle-button', { active: mode === 'card' })} > @@ -269,6 +271,7 @@ const ViewModeToggle = ({ e.currentTarget.blur(); setMode('table'); }} + onKeyDown={handleKeyboardActivation(() => setMode('table'))} className={cx('toggle-button', { active: mode === 'table' })} > @@ -509,6 +512,9 @@ export function ListView({ tabIndex={0} className="deselect-all" onClick={() => toggleAllRowsSelected(false)} + onKeyDown={handleKeyboardActivation(() => + toggleAllRowsSelected(false), + )} > {t('Deselect all')} @@ -544,6 +550,9 @@ export function ListView({ tabIndex={0} className="tag-btn" onClick={() => setShowBulkTagModal(true)} + onKeyDown={handleKeyboardActivation(() => + setShowBulkTagModal(true), + )} > {t('Add Tag')} diff --git a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx index 136c44425b9..6574bb69386 100644 --- a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx +++ b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx @@ -106,6 +106,7 @@ export default function URLShortLinkButton({ onClick={e => { e.stopPropagation(); }} + onKeyDown={e => e.stopPropagation()} > { role="button" tabIndex={0} onClick={() => dispatch(setEditMode(true))} + onKeyDown={handleKeyboardActivation(() => + dispatch(setEditMode(true)), + )} > {t('edit mode')} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx index 367072fbab9..c68cf9953e4 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { ReactNode, FC, memo } from 'react'; import { getFilterBarTestId } from '../utils'; @@ -32,6 +33,7 @@ export const FilterConfigurationLink: FC = ({
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx index e5420ad1b63..c3026f18efe 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { forwardRef, useCallback, useState } from 'react'; import { t } from '@apache-superset/core/translation'; @@ -199,6 +200,7 @@ const FilterTitleContainer = forwardRef( e.preventDefault(); restoreFilter(id); }} + onKeyDown={handleKeyboardActivation(() => restoreFilter(id))} > {t('Undo?')} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/ItemTitleContainer.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/ItemTitleContainer.tsx index 388421b5ac1..7904835fc4c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/ItemTitleContainer.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/ItemTitleContainer.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { forwardRef, useState } from 'react'; import { t } from '@apache-superset/core/translation'; @@ -167,6 +168,7 @@ const ItemTitleContainer = forwardRef( e.preventDefault(); restoreItem(id); }} + onKeyDown={handleKeyboardActivation(() => restoreItem(id))} > {t('Undo?')} diff --git a/superset-frontend/src/explore/components/ControlHeader.tsx b/superset-frontend/src/explore/components/ControlHeader.tsx index a2bbf047c31..fd62cbaf739 100644 --- a/superset-frontend/src/explore/components/ControlHeader.tsx +++ b/superset-frontend/src/explore/components/ControlHeader.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { FC, ReactNode } from 'react'; import { t } from '@apache-superset/core/translation'; import { css, useTheme, SupersetTheme } from '@apache-superset/core/theme'; @@ -139,6 +140,7 @@ const ControlHeader: FC = ({ role="button" tabIndex={0} onClick={onClick} + onKeyDown={onClick ? handleKeyboardActivation(onClick) : undefined} style={{ cursor: onClick ? 'pointer' : '' }} > {label} diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx index 55994e58d82..6c40bba70d9 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx @@ -18,7 +18,11 @@ */ import { useCallback, useEffect, useMemo, useState, MouseEvent } from 'react'; import { t } from '@apache-superset/core/translation'; -import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core'; +import { + isFeatureEnabled, + FeatureFlag, + handleKeyboardActivation, +} from '@superset-ui/core'; import { styled } from '@apache-superset/core/theme'; import { Icons } from '@superset-ui/core/components/Icons'; import Tabs from '@superset-ui/core/components/Tabs'; @@ -187,6 +191,9 @@ export const DataTablesPane = ({ role="button" tabIndex={0} onClick={() => handleCollapseChange(false)} + onKeyDown={handleKeyboardActivation(() => + handleCollapseChange(false), + )} > {caretIcon} @@ -195,6 +202,9 @@ export const DataTablesPane = ({ role="button" tabIndex={0} onClick={() => handleCollapseChange(true)} + onKeyDown={handleKeyboardActivation(() => + handleCollapseChange(true), + )} > {caretIcon} diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx index 7e9d5244130..c72efc8c5fd 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx @@ -18,7 +18,12 @@ */ import { useContext, useDeferredValue, useMemo, useState } from 'react'; import { t } from '@apache-superset/core/translation'; -import { DatasourceType, Metric, QueryFormData } from '@superset-ui/core'; +import { + DatasourceType, + Metric, + QueryFormData, + handleKeyboardActivation, +} from '@superset-ui/core'; import { Alert } from '@apache-superset/core/components'; import { css, styled, useTheme } from '@apache-superset/core/theme'; @@ -290,6 +295,9 @@ export default function DataSourcePanel({ role="button" tabIndex={0} onClick={() => setShowSaveDatasetModal(true)} + onKeyDown={handleKeyboardActivation(() => + setShowSaveDatasetModal(true), + )} className="add-dataset-alert-description" > {t('Create a dataset')} diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx index 3145bda73a2..7e17b69dce9 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx @@ -30,6 +30,7 @@ import { QueryFormData, JsonObject, getExtensionsRegistry, + handleKeyboardActivation, } from '@superset-ui/core'; import { Alert } from '@apache-superset/core/components'; import { css, styled, useTheme } from '@apache-superset/core/theme'; @@ -396,6 +397,9 @@ const ExploreChartPanel = ({ role="button" tabIndex={0} onClick={() => setShowDatasetModal(true)} + onKeyDown={handleKeyboardActivation(() => + setShowDatasetModal(true), + )} css={{ textDecoration: 'underline' }} > {t('Create a dataset')} @@ -420,7 +424,12 @@ const ExploreChartPanel = ({ {t( 'You updated the values in the control panel, but the chart was not updated automatically. Run the query by clicking on the "Update chart" button or', )}{' '} - + onQuery?.())} + > {t('click here')} . diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx index aebaa64da17..b0ca4ac0868 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.tsx @@ -37,6 +37,7 @@ import { MatrixifyFormData, DatasourceType, ensureIsArray, + handleKeyboardActivation, } from '@superset-ui/core'; import { ControlStateMapping, @@ -998,6 +999,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) { tabIndex={0} className="action-button" onClick={toggleCollapse} + onKeyDown={handleKeyboardActivation(toggleCollapse)} > ({ }, }} onClick={() => setShowAllColumns(!showAllColumns)} + onKeyDown={handleKeyboardActivation(() => + setShowAllColumns(!showAllColumns), + )} > {showAllColumns ? ( <> diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx index 1e69be2c45e..16d0aa344cc 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/ColumnSelectPopover.tsx @@ -35,6 +35,7 @@ import { DatasourceType, Metric, QueryFormMetric, + handleKeyboardActivation, } from '@superset-ui/core'; import { styled, css } from '@apache-superset/core/theme'; import { ColumnMeta, isSavedExpression } from '@superset-ui/chart-controls'; @@ -476,6 +477,9 @@ const ColumnSelectPopover = ({ role="button" tabIndex={0} onClick={setDatasetAndClose} + onKeyDown={handleKeyboardActivation( + setDatasetAndClose, + )} > {t('Create a dataset')} {' '} @@ -487,6 +491,9 @@ const ColumnSelectPopover = ({ role="button" tabIndex={0} onClick={setDatasetAndClose} + onKeyDown={handleKeyboardActivation( + setDatasetAndClose, + )} > {t('Create a dataset')} {' '} @@ -521,6 +528,9 @@ const ColumnSelectPopover = ({ role="button" tabIndex={0} onClick={setDatasetAndClose} + onKeyDown={handleKeyboardActivation( + setDatasetAndClose, + )} > {t('Create a dataset')} {' '} diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx index 05c3cd3744c..38b3a47a73d 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { ChangeEvent, useCallback, useState } from 'react'; import { t } from '@apache-superset/core/translation'; import { styled, useTheme } from '@apache-superset/core/theme'; @@ -96,6 +97,7 @@ export const DndColumnSelectPopoverTitle = ({ onMouseOut={onMouseOut} onFocus={onMouseOver} onClick={onClick} + onKeyDown={onClick ? handleKeyboardActivation(onClick) : undefined} onBlur={onBlur} role="button" tabIndex={0} diff --git a/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerTreeItem.tsx b/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerTreeItem.tsx index 72f519ff7d8..ce99c88acd3 100644 --- a/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerTreeItem.tsx +++ b/superset-frontend/src/explore/components/controls/LayerConfigsControl/LayerTreeItem.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { Icons } from '@superset-ui/core/components/Icons'; import { Button } from '@superset-ui/core/components'; import { Tag } from 'src/components'; @@ -47,6 +48,7 @@ export const LayerTreeItem: FC = ({ @@ -55,6 +57,7 @@ export const LayerTreeItem: FC = ({ diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx index ccbd3a2deab..ce0fea1968b 100644 --- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx +++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx @@ -19,7 +19,12 @@ /* eslint-disable camelcase */ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { isDefined, ensureIsArray, DatasourceType } from '@superset-ui/core'; +import { + isDefined, + ensureIsArray, + DatasourceType, + handleKeyboardActivation, +} from '@superset-ui/core'; import { t } from '@apache-superset/core/translation'; import type { editors } from '@apache-superset/core'; import { styled } from '@apache-superset/core/theme'; @@ -498,6 +503,10 @@ function AdhocMetricEditPopover({ handleDatasetModal?.(true); onClose(); }} + onKeyDown={handleKeyboardActivation(() => { + handleDatasetModal?.(true); + onClose(); + })} > {t('Create a dataset')} diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx index 57d5ed8e3c2..e9cc283f458 100644 --- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx +++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { ChangeEventHandler, FocusEvent, @@ -120,6 +121,7 @@ const AdhocMetricEditPopoverTitle: FC = ({ onMouseOut={handleMouseOut} onFocus={handleMouseOver} onClick={handleClick} + onKeyDown={handleKeyboardActivation(handleClick)} onBlur={handleBlur} role="button" tabIndex={0} diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx index ad11bd73da9..10c9fceff3f 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx @@ -18,7 +18,10 @@ */ import { useCallback, useState } from 'react'; import { t } from '@apache-superset/core/translation'; -import { getChartMetadataRegistry } from '@superset-ui/core'; +import { + getChartMetadataRegistry, + handleKeyboardActivation, +} from '@superset-ui/core'; import { css, styled, SupersetTheme } from '@apache-superset/core/theme'; import { usePluginContext } from 'src/components'; import { Icons, Modal } from '@superset-ui/core/components'; @@ -112,7 +115,12 @@ const VizTypeControl = ({ color: ${theme.colorTextTertiary}; `} > - + {t('View all charts')}
diff --git a/superset-frontend/src/features/alerts/components/NotificationMethod.tsx b/superset-frontend/src/features/alerts/components/NotificationMethod.tsx index 564dba2cc25..244f5f5f120 100644 --- a/superset-frontend/src/features/alerts/components/NotificationMethod.tsx +++ b/superset-frontend/src/features/alerts/components/NotificationMethod.tsx @@ -33,6 +33,7 @@ import { JsonResponse, SupersetClient, isFeatureEnabled, + handleKeyboardActivation, } from '@superset-ui/core'; import { styled, useTheme } from '@apache-superset/core/theme'; import { Icons } from '@superset-ui/core/components/Icons'; @@ -580,6 +581,7 @@ export const NotificationMethod: FunctionComponent = ({ tabIndex={0} className="delete-button" onClick={() => onRemove(index)} + onKeyDown={handleKeyboardActivation(() => onRemove(index))} aria-label={t('Remove notification method')} > @@ -782,6 +784,7 @@ export const NotificationMethod: FunctionComponent = ({ role="button" tabIndex={0} onClick={() => setCcVisible(true)} + onKeyDown={handleKeyboardActivation(() => setCcVisible(true))} style={{ display: ccVisible ? 'none' : 'inline-flex' }} > @@ -792,6 +795,9 @@ export const NotificationMethod: FunctionComponent = ({ role="button" tabIndex={0} onClick={() => setBccVisible(true)} + onKeyDown={handleKeyboardActivation(() => + setBccVisible(true), + )} style={{ display: bccVisible ? 'none' : 'inline-flex' }} > diff --git a/superset-frontend/src/features/charts/ChartCard.tsx b/superset-frontend/src/features/charts/ChartCard.tsx index 9a7c0569ed2..b55904f7144 100644 --- a/superset-frontend/src/features/charts/ChartCard.tsx +++ b/superset-frontend/src/features/charts/ChartCard.tsx @@ -17,7 +17,11 @@ * under the License. */ import { t } from '@apache-superset/core/translation'; -import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core'; +import { + isFeatureEnabled, + FeatureFlag, + handleKeyboardActivation, +} from '@superset-ui/core'; import { css } from '@apache-superset/core/theme'; import { Link, useHistory } from 'react-router-dom'; import { @@ -86,6 +90,7 @@ export default function ChartCard({ role="button" tabIndex={0} onClick={() => openChartEditModal(chart)} + onKeyDown={handleKeyboardActivation(() => openChartEditModal(chart))} > handleBulkChartExport([chart])} + onKeyDown={handleKeyboardActivation(() => + handleBulkChartExport([chart]), + )} > openDashboardEditModal(dashboard)} + onKeyDown={handleKeyboardActivation(() => + openDashboardEditModal(dashboard), + )} data-test="dashboard-card-option-edit-button" > {t('Edit')} @@ -96,6 +103,9 @@ function DashboardCard({ role="button" tabIndex={0} onClick={() => handleBulkDashboardExport([dashboard])} + onKeyDown={handleKeyboardActivation(() => + handleBulkDashboardExport([dashboard]), + )} className="action-button" data-test="dashboard-card-option-export-button" > @@ -114,6 +124,7 @@ function DashboardCard({ tabIndex={0} className="action-button" onClick={() => onDelete(dashboard)} + onKeyDown={handleKeyboardActivation(() => onDelete(dashboard))} data-test="dashboard-card-option-delete-button" > {t('Delete')} diff --git a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.tsx b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.tsx index 6bf357a1f3b..78287663af4 100644 --- a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.tsx +++ b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { handleKeyboardActivation } from '@superset-ui/core'; import { t } from '@apache-superset/core/translation'; import { Alert } from '@apache-superset/core/components'; import { styled } from '@apache-superset/core/theme'; @@ -216,34 +217,38 @@ const EXISTING_DATASET_DESCRIPTION = t( ); const VIEW_DATASET = t('View Dataset'); -const renderExistingDatasetAlert = (dataset?: DatasetObject) => ( - - {EXISTING_DATASET_DESCRIPTION} - { - if (dataset?.explore_url) { - // `explore_url` is router-relative from the backend (rooted under - // a subdirectory deployment); strip the root so openInNewTab's - // ensureAppRoot re-prefixes it once rather than doubling it. - openInNewTab(stripAppRoot(dataset.explore_url)); - } - }} - tabIndex={0} - className="view-dataset-button" - > - {VIEW_DATASET} - - +const renderExistingDatasetAlert = (dataset?: DatasetObject) => { + const openExplore = () => { + if (dataset?.explore_url) { + // `explore_url` is router-relative from the backend (rooted under + // a subdirectory deployment); strip the root so openInNewTab's + // ensureAppRoot re-prefixes it once rather than doubling it. + openInNewTab(stripAppRoot(dataset.explore_url)); } - /> -); + }; + return ( + + {EXISTING_DATASET_DESCRIPTION} + + {VIEW_DATASET} + + + } + /> + ); +}; const DatasetPanel = ({ tableName, diff --git a/superset-frontend/src/pages/ChartList/index.tsx b/superset-frontend/src/pages/ChartList/index.tsx index 45e65370170..00ea34af07e 100644 --- a/superset-frontend/src/pages/ChartList/index.tsx +++ b/superset-frontend/src/pages/ChartList/index.tsx @@ -25,6 +25,7 @@ import { JsonResponse, SupersetClient, isMatrixifyEnabled, + handleKeyboardActivation, } from '@superset-ui/core'; import { css, styled } from '@apache-superset/core/theme'; import { useState, useMemo, useCallback } from 'react'; @@ -544,6 +545,7 @@ function ChartList(props: ChartListProps) { tabIndex={0} className="action-button" onClick={openEditModal} + onKeyDown={handleKeyboardActivation(openEditModal)} > @@ -561,6 +563,7 @@ function ChartList(props: ChartListProps) { tabIndex={0} className="action-button" onClick={handleExport} + onKeyDown={handleKeyboardActivation(handleExport)} > @@ -589,6 +592,7 @@ function ChartList(props: ChartListProps) { tabIndex={0} className="action-button" onClick={confirmDelete} + onKeyDown={handleKeyboardActivation(confirmDelete)} > diff --git a/superset-frontend/src/pages/DashboardList/index.tsx b/superset-frontend/src/pages/DashboardList/index.tsx index 100669050bb..91d3ef3bc80 100644 --- a/superset-frontend/src/pages/DashboardList/index.tsx +++ b/superset-frontend/src/pages/DashboardList/index.tsx @@ -21,6 +21,7 @@ import { isFeatureEnabled, FeatureFlag, SupersetClient, + handleKeyboardActivation, } from '@superset-ui/core'; import { styled } from '@apache-superset/core/theme'; import { useSelector } from 'react-redux'; @@ -503,6 +504,7 @@ function DashboardList(props: DashboardListProps) { tabIndex={0} className="action-button" onClick={handleEdit} + onKeyDown={handleKeyboardActivation(handleEdit)} > @@ -520,6 +522,7 @@ function DashboardList(props: DashboardListProps) { tabIndex={0} className="action-button" onClick={handleExport} + onKeyDown={handleKeyboardActivation(handleExport)} > @@ -548,6 +551,7 @@ function DashboardList(props: DashboardListProps) { tabIndex={0} className="action-button" onClick={confirmDelete} + onKeyDown={handleKeyboardActivation(confirmDelete)} > setSlCurrentlyDeleting(original)} + onKeyDown={handleKeyboardActivation(() => + setSlCurrentlyDeleting(original), + )} > @@ -715,6 +719,9 @@ function DatabaseList({ onClick={() => setSlCurrentlyEditing(original.uuid ?? null) } + onKeyDown={handleKeyboardActivation(() => + setSlCurrentlyEditing(original.uuid ?? null), + )} > @@ -746,6 +753,7 @@ function DatabaseList({ tabIndex={0} className="action-button" onClick={handleEdit} + onKeyDown={handleKeyboardActivation(handleEdit)} > @@ -762,6 +770,7 @@ function DatabaseList({ tabIndex={0} className="action-button" onClick={handleExport} + onKeyDown={handleKeyboardActivation(handleExport)} > @@ -779,6 +788,7 @@ function DatabaseList({ tabIndex={0} className="action-button" onClick={handleSync} + onKeyDown={handleKeyboardActivation(handleSync)} > @@ -791,6 +801,7 @@ function DatabaseList({ className="action-button" data-test="database-delete" onClick={handleDelete} + onKeyDown={handleKeyboardActivation(handleDelete)} > = ({ tabIndex={0} className="action-button" onClick={() => handleSemanticViewDelete(original)} + onKeyDown={handleKeyboardActivation(() => + handleSemanticViewDelete(original), + )} > @@ -886,6 +890,9 @@ const DatasetList: FunctionComponent = ({ tabIndex={0} className="action-button" onClick={() => setSvCurrentlyEditing(original)} + onKeyDown={handleKeyboardActivation(() => + setSvCurrentlyEditing(original), + )} > @@ -932,6 +939,11 @@ const DatasetList: FunctionComponent = ({ tabIndex={0} className={`action-button ${allowEdit ? '' : 'disabled'}`} onClick={allowEdit ? handleEdit : undefined} + onKeyDown={ + allowEdit + ? handleKeyboardActivation(handleEdit) + : undefined + } > @@ -949,6 +961,7 @@ const DatasetList: FunctionComponent = ({ tabIndex={0} className="action-button" onClick={handleExport} + onKeyDown={handleKeyboardActivation(handleExport)} > @@ -966,6 +979,7 @@ const DatasetList: FunctionComponent = ({ tabIndex={0} className="action-button" onClick={handleDuplicate} + onKeyDown={handleKeyboardActivation(handleDuplicate)} > @@ -983,6 +997,7 @@ const DatasetList: FunctionComponent = ({ tabIndex={0} className="action-button" onClick={handleDelete} + onKeyDown={handleKeyboardActivation(handleDelete)} > diff --git a/superset-frontend/src/pages/RowLevelSecurityList/index.tsx b/superset-frontend/src/pages/RowLevelSecurityList/index.tsx index 4d77911ba1d..689c493e49e 100644 --- a/superset-frontend/src/pages/RowLevelSecurityList/index.tsx +++ b/superset-frontend/src/pages/RowLevelSecurityList/index.tsx @@ -17,7 +17,7 @@ * under the License. */ import { t } from '@apache-superset/core/translation'; -import { SupersetClient } from '@superset-ui/core'; +import { SupersetClient, handleKeyboardActivation } from '@superset-ui/core'; import { useCallback, useMemo, useState } from 'react'; import { ConfirmStatusChange, Tooltip } from '@superset-ui/core/components'; import { @@ -209,6 +209,7 @@ function RowLevelSecurityList(props: RLSProps) { tabIndex={0} className="action-button" onClick={handleEdit} + onKeyDown={handleKeyboardActivation(handleEdit)} > @@ -236,6 +237,7 @@ function RowLevelSecurityList(props: RLSProps) { tabIndex={0} className="action-button" onClick={confirmDelete} + onKeyDown={handleKeyboardActivation(confirmDelete)} > diff --git a/superset-frontend/src/pages/TaskList/index.tsx b/superset-frontend/src/pages/TaskList/index.tsx index 41b25d65602..3acac2d5177 100644 --- a/superset-frontend/src/pages/TaskList/index.tsx +++ b/superset-frontend/src/pages/TaskList/index.tsx @@ -21,6 +21,7 @@ import { FeatureFlag, isFeatureEnabled, SupersetClient, + handleKeyboardActivation, } from '@superset-ui/core'; import { useTheme } from '@apache-superset/core/theme'; import { t } from '@apache-superset/core/translation'; @@ -519,6 +520,9 @@ function TaskList({ addDangerToast, addSuccessToast, user }: TaskListProps) { tabIndex={0} className="action-button" onClick={() => openCancelModal(original)} + onKeyDown={handleKeyboardActivation(() => + openCancelModal(original), + )} >