Skip to content

Commit 507cf2f

Browse files
authored
[wb-1797] Address cell color contrast a11y issues (#2435)
## Summary: Updating state styles based on the new designs to address color contrast issues: https://www.figma.com/design/qSKXmIvmsNPKFlQNP5cztQ?node-id=4337-8746#1091493830 Styles to highlight: - If a cell is selected (active: true), there is no hover and pressed state since clicking on the cell will bring the user to the same page. It continues to be focusable - Press state and selected states now have left visual indicators of different widths so that color is not the only indicator - The disabled focus outline is now using the disabled color token - See changeset for more specific details on the style changes A new primitive color token is also added: `fadedOffBlack72`. This is the new value for `semanticColor.text.secondary`. This is a darker gray that has better color contrast with a `fadedBlue8` background Issue: WB-1797 ## Test plan: - Confirm DetailCell and CompactCell have no color contrast issues and the styling of the states are expected - DetailCell `?path=/docs/packages-cell-detailcell-all-variants--docs` - CompactCell `?path=/docs/packages-cell-compactcell-all-variants--docs` - Testing the snapshot with webapp: https://prod-znd-250120-28792-a2bc7f4.khanacademy.org/ Author: beaesguerra Reviewers: beaesguerra, jandrade Required Reviewers: Approved By: jandrade Checks: ✅ 12 checks were successful, ⏭️ 2 checks have been skipped Pull Request URL: #2435
1 parent 78c3535 commit 507cf2f

File tree

10 files changed

+292
-13
lines changed

10 files changed

+292
-13
lines changed

.changeset/shiny-chicken-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@khanacademy/wonder-blocks-tokens": minor
3+
---
4+
5+
Adds `fadedOffBlack72` color primitive token and sets the `semanticColor.text.secondary` token to this primitive. The slightly darker gray has better color contrast on a variety of backgrounds, including the fadedBlue8 background

.changeset/wise-actors-peel.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@khanacademy/wonder-blocks-cell": patch
3+
---
4+
5+
DetailCell and CompactCell: update styling to address accessibility issues (color contrast and using color as the only visual indicator). Updated styles include:
6+
7+
- General:
8+
- Changing the grey used for subtitles
9+
- Using `icon.primary` for the right accessory
10+
- Press state:
11+
- Changing the background to `fadedBlue8`
12+
- Adding a thin left border when clickable cells are pressed
13+
- Hover state:
14+
- Changing the background to `fadedBlue8`
15+
- Disabled state:
16+
- Changing the focus outline to `action.disabled.default`
17+
- Selected state (cells with `active=true`):
18+
- Adding a thick left border
19+
- Changing the text color to `activeBlue`
20+
- The styling no longer changes when a selected cell is hovered or pressed on
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import * as React from "react";
2+
import {StyleSheet} from "aphrodite";
3+
import type {Meta, StoryObj} from "@storybook/react";
4+
5+
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";
6+
import {spacing} from "@khanacademy/wonder-blocks-tokens";
7+
import {CompactCell} from "@khanacademy/wonder-blocks-cell";
8+
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
9+
import {IconMappings} from "../wonder-blocks-icon/phosphor-icon.argtypes";
10+
import {LabelSmall} from "@khanacademy/wonder-blocks-typography";
11+
12+
/**
13+
* The following stories are used to generate the pseudo states for the
14+
* CompactCell component. This is only used for visual testing in Chromatic.
15+
*/
16+
export default {
17+
title: "Packages / Cell / CompactCell / All Variants",
18+
parameters: {
19+
docs: {
20+
autodocs: false,
21+
},
22+
backgrounds: {
23+
default: "offWhite",
24+
},
25+
},
26+
} as Meta;
27+
28+
type StoryComponentType = StoryObj<typeof CompactCell>;
29+
30+
const states = [
31+
{
32+
label: "Default",
33+
props: {},
34+
},
35+
{
36+
label: "Disabled",
37+
props: {disabled: true},
38+
},
39+
{
40+
label: "Selected using active: true",
41+
props: {active: true},
42+
},
43+
];
44+
45+
const defaultProps = {
46+
title: "Title for article item",
47+
leftAccessory: (
48+
<PhosphorIcon icon={IconMappings.playCircle} size="medium" />
49+
),
50+
rightAccessory: <PhosphorIcon icon={IconMappings.caretRight} />,
51+
};
52+
53+
const States = (props: {label: string} & PropsFor<typeof CompactCell>) => {
54+
return (
55+
<View>
56+
<View style={[styles.scenarios]}>
57+
{states.map((scenario) => {
58+
return (
59+
<View style={styles.scenario} key={scenario.label}>
60+
<LabelSmall>
61+
{props.label} ({scenario.label})
62+
</LabelSmall>
63+
<CompactCell {...props} {...scenario.props} />
64+
</View>
65+
);
66+
})}
67+
</View>
68+
</View>
69+
);
70+
};
71+
72+
const AllVariants = () => (
73+
<View style={{gap: spacing.large_24}}>
74+
<States label="Default" {...defaultProps} />
75+
<States label="Clickable" {...defaultProps} onClick={() => {}} />
76+
<States label="Link" {...defaultProps} href="/" />
77+
</View>
78+
);
79+
80+
export const Default: StoryComponentType = {
81+
render: AllVariants,
82+
};
83+
84+
export const Hover: StoryComponentType = {
85+
render: AllVariants,
86+
parameters: {pseudo: {hover: true}},
87+
};
88+
89+
export const Focus: StoryComponentType = {
90+
render: AllVariants,
91+
parameters: {pseudo: {focusVisible: true}},
92+
};
93+
94+
export const HoverFocus: StoryComponentType = {
95+
name: "Hover + Focus",
96+
render: AllVariants,
97+
parameters: {pseudo: {hover: true, focusVisible: true}},
98+
};
99+
100+
export const Active: StoryComponentType = {
101+
render: AllVariants,
102+
parameters: {pseudo: {active: true}},
103+
};
104+
105+
const styles = StyleSheet.create({
106+
statesContainer: {
107+
padding: spacing.medium_16,
108+
},
109+
scenarios: {
110+
display: "flex",
111+
flexDirection: "row",
112+
alignItems: "center",
113+
gap: spacing.xxxLarge_64,
114+
flexWrap: "wrap",
115+
},
116+
scenario: {
117+
gap: spacing.small_12,
118+
overflow: "hidden",
119+
},
120+
});

__docs__/wonder-blocks-cell/detail-cell-variants.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const states = [
3737
props: {disabled: true},
3838
},
3939
{
40-
label: "Active",
40+
label: "Selected using active: true",
4141
props: {active: true},
4242
},
4343
];

