@scality/core-ui 0.198.0 → 0.200.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/buttonv2/Buttonv2.component.d.ts.map +1 -1
- package/dist/components/buttonv2/Buttonv2.component.js +9 -5
- package/dist/components/icon/iconTable.d.ts +1 -0
- package/dist/components/icon/iconTable.d.ts.map +1 -1
- package/dist/components/icon/iconTable.js +1 -0
- package/dist/components/navbar/Navbar.component.d.ts.map +1 -1
- package/dist/components/navbar/Navbar.component.js +15 -10
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +24 -0
- package/package.json +1 -1
- package/src/lib/components/buttonv2/Buttonv2.component.tsx +8 -5
- package/src/lib/components/icon/iconTable.ts +2 -0
- package/src/lib/components/navbar/Navbar.component.tsx +15 -10
- package/src/lib/utils.test.ts +48 -0
- package/src/lib/utils.ts +32 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Buttonv2.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/buttonv2/Buttonv2.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Buttonv2.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/buttonv2/Buttonv2.component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAKlD,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AACpD,OAAO,EAAW,KAAK,IAAI,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE9E,eAAO,MAAM,iBAAiB,oIAI7B,CAAC;AAEF,0DAA0D;AAC1D,KAAK,iBAAiB,GAAG,IAAI,CAC3B,oBAAoB,CAAC,iBAAiB,CAAC,EACvC,MAAM,GAAG,OAAO,CACjB,GAAG;IACF,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IACzD,IAAI,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACxB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,wDAAwD;AACxD,KAAK,eAAe,GAAG,iBAAiB,GAAG;IACzC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,OAAO,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;CAC1C,CAAC;AAEF,+EAA+E;AAC/E,KAAK,cAAc,GAAG,iBAAiB,GAAG;IACxC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,GAAG,CACA;IACE,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,GACD;IACE,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB,CACJ,CAAC;AAEF,MAAM,MAAM,KAAK,GAAG,eAAe,GAAG,cAAc,CAAC;AACrD,eAAO,MAAM,YAAY,2HA4KxB,CAAC;AACF,eAAO,MAAM,WAAW,0GAIvB,CAAC;AACF,eAAO,MAAM,UAAU;WAAwB,KAAK,CAAC,SAAS;SAS7D,CAAC;AAEF,eAAO,MAAM,YAAY;;;SAaxB,CAAC;AAEF,iBAAS,MAAM,CAAC,EACd,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,KAAK,EACL,IAAI,EACJ,OAAO,EACP,OAAO,EACP,SAAS,EACT,GAAG,IAAI,EACR,EAAE,KAAK,2CA2CP;AAED,OAAO,EAAE,MAAM,EAAE,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import styled, { css } from 'styled-components';
|
|
3
3
|
import { spacing } from '../../spacing';
|
|
4
4
|
import { fontSize, fontWeight } from '../../style/theme';
|
|
5
|
+
import { getContrastText } from '../../utils';
|
|
5
6
|
import { Loader } from '../loader/Loader.component';
|
|
6
7
|
import { Tooltip } from '../tooltip/Tooltip.component';
|
|
7
8
|
export const FocusVisibleStyle = css `
|
|
@@ -31,31 +32,34 @@ export const ButtonStyled = styled.button `
|
|
|
31
32
|
white-space: nowrap;
|
|
32
33
|
height: ${(props) => (props.size === 'inline' ? spacing.r24 : spacing.r32)};
|
|
33
34
|
${(props) => {
|
|
35
|
+
var _a;
|
|
34
36
|
const brand = props.theme;
|
|
35
37
|
switch (props.variant) {
|
|
36
|
-
case 'primary':
|
|
38
|
+
case 'primary': {
|
|
39
|
+
const primaryTextColor = (_a = getContrastText(brand.buttonPrimary, brand.textPrimary, brand.textReverse)) !== null && _a !== void 0 ? _a : brand.textPrimary;
|
|
37
40
|
return css `
|
|
38
41
|
background: ${brand.buttonPrimary};
|
|
39
42
|
background-clip: padding-box, border-box;
|
|
40
43
|
border: ${spacing.r1} solid transparent;
|
|
41
44
|
border-color: ${brand.buttonPrimary};
|
|
42
|
-
color: ${
|
|
45
|
+
color: ${primaryTextColor};
|
|
43
46
|
&:hover:enabled {
|
|
44
47
|
cursor: pointer;
|
|
45
48
|
border: ${spacing.r1} solid ${brand.infoPrimary};
|
|
46
|
-
color: ${
|
|
49
|
+
color: ${primaryTextColor};
|
|
47
50
|
}
|
|
48
51
|
// :focus-visible is the keyboard-only version of :focus
|
|
49
52
|
&:focus-visible:enabled {
|
|
50
53
|
${FocusVisibleStyle}
|
|
51
|
-
color: ${
|
|
54
|
+
color: ${primaryTextColor};
|
|
52
55
|
}
|
|
53
56
|
&:active:enabled {
|
|
54
57
|
cursor: pointer;
|
|
55
|
-
color: ${
|
|
58
|
+
color: ${primaryTextColor};
|
|
56
59
|
border: ${spacing.r1} solid ${brand.infoSecondary};
|
|
57
60
|
}
|
|
58
61
|
`;
|
|
62
|
+
}
|
|
59
63
|
case 'secondary':
|
|
60
64
|
return css `
|
|
61
65
|
background: ${brand.buttonSecondary};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iconTable.d.ts","sourceRoot":"","sources":["../../../src/lib/components/icon/iconTable.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"iconTable.d.ts","sourceRoot":"","sources":["../../../src/lib/components/icon/iconTable.ts"],"names":[],"mappings":"AACA;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiIrB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Navbar.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/navbar/Navbar.component.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAY,KAAK,IAAI,EAAE,MAAM,gCAAgC,CAAC;AAErE,OAAO,EAA6B,KAAK,KAAK,IAAI,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAEtG,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,QAAQ,CAAC;CAChB,GAAG,WAAW,CAAC;AAChB,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B,CAAC;AAEF,KAAK,MAAM,GAAG,cAAc,GAAG,YAAY,GAAG,YAAY,CAAC;AAC3D,KAAK,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;AAC7B,KAAK,GAAG,GAAG;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAC9B,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;CACtB,CAAC;AACF,MAAM,MAAM,KAAK,GAAG;IAClB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;CACnB,CAAC;
|
|
1
|
+
{"version":3,"file":"Navbar.component.d.ts","sourceRoot":"","sources":["../../../src/lib/components/navbar/Navbar.component.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAY,KAAK,IAAI,EAAE,MAAM,gCAAgC,CAAC;AAErE,OAAO,EAA6B,KAAK,KAAK,IAAI,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAEtG,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,QAAQ,CAAC;CAChB,GAAG,WAAW,CAAC;AAChB,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,KAAK,CAAC,aAAa,CAAC;CAC7B,CAAC;AAEF,KAAK,MAAM,GAAG,cAAc,GAAG,YAAY,GAAG,YAAY,CAAC;AAC3D,KAAK,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;AAC7B,KAAK,GAAG,GAAG;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,IAAI,CAAC;IAC9B,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;CACtB,CAAC;AACF,MAAM,MAAM,KAAK,GAAG;IAClB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;CACnB,CAAC;AAwKF,iBAAS,MAAM,CAAC,EACd,aAAa,EACb,IAAI,EACJ,IAAS,EACT,YAAiB,EACjB,GAAG,IAAI,EACR,EAAE,KAAK,2CAuDP;AAED,eAAO,MAAM,MAAM,eAAS,CAAC"}
|
|
@@ -4,20 +4,21 @@ import styled, { css } from 'styled-components';
|
|
|
4
4
|
import { Logo } from '../../icons/branding';
|
|
5
5
|
import { spacing } from '../../spacing';
|
|
6
6
|
import { fontSize, navbarHeight, navbarItemWidth } from '../../style/theme';
|
|
7
|
-
import { getThemePropSelector } from '../../utils';
|
|
7
|
+
import { getContrastText, getThemePropSelector } from '../../utils';
|
|
8
8
|
import { Dropdown } from '../dropdown/Dropdown.component';
|
|
9
9
|
import { Icon } from '../icon/Icon.component';
|
|
10
10
|
import { Button, FocusVisibleStyle } from '../buttonv2/Buttonv2.component';
|
|
11
|
+
const getNavbarTextColor = (props) => { var _a; return (_a = getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse)) !== null && _a !== void 0 ? _a : props.theme.textPrimary; };
|
|
11
12
|
const NavbarContainer = styled.div `
|
|
12
13
|
height: ${navbarHeight};
|
|
13
14
|
display: flex;
|
|
14
15
|
justify-content: space-between;
|
|
15
16
|
${css `
|
|
16
17
|
background-color: ${getThemePropSelector('navbarBackground')};
|
|
17
|
-
color: ${
|
|
18
|
+
color: ${getNavbarTextColor};
|
|
18
19
|
.fas,
|
|
19
20
|
.sc-trigger-text {
|
|
20
|
-
color: ${
|
|
21
|
+
color: ${getNavbarTextColor};
|
|
21
22
|
}
|
|
22
23
|
box-sizing: border-box;
|
|
23
24
|
border-bottom: 0.5px solid ${(props) => props.theme.backgroundLevel2};
|
|
@@ -45,21 +46,23 @@ const NavbarTabs = styled.div `
|
|
|
45
46
|
border-bottom: ${spacing.r2} solid transparent;
|
|
46
47
|
border-top: ${spacing.r2} solid transparent;
|
|
47
48
|
${(props) => {
|
|
49
|
+
var _a;
|
|
48
50
|
const { selectedActive } = props.theme;
|
|
51
|
+
const navTextColor = (_a = getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse)) !== null && _a !== void 0 ? _a : props.theme.textPrimary;
|
|
49
52
|
return css `
|
|
50
|
-
color: ${
|
|
53
|
+
color: ${navTextColor};
|
|
51
54
|
&:hover {
|
|
52
55
|
background-color: ${getThemePropSelector('highlight')};
|
|
53
56
|
}
|
|
54
57
|
&.selected {
|
|
55
|
-
color: ${
|
|
58
|
+
color: ${navTextColor};
|
|
56
59
|
font-weight: bold;
|
|
57
60
|
border-bottom-color: ${selectedActive};
|
|
58
61
|
}
|
|
59
62
|
// :focus-visible is the keyboard-only version of :focus
|
|
60
63
|
&:focus-visible {
|
|
61
64
|
${FocusVisibleStyle}
|
|
62
|
-
color: ${
|
|
65
|
+
color: ${navTextColor};
|
|
63
66
|
}
|
|
64
67
|
`;
|
|
65
68
|
}};
|
|
@@ -73,9 +76,10 @@ const TabItem = styled.div `
|
|
|
73
76
|
align-items: center;
|
|
74
77
|
padding: 0 ${spacing.r16};
|
|
75
78
|
${(props) => {
|
|
76
|
-
|
|
79
|
+
var _a;
|
|
80
|
+
const navTextColor = (_a = getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse)) !== null && _a !== void 0 ? _a : props.theme.textPrimary;
|
|
77
81
|
return css `
|
|
78
|
-
color: ${
|
|
82
|
+
color: ${navTextColor};
|
|
79
83
|
&:hover {
|
|
80
84
|
border-bottom: ${spacing.r2} solid;
|
|
81
85
|
border-top: ${spacing.r2} solid;
|
|
@@ -84,7 +88,7 @@ const TabItem = styled.div `
|
|
|
84
88
|
// :focus-visible is the keyboard-only version of :focus
|
|
85
89
|
&:focus-visible {
|
|
86
90
|
${FocusVisibleStyle}
|
|
87
|
-
color: ${
|
|
91
|
+
color: ${navTextColor};
|
|
88
92
|
}
|
|
89
93
|
`;
|
|
90
94
|
}};
|
|
@@ -118,13 +122,14 @@ const NavbarMenuItem = styled.div `
|
|
|
118
122
|
height: ${navbarHeight};
|
|
119
123
|
font-size: ${fontSize.base};
|
|
120
124
|
background-color: ${getThemePropSelector('navbarBackground')};
|
|
125
|
+
color: ${(props) => { var _a; return (_a = getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse)) !== null && _a !== void 0 ? _a : props.theme.textPrimary; }};
|
|
121
126
|
&:hover {
|
|
122
127
|
background-color: ${getThemePropSelector('highlight')};
|
|
123
128
|
}
|
|
124
129
|
// :focus-visible is the keyboard-only version of :focus
|
|
125
130
|
&:focus-visible {
|
|
126
131
|
${FocusVisibleStyle}
|
|
127
|
-
color: ${(props) => props.theme.textPrimary};
|
|
132
|
+
color: ${(props) => { var _a; return (_a = getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse)) !== null && _a !== void 0 ? _a : props.theme.textPrimary; }};
|
|
128
133
|
}
|
|
129
134
|
width: ${navbarItemWidth};
|
|
130
135
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const getThemeVariantSelector: () => (props: any) => any;
|
|
|
7
7
|
/** Returns the theme color key for a given variant (e.g. for use with Icon color prop). */
|
|
8
8
|
export declare const getVariantThemeKey: (variant: string) => string;
|
|
9
9
|
export declare const hex2RGB: (str: string) => [number, number, number];
|
|
10
|
+
export declare const getContrastText: (bgColor: string, textPrimary: string, textReverse: string) => string | null;
|
|
10
11
|
export declare const convertRemToPixels: (rem: number) => number;
|
|
11
12
|
type FormatISONumberOptions = {
|
|
12
13
|
decimals?: number;
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.ts"],"names":[],"mappings":"AAYA;mCACmC;AACnC,eAAO,MAAM,oBAAoB,GAAI,QAAG,MAAM,UAAK,QAGlD,CAAC;AAEF;mCACmC;AACnC,eAAO,MAAM,uBAAuB,SAAU,UAAK,QAIlD,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MACjB,CAAC;AAErC,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAc5D,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,KAAG,MAahD,CAAC;AAEF,KAAK,sBAAsB,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,MAAM,EACb,UAAS,sBAA2B,KACnC,MAoBF,CAAC"}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.ts"],"names":[],"mappings":"AAYA;mCACmC;AACnC,eAAO,MAAM,oBAAoB,GAAI,QAAG,MAAM,UAAK,QAGlD,CAAC;AAEF;mCACmC;AACnC,eAAO,MAAM,uBAAuB,SAAU,UAAK,QAIlD,CAAC;AAEF,2FAA2F;AAC3F,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG,MACjB,CAAC;AAErC,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAc5D,CAAC;AAmBF,eAAO,MAAM,eAAe,GAC1B,SAAS,MAAM,EACf,aAAa,MAAM,EACnB,aAAa,MAAM,KAClB,MAAM,GAAG,IASX,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,KAAG,MAahD,CAAC;AAEF,KAAK,sBAAsB,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO,MAAM,EACb,UAAS,sBAA2B,KACnC,MAoBF,CAAC"}
|
package/dist/utils.js
CHANGED
|
@@ -37,6 +37,30 @@ export const hex2RGB = (str) => {
|
|
|
37
37
|
}
|
|
38
38
|
throw new Error('Invalid hex string provided');
|
|
39
39
|
};
|
|
40
|
+
// WCAG 2.0 relative luminance
|
|
41
|
+
const relativeLuminance = (r, g, b) => {
|
|
42
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
43
|
+
const s = c / 255;
|
|
44
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
45
|
+
});
|
|
46
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
47
|
+
};
|
|
48
|
+
const wcagContrastRatio = (l1, l2) => (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
49
|
+
const luminanceOf = (hex) => {
|
|
50
|
+
const [r, g, b] = hex2RGB(hex);
|
|
51
|
+
return relativeLuminance(r, g, b);
|
|
52
|
+
};
|
|
53
|
+
export const getContrastText = (bgColor, textPrimary, textReverse) => {
|
|
54
|
+
try {
|
|
55
|
+
const bgLum = luminanceOf(bgColor);
|
|
56
|
+
const primaryContrast = wcagContrastRatio(luminanceOf(textPrimary), bgLum);
|
|
57
|
+
const reverseContrast = wcagContrastRatio(luminanceOf(textReverse), bgLum);
|
|
58
|
+
return reverseContrast > primaryContrast ? textReverse : textPrimary;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
40
64
|
export const convertRemToPixels = (rem) => {
|
|
41
65
|
if (document.documentElement &&
|
|
42
66
|
rem &&
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import type { ButtonHTMLAttributes } from 'react';
|
|
|
2
2
|
import styled, { css } from 'styled-components';
|
|
3
3
|
import { spacing } from '../../spacing';
|
|
4
4
|
import { fontSize, fontWeight } from '../../style/theme';
|
|
5
|
+
import { getContrastText } from '../../utils';
|
|
5
6
|
import { Loader } from '../loader/Loader.component';
|
|
6
7
|
import { Tooltip, Props as TooltipProps } from '../tooltip/Tooltip.component';
|
|
7
8
|
|
|
@@ -71,29 +72,31 @@ export const ButtonStyled = styled.button<ButtonStyledProps>`
|
|
|
71
72
|
const brand = props.theme;
|
|
72
73
|
|
|
73
74
|
switch (props.variant) {
|
|
74
|
-
case 'primary':
|
|
75
|
+
case 'primary': {
|
|
76
|
+
const primaryTextColor = getContrastText(brand.buttonPrimary, brand.textPrimary, brand.textReverse) ?? brand.textPrimary;
|
|
75
77
|
return css`
|
|
76
78
|
background: ${brand.buttonPrimary};
|
|
77
79
|
background-clip: padding-box, border-box;
|
|
78
80
|
border: ${spacing.r1} solid transparent;
|
|
79
81
|
border-color: ${brand.buttonPrimary};
|
|
80
|
-
color: ${
|
|
82
|
+
color: ${primaryTextColor};
|
|
81
83
|
&:hover:enabled {
|
|
82
84
|
cursor: pointer;
|
|
83
85
|
border: ${spacing.r1} solid ${brand.infoPrimary};
|
|
84
|
-
color: ${
|
|
86
|
+
color: ${primaryTextColor};
|
|
85
87
|
}
|
|
86
88
|
// :focus-visible is the keyboard-only version of :focus
|
|
87
89
|
&:focus-visible:enabled {
|
|
88
90
|
${FocusVisibleStyle}
|
|
89
|
-
color: ${
|
|
91
|
+
color: ${primaryTextColor};
|
|
90
92
|
}
|
|
91
93
|
&:active:enabled {
|
|
92
94
|
cursor: pointer;
|
|
93
|
-
color: ${
|
|
95
|
+
color: ${primaryTextColor};
|
|
94
96
|
border: ${spacing.r1} solid ${brand.infoSecondary};
|
|
95
97
|
}
|
|
96
98
|
`;
|
|
99
|
+
}
|
|
97
100
|
|
|
98
101
|
case 'secondary':
|
|
99
102
|
return css`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
/**
|
|
2
3
|
* Maps human-readable icon names to FontAwesome icon info.
|
|
3
4
|
* Format: 'iconType iconClass' where:
|
|
@@ -134,4 +135,5 @@ export const iconTable = {
|
|
|
134
135
|
ThumbsDown: 'far faThumbsDown',
|
|
135
136
|
Sidebar: 'fas faTableColumns',
|
|
136
137
|
Bookopen: 'fas faBookOpen',
|
|
138
|
+
Book: 'fas faBook',
|
|
137
139
|
};
|
|
@@ -3,7 +3,7 @@ import styled, { css } from 'styled-components';
|
|
|
3
3
|
import { Logo } from '../../icons/branding';
|
|
4
4
|
import { spacing } from '../../spacing';
|
|
5
5
|
import { fontSize, navbarHeight, navbarItemWidth } from '../../style/theme';
|
|
6
|
-
import { getThemePropSelector } from '../../utils';
|
|
6
|
+
import { getContrastText, getThemePropSelector } from '../../utils';
|
|
7
7
|
import { Dropdown, type Item } from '../dropdown/Dropdown.component';
|
|
8
8
|
import { Icon } from '../icon/Icon.component';
|
|
9
9
|
import { Button, FocusVisibleStyle, type Props as ButtonProps } from '../buttonv2/Buttonv2.component';
|
|
@@ -38,16 +38,19 @@ export type Props = {
|
|
|
38
38
|
logo?: JSX.Element;
|
|
39
39
|
tabs?: Array<Tab>;
|
|
40
40
|
};
|
|
41
|
+
const getNavbarTextColor = (props) =>
|
|
42
|
+
getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse) ?? props.theme.textPrimary;
|
|
43
|
+
|
|
41
44
|
const NavbarContainer = styled.div`
|
|
42
45
|
height: ${navbarHeight};
|
|
43
46
|
display: flex;
|
|
44
47
|
justify-content: space-between;
|
|
45
48
|
${css`
|
|
46
49
|
background-color: ${getThemePropSelector('navbarBackground')};
|
|
47
|
-
color: ${
|
|
50
|
+
color: ${getNavbarTextColor};
|
|
48
51
|
.fas,
|
|
49
52
|
.sc-trigger-text {
|
|
50
|
-
color: ${
|
|
53
|
+
color: ${getNavbarTextColor};
|
|
51
54
|
}
|
|
52
55
|
box-sizing: border-box;
|
|
53
56
|
border-bottom: 0.5px solid ${(props) => props.theme.backgroundLevel2};
|
|
@@ -76,20 +79,21 @@ const NavbarTabs = styled.div`
|
|
|
76
79
|
border-top: ${spacing.r2} solid transparent;
|
|
77
80
|
${(props) => {
|
|
78
81
|
const { selectedActive } = props.theme;
|
|
82
|
+
const navTextColor = getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse) ?? props.theme.textPrimary;
|
|
79
83
|
return css`
|
|
80
|
-
color: ${
|
|
84
|
+
color: ${navTextColor};
|
|
81
85
|
&:hover {
|
|
82
86
|
background-color: ${getThemePropSelector('highlight')};
|
|
83
87
|
}
|
|
84
88
|
&.selected {
|
|
85
|
-
color: ${
|
|
89
|
+
color: ${navTextColor};
|
|
86
90
|
font-weight: bold;
|
|
87
91
|
border-bottom-color: ${selectedActive};
|
|
88
92
|
}
|
|
89
93
|
// :focus-visible is the keyboard-only version of :focus
|
|
90
94
|
&:focus-visible {
|
|
91
95
|
${FocusVisibleStyle}
|
|
92
|
-
color: ${
|
|
96
|
+
color: ${navTextColor};
|
|
93
97
|
}
|
|
94
98
|
`;
|
|
95
99
|
}};
|
|
@@ -103,9 +107,9 @@ const TabItem = styled.div<{ selected: boolean }>`
|
|
|
103
107
|
align-items: center;
|
|
104
108
|
padding: 0 ${spacing.r16};
|
|
105
109
|
${(props) => {
|
|
106
|
-
const
|
|
110
|
+
const navTextColor = getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse) ?? props.theme.textPrimary;
|
|
107
111
|
return css`
|
|
108
|
-
color: ${
|
|
112
|
+
color: ${navTextColor};
|
|
109
113
|
&:hover {
|
|
110
114
|
border-bottom: ${spacing.r2} solid;
|
|
111
115
|
border-top: ${spacing.r2} solid;
|
|
@@ -114,7 +118,7 @@ const TabItem = styled.div<{ selected: boolean }>`
|
|
|
114
118
|
// :focus-visible is the keyboard-only version of :focus
|
|
115
119
|
&:focus-visible {
|
|
116
120
|
${FocusVisibleStyle}
|
|
117
|
-
color: ${
|
|
121
|
+
color: ${navTextColor};
|
|
118
122
|
}
|
|
119
123
|
`;
|
|
120
124
|
}};
|
|
@@ -149,13 +153,14 @@ const NavbarMenuItem = styled.div`
|
|
|
149
153
|
height: ${navbarHeight};
|
|
150
154
|
font-size: ${fontSize.base};
|
|
151
155
|
background-color: ${getThemePropSelector('navbarBackground')};
|
|
156
|
+
color: ${(props) => getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse) ?? props.theme.textPrimary};
|
|
152
157
|
&:hover {
|
|
153
158
|
background-color: ${getThemePropSelector('highlight')};
|
|
154
159
|
}
|
|
155
160
|
// :focus-visible is the keyboard-only version of :focus
|
|
156
161
|
&:focus-visible {
|
|
157
162
|
${FocusVisibleStyle}
|
|
158
|
-
color: ${(props) => props.theme.textPrimary};
|
|
163
|
+
color: ${(props) => getContrastText(props.theme.navbarBackground, props.theme.textPrimary, props.theme.textReverse) ?? props.theme.textPrimary};
|
|
159
164
|
}
|
|
160
165
|
width: ${navbarItemWidth};
|
|
161
166
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getContrastText } from './utils';
|
|
2
|
+
|
|
3
|
+
const LIGHT_TEXT = '#EAEAEA';
|
|
4
|
+
const DARK_TEXT = '#000000';
|
|
5
|
+
|
|
6
|
+
describe('getContrastText', () => {
|
|
7
|
+
it('returns textPrimary on dark backgrounds when textPrimary is light', () => {
|
|
8
|
+
expect(getContrastText('#000000', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
|
|
9
|
+
expect(getContrastText('#1A1A1A', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
|
|
10
|
+
expect(getContrastText('#121219', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
|
|
11
|
+
expect(getContrastText('#2F4185', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns textReverse on light backgrounds when textPrimary is light', () => {
|
|
15
|
+
expect(getContrastText('#FFFFFF', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
|
|
16
|
+
expect(getContrastText('#F5F5F5', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
|
|
17
|
+
expect(getContrastText('#FCFCFC', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('picks the text color with better contrast against a vivid background', () => {
|
|
21
|
+
expect(getContrastText('#E9041E', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
|
|
22
|
+
expect(getContrastText('#E9041E', '#FFFFFF', '#000000')).toBe('#FFFFFF');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('handles 3-character hex shorthand', () => {
|
|
26
|
+
expect(getContrastText('#FFF', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
|
|
27
|
+
expect(getContrastText('#000', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('handles hex without # prefix', () => {
|
|
31
|
+
expect(getContrastText('000000', LIGHT_TEXT, DARK_TEXT)).toBe(LIGHT_TEXT);
|
|
32
|
+
expect(getContrastText('FFFFFF', LIGHT_TEXT, DARK_TEXT)).toBe(DARK_TEXT);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns null for non-hex values', () => {
|
|
36
|
+
expect(
|
|
37
|
+
getContrastText(
|
|
38
|
+
'linear-gradient(130deg, #9355E7 0%, #2E4AA3 60%)',
|
|
39
|
+
LIGHT_TEXT,
|
|
40
|
+
DARK_TEXT,
|
|
41
|
+
),
|
|
42
|
+
).toBeNull();
|
|
43
|
+
expect(getContrastText('not-a-color', LIGHT_TEXT, DARK_TEXT)).toBeNull();
|
|
44
|
+
expect(
|
|
45
|
+
getContrastText('rgb(255, 0, 0)', LIGHT_TEXT, DARK_TEXT),
|
|
46
|
+
).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
package/src/lib/utils.ts
CHANGED
|
@@ -45,6 +45,38 @@ export const hex2RGB = (str: string): [number, number, number] => {
|
|
|
45
45
|
throw new Error('Invalid hex string provided');
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
// WCAG 2.0 relative luminance
|
|
49
|
+
const relativeLuminance = (r: number, g: number, b: number): number => {
|
|
50
|
+
const [rs, gs, bs] = [r, g, b].map((c) => {
|
|
51
|
+
const s = c / 255;
|
|
52
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
53
|
+
});
|
|
54
|
+
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const wcagContrastRatio = (l1: number, l2: number): number =>
|
|
58
|
+
(Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
59
|
+
|
|
60
|
+
const luminanceOf = (hex: string): number => {
|
|
61
|
+
const [r, g, b] = hex2RGB(hex);
|
|
62
|
+
return relativeLuminance(r, g, b);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const getContrastText = (
|
|
66
|
+
bgColor: string,
|
|
67
|
+
textPrimary: string,
|
|
68
|
+
textReverse: string,
|
|
69
|
+
): string | null => {
|
|
70
|
+
try {
|
|
71
|
+
const bgLum = luminanceOf(bgColor);
|
|
72
|
+
const primaryContrast = wcagContrastRatio(luminanceOf(textPrimary), bgLum);
|
|
73
|
+
const reverseContrast = wcagContrastRatio(luminanceOf(textReverse), bgLum);
|
|
74
|
+
return reverseContrast > primaryContrast ? textReverse : textPrimary;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
48
80
|
export const convertRemToPixels = (rem: number): number => {
|
|
49
81
|
if (
|
|
50
82
|
document.documentElement &&
|