1
- import React , { forwardRef , MouseEventHandler , useMemo } from 'react'
2
1
import { CSSObject } from '@styled-system/css'
2
+ import { getContrast , getLuminance , toHex } from 'color2k'
3
+ import { Hsluv } from 'hsluv'
4
+ import React from 'react'
5
+ import { get } from '../constants'
6
+ import { useTheme } from '../ThemeProvider'
7
+ import { ForwardRefComponent as PolymorphicForwardRefComponent } from '../utils/polymorphic'
3
8
import TokenBase , { defaultTokenSize , isTokenInteractive , TokenBaseProps } from './TokenBase'
4
9
import RemoveTokenButton from './_RemoveTokenButton'
5
- import { parseToHsla , parseToRgba } from 'color2k'
6
- import { useTheme } from '../ThemeProvider'
7
10
import TokenTextContainer from './_TokenTextContainer'
8
- import { ForwardRefComponent as PolymorphicForwardRefComponent } from '../utils/polymorphic'
9
11
10
12
export interface IssueLabelTokenProps extends TokenBaseProps {
11
13
/**
@@ -16,31 +18,7 @@ export interface IssueLabelTokenProps extends TokenBaseProps {
16
18
17
19
const tokenBorderWidthPx = 1
18
20
19
- const lightModeStyles = {
20
- '--lightness-threshold' : '0.453' ,
21
- '--border-threshold' : '0.96' ,
22
- '--border-alpha' : 'max(0, min(calc((var(--perceived-lightness) - var(--border-threshold)) * 100), 1))' ,
23
- background : 'rgb(var(--label-r), var(--label-g), var(--label-b))' ,
24
- color : 'hsl(0, 0%, calc(var(--lightness-switch) * 100%))' ,
25
- borderWidth : tokenBorderWidthPx ,
26
- borderStyle : 'solid' ,
27
- borderColor : 'hsla(var(--label-h),calc(var(--label-s) * 1%),calc((var(--label-l) - 25) * 1%),var(--border-alpha))' ,
28
- }
29
-
30
- const darkModeStyles = {
31
- '--lightness-threshold' : '0.6' ,
32
- '--background-alpha' : '0.18' ,
33
- '--border-alpha' : '0.3' ,
34
- '--lighten-by' : 'calc(((var(--lightness-threshold) - var(--perceived-lightness)) * 100) * var(--lightness-switch))' ,
35
- borderWidth : tokenBorderWidthPx ,
36
- borderStyle : 'solid' ,
37
- background : 'rgba(var(--label-r), var(--label-g), var(--label-b), var(--background-alpha))' ,
38
- color : 'hsl(var(--label-h), calc(var(--label-s) * 1%), calc((var(--label-l) + var(--lighten-by)) * 1%))' ,
39
- borderColor :
40
- 'hsla(var(--label-h), calc(var(--label-s) * 1%),calc((var(--label-l) + var(--lighten-by)) * 1%),var(--border-alpha))' ,
41
- }
42
-
43
- const IssueLabelToken = forwardRef ( ( props , forwardedRef ) => {
21
+ const IssueLabelToken = React . forwardRef ( ( props , forwardedRef ) => {
44
22
const {
45
23
as,
46
24
fillColor = '#999' ,
@@ -54,42 +32,52 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => {
54
32
onClick,
55
33
...rest
56
34
} = props
57
- const interactiveTokenProps = {
58
- as,
59
- href,
60
- onClick,
61
- }
62
- const { colorScheme} = useTheme ( )
35
+
36
+ const interactiveTokenProps = { as, href, onClick}
37
+
38
+ const colorMode = useColorMode ( )
39
+
63
40
const hasMultipleActionTargets = isTokenInteractive ( props ) && Boolean ( onRemove ) && ! hideRemoveButton
64
- const onRemoveClick : MouseEventHandler = e => {
65
- e . stopPropagation ( )
66
- onRemove && onRemove ( )
41
+
42
+ const onRemoveClick : React . MouseEventHandler = event => {
43
+ event . stopPropagation ( )
44
+ onRemove ?.( )
67
45
}
68
- const labelStyles : CSSObject = useMemo ( ( ) => {
69
- const [ r , g , b ] = parseToRgba ( fillColor )
70
- const [ h , s , l ] = parseToHsla ( fillColor )
71
46
72
- // label hack taken from https://github.com/github/github/blob/master/app/assets/stylesheets/hacks/hx_primer-labels.scss#L43-L108
73
- // this logic should eventually live in primer/components. Also worthy of note is that the dotcom hack code will be moving to primer/css soon.
47
+ const labelStyles : CSSObject = React . useMemo ( ( ) => {
48
+ // Parse label color into hue, saturation, lightness using HSLUV
49
+ const { h, s} = hexToHsluv ( fillColor )
50
+
51
+ // Initialize color variables
52
+ let bgColor = ''
53
+ let textColor = ''
54
+ let borderColor = ''
55
+
56
+ // Set color variables based on current color mode
57
+ switch ( colorMode ) {
58
+ case 'light' : {
59
+ bgColor = hsluvToHex ( { h, s : Math . min ( s , 90 ) , l : 97 } )
60
+ textColor = minContrast ( hsluvToHex ( { h, s : Math . min ( s , 85 ) , l : 45 } ) , bgColor , 4.5 )
61
+ borderColor = hsluvToHex ( { h, s : Math . min ( s , 70 ) , l : 82 } )
62
+ break
63
+ }
64
+
65
+ case 'dark' : {
66
+ bgColor = hsluvToHex ( { h, s : Math . min ( s , 90 ) , l : 8 } )
67
+ textColor = minContrast ( hsluvToHex ( { h, s : Math . min ( s , 50 ) , l : 70 } ) , bgColor , 4.5 )
68
+ borderColor = hsluvToHex ( { h, s : Math . min ( s , 80 ) , l : 20 } )
69
+ break
70
+ }
71
+ }
72
+
74
73
return {
75
- '--label-r' : String ( r ) ,
76
- '--label-g' : String ( g ) ,
77
- '--label-b' : String ( b ) ,
78
- '--label-h' : String ( Math . round ( h ) ) ,
79
- '--label-s' : String ( Math . round ( s * 100 ) ) ,
80
- '--label-l' : String ( Math . round ( l * 100 ) ) ,
81
- '--perceived-lightness' :
82
- 'calc(((var(--label-r) * 0.2126) + (var(--label-g) * 0.7152) + (var(--label-b) * 0.0722)) / 255)' ,
83
- '--lightness-switch' : 'max(0, min(calc((var(--perceived-lightness) - var(--lightness-threshold)) * -1000), 1))' ,
84
- paddingRight : hideRemoveButton || ! onRemove ? undefined : 0 ,
85
74
position : 'relative' ,
86
- ...( colorScheme === 'light' ? lightModeStyles : darkModeStyles ) ,
75
+ color : textColor ,
76
+ background : bgColor ,
77
+ border : `${ tokenBorderWidthPx } px solid ${ borderColor } ` ,
78
+ paddingRight : onRemove && ! hideRemoveButton ? 0 : undefined ,
87
79
...( isSelected
88
80
? {
89
- background :
90
- colorScheme === 'light'
91
- ? 'hsl(var(--label-h), calc(var(--label-s) * 1%), calc((var(--label-l) - 5) * 1%))'
92
- : darkModeStyles . background ,
93
81
':focus' : {
94
82
outline : 'none' ,
95
83
} ,
@@ -103,17 +91,13 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => {
103
91
left : `-${ tokenBorderWidthPx * 2 } px` ,
104
92
display : 'block' ,
105
93
pointerEvents : 'none' ,
106
- boxShadow : `0 0 0 ${ tokenBorderWidthPx * 2 } px ${
107
- colorScheme === 'light'
108
- ? 'rgb(var(--label-r), var(--label-g), var(--label-b))'
109
- : 'hsl(var(--label-h), calc(var(--label-s) * 1%), calc((var(--label-l) + var(--lighten-by)) * 1%))'
110
- } `,
94
+ boxShadow : `0 0 0 ${ tokenBorderWidthPx * 2 } px ${ textColor } ` ,
111
95
borderRadius : '999px' ,
112
96
} ,
113
97
}
114
98
: { } ) ,
115
99
}
116
- } , [ colorScheme , fillColor , isSelected , hideRemoveButton , onRemove ] )
100
+ } , [ colorMode , fillColor , isSelected , hideRemoveButton , onRemove ] )
117
101
118
102
return (
119
103
< TokenBase
@@ -152,3 +136,49 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => {
152
136
IssueLabelToken . displayName = 'IssueLabelToken'
153
137
154
138
export default IssueLabelToken
139
+
140
+ // Helper functions
141
+
142
+ function useColorMode ( ) : 'light' | 'dark' {
143
+ const { theme} = useTheme ( )
144
+ // Determine color mode by luminance
145
+ const colorMode = getLuminance ( get ( 'colors.canvas.default' ) ( { theme} ) || '#fff' ) > 0.5 ? 'light' : 'dark'
146
+ return colorMode
147
+ }
148
+
149
+ function hexToHsluv ( hex : string ) {
150
+ const color = new Hsluv ( )
151
+ color . hex = toHex ( hex ) // Ensure hex is actually a hex color
152
+ color . hexToHsluv ( )
153
+ return { h : color . hsluv_h , s : color . hsluv_s , l : color . hsluv_l }
154
+ }
155
+
156
+ function hsluvToHex ( { h, s, l} : { h : number ; s : number ; l : number } ) {
157
+ const color = new Hsluv ( )
158
+ // eslint-disable-next-line camelcase
159
+ color . hsluv_h = h
160
+ // eslint-disable-next-line camelcase
161
+ color . hsluv_s = s
162
+ // eslint-disable-next-line camelcase
163
+ color . hsluv_l = l
164
+ color . hsluvToHex ( )
165
+ return color . hex
166
+ }
167
+
168
+ /** Returns a foreground color that has a given minimum contrast ratio against the given background color */
169
+ function minContrast ( fg : string , bg : string , minRatio : number ) {
170
+ // eslint-disable-next-line prefer-const
171
+ let { h, s, l} = hexToHsluv ( fg )
172
+
173
+ // While foreground color doesn't meet the contrast ratio,
174
+ // increase or decrease the lightness until it does
175
+ while ( getContrast ( hsluvToHex ( { h, s, l} ) , bg ) < minRatio && l <= 100 && l >= 0 ) {
176
+ if ( getLuminance ( bg ) > getLuminance ( fg ) ) {
177
+ l -= 1
178
+ } else {
179
+ l += 1
180
+ }
181
+ }
182
+
183
+ return hsluvToHex ( { h, s, l} )
184
+ }
0 commit comments