Compare commits

...

6 Commits

Author SHA1 Message Date
dependabot[bot]
490d284d75 chore(deps): bump actions/setup-python from 6.3.0 to 7.0.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.3.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](ece7cb06ca...5fda3b95a4)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 07:06:43 +00:00
Sepuri Sai Krishna
6856d0fc67 fix(select): rank case-insensitive matches consistently in dropdown search (#42408) 2026-07-26 19:29:30 -07:00
Hans Yu
8f8331f656 chore: session enforce sqlalchemy 2.0 (#42365)
Co-authored-by: Evan Rusackas <evan@rusackas.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 19:28:42 -07:00
Evan Rusackas
e54eccd5fb chore(a11y): enable jsx-a11y/prefer-tag-over-role as error (#42078)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:38:55 -07:00
Evan Rusackas
710037d3d2 chore(importers): log field names instead of full config on validation failure (#42399)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 17:36:34 -07:00
Elizabeth Thompson
90040fc1f6 fix(mcp_service): downgrade client-disconnect transport noise to WARNING (SC-115264) (#42441)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-07-26 15:02:15 -07:00
274 changed files with 1776 additions and 1359 deletions

View File

@@ -40,7 +40,7 @@ jobs:
uses: ./.github/actions/setup-supersetbot/
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"

View File

@@ -144,7 +144,7 @@ repos:
git fetch --no-recurse-submodules origin "$TARGET_BRANCH" 2>/dev/null || true
fi
BASE=$(git merge-base origin/"$TARGET_BRANCH" HEAD 2>/dev/null) || BASE="HEAD"
files=$(git diff --name-only --diff-filter=ACM "$BASE"..HEAD 2>/dev/null | grep '^superset/.*\.py$' || true)
files=$(git diff --name-only --diff-filter=ACM "$BASE"..HEAD 2>/dev/null | grep '^superset/.*\.py$' | grep -v '^superset/migrations/' || true)
if [ -n "$files" ]; then
pylint --rcfile=.pylintrc --load-plugins=superset.extensions.pylint --reports=no $files
else

View File

@@ -215,6 +215,7 @@
"jsx-a11y/no-noninteractive-tabindex": "error",
"jsx-a11y/no-redundant-roles": "error",
"jsx-a11y/no-static-element-interactions": "error",
"jsx-a11y/prefer-tag-over-role": "error",
"jsx-a11y/role-has-required-aria-props": "error",
"jsx-a11y/role-supports-aria-props": "error",
"jsx-a11y/scope": "error",

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { render, screen, userEvent, fireEvent } from '@superset-ui/core/spec';
import { render, screen, userEvent } from '@superset-ui/core/spec';
import { Icons } from '@superset-ui/core/components/Icons';
import { ActionButton } from '.';
@@ -45,18 +45,6 @@ test('calls onClick when clicked', async () => {
expect(onClick).toHaveBeenCalledTimes(1);
});
test('calls onClick when activated with the keyboard', () => {
const onClick = jest.fn();
render(<ActionButton {...defaultProps} onClick={onClick} />);
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(<ActionButton {...defaultProps} tooltip={tooltipText} />);
@@ -115,6 +103,6 @@ test('has proper accessibility attributes', () => {
render(<ActionButton {...defaultProps} />);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('tabIndex', '0');
expect(button).toHaveAttribute('role', 'button');
expect(button.tagName).toBe('BUTTON');
expect(button).toHaveAttribute('type', 'button');
});

View File

@@ -17,8 +17,8 @@
* under the License.
*/
import { handleKeyboardActivation } from '../../utils';
import type { ReactElement, ReactNode } from 'react';
import cx from 'classnames';
import { Tooltip, type TooltipPlacement } from '@superset-ui/core/components';
import { css, useTheme } from '@apache-superset/core/theme';
@@ -28,6 +28,9 @@ export interface ActionProps {
placement?: TooltipPlacement;
icon: ReactNode;
onClick: () => void;
className?: string;
disabled?: boolean;
dataTest?: string;
}
export const ActionButton = ({
@@ -36,30 +39,45 @@ export const ActionButton = ({
placement,
icon,
onClick,
className,
disabled = false,
dataTest,
}: ActionProps) => {
const theme = useTheme();
const actionButton = (
<span
role="button"
tabIndex={0}
<button
type="button"
aria-disabled={disabled}
aria-label={typeof tooltip === 'string' ? tooltip : label}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
margin: 0;
font: inherit;
line-height: 1;
display: inline-flex;
align-items: center;
cursor: pointer;
color: ${theme.colorIcon};
margin-right: ${theme.sizeUnit}px;
&:hover {
&:not(.disabled):hover {
path {
fill: ${theme.colorPrimary};
}
}
&.disabled {
color: ${theme.colorTextDisabled};
cursor: not-allowed;
}
`}
className="action-button"
data-test={label}
onClick={onClick}
onKeyDown={onClick ? handleKeyboardActivation(onClick) : undefined}
className={cx('action-button', className, { disabled })}
data-test={dataTest ?? label}
onClick={disabled ? undefined : onClick}
>
{icon}
</span>
</button>
);
const tooltipId = `${label.replaceAll(' ', '-').toLowerCase()}-tooltip`;

View File

@@ -21,7 +21,11 @@ import type { ButtonGroupProps } from './types';
export function ButtonGroup(props: ButtonGroupProps) {
const { className, children } = props;
return (
// role="group" is the correct ARIA pattern for a generic button toolbar;
// the suggested native tags (fieldset, etc.) carry unrelated form
// semantics and unwanted default browser styling.
<div
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="group"
className={className}
css={{

View File

@@ -134,6 +134,10 @@ const ImageContainer = ({
? imageMap[image as keyof typeof imageMap]
: image;
return (
// Groups Empty's SVG illustration + description into one accessible
// image; can't be a literal <img> since it's a component tree, not an
// image file reference.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
<div role="img" aria-label="empty">
<Empty
description={false}

View File

@@ -25,8 +25,12 @@ import { Icons } from '@superset-ui/core/components/Icons';
import { Tooltip } from '../Tooltip';
import type { FaveStarProps } from './types';
const StyledLink = styled.a`
const StyledLink = styled.button`
${({ theme }) => css`
appearance: none;
border: none;
background: none;
font: inherit;
font-size: ${theme.fontSizeXL}px;
display: flex;
padding: 0 0 0 ${theme.sizeUnit * 2}px;
@@ -55,12 +59,10 @@ export const FaveStar = ({
const content = (
<StyledLink
href="#"
type="button"
onClick={onClick}
className="fave-unfave-icon"
data-test="fave-unfave-icon"
role="button"
tabIndex={0}
>
{isStarred ? (
<Icons.StarFilled

View File

@@ -102,6 +102,10 @@ export const LabeledErrorBoundInput = ({
</Tooltip>
)
}
// input[type=password] doesn't get an implicit "textbox" role
// (unlike other text inputs), so this explicit override is
// needed for it to be discoverable via role-based queries/AT.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="textbox"
/>
) : renderAsTextArea ? (

View File

@@ -79,8 +79,12 @@ const IconButton: React.FC<IconButtonProps> = ({
};
return (
// antd's Card renders a fixed <div> (no polymorphic tag support) with
// its own rich internal layout (cover/title/tooltip); that doesn't map
// onto a native <button>, so role="button" is used instead.
<Card
hoverable
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
tabIndex={0}
aria-label={buttonText}

View File

@@ -99,6 +99,10 @@ export function Loading({
$spinnerHeight="auto"
$opacity={opacity}
className={cls('loading', position, className)}
// role="status" is the standard WAI-ARIA live-region pattern for a
// loading spinner; <output> (the suggested tag) is for form
// calculation results, not a fit here.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="status"
aria-live="polite"
aria-label={t('Loading')}

View File

@@ -127,10 +127,15 @@ export const ModalTrigger = forwardRef(
</Button>
)}
{!isButton && (
// Generic wrapper used by 19+ callers passing arbitrary
// triggerNode content that relies on block-level <div> layout;
// swapping to <button> risks a layout regression across callers
// with no shared CSS to guide a safe reset.
<div
data-test="span-modal-trigger"
onClick={open}
onKeyDown={handleKeyboardActivation(open)}
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
tabIndex={0}
>

View File

@@ -103,7 +103,18 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
})),
}}
>
<div role="button" css={{ display: 'flex', alignItems: 'center' }}>
<button
type="button"
css={{
appearance: 'none',
border: 'none',
background: 'none',
padding: 0,
font: 'inherit',
display: 'flex',
alignItems: 'center',
}}
>
{selected && renderButton(selected)}
<Icons.DownOutlined
iconSize="s"
@@ -112,7 +123,7 @@ const PopoverDropdown = (props: PopoverDropdownProps) => {
marginLeft: theme.sizeUnit * 0.5,
}}
/>
</div>
</button>
</Dropdown>
);
};

View File

@@ -22,7 +22,7 @@ import PopoverSection from '.';
test('renders with default props', async () => {
render(
<PopoverSection title="Title">
<div role="form" />
<form aria-label="test-form" />
</PopoverSection>,
);
expect(await screen.findByRole('form')).toBeInTheDocument();
@@ -32,7 +32,7 @@ test('renders with default props', async () => {
test('renders tooltip icon', async () => {
render(
<PopoverSection title="Title" info="Tooltip">
<div role="form" />
<form />
</PopoverSection>,
);
expect((await screen.findAllByRole('img')).length).toBe(2);
@@ -41,7 +41,7 @@ test('renders tooltip icon', async () => {
test('renders a tooltip when hovered', async () => {
render(
<PopoverSection title="Title" info="Tooltip">
<div role="form" />
<form />
</PopoverSection>,
);
await userEvent.hover(screen.getAllByRole('img')[0]);
@@ -52,7 +52,7 @@ test('calls onSelect when clicked', async () => {
const onSelect = jest.fn();
render(
<PopoverSection title="Title" onSelect={onSelect}>
<div role="form" />
<form />
</PopoverSection>,
);
await userEvent.click(await screen.findByRole('img'));

View File

@@ -16,8 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { handleKeyboardActivation } from '../../utils';
import { ReactNode, SyntheticEvent } from 'react';
import { MouseEventHandler, ReactNode } from 'react';
import { css, useTheme } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import { Tooltip } from '../Tooltip';
@@ -25,10 +24,7 @@ import { Tooltip } from '../Tooltip';
export interface PopoverSectionProps {
title: string;
isSelected?: boolean;
// `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;
onSelect?: MouseEventHandler<HTMLButtonElement>;
info?: string;
children?: ReactNode;
}
@@ -48,12 +44,17 @@ export default function PopoverSection({
opacity: isSelected ? 1 : 0.6,
}}
>
<div
role="button"
tabIndex={0}
<button
type="button"
onClick={onSelect}
onKeyDown={onSelect ? handleKeyboardActivation(onSelect) : undefined}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
text-align: left;
width: 100%;
display: flex;
align-items: center;
cursor: ${onSelect ? 'pointer' : 'default'};
@@ -68,8 +69,9 @@ export default function PopoverSection({
margin-right: ${theme.sizeUnit}px;
`}
>
{/* role is auto-computed by BaseIconComponent as "img" since
there's no onClick, so no explicit role needed here. */}
<Icons.InfoCircleOutlined
role="img"
iconSize="s"
iconColor={theme.colorIcon}
/>
@@ -77,10 +79,9 @@ export default function PopoverSection({
)}
<Icons.CheckOutlined
iconSize="s"
role="img"
iconColor={isSelected ? theme.colorPrimary : theme.colorIcon}
/>
</div>
</button>
<div
css={css`
margin-left: ${theme.sizeUnit}px;

View File

@@ -35,7 +35,8 @@ const RefreshLabel = ({
<Tooltip title={tooltipContent}>
<Icons.SyncOutlined
iconSize="l"
role="button"
// role is auto-computed by BaseIconComponent as "button" whenever
// onClick is present (and "img" otherwise), so no explicit role here.
tabIndex={disabled ? -1 : 0}
onClick={disabled ? undefined : onClick}
css={(theme: SupersetTheme) => ({

View File

@@ -870,14 +870,14 @@ test('Renders only an overflow tag if dropdown is open in oneLine mode', async (
test('does not fire onChange when searching but no selection', async () => {
const onChange = jest.fn();
render(
<div role="main">
<main>
<AsyncSelect
{...defaultProps}
onChange={onChange}
mode="multiple"
allowNewOptions
/>
</div>,
</main>,
);
await open();
await type('Joh');

View File

@@ -1134,14 +1134,14 @@ test('dropdown takes full width of the select input for single select', async ()
test('does not fire onChange when searching but no selection', async () => {
const onChange = jest.fn();
render(
<div role="main">
<main>
<Select
{...defaultProps}
onChange={onChange}
mode="multiple"
allowNewOptions
/>
</div>,
</main>,
);
await open();
await type('Joh');

View File

@@ -142,7 +142,9 @@ EditableTabs.defaultProps = {
};
EditableTabs.TabPane.defaultProps = {
closeIcon: <StyledCloseOutlined iconSize="s" role="button" tabIndex={0} />,
// rc-tabs already wraps closeIcon in its own <button role="tab"
// aria-label="remove">; this is just decorative content inside it.
closeIcon: <StyledCloseOutlined iconSize="s" />,
};
export const StyledLineEditableTabs = styled(EditableTabs)`

View File

@@ -66,3 +66,30 @@ test('falls back to localeCompare when strings have no match relationship to sea
expect(rankedSearchCompare('abc', 'def', 'xyz')).toBeLessThan(0);
expect(rankedSearchCompare('def', 'abc', 'xyz')).toBeGreaterThan(0);
});
test('ranks a case-insensitive substring match above a non-match', () => {
// `zzABCzz` contains the search term ignoring case, `aaaa` does not match at
// all, so the match must win even though localeCompare would order it last.
expect(rankedSearchCompare('zzABCzz', 'aaaa', 'abc')).toBeLessThan(0);
expect(rankedSearchCompare('aaaa', 'zzABCzz', 'abc')).toBeGreaterThan(0);
expect(['aaaa', 'zzABCzz'].sort(searchSort('abc'))).toEqual([
'zzABCzz',
'aaaa',
]);
});
test('is antisymmetric so Array.prototype.sort stays well defined', () => {
const pairs: [string, string, string][] = [
['zzABCzz', 'aaaa', 'abc'],
['Total Revenue', 'ARR', 'revenue'],
['My Country', 'zzz', 'country'],
['%f %B', '%F %b', '%F'],
['her', 'Cher', 'Her'],
];
pairs.forEach(([a, b, search]) => {
expect(
Math.sign(rankedSearchCompare(a, b, search)) +
Math.sign(rankedSearchCompare(b, a, search)),
).toBe(0);
});
});

View File

@@ -32,7 +32,8 @@ export function rankedSearchCompare(a: string, b: string, search: string) {
Number(bLower.startsWith(searchLower)) -
Number(aLower.startsWith(searchLower)) ||
Number(b.includes(search)) - Number(a.includes(search)) ||
Number(bLower.includes(searchLower)) - Number(a.includes(searchLower)) ||
Number(bLower.includes(searchLower)) -
Number(aLower.includes(searchLower)) ||
a.localeCompare(b)
);
}

View File

@@ -69,8 +69,9 @@ describe('ChartDataProvider', () => {
formData: { ...bigNumberFormData },
children: ({ loading, payload, error }) => (
<div>
{/* eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors the real Loading component's role="status" pattern */}
{loading && <span role="status">Loading...</span>}
{payload && <pre role="contentinfo">{JSON.stringify(payload)}</pre>}
{payload && <footer>{JSON.stringify(payload)}</footer>}
{error && <div role="alert">{error.message}</div>}
</div>
),

View File

@@ -19,8 +19,7 @@
* under the License.
*/
import { handleKeyboardActivation } from '@superset-ui/core';
import { useRef, useState, useEffect, SyntheticEvent } from 'react';
import { useRef, useState, useEffect, MouseEvent } from 'react';
import { t } from '@apache-superset/core/translation';
import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons';
import { Column } from '@superset-ui/core/components/ThemedAgGridReact';
@@ -187,10 +186,7 @@ const CustomHeader: React.FC<CustomHeaderParams> = ({
return undefined;
}, [lastFilteredColumn, colId, lastFilteredInputPosition]);
// `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) => {
const handleMenuClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setMenuVisible(!isMenuVisible);
};
@@ -205,37 +201,27 @@ const CustomHeader: React.FC<CustomHeaderParams> = ({
const menuContent = (
<MenuContainer>
{shouldShowAsc && (
<div
role="button"
tabIndex={0}
<button
type="button"
onClick={() => applySort('asc')}
onKeyDown={handleKeyboardActivation(() => applySort('asc'))}
className="menu-item"
>
<ArrowUpOutlined /> {t('Sort Ascending')}
</div>
</button>
)}
{shouldShowDesc && (
<div
role="button"
tabIndex={0}
<button
type="button"
onClick={() => applySort('desc')}
onKeyDown={handleKeyboardActivation(() => applySort('desc'))}
className="menu-item"
>
<ArrowDownOutlined /> {t('Sort Descending')}
</div>
</button>
)}
{currentSort && currentSort?.colId === colId && (
<div
role="button"
tabIndex={0}
onClick={clearSort}
onKeyDown={handleKeyboardActivation(clearSort)}
className="menu-item"
>
<button type="button" onClick={clearSort} className="menu-item">
<span style={{ fontSize: 16 }}></span> {t('Clear Sort')}
</div>
</button>
)}
</MenuContainer>
);
@@ -269,15 +255,13 @@ const CustomHeader: React.FC<CustomHeaderParams> = ({
isOpen={isMenuVisible}
onClose={() => setMenuVisible(false)}
>
<div
role="button"
tabIndex={0}
<button
type="button"
className="three-dots-menu"
onClick={handleMenuClick}
onKeyDown={handleKeyboardActivation(handleMenuClick)}
>
<KebabMenu />
</div>
</button>
</CustomPopover>
)}
</Container>

View File

@@ -27,6 +27,10 @@ export const Container = styled.div`
width: 100%;
.three-dots-menu {
appearance: none;
border: none;
background: none;
font: inherit;
align-self: center;
margin-left: ${theme.sizeUnit}px;
cursor: pointer;
@@ -122,6 +126,12 @@ export const MenuContainer = styled.div`
padding: ${theme.sizeUnit}px 0;
.menu-item {
appearance: none;
border: none;
background: none;
font: inherit;
width: 100%;
text-align: left;
padding: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 4}px;
cursor: pointer;
display: flex;

View File

@@ -144,7 +144,23 @@ export const Styles = styled.div<{ isDashboardEditMode: boolean }>`
}
.toggle {
appearance: none;
border: none;
background: none;
padding-right: ${theme.sizeUnit}px;
font: inherit;
cursor: pointer;
}
.sort-icon-btn {
display: inline-flex;
align-items: center;
appearance: none;
border: none;
background: none;
padding: 0;
margin: 0;
font: inherit;
cursor: pointer;
}

View File

@@ -20,14 +20,13 @@
import {
ReactNode,
MouseEvent,
SyntheticEvent,
useState,
useCallback,
useRef,
useMemo,
useEffect,
} from 'react';
import { safeHtmlSpan, handleKeyboardActivation } from '@superset-ui/core';
import { safeHtmlSpan } from '@superset-ui/core';
import { t } from '@apache-superset/core/translation';
import { supersetTheme } from '@apache-superset/core/theme';
import PropTypes from 'prop-types';
@@ -155,10 +154,7 @@ function displayCell(value: unknown, allowRenderHtml?: boolean): ReactNode {
function displayHeaderCell(
needToggle: boolean,
ArrowIcon: ReactNode,
// `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,
onArrowClick: ((e: MouseEvent<HTMLButtonElement>) => void) | null,
value: unknown,
namesMapping: Record<string, string>,
allowRenderHtml?: boolean,
@@ -171,17 +167,13 @@ function displayHeaderCell(
: parsedLabel;
return needToggle ? (
<span className="toggle-wrapper">
<span
role="button"
tabIndex={0}
<button
type="button"
className="toggle"
onClick={onArrowClick || undefined}
onKeyDown={
onArrowClick ? handleKeyboardActivation(onArrowClick) : undefined
}
>
{ArrowIcon}
</span>
</button>
<span className="toggle-val">{labelContent}</span>
</span>
) : (
@@ -468,7 +460,7 @@ export function TableRenderer(props: TableRendererProps) {
);
const toggleRowKey = useCallback(
(flatRowKey: string) => (e: SyntheticEvent) => {
(flatRowKey: string) => (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setCollapsedRows(state => ({
...state,
@@ -479,7 +471,7 @@ export function TableRenderer(props: TableRendererProps) {
);
const toggleColKey = useCallback(
(flatColKey: string) => (e: SyntheticEvent) => {
(flatColKey: string) => (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
setCollapsedCols(state => ({
...state,
@@ -1044,9 +1036,9 @@ export function TableRenderer(props: TableRendererProps) {
namesMapping,
settingsAllowRenderHtml,
)}
<span
role="button"
tabIndex={0}
<button
type="button"
className="sort-icon-btn"
// Prevents event bubbling to avoid conflict with column header click handlers
// Ensures sort operation executes without triggering cross-filtration
onClick={e => {
@@ -1060,7 +1052,7 @@ export function TableRenderer(props: TableRendererProps) {
}
>
{visibleSortIcon && getSortIcon(i)}
</span>
</button>
</th>,
);
} else if (attrIdx === colKey.length) {

View File

@@ -98,16 +98,13 @@ export default memo(
key={item}
className={currentPage === item ? 'active' : undefined}
>
<a
href={`#page-${item}`}
role="button"
onClick={e => {
e.preventDefault();
onPageChange(item);
}}
<button
type="button"
aria-label={`${item + 1}`}
onClick={() => onPageChange(item)}
>
{item + 1}
</a>
</button>
</li>
) : (
<li key={item} className="dt-pagination-ellipsis">

View File

@@ -363,12 +363,16 @@ function StickyWrap({
}
return (
// Virtualized/sticky table built from divs so the header/body can be
// positioned independently; a real <table> would break that layout, so
// role="table" is the correct ARIA pattern here, not the suggested tag.
<div
style={{
width: maxWidth,
height: sticky.realHeight || maxHeight,
overflow: 'hidden',
}}
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="table"
>
{headerTable}

View File

@@ -163,8 +163,11 @@ export default styled.div`
margin: 0 ${theme.marginXXS}px;
}
.dt-pagination .pagination > li > a,
.dt-pagination .pagination > li > button,
.dt-pagination .pagination > li > span {
appearance: none;
border: 1px solid transparent;
font: inherit;
background-color: ${theme.colorBgBase};
color: ${theme.colorText};
border-color: ${theme.colorBorderSecondary};
@@ -172,10 +175,10 @@ export default styled.div`
border-radius: ${theme.borderRadius}px;
}
.dt-pagination .pagination > li.active > a,
.dt-pagination .pagination > li.active > button,
.dt-pagination .pagination > li.active > span,
.dt-pagination .pagination > li.active > a:focus,
.dt-pagination .pagination > li.active > a:hover,
.dt-pagination .pagination > li.active > button:focus,
.dt-pagination .pagination > li.active > button:hover,
.dt-pagination .pagination > li.active > span:focus,
.dt-pagination .pagination > li.active > span:hover {
background-color: ${theme.colorPrimary};

View File

@@ -2334,7 +2334,12 @@ describe('plugin-chart-table', () => {
expect(screen.getByText('User 1')).toBeInTheDocument();
expect(screen.queryByText('User 11')).not.toBeInTheDocument();
const page2Link = container.querySelector('a[href="#page-1"]')!;
// The pagination bar is styled `visibility: hidden` until sticky
// height is measured, which jsdom never reports. Accessible-name
// computation treats CSS-hidden elements as nameless regardless of
// the `hidden: true` query option, so query the button directly by
// its `aria-label` instead of through the accessibility tree.
const page2Link = container.querySelector('button[aria-label="2"]')!;
expect(page2Link).toBeTruthy();
fireEvent.click(page2Link);
@@ -2381,8 +2386,8 @@ describe('plugin-chart-table', () => {
expect(screen.queryByText('User 1')).not.toBeInTheDocument();
});
const activePage = container.querySelector('li.active a')!;
expect(activePage).toHaveAttribute('href', '#page-1');
const activePage = container.querySelector('li.active button')!;
expect(activePage).toHaveTextContent('2');
});
test('should build columnLabelToNameMap for adhoc columns with custom labels', () => {

View File

@@ -71,9 +71,9 @@ test('leaves labels untouched when no format is provided', () => {
});
test('clicking a legend item toggles the category without triggering anchor navigation', () => {
// Regression proof for #33576: the legend items are href="#" anchors, so a
// click whose default action is not prevented would navigate to "#" and
// scroll the browser window to the top of the page.
// Regression proof for #33576: legend items render as real <button>
// elements (not href="#" anchors), so a click has no default navigation
// action to begin with.
const toggleCategory = jest.fn();
renderWithTheme(
<Legend
@@ -87,18 +87,18 @@ test('clicking a legend item toggles the category without triggering anchor navi
);
const legendItem = screen.getByRole('button', { name: 'Positive' });
expect(legendItem.tagName).toBe('BUTTON');
const clickEvent = createEvent.click(legendItem);
fireEvent(legendItem, clickEvent);
expect(clickEvent.defaultPrevented).toBe(true);
expect(toggleCategory).toHaveBeenCalledTimes(1);
expect(toggleCategory).toHaveBeenCalledWith('Positive');
});
test('ctrl+clicking a legend item toggles the category without opening a new tab', () => {
// Regression proof for #34157: legend items are href="#" anchors, so a
// ctrl+click whose default action is not prevented would ask the browser
// to open the "#" href in a new tab instead of just toggling the layer.
// Regression proof for #34157: legend items render as real <button>
// elements (not href="#" anchors), so a ctrl+click has no "open link in
// new tab" behavior to begin with.
const toggleCategory = jest.fn();
renderWithTheme(
<Legend
@@ -112,14 +112,12 @@ test('ctrl+clicking a legend item toggles the category without opening a new tab
);
const legendItem = screen.getByRole('button', { name: 'cat1' });
expect(legendItem.tagName).toBe('BUTTON');
const ctrlClickEvent = createEvent.click(legendItem, {
ctrlKey: true,
}) as MouseEvent;
fireEvent(legendItem, ctrlClickEvent);
// preventDefault() in the onClick handler is what stops the browser's
// native ctrl+click "open link in new tab" behavior on the anchor.
expect(ctrlClickEvent.defaultPrevented).toBe(true);
expect(ctrlClickEvent.ctrlKey).toBe(true);
expect(toggleCategory).toHaveBeenCalledTimes(1);
expect(toggleCategory).toHaveBeenCalledWith('cat1');

View File

@@ -1,6 +1,5 @@
/* eslint-disable react/jsx-sort-default-props */
/* eslint-disable react/sort-prop-types */
/* eslint-disable jsx-a11y/anchor-is-valid */
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -43,11 +42,18 @@ const StyledLegend = styled.div`
padding-left: 0;
margin: 0;
& li a {
& li button {
appearance: none;
border: none;
background: none;
font: inherit;
width: 100%;
text-align: left;
display: flex;
color: ${theme.colorText};
text-decoration: none;
padding: ${theme.sizeUnit}px 0;
cursor: pointer;
& span {
margin-right: ${theme.sizeUnit}px;
@@ -160,20 +166,13 @@ const Legend = ({
return (
<li key={k}>
<a
href="#"
role="button"
onClick={e => {
e.preventDefault();
toggleCategory(k);
}}
onDoubleClick={e => {
e.preventDefault();
showSingleCategory(k);
}}
<button
type="button"
onClick={() => toggleCategory(k)}
onDoubleClick={() => showSingleCategory(k)}
>
<span aria-hidden style={swatchStyle} /> {formatCategoryLabel(k)}
</a>
</button>
</li>
);
});

View File

@@ -43,7 +43,6 @@ const ExploreResultsButton = ({
icon={<Icons.LineChartOutlined iconSize="m" />}
onClick={onClick}
disabled={!allowsSubquery}
role="button"
tooltip={t('Create chart')}
aria-label={t('Create chart')}
data-test="explore-results-button"

View File

@@ -111,7 +111,9 @@ const TemplateParamsEditor = ({
title={t('Edit template parameters')}
trigger={['hover']}
>
<div role="button" css={{ width: 'inherit' }}>
{/* ModalTrigger's own wrapper div already provides role="button"
and the click handler; this inner div is just layout. */}
<div css={{ width: 'inherit' }}>
{t('Parameters ')}
<Badge count={paramCount} />
{!isValid && (

View File

@@ -101,7 +101,7 @@ test('shows the stop message and a re-run affordance when the query was stopped'
expect(screen.getByText('Updating chart was stopped')).toBeInTheDocument();
const rerun = screen.getByText('click here');
expect(rerun.tagName).toBe('BUTTON');
fireEvent.click(rerun);
fireEvent.keyDown(rerun, { key: 'Enter' });
expect(onQuery).toHaveBeenCalledTimes(2);
expect(onQuery).toHaveBeenCalledTimes(1);
});

View File

@@ -30,9 +30,8 @@ import {
type FilterState,
type JsonObject,
type AgGridChartState,
handleKeyboardActivation,
} from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { css, styled } from '@apache-superset/core/theme';
import type { ChartState, Datasource, ChartStatus } from 'src/explore/types';
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
import { EmptyState, Loading } from '@superset-ui/core/components';
@@ -448,14 +447,21 @@ function Chart({
description={
<span>
{t('Run a new query using the "Update chart" button or')}{' '}
<span
role="button"
tabIndex={0}
<button
type="button"
onClick={onQuery}
onKeyDown={handleKeyboardActivation(() => onQuery?.())}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
text-decoration: underline;
`}
>
{t('click here')}
</span>
</button>
.
</span>
}
@@ -490,14 +496,21 @@ function Chart({
{t(
'Click on "Create chart" button in the control panel on the left to preview a visualization or',
)}{' '}
<span
role="button"
tabIndex={0}
<button
type="button"
onClick={onQuery}
onKeyDown={handleKeyboardActivation(() => onQuery?.())}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
text-decoration: underline;
`}
>
{t('click here')}
</span>
</button>
.
</span>
}

View File

@@ -267,7 +267,7 @@ test('render breadcrumbs', async () => {
// we need to assert that there is only 1 element now
// eslint-disable-next-line jest-dom/prefer-in-document
expect(newBreadcrumbItems).toHaveLength(1);
expect(within(breadcrumbItems[0]).getByText('gender')).toBeInTheDocument();
expect(within(newBreadcrumbItems[0]).getByText('gender')).toBeInTheDocument();
});
test('should render "Edit chart" as disabled without can_explore permission', async () => {

View File

@@ -17,14 +17,7 @@
* under the License.
*/
import {
SyntheticEvent,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { t } from '@apache-superset/core/translation';
import {
BinaryQueryObjectFilterClause,
@@ -35,7 +28,6 @@ import {
isDefined,
ContextMenuFilters,
AdhocFilter,
handleKeyboardActivation,
} from '@superset-ui/core';
import { Alert } from '@apache-superset/core/components';
import { css, useTheme } from '@apache-superset/core/theme';
@@ -562,30 +554,27 @@ 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 ? (
<span data-test="drill-by-breadcrumb-item">
{route.title}
{paths}
</span>
) : (
<span
<button
type="button"
data-test="drill-by-breadcrumb-item"
role="button"
tabIndex={0}
onClick={onRouteClick}
onKeyDown={handleKeyboardActivation(() => onRouteClick?.())}
onClick={route.onClick}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
`}
>
{route.title}
</span>
</button>
);
}}
/>

View File

@@ -84,7 +84,7 @@ export const DrillBySubmenu = ({
const [debouncedSearchInput, setDebouncedSearchInput] = useState('');
const [popoverOpen, setPopoverOpen] = useState(false);
const ref = useRef<InputRef>(null);
const menuItemRef = useRef<HTMLDivElement>(null);
const menuItemRef = useRef<HTMLButtonElement>(null);
const columns = useMemo(
() => (dataset ? ensureIsArray(dataset.drillable_columns) : []),
@@ -284,11 +284,18 @@ export const DrillBySubmenu = ({
);
const menuItem = (
<div
<button
type="button"
ref={menuItemRef}
role="button"
aria-disabled={isDisabled}
tabIndex={isDisabled ? -1 : 0}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
@@ -299,12 +306,6 @@ export const DrillBySubmenu = ({
}
`}
onClick={() => !isDisabled && setPopoverOpen(!popoverOpen)}
onKeyDown={e => {
if (!isDisabled && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
setPopoverOpen(!popoverOpen);
}
}}
>
<span>{t('Drill by')}</span>
{isDisabled ? (
@@ -312,7 +313,7 @@ export const DrillBySubmenu = ({
) : (
<Icons.RightOutlined iconSize="s" iconColor={theme.colorTextTertiary} />
)}
</div>
</button>
);
if (isDisabled) {

View File

@@ -58,11 +58,19 @@ const DownloadDropdown = ({
}}
>
<Tooltip title={t('Download')}>
<span
tabIndex={0}
role="button"
<button
type="button"
aria-label={t('Download')}
data-test="drill-detail-download-btn"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
display: inline-flex;
align-items: center;
`}
>
<Icons.DownloadOutlined
iconColor={theme.colorIcon}
@@ -73,7 +81,7 @@ const DownloadDropdown = ({
}
`}
/>
</span>
</button>
</Tooltip>
</Dropdown>
);

View File

@@ -158,11 +158,12 @@ export default function TableControls({
</Tooltip>
)}
<Tooltip title={t('Reload')}>
{/* role is auto-computed by BaseIconComponent as "button" since
onClick is present, so no explicit role needed here. */}
<Icons.ReloadOutlined
iconColor={theme.colorIcon}
iconSize="l"
aria-label={t('Reload')}
role="button"
tabIndex={0}
onClick={onReload}
/>

View File

@@ -31,7 +31,7 @@ export const InteractiveCopyToClipboard = ({ copyNode, ...rest }: any) => {
if (copyNode === 'Icon') {
node = <Icons.CopyOutlined />;
} else if (copyNode === 'Text') {
node = <span role="button">Copy</span>;
node = <button type="button">Copy</button>;
}
return (
<>

View File

@@ -100,7 +100,7 @@ function CopyToClip({
}
const handleKeyDown = disabled
? undefined
: (event: KeyboardEvent<HTMLSpanElement>) => {
: (event: KeyboardEvent<HTMLButtonElement>) => {
if (event.key === 'Enter' || event.key === ' ') {
// Prevent space-scroll when the wrapper is focused.
event.preventDefault();
@@ -108,16 +108,23 @@ function CopyToClip({
}
};
return (
<span
<button
type="button"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
`}
style={{ cursor }}
onClick={disabled ? undefined : onClick}
onKeyDown={handleKeyDown}
role="button"
aria-disabled={disabled || undefined}
tabIndex={disabled ? -1 : 0}
>
{copyNode}
</span>
</button>
);
}, [copyNode, disabled, onClick]);

View File

@@ -84,7 +84,12 @@ const ConfirmModalStyled = styled.div`
}
`;
const StyledSpan = styled.span`
const StyledSpan = styled.button`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
color: ${({ theme }) => theme.colorPrimaryText};
&: hover {
@@ -202,8 +207,7 @@ const ChangeDatasourceModal: FunctionComponent<ChangeDatasourceModalProps> = ({
{
Cell: ({ row: { original } }: any) => (
<StyledSpan
role="button"
tabIndex={0}
type="button"
data-test="datasource-link"
onClick={() => selectDatasource({ type: 'table', ...original })}
>

View File

@@ -446,11 +446,12 @@ export default function CRUDCollection({
color: ${theme.colorTextTertiary};
`}
>
{/* role is auto-computed by BaseIconComponent as "button" since
onClick is present, so no explicit role needed here. */}
<Icons.DeleteOutlined
aria-label={t('Delete item')}
className="pointer"
data-test="crud-delete-icon"
role="button"
tabIndex={0}
onClick={() => deleteItem(record.id)}
iconSize="l"

View File

@@ -36,7 +36,6 @@ import {
SupersetClient,
getClientErrorObject,
getExtensionsRegistry,
handleKeyboardActivation,
} from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { t } from '@apache-superset/core/translation';
@@ -1754,14 +1753,17 @@ function DatasourceEditor({
() => (
<div>
<EditLockContainer>
<span
<button
type="button"
css={themeParam => css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
color: ${themeParam.colorTextTertiary};
`}
role="button"
tabIndex={0}
onClick={onChangeEditMode}
onKeyDown={handleKeyboardActivation(onChangeEditMode)}
>
{isEditMode ? (
<Icons.UnlockOutlined
@@ -1778,7 +1780,7 @@ function DatasourceEditor({
})}
/>
)}
</span>
</button>
{!isEditMode && <div>{t('Click the lock to make changes.')}</div>}
{isEditMode && (
<div>{t('Click the lock to prevent further changes.')}</div>

View File

@@ -26,6 +26,7 @@ jest.mock(
'@superset-ui/core/components/Icons/AsyncIcon',
() =>
({ fileName }: { fileName: string }) => (
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
<span role="img" aria-label={fileName.replace('_', '-')} />
),
);

View File

@@ -25,6 +25,7 @@ jest.mock(
'@superset-ui/core/components/Icons/AsyncIcon',
() =>
({ fileName }: { fileName: string }) => (
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
<span role="img" aria-label={fileName.replace('_', '-')} />
),
);

View File

@@ -25,6 +25,7 @@ jest.mock(
'@superset-ui/core/components/Icons/AsyncIcon',
() =>
({ fileName }: { fileName: string }) => (
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
<span role="img" aria-label={fileName.replace('_', '-')} />
),
);

View File

@@ -16,11 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { handleKeyboardActivation } from '@superset-ui/core';
import { useState } from 'react';
import { t } from '@apache-superset/core/translation';
import { Alert } from '@apache-superset/core/components';
import { useTheme } from '@apache-superset/core/theme';
import { css, useTheme } from '@apache-superset/core/theme';
import {
Icons,
Modal,
@@ -100,15 +99,21 @@ export const ErrorAlert: React.FC<ErrorAlertProps> = ({
{descriptionDetails}
</Typography.Paragraph>
)}
<span
role="button"
tabIndex={0}
<button
type="button"
onClick={toggleDescription}
onKeyDown={handleKeyboardActivation(toggleDescription)}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
color: inherit;
`}
style={{ textDecoration: 'underline', cursor: 'pointer' }}
>
{isDescriptionVisible ? t('See less') : t('See more')}
</span>
</button>
</div>
)}
{children}
@@ -129,14 +134,19 @@ export const ErrorAlert: React.FC<ErrorAlertProps> = ({
return (
<>
<Tooltip title={`${errorType}: ${message}`}>
<span
role="button"
<button
type="button"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
`}
onClick={() => setShowModal(true)}
onKeyDown={handleKeyboardActivation(() => setShowModal(true))}
tabIndex={0}
>
{renderTrigger()}
</span>
</button>
</Tooltip>
<Modal
name={errorType}

View File

@@ -26,6 +26,7 @@ jest.mock(
'@superset-ui/core/components/Icons/AsyncIcon',
() =>
({ fileName }: { fileName: string }) => (
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
<span role="img" aria-label={fileName.replace('_', '-')} />
),
);

View File

@@ -25,6 +25,7 @@ jest.mock(
'@superset-ui/core/components/Icons/AsyncIcon',
() =>
({ fileName }: { fileName: string }) => (
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
<span role="img" aria-label={fileName.replace('_', '-')} />
),
);

View File

@@ -25,6 +25,7 @@ jest.mock(
'@superset-ui/core/components/Icons/AsyncIcon',
() =>
({ fileName }: { fileName: string }) => (
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
<span role="img" aria-label={fileName.replace('_', '-')} />
),
);

View File

@@ -25,6 +25,7 @@ jest.mock(
'@superset-ui/core/components/Icons/AsyncIcon',
() =>
({ fileName }: { fileName: string }) => (
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role -- mirrors AsyncIcon's real span+role="img" shape
<span role="img" aria-label={fileName.replace('_', '-')} />
),
);

View File

@@ -259,6 +259,11 @@ function CompactSelectPanel(
/>
</SearchRow>
)}
{/* Custom-rendered, searchable, keyboard-navigable option list with
rich per-option content (active state, icons); native <select>/
<option> can't render that, so the ARIA listbox pattern is used
instead of the tag the linter suggests. */}
{/* eslint-disable-next-line jsx-a11y/prefer-tag-over-role */}
<OptionList role="listbox" aria-label={t('Filter options')}>
{isLoading ? (
<StatusText>{t('Loading...')}</StatusText>
@@ -275,6 +280,8 @@ function CompactSelectPanel(
<OptionItem
key={opt.value}
$active={isActive}
// See the OptionList comment above: native <option> can't render this.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="option"
aria-selected={isActive}
tabIndex={0}

View File

@@ -16,13 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import {
render,
screen,
within,
waitFor,
fireEvent,
} from 'spec/helpers/testing-library';
import { render, screen, within, waitFor } 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';
@@ -366,7 +360,7 @@ describe('ListView', () => {
expect(mockedPropsComprehensive.fetchData).toHaveBeenCalled();
});
test('switches view mode via keyboard activation of the toggle buttons', () => {
test('switches view mode via click of the toggle buttons', async () => {
const { container } = factory({
renderCard: jest.fn(),
data: [],
@@ -379,10 +373,10 @@ describe('ListView', () => {
expect(screen.getByTestId('empty-state')).toHaveClass('card');
fireEvent.keyDown(tableToggle, { key: 'Enter' });
await userEvent.click(tableToggle);
expect(screen.getByTestId('empty-state')).toHaveClass('table');
fireEvent.keyDown(cardToggle, { key: ' ' });
await userEvent.click(cardToggle);
expect(screen.getByTestId('empty-state')).toHaveClass('card');
});
});

View File

@@ -16,10 +16,9 @@
* 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';
import { css, styled } from '@apache-superset/core/theme';
import {
useCallback,
useEffect,
@@ -185,10 +184,15 @@ const ViewModeContainer = styled.div`
display: inline-block;
.toggle-button {
appearance: none;
border: none;
background: none;
font: inherit;
display: inline-block;
border-radius: ${theme.borderRadius}px;
padding: ${theme.sizeUnit}px;
padding-bottom: ${theme.sizeUnit * 0.5}px;
cursor: pointer;
&:first-of-type {
margin-right: ${theme.sizeUnit * 2}px;
@@ -205,6 +209,15 @@ const ViewModeContainer = styled.div`
`}
`;
const inlineTextButtonCss = css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
`;
const ClearAllButton = styled.button`
${({ theme }) => `
background: none;
@@ -248,34 +261,30 @@ const ViewModeToggle = ({
}) => (
<ViewModeContainer>
<Tooltip title={t('Grid view')}>
<div
role="button"
tabIndex={0}
<button
type="button"
aria-pressed={mode === 'card'}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.blur();
setMode('card');
}}
onKeyDown={handleKeyboardActivation(() => setMode('card'))}
className={cx('toggle-button', { active: mode === 'card' })}
>
<Icons.AppstoreOutlined iconSize="xl" />
</div>
</button>
</Tooltip>
<Tooltip title={t('List view')}>
<div
role="button"
tabIndex={0}
<button
type="button"
aria-pressed={mode === 'table'}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget.blur();
setMode('table');
}}
onKeyDown={handleKeyboardActivation(() => setMode('table'))}
className={cx('toggle-button', { active: mode === 'table' })}
>
<Icons.UnorderedListOutlined iconSize="xl" />
</div>
</button>
</Tooltip>
</ViewModeContainer>
);
@@ -505,19 +514,15 @@ export function ListView<T extends object = any>({
</div>
{Boolean(selectedFlatRows.length) && (
<>
<span
<button
type="button"
data-test="bulk-select-deselect-all"
style={{ cursor: 'pointer' }}
role="button"
tabIndex={0}
css={inlineTextButtonCss}
className="deselect-all"
onClick={() => toggleAllRowsSelected(false)}
onKeyDown={handleKeyboardActivation(() =>
toggleAllRowsSelected(false),
)}
>
{t('Deselect all')}
</span>
</button>
<div className="divider" />
{bulkActions
.filter(
@@ -543,19 +548,15 @@ export function ListView<T extends object = any>({
</Button>
))}
{enableBulkTag && (
<span
<button
type="button"
data-test="bulk-select-tag-btn"
role="button"
style={{ cursor: 'pointer' }}
tabIndex={0}
css={inlineTextButtonCss}
className="tag-btn"
onClick={() => setShowBulkTagModal(true)}
onKeyDown={handleKeyboardActivation(() =>
setShowBulkTagModal(true),
)}
>
{t('Add Tag')}
</span>
</button>
)}
</>
)}

View File

@@ -152,10 +152,11 @@ export default function Toast({ toast, onCloseToast }: ToastPresenterProps) {
{icon}
<Interweave content={toast.text} noHtml={!toast.allowHtml} />
</div>
{/* role is auto-computed by BaseIconComponent as "button" since
onClick is present, so no explicit role needed here. */}
<Icons.CloseOutlined
iconSize="m"
className="toast__close pointer"
role="button"
tabIndex={0}
onClick={handleClosePress}
aria-label={t('Close')}

View File

@@ -162,6 +162,10 @@ export const StatusIndicatorDot: FC<StatusIndicatorDotProps> = ({
return (
<span
css={dotStyles}
// role="status" is the standard WAI-ARIA live-region pattern for a
// status indicator; <output> (the suggested tag) is for form
// calculation results, not a fit here.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="status"
aria-label={`Auto-refresh status: ${displayStatus}`}
data-test="status-indicator-dot"

View File

@@ -295,6 +295,10 @@ export const CustomizationsBadge = ({ chartId }: CustomizationsBadgeProps) => {
<StyledTag
ref={triggerRef}
aria-label={t('Display controls (%s)', customizationsCount)}
// Tag doesn't support a polymorphic `as` prop, and this element has
// no click action of its own (Tooltip attaches its own hover/focus
// handlers to it) - tabIndex + role keep it keyboard-discoverable.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
tabIndex={0}
>

View File

@@ -23,7 +23,7 @@ import type { IconType } from '@superset-ui/core/components/Icons/types';
import IconButton from './IconButton';
type DeleteComponentButtonProps = {
onDelete: MouseEventHandler<HTMLDivElement>;
onDelete: MouseEventHandler<HTMLButtonElement>;
iconSize?: IconType['iconSize'];
};

View File

@@ -37,7 +37,7 @@ const mockPopoverTriggerRef = {
current: {
focus: jest.fn(),
},
} as unknown as RefObject<HTMLDivElement>;
} as unknown as RefObject<HTMLButtonElement>;
const createProps = () => ({
popoverVisible: true,

View File

@@ -39,7 +39,7 @@ export interface DetailsPanelProps {
children: JSX.Element;
popoverVisible: boolean;
popoverContentRef: RefObject<HTMLDivElement>;
popoverTriggerRef: RefObject<HTMLDivElement>;
popoverTriggerRef: RefObject<HTMLButtonElement>;
setPopoverVisible: (visible: boolean) => void;
}

View File

@@ -55,8 +55,11 @@ export interface FiltersBadgeProps {
chartId: number;
}
const StyledFilterCount = styled.div`
const StyledFilterCount = styled.button`
${({ theme }) => `
appearance: none;
border: none;
font: inherit;
display: flex;
justify-items: center;
align-items: center;
@@ -135,7 +138,7 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
);
const [popoverVisible, setPopoverVisible] = useState(false);
const popoverContentRef = useRef<HTMLDivElement>(null);
const popoverTriggerRef = useRef<HTMLDivElement>(null);
const popoverTriggerRef = useRef<HTMLButtonElement>(null);
const onHighlightFilterSource = useCallback(
(path: string[]) => {
@@ -144,7 +147,7 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
[dispatch],
);
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
if (event.key === 'Enter') {
setPopoverVisible(true);
}
@@ -306,15 +309,14 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
popoverTriggerRef={popoverTriggerRef}
>
<StyledFilterCount
type="button"
aria-label={t('Applied filters (%s)', filterCount)}
aria-haspopup="true"
role="button"
ref={popoverTriggerRef}
className={cx(
'filter-counts',
!!appliedCrossFilterIndicators.length && 'has-cross-filters',
)}
tabIndex={0}
onKeyDown={handleKeyDown}
>
<Icons.FilterOutlined iconSize="m" />

View File

@@ -19,10 +19,10 @@
import { forwardRef, HTMLAttributes, MouseEventHandler } from 'react';
import { styled, SupersetTheme } from '@apache-superset/core/theme';
interface IconButtonProps extends HTMLAttributes<HTMLDivElement> {
interface IconButtonProps extends HTMLAttributes<HTMLButtonElement> {
icon: JSX.Element;
label?: string;
onClick: MouseEventHandler<HTMLDivElement>;
onClick: MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
'data-test'?: string;
}
@@ -39,7 +39,11 @@ const activeCss = ({ theme }: { theme: SupersetTheme }) => `
}
`;
const StyledDiv = styled.div<{ isDisabled?: boolean }>`
const StyledButton = styled.button<{ isDisabled?: boolean }>`
appearance: none;
border: none;
background: none;
font: inherit;
display: flex;
align-items: center;
cursor: pointer;
@@ -54,7 +58,7 @@ const StyledSpan = styled.span`
margin-left: ${({ theme }) => theme.sizeUnit * 2}px;
`;
const IconButton = forwardRef<HTMLDivElement, IconButtonProps>(
const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
(
{
icon,
@@ -67,11 +71,10 @@ const IconButton = forwardRef<HTMLDivElement, IconButtonProps>(
},
ref,
) => (
<StyledDiv
<StyledButton
{...rest}
ref={ref}
tabIndex={disabled ? -1 : 0}
role="button"
type="button"
isDisabled={disabled}
aria-disabled={disabled}
data-test={dataTest}
@@ -89,7 +92,7 @@ const IconButton = forwardRef<HTMLDivElement, IconButtonProps>(
>
{icon}
{label && <StyledSpan>{label}</StyledSpan>}
</StyledDiv>
</StyledButton>
),
);

View File

@@ -170,7 +170,7 @@ const createOwnStateWithChartState = (
const Chart = (props: ChartProps) => {
const dispatch = useDispatch();
const descriptionRef = useRef<HTMLDivElement>(null);
const descriptionRef = useRef<HTMLElement>(null);
const headerRef = useRef<HTMLDivElement>(null);
const boundActionCreators = useMemo(
@@ -756,14 +756,13 @@ const Chart = (props: ChartProps) => {
https://github.com/apache/superset/pull/23862
*/}
{isExpanded && slice.description_markdown && (
<div
<aside
className="slice_description bs-callout bs-callout-default"
ref={descriptionRef}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: slice.description_markdown,
}}
role="complementary"
/>
)}

View File

@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { handleKeyboardActivation } from '@superset-ui/core';
import {
Fragment,
useCallback,
@@ -28,7 +27,7 @@ import {
} from 'react';
import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux';
import { styled } from '@apache-superset/core/theme';
import { css, styled } from '@apache-superset/core/theme';
import { t } from '@apache-superset/core/translation';
import { EditableTitle, EmptyState } from '@superset-ui/core/components';
@@ -348,16 +347,21 @@ const Tab = (props: TabProps): ReactElement => {
) : (
<span>
{t('You can add the components in the')}{' '}
<span
role="button"
tabIndex={0}
<button
type="button"
onClick={() => dispatch(setEditMode(true))}
onKeyDown={handleKeyboardActivation(() =>
dispatch(setEditMode(true)),
)}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
text-decoration: underline;
`}
>
{t('edit mode')}
</span>
</button>
</span>
))
}

View File

@@ -98,8 +98,6 @@ interface ShowDropIndicatorsResult {
interface CloseIconWithDropIndicatorProps {
showDropIndicators: ShowDropIndicatorsResult;
role?: string;
tabIndex?: number;
}
const CloseIconWithDropIndicator = (
@@ -481,9 +479,9 @@ const Tabs = (props: TabsProps): ReactElement => {
</>
),
closeIcon: (
// rc-tabs already wraps closeIcon content in its own
// <button role="tab" aria-label="remove">.
<CloseIconWithDropIndicator
role="button"
tabIndex={tabIndex}
showDropIndicators={showDropIndicators(tabIndex)}
/>
),

View File

@@ -80,7 +80,8 @@ const CrossFilterChartTitle = (props: {
<StyledIconSearch
iconSize="s"
data-test="cross-filters-highlight-emitter"
role="button"
// role is auto-computed by BaseIconComponent as "button" whenever
// onClick is present, so no explicit role here.
tabIndex={0}
aria-label={t('Locate the chart')}
onClick={onHighlightFilterSource}

View File

@@ -177,11 +177,8 @@ test('Uses callbacks on click', () => {
expect(DEFAULT_PROPS.removeCustomScope).toHaveBeenCalledWith(4);
});
test('Renders charts scoping list panel with FilterTitle rendered with role="button"', () => {
test('Renders charts scoping list panel with FilterTitle rendered as a button', () => {
setup();
expect(screen.getByText('All charts/global scoping')).toBeVisible();
expect(screen.getByText('All charts/global scoping')).toHaveAttribute(
'role',
'button',
);
expect(screen.getByText('All charts/global scoping').tagName).toBe('BUTTON');
});

View File

@@ -141,8 +141,16 @@ export const ChartsScopingListPanel = ({
</Button>
</AddButtonContainer>
<FilterTitle
role="button"
tabIndex={0}
as="button"
{...{ type: 'button' }}
css={css`
appearance: none;
border: none;
background: none;
font: inherit;
text-align: left;
width: 100%;
`}
onClick={() => setCurrentChartId(undefined)}
className={activeChartId === undefined ? 'active' : ''}
>

View File

@@ -47,6 +47,12 @@ const CrossFiltersVerticalCollapse = (props: {
const sectionHeaderStyle = useCallback(
(theme: SupersetTheme) => css`
appearance: none;
border: none;
background: none;
font: inherit;
text-align: left;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
@@ -119,21 +125,10 @@ const CrossFiltersVerticalCollapse = (props: {
return (
<div css={sectionContainerStyle}>
{!hideHeader && (
<div
css={sectionHeaderStyle}
onClick={toggleSection}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSection();
}
}}
role="button"
tabIndex={0}
>
<button type="button" css={sectionHeaderStyle} onClick={toggleSection}>
<h4 css={sectionTitleStyle}>{t('Cross-filters')}</h4>
<Icons.UpOutlined iconSize="m" css={iconStyle(isOpen, theme)} />
</div>
</button>
)}
{isOpen && <div css={sectionContentStyle}>{crossFiltersIndicators}</div>}
{isOpen && <div css={dividerStyle} data-test="cross-filters-divider" />}

View File

@@ -16,8 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { handleKeyboardActivation } from '@superset-ui/core';
import { ReactNode, FC, memo } from 'react';
import { css } from '@apache-superset/core/theme';
import { getFilterBarTestId } from '../utils';
@@ -30,15 +30,20 @@ export const FilterConfigurationLink: FC<FCBProps> = ({
onClick,
children,
}) => (
<div
<button
type="button"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
`}
{...getFilterBarTestId('create-filter')}
onClick={onClick}
onKeyDown={onClick ? handleKeyboardActivation(onClick) : undefined}
role="button"
tabIndex={0}
>
{children}
</div>
</button>
);
export default memo(FilterConfigurationLink);

View File

@@ -236,6 +236,10 @@ export const DescriptionToolTip = ({
whiteSpace: 'normal',
}}
>
{/* Deliberate role="button" on this tooltip-trigger icon (no click
handler) — mirrors DeckglLayerVisibilityTooltip below; not a fit
for the linter's suggested <button> tag. */}
{/* eslint-disable-next-line jsx-a11y/prefer-tag-over-role */}
<StyledInfoCircleOutlined className="text-muted" role="button" />
</Tooltip>
</ToolTipContainer>
@@ -249,8 +253,12 @@ export const DeckglLayerVisibilityTooltip = () => (
)}
placement="right"
>
{/* Deliberate role="button" on this tooltip-trigger icon (no click
handler) — covered by existing test expectations; not a fit for
the linter's suggested <button> tag. */}
<StyledInfoCircleOutlined
className="text-muted"
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
data-test="deckgl-layer-visibility-tooltip-icon"
/>

View File

@@ -104,7 +104,13 @@ const SectionContainer = styled.div`
margin-bottom: ${({ theme }) => theme.sizeUnit * 3}px;
`;
const SectionHeader = styled.div`
const SectionHeader = styled.button`
appearance: none;
border: none;
background: none;
font: inherit;
text-align: left;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
@@ -317,15 +323,9 @@ const FilterControls: FC<FilterControlsProps> = ({
<SectionContainer>
{!hideHeader && (
<SectionHeader
type="button"
aria-expanded={sectionsOpen.filters}
onClick={() => toggleSection('filters')}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSection('filters');
}
}}
role="button"
tabIndex={0}
>
<Title
level={5}
@@ -361,15 +361,9 @@ const FilterControls: FC<FilterControlsProps> = ({
<SectionContainer>
{!hideHeader && (
<SectionHeader
type="button"
aria-expanded={sectionsOpen.chartCustomization}
onClick={() => toggleSection('chartCustomization')}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSection('chartCustomization');
}
}}
role="button"
tabIndex={0}
>
<Title
level={5}

View File

@@ -203,6 +203,11 @@ const DescriptionTooltip = ({ description }: { description: string }) => (
>
<Icons.InfoCircleOutlined
className="text-muted"
// Deliberate role="button" override on this tooltip-trigger icon
// (no click handler; BaseIconComponent would otherwise auto-compute
// role="img" since there's no onClick) — matches the same pattern
// used by DescriptionToolTip/DeckglLayerVisibilityTooltip.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
css={theme => ({
paddingLeft: `${theme.sizeUnit}px`,

View File

@@ -31,6 +31,12 @@ const sectionContainerStyle = (theme: SupersetTheme) => css`
`;
const sectionHeaderStyle = (theme: SupersetTheme) => css`
appearance: none;
border: none;
background: none;
font: inherit;
text-align: left;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
@@ -104,24 +110,18 @@ const UrlFiltersVerticalCollapse = (props: {
return (
<div css={sectionContainerStyle}>
<div
<button
type="button"
css={sectionHeaderStyle}
aria-expanded={isOpen}
onClick={toggleSection}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSection();
}
}}
role="button"
tabIndex={0}
>
<h4 css={sectionTitleStyle}>
<Icons.LinkOutlined iconSize="s" />
{t('URL Filters')}
</h4>
<Icons.UpOutlined iconSize="m" css={iconStyle(isOpen, theme)} />
</div>
</button>
{isOpen && <div css={sectionContentStyle}>{filterIndicators}</div>}
{isOpen && <div css={dividerStyle} data-test="url-filters-divider" />}
</div>

View File

@@ -88,8 +88,12 @@ const Bar = styled.div<{ width: number }>`
`}
`;
const CollapsedBar = styled.div<{ offset: number }>`
const CollapsedBar = styled.button<{ offset: number }>`
${({ theme, offset }) => `
appearance: none;
border: none;
background: none;
font: inherit;
position: absolute;
top: ${offset}px;
left: 0;
@@ -262,11 +266,10 @@ const VerticalFilterBar: FC<VerticalBarProps> = ({
width={width}
>
<CollapsedBar
type="button"
{...getFilterBarTestId('collapsable')}
className={cx({ open: !filtersOpen })}
onClick={openFiltersBar}
role="button"
tabIndex={0}
offset={offset}
>
<Icons.VerticalAlignTopOutlined

View File

@@ -46,7 +46,7 @@ const DependencyValue = ({
return (
<span>
{hasSeparator && <span>, </span>}
<DependencyItem role="button" onClick={handleClick} tabIndex={0}>
<DependencyItem type="button" onClick={handleClick}>
{dependency.name}
</DependencyItem>
</span>

View File

@@ -64,7 +64,12 @@ export const FilterName = styled.span`
white-space: nowrap;
`;
export const DependencyItem = styled.span`
export const DependencyItem = styled.button`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
text-decoration: underline;
cursor: pointer;
`;

View File

@@ -16,11 +16,10 @@
* 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';
import { styled } from '@apache-superset/core/theme';
import { css, styled } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import type { DragEndEvent } from '@dnd-kit/core';
import {
@@ -191,19 +190,25 @@ const FilterTitleContainer = forwardRef<HTMLDivElement, Props>(
<StyledWarning className="warning" iconSize="s" />
)}
{isRemoved && (
<span
css={{ alignSelf: 'flex-end', marginLeft: 'auto' }}
role="button"
<button
type="button"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
align-self: flex-end;
margin-left: auto;
`}
data-test="undo-button"
tabIndex={0}
onClick={e => {
e.preventDefault();
restoreFilter(id);
}}
onKeyDown={handleKeyboardActivation(() => restoreFilter(id))}
>
{t('Undo?')}
</span>
</button>
)}
</div>
<div css={{ alignSelf: 'flex-start', marginLeft: 'auto' }}>

View File

@@ -41,8 +41,13 @@ const MainPanel = styled.div`
flex-direction: column;
`;
const AddFilter = styled.div`
const AddFilter = styled.button`
${({ theme }) => `
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
display: inline-flex;
flex-direction: row;
align-items: center;
@@ -175,7 +180,7 @@ const List = ({
/>
))}
{availableFilters.length > rows.length && (
<AddFilter role="button" tabIndex={0} onClick={onAdd}>
<AddFilter type="button" onClick={onAdd}>
<Icons.PlusOutlined iconSize="xs" />
{t('Add filter')}
</AddFilter>

View File

@@ -16,11 +16,10 @@
* 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';
import { styled } from '@apache-superset/core/theme';
import { css, styled } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import { useDndMonitor } from '@dnd-kit/core';
import {
@@ -159,19 +158,25 @@ const ItemTitleContainer = forwardRef<HTMLDivElement, Props>(
<StyledWarning className="warning" iconSize="s" />
)}
{isRemoved && (
<span
css={{ alignSelf: 'flex-end', marginLeft: 'auto' }}
role="button"
<button
type="button"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
align-self: flex-end;
margin-left: auto;
`}
data-test="undo-button"
tabIndex={0}
onClick={e => {
e.preventDefault();
restoreItem(id);
}}
onKeyDown={handleKeyboardActivation(() => restoreItem(id))}
>
{t('Undo?')}
</span>
</button>
)}
</div>
<div css={{ alignSelf: 'flex-start', marginLeft: 'auto' }}>

View File

@@ -136,7 +136,12 @@ const ControlHeader: FC<ControlHeaderProps> = ({
htmlFor={name}
>
{leftNode && <span>{leftNode} </span>}
{/* This label text sits inside FormLabel's <label>, and HTML5
prohibits interactive content (including <button>) as a
descendant of <label>, so a real button tag isn't an option
here. */}
<span
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
tabIndex={0}
onClick={onClick}

View File

@@ -65,11 +65,20 @@ export const CopyToClipboardButton = ({
disabled={disabled}
wrapped={false}
copyNode={
<span
role="button"
<button
type="button"
aria-label={t('Copy')}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
display: inline-flex;
align-items: center;
`}
>
<Icons.CopyOutlined
iconColor={theme.colorIcon}
@@ -82,7 +91,7 @@ export const CopyToClipboardButton = ({
}
`}
/>
</span>
</button>
}
/>
);

View File

@@ -18,12 +18,8 @@
*/
import { useCallback, useEffect, useMemo, useState, MouseEvent } from 'react';
import { t } from '@apache-superset/core/translation';
import {
isFeatureEnabled,
FeatureFlag,
handleKeyboardActivation,
} from '@superset-ui/core';
import { styled } from '@apache-superset/core/theme';
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
import { css, styled } from '@apache-superset/core/theme';
import { Icons } from '@superset-ui/core/components/Icons';
import Tabs from '@superset-ui/core/components/Tabs';
import {
@@ -184,30 +180,31 @@ export const DataTablesPane = ({
) : (
<Icons.DownOutlined aria-label={t('Expand data panel')} />
);
const resetButtonCss = css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
`;
return (
<div>
{panelOpen ? (
<span
role="button"
tabIndex={0}
<button
type="button"
css={resetButtonCss}
onClick={() => handleCollapseChange(false)}
onKeyDown={handleKeyboardActivation(() =>
handleCollapseChange(false),
)}
>
{caretIcon}
</span>
</button>
) : (
<span
role="button"
tabIndex={0}
<button
type="button"
css={resetButtonCss}
onClick={() => handleCollapseChange(true)}
onKeyDown={handleKeyboardActivation(() =>
handleCollapseChange(true),
)}
>
{caretIcon}
</span>
</button>
)}
</div>
);

View File

@@ -135,11 +135,12 @@ export const TableControls = ({
)}
{onReload && (
<Tooltip title={t('Reload')}>
{/* role is auto-computed by BaseIconComponent as "button" since
onClick is present, so no explicit role needed here. */}
<Icons.ReloadOutlined
iconColor={theme.colorIcon}
iconSize="l"
aria-label={t('Reload')}
role="button"
tabIndex={0}
onClick={onReload}
/>

View File

@@ -18,12 +18,7 @@
*/
import { useContext, useDeferredValue, useMemo, useState } from 'react';
import { t } from '@apache-superset/core/translation';
import {
DatasourceType,
Metric,
QueryFormData,
handleKeyboardActivation,
} from '@superset-ui/core';
import { DatasourceType, Metric, QueryFormData } from '@superset-ui/core';
import { Alert } from '@apache-superset/core/components';
import { css, styled, useTheme } from '@apache-superset/core/theme';
@@ -291,17 +286,21 @@ export default function DataSourcePanel({
message=""
description={
<>
<span
role="button"
tabIndex={0}
<button
type="button"
onClick={() => setShowSaveDatasetModal(true)}
onKeyDown={handleKeyboardActivation(() =>
setShowSaveDatasetModal(true),
)}
className="add-dataset-alert-description"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
cursor: pointer;
`}
>
{t('Create a dataset')}
</span>
</button>
{t(' to edit or add columns and metrics.')}
</>
}

View File

@@ -106,9 +106,19 @@ const EmbedCodeContent: FC<EmbedCodeContentProps> = ({
shouldShowText={false}
text={html}
copyNode={
<span role="button" aria-label={t('Copy to clipboard')}>
<button
type="button"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
`}
aria-label={t('Copy to clipboard')}
>
<Icons.CopyOutlined />
</span>
</button>
}
/>
<Input.TextArea

View File

@@ -30,7 +30,6 @@ 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';
@@ -393,17 +392,22 @@ const ExploreChartPanel = ({
{t(
'This chart type is not supported when using an unsaved query as a chart source. ',
)}
<span
role="button"
tabIndex={0}
<button
type="button"
onClick={() => setShowDatasetModal(true)}
onKeyDown={handleKeyboardActivation(() =>
setShowDatasetModal(true),
)}
css={{ textDecoration: 'underline' }}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
color: inherit;
text-decoration: underline;
cursor: pointer;
`}
>
{t('Create a dataset')}
</span>
</button>
{t(' to visualize your data.')}
</>
}
@@ -424,14 +428,21 @@ 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',
)}{' '}
<span
role="button"
tabIndex={0}
<button
type="button"
onClick={onQuery}
onKeyDown={handleKeyboardActivation(() => onQuery?.())}
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
color: inherit;
cursor: pointer;
`}
>
{t('click here')}
</span>
</button>
.
</span>
)

View File

@@ -37,7 +37,6 @@ import {
MatrixifyFormData,
DatasourceType,
ensureIsArray,
handleKeyboardActivation,
} from '@superset-ui/core';
import {
ControlStateMapping,
@@ -49,7 +48,7 @@ import { logging } from '@apache-superset/core/utils';
import { debounce, isEqual, isObjectLike, omit, pick } from 'lodash-es';
import { Resizable } from 're-resizable';
import { useHistory } from 'react-router-dom';
import { Tooltip } from '@superset-ui/core/components';
import { ActionButton, Tooltip } from '@superset-ui/core/components';
import { usePluginContext } from 'src/components';
import { Global } from '@emotion/react';
import { Icons } from '@superset-ui/core/components/Icons';
@@ -994,22 +993,20 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
>
<div className="title-container">
<span className="horizontal-text">{t('Chart Source')}</span>
<span
role="button"
tabIndex={0}
className="action-button"
<ActionButton
label={t('Collapse Datasource panel')}
icon={
<Icons.VerticalAlignTopOutlined
iconSize="xl"
css={css`
transform: rotate(-90deg);
`}
className="collapse-icon"
iconColor={theme.colorPrimary}
/>
}
onClick={toggleCollapse}
onKeyDown={handleKeyboardActivation(toggleCollapse)}
>
<Icons.VerticalAlignTopOutlined
iconSize="xl"
css={css`
transform: rotate(-90deg);
`}
className="collapse-icon"
iconColor={theme.colorPrimary}
/>
</span>
/>
</div>
{/* eslint-disable @typescript-eslint/no-explicit-any -- DataSourcePanel uses narrower types that are compatible at runtime */}
<DataSourcePanel
@@ -1022,16 +1019,22 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
{/* eslint-enable @typescript-eslint/no-explicit-any */}
</Resizable>
{isCollapsed ? (
<div
<button
type="button"
className="sidebar"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
margin: 0;
font: inherit;
`}
onClick={toggleCollapse}
onKeyDown={handleKeyboardActivation(toggleCollapse)}
data-test="open-datasource-tab"
role="button"
tabIndex={0}
aria-label={t('Open Datasource tab')}
>
<span role="button" tabIndex={0} className="action-button">
<span className="action-button">
<Tooltip title={t('Open Datasource tab')}>
<Icons.VerticalAlignTopOutlined
iconSize="xl"
@@ -1043,7 +1046,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
/>
</Tooltip>
</span>
</div>
</button>
) : null}
<Resizable
onResizeStop={(evt, direction, ref, d) =>

View File

@@ -194,6 +194,10 @@ const ColorBreakpointsPopoverControl = ({
};
return (
// Rendered as antd Popover content, whose visibility antd itself
// manages (mount/unmount); native <dialog> requires showModal()/the
// open attribute and would render hidden without it.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
<div role="dialog">
<StyledRow>
<Col flex="1">

View File

@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { handleKeyboardActivation } from '@superset-ui/core';
import { useMemo, useState } from 'react';
import { t } from '@apache-superset/core/translation';
import { useTheme } from '@apache-superset/core/theme';
@@ -167,10 +166,14 @@ export default function ColumnConfigControl<T extends ColumnConfig>({
/>
))}
{needShowMoreButton && (
<div
role="button"
tabIndex={-1}
<button
type="button"
css={{
appearance: 'none',
border: 'none',
background: 'none',
font: 'inherit',
width: '100%',
padding: theme.sizeUnit * 2,
textAlign: 'center',
cursor: 'pointer',
@@ -181,9 +184,6 @@ export default function ColumnConfigControl<T extends ColumnConfig>({
},
}}
onClick={() => setShowAllColumns(!showAllColumns)}
onKeyDown={handleKeyboardActivation(() =>
setShowAllColumns(!showAllColumns),
)}
>
{showAllColumns ? (
<>
@@ -194,7 +194,7 @@ export default function ColumnConfigControl<T extends ColumnConfig>({
<Icons.DownOutlined /> &nbsp; {t('Show all columns')}
</>
)}
</div>
</button>
)}
</div>
</>

View File

@@ -31,11 +31,14 @@ export type DateLabelProps = {
onClick?: (event: MouseEvent) => void;
};
const LabelContainer = styled.div<{
const LabelContainer = styled.button<{
isActive?: boolean;
isPlaceholder?: boolean;
}>`
${({ theme, isActive, isPlaceholder }) => css`
appearance: none;
font: inherit;
width: 100%;
height: ${theme.sizeUnit * 8}px;
display: flex;
@@ -80,7 +83,7 @@ const LabelContainer = styled.div<{
export const DateLabel = forwardRef(
(props: DateLabelProps, ref: RefObject<HTMLSpanElement>) => (
<LabelContainer {...props} tabIndex={0} role="button">
<LabelContainer type="button" {...props}>
<span
id={`date-label-${props.name}`}
className="date-label-content"

View File

@@ -35,7 +35,6 @@ 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';
@@ -91,6 +90,16 @@ const MetricLabel = styled.span`
color: ${({ theme }) => theme.colorText};
`;
const inlineTextButtonCss = css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
color: inherit;
cursor: pointer;
`;
export interface ColumnSelectPopoverProps {
columns: ColumnMeta[];
editedColumn?: ColumnMeta | AdhocColumn;
@@ -473,30 +482,24 @@ const ColumnSelectPopover = ({
description={
isTemporal ? (
<>
<span
role="button"
tabIndex={0}
<button
type="button"
css={inlineTextButtonCss}
onClick={setDatasetAndClose}
onKeyDown={handleKeyboardActivation(
setDatasetAndClose,
)}
>
{t('Create a dataset')}
</span>{' '}
</button>{' '}
{t(' to mark a column as a time column')}
</>
) : (
<>
<span
role="button"
tabIndex={0}
<button
type="button"
css={inlineTextButtonCss}
onClick={setDatasetAndClose}
onKeyDown={handleKeyboardActivation(
setDatasetAndClose,
)}
>
{t('Create a dataset')}
</span>{' '}
</button>{' '}
{t(' to add calculated columns')}
</>
)
@@ -524,16 +527,13 @@ const ColumnSelectPopover = ({
)
) : (
<>
<span
role="button"
tabIndex={0}
<button
type="button"
css={inlineTextButtonCss}
onClick={setDatasetAndClose}
onKeyDown={handleKeyboardActivation(
setDatasetAndClose,
)}
>
{t('Create a dataset')}
</span>{' '}
</button>{' '}
{t(' to mark a column as a time column')}
</>
)

View File

@@ -16,10 +16,9 @@
* 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';
import { css, styled, useTheme } from '@apache-superset/core/theme';
import { Input, Tooltip } from '@superset-ui/core/components';
import { Icons } from '@superset-ui/core/components/Icons';
@@ -90,17 +89,22 @@ export const DndColumnSelectPopoverTitle = ({
/>
) : (
<Tooltip placement="top" title={t('Click to edit label')}>
<span
<button
type="button"
css={css`
appearance: none;
border: none;
background: none;
padding: 0;
font: inherit;
`}
className="AdhocMetricEditPopoverTitle inline-editable"
data-test="AdhocMetricEditTitle#trigger"
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
onFocus={onMouseOver}
onClick={onClick}
onKeyDown={onClick ? handleKeyboardActivation(onClick) : undefined}
onBlur={onBlur}
role="button"
tabIndex={0}
>
{title || defaultLabel}
&nbsp;
@@ -108,7 +112,7 @@ export const DndColumnSelectPopoverTitle = ({
iconColor={isHovered ? theme.colorPrimary : theme.colorText}
iconSize="m"
/>
</span>
</button>
</Tooltip>
);
};

View File

@@ -54,11 +54,10 @@ export default function Option({
<OptionControlContainer data-test="option-label" withCaret={withCaret}>
{canDelete && (
<CloseContainer
type="button"
css={css`
text-align: center;
`}
role="button"
tabIndex={0}
data-test="remove-control-button"
onClick={onClickClose}
>

View File

@@ -134,6 +134,11 @@ export default function useResizeButton(
return [
<Icons.ArrowsAltOutlined
// Drag-to-resize handle activated via mousedown, not click; there's
// no keyboard equivalent, so role="button" (which implies a
// click/Enter/Space-activatable control) isn't quite right, but no
// native tag fits a drag handle either.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
aria-label={t('Resize')}
tabIndex={0}

View File

@@ -24,6 +24,7 @@ import {
type ReactNode,
} from 'react';
import { SupersetClient, ensureIsArray } from '@superset-ui/core';
import { css } from '@apache-superset/core/theme';
import { logging } from '@apache-superset/core/utils';
import { t } from '@apache-superset/core/translation';
import ControlHeader from 'src/explore/components/ControlHeader';
@@ -412,7 +413,17 @@ function AdhocFilterControl({
? values.map((val, index) => valueRenderer(val, index))
: []),
addNewFilterPopoverTrigger(
<AddControlLabel role="button" data-test="add-filter-button">
<AddControlLabel
as="button"
{...{ type: 'button' }}
css={css`
appearance: none;
background: none;
font: inherit;
text-align: left;
`}
data-test="add-filter-button"
>
<Icons.PlusOutlined iconSize="m" />
{t('Add filter')}
</AddControlLabel>,

View File

@@ -418,6 +418,11 @@ function AdhocFilterEditPopover({
{t('Save')}
</Button>
<Icons.ArrowsAltOutlined
// Drag-to-resize handle activated via mousedown, not click; there's
// no keyboard equivalent, so role="button" (which implies a
// click/Enter/Space-activatable control) isn't quite right, but no
// native tag fits a drag handle either.
// eslint-disable-next-line jsx-a11y/prefer-tag-over-role
role="button"
aria-label={t('Resize')}
tabIndex={0}

Some files were not shown because too many files have changed in this diff Show More