-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
54eb2eb
Rename files
mirka 282c9a2
FocalPickerControl: Convert to TypeScript
mirka 7181b31
Improve Storybook
mirka 8c6606d
Add changelog
mirka ad368db
Fix test import
mirka 729e480
Fix onDragEnd() types
mirka 4404e59
Rename misleading `UnitControl` subcomponent
mirka 2108701
Listen to all `on*` callbacks in Storybook
mirka bfedb27
Use prop types from FocalPointPickerMediaProps
mirka 5b9cd4d
Disallow undefined return from resolvePoint()
mirka 4090caa
Better type `onLoad`
mirka 4a5f79c
Fix typo
mirka 4736d15
Fix useDragging types
mirka 6a226c8
Annotate `getValueWithinDragArea` complication
mirka e23efc1
Remove parseFloat calls
mirka d354a6a
Fixup control types
mirka f709d34
Cast mediaRef types
mirka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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; | ||
|
@@ -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( | ||
|
@@ -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 ) ); | ||
}; | ||
|
||
|
@@ -161,6 +238,7 @@ export default function FocalPointPicker( { | |
|
||
return ( | ||
<BaseControl | ||
{ ...restProps } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
label={ label } | ||
id={ id } | ||
help={ help } | ||
|
@@ -176,7 +254,7 @@ export default function FocalPointPicker( { | |
} } | ||
ref={ dragAreaRef } | ||
role="button" | ||
tabIndex="-1" | ||
tabIndex={ -1 } | ||
> | ||
<Grid bounds={ bounds } showOverlay={ showGridOverlay } /> | ||
<Media | ||
|
@@ -200,3 +278,5 @@ export default function FocalPointPicker( { | |
</BaseControl> | ||
); | ||
} | ||
|
||
export default FocalPointPicker; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.