Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions workspaces/theme/.changeset/amber-scroll-mend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@red-hat-developer-hub/backstage-plugin-theme': patch
'@red-hat-developer-hub/backstage-plugin-bcc-test': patch
'@red-hat-developer-hub/backstage-plugin-bui-test': patch
'@red-hat-developer-hub/backstage-plugin-mui4-test': patch
'@red-hat-developer-hub/backstage-plugin-mui5-test': patch
---

Fix light-theme WCAG contrast and demo a11y issues so the theme e2e axe suite can enforce violations.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
'accessibility-scan-results.json',
{ skipViolationsAssert: true },
);

for (const theme of themes) {
Expand Down Expand Up @@ -86,15 +85,13 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
`${themeName}-${tab}-accessibility`,
{ skipViolationsAssert: true },
);
for (const subTab of subTabs) {
await page.getByRole('tab', { name: subTab }).click();
await runAccessibilityTests(
page,
testInfo,
`${themeName}-${tab}-${subTab}-accessibility`,
{ skipViolationsAssert: true },
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, Page, TestInfo } from '@playwright/test';

/**
* Upstream library false positives that cannot be fixed in the theme plugin:
* - aria-valid-attr-value: @backstage/ui / react-aria use React useId() IDs
* containing ":" in aria-controls (axe rejects colon IDREFs).
* - nested-interactive: @material-table/core nests focusable controls inside
* MuiTableSortLabel (role=button). Demo also sets options.draggable=false.
*/
const UPSTREAM_AXE_DISABLE_RULES = [
'aria-valid-attr-value',
'nested-interactive',
] as const;

export async function runAccessibilityTests(
page: Page,
testInfo: TestInfo,
Expand All @@ -26,6 +38,7 @@ export async function runAccessibilityTests(
) {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.disableRules([...UPSTREAM_AXE_DISABLE_RULES])
.analyze();

await testInfo.attach(attachName, {
Expand Down
29 changes: 29 additions & 0 deletions workspaces/theme/packages/app-legacy/src/buiTokenBridge.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Red Hat, Inc.
*
* 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.
*/

/**
* Override Backstage UI --bui-fg-secondary after @backstage/ui/css/styles.css.
* Default oklch(0% 0 0 / 50%) (~#797979) fails WCAG AA on RHDH #f2f2f2 chrome.
* Unlayered + !important beats BUI @layer tokens.
*/
:root,
[data-theme-mode='light'] {
--bui-fg-secondary: #6a6e73 !important;
}

[data-theme-mode='dark'] {
--bui-fg-secondary: #c7c7c7 !important;
}
2 changes: 2 additions & 0 deletions workspaces/theme/packages/app-legacy/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@backstage/ui/css/styles.css';
// Must load after @backstage/ui styles so RHDH AA secondary text wins.
import './buiTokenBridge.css';

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
'accessibility-scan-results.json',
{ skipViolationsAssert: true },
);

for (const theme of themes) {
Expand Down Expand Up @@ -82,15 +81,13 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
`${themeName}-${tab}-accessibility`,
{ skipViolationsAssert: true },
);
for (const subTab of subTabs) {
await page.getByRole('tab', { name: subTab }).click();
await runAccessibilityTests(
page,
testInfo,
`${themeName}-${tab}-${subTab}-accessibility`,
{ skipViolationsAssert: true },
);
}
}
Expand Down
13 changes: 13 additions & 0 deletions workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, Page, TestInfo } from '@playwright/test';

/**
* Upstream library false positives that cannot be fixed in the theme plugin:
* - aria-valid-attr-value: @backstage/ui / react-aria use React useId() IDs
* containing ":" in aria-controls (axe rejects colon IDREFs).
* - nested-interactive: @material-table/core nests focusable controls inside
* MuiTableSortLabel (role=button). Demo also sets options.draggable=false.
*/
const UPSTREAM_AXE_DISABLE_RULES = [
'aria-valid-attr-value',
'nested-interactive',
] as const;

export async function runAccessibilityTests(
page: Page,
testInfo: TestInfo,
Expand All @@ -26,6 +38,7 @@ export async function runAccessibilityTests(
) {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.disableRules([...UPSTREAM_AXE_DISABLE_RULES])
.analyze();

await testInfo.attach(attachName, {
Expand Down
29 changes: 29 additions & 0 deletions workspaces/theme/packages/app/src/buiTokenBridge.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright Red Hat, Inc.
*
* 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.
*/

/**
* Override Backstage UI --bui-fg-secondary after @backstage/ui/css/styles.css.
* Default oklch(0% 0 0 / 50%) (~#797979) fails WCAG AA on RHDH #f2f2f2 chrome.
* Unlayered + !important beats BUI @layer tokens.
*/
:root,
[data-theme-mode='light'] {
--bui-fg-secondary: #6a6e73 !important;
}

[data-theme-mode='dark'] {
--bui-fg-secondary: #c7c7c7 !important;
}
2 changes: 2 additions & 0 deletions workspaces/theme/packages/app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@backstage/ui/css/styles.css';
// Must load after @backstage/ui styles so RHDH AA secondary text wins.
import './buiTokenBridge.css';

ReactDOM.createRoot(document.getElementById('root')!).render(App.createRoot());
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ export const BCCTestPage = () => {
return (
<Page themeId="tool">
<Header title="Backstage Core Components Tests">
<UserSettingsThemeToggle />
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
<UserSettingsThemeToggle />
</ul>
</Header>
<TabbedLayout>
<TabbedLayout.Route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,5 +206,14 @@ const columns: TableColumn<User>[] = [
];

export const TableExample = () => {
return <Table title="Table Example" columns={columns} data={users} />;
return (
<Table
title="Table Example"
columns={columns}
data={users}
// material-table defaults enable column drag handles inside TableSortLabel
// (nested-interactive). Keep sorting; disable drag for the a11y demo.
options={{ draggable: false }}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ export const BUITestPage = () => {
<>
<Header
title="Backstage UI Tests"
customActions={<UserSettingsThemeToggle />}
customActions={
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
<UserSettingsThemeToggle />
</ul>
}
tabs={[
{
id: 'form-components',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ const Checkboxes = () => {
<Flex>
<Text>Checkboxes</Text>
<Flex align="center">
<Checkbox> </Checkbox>
<Checkbox defaultSelected> </Checkbox>
<Checkbox aria-label="Checkbox"> </Checkbox>
<Checkbox aria-label="Checkbox" defaultSelected>
{' '}
</Checkbox>
<Checkbox>Checkbox</Checkbox>
<Checkbox defaultSelected>Checkbox</Checkbox>
</Flex>
Expand All @@ -114,8 +116,8 @@ const Switches = () => {
<Flex>
<Text>Switches</Text>
<Flex align="center">
<Switch />
<Switch isSelected />
<Switch aria-label="Switch" />
<Switch aria-label="Switch" isSelected />
<Switch label="Switch" />
<Switch label="Switch" isSelected />
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
withPadding: {
padding: 32,
backgroundColor: 'gray',
// CSS 'gray' (#808080) fails WCAG AA with white label text (3.94:1).
backgroundColor: '#424242',
color: '#ffffff',
},
withSpacing: {
padding: theme.spacing(4),
Expand All @@ -40,7 +42,7 @@ export const InlineStyles = () => {
Default button with{' '}
<code>
{
"makeStyles({ withPadding: { padding: 32, backgroundColor: 'gray' }})"
"makeStyles({ withPadding: { padding: 32, backgroundColor: '#424242', color: '#ffffff' }})"
}
</code>
</h1>
Expand All @@ -58,9 +60,17 @@ export const InlineStyles = () => {

<h1>
Default button with{' '}
<code>{"style={{ padding: 32, backgroundColor: 'gray' }}"}</code>
<code>
{
"style={{ padding: 32, backgroundColor: '#424242', color: '#ffffff' }}"
}
</code>
</h1>
<Button style={{ padding: 32, backgroundColor: 'gray' }}>a button</Button>
<Button
style={{ padding: 32, backgroundColor: '#424242', color: '#ffffff' }}
>
a button
</Button>
</InfoCard>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export const MUI4TestPage = () => {
return (
<Page themeId="tool">
<Header title="MUI v4 Test Page">
<UserSettingsThemeToggle />
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
<UserSettingsThemeToggle />
</ul>
</Header>
<TabbedLayout>
<TabbedLayout.Route path="/" title="Form components">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,17 @@ export const InlineStyles = () => {

<h1>
Default button with{' '}
<code>{"style={{ padding: 32, backgroundColor: 'gray' }}"}</code>
<code>
{
"style={{ padding: 32, backgroundColor: '#424242', color: '#ffffff' }}"
}
</code>
</h1>
<Button style={{ padding: 32, backgroundColor: 'gray' }}>a button</Button>
<Button
style={{ padding: 32, backgroundColor: '#424242', color: '#ffffff' }}
>
a button
</Button>

<h1>
Default button with{' '}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export const MUI5TestPage = () => {
return (
<Page themeId="tool">
<Header title="MUI v5 Test Page">
<UserSettingsThemeToggle />
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
<UserSettingsThemeToggle />
</ul>
</Header>
<TabbedLayout>
<TabbedLayout.Route path="/form-components" title="Form components">
Expand Down
4 changes: 3 additions & 1 deletion workspaces/theme/plugins/theme/dev/ThemeTestPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import { UserSettingsThemeToggle } from '@backstage/plugin-user-settings';
export const ThemeTestPage = () => (
<Page themeId="tool">
<Header title="Theme Test Page">
<UserSettingsThemeToggle />
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
<UserSettingsThemeToggle />
</ul>
</Header>
<Content>
<Grid container>
Expand Down
31 changes: 22 additions & 9 deletions workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,28 @@ const ThemeProvider = ({
}: {
theme: UnifiedTheme;
children: ReactNode;
}) => (
<UnifiedThemeProvider theme={theme}>
<StyledEngineProvider injectFirst>
<Mui5Provider theme={theme.getTheme('v5') as Mui5Theme}>
{children}
</Mui5Provider>
</StyledEngineProvider>
</UnifiedThemeProvider>
);
}) => {
const mui5Theme = theme.getTheme('v5') as Mui5Theme;
const secondary = mui5Theme.palette.text.secondary;
return (
<UnifiedThemeProvider theme={theme}>
<StyledEngineProvider injectFirst>
<Mui5Provider theme={mui5Theme}>
{/*
Native style tag (not Emotion) so the declaration is unlayered and
reliably overrides @backstage/ui @layer tokens for --bui-fg-secondary.
*/}
<style>{`
:root, [data-theme-mode='light'], [data-theme-mode='dark'] {
--bui-fg-secondary: ${secondary} !important;
}
`}</style>
{children}
</Mui5Provider>
</StyledEngineProvider>
</UnifiedThemeProvider>
);
};

export const createThemeProvider = (
theme: UnifiedTheme,
Expand Down
10 changes: 9 additions & 1 deletion workspaces/theme/plugins/theme/src/lightTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ export const lightThemeOverrides: Partial<ThemeConfigPalette> = {
},
text: {
primary: '#151515',
secondary: '#757575',
// PatternFly --pf-v5-global--Color--200; AA on white and #f2f2f2 nav chrome
secondary: '#6A6E73',
},
// MUI defaults (#0288d1 / #ed6c02) fail WCAG AA on white (~3.1–3.9:1)
info: {
main: '#0066CC',
},
warning: {
main: '#C05600',
},
background: {
default: '#FFFFFF',
Expand Down
Loading