Skip to content

Commit

Permalink
FocalPointPicker: Convert to TypeScript (#43872)
Browse files Browse the repository at this point in the history
* Rename files

* FocalPickerControl: Convert to TypeScript

* Improve Storybook

* Add changelog

* Fix test import

* Fix onDragEnd() types

* Rename misleading `UnitControl` subcomponent

* Listen to all `on*` callbacks in Storybook

* Use prop types from FocalPointPickerMediaProps

* Disallow undefined return from resolvePoint()

* Better type `onLoad`

* Fix typo

* Fix useDragging types

* Annotate `getValueWithinDragArea` complication

* Remove parseFloat calls

* Fixup control types

* Cast mediaRef types
  • Loading branch information
mirka authored Sep 8, 2022
1 parent 52cc86e commit 4efdb49
Show file tree
Hide file tree
Showing 16 changed files with 380 additions and 136 deletions.
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;

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;

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?.();
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;

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;

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 }
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

0 comments on commit 4efdb49

Please sign in to comment.