diff --git a/packages/block-editor/src/components/colors-gradients/style.scss b/packages/block-editor/src/components/colors-gradients/style.scss index cd5992a613cfc..7bc533fd8ec60 100644 --- a/packages/block-editor/src/components/colors-gradients/style.scss +++ b/packages/block-editor/src/components/colors-gradients/style.scss @@ -1,11 +1,11 @@ .block-editor-color-gradient-control { - &__color-indicator { - margin-bottom: $grid-unit-10; + .block-editor-color-gradient-control__color-indicator { + margin-bottom: $grid-unit-15; } - &__button-tabs { + .block-editor-color-gradient-control__button-tabs { display: block; - margin-bottom: $grid-unit-10; + margin-bottom: $grid-unit-15; } } diff --git a/packages/block-editor/src/components/gradient-picker/index.js b/packages/block-editor/src/components/gradient-picker/index.js index f698432d99025..cf1ebac3ed0d9 100644 --- a/packages/block-editor/src/components/gradient-picker/index.js +++ b/packages/block-editor/src/components/gradient-picker/index.js @@ -6,7 +6,7 @@ import { pick } from 'lodash'; /** * WordPress dependencies */ -import { __experimentalGradientPicker } from '@wordpress/components'; +import { __experimentalGradientPicker as GradientPicker } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; function GradientPickerWithGradients( props ) { @@ -19,7 +19,7 @@ function GradientPickerWithGradients( props ) { [] ); return ( - <__experimentalGradientPicker + ; } diff --git a/packages/block-editor/src/components/responsive-block-control/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/responsive-block-control/test/__snapshots__/index.js.snap index dd8516642921c..f7952517ad626 100644 --- a/packages/block-editor/src/components/responsive-block-control/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/responsive-block-control/test/__snapshots__/index.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Basic rendering should render with required props 1`] = `"
Padding

Toggle between using the same value for all screen sizes or using a unique value per screen size.

All is used here for testing purposes to ensure we have access to details about the device.

"`; +exports[`Basic rendering should render with required props 1`] = `"
Padding

Toggle between using the same value for all screen sizes or using a unique value per screen size.

All is used here for testing purposes to ensure we have access to details about the device.

"`; diff --git a/packages/components/src/angle-picker-control/angle-circle.js b/packages/components/src/angle-picker-control/angle-circle.js new file mode 100644 index 0000000000000..6122cda215daf --- /dev/null +++ b/packages/components/src/angle-picker-control/angle-circle.js @@ -0,0 +1,93 @@ +/** + * WordPress dependencies + */ +import { useEffect, useRef } from '@wordpress/element'; +import { __experimentalUseDragging as useDragging } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { + CircleRoot, + CircleIndicatorWrapper, + CircleIndicator, +} from './styles/angle-picker-control-styles'; + +function AngleCircle( { value, onChange, ...props } ) { + const angleCircleRef = useRef(); + const angleCircleCenter = useRef(); + const previousCursorValue = 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; + // Prevent (drag) mouse events from selecting and accidentally + // triggering actions from other elements. + event.preventDefault(); + + onChange( getAngle( centerX, centerY, event.clientX, event.clientY ) ); + }; + + const { startDrag, isDragging } = useDragging( { + onDragStart: ( event ) => { + setAngleCircleCenter(); + changeAngleToPosition( event ); + }, + onDragMove: changeAngleToPosition, + onDragEnd: changeAngleToPosition, + } ); + + useEffect( () => { + if ( isDragging ) { + if ( previousCursorValue.current === undefined ) { + previousCursorValue.current = document.body.style.cursor; + } + document.body.style.cursor = 'grabbing'; + } else { + document.body.style.cursor = previousCursorValue.current || null; + previousCursorValue.current = undefined; + } + }, [ isDragging ] ); + + return ( + /* eslint-disable jsx-a11y/no-static-element-interactions */ + + + + + + /* eslint-enable jsx-a11y/no-static-element-interactions */ + ); +} + +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; +} + +export default AngleCircle; diff --git a/packages/components/src/angle-picker-control/index.js b/packages/components/src/angle-picker-control/index.js index 9350c61f74f15..9d6e52c5b7e84 100644 --- a/packages/components/src/angle-picker-control/index.js +++ b/packages/components/src/angle-picker-control/index.js @@ -1,116 +1,74 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ -import { useRef } from '@wordpress/element'; -import { - useInstanceId, - __experimentalUseDragging as useDragging, -} from '@wordpress/compose'; -import { __ } from '@wordpress/i18n'; +import { useInstanceId } from '@wordpress/compose'; /** * 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, ...props } ) => { - 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; - // Prevent (drag) mouse events from selecting and accidentally - // triggering actions from other elements. - event.preventDefault(); - - 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 */ - ); -}; +import { FlexBlock } from '../flex'; +import NumberControl from '../number-control'; +import AngleCircle from './angle-circle'; +import { + Root, + NumberControlWrapper, +} from './styles/angle-picker-control-styles'; export default function AnglePickerControl( { + className, + id: idProp, value, onChange, - label = __( 'Angle' ), + label, + ...props } ) { - const instanceId = useInstanceId( AnglePickerControl ); - const inputId = `components-angle-picker-control__input-${ instanceId }`; + const instanceId = useInstanceId( + AnglePickerControl, + 'components-angle-picker-control__input' + ); + const id = idProp || instanceId; + + const handleOnNumberChange = ( unprocessedValue ) => { + const inputValue = + unprocessedValue !== '' ? parseInt( unprocessedValue, 10 ) : 0; + onChange( inputValue ); + }; + + const classes = classnames( 'components-angle-picker-control', className ); + return ( - ); } diff --git a/packages/components/src/angle-picker-control/style.scss b/packages/components/src/angle-picker-control/style.scss deleted file mode 100644 index a581517902da0..0000000000000 --- a/packages/components/src/angle-picker-control/style.scss +++ /dev/null @@ -1,43 +0,0 @@ -.components-angle-picker-control { - width: 50%; - &.components-base-control .components-base-control__label { - display: block; - } -} - -.components-angle-picker-control__input-field[type="number"] { - @include input-control; - width: calc(100% - #{$button-size}); - max-width: 100px; -} - -.components-angle-picker-control__angle-circle { - width: $button-size - ( 2 * $grid-unit-05 ); - height: $button-size - ( 2 * $grid-unit-05 ); - border: 2px solid $dark-gray-500; - border-radius: 50%; - float: left; - margin-right: $grid-unit-05; - cursor: grab; -} - -.components-angle-picker-control__angle-circle-indicator-wrapper { - position: relative; - width: 100%; - height: 100%; -} - -.components-angle-picker-control__angle-circle-indicator { - width: 1px; - height: 1px; - border-radius: 50%; - border: 3px solid $dark-gray-500; - display: block; - position: absolute; - top: -($button-size - (2 * $grid-unit-05)) / 2; - bottom: 0; - left: 0; - right: 0; - margin: auto; - background: $dark-gray-500; -} diff --git a/packages/components/src/angle-picker-control/styles/angle-picker-control-styles.js b/packages/components/src/angle-picker-control/styles/angle-picker-control-styles.js new file mode 100644 index 0000000000000..f63f9bc6d1b00 --- /dev/null +++ b/packages/components/src/angle-picker-control/styles/angle-picker-control-styles.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import styled from '@emotion/styled'; + +/** + * Internal dependencies + */ +import { Flex, FlexItem } from '../../flex'; +import { color } from '../../utils/style-mixins'; + +const CIRCLE_SIZE = 30; + +export const Root = styled( Flex )` + max-width: 200px; +`; + +export const NumberControlWrapper = styled( FlexItem )` + width: 80px; +`; + +export const CircleRoot = styled.div` + border-radius: 50%; + border: 1px solid ${ color( 'ui.borderLight' ) }; + box-sizing: border-box; + cursor: grab; + height: ${ CIRCLE_SIZE }px; + overflow: hidden; + width: ${ CIRCLE_SIZE }px; +`; + +export const CircleIndicatorWrapper = styled.div` + box-sizing: border-box; + position: relative; + width: 100%; + height: 100%; +`; + +export const CircleIndicator = styled.div` + background: ${ color( 'ui.border' ) }; + border-radius: 50%; + border: 3px solid ${ color( 'ui.border' ) }; + bottom: 0; + box-sizing: border-box; + display: block; + height: 1px; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: -${ CIRCLE_SIZE / 2 }px; + width: 1px; +`; diff --git a/packages/components/src/circular-option-picker/style.scss b/packages/components/src/circular-option-picker/style.scss index f82cde47eddb3..23003048b137e 100644 --- a/packages/components/src/circular-option-picker/style.scss +++ b/packages/components/src/circular-option-picker/style.scss @@ -3,7 +3,6 @@ $color-palette-circle-spacing: 12px; .components-circular-option-picker { display: inline-block; - margin-top: 0.6rem; width: 100%; .components-circular-option-picker__custom-clear-wrapper { @@ -16,7 +15,7 @@ $color-palette-circle-spacing: 12px; display: inline-block; height: $color-palette-circle-size; width: $color-palette-circle-size; - margin-right: $color-palette-circle-spacing; + margin-right: $color-palette-circle-spacing + $grid-unit-05; margin-bottom: $color-palette-circle-spacing; vertical-align: top; transform: scale(1); @@ -32,6 +31,11 @@ $color-palette-circle-spacing: 12px; height: 100%; width: 100%; } + + // Remove right margin on every 6th item so it aligns with gradient control. + &:nth-child(6n+6) { + margin-right: 0; + } } .components-circular-option-picker__option-wrapper::before { diff --git a/packages/components/src/custom-gradient-picker/constants.js b/packages/components/src/custom-gradient-picker/constants.js index bb5f826f5ccd4..2fe6b4a6f06dd 100644 --- a/packages/components/src/custom-gradient-picker/constants.js +++ b/packages/components/src/custom-gradient-picker/constants.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + export const INSERT_POINT_WIDTH = 23; export const GRADIENT_MARKERS_WIDTH = 18; export const MINIMUM_DISTANCE_BETWEEN_INSERTER_AND_MARKER = @@ -16,3 +21,8 @@ export const HORIZONTAL_GRADIENT_ORIENTATION = { type: 'angular', value: 90, }; + +export const GRADIENT_OPTIONS = [ + { value: 'linear-gradient', label: __( 'Linear' ) }, + { value: 'radial-gradient', label: __( 'Radial' ) }, +]; diff --git a/packages/components/src/custom-gradient-picker/custom-gradient-bar.js b/packages/components/src/custom-gradient-picker/custom-gradient-bar.js index 6bf81d83f0082..18c6927aaa40c 100644 --- a/packages/components/src/custom-gradient-picker/custom-gradient-bar.js +++ b/packages/components/src/custom-gradient-picker/custom-gradient-bar.js @@ -9,7 +9,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useRef, useReducer, useState } from '@wordpress/element'; -import { plusCircle } from '@wordpress/icons'; +import { plus } from '@wordpress/icons'; /** * Internal dependencies @@ -60,7 +60,7 @@ function InsertPoint( { onToggle(); } } className="components-custom-gradient-picker__insert-point" - icon={ plusCircle } + icon={ plus } style={ { left: insertPosition !== null diff --git a/packages/components/src/custom-gradient-picker/icons.js b/packages/components/src/custom-gradient-picker/icons.js deleted file mode 100644 index 137bf2cb4d89d..0000000000000 --- a/packages/components/src/custom-gradient-picker/icons.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * WordPress dependencies - */ -import { withInstanceId } from '@wordpress/compose'; -import { - Circle, - LinearGradient, - Path, - RadialGradient, - Stop, - SVG, -} from '@wordpress/primitives'; - -/** - * Internal dependencies - */ -export const LinearGradientIcon = withInstanceId( ( { instanceId } ) => { - const linerGradientId = `linear-gradient-${ instanceId }`; - return ( - - - - - - - - ); -} ); - -export const RadialGradientIcon = withInstanceId( ( { instanceId } ) => { - const radialGradientId = `radial-gradient-${ instanceId }`; - return ( - - - - - - - - ); -} ); diff --git a/packages/components/src/custom-gradient-picker/index.js b/packages/components/src/custom-gradient-picker/index.js index 1c9a26ab3f8e2..57220e5b40cf2 100644 --- a/packages/components/src/custom-gradient-picker/index.js +++ b/packages/components/src/custom-gradient-picker/index.js @@ -12,16 +12,17 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import AnglePickerControl from '../angle-picker-control'; -import { LinearGradientIcon, RadialGradientIcon } from './icons'; import CustomGradientBar from './custom-gradient-bar'; -import BaseControl from '../base-control'; +import { Flex, FlexItem } from '../flex'; +import SelectControl from '../select-control'; import { getGradientParsed } from './utils'; import { serializeGradient } from './serializer'; -import ToolbarGroup from '../toolbar-group'; import { DEFAULT_LINEAR_GRADIENT_ANGLE, HORIZONTAL_GRADIENT_ORIENTATION, + GRADIENT_OPTIONS, } from './constants'; +import { SelectWrapper } from './styles/custom-gradient-picker-styles'; const GradientAnglePicker = ( { gradientAST, hasGradient, onChange } ) => { const angle = get( @@ -71,27 +72,23 @@ const GradientTypePicker = ( { gradientAST, hasGradient, onChange } ) => { ); }; + const handleOnChange = ( next ) => { + if ( next === 'linear-gradient' ) { + onSetLinearGradient(); + } + if ( next === 'radial-gradient' ) { + onSetRadialGradient(); + } + }; + return ( - - { __( 'Type' ) } - , - title: __( 'Linear Gradient' ), - isActive: hasGradient && type === 'linear-gradient', - onClick: onSetLinearGradient, - }, - { - icon: , - title: __( 'Radial Gradient' ), - isActive: hasGradient && type === 'radial-gradient', - onClick: onSetRadialGradient, - }, - ] } - /> - + ); }; @@ -101,20 +98,27 @@ export default function CustomGradientPicker( { value, onChange } ) { return (
-
- - { type === 'linear-gradient' && ( - + + + + { type === 'linear-gradient' && ( + + + ) } -
+
); } diff --git a/packages/components/src/custom-gradient-picker/style.scss b/packages/components/src/custom-gradient-picker/style.scss index 61f15bfbb1414..972a3045b34ff 100644 --- a/packages/components/src/custom-gradient-picker/style.scss +++ b/packages/components/src/custom-gradient-picker/style.scss @@ -1,20 +1,17 @@ -$components-custom-gradient-picker__padding: 3px; // 24px container, 18px handles inside, that leaves 6px padding, half of which is 3. - -.components-custom-gradient-picker { - margin-top: $grid-unit-10; -} +$components-custom-gradient-picker__padding: 6px; // 36px container, 24px handles inside, that leaves 12px padding, half of which is 6. .components-custom-gradient-picker__gradient-bar:not(.has-gradient) { opacity: 0.4; } .components-custom-gradient-picker__gradient-bar { + margin-top: $grid-unit-15; width: 100%; - height: $button-size-small; - border-radius: $button-size-small; - margin-bottom: $grid-unit-10; + height: $button-size; + border-radius: $button-size; + margin-bottom: $grid-unit-15; padding-left: $components-custom-gradient-picker__padding; - padding-right: $button-size-small - $components-custom-gradient-picker__padding; + padding-right: $button-size - $components-custom-gradient-picker__padding; .components-custom-gradient-picker__markers-container { position: relative; @@ -24,10 +21,12 @@ $components-custom-gradient-picker__padding: 3px; // 24px container, 18px handle border-radius: 50%; background: $white; padding: 2px; + top: $components-custom-gradient-picker__padding; min-width: $button-size-small; width: $button-size-small; height: $button-size-small; position: relative; + color: $gray-900; svg { height: 100%; @@ -36,21 +35,20 @@ $components-custom-gradient-picker__padding: 3px; // 24px container, 18px handle } .components-custom-gradient-picker__control-point-button { - border: 2px solid $white; + border: 2px solid transparent; + box-shadow: inset 0 0 0 $border-width-focus $white; border-radius: 50%; - height: 18px; + height: $button-size-small; + width: $button-size-small; padding: 0; position: absolute; - width: 18px; top: $components-custom-gradient-picker__padding; + &:focus, &.is-active { - background: #fafafa; - color: #23282d; - border-color: #999; box-shadow: 0 0 0 1px $white, - 0 0 0 3px var(--wp-admin-theme-color); + 0 0 0 3px $gray-900; } } } @@ -73,12 +71,9 @@ $components-custom-gradient-picker__padding: 3px; // 24px container, 18px handle height: 20px; } -.components-custom-gradient-picker__ui-line { - display: flex; - justify-content: space-between; -} - .components-custom-gradient-picker .components-custom-gradient-picker__ui-line { + margin-bottom: $grid-unit-20; + .components-base-control.components-angle-picker, .components-base-control.components-custom-gradient-picker__type-picker { margin-bottom: 0; diff --git a/packages/components/src/custom-gradient-picker/styles/custom-gradient-picker-styles.js b/packages/components/src/custom-gradient-picker/styles/custom-gradient-picker-styles.js new file mode 100644 index 0000000000000..f3d39295b163f --- /dev/null +++ b/packages/components/src/custom-gradient-picker/styles/custom-gradient-picker-styles.js @@ -0,0 +1,12 @@ +/** + * External dependencies + */ +import styled from '@emotion/styled'; +/** + * Internal dependencies + */ +import { FlexItem } from '../../flex'; + +export const SelectWrapper = styled( FlexItem )` + width: 110px; +`; diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap index 9e100abfd43dc..f25202bad70ea 100644 --- a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap +++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DimensionControl rendering renders with custom sizes 1`] = ` - { const base = 4; const value = typeof gap === 'number' ? base * gap : base; const dir = isReversed ? 'left' : 'right'; - const padding = `padding-${ dir }`; + const margin = `margin-${ dir }`; return css` > * { - ${ padding }: ${ value }px; + ${ margin }: ${ value }px; &:last-child { - ${ padding }: 0; + ${ margin }: 0; } } `; diff --git a/packages/components/src/input-control/index.js b/packages/components/src/input-control/index.js index ad3b7b79eb47d..3a054c28b84de 100644 --- a/packages/components/src/input-control/index.js +++ b/packages/components/src/input-control/index.js @@ -13,10 +13,8 @@ import { useState, forwardRef } from '@wordpress/element'; /** * Internal dependencies */ -import Backdrop from './backdrop'; +import InputBase from './input-base'; import InputField from './input-field'; -import Label from './label'; -import { Container, Root, Prefix, Suffix } from './styles/input-control-styles'; import { isValueEmpty } from '../utils/values'; function useUniqueId( idProp ) { @@ -29,7 +27,6 @@ function useUniqueId( idProp ) { export function InputControl( { __unstableStateReducer: stateReducer = ( state ) => state, - children, className, disabled = false, hideLabelFromVision = false, @@ -74,74 +71,46 @@ export function InputControl( ! hideLabelFromVision && isFloatingLabel && label; return ( - - - - { prefix && ( - - { prefix } - - ) } - - { suffix && ( - - { suffix } - - ) } - - + stateReducer={ stateReducer } + value={ value } + /> + ); } -export default forwardRef( InputControl ); +const ForwardedComponent = forwardRef( InputControl ); + +export default ForwardedComponent; diff --git a/packages/components/src/input-control/input-base.js b/packages/components/src/input-control/input-base.js new file mode 100644 index 0000000000000..b946175b2b043 --- /dev/null +++ b/packages/components/src/input-control/input-base.js @@ -0,0 +1,102 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; +import { forwardRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Backdrop from './backdrop'; +import Label from './label'; +import { + Container, + Root, + Prefix, + Suffix, + LabelWrapper, +} from './styles/input-control-styles'; + +function useUniqueId( idProp ) { + const instanceId = useInstanceId( InputBase ); + const id = `input-base-control-${ instanceId }`; + + return idProp || id; +} + +export function InputBase( + { + children, + className, + disabled = false, + hideLabelFromVision = false, + id: idProp, + isFloatingLabel = false, + isFilled = false, + isFocused = false, + label, + prefix, + size = 'default', + suffix, + ...props + }, + ref +) { + const id = useUniqueId( idProp ); + + const isFloating = isFloatingLabel ? isFilled || isFocused : false; + const isFloatingLabelSet = + ! hideLabelFromVision && isFloatingLabel && label; + + return ( + + + + + + { prefix && ( + + { prefix } + + ) } + { children } + { suffix && ( + + { suffix } + + ) } + + + ); +} + +export default forwardRef( InputBase ); diff --git a/packages/components/src/input-control/styles/input-control-styles.js b/packages/components/src/input-control/styles/input-control-styles.js index dd58d7f8275f2..4ac83a36645ef 100644 --- a/packages/components/src/input-control/styles/input-control-styles.js +++ b/packages/components/src/input-control/styles/input-control-styles.js @@ -7,7 +7,7 @@ import styled from '@emotion/styled'; /** * Internal dependencies */ -import Flex from '../../flex'; +import Flex, { FlexItem } from '../../flex'; import Text from '../../text'; import { color, rtl, reduceMotion } from '../../utils/style-mixins'; @@ -187,6 +187,7 @@ export const Input = styled.input` box-shadow: none !important; color: ${ color( 'black' ) }; display: block; + margin: 0; outline: none; padding-left: 8px; padding-right: 8px; @@ -268,7 +269,6 @@ const labelTruncation = ( { isFloating } ) => { if ( isFloating ) return ''; return css` - max-width: calc( 100% - 10px ); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -301,6 +301,10 @@ const BaseLabel = styled( Text )` export const Label = ( props ) => ; +export const LabelWrapper = styled( FlexItem )` + max-width: calc( 100% - 10px ); +`; + const fieldsetTopStyles = ( { isFloatingLabel } ) => { const top = isFloatingLabel ? -5 : 0; return css( { top } ); diff --git a/packages/components/src/select-control/index.js b/packages/components/src/select-control/index.js index 8ad65f890e7ac..369cc079f7f84 100644 --- a/packages/components/src/select-control/index.js +++ b/packages/components/src/select-control/index.js @@ -1,31 +1,68 @@ /** * External dependencies */ -import { isEmpty } from 'lodash'; +import { isEmpty, noop } from 'lodash'; +import classNames from 'classnames'; /** * WordPress dependencies */ import { useInstanceId } from '@wordpress/compose'; +import { useState, forwardRef } from '@wordpress/element'; +import { Icon, chevronDown } from '@wordpress/icons'; /** * Internal dependencies */ import BaseControl from '../base-control'; +import InputBase from '../input-control/input-base'; +import { Select, DownArrowWrapper } from './styles/select-control-styles'; -export default function SelectControl( { - help, - label, - multiple = false, - onChange, - options = [], - className, - hideLabelFromVision, - ...props -} ) { +function useUniqueId( idProp ) { const instanceId = useInstanceId( SelectControl ); const id = `inspector-select-control-${ instanceId }`; - const onChangeValue = ( event ) => { + + return idProp || id; +} + +function SelectControl( + { + className, + disabled = false, + help, + hideLabelFromVision, + id: idProp, + isFloatingLabel = false, + label, + multiple = false, + onBlur = noop, + onChange = noop, + onFocus = noop, + options = [], + size = 'default', + value: valueProp, + ...props + }, + ref +) { + const [ isFocused, setIsFocused ] = useState( false ); + const id = useUniqueId( idProp ); + const helpId = help ? `${ id }__help` : undefined; + + // Disable reason: A select with an onchange throws a warning + if ( isEmpty( options ) ) return null; + + const handleOnBlur = ( event ) => { + onBlur( event ); + setIsFocused( false ); + }; + + const handleOnFocus = ( event ) => { + onFocus( event ); + setIsFocused( true ); + }; + + const handleOnChange = ( event ) => { if ( multiple ) { const selectedOptions = [ ...event.target.options ].filter( ( { selected } ) => selected @@ -34,41 +71,74 @@ export default function SelectControl( { onChange( newValues ); return; } - onChange( event.target.value ); + + onChange( event.target.value, { event } ); }; - // Disable reason: A select with an onchange throws a warning + const isFilled = true; + const isFloating = isFloatingLabel ? isFilled || isFocused : false; + const isFloatingLabelSet = + ! hideLabelFromVision && isFloatingLabel && label; + + const classes = classNames( 'components-select-control', className ); /* eslint-disable jsx-a11y/no-onchange */ return ( - ! isEmpty( options ) && ( - + + + + } > - - - ) + { options.map( ( option, index ) => { + const key = + option.id || + `${ option.label }-${ option.value }-${ index }`; + + return ( + + ); + } ) } + + + ); /* eslint-enable jsx-a11y/no-onchange */ } + +const ForwardedComponent = forwardRef( SelectControl ); + +export default ForwardedComponent; diff --git a/packages/components/src/select-control/stories/index.js b/packages/components/src/select-control/stories/index.js index dec6d48405e5a..f72e5af016585 100644 --- a/packages/components/src/select-control/stories/index.js +++ b/packages/components/src/select-control/stories/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { boolean, object, text } from '@storybook/addon-knobs'; +import { boolean, object, select, text } from '@storybook/addon-knobs'; /** * WordPress dependencies @@ -31,29 +31,28 @@ const SelectControlWithState = ( props ) => { }; export const _default = () => { - const label = text( 'Label', 'Label Text' ); - const hideLabelFromVision = boolean( 'Hide Label From Vision', false ); - const help = text( - 'Help Text', - 'Help text to explain the select control.' - ); - const multiple = boolean( 'Allow Multiple Selection', false ); - const options = object( 'Options', [ - { value: null, label: 'Select an Option', disabled: true }, - { value: 'a', label: 'Option A' }, - { value: 'b', label: 'Option B' }, - { value: 'c', label: 'Option C' }, - ] ); - const className = text( 'Class Name', '' ); + const props = { + disabled: boolean( 'disabled', false ), + help: text( 'help', 'Help text to explain the select control.' ), + hideLabelFromVision: boolean( 'hideLabelFromVision', false ), + isFloatingLabel: boolean( 'isFloatingLabel', false ), + label: text( 'label', 'Value' ), + multiple: boolean( 'multiple', false ), + options: object( 'Options', [ + { value: null, label: 'Select an Option', disabled: true }, + { value: 'a', label: 'Option A' }, + { value: 'b', label: 'Option B' }, + { value: 'c', label: 'Option C' }, + ] ), + size: select( + 'size', + { + default: 'default', + small: 'small', + }, + 'default' + ), + }; - return ( - - ); + return ; }; diff --git a/packages/components/src/select-control/styles/select-control-styles.js b/packages/components/src/select-control/styles/select-control-styles.js new file mode 100644 index 0000000000000..4d4ab42178c1f --- /dev/null +++ b/packages/components/src/select-control/styles/select-control-styles.js @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import { css } from '@emotion/core'; +import styled from '@emotion/styled'; + +/** + * Internal dependencies + */ +import { color, rtl } from '../../utils/style-mixins'; + +const disabledStyles = ( { disabled } ) => { + if ( ! disabled ) return ''; + + return css( { + color: color( 'ui.textDisabled' ), + } ); +}; + +const fontSizeStyles = ( { size } ) => { + const sizes = { + default: '13px', + small: '11px', + }; + + const fontSize = sizes[ size ]; + const fontSizeMobile = '16px'; + + if ( ! fontSize ) return ''; + + return css` + font-size: ${ fontSizeMobile }; + + @media ( min-width: 600px ) { + font-size: ${ fontSize }; + } + `; +}; + +const sizeStyles = ( { size } ) => { + const sizes = { + default: { + height: 30, + lineHeight: 1, + minHeight: 30, + }, + small: { + height: 24, + lineHeight: 1, + minHeight: 24, + }, + }; + + const style = sizes[ size ] || sizes.default; + + return css( style ); +}; + +// TODO: Resolve need to use &&& to increase specificity +// https://github.com/WordPress/gutenberg/issues/18483 + +export const Select = styled.select` + &&& { + appearance: none; + background: transparent; + box-sizing: border-box; + border: none; + box-shadow: none !important; + color: ${ color( 'black' ) }; + display: block; + margin: 0; + outline: none; + width: 100%; + + ${ disabledStyles }; + ${ fontSizeStyles }; + ${ sizeStyles }; + + ${ rtl( { paddingLeft: 8, paddingRight: 24 } )() } + } +`; + +export const DownArrowWrapper = styled.div` + align-items: center; + bottom: 0; + box-sizing: border-box; + display: flex; + padding: 0 4px; + pointer-events: none; + position: absolute; + top: 0; + + ${ rtl( { right: 0 } )() } + + svg { + display: block; + } +`; diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index 36c406c3c7a41..034198da732e3 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -1,5 +1,4 @@ @import "./animate/style.scss"; -@import "./angle-picker-control/style.scss"; @import "./autocomplete/style.scss"; @import "./base-control/style.scss"; @import "./button-group/style.scss";