@pandacss/studio
Version:
The automated token documentation for Panda CSS
227 lines (179 loc) • 5.59 kB
text/typescript
// Credits: https://github.com/bbc/color-contrast-checker
interface Pair {
colorA: string
colorB: string
fontSize: number
}
interface Rgb {
r: number
g: number
b: number
}
class RgbClass {
r = 0
g = 0
b = 0
toString = () => {
return '<r: ' + this.r + ' g: ' + this.g + ' b: ' + this.b + ' >'
}
}
class Result {
WCAG_AA = false
WCAG_AAA = false
customRatio: number | undefined = undefined
toString = () => {
return '< WCAG-AA: ' + (this.WCAG_AA ? 'pass' : 'fail') + ' WCAG-AAA: ' + (this.WCAG_AAA ? 'pass' : 'fail') + ' >'
}
}
export class ColorContrastChecker {
fontSize = 14
rgbClass = new RgbClass()
isValidSixDigitColorCode = (hex: string) => {
const regSixDigitColorcode = /^(#)?([0-9a-fA-F]{6})?$/
return regSixDigitColorcode.test(hex)
}
isValidThreeDigitColorCode = (hex: string) => {
const regThreeDigitColorcode = /^(#)?([0-9a-fA-F]{3})?$/
return regThreeDigitColorcode.test(hex)
}
isValidColorCode = (hex: string) => {
return this.isValidSixDigitColorCode(hex) || this.isValidThreeDigitColorCode(hex)
}
isValidRatio = (ratio: unknown) => {
return typeof ratio === 'number'
}
convertColorToSixDigit = (hex: string) => {
return '#' + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]
}
hexToLuminance = (color: string) => {
if (!this.isValidColorCode(color)) {
throw new Error('Invalid Color :' + color)
}
if (this.isValidThreeDigitColorCode(color)) {
color = this.convertColorToSixDigit(color)
}
const rgb = this.getRGBFromHex(color)
const LRGB = this.calculateLRGB(rgb)
return this.calculateLuminance(LRGB)
}
check = (colorA: string, colorB: string, fontSize?: number, customRatio?: number) => {
if (typeof fontSize !== 'undefined') {
this.fontSize = fontSize
}
if (!colorA || !colorB) {
return false
}
const l1 = this.hexToLuminance(colorA) /* higher value */
const l2 = this.hexToLuminance(colorB) /* lower value */
const contrastRatio = this.getContrastRatio(l1, l2)
if (typeof customRatio !== 'undefined') {
if (!this.isValidRatio(customRatio)) {
return false
}
return this.verifyCustomContrastRatio(contrastRatio, customRatio)
} else {
return this.verifyContrastRatio(contrastRatio)
}
}
checkPairs = (pairs: Pair[], customRatio?: number) => {
const results = []
for (const i in pairs) {
const pair = pairs[i]
if (typeof pair.fontSize !== 'undefined') {
results.push(this.check(pair.colorA, pair.colorB, pair.fontSize, customRatio))
} else {
results.push(this.check(pair.colorA, pair.colorB, void 0, customRatio))
}
}
return results
}
calculateLuminance = (lRGB: Rgb) => {
return 0.2126 * lRGB.r + 0.7152 * lRGB.g + 0.0722 * lRGB.b
}
isLevelAA = (colorA: string, colorB: string, fontSize: number) => {
const result = this.check(colorA, colorB, fontSize)
return result.WCAG_AA
}
isLevelAAA = (colorA: string, colorB: string, fontSize: number) => {
const result = this.check(colorA, colorB, fontSize)
return result.WCAG_AAA
}
isLevelCustom = (colorA: string, colorB: string, ratio: number) => {
const result = this.check(colorA, colorB, undefined, ratio)
return result.customRatio
}
getRGBFromHex = (color: string) => {
const rgb = new RgbClass()
if (typeof color !== 'string') {
throw new Error('must use string')
}
const rVal = parseInt(color.slice(1, 3), 16)
const gVal = parseInt(color.slice(3, 5), 16)
const bVal = parseInt(color.slice(5, 7), 16)
rgb.r = rVal
rgb.g = gVal
rgb.b = bVal
return rgb
}
calculateSRGB = (rgb: Rgb) => {
const sRGB = new RgbClass()
for (const key in rgb) {
sRGB[key as keyof Rgb] = parseFloat((rgb[key as keyof Rgb] / 255).toString())
}
return sRGB
}
calculateLRGB = (rgb: Rgb) => {
const sRGB = this.calculateSRGB(rgb)
const lRGB = Object.create(this.rgbClass)
let val = 0
for (const key in sRGB) {
//@ts-ignore
val = parseFloat(sRGB[key])
if (val <= 0.03928) {
lRGB[key] = val / 12.92
} else {
lRGB[key] = Math.pow((val + 0.055) / 1.055, 2.4)
}
}
return lRGB
}
getContrastRatio = (lumA: number, lumB: number) => {
let lighter: number
let darker: number
if (lumA >= lumB) {
lighter = lumA
darker = lumB
} else {
lighter = lumB
darker = lumA
}
const ratio = (lighter + 0.05) / (darker + 0.05)
return ratio
}
verifyContrastRatio = (ratio: number) => {
const WCAG_REQ_RATIO_AA_LG = 3.0,
WCAG_REQ_RATIO_AA_SM = 4.5,
WCAG_REQ_RATIO_AAA_LG = 4.5,
WCAG_REQ_RATIO_AAA_SM = 7.0,
WCAG_FONT_CUTOFF = 18
const results = new Result()
const fontSize = this.fontSize || 14
if (fontSize >= WCAG_FONT_CUTOFF) {
results.WCAG_AA = ratio >= WCAG_REQ_RATIO_AA_LG
results.WCAG_AAA = ratio >= WCAG_REQ_RATIO_AAA_LG
} else {
results.WCAG_AA = ratio >= WCAG_REQ_RATIO_AA_SM
results.WCAG_AAA = ratio >= WCAG_REQ_RATIO_AAA_SM
}
return results
}
verifyCustomContrastRatio = (inputRatio: number, checkRatio: number) => {
const resultsClass = new Result()
resultsClass.toString = function () {
return '< Custom Ratio: ' + (this.customRatio ? 'pass' : 'fail') + ' >'
}
const results = Object.create(resultsClass)
results.customRatio = inputRatio >= checkRatio
return results
}
}