diff --git a/.changeset/wild-donkeys-sneeze.md b/.changeset/wild-donkeys-sneeze.md new file mode 100644 index 0000000000..5e2c4474c5 --- /dev/null +++ b/.changeset/wild-donkeys-sneeze.md @@ -0,0 +1,5 @@ +--- +'@backstage/ui': patch +--- + +Added new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers. diff --git a/docs-ui/src/content/components/visually-hidden.mdx b/docs-ui/src/content/components/visually-hidden.mdx new file mode 100644 index 0000000000..26a609b245 --- /dev/null +++ b/docs-ui/src/content/components/visually-hidden.mdx @@ -0,0 +1,51 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { VisuallyHiddenSnippet } from '@/snippets/stories-snippets'; +import { + visuallyHiddenPropDefs, + visuallyHiddenUsageSnippet, + visuallyHiddenDefaultSnippet, + visuallyHiddenExampleUsageSnippet, +} from './visually-hidden.props'; +import { PageTitle } from '@/components/PageTitle'; +import { Theming } from '@/components/Theming'; +import { ChangelogComponent } from '@/components/ChangelogComponent'; + + + +} + code={visuallyHiddenDefaultSnippet} +/> + +## Usage + + + +## API reference + + + +## Examples + +### Example Usage + +Here's an example of providing screen reader context for a list of links in a footer. + +} + code={visuallyHiddenExampleUsageSnippet} + open +/> + + + + diff --git a/docs-ui/src/content/components/visually-hidden.props.ts b/docs-ui/src/content/components/visually-hidden.props.ts new file mode 100644 index 0000000000..18a1c503d1 --- /dev/null +++ b/docs-ui/src/content/components/visually-hidden.props.ts @@ -0,0 +1,47 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const visuallyHiddenPropDefs: Record = { + children: { + type: 'enum', + values: ['ReactNode'], + responsive: false, + }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const visuallyHiddenUsageSnippet = `import { VisuallyHidden } from '@backstage/ui'; + + + This content is visually hidden but accessible to screen readers +`; + +export const visuallyHiddenDefaultSnippet = ` + + This text is followed by a paragraph that is visually hidden but + accessible to screen readers. Try using a screen reader to hear it, or + inspect the DOM to see it's there. + + + This content is visually hidden but accessible to screen readers + +`; + +export const visuallyHiddenExampleUsageSnippet = ` + + Footer links + + + About us + + + Jobs + + + Terms and Conditions + +`; diff --git a/docs-ui/src/snippets/stories-snippets.tsx b/docs-ui/src/snippets/stories-snippets.tsx index 53b1f21ac4..3a687929e8 100644 --- a/docs-ui/src/snippets/stories-snippets.tsx +++ b/docs-ui/src/snippets/stories-snippets.tsx @@ -29,6 +29,7 @@ import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPa import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories'; import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories'; import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories'; +import * as VisuallyHiddenStories from '../../../packages/ui/src/components/visuallyHidden/VisuallyHidden.stories'; // Helper function to create snippet components // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -74,3 +75,6 @@ export const HeaderSnippet = createSnippetComponent(HeaderStories); export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories); export const TableSnippet = createSnippetComponent(TableStories); export const TagGroupSnippet = createSnippetComponent(TagGroupStories); +export const VisuallyHiddenSnippet = createSnippetComponent( + VisuallyHiddenStories, +); diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index bd5b7b1ba1..18d75d31c8 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -182,6 +182,11 @@ export const components: Page[] = [ slug: 'tooltip', status: 'alpha', }, + { + title: 'VisuallyHidden', + slug: 'visually-hidden', + status: 'alpha', + }, ]; export type ScreenSize = { diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 91d81e29e3..e0b8132f7f 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -743,6 +743,11 @@ export const componentDefinitions: { readonly arrow: 'bui-TooltipArrow'; }; }; + readonly VisuallyHidden: { + readonly classNames: { + readonly root: 'bui-VisuallyHidden'; + }; + }; }; // @public (undocumented) @@ -1534,4 +1539,13 @@ export interface UtilityProps extends SpaceProps { // (undocumented) rowSpan?: Responsive; } + +// @public +export const VisuallyHidden: (props: VisuallyHiddenProps) => JSX_2.Element; + +// @public +export interface VisuallyHiddenProps extends ComponentProps<'div'> { + // (undocumented) + children?: React.ReactNode; +} ``` diff --git a/packages/ui/src/components/VisuallyHidden/VisuallyHidden.module.css b/packages/ui/src/components/VisuallyHidden/VisuallyHidden.module.css new file mode 100644 index 0000000000..dbb7500405 --- /dev/null +++ b/packages/ui/src/components/VisuallyHidden/VisuallyHidden.module.css @@ -0,0 +1,32 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed 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. + */ + +@layer tokens, base, components, utilities; + +@layer components { + .bui-VisuallyHidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(100%); + white-space: nowrap; + border: 0; + } +} diff --git a/packages/ui/src/components/VisuallyHidden/VisuallyHidden.stories.tsx b/packages/ui/src/components/VisuallyHidden/VisuallyHidden.stories.tsx new file mode 100644 index 0000000000..9706edfc9b --- /dev/null +++ b/packages/ui/src/components/VisuallyHidden/VisuallyHidden.stories.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed 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 type { Meta, StoryObj } from '@storybook/react-vite'; +import { VisuallyHidden } from './VisuallyHidden'; +import { Text } from '../Text'; +import { Flex } from '../Flex'; + +const meta = { + title: 'Backstage UI/VisuallyHidden', + component: VisuallyHidden, + parameters: { + docs: { + description: { + component: + 'Visually hides content while keeping it accessible to screen readers. Commonly used for descriptive labels, and other screen-reader-only content.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + This text is followed by a paragraph that is visually hidden but + accessible to screen readers. Try using a screen reader to hear it, or + inspect the DOM to see it's there. + + + This content is visually hidden but accessible to screen readers + + + ), +}; + +export const ExampleUsage: Story = { + render: () => ( + + + Footer links + + + About us + + + Jobs + + + Terms and Conditions + + + (Screen readers hear: "Footer links" followed by the list of links) + + + ), +}; diff --git a/packages/ui/src/components/VisuallyHidden/VisuallyHidden.tsx b/packages/ui/src/components/VisuallyHidden/VisuallyHidden.tsx new file mode 100644 index 0000000000..305f8fc424 --- /dev/null +++ b/packages/ui/src/components/VisuallyHidden/VisuallyHidden.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed 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 { useStyles } from '../../hooks/useStyles'; +import { VisuallyHiddenProps } from './types'; +import styles from './VisuallyHidden.module.css'; +import clsx from 'clsx'; + +/** + * Visually hides content while keeping it accessible to screen readers. + * Useful for descriptive labels and other screen-reader-only content. + * + * Note: This component is for content that should ALWAYS remain visually hidden. + * For skip links that become visible on focus, use a different approach. + * + * @public + */ +export const VisuallyHidden = (props: VisuallyHiddenProps) => { + const { classNames, cleanedProps } = useStyles('VisuallyHidden', props); + const { className, ...rest } = cleanedProps; + + return ( +
+ ); +}; diff --git a/packages/ui/src/components/VisuallyHidden/index.ts b/packages/ui/src/components/VisuallyHidden/index.ts new file mode 100644 index 0000000000..6946ec00e5 --- /dev/null +++ b/packages/ui/src/components/VisuallyHidden/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed 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 { VisuallyHidden } from './VisuallyHidden'; +export type { VisuallyHiddenProps } from './types'; diff --git a/packages/ui/src/components/VisuallyHidden/types.ts b/packages/ui/src/components/VisuallyHidden/types.ts new file mode 100644 index 0000000000..88951d7ee2 --- /dev/null +++ b/packages/ui/src/components/VisuallyHidden/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed 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 { ComponentProps } from 'react'; + +/** + * Properties for {@link VisuallyHidden} + * + * @public + */ +export interface VisuallyHiddenProps extends ComponentProps<'div'> { + children?: React.ReactNode; +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 4484a858d5..1bb10cfbd1 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -52,6 +52,7 @@ export * from './components/Link'; export * from './components/Select'; export * from './components/Skeleton'; export * from './components/Switch'; +export * from './components/VisuallyHidden'; // Types export * from './types'; diff --git a/packages/ui/src/utils/componentDefinitions.ts b/packages/ui/src/utils/componentDefinitions.ts index d2d1450f59..4f914cd1f9 100644 --- a/packages/ui/src/utils/componentDefinitions.ts +++ b/packages/ui/src/utils/componentDefinitions.ts @@ -395,4 +395,9 @@ export const componentDefinitions = { arrow: 'bui-TooltipArrow', }, }, + VisuallyHidden: { + classNames: { + root: 'bui-VisuallyHidden', + }, + }, } as const satisfies Record;