__docs__/wonder-blocks-cell/detail-cell.stories.tsx

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import {StyleSheet} from "aphrodite";
33
import {MemoryRouter, Route, Switch} from "react-router-dom";
44
import type {Meta, StoryObj} from "@storybook/react";
55

6-
import {View} from "@khanacademy/wonder-blocks-core";
7-
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
6+
import {PropsFor, View} from "@khanacademy/wonder-blocks-core";
7+
import {
8+
border,
9+
color,
10+
semanticColor,
11+
spacing,
12+
} from "@khanacademy/wonder-blocks-tokens";
813
import {PhosphorIcon} from "@khanacademy/wonder-blocks-icon";
914

1015
import {DetailCell} from "@khanacademy/wonder-blocks-cell";
@@ -464,6 +469,82 @@ export const Scenarios = () => {
464469
);
465470
};
466471

472+
/**
473+
* Custom styling can be applied to the component using the `rootStyle` or
474+
* `style` props.
475+
*/
476+
export const CustomRootStyle = {
477+
args: {
478+
title: "Title for article item",
479+
subtitle1: "Subtitle for article item",
480+
subtitle2: "Subtitle for article item",
481+
leftAccessory: (
482+
<PhosphorIcon icon={IconMappings.playCircle} size="medium" />
483+
),
484+
},
485+
render(args: PropsFor<typeof DetailCell>) {
486+
return (
487+
<View style={{gap: spacing.large_24}}>
488+
Active (with rootStyle prop):
489+
<DetailCell
490+
{...args}
491+
rootStyle={{borderRadius: border.radius.xLarge_12}}
492+
active={true}
493+
/>
494+
Pressed (with rootStyle prop):
495+
<DetailCell
496+
{...args}
497+
rootStyle={{borderRadius: border.radius.xLarge_12}}
498+
/>
499+
Different content heights (with style prop)
500+
<View
501+
style={{
502+
display: "grid",
503+
gridTemplateColumns: "1fr 1fr 1fr",
504+
gap: "16px",
505+
}}
506+
>
507+
<DetailCell
508+
title="Title"
509+
subtitle1="Subtitle 1"
510+
subtitle2="Subtitle2"
511+
onClick={() => {}}
512+
style={[
513+
{
514+
border: `1px solid ${semanticColor.border.primary}`,
515+
},
516+
]}
517+
horizontalRule={"none"}
518+
/>
519+
<DetailCell
520+
title="Title"
521+
onClick={() => {}}
522+
style={[
523+
args.rootStyle,
524+
{
525+
border: `1px solid ${semanticColor.border.primary}`,
526+
},
527+
]}
528+
horizontalRule={"none"}
529+
/>
530+
<DetailCell
531+
title="Title"
532+
onClick={() => {}}
533+
style={[
534+
args.rootStyle,
535+
{
536+
border: `1px solid ${semanticColor.border.primary}`,
537+
},
538+
]}
539+
horizontalRule={"none"}
540+
/>
541+
</View>
542+
</View>
543+
);
544+
},
545+
parameters: {pseudo: {active: true}},
546+
};
547+
467548
const styles = StyleSheet.create({
468549
example: {
469550
backgroundColor: color.offWhite,

packages/wonder-blocks-cell/src/components/detail-cell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from "react";
22
import {StyleSheet} from "aphrodite";
33

44
import {Strut} from "@khanacademy/wonder-blocks-layout";
5-
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
5+
import {semanticColor, spacing} from "@khanacademy/wonder-blocks-tokens";
66
import {LabelSmall, LabelMedium} from "@khanacademy/wonder-blocks-typography";
77

88
import CellCore from "./internal/cell-core";
@@ -94,7 +94,7 @@ const DetailCell = function (props: DetailCellProps): React.ReactElement {
9494

9595
const styles = StyleSheet.create({
9696
subtitle: {
97-
color: color.offBlack64,
97+
color: semanticColor.text.secondary,
9898
},
9999

100100
// This is to override the default padding of the CellCore innerWrapper.

packages/wonder-blocks-cell/src/components/internal/cell-core.tsx

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import type {StyleType} from "@khanacademy/wonder-blocks-core";
66
import Clickable from "@khanacademy/wonder-blocks-clickable";
77
import {View} from "@khanacademy/wonder-blocks-core";
88
import {Strut} from "@khanacademy/wonder-blocks-layout";
9-
import {color, spacing} from "@khanacademy/wonder-blocks-tokens";
9+
import {
10+
border,
11+
color,
12+
semanticColor,
13+
spacing,
14+
} from "@khanacademy/wonder-blocks-tokens";
1015

1116
import {CellMeasurements, getHorizontalRuleStyles} from "./common";
1217

@@ -118,7 +123,11 @@ function CellInner(props: CellCoreProps): React.ReactElement {
118123
// custom styles
119124
style,
120125
horizontalRuleStyles,
126+
active && styles.activeInnerWrapper,
121127
]}
128+
// Set className so we can set styles on the inner wrapper directly
129+
// when the clickable element is pressed
130+
className="inner-wrapper"
122131
>
123132
{/* Left accessory */}
124133
<LeftAccessory
@@ -235,6 +244,12 @@ const styles = StyleSheet.create({
235244
padding: `${CellMeasurements.cellPadding.paddingVertical}px ${CellMeasurements.cellPadding.paddingHorizontal}px`,
236245
flexDirection: "row",
237246
flex: 1,
247+
borderRadius: "inherit",
248+
// Hide overflow so that if custom styling applies a border radius, the
249+
// left visual indicator for press/active states does not overflow
250+
overflow: "hidden",
251+
// Make sure inner wrapper is always the same height as parent
252+
height: "100%",
238253

239254
// Reduce the padding of the innerWrapper when the focus ring is
240255
// visible.
@@ -244,6 +259,18 @@ const styles = StyleSheet.create({
244259
}px`,
245260
},
246261
},
262+
activeInnerWrapper: {
263+
position: "relative",
264+
":before": {
265+
content: "''",
266+
position: "absolute",
267+
top: 0,
268+
left: 0,
269+
bottom: 0,
270+
width: border.width.thick,
271+
backgroundColor: semanticColor.surface.emphasis,
272+
},
273+
},
247274

248275
content: {
249276
alignSelf: "center",
@@ -264,7 +291,7 @@ const styles = StyleSheet.create({
264291
accessoryRight: {
265292
// The right accessory will have this color by default. Unless the
266293
// accessory element overrides that color internally.
267-
color: color.offBlack64,
294+
color: semanticColor.icon.primary,
268295
},
269296

270297
/**
@@ -309,28 +336,52 @@ const styles = StyleSheet.create({
309336
border: `${spacing.xxxxSmall_2}px solid ${color.blue}`,
310337
borderRadius: spacing.xxxSmall_4,
311338
},
339+
[":focus-visible[aria-disabled=true]:after" as any]: {
340+
borderColor: semanticColor.focus.outer,
341+
},
312342

313343
// hover + enabled
314344
[":hover[aria-disabled=false]" as any]: {
315-
background: color.offBlack8,
345+
background: color.fadedBlue8,
316346
},
317347

318348
// pressed + enabled
319349
[":active[aria-disabled=false]" as any]: {
320-
background: color.offBlack16,
350+
background: color.fadedBlue8,
321351
},
352+
// press + enabled + not currently selected (active prop: false)
353+
// We apply the left bar indicator styles on the inner-wrapper element
354+
// instead of the clickable element directly because we need to hide the
355+
// left bar overflow when custom cell styles apply a border-radius. We
356+
// have overflow: hidden on the inner wrapper instead of the clickable element
357+
// because setting it on the clickable element causes issues with existing
358+
// cases.
359+
[":active[aria-disabled=false]:not([aria-current=true]) .inner-wrapper" as any]:
360+
{
361+
position: "relative",
362+
":before": {
363+
content: "''",
364+
position: "absolute",
365+
top: 0,
366+
left: 0,
367+
bottom: 0,
368+
width: border.width.thin,
369+
backgroundColor: semanticColor.surface.emphasis,
370+
},
371+
},
322372
},
323373

324374
active: {
325375
background: color.fadedBlue8,
326-
color: color.blue,
376+
color: color.activeBlue,
377+
cursor: "default",
327378

328379
[":hover[aria-disabled=false]" as any]: {
329-
background: color.fadedBlue16,
380+
background: color.fadedBlue8,
330381
},
331382

332383
[":active[aria-disabled=false]" as any]: {
333-
background: color.fadedBlue24,
384+
background: color.fadedBlue8,
334385
},
335386
},
336387

0 commit comments

Comments
 (0)