Skip to content

Commit

Permalink
Contrast Checker: check link color (#38100)
Browse files Browse the repository at this point in the history
* Initial commit. Detecting the color of an A tag, if it exists and passing it to the ConstrastChecker

Added base logic and tests.
Very messy. To be optimzized. Don't hate me.

* A WIP commit. Reducing logic complexity into a hook. Will be squished.

* WIP to be squashed. Using hook

* Reducing logic with passing tests.

* Extracting ContrastCheckerMessage component

* Extracting readable options

* Update DOM query to find an A tag even if its nested in, say, a strong or em tag.

* This commit refactors the contrast checker to accept an array of text colors.
This is so it can handle checking multiple text/foreground colors on a single background, e.g., text, link and so on.
Updating arguments and tests.

* Reverting change in ContrastChecker props model. These things should be done iteratively as it touches too many parts, e.g. mobile.

* Update packages/block-editor/src/components/contrast-checker/index.js

Co-authored-by: Aaron Robertshaw <[email protected]>

* Reorder dependencies
Update comment so that it reflects what's going on in the code.

Co-authored-by: Aaron Robertshaw <[email protected]>
  • Loading branch information
ramonjd and aaronrobertshaw authored Feb 7, 2022
1 parent 48da1d9 commit ca3c6da
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 94 deletions.
189 changes: 102 additions & 87 deletions packages/block-editor/src/components/contrast-checker/index.js
Original file line number Diff line number Diff line change
@@ -1,125 +1,140 @@
/**
* External dependencies
*/
import { colord, extend } from 'colord';
import namesPlugin from 'colord/plugins/names';
import a11yPlugin from 'colord/plugins/a11y';
import namesPlugin from 'colord/plugins/names';
import { colord, extend } from 'colord';

/**
* WordPress dependencies
*/
import { speak } from '@wordpress/a11y';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { Notice } from '@wordpress/components';
import { useEffect } from '@wordpress/element';
import { speak } from '@wordpress/a11y';

extend( [ namesPlugin, a11yPlugin ] );

function ContrastCheckerMessage( {
colordBackgroundColor,
colordTextColor,
backgroundColor,
textColor,
shouldShowTransparencyWarning,
} ) {
let msg = '';
if ( shouldShowTransparencyWarning ) {
msg = __( 'Transparent text may be hard for people to read.' );
} else {
msg =
colordBackgroundColor.brightness() < colordTextColor.brightness()
? __(
'This color combination may be hard for people to read. Try using a darker background color and/or a brighter text color.'
)
: __(
'This color combination may be hard for people to read. Try using a brighter background color and/or a darker text color.'
);
}

// Note: The `Notice` component can speak messages via its `spokenMessage`
// prop, but the contrast checker requires granular control over when the
// announcements are made. Notably, the message will be re-announced if a
// new color combination is selected and the contrast is still insufficient.
useEffect( () => {
const speakMsg = shouldShowTransparencyWarning
? __( 'Transparent text may be hard for people to read.' )
: __( 'This color combination may be hard for people to read.' );
speak( speakMsg );
}, [ backgroundColor, textColor ] );

return (
<div className="block-editor-contrast-checker">
<Notice
spokenMessage={ null }
status="warning"
isDismissible={ false }
>
{ msg }
</Notice>
</div>
);
}

function ContrastChecker( {
backgroundColor,
fallbackBackgroundColor,
fallbackTextColor,
fallbackLinkColor,
fontSize, // font size value in pixels
isLargeText,
textColor,
linkColor,
enableAlphaChecker = false,
} ) {
if (
! ( backgroundColor || fallbackBackgroundColor ) ||
! ( textColor || fallbackTextColor )
) {
const currentBackgroundColor = backgroundColor || fallbackBackgroundColor;

// Must have a background color.
if ( ! currentBackgroundColor ) {
return null;
}
const colordBackgroundColor = colord(
backgroundColor || fallbackBackgroundColor
);
const colordTextColor = colord( textColor || fallbackTextColor );
const textColorHasTransparency = colordTextColor.alpha() < 1;

const currentTextColor = textColor || fallbackTextColor;
const currentLinkColor = linkColor || fallbackLinkColor;

// Must have at least one text color.
if ( ! currentTextColor && ! currentLinkColor ) {
return null;
}

const textColors = [
{
color: currentTextColor,
description: __( 'text color' ),
},
{
color: currentLinkColor,
description: __( 'link color' ),
},
];
const colordBackgroundColor = colord( currentBackgroundColor );
const backgroundColorHasTransparency = colordBackgroundColor.alpha() < 1;
const hasTransparency =
textColorHasTransparency || backgroundColorHasTransparency;
const isReadable = colordTextColor.isReadable( colordBackgroundColor, {
const backgroundColorBrightness = colordBackgroundColor.brightness();
const isReadableOptions = {
level: 'AA',
size:
isLargeText || ( isLargeText !== false && fontSize >= 24 )
? 'large'
: 'small',
} );
};

// Don't show the message if the text is readable AND there's no transparency.
// This is the default.
if ( isReadable && ! hasTransparency ) {
return null;
}
let message = '';
let speakMessage = '';
for ( const item of textColors ) {
// If there is no color, go no further.
if ( ! item.color ) {
continue;
}
const colordTextColor = colord( item.color );
const isColordTextReadable = colordTextColor.isReadable(
colordBackgroundColor,
isReadableOptions
);
const textHasTransparency = colordTextColor.alpha() < 1;

// If the contrast is not readable.
if ( ! isColordTextReadable ) {
// Don't show the message if the background or text is transparent.
if ( backgroundColorHasTransparency || textHasTransparency ) {
continue;
}
message =
backgroundColorBrightness < colordTextColor.brightness()
? sprintf(
// translators: %s is a type of text color, e.g., "text color" or "link color"
__(
'This color combination may be hard for people to read. Try using a darker background color and/or a brighter %s.'
),
item.description
)
: sprintf(
// translators: %s is a type of text color, e.g., "text color" or "link color"
__(
'This color combination may be hard for people to read. Try using a brighter background color and/or a darker %s.'
),
item.description
);
speakMessage = __(
'This color combination may be hard for people to read.'
);
// Break from the loop when we have a contrast warning.
// These messages take priority over the transparency warning.
break;
}

if ( hasTransparency ) {
if (
// If there's transparency, don't show the message if the alpha checker is disabled.
! enableAlphaChecker ||
// If the alpha checker is enabled, we only show the warning if the text has transparency.
( isReadable && ! textColorHasTransparency )
) {
return null;
// If there is no contrast warning and the text is transparent,
// show the transparent warning if alpha check is enabled.
if ( textHasTransparency && enableAlphaChecker ) {
message = __( 'Transparent text may be hard for people to read.' );
speakMessage = __(
'Transparent text may be hard for people to read.'
);
}
}

if ( ! message ) {
return null;
}

// Note: The `Notice` component can speak messages via its `spokenMessage`
// prop, but the contrast checker requires granular control over when the
// announcements are made. Notably, the message will be re-announced if a
// new color combination is selected and the contrast is still insufficient.
speak( speakMessage );

return (
<ContrastCheckerMessage
backgroundColor={ backgroundColor }
textColor={ textColor }
colordBackgroundColor={ colordBackgroundColor }
colordTextColor={ colordTextColor }
// Flag to warn about transparency only if the text is otherwise readable according to colord
// to ensure the readability warnings take precedence.
shouldShowTransparencyWarning={
isReadable && textColorHasTransparency
}
/>
<div className="block-editor-contrast-checker">
<Notice
spokenMessage={ null }
status="warning"
isDismissible={ false }
>
{ message }
</Notice>
</div>
);
}

Expand Down
Loading

0 comments on commit ca3c6da

Please sign in to comment.