diff --git a/superset-frontend/src/components/Accessibility/VisuallyHidden.test.tsx b/superset-frontend/src/components/Accessibility/VisuallyHidden.test.tsx new file mode 100644 index 000000000000..a5708e7fd1b4 --- /dev/null +++ b/superset-frontend/src/components/Accessibility/VisuallyHidden.test.tsx @@ -0,0 +1,54 @@ +/** + * 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 { render, screen } from 'spec/helpers/testing-library'; +import VisuallyHidden from './VisuallyHidden'; + +test('renders children', () => { + render(Screen-reader only text); + expect(screen.getByText('Screen-reader only text')).toBeInTheDocument(); +}); + +test('renders as span by default', () => { + render(Default tag); + const el = screen.getByText('Default tag'); + expect(el.tagName).toBe('SPAN'); +}); + +test('renders as a custom element when as prop is provided', () => { + render(Page heading); + const el = screen.getByText('Page heading'); + expect(el.tagName).toBe('H1'); +}); + +test('forwards id and className props', () => { + render( + + Text + , + ); + const el = screen.getByText('Text'); + expect(el).toHaveAttribute('id', 'my-id'); + expect(el).toHaveClass('my-class'); +}); + +test('applies clip-rect style for screen-reader-only behavior', () => { + render(Hidden); + const el = screen.getByText('Hidden'); + expect(el).toHaveStyle({ position: 'absolute' }); +}); diff --git a/superset-frontend/src/components/Accessibility/VisuallyHidden.tsx b/superset-frontend/src/components/Accessibility/VisuallyHidden.tsx new file mode 100644 index 000000000000..c827bb9dbce5 --- /dev/null +++ b/superset-frontend/src/components/Accessibility/VisuallyHidden.tsx @@ -0,0 +1,60 @@ +/** + * 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 { ElementType, FC, ReactNode } from 'react'; +import { styled } from '@apache-superset/core/theme'; + +/** + * VisuallyHidden — content that is available to assistive technology but not + * visually rendered. Use for screen-reader-only headings, labels, and live + * regions where a duplicate visible element would be redundant. + * + * Renders a `` by default. Pass `as` to render a different element + * (e.g. `as="h1"` for an sr-only page heading). + */ +const HiddenElement = styled.span` + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +`; + +export interface VisuallyHiddenProps { + /** Element type to render. Defaults to `'span'`. */ + as?: ElementType; + children?: ReactNode; + id?: string; + className?: string; +} + +const VisuallyHidden: FC = ({ + as = 'span', + children, + ...rest +}) => ( + + {children} + +); + +export default VisuallyHidden; diff --git a/superset-frontend/src/components/Accessibility/index.tsx b/superset-frontend/src/components/Accessibility/index.tsx new file mode 100644 index 000000000000..0a81b1e06f72 --- /dev/null +++ b/superset-frontend/src/components/Accessibility/index.tsx @@ -0,0 +1,22 @@ +/** + * 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. + */ +export { + default as VisuallyHidden, + type VisuallyHiddenProps, +} from './VisuallyHidden'; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index a507c65318de..e27fd6a711f8 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -25,6 +25,7 @@ import { css, styled, useTheme } from '@apache-superset/core/theme'; import { useDispatch, useSelector } from 'react-redux'; import { EmptyState, Loading } from '@superset-ui/core/components'; import { ErrorBoundary, BasicErrorAlert } from 'src/components'; +import { VisuallyHidden } from 'src/components/Accessibility'; import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane'; import DashboardHeader from 'src/dashboard/components/Header'; import { Icons } from '@superset-ui/core/components/Icons'; @@ -351,6 +352,7 @@ const StyledDashboardContent = styled.div<{ `} `; + const ELEMENT_ON_SCREEN_OPTIONS = { threshold: [1], }; @@ -509,10 +511,13 @@ const DashboardBuilder = () => { {!hideDashboardHeader && } {showFilterBar && filterBarOrientation === FilterBarOrientation.Horizontal && ( -