rebase from live enviornment

This commit is contained in:
Rachit Bhargava
2024-01-09 22:14:20 -05:00
parent ff0b49a046
commit 3a22fcaa4a
15968 changed files with 2344674 additions and 45234 deletions

View File

@@ -0,0 +1,10 @@
export * from './use-container-queries';
export * from './use-local-storage-state';
export * from './use-position-relative-to-viewport';
export * from './use-previous';
export * from './use-shallow-equal';
export * from './use-throw-error';
export * from './use-typography-props';
export * from './use-is-mounted';
export * from './use-spoken-message';
export * from './use-style-props';

View File

@@ -0,0 +1,86 @@
/**
* External dependencies
*/
import { render, screen, act } from '@testing-library/react';
/**
* Internal dependencies
*/
import { usePositionRelativeToViewport } from '../use-position-relative-to-viewport';
describe( 'usePositionRelativeToViewport', () => {
function setup() {
const TestComponent = () => {
const [ referenceElement, positionRelativeToViewport ] =
usePositionRelativeToViewport();
return (
<>
{ referenceElement }
{ positionRelativeToViewport === 'below' && (
<p data-testid="below"></p>
) }
{ positionRelativeToViewport === 'visible' && (
<p data-testid="visible"></p>
) }
{ positionRelativeToViewport === 'above' && (
<p data-testid="above"></p>
) }
</>
);
};
return render( <TestComponent /> );
}
it( "calls IntersectionObserver's `observe` and `unobserve` events", async () => {
const observe = jest.fn();
const unobserve = jest.fn();
// @ts-ignore
IntersectionObserver = jest.fn( () => ( {
observe,
unobserve,
} ) );
const { unmount } = setup();
expect( observe ).toHaveBeenCalled();
unmount();
expect( unobserve ).toHaveBeenCalled();
} );
it.each`
position | isIntersecting | top
${ 'visible' } | ${ true } | ${ 0 }
${ 'below' } | ${ false } | ${ 10 }
${ 'above' } | ${ false } | ${ 0 }
${ 'above' } | ${ false } | ${ -10 }
`(
"position relative to viewport is '$position' with isIntersecting=$isIntersecting and top=$top",
( { position, isIntersecting, top } ) => {
let intersectionObserverCallback = ( entries ) => entries;
// @ts-ignore
IntersectionObserver = jest.fn( ( callback ) => {
// @ts-ignore
intersectionObserverCallback = callback;
return {
observe: () => void null,
unobserve: () => void null,
};
} );
setup();
act( () => {
intersectionObserverCallback( [
{ isIntersecting, boundingClientRect: { top } },
] );
} );
expect( screen.getAllByTestId( position ) ).toHaveLength( 1 );
}
);
} );

View File

