Copilot commented on code in PR #37211:
URL: https://github.com/apache/superset/pull/37211#discussion_r2713899153
##########
superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx:
##########
@@ -47,39 +46,44 @@ const containerStyle = (theme: SupersetTheme) => css`
display: flex;
&& > .filter-clear-all-button {
- color: ${theme.colorTextSecondary};
+ color: ${theme.colors.grayscale.base};
margin-left: 0;
&:hover {
- color: ${theme.colorPrimaryText};
+ color: ${theme.colors.primary.dark1};
}
&[disabled],
&[disabled]:hover {
- color: ${theme.colorTextDisabled};
+ color: ${theme.colors.grayscale.light1};
}
}
`;
const verticalStyle = (theme: SupersetTheme, width: number) => css`
flex-direction: column;
align-items: center;
+ pointer-events: none;
position: fixed;
z-index: 100;
// filter bar width minus 1px for border
width: ${width - 1}px;
bottom: 0;
- padding: ${theme.sizeUnit * 4}px;
- padding-top: ${theme.sizeUnit * 6}px;
+ padding: ${theme.gridUnit * 4}px;
+ padding-top: ${theme.gridUnit * 6}px;
background: linear-gradient(
- ${tinycolor(theme.colorBgLayout).setAlpha(0).toRgbString()},
- ${theme.colorBgContainer} 20%
+ ${rgba(theme.colors.grayscale.light5, 0)},
+ ${theme.colors.grayscale.light5} ${theme.opacity.mediumLight}
Review Comment:
The gradient background in the vertical filter bar uses rgba with
theme.opacity.mediumLight, but the original used a percentage value (20%). The
property `theme.opacity.mediumLight` may not exist in the theme object, which
would cause a runtime error. Verify that this theme property exists, or use a
literal opacity value.
```suggestion
${theme.colors.grayscale.light5} 20%
```
##########
superset-frontend/src/dashboard/components/SliceHeader/index.tsx:
##########
@@ -16,30 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
-import {
+import React, {
forwardRef,
ReactNode,
useContext,
useEffect,
useRef,
useState,
} from 'react';
-import { t } from '@apache-superset/core';
-import { getExtensionsRegistry, QueryData } from '@superset-ui/core';
-import { css, styled, SupersetTheme, useTheme } from
'@apache-superset/core/ui';
+import { css, getExtensionsRegistry, styled, t } from '@superset-ui/core';
import { useUiConfig } from 'src/components/UiConfigContext';
-import { isEmbedded } from 'src/dashboard/util/isEmbedded';
-import { Tooltip, EditableTitle, Icons } from '@superset-ui/core/components';
+import { Tooltip } from 'src/components/Tooltip';
import { useSelector } from 'react-redux';
-import SliceHeaderControls from 'src/dashboard/components/SliceHeaderControls';
-import { SliceHeaderControlsProps } from
'src/dashboard/components/SliceHeaderControls/types';
+import EditableTitle from 'src/components/EditableTitle';
Review Comment:
The import path for EditableTitle is incorrect. The component should be
imported from '@superset-ui/core' or '@superset-ui/core/components' where it is
exported (as shown in line 104 of
packages/superset-ui-core/src/components/index.ts), not from
'src/components/EditableTitle' which doesn't exist. This will cause a module
resolution error.
```suggestion
import EditableTitle from '@superset-ui/core';
```
##########
superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx:
##########
@@ -16,19 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useMemo } from 'react';
-import { t } from '@apache-superset/core';
+import React, { useMemo } from 'react';
import {
+ css,
DataMaskState,
DataMaskStateWithId,
+ t,
isDefined,
- ChartCustomization,
- ChartCustomizationDivider,
+ SupersetTheme,
} from '@superset-ui/core';
-import { css, SupersetTheme, styled } from '@apache-superset/core/ui';
-import { Button } from '@superset-ui/core/components';
+import Button from 'src/components/Button';
Review Comment:
The import path for Button is incorrect. The component should be imported
from '@superset-ui/core' or '@superset-ui/core/components' where it is actually
exported, not from 'src/components/Button' which doesn't exist in the codebase.
This will cause a module resolution error.
##########
superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx:
##########
@@ -107,63 +111,55 @@ const ActionButtons = ({
dataMaskSelected,
isApplyDisabled,
filterBarOrientation = FilterBarOrientation.Vertical,
- chartCustomizationItems,
}: ActionButtonsProps) => {
const isClearAllEnabled = useMemo(() => {
- const hasSelectedChanges = Object.entries(dataMaskSelected).some(
- ([, mask]) => {
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- },
+ // Check both selected (pending) and applied filters to determine if clear
is available
+ const hasSelectedValues = Object.values(dataMaskSelected).some(
+ mask => isDefined(mask.filterState?.value),
);
-
- const hasAppliedChanges = Object.entries(dataMaskApplied).some(
- ([, mask]) => {
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- },
+ const hasAppliedValues = Object.values(dataMaskApplied).some(
+ mask => isDefined(mask.filterState?.value),
);
-
- const hasChartCustomizations = chartCustomizationItems?.some(item => {
- if (item.removed) return false;
- const mask = dataMaskApplied[item.id] || dataMaskSelected[item.id];
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- });
-
- return hasSelectedChanges || hasAppliedChanges || hasChartCustomizations;
- }, [dataMaskSelected, dataMaskApplied, chartCustomizationItems]);
+ return hasSelectedValues || hasAppliedValues;
+ }, [dataMaskSelected, dataMaskApplied]);
const isVertical = filterBarOrientation === FilterBarOrientation.Vertical;
return (
- <ButtonsContainer
- isVertical={isVertical}
- width={width}
+ <div
+ css={(theme: SupersetTheme) => [
+ containerStyle(theme),
+ isVertical ? verticalStyle(theme, width) : horizontalStyle(theme),
+ ]}
Review Comment:
The styled component `ButtonsContainer` has been removed and replaced with
inline `css` prop usage. However, this change introduces potential
maintainability issues. The css function is being passed an array of style
functions, which is less type-safe and harder to debug than the previous
styled-component approach. Consider keeping the styled component for better
separation of concerns and type safety.
##########
superset-frontend/src/components/Accessibility/StatusAnnouncer.test.tsx:
##########
@@ -0,0 +1,657 @@
+/**
+ * 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 { act, render, screen, waitFor } from 'spec/helpers/testing-library';
Review Comment:
Unused import waitFor.
```suggestion
import { act, render, screen } from 'spec/helpers/testing-library';
```
##########
superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx:
##########
@@ -107,63 +111,55 @@ const ActionButtons = ({
dataMaskSelected,
isApplyDisabled,
filterBarOrientation = FilterBarOrientation.Vertical,
- chartCustomizationItems,
}: ActionButtonsProps) => {
const isClearAllEnabled = useMemo(() => {
- const hasSelectedChanges = Object.entries(dataMaskSelected).some(
- ([, mask]) => {
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- },
+ // Check both selected (pending) and applied filters to determine if clear
is available
+ const hasSelectedValues = Object.values(dataMaskSelected).some(
+ mask => isDefined(mask.filterState?.value),
);
-
- const hasAppliedChanges = Object.entries(dataMaskApplied).some(
- ([, mask]) => {
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- },
+ const hasAppliedValues = Object.values(dataMaskApplied).some(
+ mask => isDefined(mask.filterState?.value),
);
-
- const hasChartCustomizations = chartCustomizationItems?.some(item => {
- if (item.removed) return false;
- const mask = dataMaskApplied[item.id] || dataMaskSelected[item.id];
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- });
-
- return hasSelectedChanges || hasAppliedChanges || hasChartCustomizations;
- }, [dataMaskSelected, dataMaskApplied, chartCustomizationItems]);
+ return hasSelectedValues || hasAppliedValues;
+ }, [dataMaskSelected, dataMaskApplied]);
Review Comment:
The `chartCustomizationItems` prop and all related logic have been removed
from the ActionButtons component. This includes the logic for checking chart
customizations when determining if the "Clear all" button should be enabled.
This is a breaking change that affects the component's API and functionality,
contradicting the PR description's claim of "No breaking changes to existing
functionality".
##########
superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx:
##########
@@ -16,19 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useMemo } from 'react';
-import { t } from '@apache-superset/core';
+import React, { useMemo } from 'react';
import {
+ css,
DataMaskState,
DataMaskStateWithId,
+ t,
isDefined,
- ChartCustomization,
- ChartCustomizationDivider,
+ SupersetTheme,
} from '@superset-ui/core';
-import { css, SupersetTheme, styled } from '@apache-superset/core/ui';
-import { Button } from '@superset-ui/core/components';
+import Button from 'src/components/Button';
+import { Tooltip } from 'src/components/Tooltip';
Review Comment:
The import path for Tooltip is incorrect. The component should be imported
from '@superset-ui/core' or '@superset-ui/core/components' where it is actually
exported, not from 'src/components/Tooltip' which doesn't exist in the
codebase. This will cause a module resolution error.
##########
superset-frontend/src/dashboard/components/SliceHeader/index.tsx:
##########
@@ -165,200 +171,147 @@ const SliceHeader = forwardRef<HTMLDivElement,
SliceHeaderProps>(
formData,
width,
height,
- exportPivotExcel = () => ({}),
},
ref,
) => {
- const SliceHeaderExtension = extensionsRegistry.get(
- 'dashboard.slice.header',
- );
- const uiConfig = useUiConfig();
- const shouldShowRowLimitWarning =
- !isEmbedded() || uiConfig.showRowLimitWarning;
- const dashboardPageId = useContext(DashboardPageIdContext);
- const [headerTooltip, setHeaderTooltip] = useState<ReactNode | null>(null);
- const headerRef = useRef<HTMLDivElement>(null);
- // TODO: change to indicator field after it will be implemented
- const crossFilterValue = useSelector<RootState, any>(
- state => state.dataMask[slice?.slice_id]?.filterState?.value,
- );
- const isCrossFiltersEnabled = useSelector<RootState, boolean>(
- ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
- );
-
- const firstQueryResponse = useSelector<RootState, QueryData | undefined>(
- state => state.charts[slice.slice_id].queriesResponse?.[0],
- );
-
- const theme = useTheme();
-
- const rowLimit = Number(formData.row_limit || -1);
- const sqlRowCount = Number(firstQueryResponse?.sql_rowcount || 0);
+ const SliceHeaderExtension =
extensionsRegistry.get('dashboard.slice.header');
+ const uiConfig = useUiConfig();
+ const dashboardPageId = useContext(DashboardPageIdContext);
+ const [headerTooltip, setHeaderTooltip] = useState<ReactNode | null>(null);
+ const headerRef = useRef<HTMLDivElement>(null);
+ // TODO: change to indicator field after it will be implemented
+ const crossFilterValue = useSelector<RootState, any>(
+ state => state.dataMask[slice?.slice_id]?.filterState?.value,
+ );
+ const isCrossFiltersEnabled = useSelector<RootState, boolean>(
+ ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
+ );
- const canExplore = !editMode && supersetCanExplore;
+ const canExplore = !editMode && supersetCanExplore;
- useEffect(() => {
- const headerElement = headerRef.current;
- if (canExplore) {
- setHeaderTooltip(getSliceHeaderTooltip(sliceName));
- } else if (
- headerElement &&
- (headerElement.scrollWidth > headerElement.offsetWidth ||
- headerElement.scrollHeight > headerElement.offsetHeight)
- ) {
- setHeaderTooltip(sliceName ?? null);
- } else {
- setHeaderTooltip(null);
- }
- }, [sliceName, width, height, canExplore]);
-
- const exploreUrl =
`/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;
+ useEffect(() => {
+ const headerElement = headerRef.current;
+ if (canExplore) {
+ setHeaderTooltip(getSliceHeaderTooltip(sliceName));
+ } else if (
+ headerElement &&
+ (headerElement.scrollWidth > headerElement.offsetWidth ||
+ headerElement.scrollHeight > headerElement.offsetHeight)
+ ) {
+ setHeaderTooltip(sliceName ?? null);
+ } else {
+ setHeaderTooltip(null);
+ }
+ }, [sliceName, width, height, canExplore]);
- const renderExploreLink = (title: string) => (
- <Link
- to={exploreUrl}
- css={(theme: SupersetTheme) => css`
- color: ${theme.colorText};
- text-decoration: none;
- :hover {
- text-decoration: underline;
- }
- display: inline-block;
- `}
- >
- {title}
- </Link>
- );
+ const exploreUrl =
`/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;
- return (
- <ChartHeaderStyles data-test="slice-header" ref={ref}>
- <div className="header-title" ref={headerRef}>
+ return (
+ <ChartHeaderStyles data-test="slice-header" ref={ref}>
+ <div className="header-title" ref={headerRef}>
+ {/* WCAG 1.3.1: Chart title as semantic heading for screen readers */}
+ <h2>
<Tooltip title={headerTooltip}>
- {/* this div ensures the hover event triggers correctly and
prevents flickering */}
- <div>
- <EditableTitle
- title={
- sliceName ||
- (editMode
- ? '---' // this makes an empty title clickable
- : '')
- }
- canEdit={editMode}
- onSaveTitle={updateSliceName}
- showTooltip={false}
- renderLink={
- canExplore && exploreUrl ? renderExploreLink : undefined
- }
- />
- </div>
+ <EditableTitle
+ title={
+ sliceName ||
+ (editMode
+ ? '---' // this makes an empty title clickable
+ : '')
+ }
+ canEdit={editMode}
+ onSaveTitle={updateSliceName}
+ showTooltip={false}
+ url={canExplore ? exploreUrl : undefined}
+ />
</Tooltip>
- {!!Object.values(annotationQuery).length && (
- <Tooltip
- id="annotations-loading-tooltip"
- placement="top"
- title={annotationsLoading}
- >
- <Icons.ReloadOutlined
- className="warning"
- aria-label={annotationsLoading}
+ </h2>
Review Comment:
The semantic `<h2>` element wrapping the chart title is a good accessibility
improvement (WCAG 1.3.1). However, the nested structure
`<h2><Tooltip><EditableTitle></EditableTitle></Tooltip></h2>` may cause issues.
EditableTitle likely renders its own elements (divs, spans, or inputs), and
nesting these inside an h2 could create invalid HTML if EditableTitle renders
block-level elements. Consider reviewing the EditableTitle component's output
to ensure it only renders inline or phrasing content compatible with heading
elements.
##########
superset-frontend/src/dashboard/components/SliceHeader/index.tsx:
##########
@@ -16,30 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
-import {
+import React, {
forwardRef,
ReactNode,
useContext,
useEffect,
useRef,
useState,
} from 'react';
-import { t } from '@apache-superset/core';
-import { getExtensionsRegistry, QueryData } from '@superset-ui/core';
-import { css, styled, SupersetTheme, useTheme } from
'@apache-superset/core/ui';
+import { css, getExtensionsRegistry, styled, t } from '@superset-ui/core';
import { useUiConfig } from 'src/components/UiConfigContext';
-import { isEmbedded } from 'src/dashboard/util/isEmbedded';
-import { Tooltip, EditableTitle, Icons } from '@superset-ui/core/components';
+import { Tooltip } from 'src/components/Tooltip';
Review Comment:
The import path for Tooltip is incorrect. The component should be imported
from '@superset-ui/core' or '@superset-ui/core/components' where it is
exported, not from 'src/components/Tooltip' which doesn't exist. This will
cause a module resolution error.
##########
superset-frontend/src/dashboard/components/SliceHeader/index.tsx:
##########
@@ -16,30 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
-import {
+import React, {
forwardRef,
ReactNode,
useContext,
useEffect,
useRef,
useState,
} from 'react';
-import { t } from '@apache-superset/core';
-import { getExtensionsRegistry, QueryData } from '@superset-ui/core';
-import { css, styled, SupersetTheme, useTheme } from
'@apache-superset/core/ui';
+import { css, getExtensionsRegistry, styled, t } from '@superset-ui/core';
import { useUiConfig } from 'src/components/UiConfigContext';
-import { isEmbedded } from 'src/dashboard/util/isEmbedded';
-import { Tooltip, EditableTitle, Icons } from '@superset-ui/core/components';
+import { Tooltip } from 'src/components/Tooltip';
import { useSelector } from 'react-redux';
-import SliceHeaderControls from 'src/dashboard/components/SliceHeaderControls';
-import { SliceHeaderControlsProps } from
'src/dashboard/components/SliceHeaderControls/types';
+import EditableTitle from 'src/components/EditableTitle';
+import SliceHeaderControls, {
+ SliceHeaderControlsProps,
+} from 'src/dashboard/components/SliceHeaderControls';
import FiltersBadge from 'src/dashboard/components/FiltersBadge';
-import CustomizationsBadge from 'src/dashboard/components/CustomizationsBadge';
+import Icons from 'src/components/Icons';
Review Comment:
The import path for Icons is incorrect. Icons should be imported from
'@superset-ui/core' or '@superset-ui/core/components' where it is exported, not
from 'src/components/Icons' which doesn't exist. This will cause a module
resolution error. Also note that Icons is a named export, not a default export,
so the import statement should use named import syntax: `import { Icons } from
'@superset-ui/core'`.
##########
superset-frontend/src/components/Accessibility/SkipLink.tsx:
##########
@@ -0,0 +1,91 @@
+/**
+ * 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 React from 'react';
+import { styled } from '@superset-ui/core';
+
+/**
+ * SkipLink - WCAG 2.4.1 Bypass Blocks
+ * Allows keyboard users to skip navigation and jump directly to main content.
+ * The link is visually hidden but becomes visible when focused.
+ */
+
+const StyledSkipLink = styled.a`
+ position: absolute;
+ top: -100px;
+ left: 0;
+ background: ${({ theme }) => theme.colors.primary.dark1};
+ color: ${({ theme }) => theme.colors.grayscale.light5};
+ padding: ${({ theme }) => theme.gridUnit * 3}px ${({ theme }) =>
theme.gridUnit * 4}px;
+ z-index: 10000;
+ text-decoration: none;
+ font-weight: ${({ theme }) => theme.typography.weights.bold};
+ font-size: ${({ theme }) => theme.typography.sizes.m}px;
+ border-radius: 0 0 ${({ theme }) => theme.borderRadius}px ${({ theme }) =>
theme.borderRadius}px;
+ transition: top 0.2s ease-in-out;
+
+ &:focus,
+ &:focus-visible {
+ top: 0 !important;
+ outline: 3px solid ${({ theme }) => theme.colors.primary.light1};
+ outline-offset: 2px;
+ }
+
+ &:hover {
+ background: ${({ theme }) => theme.colors.primary.base};
+ }
+`;
+
+interface SkipLinkProps {
+ targetId?: string;
+ children?: React.ReactNode;
+}
+
+const SkipLink: React.FC<SkipLinkProps> = ({
+ targetId = 'main-content',
+ children = 'Skip to main content',
+}) => {
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
+ e.preventDefault();
+ const el = document.getElementById(targetId);
+ if (el) {
+ // Make sure the element is focusable and focus it
+ // Note: We intentionally keep the tabindex to ensure the element
remains focusable
+ // for subsequent keyboard navigation (fixes skip link accessibility)
Review Comment:
The comment states that the tabindex attribute is intentionally kept to
ensure the element remains focusable for subsequent keyboard navigation.
However, setting tabindex="-1" makes the element only programmatically
focusable (not reachable via keyboard Tab navigation). This conflicts with the
stated goal of "subsequent keyboard navigation."
If the intent is to allow keyboard navigation to the target after focus, the
tabindex should remain at "-1" (which is correct for skip link targets), but
the comment is misleading about what this achieves.
```suggestion
// Ensure the target can be programmatically focused for the skip link
// Note: tabindex="-1" makes the element focusable via script without
adding it
// to the Tab order, so users can continue normal keyboard navigation
afterward.
```
##########
superset-frontend/src/components/Accessibility/SkipLink.test.tsx:
##########
@@ -0,0 +1,345 @@
+/**
+ * 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 userEvent from '@testing-library/user-event';
+import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import SkipLink from './SkipLink';
+
+describe('SkipLink', () => {
+ beforeEach(() => {
+ // Clean up any target elements from previous tests
+ const existingTarget = document.getElementById('main-content');
+ if (existingTarget) {
+ existingTarget.remove();
+ }
+ });
+
+ describe('Rendering', () => {
+ test('renders with default "Skip to main content" text', () => {
+ render(<SkipLink />);
+ expect(screen.getByText('Skip to main content')).toBeInTheDocument();
+ });
+
+ test('renders with custom children text', () => {
+ render(<SkipLink>Skip to navigation</SkipLink>);
+ expect(screen.getByText('Skip to navigation')).toBeInTheDocument();
+ });
+
+ test('renders as anchor element with correct href', () => {
+ render(<SkipLink targetId="custom-target" />);
+ const link = screen.getByRole('link');
+ expect(link).toHaveAttribute('href', '#custom-target');
+ });
+
+ test('applies styled-component class', () => {
+ render(<SkipLink />);
+ const link = screen.getByRole('link');
+ expect(link).toHaveClass('a11y-skip-link');
+ });
+ });
+
+ describe('Visual States', () => {
+ test('is positioned off-screen by default (top: -100px)', () => {
+ render(<SkipLink />);
+ const link = screen.getByRole('link');
+ expect(link).toHaveStyleRule('top', '-100px');
+ });
+
+ test('has correct z-index for overlay (10000)', () => {
+ render(<SkipLink />);
+ const link = screen.getByRole('link');
+ expect(link).toHaveStyleRule('z-index', '10000');
+ });
+
+ test('has absolute positioning', () => {
+ render(<SkipLink />);
+ const link = screen.getByRole('link');
+ expect(link).toHaveStyleRule('position', 'absolute');
+ });
+ });
+
+ describe('Focus Behavior', () => {
+ test('becomes visible when focused (top: 0)', async () => {
+ render(<SkipLink />);
+ const link = screen.getByRole('link');
+ link.focus();
+ expect(link).toHaveStyleRule('top', '0 !important', {
+ modifier: ':focus',
+ });
+ });
+
+ test('receives focus on Tab key press', async () => {
+ const user = userEvent.setup();
+ render(<SkipLink />);
+ await user.tab();
+ const link = screen.getByRole('link');
+ expect(link).toHaveFocus();
+ });
+
+ test('shows focus outline styling', () => {
+ render(<SkipLink />);
+ const link = screen.getByRole('link');
+ expect(link).toHaveStyleRule('outline', expect.stringContaining('3px
solid'), {
+ modifier: ':focus',
+ });
+ });
+
+ test('shows focus-visible outline styling', () => {
+ render(<SkipLink />);
+ const link = screen.getByRole('link');
+ expect(link).toHaveStyleRule('top', '0 !important', {
+ modifier: ':focus-visible',
+ });
+ });
+ });
+
+ describe('Keyboard Navigation', () => {
+ test('activates on Enter key press', async () => {
+ const user = userEvent.setup();
+ const targetElement = document.createElement('main');
+ targetElement.id = 'main-content';
+ document.body.appendChild(targetElement);
+
+ render(<SkipLink />);
+ await user.tab();
+ await user.keyboard('{Enter}');
+
+ await waitFor(() => {
+ expect(targetElement).toHaveFocus();
+ });
+
+ targetElement.remove();
+ });
+
+ test('activates on click', async () => {
+ const user = userEvent.setup();
+ const targetElement = document.createElement('main');
+ targetElement.id = 'main-content';
+ document.body.appendChild(targetElement);
+
+ render(<SkipLink />);
+ await user.click(screen.getByRole('link'));
+
+ await waitFor(() => {
+ expect(targetElement).toHaveFocus();
+ });
+
+ targetElement.remove();
+ });
+ });
+
+ describe('Click Handler', () => {
+ test('prevents default anchor behavior', async () => {
+ const user = userEvent.setup();
Review Comment:
Unused variable user.
```suggestion
```
##########
superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx:
##########
@@ -107,63 +111,55 @@ const ActionButtons = ({
dataMaskSelected,
isApplyDisabled,
filterBarOrientation = FilterBarOrientation.Vertical,
- chartCustomizationItems,
}: ActionButtonsProps) => {
const isClearAllEnabled = useMemo(() => {
- const hasSelectedChanges = Object.entries(dataMaskSelected).some(
- ([, mask]) => {
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- },
+ // Check both selected (pending) and applied filters to determine if clear
is available
+ const hasSelectedValues = Object.values(dataMaskSelected).some(
+ mask => isDefined(mask.filterState?.value),
);
-
- const hasAppliedChanges = Object.entries(dataMaskApplied).some(
- ([, mask]) => {
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- },
+ const hasAppliedValues = Object.values(dataMaskApplied).some(
+ mask => isDefined(mask.filterState?.value),
);
-
- const hasChartCustomizations = chartCustomizationItems?.some(item => {
- if (item.removed) return false;
- const mask = dataMaskApplied[item.id] || dataMaskSelected[item.id];
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- });
-
- return hasSelectedChanges || hasAppliedChanges || hasChartCustomizations;
- }, [dataMaskSelected, dataMaskApplied, chartCustomizationItems]);
+ return hasSelectedValues || hasAppliedValues;
+ }, [dataMaskSelected, dataMaskApplied]);
Review Comment:
The isClearAllEnabled logic has been simplified and now only checks for
`filterState?.value`, but the original implementation also checked for
`ownState?.column` (hasGroupBy). This change may cause the "Clear all" button
to be incorrectly disabled when filters have groupBy columns but no explicit
values. This could break existing filter clearing functionality.
##########
superset-frontend/src/dashboard/components/SliceHeader/index.tsx:
##########
@@ -165,200 +171,147 @@ const SliceHeader = forwardRef<HTMLDivElement,
SliceHeaderProps>(
formData,
width,
height,
- exportPivotExcel = () => ({}),
},
ref,
) => {
- const SliceHeaderExtension = extensionsRegistry.get(
- 'dashboard.slice.header',
- );
- const uiConfig = useUiConfig();
- const shouldShowRowLimitWarning =
- !isEmbedded() || uiConfig.showRowLimitWarning;
- const dashboardPageId = useContext(DashboardPageIdContext);
- const [headerTooltip, setHeaderTooltip] = useState<ReactNode | null>(null);
- const headerRef = useRef<HTMLDivElement>(null);
- // TODO: change to indicator field after it will be implemented
- const crossFilterValue = useSelector<RootState, any>(
- state => state.dataMask[slice?.slice_id]?.filterState?.value,
- );
- const isCrossFiltersEnabled = useSelector<RootState, boolean>(
- ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
- );
-
- const firstQueryResponse = useSelector<RootState, QueryData | undefined>(
- state => state.charts[slice.slice_id].queriesResponse?.[0],
- );
-
- const theme = useTheme();
-
- const rowLimit = Number(formData.row_limit || -1);
- const sqlRowCount = Number(firstQueryResponse?.sql_rowcount || 0);
+ const SliceHeaderExtension =
extensionsRegistry.get('dashboard.slice.header');
+ const uiConfig = useUiConfig();
+ const dashboardPageId = useContext(DashboardPageIdContext);
+ const [headerTooltip, setHeaderTooltip] = useState<ReactNode | null>(null);
+ const headerRef = useRef<HTMLDivElement>(null);
+ // TODO: change to indicator field after it will be implemented
+ const crossFilterValue = useSelector<RootState, any>(
+ state => state.dataMask[slice?.slice_id]?.filterState?.value,
+ );
+ const isCrossFiltersEnabled = useSelector<RootState, boolean>(
+ ({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
+ );
- const canExplore = !editMode && supersetCanExplore;
+ const canExplore = !editMode && supersetCanExplore;
- useEffect(() => {
- const headerElement = headerRef.current;
- if (canExplore) {
- setHeaderTooltip(getSliceHeaderTooltip(sliceName));
- } else if (
- headerElement &&
- (headerElement.scrollWidth > headerElement.offsetWidth ||
- headerElement.scrollHeight > headerElement.offsetHeight)
- ) {
- setHeaderTooltip(sliceName ?? null);
- } else {
- setHeaderTooltip(null);
- }
- }, [sliceName, width, height, canExplore]);
-
- const exploreUrl =
`/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;
+ useEffect(() => {
+ const headerElement = headerRef.current;
+ if (canExplore) {
+ setHeaderTooltip(getSliceHeaderTooltip(sliceName));
+ } else if (
+ headerElement &&
+ (headerElement.scrollWidth > headerElement.offsetWidth ||
+ headerElement.scrollHeight > headerElement.offsetHeight)
+ ) {
+ setHeaderTooltip(sliceName ?? null);
+ } else {
+ setHeaderTooltip(null);
+ }
+ }, [sliceName, width, height, canExplore]);
- const renderExploreLink = (title: string) => (
- <Link
- to={exploreUrl}
- css={(theme: SupersetTheme) => css`
- color: ${theme.colorText};
- text-decoration: none;
- :hover {
- text-decoration: underline;
- }
- display: inline-block;
- `}
- >
- {title}
- </Link>
- );
+ const exploreUrl =
`/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${slice.slice_id}`;
- return (
- <ChartHeaderStyles data-test="slice-header" ref={ref}>
- <div className="header-title" ref={headerRef}>
+ return (
+ <ChartHeaderStyles data-test="slice-header" ref={ref}>
+ <div className="header-title" ref={headerRef}>
+ {/* WCAG 1.3.1: Chart title as semantic heading for screen readers */}
+ <h2>
<Tooltip title={headerTooltip}>
- {/* this div ensures the hover event triggers correctly and
prevents flickering */}
- <div>
- <EditableTitle
- title={
- sliceName ||
- (editMode
- ? '---' // this makes an empty title clickable
- : '')
- }
- canEdit={editMode}
- onSaveTitle={updateSliceName}
- showTooltip={false}
- renderLink={
- canExplore && exploreUrl ? renderExploreLink : undefined
- }
- />
- </div>
+ <EditableTitle
+ title={
+ sliceName ||
+ (editMode
+ ? '---' // this makes an empty title clickable
+ : '')
+ }
+ canEdit={editMode}
+ onSaveTitle={updateSliceName}
+ showTooltip={false}
+ url={canExplore ? exploreUrl : undefined}
Review Comment:
The EditableTitle component does not accept a `url` prop. According to the
component's API, you should use the `renderLink` callback prop instead, which
takes the title as a parameter and returns a React node. The current usage will
cause TypeScript errors or runtime issues.
You need to pass a function that renders a link component rather than just a
URL string.
```suggestion
renderLink={
canExplore && exploreUrl
? title => <a href={exploreUrl}>{title}</a>
: undefined
}
```
##########
superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx:
##########
@@ -107,63 +111,55 @@ const ActionButtons = ({
dataMaskSelected,
isApplyDisabled,
filterBarOrientation = FilterBarOrientation.Vertical,
- chartCustomizationItems,
}: ActionButtonsProps) => {
const isClearAllEnabled = useMemo(() => {
- const hasSelectedChanges = Object.entries(dataMaskSelected).some(
- ([, mask]) => {
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- },
+ // Check both selected (pending) and applied filters to determine if clear
is available
+ const hasSelectedValues = Object.values(dataMaskSelected).some(
+ mask => isDefined(mask.filterState?.value),
);
-
- const hasAppliedChanges = Object.entries(dataMaskApplied).some(
- ([, mask]) => {
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- },
+ const hasAppliedValues = Object.values(dataMaskApplied).some(
+ mask => isDefined(mask.filterState?.value),
);
-
- const hasChartCustomizations = chartCustomizationItems?.some(item => {
- if (item.removed) return false;
- const mask = dataMaskApplied[item.id] || dataMaskSelected[item.id];
- const hasValue = isDefined(mask?.filterState?.value);
- const hasGroupBy = isDefined(mask?.ownState?.column);
- return hasValue || hasGroupBy;
- });
-
- return hasSelectedChanges || hasAppliedChanges || hasChartCustomizations;
- }, [dataMaskSelected, dataMaskApplied, chartCustomizationItems]);
+ return hasSelectedValues || hasAppliedValues;
+ }, [dataMaskSelected, dataMaskApplied]);
const isVertical = filterBarOrientation === FilterBarOrientation.Vertical;
return (
- <ButtonsContainer
- isVertical={isVertical}
- width={width}
+ <div
+ css={(theme: SupersetTheme) => [
+ containerStyle(theme),
+ isVertical ? verticalStyle(theme, width) : horizontalStyle(theme),
+ ]}
data-test="filterbar-action-buttons"
>
- <Button
- disabled={isApplyDisabled}
- buttonStyle="primary"
- htmlType="submit"
- className="filter-apply-button"
- onClick={onApply}
- {...getFilterBarTestId('apply-button')}
+ <Tooltip
+ title={isApplyDisabled ? t('Select filter values to apply') :
undefined}
+ placement="top"
>
- {isVertical ? t('Apply filters') : t('Apply')}
- </Button>
+ <span>
+ <Button
+ disabled={isApplyDisabled}
+ buttonStyle="primary"
+ htmlType="submit"
+ className="filter-apply-button"
+ onClick={onApply}
+ {...getFilterBarTestId('apply-button')}
+ >
+ {isVertical ? t('Apply filters') : t('Apply')}
+ </Button>
+ </span>
Review Comment:
Wrapping the Button in a span element to support the Tooltip is correct for
disabled buttons (since disabled buttons don't trigger mouse events). However,
the span should have `display: inline-block` styling to ensure proper tooltip
positioning and behavior. Without this, the tooltip may not appear correctly in
all scenarios.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]