You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by di...@apache.org on 2025/05/09 12:39:04 UTC

(superset) 01/01: chore(Accessibility): Improve keyboard navigation and screen access

This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch geido/chore/accessibility-phase-1
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 0f094e33e2020b6d32fffdf31575b9b7966299c9
Author: Diego Pucci <di...@gmail.com>
AuthorDate: Fri May 9 14:37:35 2025 +0200

    chore(Accessibility): Improve keyboard navigation and screen access
---
 .../src/components/ControlHeader.tsx               |  6 +--
 .../components/RadioButtonControl.tsx              | 23 ++++++++++-
 .../src/components/Datasource/DatasourceEditor.jsx |  2 +
 superset-frontend/src/components/Modal/Modal.tsx   | 19 +++++++--
 .../src/components/Pagination/Item.tsx             |  3 +-
 .../src/components/Select/AsyncSelect.tsx          |  6 ++-
 superset-frontend/src/components/Select/Select.tsx |  6 ++-
 superset-frontend/src/components/Select/styles.tsx |  4 ++
 superset-frontend/src/components/Select/utils.tsx  | 21 +++++++++-
 .../src/components/TimezoneSelector/index.tsx      |  2 +
 .../components/controls/TextAreaControl.jsx        |  1 +
 .../controls/VizTypeControl/VizTypeGallery.tsx     |  6 ++-
 .../src/features/alerts/AlertReportModal.tsx       | 16 ++++++++
 .../src/features/alerts/components/NumberInput.tsx |  2 +
 superset-frontend/src/features/home/SubMenu.tsx    | 17 +++++---
 .../Pagination/Item.tsx => hooks/useOpenerRef.ts}  | 32 +++++----------
 .../src/pages/AlertReportList/index.tsx            | 46 +++++++++++++---------
 17 files changed, 154 insertions(+), 58 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/components/ControlHeader.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/components/ControlHeader.tsx
index 44e47d55e9..e1968f91a9 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/components/ControlHeader.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/components/ControlHeader.tsx
@@ -94,9 +94,9 @@ 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 } : {})}
+            {...(onClick ? { tabIndex: 0 } : {})}
             className={labelClass}
             style={{ cursor: onClick ? 'pointer' : '' }}
           >
diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx
index 3af514f171..13e19427c3 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx
@@ -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,20 @@ export default function RadioButtonControl({
           </button>
         ))}
       </div>
+      {/* accessibility begin */}
+      <div
+        aria-live="polite"
+        style={{
+          position: 'absolute',
+          left: '-9999px',
+          height: '1px',
+          width: '1px',
+          overflow: 'hidden',
+        }}
+      >
+        {options.find(([val]) => val === currentValue)?.[1]} tab selected
+      </div>
+      {/* accessibility end */}
     </div>
   );
 }
diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
index dce962a6ce..ab6e7fed15 100644
--- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
+++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx
@@ -426,6 +426,7 @@ 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}
@@ -476,6 +477,7 @@ 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}
diff --git a/superset-frontend/src/components/Modal/Modal.tsx b/superset-frontend/src/components/Modal/Modal.tsx
index a521586086..0be298cf5c 100644
--- a/superset-frontend/src/components/Modal/Modal.tsx
+++ b/superset-frontend/src/components/Modal/Modal.tsx
@@ -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 = null,
   ...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}
diff --git a/superset-frontend/src/components/Pagination/Item.tsx b/superset-frontend/src/components/Pagination/Item.tsx
index d214499f9e..b71da3c939 100644
--- a/superset-frontend/src/components/Pagination/Item.tsx
+++ b/superset-frontend/src/components/Pagination/Item.tsx
@@ -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);
diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx
index 4615f93418..5e832e14bf 100644
--- a/superset-frontend/src/components/Select/AsyncSelect.tsx
+++ b/superset-frontend/src/components/Select/AsyncSelect.tsx
@@ -600,7 +600,11 @@ const AsyncSelect = forwardRef(
         )}
         <StyledSelect
           allowClear={!isLoading && allowClear}
-          aria-label={ariaLabel || name}
+          aria-label={
+            isSingleMode && isLabeledValue(selectValue)
+              ? `${ariaLabel || name}: ${selectValue.label}`
+              : ariaLabel || name
+          }
           autoClearSearchValue={autoClearSearchValue}
           dropdownRender={dropdownRender}
           filterOption={handleFilterOption}
diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx
index 71ca926632..d91a225c2e 100644
--- a/superset-frontend/src/components/Select/Select.tsx
+++ b/superset-frontend/src/components/Select/Select.tsx
@@ -673,7 +673,11 @@ const Select = forwardRef(
         <StyledSelect
           id={name}
           allowClear={!isLoading && allowClear}
-          aria-label={ariaLabel}
+          aria-label={
+            isSingleMode && isLabeledValue(selectValue)
+              ? `${ariaLabel || name}: ${selectValue.label}`
+              : ariaLabel || name
+          }
           autoClearSearchValue={autoClearSearchValue}
           dropdownRender={dropdownRender}
           filterOption={handleFilterOption}
diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx
index 966d9ade42..a56f349386 100644
--- a/superset-frontend/src/components/Select/styles.tsx
+++ b/superset-frontend/src/components/Select/styles.tsx
@@ -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;
diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx
index 528d9249df..3db5cdb24f 100644
--- a/superset-frontend/src/components/Select/utils.tsx
+++ b/superset-frontend/src/components/Select/utils.tsx
@@ -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}
     </>
   );
diff --git a/superset-frontend/src/components/TimezoneSelector/index.tsx b/superset-frontend/src/components/TimezoneSelector/index.tsx
index 4a3c69ca08..d815b94b29 100644
--- a/superset-frontend/src/components/TimezoneSelector/index.tsx
+++ b/superset-frontend/src/components/TimezoneSelector/index.tsx
@@ -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}
     />
   );
 }
diff --git a/superset-frontend/src/explore/components/controls/TextAreaControl.jsx b/superset-frontend/src/explore/components/controls/TextAreaControl.jsx
index e8f165c8e1..171735c2ce 100644
--- a/superset-frontend/src/explore/components/controls/TextAreaControl.jsx
+++ b/superset-frontend/src/explore/components/controls/TextAreaControl.jsx
@@ -131,6 +131,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>
     );
diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
index 13512e5d8c..66ba957789 100644
--- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
+++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx
@@ -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
diff --git a/superset-frontend/src/features/alerts/AlertReportModal.tsx b/superset-frontend/src/features/alerts/AlertReportModal.tsx
index ba2e9db945..062f5602d7 100644
--- a/superset-frontend/src/features/alerts/AlertReportModal.tsx
+++ b/superset-frontend/src/features/alerts/AlertReportModal.tsx
@@ -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>
               </>
diff --git a/superset-frontend/src/features/alerts/components/NumberInput.tsx b/superset-frontend/src/features/alerts/components/NumberInput.tsx
index 2975640547..1254e576dc 100644
--- a/superset-frontend/src/features/alerts/components/NumberInput.tsx
+++ b/superset-frontend/src/features/alerts/components/NumberInput.tsx
@@ -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}
     />
   );
 }
diff --git a/superset-frontend/src/features/home/SubMenu.tsx b/superset-frontend/src/features/home/SubMenu.tsx
index dd03fba9d1..83296066da 100644
--- a/superset-frontend/src/features/home/SubMenu.tsx
+++ b/superset-frontend/src/features/home/SubMenu.tsx
@@ -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}
diff --git a/superset-frontend/src/components/Pagination/Item.tsx b/superset-frontend/src/hooks/useOpenerRef.ts
similarity index 58%
copy from superset-frontend/src/components/Pagination/Item.tsx
copy to superset-frontend/src/hooks/useOpenerRef.ts
index d214499f9e..be4b115ede 100644
--- a/superset-frontend/src/components/Pagination/Item.tsx
+++ b/superset-frontend/src/hooks/useOpenerRef.ts
@@ -17,28 +17,16 @@
  * under the License.
  */
 
-import { ReactNode } from 'react';
-import classNames from 'classnames';
-import { PaginationButtonProps } from './types';
+import { useEffect, useRef } from 'react';
 
-interface PaginationItemButton extends PaginationButtonProps {
-  active?: boolean;
-  children: ReactNode;
-}
+export function useOpenerRef(active: boolean) {
+  const openerRef = useRef<HTMLElement | null>(null);
+
+  useEffect(() => {
+    if (active) {
+      openerRef.current = document.activeElement as HTMLElement;
+    }
+  }, [active]);
 
-export function Item({ active, children, onClick }: PaginationItemButton) {
-  return (
-    <li className={classNames({ active })}>
-      <span
-        role="button"
-        tabIndex={active ? -1 : 0}
-        onClick={e => {
-          e.preventDefault();
-          if (!active) onClick(e);
-        }}
-      >
-        {children}
-      </span>
-    </li>
-  );
+  return openerRef;
 }
diff --git a/superset-frontend/src/pages/AlertReportList/index.tsx b/superset-frontend/src/pages/AlertReportList/index.tsx
index 2d24444d24..8d53a410d9 100644
--- a/superset-frontend/src/pages/AlertReportList/index.tsx
+++ b/superset-frontend/src/pages/AlertReportList/index.tsx
@@ -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>