@@ -0,0 +1,98 @@
// We need to disable the following eslint check as it's only applicable
// to testing-library/react not `react-test-renderer` used here
/* eslint-disable testing-library/await-async-query */
/**
* External dependencies
*/
import TestRenderer, { act } from 'react-test-renderer';
/**
* Internal dependencies
*/
import { usePrevious } from '../use-previous';
describe( 'usePrevious', () => {
const TestComponent = ( { testValue, validation } ) => {
const previousValue = usePrevious( testValue, validation );
return (
<div
data-testValue={ testValue }
data-previousValue={ previousValue }
/>
);
};
let renderer;
beforeEach( () => ( renderer = null ) );
it( 'should be undefined at first pass', () => {
act( () => {
renderer = TestRenderer.create( <TestComponent testValue={ 1 } /> );
} );
const testValue =
renderer.root.findByType( 'div' ).props[ 'data-testValue' ];
const testPreviousValue =
renderer.root.findByType( 'div' ).props[ 'data-previousValue' ];
expect( testValue ).toBe( 1 );
expect( testPreviousValue ).toBe( undefined );
} );
it( 'test new and previous value', () => {
let testValue;
let testPreviousValue;
act( () => {
renderer = TestRenderer.create( <TestComponent testValue={ 1 } /> );
} );
act( () => {
renderer.update( <TestComponent testValue={ 2 } /> );
} );
testValue = renderer.root.findByType( 'div' ).props[ 'data-testValue' ];
testPreviousValue =
renderer.root.findByType( 'div' ).props[ 'data-previousValue' ];
expect( testValue ).toBe( 2 );
expect( testPreviousValue ).toBe( 1 );
act( () => {
renderer.update( <TestComponent testValue={ 3 } /> );
} );
testValue = renderer.root.findByType( 'div' ).props[ 'data-testValue' ];
testPreviousValue =
renderer.root.findByType( 'div' ).props[ 'data-previousValue' ];
expect( testValue ).toBe( 3 );
expect( testPreviousValue ).toBe( 2 );
} );
it( 'should not update value if validation fails', () => {
let testValue;
let testPreviousValue;
act( () => {
renderer = TestRenderer.create(
<TestComponent testValue={ 1 } validation={ Number.isFinite } />
);
} );
act( () => {
renderer.update(
<TestComponent testValue="abc" validation={ Number.isFinite } />
);
} );
testValue = renderer.root.findByType( 'div' ).props[ 'data-testValue' ];
testPreviousValue =
renderer.root.findByType( 'div' ).props[ 'data-previousValue' ];
expect( testValue ).toBe( 'abc' );
expect( testPreviousValue ).toBe( 1 );
act( () => {
renderer.update(
<TestComponent testValue={ 3 } validation={ Number.isFinite } />
);
} );
testValue = renderer.root.findByType( 'div' ).props[ 'data-testValue' ];
testPreviousValue =
renderer.root.findByType( 'div' ).props[ 'data-previousValue' ];
expect( testValue ).toBe( 3 );
expect( testPreviousValue ).toBe( 1 );
} );
} );

View File

@@ -0,0 +1,79 @@
// We need to disable the following eslint check as it's only applicable
// to testing-library/react not `react-test-renderer` used here
/* eslint-disable testing-library/await-async-query */
/**
* External dependencies
*/
import TestRenderer, { act } from 'react-test-renderer';
/**
* Internal dependencies
*/
import { useShallowEqual } from '../use-shallow-equal';
describe( 'useShallowEqual', () => {
const TestComponent = ( { testValue } ) => {
const newValue = useShallowEqual( testValue );
return <div data-newValue={ newValue } />;
};
let renderer;
beforeEach( () => ( renderer = null ) );
it.each`
testValueA | aType | testValueB | bType
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'b' } } | ${ 'object' }
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'b', 'bar' ] } | ${ 'array' }
${ 1 } | ${ 'number' } | ${ 1 } | ${ 'number' }
${ '1' } | ${ 'string' } | ${ '1' } | ${ 'string' }
${ true } | ${ 'bool' } | ${ true } | ${ 'bool' }
`(
'$testValueA ($aType) and $testValueB ($bType) are expected to be equal',
( { testValueA, testValueB } ) => {
let testPropValue;
act( () => {
renderer = TestRenderer.create(
<TestComponent testValue={ testValueA } />
);
} );
testPropValue =
renderer.root.findByType( 'div' ).props[ 'data-newValue' ];
expect( testPropValue ).toBe( testValueA );
// do update
act( () => {
renderer.update( <TestComponent testValue={ testValueB } /> );
} );
testPropValue =
renderer.root.findByType( 'div' ).props[ 'data-newValue' ];
expect( testPropValue ).toBe( testValueA );
}
);
it.each`
testValueA | aType | testValueB | bType
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'c' } } | ${ 'object' }
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'bar', 'b' ] } | ${ 'array' }
${ 1 } | ${ 'number' } | ${ '1' } | ${ 'string' }
${ 1 } | ${ 'number' } | ${ 2 } | ${ 'number' }
${ 1 } | ${ 'number' } | ${ true } | ${ 'bool' }
${ 0 } | ${ 'number' } | ${ false } | ${ 'bool' }
`(
'$testValueA ($aType) and $testValueB ($bType) are expected to not be equal',
( { testValueA, testValueB } ) => {
let testPropValue;
act( () => {
renderer = TestRenderer.create(
<TestComponent testValue={ testValueA } />
);
} );
testPropValue =
renderer.root.findByType( 'div' ).props[ 'data-newValue' ];
expect( testPropValue ).toBe( testValueA );
// do update
act( () => {
renderer.update( <TestComponent testValue={ testValueB } /> );
} );
testPropValue =
renderer.root.findByType( 'div' ).props[ 'data-newValue' ];
expect( testPropValue ).toBe( testValueB );
}
);
} );

