1- import React , { forwardRef , MouseEventHandler , useMemo } from 'react'
21import { 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'
38import TokenBase , { defaultTokenSize , isTokenInteractive , TokenBaseProps } from './TokenBase'
49import RemoveTokenButton from './_RemoveTokenButton'
5- import { parseToHsla , parseToRgba } from 'color2k'
6- import { useTheme } from '../ThemeProvider'
710import TokenTextContainer from './_TokenTextContainer'
8- import { ForwardRefComponent as PolymorphicForwardRefComponent } from '../utils/polymorphic'
911
1012export interface IssueLabelTokenProps extends TokenBaseProps {
1113 /**
@@ -16,31 +18,7 @@ export interface IssueLabelTokenProps extends TokenBaseProps {
1618
1719const tokenBorderWidthPx = 1
1820
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 ) => {
4422 const {
4523 as,
4624 fillColor = '#999' ,
@@ -54,42 +32,52 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => {
5432 onClick,
5533 ...rest
5634 } = 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+
6340 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 ?.( )
6745 }
68- const labelStyles : CSSObject = useMemo ( ( ) => {
69- const [ r , g , b ] = parseToRgba ( fillColor )
70- const [ h , s , l ] = parseToHsla ( fillColor )
7146
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+
7473 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 ,
8574 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 ,
8779 ...( isSelected
8880 ? {
89- background :
90- colorScheme === 'light'
91- ? 'hsl(var(--label-h), calc(var(--label-s) * 1%), calc((var(--label-l) - 5) * 1%))'
92- : darkModeStyles . background ,
9381 ':focus' : {
9482 outline : 'none' ,
9583 } ,
@@ -103,17 +91,13 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => {
10391 left : `-${ tokenBorderWidthPx * 2 } px` ,
10492 display : 'block' ,
10593 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 } ` ,
11195 borderRadius : '999px' ,
11296 } ,
11397 }
11498 : { } ) ,
11599 }
116- } , [ colorScheme , fillColor , isSelected , hideRemoveButton , onRemove ] )
100+ } , [ colorMode , fillColor , isSelected , hideRemoveButton , onRemove ] )
117101
118102 return (
119103 < TokenBase
@@ -152,3 +136,49 @@ const IssueLabelToken = forwardRef((props, forwardedRef) => {
152136IssueLabelToken . displayName = 'IssueLabelToken'
153137
154138export 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