From c897f3d2e780e170f316350b52739fb1787e9717 Mon Sep 17 00:00:00 2001 From: Fedo Hagge-Kubat Date: Mon, 2 Mar 2026 18:38:04 +0100 Subject: [PATCH 1/2] =?UTF-8?q?fix(a11y):=20WCAG=202.4.7=20=E2=80=94=20ens?= =?UTF-8?q?ure=20visible=20keyboard=20focus=20on=20all=20interactive=20ele?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add global *:focus-visible baseline rule in GlobalStyles.tsx (2px solid colorPrimary) - Replace outline:none with outline:2px solid transparent in dashboard styles.ts - Fix focus visibility in RangeFilterPlugin, TimeFilterPlugin, ConditionalFormattingControl, VizTypeGallery - Fix focus visibility in FilterScopeSelector, WithPopoverMenu, FiltersBadge/Styles - Add :focus-visible box-shadow indicators where no alternative focus style existed Co-Authored-By: Claude Opus 4.6 --- .../superset-core/src/theme/GlobalStyles.tsx | 8 ++++++++ .../src/dashboard/components/FiltersBadge/Styles.tsx | 7 ++++++- .../components/filterscope/FilterScopeSelector.tsx | 7 ++++--- .../dashboard/components/menu/WithPopoverMenu.tsx | 2 +- superset-frontend/src/dashboard/styles.ts | 2 +- .../ConditionalFormattingControl.tsx | 6 +++++- .../controls/VizTypeControl/VizTypeGallery.tsx | 12 +++++++++--- .../filters/components/Range/RangeFilterPlugin.tsx | 2 +- .../src/filters/components/Time/TimeFilterPlugin.tsx | 4 ++-- 9 files changed, 37 insertions(+), 13 deletions(-) diff --git a/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx b/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx index 33a30a9fa613..c6cf097f829e 100644 --- a/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx +++ b/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx @@ -109,6 +109,14 @@ export const GlobalStyles = () => { display: flex; margin-top: ${theme.marginXS}px; } + + /* WCAG 2.4.7: Focus Visible — ensure all interactive elements have a visible + keyboard focus indicator. Uses :focus-visible to avoid showing on mouse clicks. + Individual components can override with their own focus styles. */ + *:focus-visible { + outline: 2px solid ${theme.colorPrimary}; + outline-offset: 2px; + } `} /> ); diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx index 8009520cb424..5884028c0a16 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx @@ -80,10 +80,15 @@ export const FilterItem = styled.button` padding: 0; border: none; background: none; - outline: none; + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring with box-shadow */ width: 100%; color: inherit; + &:focus-visible { + box-shadow: 0 0 0 2px ${theme.colorPrimary}; + border-radius: ${theme.borderRadius}px; + } + &::-moz-focus-inner { border: 0; } diff --git a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx index 7d10c1b691d0..25bb98a830b5 100644 --- a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx +++ b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx @@ -211,8 +211,9 @@ const ScopeSelector = styled.div` text-decoration: underline; } - &:focus { - outline: none; + &:focus-visible { + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring */ + box-shadow: 0 0 0 2px ${theme.colorPrimary}; } } @@ -360,7 +361,7 @@ const ScopeSelector = styled.div` border: 1px solid ${theme.colorBorder}; padding: ${theme.sizeUnit}px ${theme.sizeUnit * 2}px; font-size: ${theme.fontSize}px; - outline: none; + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring; border change on focus provides visible indicator */ &:focus { border: 1px solid ${theme.colorPrimary}; diff --git a/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.tsx b/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.tsx index 3fb061ed15b7..a2a6f62f1cec 100644 --- a/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.tsx +++ b/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.tsx @@ -50,7 +50,7 @@ interface WithPopoverMenuState { const WithPopoverMenuStyles = styled.div` ${({ theme }) => css` position: relative; - outline: none; + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring; :after pseudo-element provides visible focus border */ &.with-popover-menu--focused:after { content: ''; diff --git a/superset-frontend/src/dashboard/styles.ts b/superset-frontend/src/dashboard/styles.ts index ca456b5e2395..5a97b5bcfed7 100644 --- a/superset-frontend/src/dashboard/styles.ts +++ b/superset-frontend/src/dashboard/styles.ts @@ -96,7 +96,7 @@ export const focusStyle = (theme: SupersetTheme) => css` &:focus-visible { box-shadow: 0 0 0 2px ${theme.colorPrimaryText}; border-radius: ${theme.borderRadius}px; - outline: none; + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring; box-shadow provides visible focus */ text-decoration: none; } &:not( diff --git a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx index 52cb263f5030..6d7dea284d50 100644 --- a/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx +++ b/superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx @@ -61,9 +61,13 @@ export const CloseButton = styled.button` border: none; border-right: solid 1px ${theme.colorBorder}; padding: 0; - outline: none; + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring with global baseline */ border-bottom-left-radius: 3px; border-top-left-radius: 3px; + + &:focus-visible { + box-shadow: 0 0 0 2px ${theme.colorPrimary}; + } `} `; diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx index 56de34befc0a..9f56734a6236 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx @@ -174,8 +174,9 @@ const SelectorLabel = styled.button` position: relative; color: ${theme.colorText}; - &:focus { - outline: initial; + &:focus-visible { + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring with box-shadow */ + box-shadow: 0 0 0 2px ${theme.colorPrimary}; } &.selected { @@ -270,7 +271,12 @@ const thumbnailContainerCss = (theme: SupersetTheme) => css` cursor: pointer; width: ${theme.sizeUnit * THUMBNAIL_GRID_UNITS}px; position: relative; - outline: none; /* Remove focus outline to show only selected state */ + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring with box-shadow */ + + &:focus-visible { + box-shadow: 0 0 0 2px ${theme.colorPrimary}; + border-radius: ${theme.borderRadius}px; + } img { min-width: ${theme.sizeUnit * THUMBNAIL_GRID_UNITS}px; diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx index f2db6168408c..83ed402e7c1b 100644 --- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx @@ -114,7 +114,7 @@ const FocusContainer = styled.div` box-shadow: 0 0 0 2px ${theme.colorPrimary}; } &:focus-visible { - outline: none; + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring; box-shadow on :focus provides visible indicator */ }`} `; diff --git a/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx b/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx index f2f88f67621e..975f5b505e9a 100644 --- a/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx @@ -62,8 +62,8 @@ const ControlContainer = styled.div<{ &:focus > div { border-color: ${({ theme }) => theme.colorPrimary}; - box-shadow: ${({ theme }) => `0 0 0 2px ${theme.controlOutline}`}; - outline: 0; + box-shadow: ${({ theme }) => `0 0 0 2px ${theme.colorPrimary}`}; + outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring; box-shadow provides visible focus */ } `; From 5edcd759c7f5a307deb556cb358aadce2d7669b7 Mon Sep 17 00:00:00 2001 From: Fedo Hagge-Kubat Date: Thu, 9 Apr 2026 21:25:50 +0200 Subject: [PATCH 2/2] fix(a11y): add !important to global focus rule, use inset shadow, scope HC outline, fix focus target --- .../packages/superset-core/src/theme/GlobalStyles.tsx | 4 ++-- .../src/dashboard/components/FiltersBadge/Styles.tsx | 2 +- .../components/filterscope/FilterScopeSelector.tsx | 10 ++++++---- .../src/dashboard/components/menu/WithPopoverMenu.tsx | 4 +++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx b/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx index c6cf097f829e..9da0b0cd4571 100644 --- a/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx +++ b/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx @@ -114,8 +114,8 @@ export const GlobalStyles = () => { keyboard focus indicator. Uses :focus-visible to avoid showing on mouse clicks. Individual components can override with their own focus styles. */ *:focus-visible { - outline: 2px solid ${theme.colorPrimary}; - outline-offset: 2px; + outline: 2px solid ${theme.colorPrimary} !important; + outline-offset: 2px !important; } `} /> diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx index 5884028c0a16..6b940d8f2091 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/Styles.tsx @@ -85,7 +85,7 @@ export const FilterItem = styled.button` color: inherit; &:focus-visible { - box-shadow: 0 0 0 2px ${theme.colorPrimary}; + box-shadow: inset 0 0 0 2px ${theme.colorPrimary}; border-radius: ${theme.borderRadius}px; } diff --git a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx index 25bb98a830b5..a9bb6ef12a82 100644 --- a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx +++ b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx @@ -210,11 +210,13 @@ const ScopeSelector = styled.div` &:hover { text-decoration: underline; } + } - &:focus-visible { - outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring */ - box-shadow: 0 0 0 2px ${theme.colorPrimary}; - } + /* WCAG 2.4.7: Focus on the clickable button that wraps the rct-icon spans */ + .react-checkbox-tree button:focus-visible { + outline: 2px solid transparent; + box-shadow: 0 0 0 2px ${theme.colorPrimary}; + border-radius: ${theme.borderRadius}px; } .filter-field-pane { diff --git a/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.tsx b/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.tsx index a2a6f62f1cec..7339a5fbe39c 100644 --- a/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.tsx +++ b/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.tsx @@ -50,7 +50,9 @@ interface WithPopoverMenuState { const WithPopoverMenuStyles = styled.div` ${({ theme }) => css` position: relative; - outline: 2px solid transparent; /* WCAG 2.4.7: transparent outline prevents double-ring; :after pseudo-element provides visible focus border */ + &:focus-visible { + outline: 2px solid transparent; /* WCAG 2.4.7: HC Mode fallback only when focused */ + } &.with-popover-menu--focused:after { content: '';