diff --git a/packages/components/src/angle-picker/index.js b/packages/components/src/angle-picker/index.js new file mode 100644 index 00000000000000..c6faf2ad200d78 --- /dev/null +++ b/packages/components/src/angle-picker/index.js @@ -0,0 +1,98 @@ +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; +import { useInstanceId, __experimentalUseDragging as useDragging } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import BaseControl from '../base-control'; + +function getAngle( centerX, centerY, pointX, pointY ) { + const y = pointY - centerY; + const x = pointX - centerX; + + const angleInRadians = Math.atan2( y, x ); + const angleInDeg = Math.round( angleInRadians * ( 180 / Math.PI ) ) + 90; + if ( angleInDeg < 0 ) { + return 360 + angleInDeg; + } + return angleInDeg; +} + +const AngleCircle = ( { value, onChange } ) => { + const angleCircleRef = useRef(); + const angleCircleCenter = useRef(); + + const setAngleCircleCenter = () => { + const rect = angleCircleRef.current.getBoundingClientRect(); + angleCircleCenter.current = { + x: rect.x + ( rect.width / 2 ), + y: rect.y + ( rect.height / 2 ), + }; + }; + + const changeAngleToPosition = ( event ) => { + const { x: centerX, y: centerY } = angleCircleCenter.current; + onChange( getAngle( centerX, centerY, event.clientX, event.clientY ) ); + }; + + const { startDrag, isDragging } = useDragging( { + onDragStart: ( event ) => { + setAngleCircleCenter(); + changeAngleToPosition( event ); + }, + onDragMove: changeAngleToPosition, + onDragEnd: changeAngleToPosition, + } ); + return ( + /* eslint-disable jsx-a11y/no-static-element-interactions */ +
+
+ +
+
+ /* eslint-enable jsx-a11y/no-static-element-interactions */ + ); +}; + +export default function AnglePicker( { value, onChange, label = __( 'Angle' ) } ) { + const instanceId = useInstanceId( AnglePicker ); + const inputId = `components-custom-gradient-picker__angle-picker-${ instanceId }`; + return ( + + + { + const unprocessedValue = event.target.value; + const inputValue = unprocessedValue !== '' ? + parseInt( event.target.value, 10 ) : + 0; + onChange( inputValue ); + } } + value={ value } + min={ 0 } + max={ 360 } + step="1" + /> + + ); +} + diff --git a/packages/components/src/angle-picker/stories/index.js b/packages/components/src/angle-picker/stories/index.js new file mode 100644 index 00000000000000..445ef54907b162 --- /dev/null +++ b/packages/components/src/angle-picker/stories/index.js @@ -0,0 +1,23 @@ + +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import AnglePicker from '../'; + +export default { title: 'Components|AnglePicker', component: AnglePicker }; + +const AnglePickerWithState = () => { + const [ angle, setAngle ] = useState(); + return ( + + ); +}; + +export const _default = () => { + return ( ); +}; diff --git a/packages/components/src/angle-picker/style.scss b/packages/components/src/angle-picker/style.scss new file mode 100644 index 00000000000000..c1fdd0c53bc748 --- /dev/null +++ b/packages/components/src/angle-picker/style.scss @@ -0,0 +1,42 @@ +.components-angle-picker { + width: 50%; + &.components-base-control .components-base-control__label { + display: block; + } +} + +.components-angle-picker__input-field { + width: calc(100% - #{$icon-button-size}); + max-width: 100px; +} + +.components-angle-picker__angle-circle { + width: $icon-button-size - ( 2 * $grid-size-small ); + height: $icon-button-size - ( 2 * $grid-size-small ); + border: 2px solid $dark-gray-500; + border-radius: 50%; + float: left; + margin-right: $grid-size-small; + cursor: grab; +} + +.components-angle-picker__angle-circle-indicator-wrapper { + position: relative; + width: 100%; + height: 100%; +} + +.components-angle-picker__angle-circle-indicator { + width: 1px; + height: 1px; + border-radius: 50%; + border: 3px solid $dark-gray-500; + display: block; + position: absolute; + top: -($icon-button-size - (2 * $grid-size-small)) / 2; + bottom: 0; + left: 0; + right: 0; + margin: auto; + background: $dark-gray-500; +} diff --git a/packages/components/src/index.js b/packages/components/src/index.js index d59d33984f16c6..13df1cffea680e 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -1,6 +1,7 @@ // Components export * from './primitives'; export { default as Animate } from './animate'; +export { default as __experimentalAnglePicker } from './angle-picker'; export { default as Autocomplete } from './autocomplete'; export { default as BaseControl } from './base-control'; export { default as Button } from './button'; diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index f99717ec65d84b..a4728386ed030f 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -1,4 +1,5 @@ @import "./animate/style.scss"; +@import "./angle-picker/style.scss"; @import "./autocomplete/style.scss"; @import "./base-control/style.scss"; @import "./button-group/style.scss"; diff --git a/packages/compose/src/hooks/use-dragging/index.js b/packages/compose/src/hooks/use-dragging/index.js new file mode 100644 index 00000000000000..94794df07dbf59 --- /dev/null +++ b/packages/compose/src/hooks/use-dragging/index.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { useEffect, useCallback, useState, useRef } from '@wordpress/element'; + +export default function useDragging( { onDragStart, onDragMove, onDragEnd } ) { + const [ isDragging, setIsDragging ] = useState( false ); + + const eventsRef = useRef( { + onDragStart, + onDragMove, + onDragEnd, + } ); + useEffect( + () => { + eventsRef.current.onDragStart = onDragStart; + eventsRef.current.onDragMove = onDragMove; + eventsRef.current.onDragEnd = onDragEnd; + }, + [ onDragStart, onDragMove, onDragEnd ] + ); + + const startDrag = useCallback( ( ...args ) => { + if ( eventsRef.current.onDragEnd ) { + eventsRef.current.onDragStart( ...args ); + } + setIsDragging( true ); + } ); + const onMouseMove = useCallback( ( ...args ) => ( eventsRef.current.onDragMove && eventsRef.current.onDragMove( ...args ) ) ); + const endDrag = useCallback( ( ...args ) => { + if ( eventsRef.current.onDragEnd ) { + eventsRef.current.onDragEnd( ...args ); + } + setIsDragging( false ); + } ); + + useEffect( () => { + if ( isDragging ) { + document.addEventListener( 'mousemove', onMouseMove ); + document.addEventListener( 'mouseup', endDrag ); + } + + return () => { + document.removeEventListener( 'mousemove', onMouseMove ); + document.removeEventListener( 'mouseup', endDrag ); + }; + }, [ isDragging ] ); + return { + startDrag, + endDrag, + isDragging, + }; +} diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js index 5fce542d28e0e8..3a1aabde92cfef 100644 --- a/packages/compose/src/index.js +++ b/packages/compose/src/index.js @@ -13,6 +13,7 @@ export { default as withSafeTimeout } from './higher-order/with-safe-timeout'; export { default as withState } from './higher-order/with-state'; // Hooks +export { default as __experimentalUseDragging } from './hooks/use-dragging'; export { default as useInstanceId } from './hooks/use-instance-id'; export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut'; export { default as useMediaQuery } from './hooks/use-media-query'; diff --git a/storybook/test/__snapshots__/index.js.snap b/storybook/test/__snapshots__/index.js.snap index eef58a88e9ec15..d744e1c08098be 100644 --- a/storybook/test/__snapshots__/index.js.snap +++ b/storybook/test/__snapshots__/index.js.snap @@ -1,5 +1,43 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Storyshots Components|AnglePicker Default 1`] = ` +
+
+ +
+
+ +
+
+ +
+
+`; + exports[`Storyshots Components|Animate Appear Bottom Left 1`] = `