View File

@@ -0,0 +1,47 @@
/**
* External dependencies
*/
import { useResizeObserver } from '@wordpress/compose';
/**
* Returns a resizeListener element and a class name based on its width.
* Class names are based on the smaller of the breakpoints:
* https://github.com/WordPress/gutenberg/tree/master/packages/viewport#usage
* Values are also based on those breakpoints minus ~80px which is approximately
* the left + right margin in Storefront with a font-size of 16px.
* _Note: `useContainerQueries` will return an empty class name `` until after
* first render_
*
* @return {Array} An array of {Element} `resizeListener` and {string} `className`.
*
* @example
*
* ```js
* const App = () => {
* const [ resizeListener, containerClassName ] = useContainerQueries();
*
* return (
* <div className={ containerClassName }>
* { resizeListener }
* Your content here
* </div>
* );
* };
* ```
*/
export const useContainerQueries = (): [ React.ReactElement, string ] => {
const [ resizeListener, { width } ] = useResizeObserver();
let className = '';
if ( width > 700 ) {
className = 'is-large';
} else if ( width > 520 ) {
className = 'is-medium';
} else if ( width > 400 ) {
className = 'is-small';
} else if ( width ) {
className = 'is-mobile';
}
return [ resizeListener, className ];
};

View File

@@ -0,0 +1,38 @@
/**
* External dependencies
*/
import { useCallback, useEffect, useRef } from '@wordpress/element';
/**
* Returns a boolean value based on whether the current component has been mounted.
*
* @return {boolean} If the component has been mounted.
*
* @example
*
* ```js
* const App = () => {
* const isMounted = useIsMounted();
*
* if ( ! isMounted() ) {
* return null;
* }
*
* return </div>;
* };
* ```
*/
export function useIsMounted() {
const isMounted = useRef( false );
useEffect( () => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, [] );
return useCallback( () => isMounted.current, [] );
}

View File

@@ -0,0 +1,37 @@
/**
* External dependencies
*/
import { useEffect, useState } from '@wordpress/element';
import type { Dispatch, SetStateAction } from 'react';
export const useLocalStorageState = < T >(
key: string,
initialValue: T
): [ T, Dispatch< SetStateAction< T > > ] => {
const [ state, setState ] = useState< T >( () => {
const valueInLocalStorage = window.localStorage.getItem( key );
if ( valueInLocalStorage ) {
try {
return JSON.parse( valueInLocalStorage );
} catch {
// eslint-disable-next-line no-console
console.error(
`Value for key '${ key }' could not be retrieved from localStorage because it can't be parsed.`
);
}
}
return initialValue;
} );
useEffect( () => {
try {
window.localStorage.setItem( key, JSON.stringify( state ) );
} catch {
// eslint-disable-next-line no-console
console.error(
`Value for key '${ key }' could not be saved in localStorage because it can't be converted into a string.`
);
}
}, [ key, state ] );
return [ state, setState ];
};

View File

