mirror of
https://github.com/apache/superset.git
synced 2026-07-27 09:02:29 +00:00
Compare commits
6 Commits
fix/issue-
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
490d284d75 | ||
|
|
6856d0fc67 | ||
|
|
8f8331f656 | ||
|
|
e54eccd5fb | ||
|
|
710037d3d2 | ||
|
|
90040fc1f6 |
2
.github/workflows/bump-python-package.yml
vendored
2
.github/workflows/bump-python-package.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 })}
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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('_', '-')} />
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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'];
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ const mockPopoverTriggerRef = {
|
||||
current: {
|
||||
focus: jest.fn(),
|
||||
},
|
||||
} as unknown as RefObject<HTMLDivElement>;
|
||||
} as unknown as RefObject<HTMLButtonElement>;
|
||||
|
||||
const createProps = () => ({
|
||||
popoverVisible: true,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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' : ''}
|
||||
>
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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.')}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 /> {t('Show all columns')}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -108,7 +112,7 @@ export const DndColumnSelectPopoverTitle = ({
|
||||
iconColor={isHovered ? theme.colorPrimary : theme.colorText}
|
||||
iconSize="m"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user