diff --git a/docs/manifest.json b/docs/manifest.json index cba23e20f0a07a..cb10d3c6e80a36 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -611,6 +611,12 @@ "markdown_source": "../packages/components/src/base-field/README.md", "parent": "components" }, + { + "title": "BorderControl", + "slug": "border-control", + "markdown_source": "../packages/components/src/border-control/border-control/README.md", + "parent": "components" + }, { "title": "BoxControl", "slug": "box-control", diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx new file mode 100644 index 00000000000000..e7bad227f72edd --- /dev/null +++ b/packages/components/src/border-control/border-control-dropdown/component.tsx @@ -0,0 +1,252 @@ +/** + * External dependencies + */ +import type { CSSProperties } from 'react'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { closeSmall } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import BorderControlStylePicker from '../border-control-style-picker'; +import Button from '../../button'; +import ColorIndicator from '../../color-indicator'; +import ColorPalette from '../../color-palette'; +import Dropdown from '../../dropdown'; +import { HStack } from '../../h-stack'; +import { VStack } from '../../v-stack'; +import { contextConnect, WordPressComponentProps } from '../../ui/context'; +import { useBorderControlDropdown } from './hook'; +import { StyledLabel } from '../../base-control/styles/base-control-styles'; + +import type { + Color, + ColorOrigin, + Colors, + DropdownProps, + PopoverProps, +} from '../types'; + +const noop = () => undefined; +const getColorObject = ( + colorValue: CSSProperties[ 'borderColor' ], + colors: Colors | undefined, + hasMultipleColorOrigins: boolean +) => { + if ( ! colorValue || ! colors ) { + return; + } + + if ( hasMultipleColorOrigins ) { + let matchedColor; + + ( colors as ColorOrigin[] ).some( ( origin ) => + origin.colors.some( ( color ) => { + if ( color.color === colorValue ) { + matchedColor = color; + return true; + } + + return false; + } ) + ); + + return matchedColor; + } + + return ( colors as Color[] ).find( + ( color ) => color.color === colorValue + ); +}; + +const getToggleAriaLabel = ( + colorValue: CSSProperties[ 'borderColor' ], + colorObject: Color | undefined, + style: CSSProperties[ 'borderStyle' ], + isStyleEnabled: boolean +) => { + if ( isStyleEnabled ) { + if ( colorObject ) { + return style + ? sprintf( + // translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g.: "#f00:". %3$s: The current border style selection e.g. "solid". + 'Border color and style picker. The currently selected color is called "%1$s" and has a value of "%2$s". The currently selected style is "%3$s".', + colorObject.name, + colorObject.color, + style + ) + : sprintf( + // translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g.: "#f00:". + 'Border color and style picker. The currently selected color is called "%1$s" and has a value of "%2$s".', + colorObject.name, + colorObject.color + ); + } + + if ( colorValue ) { + return style + ? sprintf( + // translators: %1$s: The color's hex code e.g.: "#f00:". %2$s: The current border style selection e.g. "solid". + 'Border color and style picker. The currently selected color has a value of "%1$s". The currently selected style is "%2$s".', + colorValue, + style + ) + : sprintf( + // translators: %1$s: The color's hex code e.g.: "#f00:". + 'Border color and style picker. The currently selected color has a value of "%1$s".', + colorValue + ); + } + + return __( 'Border color and style picker.' ); + } + + if ( colorObject ) { + return sprintf( + // translators: %1$s: The name of the color e.g. "vivid red". %2$s: The color's hex code e.g.: "#f00:". + 'Border color picker. The currently selected color is called "%1$s" and has a value of "%2$s".', + colorObject.name, + colorObject.color + ); + } + + if ( colorValue ) { + return sprintf( + // translators: %1$s: The color's hex code e.g.: "#f00:". + 'Border color picker. The currently selected color has a value of "%1$s".', + colorValue + ); + } + + return __( 'Border color picker.' ); +}; + +const BorderControlDropdown = ( + props: WordPressComponentProps< DropdownProps, 'div' >, + forwardedRef: React.ForwardedRef< any > +) => { + const { + __experimentalHasMultipleOrigins, + __experimentalIsRenderedInSidebar, + border, + colors, + disableCustomColors, + enableAlpha, + indicatorClassName, + indicatorWrapperClassName, + onReset, + onColorChange, + onStyleChange, + popoverClassName, + popoverContentClassName, + popoverControlsClassName, + resetButtonClassName, + showDropdownHeader, + enableStyle = true, + ...otherProps + } = useBorderControlDropdown( props ); + + const { color, style } = border || {}; + const colorObject = getColorObject( + color, + colors, + !! __experimentalHasMultipleOrigins + ); + + const toggleAriaLabel = getToggleAriaLabel( + color, + colorObject, + style, + enableStyle + ); + + const dropdownPosition = __experimentalIsRenderedInSidebar + ? 'bottom left' + : undefined; + + const renderToggle = ( { onToggle = noop } ) => ( + + ); + + const renderContent = ( { onClose }: PopoverProps ) => ( + <> + + { showDropdownHeader ? ( + + { __( 'Border color' ) } + + + ); + + return ( + + ); +}; + +const ConnectedBorderControlDropdown = contextConnect( + BorderControlDropdown, + 'BorderControlDropdown' +); + +export default ConnectedBorderControlDropdown; diff --git a/packages/components/src/border-control/border-control-dropdown/hook.ts b/packages/components/src/border-control/border-control-dropdown/hook.ts new file mode 100644 index 00000000000000..4bdbdc7ded5c41 --- /dev/null +++ b/packages/components/src/border-control/border-control-dropdown/hook.ts @@ -0,0 +1,97 @@ +/** + * WordPress dependencies + */ +import { useMemo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import * as styles from '../styles'; +import { parseQuantityAndUnitFromRawValue } from '../../unit-control/utils'; +import { useContextSystem, WordPressComponentProps } from '../../ui/context'; +import { useCx } from '../../utils/hooks/use-cx'; + +import type { DropdownProps } from '../types'; + +export function useBorderControlDropdown( + props: WordPressComponentProps< DropdownProps, 'div' > +) { + const { + border, + className, + colors, + onChange, + previousStyleSelection, + ...otherProps + } = useContextSystem( props, 'BorderControlDropdown' ); + + const [ widthValue ] = parseQuantityAndUnitFromRawValue( border?.width ); + const hasZeroWidth = widthValue === 0; + + const onColorChange = ( color?: string ) => { + const style = + border?.style === 'none' ? previousStyleSelection : border?.style; + const width = hasZeroWidth && !! color ? '1px' : border?.width; + + onChange( { color, style, width } ); + }; + + const onStyleChange = ( style?: string ) => { + const width = hasZeroWidth && !! style ? '1px' : border?.width; + onChange( { ...border, style, width } ); + }; + + const onReset = () => { + onChange( { + ...border, + color: undefined, + style: undefined, + } ); + }; + + // Generate class names. + const cx = useCx(); + const classes = useMemo( () => { + return cx( styles.borderControlDropdown(), className ); + }, [ className, cx ] ); + + const indicatorClassName = useMemo( () => { + return cx( styles.borderColorIndicator ); + }, [ cx ] ); + + const indicatorWrapperClassName = useMemo( () => { + return cx( styles.colorIndicatorWrapper( border ) ); + }, [ border, cx ] ); + + const popoverClassName = useMemo( () => { + return cx( styles.borderControlPopover ); + }, [ cx ] ); + + const popoverControlsClassName = useMemo( () => { + return cx( styles.borderControlPopoverControls ); + }, [ cx ] ); + + const popoverContentClassName = useMemo( () => { + return cx( styles.borderControlPopoverContent ); + }, [ cx ] ); + + const resetButtonClassName = useMemo( () => { + return cx( styles.resetButton ); + }, [ cx ] ); + + return { + ...otherProps, + border, + className: classes, + colors, + indicatorClassName, + indicatorWrapperClassName, + onColorChange, + onStyleChange, + onReset, + popoverClassName, + popoverContentClassName, + popoverControlsClassName, + resetButtonClassName, + }; +} diff --git a/packages/components/src/border-control/border-control-dropdown/index.ts b/packages/components/src/border-control/border-control-dropdown/index.ts new file mode 100644 index 00000000000000..b404d7fd44a81a --- /dev/null +++ b/packages/components/src/border-control/border-control-dropdown/index.ts @@ -0,0 +1 @@ +export { default } from './component'; diff --git a/packages/components/src/border-control/border-control-style-picker/component.tsx b/packages/components/src/border-control/border-control-style-picker/component.tsx new file mode 100644 index 00000000000000..c61cec058be1af --- /dev/null +++ b/packages/components/src/border-control/border-control-style-picker/component.tsx @@ -0,0 +1,89 @@ +/** + * WordPress dependencies + */ +import { lineDashed, lineDotted, lineSolid } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import Button from '../../button'; +import { StyledLabel } from '../../base-control/styles/base-control-styles'; +import { View } from '../../view'; +import { Flex } from '../../flex'; +import { VisuallyHidden } from '../../visually-hidden'; +import { contextConnect, WordPressComponentProps } from '../../ui/context'; +import { useBorderControlStylePicker } from './hook'; + +import type { LabelProps, StylePickerProps } from '../types'; + +const BORDER_STYLES = [ + { label: __( 'Solid' ), icon: lineSolid, value: 'solid' }, + { label: __( 'Dashed' ), icon: lineDashed, value: 'dashed' }, + { label: __( 'Dotted' ), icon: lineDotted, value: 'dotted' }, +]; + +const Label = ( props: LabelProps ) => { + const { label, hideLabelFromVision } = props; + + if ( ! label ) { + return null; + } + + return hideLabelFromVision ? ( + { label } + ) : ( + { label } + ); +}; + +const BorderControlStylePicker = ( + props: WordPressComponentProps< StylePickerProps, 'div' >, + forwardedRef: React.ForwardedRef< any > +) => { + const { + buttonClassName, + hideLabelFromVision, + label, + onChange, + value, + ...otherProps + } = useBorderControlStylePicker( props ); + + return ( + +