-
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
ColorPicker: improve UX of dragging the handle when in popover on top of the editor #55149
Conversation
Flaky tests detected in d008cab. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/6488089157
|
942d7d1
to
2f6d2f5
Compare
onPickerDragStart, | ||
onPickerDragEnd, |
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.
These "props" are only exposed via internal context, therefore not causing an API change to the component
const onPointerLeave: EventListener = () => { | ||
leftWhileDragging.current = isDragging.current; | ||
}; | ||
|
||
const onPointerEnter: EventListener = ( event ) => { | ||
const noPointerButtonsArePressed = | ||
( event as PointerEvent ).buttons === 0; | ||
|
||
if ( leftWhileDragging.current && noPointerButtonsArePressed ) { | ||
onPointerUp( event ); | ||
} | ||
}; |
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.
This is to make sure that the backdrop would disappear even if the user moves the cursor away from the window and stops dragging. The backdrop should disappear, at the latest, when the cursor comes back on top of the editor's window.
@@ -46,3 +48,57 @@ const Template: StoryFn< typeof ColorPicker > = ( { onChange, ...props } ) => { | |||
}; | |||
|
|||
export const Default = Template.bind( {} ); | |||
|
|||
export const WithDropdown: StoryFn< typeof ColorPicker > = ( { |
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.
This story is around for testing reason and will be removed before merging
{ renderToggle( args ) } | ||
{ isOpen && ( | ||
<Popover | ||
ref={ ( node ) => setPopoverRef( node ) } | ||
position={ position } | ||
onClose={ close } | ||
onFocusOutside={ closeIfFocusOutside } | ||
expandOnMobile={ expandOnMobile } | ||
headerTitle={ headerTitle } | ||
focusOnMount={ focusOnMount } | ||
// This value is used to ensure that the dropdowns | ||
// align with the editor header by default. | ||
offset={ 13 } | ||
anchor={ | ||
! popoverPropsHaveAnchor | ||
? fallbackPopoverAnchor | ||
: undefined | ||
} | ||
variant={ variant } | ||
{ ...popoverProps } | ||
className={ classnames( | ||
'components-dropdown__content', | ||
popoverProps?.className, | ||
contentClassName | ||
) } | ||
> | ||
{ renderContent( args ) } | ||
</Popover> | ||
) } |
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.
This part didn't actually change, just its indentation
*/ | ||
createPortal( | ||
<DropdownPointerEventsCapture | ||
onClick={ () => setShowBackdrop( false ) } |
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.
The backdrop should always disappear when the dragging ends, but if it was to stick around for any reason, the first click would dismiss it.
This is mostly to make sure that the page doesn't end up in a state in which most of the UI isn't clickable
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.
This solution feels good to me and works well for the color/saturation picket. It does look like we still have the same problem of lost focus closing the modal if we try dragging the handle(s) of the hue
or alpha
sliders outside of the ColorPicker
modal.
Screen.Recording.2023-10-10.at.12.56.13.mov
This doesn't feel like it would be an issue anywhere near as often, but can/should we apply the overlay to these two handles as well?
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.
This is testing great for the color picker for me, and I also like that this means the color picker handle is no longer cut off when you drag to the edge of the picker:
Feels nice and stable when dragging around with the mouse cursor sometimes dragging out from the edge of the picker (in my screengrab, ignore the jumping due to scrollbar appearing when the color contrast warning appears) 👍
2023-10-11.12.50.14.mp4
Just left a minor comment about a selector, but the overall direction looks promising to me!
Well spotted @t-hamano ! In fact, the global styles sidebar renders a Since Let me know what you think about it! |
Size Change: +388 B (0%) Total Size: 1.65 MB
ℹ️ View Unchanged
|
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.
This is a very neat hack 👍 Thanks! 🙌
I'm happy with proceeding with a solution like this, I only would recommend adding some documentation to explain to our future selves what we're doing. Extracting the functionality to a separate hook could help self-documenting, too.
const interactiveElements = [ | ||
containerEl.querySelector( '.react-colorful__saturation' ), | ||
containerEl.querySelector( '.react-colorful__hue' ), | ||
containerEl.querySelector( '.react-colorful__alpha' ), | ||
].filter( ( el ) => !! el ) as Element[]; |
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.
Any chance there might be differences in the list of the existing interactive elements on the separate useEffect
runs? If yes, should those somehow be listed as useEffect
dependencies?
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.
These elements should always be rendered in the component, as they are part of the react-colorful
pickers. So I think we're good on this front :)
const Component = enableAlpha | ||
? RgbaStringColorPicker | ||
: RgbStringColorPicker; | ||
const rgbColor = useMemo( () => color.toRgbString(), [ color ] ); | ||
|
||
const isDragging = useRef( false ); | ||
const leftWhileDragging = useRef( false ); | ||
useEffect( () => { |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Done in e483ec2
@@ -147,6 +147,7 @@ export function CustomColorPickerDropdown( { | |||
const popoverProps = useMemo< DropdownProps[ 'popoverProps' ] >( | |||
() => ( { | |||
shift: true, | |||
resize: false, |
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
const Component = enableAlpha | ||
? RgbaStringColorPicker | ||
: RgbStringColorPicker; | ||
const rgbColor = useMemo( () => color.toRgbString(), [ color ] ); | ||
|
||
const isDragging = useRef( false ); | ||
const leftWhileDragging = useRef( false ); | ||
useEffect( () => { |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Done in d008cab
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.
This is testing well for me with all of the recent changes 🎉
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.
Thanks for the updates, the code change looks good to me too, and this is testing nicely in the site editor in Chrome, Safari, FF and Edge. LGTM! ✨
… of the editor (WordPress#55149) * ColorPicker: expose drag start / end handlers via context * ColorPicker: remove overflow hidden rule * Dropdown: add some sort of backdrop to capture pointer events * Add test story * Prevent resizing on color palette's popover, which also triggers overflow: hidden * Improve backdrop position by using react portal * Cleanup * Aria-hidden * Cleanup * Scope querySelector within color picker s container element * Listen for drag events also on hue and alpha sliders * Remove temporary styles * Remove temporary storybook example * CHANGELOG * Move pointer events trap from Dropdown to Popover component * Use state to grab ColorPicker's container element to make sure the component re-renders * Remove overflow:hidden from PaletteEdit component's popover * Update CHANGELOG * Remove dead code * Update snapshot * Move newly added ColorPicker logic to a separate hook * Add inline comment around the resize: false change * Add more comments
… of the editor (#55149) * ColorPicker: expose drag start / end handlers via context * ColorPicker: remove overflow hidden rule * Dropdown: add some sort of backdrop to capture pointer events * Add test story * Prevent resizing on color palette's popover, which also triggers overflow: hidden * Improve backdrop position by using react portal * Cleanup * Aria-hidden * Cleanup * Scope querySelector within color picker s container element * Listen for drag events also on hue and alpha sliders * Remove temporary styles * Remove temporary storybook example * CHANGELOG * Move pointer events trap from Dropdown to Popover component * Use state to grab ColorPicker's container element to make sure the component re-renders * Remove overflow:hidden from PaletteEdit component's popover * Update CHANGELOG * Remove dead code * Update snapshot * Move newly added ColorPicker logic to a separate hook * Add inline comment around the resize: false change * Add more comments
* Add Plugin post excerpt component * introduce and expose PluginPostExcerpt * fill post excerpt panel * minor code enhancement * add plugin post excerpt test * prefix plugin with __experimental * Update packages/edit-post/src/components/sidebar/plugin-post-excerpt/index.js Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> * Update packages/edit-post/src/components/sidebar/post-excerpt/index.js Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> * Update packages/edit-post/src/components/sidebar/post-excerpt/index.js Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> * Update packages/edit-post/src/components/sidebar/post-excerpt/index.js Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> * Update packages/edit-post/src/components/sidebar/plugin-post-excerpt/index.js Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> * Update packages/edit-post/src/components/sidebar/plugin-post-excerpt/test/index.js Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> * ColorPicker: improve UX of dragging the handle when in popover on top of the editor (#55149) * ColorPicker: expose drag start / end handlers via context * ColorPicker: remove overflow hidden rule * Dropdown: add some sort of backdrop to capture pointer events * Add test story * Prevent resizing on color palette's popover, which also triggers overflow: hidden * Improve backdrop position by using react portal * Cleanup * Aria-hidden * Cleanup * Scope querySelector within color picker s container element * Listen for drag events also on hue and alpha sliders * Remove temporary styles * Remove temporary storybook example * CHANGELOG * Move pointer events trap from Dropdown to Popover component * Use state to grab ColorPicker's container element to make sure the component re-renders * Remove overflow:hidden from PaletteEdit component's popover * Update CHANGELOG * Remove dead code * Update snapshot * Move newly added ColorPicker logic to a separate hook * Add inline comment around the resize: false change * Add more comments * remove test snapshots --------- Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com>
* Partially revert "ColorPicker: improve UX of dragging the handle when in popover on top of the editor (#55149)" This reverts commit 9da6f48. * Use pointer capture to improve drag gesture UX * Use the context system again * Update snapshot * Remove unnecessary context provider * Add changelog entry
* Partially revert "ColorPicker: improve UX of dragging the handle when in popover on top of the editor (WordPress#55149)" This reverts commit 9da6f48. * Use pointer capture to improve drag gesture UX * Use the context system again * Update snapshot * Remove unnecessary context provider * Add changelog entry
Fixes #49267
Alternative approach to #54164
What?
Fixes an issue with dragging the handle of the color picker when the
ColorPicker
component is rendered inside aPopover
on top of the editor canvas (aniframe
)Why?
The current UX is poor and leads to a frustrating interaction when the color picker closes unexpectedly in the middle of dragging its handle, should the user move the cursor on top of the editor canvas.
The reason why this bug is happening in the first place is because of how pointer events behave when moving the cursor over an
iframe
. By default,iframe
s would absorb all pointer events — and therefore the color picker handle wouldn't update at all when moving the pointer on top of the editor canvas.But the editor canvas' iframe has been tweaked to bubble certain events (including mouse move), and therefore uses don't experience the behaviour explained in the paragraph above. Instead, it looks like the cursor handle isn't updated as smoothly, and (even worse), moving the cursor while dragging on top of the editor canvas can result in the
iframe
itself receiving focus, which in turn causes thePopover
around theColorPicker
to automatically close (it closes when losing focus).How?
The approach tried in #54164 works, but it results in many API changes to components, an extra context being added and in general quite a lot of code.
This solution, instead, tries to prevent the problem from happening by working solely at
@wordpress/components
level.Basically, whenever a
ColorPicker
is rendered inside aPopover
component, thePopover
component will render an additional backdrop element only while the color picker handle is being dragged. The backdrop serves as a pointer events "trap", avoiding them from ever reaching the underlyingiframe
and therefore causing the bug described above.A better solution, in the future, could be to allow
Popover
to expose amodal
prop (similarly to how thePopover
component fromariakit
already does), which would basically implement this same solution but in a more robust way.Testing Instructions
Testing Instructions for Keyboard
Screenshots or screencast
Before
color-picker-before.mp4
After (note: the backdrop is purposefully tinted to better showcase the solution proposed)
color-picker-after.mp4