Skip to content

Commit c6023bb

Browse files
authored
Invert foreground color when background is light (#15230)
* Invert foreground color when background is light For readability, the foreground color is inverted when the background is light. This is based on WCGA2.0 spec.
1 parent c1a7bea commit c6023bb

File tree

17 files changed

+354
-54
lines changed

17 files changed

+354
-54
lines changed

components/brave_new_tab_ui/components/default/braveToday/hint.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import styled from 'styled-components'
88
import { CaratStrongDownIcon } from 'brave-ui/components/icons'
99
import { getLocale } from '../../../../common/locale'
1010

11-
const Hint = styled('div')`
11+
const Hint = styled('div')<{}>`
1212
display: flex;
1313
flex-direction: column;
1414
align-items: center;
1515
gap: 12px;
1616
text-align: center;
1717
font-size: 15px;
18-
color: white;
18+
color: var(--override-readability-color, #FFFFFF);
1919
> p {
2020
margin: 0;
2121
}

components/brave_new_tab_ui/components/default/clock/style.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import styled from 'styled-components'
77

88
export const StyledClock = styled('div')<{}>`
9-
color: #FFFFFF;
9+
color: var(--override-readability-color, #FFFFFF);
1010
box-sizing: border-box;
1111
line-height: 1;
1212
user-select: none;

components/brave_new_tab_ui/components/default/footer/braveTalkTooltip/braveTalkIcon.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export default function Icon () {
99
return (
1010
<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24'>
1111
<path
12-
fill='#fff'
1312
fillRule='evenodd'
1413
d='M23.5 20.18a.95.95 0 01-.99-.05l-4.26-2.84v1.09c0 1.05-.86 1.91-1.92 1.91H2.92A1.92 1.92 0 011 18.38V5.92C1 4.86 1.86 4 2.92 4h13.41c1.06 0 1.92.86 1.92 1.92V7l4.26-2.84a.96.96 0 011.49.8v14.37c0 .36-.2.68-.5.85zM22.07 6.75l-4.26 2.84c-.02.02-.05.02-.08.03a.93.93 0 01-.36.11l-.09.02-.1-.02a.91.91 0 01-.64-.34c-.01-.03-.04-.04-.06-.07l-.03-.08a.92.92 0 01-.11-.37l-.02-.08V5.92H2.92v12.5h13.41V15.5l.02-.09a.9.9 0 01.04-.18.91.91 0 01.07-.18l.03-.08.07-.07a.93.93 0 01.64-.34l.1-.02.07.02a.93.93 0 01.37.11l.08.03 4.26 2.84V6.75z'
1514
clipRule='evenodd'

components/brave_new_tab_ui/components/default/gridSites/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const TileTitle = styled('p')<{}>`
1313
line-height: 17px;
1414
max-width: 100%;
1515
height: 17px;
16-
color: white;
16+
color: var(--override-readability-color, white);
1717
padding: 0 2px;
1818
overflow: hidden;
1919
white-space: nowrap;

components/brave_new_tab_ui/components/default/page/index.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* You can obtain one at http://mozilla.org/MPL/2.0/. */
44

55
import * as React from 'react'
6-
import styled, { css } from 'styled-components'
6+
import styled, { createGlobalStyle, css } from 'styled-components'
77
import { requestAnimationFrameThrottle } from '../../../../common/throttle'
88

99
const breakpointLargeBlocks = '980px'
@@ -393,7 +393,7 @@ export const IconLink = styled('a')<{}>`
393393
height: 24px;
394394
margin: 8px;
395395
cursor: pointer;
396-
color: #ffffff;
396+
color: var(--override-readability-color, #ffffff);
397397
opacity: 0.7;
398398
transition: opacity 0.15s ease, filter 0.15s ease;
399399
@@ -412,7 +412,7 @@ export const IconButton = styled('button')<IconButtonProps>`
412412
outline: none;
413413
margin: ${p => p.isClickMenu ? '7' : '0 12'}px;
414414
cursor: pointer;
415-
color: #ffffff;
415+
color: var(--override-readability-color, #ffffff);
416416
background-color: transparent;
417417
opacity: 0.7;
418418
transition: opacity 0.15s ease, filter 0.15s ease;
@@ -467,13 +467,21 @@ export const IconButtonContainer = styled('div')<IconButtonContainerProps>`
467467
font-family: ${p => p.theme.fontFamily.heading};
468468
font-size: 13px;
469469
font-weight: 600;
470-
color: rgba(255,255,255,0.8);
470+
color: rgba(var(--override-readability-color-rgb, 255, 255, 255), 0.8);
471471
margin-right: ${p => p.textDirection === 'ltr' && '8px'};
472472
margin-left: ${p => p.textDirection === 'rtl' && '8px'};
473-
border-right: ${p => p.textDirection === 'ltr' && '1px solid rgba(255, 255, 255, 0.6)'};
474-
border-left: ${p => p.textDirection === 'rtl' && '1px solid rgba(255, 255, 255, 0.6)'};
473+
border-right: ${p => p.textDirection === 'ltr' && '1px solid rgba(var(--override-readability-color-rgb, 255, 255, 255), 0.6)'};
474+
border-left: ${p => p.textDirection === 'rtl' && '1px solid rgba(var(--override-readability-color-rgb, 255, 255, 255), 0.6)'};
475475
476476
&:hover {
477-
color: #ffffff;
477+
color: ${p => p.color};
478+
}
479+
`
480+
481+
export const OverrideReadabilityColor = createGlobalStyle<{override: boolean}>`
482+
:root {
483+
${p => p.override && css`
484+
--override-readability-color-rgb: 0, 0, 0;
485+
--override-readability-color: rgb(0, 0, 0);`}
478486
}
479487
`

components/brave_new_tab_ui/components/default/stats/style.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,10 @@ export const StyledStatsItem = styled('li')<{}>`
2323
font-size: inherit;
2424
font-family: inherit;
2525
margin: 10px 16px;
26-
27-
&:first-child {
28-
color: #FB542B;
29-
}
30-
&:nth-child(2) {
31-
color: #A0A5EB;
32-
}
33-
&:last-child {
34-
color: #FFFFFF;
26+
&:first-child { color: var(--override-readability-color, var(--interactive2)); }
27+
&:nth-child(2) { color: var(--override-readability-color, var(--interactive9)); }
28+
&:last-child {
29+
color: var(--override-readability-color, #FFFFFF);
3530
margin-right: 0;
3631
}
3732
`
@@ -59,7 +54,7 @@ export const StyledStatsItemText = styled('span')<{}>`
5954
export const StyledStatsItemDescription = styled('div')<{}>`
6055
font-size: 16px;
6156
font-weight: 500;
62-
color: #FFFFFF;
57+
color: var(--override-readability-color-rgb, #FFFFFF);
6358
margin-top: 8px;
6459
font-family: ${p => p.theme.fontFamily.heading};
6560
`

components/brave_new_tab_ui/components/default/widget/assets/ellipsis.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

components/brave_new_tab_ui/components/default/widget/index.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export interface WidgetProps {
1818
widgetTitle?: string
1919
hideMenu?: boolean
2020
isForeground?: boolean
21-
lightWidget?: boolean
2221
paddingType: 'none' | 'right' | 'default'
2322
onLearnMore?: () => void
2423
onDisconnect?: () => void
@@ -42,7 +41,6 @@ export function Widget ({
4241
widgetTitle,
4342
hideMenu,
4443
isForeground,
45-
lightWidget,
4644
paddingType,
4745
onLearnMore,
4846
onDisconnect,
@@ -78,7 +76,6 @@ export function Widget ({
7876
hideWidget={hideWidget}
7977
persistWidget={() => setWidgetMenuPersist(true)}
8078
unpersistWidget={() => setWidgetMenuPersist(false)}
81-
lightWidget={lightWidget}
8279
paddingType={paddingType} />}
8380
</StyledWidgetContainer>
8481
}

components/brave_new_tab_ui/components/default/widget/widgetMenu.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as React from 'react'
77

88
import { StyledWidgetMenuContainer, StyledWidgetMenu, StyledWidgetButton, StyledWidgetIcon, StyledSpan, StyledWidgetLink, StyledEllipsis } from './styles'
99
import { IconButton } from '../../default'
10-
import EllipsisIcon from './assets/ellipsis'
10+
import EllipsisIcon from '../../popupMenu/ellipsisIcon'
1111
import HideIcon from './assets/hide'
1212
import AddSiteIcon from './assets/add-site'
1313
import FrecencyIcon from './assets/frecency'
@@ -32,7 +32,6 @@ interface Props {
3232
onAddSite?: () => void
3333
customLinksEnabled?: boolean
3434
onToggleCustomLinksEnabled?: () => void
35-
lightWidget?: boolean
3635
paddingType: 'none' | 'right' | 'default'
3736
}
3837

@@ -100,7 +99,6 @@ export default class WidgetMenu extends React.PureComponent<Props, State> {
10099
widgetMenuPersist,
101100
widgetTitle,
102101
isForeground,
103-
lightWidget,
104102
paddingType,
105103
onLearnMore,
106104
onDisconnect,
@@ -116,7 +114,7 @@ export default class WidgetMenu extends React.PureComponent<Props, State> {
116114
<StyledWidgetMenuContainer ref={this.settingsMenuRef} paddingType={paddingType}>
117115
<StyledEllipsis widgetMenuPersist={widgetMenuPersist} isForeground={isForeground}>
118116
<IconButton isClickMenu={true} onClick={this.toggleMenu}>
119-
<EllipsisIcon lightWidget={lightWidget} />
117+
<EllipsisIcon />
120118
</IconButton>
121119
</StyledEllipsis>
122120
{showMenu && <StyledWidgetMenu

components/brave_new_tab_ui/containers/newTab/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
CryptoDotComWidget as CryptoDotCom,
2020
EditTopSite,
2121
SearchPromotion,
22-
EditCards
22+
EditCards,
23+
OverrideReadabilityColor
2324
} from '../../components/default'
2425
import { FTXWidget as FTX } from '../../widgets/ftx/components'
2526
import * as Page from '../../components/default/page'
@@ -40,6 +41,7 @@ import {
4041
fetchCryptoDotComSupportedPairs
4142
} from '../../api/cryptoDotCom'
4243
import { generateQRData } from '../../binance-utils'
44+
import isReadableOnBackground from '../../helpers/colorUtil'
4345

4446
// Types
4547
import { GeminiAssetAddress } from '../../actions/gemini_actions'
@@ -217,6 +219,10 @@ class NewTabPage extends React.Component<Props, State> {
217219
}
218220
}
219221

222+
shouldOverrideReadabilityColor (newTabData: NewTab.State) {
223+
return !newTabData.brandedWallpaper && newTabData.backgroundWallpaper?.type === 'color' && !isReadableOnBackground(newTabData.backgroundWallpaper)
224+
}
225+
220226
handleResize () {
221227
this.setState({
222228
forceToHideWidget: GetShouldForceToHideWidget(this.props, this.state.showSearchPromotion)
@@ -1166,6 +1172,7 @@ class NewTabPage extends React.Component<Props, State> {
11661172
imageHasLoaded={this.state.backgroundHasLoaded}
11671173
colorForBackground={colorForBackground}
11681174
data-show-news-prompt={((this.state.backgroundHasLoaded || colorForBackground) && this.state.isPromptingBraveToday) ? true : undefined}>
1175+
<OverrideReadabilityColor override={ this.shouldOverrideReadabilityColor(this.props.newTabData) } />
11691176
<Page.Page
11701177
hasImage={hasImage}
11711178
imageSrc={this.imageSource}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright (c) 2022 The Brave Authors. All rights reserved.
2+
// This Source Code Form is subject to the terms of the Mozilla Public
3+
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
4+
// you can obtain one at http://mozilla.org/MPL/2.0/.
5+
6+
import * as ColorUtil from './colorUtil'
7+
8+
describe('ColorData', () => {
9+
describe('constructor', () => {
10+
it('can be constructed from HEX color', () => {
11+
const color = new ColorUtil.ColorData('#123456')
12+
expect(color.type).toBe(ColorUtil.ColorType.HEX)
13+
expect(color.r).toBe(0x12)
14+
expect(color.g).toBe(0x34)
15+
expect(color.b).toBe(0x56)
16+
})
17+
18+
it('can be constructed from HEX shorthand color', () => {
19+
const color = new ColorUtil.ColorData('#123')
20+
expect(color.type).toBe(ColorUtil.ColorType.HEX)
21+
expect(color.r).toBe(0x11)
22+
expect(color.g).toBe(0x22)
23+
expect(color.b).toBe(0x33)
24+
})
25+
26+
it('can be constructed from rgb()', () => {
27+
let color = new ColorUtil.ColorData('rgb(1, 2, 3)')
28+
expect(color.type).toBe(ColorUtil.ColorType.RGB)
29+
expect(color.r).toBe(1)
30+
expect(color.g).toBe(2)
31+
expect(color.b).toBe(3)
32+
33+
color = new ColorUtil.ColorData('rgb(0, 0, 0)')
34+
expect(color.type).toBe(ColorUtil.ColorType.RGB)
35+
expect(color.r).toBe(0)
36+
expect(color.g).toBe(0)
37+
expect(color.b).toBe(0)
38+
39+
color = new ColorUtil.ColorData('rgb(255, 255, 255)')
40+
expect(color.type).toBe(ColorUtil.ColorType.RGB)
41+
expect(color.r).toBe(255)
42+
expect(color.g).toBe(255)
43+
expect(color.b).toBe(255)
44+
})
45+
})
46+
47+
describe('getRelativeLuminance()', () => {
48+
// Can cross check values from https://planetcalc.com/7779/?color=%23FFFFFF
49+
// (modify |color| query value)
50+
it('returns 0.0 for black', () => { expect(new ColorUtil.ColorData('rgb(0, 0, 0)').getRelativeLuminance()).toBe(0) })
51+
it('returns 1.0 for white', () => { expect(new ColorUtil.ColorData('rgb(255, 255, 255)').getRelativeLuminance()).toBe(1) })
52+
it('returns 0.2126 for red', () => { expect(new ColorUtil.ColorData('rgb(255, 0, 0)').getRelativeLuminance()).toBe(0.2126) })
53+
it('returns 0.7152 for green', () => { expect(new ColorUtil.ColorData('rgb(0, 255, 0)').getRelativeLuminance()).toBe(0.7152) })
54+
it('returns 0.0722 for blue', () => { expect(new ColorUtil.ColorData('rgb(0, 0, 255)').getRelativeLuminance()).toBe(0.0722) })
55+
})
56+
57+
describe('luminanceCache', () => {
58+
beforeEach(() => { ColorUtil.ColorData.clearCacheForTesting() })
59+
60+
it('has L for rgb(255, 255, 255) in the beginning', () => { expect(new ColorUtil.ColorData('rgb(255, 255, 255)').hasCachedLuminance()).toBeTruthy() })
61+
it('has L for #ffffff in the beginning', () => { expect(new ColorUtil.ColorData('#ffffff').hasCachedLuminance()).toBeTruthy() })
62+
it('has L for rgb(0, 0, 0) in the beginning', () => { expect(new ColorUtil.ColorData('rgb(0, 0, 0)').hasCachedLuminance()).toBeTruthy() })
63+
it('has L for #000 in the beginning', () => { expect(new ColorUtil.ColorData('#000').hasCachedLuminance()).toBeTruthy() })
64+
it('caches L once a L is calculated', () => {
65+
const color = new ColorUtil.ColorData('rgb(255, 0, 0)')
66+
expect(color.hasCachedLuminance()).toBeFalsy()
67+
68+
// Calculate L and make cache hot
69+
color.getRelativeLuminance()
70+
expect(color.hasCachedLuminance()).toBeTruthy()
71+
72+
// Equivalent color should use the same cache
73+
expect(new ColorUtil.ColorData('#ff0000').hasCachedLuminance).toBeTruthy()
74+
})
75+
})
76+
})
77+
78+
describe('getContrastRatio()', () => {
79+
// Can cross check values from https://planetcalc.com/7779/?color1=%23FFFFFF&color2=%23000000
80+
// (modify |color1| and |color2| query value)
81+
it('returns 1.0 for same color', () => {
82+
expect(ColorUtil.getContrastRatio(new ColorUtil.ColorData('rgb(0, 0, 0)'),
83+
new ColorUtil.ColorData('#000'))).toBe(1)
84+
})
85+
86+
it('returns 21 for black and white', () => {
87+
expect(ColorUtil.getContrastRatio(new ColorUtil.ColorData('#fff'),
88+
new ColorUtil.ColorData('#000'))).toBe(21)
89+
})
90+
91+
it('returns 2.91 for red and green', () => {
92+
expect(ColorUtil.getContrastRatio(new ColorUtil.ColorData('#f00'),
93+
new ColorUtil.ColorData('#0f0'))).toBe(2.9139375476009137)
94+
})
95+
96+
it('returns 6.26 for green and blue', () => {
97+
expect(ColorUtil.getContrastRatio(new ColorUtil.ColorData('#0f0'),
98+
new ColorUtil.ColorData('#00f'))).toBe(6.261865793780687)
99+
})
100+
101+
it('returns 2.14 for blue and red', () => {
102+
expect(ColorUtil.getContrastRatio(new ColorUtil.ColorData('#00f'),
103+
new ColorUtil.ColorData('#f00'))).toBe(2.148936170212766)
104+
})
105+
})
106+
107+
describe('isReadable()', () => {
108+
describe('When background is black, stats are visible', () => {
109+
const background = new ColorUtil.ColorData('#000')
110+
it('returns true for tracker stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#FB542B'/* --interactive2 */))).toBeTruthy() })
111+
it('returns true for bandwidth saved stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#A0A5EB'/* --interactive9 */))).toBeTruthy() })
112+
it('returns true for time saved stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#FFFFFF'))).toBeTruthy() })
113+
})
114+
115+
describe('When background is #F0CB44, stats are not visible', () => {
116+
const background = new ColorUtil.ColorData('#F0CB44')
117+
it('returns true for tracker stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#FB542B'/* --interactive2 */))).toBeTruthy() })
118+
it('returns false for bandwidth saved stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#A0A5EB'/* --interactive9 */))).toBeFalsy() })
119+
it('returns true for time saved stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#FFFFFF'))).toBeTruthy() })
120+
})
121+
122+
describe('When background is #2197F9 stats are not visible', () => {
123+
const background = new ColorUtil.ColorData('#2197F9')
124+
it('returns false for tracker stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#FB542B'/* --interactive2 */))).toBeFalsy() })
125+
it('returns false for bandwidth saved stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#A0A5EB'/* --interactive9 */))).toBeFalsy() })
126+
it('returns true for time saved stat color', () => { expect(ColorUtil.isReadable(background, new ColorUtil.ColorData('#FFFFFF'))).toBeTruthy() })
127+
})
128+
})

0 commit comments

Comments
 (0)