Skip to content
54 changes: 30 additions & 24 deletions code/addons/docs/src/blocks/controls/Boolean.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import React, { useCallback } from 'react';
import { Button } from 'storybook/internal/components';

import { opacify, transparentize } from 'polished';
import { styled } from 'storybook/theming';
import type { CSSObject, StorybookTheme } from 'storybook/theming';
import { srOnlyStyles, styled } from 'storybook/theming';

import { getControlId, getControlSetterButtonId } from './helpers';
import type { BooleanConfig, BooleanValue, ControlProps } from './types';

const Label = styled.label(({ theme }) => ({
const getBooleanControlStyles = (theme: StorybookTheme): CSSObject => ({
lineHeight: '18px',
alignItems: 'center',
marginBottom: 8,
Expand All @@ -26,30 +27,28 @@ const Label = styled.label(({ theme }) => ({
cursor: 'not-allowed',
},
},
'@media (forced-colors: active)': {
background: 'ButtonFace',
outline: '1px solid ButtonText',
},
'&:focus-within': {
outline: `1px solid ${theme.color.secondary}`,
outlineOffset: 1,

'@media (forced-colors: active)': {
outline: '1px solid Highlight',
outlineOffset: 1,
},
},

input: {
...srOnlyStyles,
appearance: 'none',
width: '100%',
height: '100%',
position: 'absolute',
left: 0,
top: 0,
margin: 0,
padding: 0,
border: 'none',
background: 'transparent',
cursor: 'pointer',
borderRadius: '3em',

'&:focus': {
outline: 'none',
boxShadow: `${theme.color.secondary} 0 0 0 1px inset !important`,
},
'@media (forced-colors: active)': {
'&:focus': {
outline: '1px solid highlight',
},
},
},

span: {
Expand All @@ -67,10 +66,6 @@ const Label = styled.label(({ theme }) => ({
color: transparentize(0.5, theme.color.defaultText),
background: 'transparent',

'&:hover': {
boxShadow: `${opacify(0.3, theme.appBorderColor)} 0 0 0 1px inset`,
},

'&:active': {
boxShadow: `${opacify(0.05, theme.appBorderColor)} 0 0 0 2px inset`,
color: opacify(1, theme.appBorderColor),
Expand All @@ -82,6 +77,11 @@ const Label = styled.label(({ theme }) => ({
'&:last-of-type': {
paddingLeft: 8,
},

'@media (forced-colors: active)': {
color: 'ButtonText',
boxShadow: 'none',
},
},

'input:checked ~ span:last-of-type, input:not(:checked) ~ span:first-of-type': {
Expand All @@ -94,10 +94,16 @@ const Label = styled.label(({ theme }) => ({
padding: '7px 15px',

'@media (forced-colors: active)': {
textDecoration: 'underline',
forcedColorAdjust: 'none',
background: 'Highlight',
color: 'HighlightText',
boxShadow: 'none',
outline: '1px solid ButtonText',
},
},
}));
});

const Label = styled.label(({ theme }) => getBooleanControlStyles(theme));

const parse = (value: string | null): boolean => value === 'true';

Expand Down
57 changes: 56 additions & 1 deletion code/e2e-tests/addon-controls.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ test.describe('addon-controls', () => {
'background-color',
'rgb(85, 90, 185)'
);
const toggle = sbPage.panelContent().locator('input[name=primary]');
const toggle = sbPage.panelContent().locator('label:has(input[name="primary"])');
await toggle.click();
await expect(async () => {
await expect(sbPage.previewRoot().locator('button')).toHaveCSS(
Expand Down Expand Up @@ -117,4 +117,59 @@ test.describe('addon-controls', () => {
await sbPage.viewAddonPanel('Controls');
await expect(sbPage.panelContent().getByText('children').first()).toBeVisible();
});

test('should render boolean control with explicit forced-colors styling', async ({ page }) => {
await page.emulateMedia({ forcedColors: 'active' });
await page.goto(`${storybookUrl}?path=/story/example-button--primary`);

const sbPage = new SbPage(page, expect);
await sbPage.waitUntilLoaded();
await sbPage.viewAddonPanel('Controls');

const panel = sbPage.panelContent();
const label = panel.locator('label:has(input[name="primary"])');
const input = panel.locator('input[name="primary"]');
const falseOption = label.locator('span').first();
const trueOption = label.locator('span').last();
const outlineColorBeforeFocus = await label.evaluate((el) => getComputedStyle(el).outlineColor);

expect(await label.evaluate(() => matchMedia('(forced-colors: active)').matches)).toBe(true);
await input.focus();
await expect(input).toBeFocused();

const outlineColorAfterFocus = await label.evaluate((el) => getComputedStyle(el).outlineColor);

await expect(label).toHaveCSS('outline-style', 'solid');
await expect(label).toHaveCSS('outline-width', '1px');
await expect(trueOption).toHaveCSS('forced-color-adjust', 'none');
await expect(falseOption).toHaveCSS('box-shadow', 'none');
await expect(trueOption).toHaveCSS('box-shadow', 'none');
expect(outlineColorAfterFocus).not.toBe(outlineColorBeforeFocus);

const selectedBackground = await trueOption.evaluate(
(el) => getComputedStyle(el).backgroundColor
);
const unselectedBackground = await falseOption.evaluate(
(el) => getComputedStyle(el).backgroundColor
);

expect(selectedBackground).not.toBe('rgba(0, 0, 0, 0)');
expect(selectedBackground).not.toBe(unselectedBackground);
});

test('should not add hover shadow to the inactive boolean option', async ({ page }) => {
await page.goto(`${storybookUrl}?path=/story/example-button--primary`);

const sbPage = new SbPage(page, expect);
await sbPage.waitUntilLoaded();
await sbPage.viewAddonPanel('Controls');

const falseOption = sbPage
.panelContent()
.locator('label:has(input[name="primary"]) span')
.first();
await falseOption.hover();

await expect(falseOption).toHaveCSS('box-shadow', 'none');
});
});
Loading