chore(Accessibility): Improve keyboard navigation and screen access (#33396)

This commit is contained in:
Geido
2025-06-13 19:29:10 +03:00
committed by GitHub
parent e6f7c12e88
commit 0d3eebd221
27 changed files with 233 additions and 70 deletions

View File

@@ -189,8 +189,10 @@ export function interceptFilterState() {
export function setFilter(filter: string, option: string) {
interceptFiltering();
cy.get(`[aria-label="${filter}"]`).first().click();
cy.get(`[aria-label="${filter}"] [title="${option}"]`).click();
cy.get(`[aria-label^="${filter}"]`).first().click();
cy.get(`.ant-select-item-option[title="${option}"]`).first().click({
force: true,
});
cy.wait('@filtering');
}
@@ -346,8 +348,10 @@ export function addParentFilterWithValue(index: number, value: string) {
return cy
.get(nativeFilters.filterConfigurationSections.displayedSection)
.within(() => {
cy.get('input[aria-label="Limit type"]').eq(index).click({ force: true });
cy.get('input[aria-label="Limit type"]')
cy.get('input[aria-label^="Limit type"]')
.eq(index)
.click({ force: true });
cy.get('input[aria-label^="Limit type"]')
.eq(index)
.type(`${value}{enter}`, { delay: 30, force: true });
});

View File

@@ -154,7 +154,7 @@ describe('Test explore links', () => {
// This time around, typing the same dashboard name
// will select the existing one
cy.get('[data-test="save-chart-modal-select-dashboard-form"]')
.find('input[aria-label="Select a dashboard"]')
.find('input[aria-label^="Select a dashboard"]')
.type(`${dashboardTitle}{enter}`, { force: true });
cy.get(`.ant-select-item[label="${dashboardTitle}"]`).click({

View File

@@ -61,8 +61,10 @@ export function interceptExploreGet() {
export function setFilter(filter: string, option: string) {
interceptFiltering();
cy.get(`[aria-label="${filter}"]`).first().click();
cy.get(`[aria-label="${filter}"] [title="${option}"]`).click();
cy.get(`[aria-label^="${filter}"]`).first().click();
cy.get(`.ant-select-item-option[title="${option}"]`).first().click({
force: true,
});
cy.wait('@filtering');
}
@@ -76,17 +78,18 @@ export function saveChartToDashboard(dashboardName: string) {
.should('be.enabled')
.should('not.be.disabled')
.click();
cy.getBySelLike('chart-modal').should('be.visible');
cy.get(
'[data-test="save-chart-modal-select-dashboard-form"] [aria-label="Select a dashboard"]',
)
.first()
.click();
cy.get(
'.ant-select-selection-search-input[aria-label="Select a dashboard"]',
).type(dashboardName, { force: true });
cy.get(`.ant-select-item-option[title="${dashboardName}"]`).click();
cy.getBySel('btn-modal-save').click();
cy.getBySelLike('chart-modal')
.should('be.visible')
.within(() => {
cy.get('[data-test="save-chart-modal-select-dashboard-form"]')
.first()
.click();
cy.get('.ant-select-selection-search-input').type(dashboardName, {
force: true,
});
cy.get(`.ant-select-item-option[title="${dashboardName}"]`).click();
cy.getBySel('btn-modal-save').click();
});
cy.wait('@update');
cy.wait('@get');

View File

@@ -94,9 +94,8 @@ export function ControlHeader({
<label className="control-label" htmlFor={name}>
{leftNode && <span>{leftNode}</span>}
<span
role="button"
tabIndex={0}
onClick={onClick}
role={onClick ? 'button' : undefined}
{...(onClick ? { onClick, tabIndex: 0 } : {})}
className={labelClass}
style={{ cursor: onClick ? 'pointer' : '' }}
>

View File

@@ -17,7 +17,7 @@
* under the License.
*/
import { ReactNode } from 'react';
import { JsonValue, useTheme } from '@superset-ui/core';
import { JsonValue, t, useTheme } from '@superset-ui/core';
import { ControlHeader } from '../../components/ControlHeader';
// [value, label]
@@ -69,17 +69,24 @@ export default function RadioButtonControl({
boxShadow: 'none',
},
}}
role="tablist"
aria-label={typeof props.label === 'string' ? props.label : undefined}
>
<ControlHeader {...props} />
<div className="btn-group btn-group-sm">
{options.map(([val, label]) => (
<button
aria-label={typeof label === 'string' ? label : undefined}
id={`tab-${val}`}
key={JSON.stringify(val)}
type="button"
aria-selected={val === currentValue}
role="tab"
className={`btn btn-default ${
val === currentValue ? 'active' : ''
}`}
onClick={() => {
onClick={e => {
e.currentTarget?.focus();
onChange(val);
}}
>
@@ -87,6 +94,23 @@ export default function RadioButtonControl({
</button>
))}
</div>
{/* accessibility begin */}
<div
aria-live="polite"
style={{
position: 'absolute',
left: '-9999px',
height: '1px',
width: '1px',
overflow: 'hidden',
}}
>
{t(
'%s tab selected',
options.find(([val]) => val === currentValue)?.[1],
)}
</div>
{/* accessibility end */}
</div>
);
}

View File

@@ -129,7 +129,7 @@ test('table should be visible when expanded is true', async () => {
name: 'Select database or type to search databases',
});
const schemaSelect = getByRole('combobox', {
name: 'Select schema or type to search schemas',
name: 'Select schema or type to search schemas: main',
});
const tableSelect = getAllByLabelText(
/Select table or type to search tables/i,

View File

@@ -211,7 +211,7 @@ test('Refresh should work', async () => {
expect(fetchMock.calls(schemaApiRoute).length).toBe(0);
const select = screen.getByRole('combobox', {
name: 'Select schema or type to search schemas',
name: 'Select schema or type to search schemas: public',
});
userEvent.click(select);
@@ -324,7 +324,7 @@ test('Should schema select display options', async () => {
const props = createProps();
render(<DatabaseSelector {...props} />, { useRedux: true, store });
const select = screen.getByRole('combobox', {
name: 'Select schema or type to search schemas',
name: 'Select schema or type to search schemas: public',
});
expect(select).toBeInTheDocument();
userEvent.click(select);
@@ -370,7 +370,7 @@ test('Sends the correct schema when changing the schema', async () => {
rerender(<DatabaseSelector {...props} />);
expect(props.onSchemaChange).toHaveBeenCalledTimes(0);
const select = screen.getByRole('combobox', {
name: 'Select schema or type to search schemas',
name: 'Select schema or type to search schemas: public',
});
expect(select).toBeInTheDocument();
userEvent.click(select);

View File

@@ -429,6 +429,10 @@ function ColumnCollectionTable({
).is_dttm;
return (
<Radio
aria-label={t(
'Set %s as default datetime column',
record.column_name,
)}
data-test={`radio-default-dttm-${record.column_name}`}
checked={checked}
disabled={disabled}
@@ -479,6 +483,10 @@ function ColumnCollectionTable({
).is_dttm;
return (
<Radio
aria-label={t(
'Set %s as default datetime column',
record.column_name,
)}
data-test={`radio-default-dttm-${record.column_name}`}
checked={checked}
disabled={disabled}

View File

@@ -71,6 +71,7 @@ export interface ModalProps {
maskClosable?: boolean;
zIndex?: number;
bodyStyle?: CSSProperties;
openerRef?: React.RefObject<HTMLElement>;
}
interface StyledModalProps {
@@ -276,22 +277,34 @@ const CustomModal = ({
resizableConfig = defaultResizableConfig(hideFooter),
draggableConfig,
destroyOnClose,
openerRef,
...rest
}: ModalProps) => {
const draggableRef = useRef<HTMLDivElement>(null);
const [bounds, setBounds] = useState<DraggableBounds>();
const [dragDisabled, setDragDisabled] = useState<boolean>(true);
const handleOnHide = () => {
openerRef?.current?.focus();
onHide();
};
let FooterComponent;
if (isValidElement(footer)) {
// If a footer component is provided inject a closeModal function
// so the footer can provide a "close" button if desired
FooterComponent = cloneElement(footer, {
closeModal: onHide,
closeModal: handleOnHide,
} as Partial<unknown>);
}
const modalFooter = isNil(FooterComponent)
? [
<Button key="back" onClick={onHide} cta data-test="modal-cancel-button">
<Button
key="back"
onClick={handleOnHide}
cta
data-test="modal-cancel-button"
>
{t('Cancel')}
</Button>,
<Button
@@ -350,7 +363,7 @@ const CustomModal = ({
<StyledModal
centered={!!centered}
onOk={onHandledPrimaryAction}
onCancel={onHide}
onCancel={handleOnHide}
width={modalWidth}
maxWidth={maxWidth}
responsive={responsive}

View File

@@ -31,7 +31,8 @@ export function Item({ active, children, onClick }: PaginationItemButton) {
<li className={classNames({ active })}>
<span
role="button"
tabIndex={active ? -1 : 0}
tabIndex={0}
aria-current={active ? 'page' : undefined}
onClick={e => {
e.preventDefault();
if (!active) onClick(e);

View File

@@ -99,7 +99,8 @@ const getElementByClassName = (className: string) =>
const getElementsByClassName = (className: string) =>
document.querySelectorAll(className)! as NodeListOf<HTMLElement>;
const getSelect = () => screen.getByRole('combobox', { name: ARIA_LABEL });
const getSelect = () =>
screen.getByRole('combobox', { name: new RegExp(ARIA_LABEL, 'i') });
const getAllSelectOptions = () =>
getElementsByClassName('.ant-select-item-option-content');

View File

@@ -600,7 +600,14 @@ const AsyncSelect = forwardRef(
)}
<StyledSelect
allowClear={!isLoading && allowClear}
aria-label={ariaLabel || name}
aria-label={
isSingleMode &&
isLabeledValue(selectValue) &&
typeof selectValue.label === 'string'
? `${ariaLabel || name}: ${selectValue.label}`
: ariaLabel || name
}
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
dropdownRender={dropdownRender}
filterOption={handleFilterOption}

View File

@@ -82,7 +82,8 @@ const getElementByClassName = (className: string) =>
const getElementsByClassName = (className: string) =>
document.querySelectorAll(className)! as NodeListOf<HTMLElement>;
const getSelect = () => screen.getByRole('combobox', { name: ARIA_LABEL });
const getSelect = () =>
screen.getByRole('combobox', { name: new RegExp(ARIA_LABEL, 'i') });
const selectAllButtonText = (length: number) => `Select all (${length})`;
const deselectAllButtonText = (length: number) => `Deselect all (${length})`;

View File

@@ -675,7 +675,14 @@ const Select = forwardRef(
<StyledSelect
id={name}
allowClear={!isLoading && allowClear}
aria-label={ariaLabel}
aria-label={
isSingleMode &&
isLabeledValue(selectValue) &&
typeof selectValue.label === 'string'
? `${ariaLabel || name}: ${selectValue.label}`
: ariaLabel || name
}
data-test={ariaLabel || name}
autoClearSearchValue={autoClearSearchValue}
dropdownRender={dropdownRender}
filterOption={handleFilterOption}

View File

@@ -46,6 +46,10 @@ export const StyledSelect = styled(AntdSelect, {
shouldForwardProp: prop => prop !== 'headerPosition' && prop !== 'oneLine',
})<{ headerPosition?: string; oneLine?: boolean }>`
${({ theme, headerPosition, oneLine }) => `
.ant-select-item-option-active:not(.ant-select-item-option-disabled) {
outline: 2px solid ${theme.colors.primary.base};
outline-offset: -2px;
}
flex: ${headerPosition === 'left' ? 1 : 0};
&& .ant-select-selector {
border-radius: ${theme.gridUnit}px;

View File

@@ -163,12 +163,31 @@ export const dropDownRenderHelper = (
if (errorComponent) {
return errorComponent;
}
// remap for accessibility for proper item count
const accessibilityNode = {
...originNode,
props: {
...originNode.props,
flattenOptions: ensureIsArray(originNode.props.flattenOptions).map(
(opt: Record<string, any>, idx: number) => ({
...opt,
data: {
...opt.data,
'aria-setsize': originNode.props.flattenOptions?.length || 0,
'aria-posinset': idx + 1,
},
}),
),
},
};
return (
<>
{helperText && (
<StyledHelperText role="note">{helperText}</StyledHelperText>
)}
{originNode}
{accessibilityNode}
{bulkSelectComponents}
</>
);

View File

@@ -93,7 +93,7 @@ test('renders with default props', async () => {
name: 'Select database or type to search databases',
});
const schemaSelect = screen.getByRole('combobox', {
name: 'Select schema or type to search schemas',
name: 'Select schema or type to search schemas: test_schema',
});
const tableSelect = screen.getByRole('combobox', {
name: 'Select table or type to search tables',

View File

@@ -55,6 +55,7 @@ export default function TimezoneSelector({
onTimezoneChange,
timezone,
minWidth = MIN_SELECT_WIDTH, // smallest size for current values
...rest
}: TimezoneSelectorProps) {
const { TIMEZONE_OPTIONS, TIMEZONE_OPTIONS_SORT_COMPARATOR, validTimezone } =
useMemo(() => {
@@ -156,6 +157,7 @@ export default function TimezoneSelector({
value={validTimezone}
options={TIMEZONE_OPTIONS}
sortComparator={TIMEZONE_OPTIONS_SORT_COMPARATOR}
{...rest}
/>
);
}

View File

@@ -140,6 +140,7 @@ class TextAreaControl extends Component {
defaultValue={this.props.initialValue}
disabled={this.props.readOnly}
style={{ height: this.props.height }}
aria-required={this.props['aria-required']}
/>
</div>
);

View File

@@ -222,7 +222,7 @@ describe('VizTypeControl', () => {
const visualizations = screen.getByTestId(getTestId('viz-row'));
userEvent.click(screen.getByRole('button', { name: 'ballot All charts' }));
userEvent.click(screen.getByRole('tab', { name: 'All charts' }));
expect(
await within(visualizations).findByText('Line Chart'),
@@ -247,7 +247,7 @@ describe('VizTypeControl', () => {
it('Submit on viz type double-click', async () => {
await waitForRenderWrapper();
userEvent.click(screen.getByRole('button', { name: 'ballot All charts' }));
userEvent.click(screen.getByRole('tab', { name: 'All charts' }));
const visualizations = screen.getByTestId(getTestId('viz-row'));
userEvent.click(within(visualizations).getByText('Bar Chart'));

View File

@@ -418,11 +418,15 @@ const Selector: FC<{
return (
<SelectorLabel
aria-label={selector}
aria-selected={isSelected}
ref={btnRef}
key={selector}
name={selector}
className={cx(className, isSelected && 'selected')}
onClick={() => onClick(selector, sectionId)}
tabIndex={0}
role="tab"
>
{icon}
{selector}
@@ -656,7 +660,7 @@ export default function VizTypeGallery(props: VizTypeGalleryProps) {
className={className}
isSelectedVizMetadata={Boolean(selectedVizMetadata)}
>
<LeftPane>
<LeftPane aria-label={t('Choose chart type')} role="tablist">
<Selector
css={({ gridUnit }) =>
// adjust style for not being inside a collapse

View File

@@ -72,6 +72,7 @@ import {
import { useSelector } from 'react-redux';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { Icons } from 'src/components/Icons';
import { useOpenerRef } from 'src/hooks/useOpenerRef';
import NumberInput from './components/NumberInput';
import { AlertReportCronScheduler } from './components/AlertReportCronScheduler';
import { NotificationMethod } from './components/NotificationMethod';
@@ -431,6 +432,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
isReport = false,
addSuccessToast,
}) => {
const openerRef = useOpenerRef(show);
const currentUser = useSelector<any, UserWithPermissionsAndRoles>(
state => state.user,
);
@@ -1465,6 +1467,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
width="500px"
centered
title={<h4 data-test="alert-report-modal-title">{getTitleText()}</h4>}
openerRef={openerRef}
>
<Collapse
expandIconPosition="right"
@@ -1504,6 +1507,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
isReport ? t('Enter report name') : t('Enter alert name')
}
onChange={onInputChange}
aria-required="true"
/>
</div>
</StyledInputContainer>
@@ -1515,6 +1519,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
<div data-test="owners-select" className="input-container">
<AsyncSelect
ariaLabel={t('Owners')}
aria-required="true"
allowClear
name="owners"
mode="multiple"
@@ -1581,6 +1586,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
<div className="input-container">
<AsyncSelect
ariaLabel={t('Database')}
aria-required="true"
name="source"
placeholder={t('Select database')}
value={
@@ -1617,6 +1623,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
readOnly={false}
initialValue={resource?.sql}
key={currentAlert?.id}
aria-required="true"
/>
</StyledInputContainer>
<div className="inline-container wrap">
@@ -1628,6 +1635,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
<div className="input-container">
<Select
ariaLabel={t('Condition')}
aria-required="true"
onChange={onConditionChange}
placeholder={t('Condition')}
value={currentAlert?.validator_config_json?.op || undefined}
@@ -1654,6 +1662,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
}
placeholder={t('Value')}
onChange={onThresholdChange}
aria-required="true"
/>
</div>
</StyledInputContainer>
@@ -1688,6 +1697,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
value={contentType}
options={CONTENT_TYPE_OPTIONS}
placeholder={t('Select content type')}
aria-required="true"
/>
</StyledInputContainer>
<StyledInputContainer>
@@ -1699,6 +1709,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
</div>
<AsyncSelect
ariaLabel={t('Chart')}
aria-required="true"
name="chart"
value={
currentAlert?.chart?.label && currentAlert?.chart?.value
@@ -1721,6 +1732,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
</div>
<AsyncSelect
ariaLabel={t('Dashboard')}
aria-required="true"
name="dashboard"
value={
currentAlert?.dashboard?.label &&
@@ -1751,6 +1763,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
</div>
<Select
ariaLabel={t('Select format')}
aria-required="true"
onChange={onFormatChange}
value={reportFormat}
options={
@@ -1845,6 +1858,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
onTimezoneChange={onTimezoneChange}
timezone={currentAlert?.timezone}
minWidth="100%"
aria-required="true"
/>
</StyledInputContainer>
<StyledInputContainer>
@@ -1855,6 +1869,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
<div className="input-container">
<Select
ariaLabel={t('Log retention')}
aria-required="true"
placeholder={t('Log retention')}
onChange={onLogRetentionChange}
value={currentAlert?.log_retention}
@@ -1878,6 +1893,7 @@ const AlertReportModal: FunctionComponent<AlertReportModalProps> = ({
placeholder={t('Time in seconds')}
onChange={onTimeoutVerifyChange}
timeUnit={t('seconds')}
aria-required="true"
/>
</div>
</>

View File

@@ -34,6 +34,7 @@ export default function NumberInput({
value,
placeholder,
onChange,
...rest
}: NumberInputProps) {
const [isFocused, setIsFocused] = useState<boolean>(false);
@@ -47,6 +48,7 @@ export default function NumberInput({
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
onChange={onChange}
{...rest}
/>
);
}

View File

@@ -120,6 +120,8 @@ type MenuChild = {
usesRouter?: boolean;
onClick?: () => void;
'data-test'?: string;
id?: string;
'aria-controls'?: string;
};
export interface ButtonProps {
@@ -196,20 +198,22 @@ const SubMenuComponent: FunctionComponent<SubMenuProps> = props => {
<StyledHeader>
<Row className="menu" role="navigation">
{props.name && <div className="header">{props.name}</div>}
<Menu mode={showMenu} disabledOverflow>
<Menu mode={showMenu} disabledOverflow role="tablist">
{props.tabs?.map(tab => {
if ((props.usesRouter || hasHistory) && !!tab.usesRouter) {
return (
<Menu.Item key={tab.label}>
<div
<Link
to={tab.url || ''}
role="tab"
id={tab.id || tab.name}
data-test={tab['data-test']}
aria-selected={tab.name === props.activeChild}
aria-controls={tab['aria-controls'] || ''}
className={tab.name === props.activeChild ? 'active' : ''}
>
<div>
<Link to={tab.url || ''}>{tab.label}</Link>
</div>
</div>
{tab.label}
</Link>
</Menu.Item>
);
}
@@ -221,6 +225,7 @@ const SubMenuComponent: FunctionComponent<SubMenuProps> = props => {
active: tab.name === props.activeChild,
})}
role="tab"
aria-selected={tab.name === props.activeChild}
>
<a href={tab.url} onClick={tab.onClick}>
{tab.label}

View File

@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { useEffect, useRef } from 'react';
export function useOpenerRef(active: boolean) {
const openerRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (active) {
openerRef.current = document.activeElement as HTMLElement;
}
}, [active]);
return openerRef;
}

View File

@@ -559,6 +559,8 @@ function AlertList({
url: '/alert/list/',
usesRouter: true,
'data-test': 'alert-list',
id: 'alert-tab',
'aria-controls': 'alert-list',
},
{
name: 'Reports',
@@ -566,6 +568,8 @@ function AlertList({
url: '/report/list/',
usesRouter: true,
'data-test': 'report-list',
id: 'report-tab',
'aria-controls': 'report-list',
},
]}
buttons={subMenuButtons}
@@ -623,24 +627,30 @@ function AlertList({
]
: [];
return (
<ListView<AlertObject>
className="alerts-list-view"
columns={columns}
count={alertsCount}
data={alerts}
emptyState={emptyState}
fetchData={fetchData}
filters={filters}
initialSort={initialSort}
loading={loading}
bulkActions={bulkActions}
bulkSelectEnabled={bulkSelectEnabled}
disableBulkSelect={toggleBulkSelect}
refreshData={refreshData}
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
pageSize={PAGE_SIZE}
/>
<div
id={isReportEnabled ? 'report-list' : 'alert-list'}
role="tabpanel"
aria-labelledby={isReportEnabled ? 'report-tab' : 'alert-tab'}
>
<ListView<AlertObject>
className="alerts-list-view"
columns={columns}
count={alertsCount}
data={alerts}
emptyState={emptyState}
fetchData={fetchData}
filters={filters}
initialSort={initialSort}
loading={loading}
bulkActions={bulkActions}
bulkSelectEnabled={bulkSelectEnabled}
disableBulkSelect={toggleBulkSelect}
refreshData={refreshData}
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
pageSize={PAGE_SIZE}
/>
</div>
);
}}
</ConfirmStatusChange>

View File

@@ -152,8 +152,8 @@ test('renders an enabled button if datasource and viz type are selected', async
userEvent.click(await screen.findByText(/test_db/i));
userEvent.click(
screen.getByRole('button', {
name: /ballot all charts/i,
screen.getByRole('tab', {
name: /All charts/i,
}),
);
userEvent.click(await screen.findByText('Table'));
@@ -167,8 +167,8 @@ test('double-click viz type does nothing if no datasource is selected', async ()
await renderComponent();
userEvent.click(
screen.getByRole('button', {
name: /ballot all charts/i,
screen.getByRole('tab', {
name: /All charts/i,
}),
);
userEvent.dblClick(await screen.findByText('Table'));
@@ -187,8 +187,8 @@ test('double-click viz type submits with formatted URL if datasource is selected
userEvent.click(await screen.findByText(/test_db/i));
userEvent.click(
screen.getByRole('button', {
name: /ballot all charts/i,
screen.getByRole('tab', {
name: /All charts/i,
}),
);
userEvent.dblClick(await screen.findByText('Table'));