Skip to content

Commit

Permalink
Borders: Add new BorderControl component (#37769)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronrobertshaw authored Mar 24, 2022
1 parent c480389 commit ddbb8c3
Show file tree
Hide file tree
Showing 18 changed files with 1,816 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 } ) => (
<Button
onClick={ onToggle }
variant="tertiary"
aria-label={ toggleAriaLabel }
position={ dropdownPosition }
>
<span className={ indicatorWrapperClassName }>
<ColorIndicator
className={ indicatorClassName }
colorValue={ color }
/>
</span>
</Button>
);

const renderContent = ( { onClose }: PopoverProps ) => (
<>
<VStack className={ popoverControlsClassName } spacing={ 6 }>
{ showDropdownHeader ? (
<HStack>
<StyledLabel>{ __( 'Border color' ) }</StyledLabel>
<Button
isSmall
label={ __( 'Close border color' ) }
icon={ closeSmall }
onClick={ onClose }
/>
</HStack>
) : undefined }
<ColorPalette
className={ popoverContentClassName }
value={ color }
onChange={ onColorChange }
{ ...{ colors, disableCustomColors } }
__experimentalHasMultipleOrigins={
__experimentalHasMultipleOrigins
}
__experimentalIsRenderedInSidebar={
__experimentalIsRenderedInSidebar
}
clearable={ false }
enableAlpha={ enableAlpha }
/>
{ enableStyle && (
<BorderControlStylePicker
label={ __( 'Style' ) }
value={ style }
onChange={ onStyleChange }
/>
) }
</VStack>
<Button
className={ resetButtonClassName }
variant="tertiary"
onClick={ () => {
onReset();
onClose();
} }
>
{ __( 'Reset to default' ) }
</Button>
</>
);

return (
<Dropdown
renderToggle={ renderToggle }
renderContent={ renderContent }
contentClassName={ popoverClassName }
{ ...otherProps }
ref={ forwardedRef }
/>
);
};

const ConnectedBorderControlDropdown = contextConnect(
BorderControlDropdown,
'BorderControlDropdown'
);

export default ConnectedBorderControlDropdown;
Original file line number Diff line number Diff line change
@@ -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,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './component';
Loading

0 comments on commit ddbb8c3

Please sign in to comment.