@@ -0,0 +1,84 @@
/**
* External dependencies
*/
import { useRef, useLayoutEffect, useState } from '@wordpress/element';
/** @typedef {import('react')} React */
/** @type {React.CSSProperties} */
const style = {
bottom: 0,
left: 0,
opacity: 0,
pointerEvents: 'none',
position: 'absolute',
right: 0,
top: 0,
zIndex: -1,
};
/**
* Returns an element and a string (`above`, `visible` or `below`) based on the
* element position relative to the viewport.
* _Note: `usePositionRelativeToViewport` will return an empty position (``)
* until after first render_
*
* @return {Array} An array of {Element} `referenceElement` and {string} `positionRelativeToViewport`.
*
* @example
*
* ```js
* const App = () => {
* const [ referenceElement, positionRelativeToViewport ] = useContainerQueries();
*
* return (
* <>
* { referenceElement }
* { positionRelativeToViewport === 'below' && <p>Reference element is below the viewport.</p> }
* { positionRelativeToViewport === 'visible' && <p>Reference element is visible in the viewport.</p> }
* { positionRelativeToViewport === 'above' && <p>Reference element is above the viewport.</p> }
* </>
* );
* };
* ```
*/
export const usePositionRelativeToViewport = () => {
const [ positionRelativeToViewport, setPositionRelativeToViewport ] =
useState( '' );
const referenceElementRef = useRef( null );
const intersectionObserver = useRef(
new IntersectionObserver(
( entries ) => {
if ( entries[ 0 ].isIntersecting ) {
setPositionRelativeToViewport( 'visible' );
} else {
setPositionRelativeToViewport(
entries[ 0 ].boundingClientRect.top > 0
? 'below'
: 'above'
);
}
},
{ threshold: 1.0 }
)
);
useLayoutEffect( () => {
const referenceElementNode = referenceElementRef.current;
const observer = intersectionObserver.current;
if ( referenceElementNode ) {
observer.observe( referenceElementNode );
}
return () => {
observer.unobserve( referenceElementNode );
};
}, [] );
const referenceElement = (
<div aria-hidden={ true } ref={ referenceElementRef } style={ style } />
);
return [ referenceElement, positionRelativeToViewport ];
};

View File

@@ -0,0 +1,32 @@
/**
* External dependencies
*/
import { useRef, useEffect } from '@wordpress/element';
interface Validation< T > {
( value: T, previousValue: T | undefined ): boolean;
}
/**
* Use Previous based on https://usehooks.com/useprevious/.
*
* @param {*} value
* @param {Function} [validation] Function that needs to validate for the value
* to be updated.
*/
export function usePrevious< T >(
value: T,
validation?: Validation< T >
): T | undefined {
const ref = useRef< T >();
useEffect( () => {
if (
ref.current !== value &&
( ! validation || validation( value, ref.current ) )
) {
ref.current = value;
}
}, [ value, validation ] );
return ref.current;
}

View File

@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { useRef } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
/**
* A custom hook that compares the provided value across renders and returns the
* previous instance if shallow equality with previous instance exists.
*
* This is particularly useful when non-primitive types are used as
* dependencies for react hooks.
*
* @param {*} value Value to keep the same if satisfies shallow equality.
*
* @return {*} The previous cached instance of the value if the current has shallow equality with it.
*/
export function useShallowEqual< T >( value: T ): T {
const ref = useRef< T >( value );
if ( ! isShallowEqual( value, ref.current ) ) {
ref.current = value;
}
return ref.current;
}

View File

@@ -0,0 +1,25 @@
/**
* External dependencies
*/
import { useEffect, renderToString } from '@wordpress/element';
import { speak } from '@wordpress/a11y';
/**
* Custom hook which announces the message with the given politeness, if a
* valid message is provided.
*/
export const useSpokenMessage = (
message: string | React.ReactNode | undefined,
politeness: 'polite' | 'assertive' | undefined
) => {
const spokenMessage =
typeof message === 'string' ? message : renderToString( message );
useEffect( () => {
if ( spokenMessage ) {
speak( spokenMessage, politeness );
}
}, [ spokenMessage, politeness ] );
};
export default useSpokenMessage;

