Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- `ResizableBox`: Make the `children` prop optional ([#79370](https://github.com/WordPress/gutenberg/pull/79370)).
- Adopt `--wpds-dimension-size-*` design tokens [#79093](https://github.com/WordPress/gutenberg/pull/79093).
- Point the legacy `--wp-components-*` color fallbacks at the design system tokens (`--wpds-*` / `--wp-admin-theme-color*`), so component styles get sensible defaults from the prebuilt token stylesheet without a runtime `<ThemeProvider>` ([#78664](https://github.com/WordPress/gutenberg/pull/78664)).
- `withFallbackStyles`: Refactor from a class component to a function component with hooks ([#78837](https://github.com/WordPress/gutenberg/pull/78837)).

### Documentation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,89 +6,59 @@ import fastDeepEqual from 'fast-deep-equal/es6/index.js';
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useState, useRef } from '@wordpress/element';
import {
createHigherOrderComponent,
useIsomorphicLayoutEffect,
} from '@wordpress/compose';

type Props = {
node?: HTMLElement;
[ key: string ]: any;
};

type State = {
fallbackStyles?: { [ key: string ]: any };
grabStylesCompleted: boolean;
};

export default (
mapNodeToProps: (
node: HTMLElement,
props: Props
) => { [ key: string ]: any }
) =>
createHigherOrderComponent( ( WrappedComponent ) => {
return class WithFallbackStyles extends Component< Props, State > {
nodeRef?: HTMLElement;
return function WithFallbackStyles( props: Props ) {
const [ fallbackStyles, setFallbackStyles ] = useState<
{ [ key: string ]: any } | undefined
>( undefined );

constructor( props: Props ) {
super( props );
this.nodeRef = this.props.node;
this.state = {
fallbackStyles: undefined,
grabStylesCompleted: false,
};
const nodeRef = useRef< HTMLDivElement >( null );

this.bindRef = this.bindRef.bind( this );
}

bindRef( node: HTMLDivElement ) {
if ( ! node ) {
return;
}
this.nodeRef = node;
}
// Runs before paint, so the fallback styles apply without a flash.
useIsomorphicLayoutEffect( () => {
const node = props.node ?? nodeRef.current;
// Derived from fallbackStyles: the grab is complete once every
// mapped value has resolved to a truthy style.
const grabStylesCompleted =
!! fallbackStyles &&
Object.values( fallbackStyles ).every( Boolean );

componentDidMount() {
this.grabFallbackStyles();
}

componentDidUpdate() {
this.grabFallbackStyles();
}

grabFallbackStyles() {
const { grabStylesCompleted, fallbackStyles } = this.state;
if ( this.nodeRef && ! grabStylesCompleted ) {
const newFallbackStyles = mapNodeToProps(
this.nodeRef,
this.props
);
if ( node && ! grabStylesCompleted ) {
const newFallbackStyles = mapNodeToProps( node, props );

if (
! fastDeepEqual( newFallbackStyles, fallbackStyles )
) {
this.setState( {
fallbackStyles: newFallbackStyles,
grabStylesCompleted:
Object.values( newFallbackStyles ).every(
Boolean
),
} );
setFallbackStyles( newFallbackStyles );
}
}
}
} );

const wrappedComponent = (
<WrappedComponent { ...props } { ...fallbackStyles } />
);

render() {
const wrappedComponent = (
<WrappedComponent
{ ...this.props }
{ ...this.state.fallbackStyles }
/>
);
return this.props.node ? (
wrappedComponent
) : (
<div ref={ this.bindRef }> { wrappedComponent } </div>
);
}
return props.node ? (
wrappedComponent
) : (
<div ref={ nodeRef }> { wrappedComponent } </div>
);
};
}, 'withFallbackStyles' );
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';

/**
* Internal dependencies
*/
import withFallbackStyles from '..';

describe( 'withFallbackStyles', () => {
const Wrapped = ( props: { fallbackColor?: string } ) => (
<div>{ props.fallbackColor ?? 'no-fallback' }</div>
);

it( 'derives the fallback styles from the provided node prop', () => {
const node = document.createElement( 'span' );
node.dataset.color = 'rgb(1, 2, 3)';
const mapNodeToProps = jest.fn( ( el: HTMLElement ) => ( {
fallbackColor: el.dataset.color,
} ) );
const Component = withFallbackStyles( mapNodeToProps )( Wrapped );

render( <Component node={ node } /> );

// The rendered value proves the actual node was read by mapNodeToProps.
expect( mapNodeToProps ).toHaveBeenCalledWith(
node,
expect.objectContaining( { node } )
);
expect( screen.getByText( 'rgb(1, 2, 3)' ) ).toBeInTheDocument();
} );

it( 'derives the fallback styles from the internal wrapper node when no node prop is given', () => {
const mapNodeToProps = jest.fn( ( el: HTMLElement ) => ( {
fallbackColor: el.tagName.toLowerCase(),
} ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to test that the node, either supplied as a prop or created in the wrapper, can be actually used to derive the styles. At this moment neither of the two tests makes any use of the DOM node.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 9a14a5a. Both tests now read from the real node (dataset on the provided node, tagName on the internal wrapper) so they actually exercise the node wiring.

const Component = withFallbackStyles( mapNodeToProps )( Wrapped );

render( <Component /> );

// The HOC wraps in a <div>, so reading the node's tagName yields 'div'.
expect( mapNodeToProps ).toHaveBeenCalled();
expect( mapNodeToProps.mock.calls[ 0 ][ 0 ].tagName ).toBe( 'DIV' );
expect( screen.getByText( 'div' ) ).toBeInTheDocument();
} );
} );
Loading