Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FocalPointPicker: Convert to TypeScript #43872

Merged
merged 17 commits into from
Sep 8, 2022
Merged
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
- Refactor `FocalPointPicker` to function component ([#39168](https://github.com/WordPress/gutenberg/pull/39168)).
- `Guide`: use `code` instead of `keyCode` for keyboard events ([#43604](https://github.com/WordPress/gutenberg/pull/43604/)).
- `ToggleControl`: Convert to TypeScript and streamline CSS ([#43717](https://github.com/WordPress/gutenberg/pull/43717)).
- `FocalPointPicker`: Convert to TypeScript ([#43872](https://github.com/WordPress/gutenberg/pull/43872)).
- `Navigation`: use `code` instead of `keyCode` for keyboard events ([#43644](https://github.com/WordPress/gutenberg/pull/43644/)).
- `ComboboxControl`: Add unit tests ([#42403](https://github.com/WordPress/gutenberg/pull/42403)).
- `NavigableContainer`: use `code` instead of `keyCode` for keyboard events, rewrite tests using RTL and `user-event` ([#43606](https://github.com/WordPress/gutenberg/pull/43606/)).
Expand Down
4 changes: 2 additions & 2 deletions packages/components/src/focal-point-picker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

Focal Point Picker is a component which creates a UI for identifying the most important visual point of an image. This component addresses a specific problem: with large background images it is common to see undesirable crops, especially when viewing on smaller viewports such as mobile phones. This component allows the selection of the point with the most important visual information and returns it as a pair of numbers between 0 and 1. This value can be easily converted into the CSS `background-position` attribute, and will ensure that the focal point is never cropped out, regardless of viewport.

Example focal point picker value: `{ x: 0.5, y: 0.1 }`
Corresponding CSS: `background-position: 50% 10%;`
- Example focal point picker value: `{ x: 0.5, y: 0.1 }`
- Corresponding CSS: `background-position: 50% 10%;`

## Usage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import { __ } from '@wordpress/i18n';
* Internal dependencies
*/
import {
UnitControl as BaseUnitControl,
StyledUnitControl,
ControlWrapper,
} from './styles/focal-point-picker-style';
import { fractionToPercentage } from './utils';
import type {
UnitControlProps,
UnitControlOnChangeCallback,
} from '../unit-control/types';
import type { FocalPointAxis, FocalPointPickerControlsProps } from './types';

const TEXTCONTROL_MIN = 0;
const TEXTCONTROL_MAX = 100;
Expand All @@ -22,11 +27,16 @@ export default function FocalPointPickerControls( {
x: 0.5,
y: 0.5,
},
} ) {
}: FocalPointPickerControlsProps ) {
const valueX = fractionToPercentage( point.x );
const valueY = fractionToPercentage( point.y );

const handleChange = ( value, axis ) => {
const handleChange = (
value: Parameters< UnitControlOnChangeCallback >[ 0 ],
axis: FocalPointAxis
) => {
if ( value === undefined ) return;
Copy link
Member Author

Choose a reason for hiding this comment

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

Return early because TS doesn't want an undefined value passed to parseInt. The result is the same though, so shouldn't be considered a runtime change.


const num = parseInt( value, 10 );

if ( ! isNaN( num ) ) {
Expand All @@ -36,25 +46,37 @@ export default function FocalPointPickerControls( {

return (
<ControlWrapper className="focal-point-picker__controls">
<UnitControl
<FocalPointUnitControl
label={ __( 'Left' ) }
value={ [ valueX, '%' ].join( '' ) }
onChange={ ( next ) => handleChange( next, 'x' ) }
onChange={
( ( next ) =>
handleChange(
next,
'x'
) ) as UnitControlOnChangeCallback
}
dragDirection="e"
/>
<UnitControl
<FocalPointUnitControl
label={ __( 'Top' ) }
value={ [ valueY, '%' ].join( '' ) }
onChange={ ( next ) => handleChange( next, 'y' ) }
onChange={
( ( next ) =>
handleChange(
next,
'y'
) ) as UnitControlOnChangeCallback
}
dragDirection="s"
/>
</ControlWrapper>
);
}

function UnitControl( props ) {
function FocalPointUnitControl( props: UnitControlProps ) {
return (
<BaseUnitControl
<StyledUnitControl
className="focal-point-picker__controls-position-unit-control"
labelPosition="top"
max={ TEXTCONTROL_MAX }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ import {
* External dependencies
*/
import classnames from 'classnames';
import type { FocalPointProps } from './types';
import type { WordPressComponentProps } from '../ui/context';

export default function FocalPoint( { left = '50%', top = '50%', ...props } ) {
export default function FocalPoint( {
left = '50%',
top = '50%',
...props
}: WordPressComponentProps< FocalPointProps, 'div' > ) {
const classes = classnames(
'components-focal-point-picker__icon_container'
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import {
GridLineX,
GridLineY,
} from './styles/focal-point-picker-style';
import type { FocalPointPickerGridProps } from './types';
import type { WordPressComponentProps } from '../ui/context/wordpress-component';

export default function FocalPointPickerGrid( { bounds, ...props } ) {
export default function FocalPointPickerGrid( {
bounds,
...props
}: WordPressComponentProps< FocalPointPickerGridProps, 'div' > ) {
return (
<GridView
{ ...props }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,61 @@ import {
} from './styles/focal-point-picker-style';
import { INITIAL_BOUNDS } from './utils';
import { useUpdateEffect } from '../utils/hooks';
import type { WordPressComponentProps } from '../ui/context/wordpress-component';
import type {
FocalPoint as FocalPointType,
FocalPointPickerProps,
} from './types';
import type { KeyboardEventHandler } from 'react';

const GRID_OVERLAY_TIMEOUT = 600;

export default function FocalPointPicker( {
/**
* Focal Point Picker is a component which creates a UI for identifying the most important visual point of an image.
*
* This component addresses a specific problem: with large background images it is common to see undesirable crops,
* especially when viewing on smaller viewports such as mobile phones. This component allows the selection of
* the point with the most important visual information and returns it as a pair of numbers between 0 and 1.
* This value can be easily converted into the CSS `background-position` attribute, and will ensure that the
* focal point is never cropped out, regardless of viewport.
*
* - Example focal point picker value: `{ x: 0.5, y: 0.1 }`
* - Corresponding CSS: `background-position: 50% 10%;`
*
* ```jsx
* import { FocalPointPicker } from '@wordpress/components';
* import { useState } from '@wordpress/element';
*
* const Example = () => {
* const [ focalPoint, setFocalPoint ] = useState( {
* x: 0.5,
* y: 0.5,
* } );
*
* const url = '/path/to/image';
*
* // Example function to render the CSS styles based on Focal Point Picker value
* const style = {
* backgroundImage: `url(${ url })`,
* backgroundPosition: `${ focalPoint.x * 100 }% ${ focalPoint.y * 100 }%`,
* };
*
* return (
* <>
* <FocalPointPicker
* url={ url }
* value={ focalPoint }
* onDragStart={ setFocalPoint }
* onDrag={ setFocalPoint }
* onChange={ setFocalPoint }
* />
* <div style={ style } />
* </>
* );
* };
* ```
*/
export function FocalPointPicker( {
autoPlay = true,
className,
help,
Expand All @@ -46,36 +97,45 @@ export default function FocalPointPicker( {
x: 0.5,
y: 0.5,
},
} ) {
...restProps
}: WordPressComponentProps< FocalPointPickerProps, 'div', false > ) {
const [ point, setPoint ] = useState( valueProp );
const [ showGridOverlay, setShowGridOverlay ] = useState( false );

const { startDrag, endDrag, isDragging } = useDragging( {
onDragStart: ( event ) => {
dragAreaRef.current.focus();
dragAreaRef.current?.focus();
const value = getValueWithinDragArea( event );

// `value` can technically be undefined if getValueWithinDragArea() is
// called before dragAreaRef is set, but this shouldn't happen in reality.
if ( ! value ) return;
mirka marked this conversation as resolved.
Show resolved Hide resolved

onDragStart?.( value, event );
setPoint( value );
},
onDragMove: ( event ) => {
// Prevents text-selection when dragging.
event.preventDefault();
const value = getValueWithinDragArea( event );
if ( ! value ) return;
onDrag?.( value, event );
setPoint( value );
},
onDragEnd: ( event ) => {
onDragEnd?.( event );
onDragEnd: () => {
onDragEnd?.();
mirka marked this conversation as resolved.
Show resolved Hide resolved
onChange?.( point );
},
} );

// Uses the internal point while dragging or else the value from props.
const { x, y } = isDragging ? point : valueProp;

const dragAreaRef = useRef();
const dragAreaRef = useRef< HTMLDivElement >( null );
const [ bounds, setBounds ] = useState( INITIAL_BOUNDS );
const refUpdateBounds = useRef( () => {
if ( ! dragAreaRef.current ) return;
Copy link
Member Author

Choose a reason for hiding this comment

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

The rest of this function doesn't work if the ref is unset.


const { clientWidth: width, clientHeight: height } =
dragAreaRef.current;
// Falls back to initial bounds if the ref has no size. Since styles
Expand All @@ -88,15 +148,29 @@ export default function FocalPointPicker( {

useEffect( () => {
const updateBounds = refUpdateBounds.current;
if ( ! dragAreaRef.current ) return;

const { defaultView } = dragAreaRef.current.ownerDocument;
defaultView.addEventListener( 'resize', updateBounds );
return () => defaultView.removeEventListener( 'resize', updateBounds );
defaultView?.addEventListener( 'resize', updateBounds );
return () => defaultView?.removeEventListener( 'resize', updateBounds );
}, [] );

// Updates the bounds to cover cases of unspecified media or load failures.
useIsomorphicLayoutEffect( () => void refUpdateBounds.current(), [] );

const getValueWithinDragArea = ( { clientX, clientY, shiftKey } ) => {
// TODO: Consider refactoring getValueWithinDragArea() into a pure function.
// https://github.com/WordPress/gutenberg/pull/43872#discussion_r963455173
const getValueWithinDragArea = ( {
clientX,
clientY,
shiftKey,
}: {
clientX: number;
clientY: number;
shiftKey: boolean;
} ) => {
if ( ! dragAreaRef.current ) return;

const { top, left } = dragAreaRef.current.getBoundingClientRect();
let nextX = ( clientX - left ) / bounds.width;
let nextY = ( clientY - top ) / bounds.height;
Expand All @@ -108,17 +182,20 @@ export default function FocalPointPicker( {
return getFinalValue( { x: nextX, y: nextY } );
};

const getFinalValue = ( value ) => {
const getFinalValue = ( value: FocalPointType ): FocalPointType => {
const resolvedValue = resolvePoint?.( value ) ?? value;
resolvedValue.x = Math.max( 0, Math.min( resolvedValue.x, 1 ) );
resolvedValue.y = Math.max( 0, Math.min( resolvedValue.y, 1 ) );
const roundToTwoDecimalPlaces = ( n: number ) =>
Math.round( n * 1e2 ) / 1e2;
ciampo marked this conversation as resolved.
Show resolved Hide resolved

return {
x: parseFloat( resolvedValue.x ).toFixed( 2 ),
y: parseFloat( resolvedValue.y ).toFixed( 2 ),
x: roundToTwoDecimalPlaces( resolvedValue.x ),
y: roundToTwoDecimalPlaces( resolvedValue.y ),
};
};

const arrowKeyStep = ( event ) => {
const arrowKeyStep: KeyboardEventHandler< HTMLDivElement > = ( event ) => {
const { code, shiftKey } = event;
if (
! [ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight' ].includes(
Expand All @@ -133,7 +210,7 @@ export default function FocalPointPicker( {
const delta =
code === 'ArrowUp' || code === 'ArrowLeft' ? -1 * step : step;
const axis = code === 'ArrowUp' || code === 'ArrowDown' ? 'y' : 'x';
value[ axis ] = parseFloat( value[ axis ] ) + delta;
value[ axis ] = value[ axis ] + delta;
onChange?.( getFinalValue( value ) );
};

Expand Down Expand Up @@ -161,6 +238,7 @@ export default function FocalPointPicker( {

return (
<BaseControl
{ ...restProps }
Copy link
Member Author

Choose a reason for hiding this comment

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

⚠️ Runtime change: Pass through rest props.

label={ label }
id={ id }
help={ help }
Expand All @@ -176,7 +254,7 @@ export default function FocalPointPicker( {
} }
ref={ dragAreaRef }
role="button"
tabIndex="-1"
tabIndex={ -1 }
>
<Grid bounds={ bounds } showOverlay={ showGridOverlay } />
<Media
Expand All @@ -200,3 +278,5 @@ export default function FocalPointPicker( {
</BaseControl>
);
}

export default FocalPointPicker;
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
/**
* External dependencies
*/
import type { Ref } from 'react';

/**
* Internal dependencies
*/
import { MediaPlaceholder } from './styles/focal-point-picker-style';
import { isVideoType } from './utils';
import type { FocalPointPickerMediaProps } from './types';

export default function Media( {
alt,
Expand All @@ -14,12 +20,12 @@ export default function Media( {
// https://github.com/testing-library/react-testing-library/issues/470
muted = true,
...props
} ) {
}: FocalPointPickerMediaProps ) {
if ( ! src ) {
return (
<MediaPlaceholder
className="components-focal-point-picker__media components-focal-point-picker__media--placeholder"
ref={ mediaRef }
ref={ mediaRef as Ref< HTMLDivElement > }
{ ...props }
/>
);
Expand All @@ -35,7 +41,7 @@ export default function Media( {
loop
muted={ muted }
onLoadedData={ onLoad }
ref={ mediaRef }
ref={ mediaRef as Ref< HTMLVideoElement > }
src={ src }
/>
) : (
Expand All @@ -44,7 +50,7 @@ export default function Media( {
alt={ alt }
className="components-focal-point-picker__media components-focal-point-picker__media--image"
onLoad={ onLoad }
ref={ mediaRef }
ref={ mediaRef as Ref< HTMLImageElement > }
src={ src }
/>
);
Expand Down
Loading