diff --git a/packages/block-editor/src/hooks/contrast-checker.js b/packages/block-editor/src/hooks/contrast-checker.js index 368e2e75264858..7cb231110401d4 100644 --- a/packages/block-editor/src/hooks/contrast-checker.js +++ b/packages/block-editor/src/hooks/contrast-checker.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useState, useEffect } from '@wordpress/element'; +import { useLayoutEffect, useReducer } from '@wordpress/element'; /** * Internal dependencies @@ -9,52 +9,86 @@ import { useState, useEffect } from '@wordpress/element'; import ContrastChecker from '../components/contrast-checker'; import { useBlockElement } from '../components/block-list/use-block-props/use-block-refs'; -function getComputedStyle( node ) { - return node.ownerDocument.defaultView.getComputedStyle( node ); +function getComputedValue( node, property ) { + return node.ownerDocument.defaultView + .getComputedStyle( node ) + .getPropertyValue( property ); +} + +function getBlockElementColors( blockEl ) { + if ( ! blockEl ) { + return {}; + } + + const firstLinkElement = blockEl.querySelector( 'a' ); + const linkColor = !! firstLinkElement?.innerText + ? getComputedValue( firstLinkElement, 'color' ) + : undefined; + + const textColor = getComputedValue( blockEl, 'color' ); + + let backgroundColorNode = blockEl; + let backgroundColor = getComputedValue( + backgroundColorNode, + 'background-color' + ); + while ( + backgroundColor === 'rgba(0, 0, 0, 0)' && + backgroundColorNode.parentNode && + backgroundColorNode.parentNode.nodeType === + backgroundColorNode.parentNode.ELEMENT_NODE + ) { + backgroundColorNode = backgroundColorNode.parentNode; + backgroundColor = getComputedValue( + backgroundColorNode, + 'background-color' + ); + } + + return { + textColor, + backgroundColor, + linkColor, + }; +} + +function reducer( prevColors, newColors ) { + const hasChanged = Object.keys( newColors ).some( + ( key ) => prevColors[ key ] !== newColors[ key ] + ); + + // Do not re-render if the colors have not changed. + return hasChanged ? newColors : prevColors; } export default function BlockColorContrastChecker( { clientId } ) { - const [ detectedBackgroundColor, setDetectedBackgroundColor ] = useState(); - const [ detectedColor, setDetectedColor ] = useState(); - const [ detectedLinkColor, setDetectedLinkColor ] = useState(); const blockEl = useBlockElement( clientId ); + const [ colors, setColors ] = useReducer( reducer, {} ); // There are so many things that can change the color of a block // So we perform this check on every render. - useEffect( () => { + useLayoutEffect( () => { if ( ! blockEl ) { return; } - setDetectedColor( getComputedStyle( blockEl ).color ); - - const firstLinkElement = blockEl.querySelector( 'a' ); - if ( firstLinkElement && !! firstLinkElement.innerText ) { - setDetectedLinkColor( getComputedStyle( firstLinkElement ).color ); - } - let backgroundColorNode = blockEl; - let backgroundColor = - getComputedStyle( backgroundColorNode ).backgroundColor; - while ( - backgroundColor === 'rgba(0, 0, 0, 0)' && - backgroundColorNode.parentNode && - backgroundColorNode.parentNode.nodeType === - backgroundColorNode.parentNode.ELEMENT_NODE - ) { - backgroundColorNode = backgroundColorNode.parentNode; - backgroundColor = - getComputedStyle( backgroundColorNode ).backgroundColor; + function updateColors() { + setColors( getBlockElementColors( blockEl ) ); } - setDetectedBackgroundColor( backgroundColor ); - }, [ blockEl ] ); + // Combine `useLayoutEffect` and two rAF calls to ensure that values are read + // after the current paint but before the next paint. + window.requestAnimationFrame( () => + window.requestAnimationFrame( updateColors ) + ); + } ); return ( ); }