diff --git a/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx b/superset-frontend/packages/superset-core/src/theme/GlobalStyles.tsx index 33a30a9fa613..9da0b0cd4571 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} !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 8009520cb424..6b940d8f2091 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: inset 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..a9bb6ef12a82 100644 --- a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx +++ b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.tsx @@ -210,10 +210,13 @@ const ScopeSelector = styled.div` &:hover { text-decoration: underline; } + } - &:focus { - outline: none; - } + /* 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 { @@ -360,7 +363,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..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: none; + &:focus-visible { + outline: 2px solid transparent; /* WCAG 2.4.7: HC Mode fallback only when focused */ + } &.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 */ } `;