View File

@@ -0,0 +1,88 @@
/**
* External dependencies
*/
import classnames from 'classnames';
import { isString, isObject } from '@woocommerce/types';
import type { Style as StyleEngineProperties } from '@wordpress/style-engine/src/types';
import type { CSSProperties } from 'react';
/**
* Internal dependencies
*/
import { useTypographyProps } from './use-typography-props';
import {
getColorClassesAndStyles,
getBorderClassesAndStyles,
getSpacingClassesAndStyles,
} from '../utils';
export type StyleProps = {
className: string;
style: CSSProperties;
};
type BlockAttributes = Record< string, unknown > & {
style?: StyleEngineProperties | string | undefined;
};
type StyleAttributes = Record< string, unknown > & {
style: StyleEngineProperties;
};
/**
* Parses incoming props.
*
* This may include style properties at the top level, or may include a nested `style` object. This ensures the expected
* values are present and converts any string based values to objects as required.
*/
const parseStyleAttributes = ( rawProps: BlockAttributes ): StyleAttributes => {
const props = isObject( rawProps )
? rawProps
: {
style: {},
};
let style = props.style;
if ( isString( style ) ) {
style = JSON.parse( style ) || {};
}
if ( ! isObject( style ) ) {
style = {};
}
return {
...props,
style,
};
};
/**
* Returns the CSS class names and inline styles for a block when provided with its props/attributes.
*
* This hook (and its utilities) borrow functionality from the Gutenberg Block Editor package--something we don't want
* to import on the frontend.
*/
export const useStyleProps = ( props: BlockAttributes ): StyleProps => {
const styleAttributes = parseStyleAttributes( props );
const colorProps = getColorClassesAndStyles( styleAttributes );
const borderProps = getBorderClassesAndStyles( styleAttributes );
const spacingProps = getSpacingClassesAndStyles( styleAttributes );
const typographyProps = useTypographyProps( styleAttributes );
return {
className: classnames(
typographyProps.className,
colorProps.className,
borderProps.className,
spacingProps.className
),
style: {
...typographyProps.style,
...colorProps.style,
...borderProps.style,
...spacingProps.style,
},
};
};

View File

@@ -0,0 +1,18 @@
/**
* External dependencies
*/
import { useState, useCallback } from '@wordpress/element';
/**
* Helper method for throwing an error in a React Hook.
*
* @see https://github.com/facebook/react/issues/14981
*/
export const useThrowError = (): ( ( error: Error ) => void ) => {
const [ , setState ] = useState();
return useCallback( ( error: Error ): void => {
setState( () => {
throw error;
} );
}, [] );
};

View File

@@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { isObject, isString } from '@woocommerce/types';
import type { Style as StyleEngineProperties } from '@wordpress/style-engine/src/types';
/**
* Internal dependencies
*/
import type { StyleProps } from './use-style-props';
type blockAttributes = {
style: StyleEngineProperties;
// String identifier for the font size preset--not an absolute value.
fontSize?: string | undefined;
// String identifier for the font family preset, not the actual font family.
fontFamily?: string | undefined;
};
export const useTypographyProps = ( props: blockAttributes ): StyleProps => {
const typography = isObject( props.style.typography )
? props.style.typography
: {};
const classNameFallback = isString( typography.fontFamily )
? typography.fontFamily
: '';
const className = props.fontFamily
? `has-${ props.fontFamily }-font-family`
: classNameFallback;
return {
className,
style: {
fontSize: props.fontSize
? `var(--wp--preset--font-size--${ props.fontSize })`
: typography.fontSize,
fontStyle: typography.fontStyle,
fontWeight: typography.fontWeight,
letterSpacing: typography.letterSpacing,
lineHeight: typography.lineHeight,
textDecoration: typography.textDecoration,
textTransform: typography.textTransform,
},
};
};