Skip to content

Commit

Permalink
ColorPicker: improve UX of dragging the handle when in popover on top…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
ciampo authored Oct 12, 2023
1 parent 05693fb commit 9da6f48
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 64 deletions.
1 change: 1 addition & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

### Bug Fix

- Render a "mouse event trap" when using a `ColorPicker` inside a `Popover` to prevent issues when rendering on top of `iframes` ([#55149](https://github.com/WordPress/gutenberg/pull/55149)).
- `Modal`: fix closing when contained iframe is focused ([#51602](https://github.com/WordPress/gutenberg/pull/51602)).

### Internal
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/color-palette/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ export function CustomColorPickerDropdown( {
const popoverProps = useMemo< DropdownProps[ 'popoverProps' ] >(
() => ( {
shift: true,
// Disabling resize as it would otherwise cause the popover to show
// scrollbars while dragging the color picker's handle close to the
// popover edge.
resize: false,
...( isRenderedInSidebar
? {
// When in the sidebar: open to the left (stacking),
Expand Down
28 changes: 25 additions & 3 deletions packages/components/src/color-picker/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -49,8 +49,24 @@ const UnconnectedColorPicker = (
onChange,
defaultValue = '#fff',
copyFormat,

// Context
onPickerDragStart,
onPickerDragEnd,
...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( {
Expand All @@ -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">
Expand Down
98 changes: 96 additions & 2 deletions packages/components/src/color-picker/picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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( () => {
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[];

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 }
Expand Down
1 change: 0 additions & 1 deletion packages/components/src/color-picker/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export const ColorfulWrapper = styled.div`
align-items: center;
width: 216px;
height: auto;
overflow: hidden;
}
.react-colorful__saturation {
Expand Down
3 changes: 3 additions & 0 deletions packages/components/src/color-picker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export interface PickerProps {
color: Colord;
enableAlpha: boolean;
onChange: ( nextColor: Colord ) => void;
containerEl: HTMLElement | null;
onDragStart?: ( event: MouseEvent ) => void;
onDragEnd?: ( event: MouseEvent ) => void;
}

export interface ColorInputProps {
Expand Down
4 changes: 4 additions & 0 deletions packages/components/src/palette-edit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ function ColorPickerPopover< T extends Color | Gradient >( {
() => ( {
shift: true,
offset: 20,
// Disabling resize as it would otherwise cause the popover to show
// scrollbars while dragging the color picker's handle close to the
// popover edge.
resize: false,
placement: 'left-start',
...receivedPopoverProps,
className: classnames(
Expand Down
Loading

0 comments on commit 9da6f48

Please sign in to comment.