-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
ColorPicker: improve UX of dragging the handle when in popover on top of the editor #55149
Changes from all commits
d508487
d6b7e84
64cfbef
37ff16f
d6b387b
49030c6
7e342bf
1c5ae3d
54146e2
74e1418
fc32af5
90147e6
fdf61a0
c9a6923
2b8f56f
d475284
e3a7be0
9318bf8
7a4afc1
a065cf8
e483ec2
8c95ac1
d008cab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ import namesPlugin from 'colord/plugins/names'; | |
* WordPress dependencies | ||
*/ | ||
import { useCallback, useState, useMemo } from '@wordpress/element'; | ||
import { useDebounce } from '@wordpress/compose'; | ||
import { useDebounce, useMergeRefs } from '@wordpress/compose'; | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
/** | ||
|
@@ -49,8 +49,24 @@ const UnconnectedColorPicker = ( | |
onChange, | ||
defaultValue = '#fff', | ||
copyFormat, | ||
|
||
// Context | ||
onPickerDragStart, | ||
onPickerDragEnd, | ||
Comment on lines
+54
to
+55
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. These "props" are only exposed via internal context, therefore not causing an API change to the component |
||
...divProps | ||
} = useContextSystem( props, 'ColorPicker' ); | ||
} = useContextSystem< | ||
ColorPickerProps & { | ||
onPickerDragStart?: ( event: MouseEvent ) => void; | ||
onPickerDragEnd?: ( event: MouseEvent ) => void; | ||
} | ||
>( props, 'ColorPicker' ); | ||
|
||
const [ containerEl, setContainerEl ] = useState< HTMLElement | null >( | ||
null | ||
); | ||
const containerRef = ( node: HTMLElement | null ) => { | ||
setContainerEl( node ); | ||
}; | ||
|
||
// Use a safe default value for the color and remove the possibility of `undefined`. | ||
const [ color, setColor ] = useControlledValue( { | ||
|
@@ -77,11 +93,17 @@ const UnconnectedColorPicker = ( | |
); | ||
|
||
return ( | ||
<ColorfulWrapper ref={ forwardedRef } { ...divProps }> | ||
<ColorfulWrapper | ||
ref={ useMergeRefs( [ containerRef, forwardedRef ] ) } | ||
{ ...divProps } | ||
> | ||
<Picker | ||
containerEl={ containerEl } | ||
onChange={ handleChange } | ||
color={ safeColordColor } | ||
enableAlpha={ enableAlpha } | ||
onDragStart={ onPickerDragStart } | ||
onDragEnd={ onPickerDragEnd } | ||
/> | ||
<AuxiliaryColorArtefactWrapper> | ||
<AuxiliaryColorArtefactHStackHeader justify="space-between"> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,18 +7,112 @@ import { colord } from 'colord'; | |
/** | ||
* WordPress dependencies | ||
*/ | ||
import { useMemo } from '@wordpress/element'; | ||
import { useMemo, useEffect, useRef } from '@wordpress/element'; | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import type { PickerProps } from './types'; | ||
|
||
export const Picker = ( { color, enableAlpha, onChange }: PickerProps ) => { | ||
/** | ||
* Track the start and the end of drag pointer events related to controlling | ||
* the picker's saturation / hue / alpha, and fire the corresponding callbacks. | ||
* This is particularly useful to implement synergies like the one with the | ||
* `Popover` component, where a pointer events "trap" is rendered while | ||
* the user is dragging the pointer to avoid potential interference with iframe | ||
* elements. | ||
* | ||
* @param props | ||
* @param props.containerEl | ||
* @param props.onDragStart | ||
* @param props.onDragEnd | ||
*/ | ||
const useOnPickerDrag = ( { | ||
containerEl, | ||
onDragStart, | ||
onDragEnd, | ||
}: Pick< PickerProps, 'containerEl' | 'onDragStart' | 'onDragEnd' > ) => { | ||
const isDragging = useRef( false ); | ||
const leftWhileDragging = useRef( false ); | ||
useEffect( () => { | ||
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. Does it make sense to move this functionality to a separate hook? I think it makes sense because it's separate and independent enough to be its own hook, but also because it can help document the behavior better. WDYT? 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. Done in e483ec2 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. It makes sense to add some comments that explain what we're doing here. The documentation could also include what else we're doing to achieve the mouse trap hack (like the additional CSS and the backdrop show and hide trick). 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. Done in d008cab |
||
if ( ! containerEl || ( ! onDragStart && ! onDragEnd ) ) { | ||
return; | ||
} | ||
const interactiveElements = [ | ||
containerEl.querySelector( '.react-colorful__saturation' ), | ||
containerEl.querySelector( '.react-colorful__hue' ), | ||
containerEl.querySelector( '.react-colorful__alpha' ), | ||
].filter( ( el ) => !! el ) as Element[]; | ||
Comment on lines
+40
to
+44
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. Any chance there might be differences in the list of the existing interactive elements on the separate 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. These elements should always be rendered in the component, as they are part of the |
||
|
||
if ( interactiveElements.length === 0 ) { | ||
return; | ||
} | ||
|
||
const doc = containerEl.ownerDocument; | ||
|
||
const onPointerUp: EventListener = ( event ) => { | ||
isDragging.current = false; | ||
leftWhileDragging.current = false; | ||
onDragEnd?.( event as MouseEvent ); | ||
}; | ||
|
||
const onPointerDown: EventListener = ( event ) => { | ||
isDragging.current = true; | ||
onDragStart?.( event as MouseEvent ); | ||
}; | ||
|
||
const onPointerLeave: EventListener = () => { | ||
leftWhileDragging.current = isDragging.current; | ||
}; | ||
|
||
// Try to detect if the user released the pointer while away from the | ||
// current window. If the check is successfull, the dragEnd callback will | ||
// called as soon as the pointer re-enters the window (better late than never) | ||
const onPointerEnter: EventListener = ( event ) => { | ||
const noPointerButtonsArePressed = | ||
( event as PointerEvent ).buttons === 0; | ||
|
||
if ( leftWhileDragging.current && noPointerButtonsArePressed ) { | ||
onPointerUp( event ); | ||
} | ||
}; | ||
|
||
// The pointerdown event is added on the interactive elements, | ||
// while the remaining events are added on the document object since | ||
// the pointer wouldn't necessarily be hovering the initial interactive | ||
// element at that point. | ||
interactiveElements.forEach( ( el ) => | ||
el.addEventListener( 'pointerdown', onPointerDown ) | ||
); | ||
doc.addEventListener( 'pointerup', onPointerUp ); | ||
doc.addEventListener( 'pointerenter', onPointerEnter ); | ||
doc.addEventListener( 'pointerleave', onPointerLeave ); | ||
|
||
return () => { | ||
interactiveElements.forEach( ( el ) => | ||
el.removeEventListener( 'pointerdown', onPointerDown ) | ||
); | ||
doc.removeEventListener( 'pointerup', onPointerUp ); | ||
doc.removeEventListener( 'pointerenter', onPointerEnter ); | ||
doc.removeEventListener( 'pointerleave', onPointerUp ); | ||
}; | ||
}, [ onDragStart, onDragEnd, containerEl ] ); | ||
}; | ||
|
||
export const Picker = ( { | ||
color, | ||
enableAlpha, | ||
onChange, | ||
onDragStart, | ||
onDragEnd, | ||
containerEl, | ||
}: PickerProps ) => { | ||
const Component = enableAlpha | ||
? RgbaStringColorPicker | ||
: RgbStringColorPicker; | ||
const rgbColor = useMemo( () => color.toRgbString(), [ color ] ); | ||
|
||
useOnPickerDrag( { containerEl, onDragStart, onDragEnd } ); | ||
|
||
return ( | ||
<Component | ||
color={ rgbColor } | ||
|
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.
Why is the
resize: false
necessary?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.
Added a comment in 8c95ac1