From ad8020434ff85de6f64ae9be703633d90909c0a2 Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Tue, 4 Jan 2022 19:31:57 +1000
Subject: [PATCH 01/31] Add BorderControl component
This provides a new component that allows configuration of border color, style, and width.
---
docs/manifest.json | 6 +
.../border-control-dropdown/component.tsx | 139 ++++++++
.../border-control-dropdown/hook.ts | 92 +++++
.../border-control-dropdown/index.ts | 1 +
.../border-control-style-picker/component.tsx | 86 +++++
.../border-control-style-picker/hook.ts | 34 ++
.../border-control-style-picker/index.ts | 1 +
.../border-control/border-control/README.md | 159 +++++++++
.../border-control/component.tsx | 108 ++++++
.../src/border-control/border-control/hook.ts | 141 ++++++++
.../border-control/border-control/index.ts | 2 +
.../components/src/border-control/index.ts | 2 +
.../src/border-control/stories/index.js | 70 ++++
.../components/src/border-control/styles.ts | 158 ++++++++
.../src/border-control/test/index.js | 336 ++++++++++++++++++
.../components/src/border-control/types.ts | 137 +++++++
packages/components/src/index.js | 1 +
packages/components/tsconfig.json | 1 +
18 files changed, 1474 insertions(+)
create mode 100644 packages/components/src/border-control/border-control-dropdown/component.tsx
create mode 100644 packages/components/src/border-control/border-control-dropdown/hook.ts
create mode 100644 packages/components/src/border-control/border-control-dropdown/index.ts
create mode 100644 packages/components/src/border-control/border-control-style-picker/component.tsx
create mode 100644 packages/components/src/border-control/border-control-style-picker/hook.ts
create mode 100644 packages/components/src/border-control/border-control-style-picker/index.ts
create mode 100644 packages/components/src/border-control/border-control/README.md
create mode 100644 packages/components/src/border-control/border-control/component.tsx
create mode 100644 packages/components/src/border-control/border-control/hook.ts
create mode 100644 packages/components/src/border-control/border-control/index.ts
create mode 100644 packages/components/src/border-control/index.ts
create mode 100644 packages/components/src/border-control/stories/index.js
create mode 100644 packages/components/src/border-control/styles.ts
create mode 100644 packages/components/src/border-control/test/index.js
create mode 100644 packages/components/src/border-control/types.ts
diff --git a/docs/manifest.json b/docs/manifest.json
index cba23e20f0a07..cb10d3c6e80a3 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 0000000000000..b89ef1ef8daca
--- /dev/null
+++ b/packages/components/src/border-control/border-control-dropdown/component.tsx
@@ -0,0 +1,139 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { closeSmall } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import BorderControlStylePicker from '../border-control-style-picker';
+import Button from '../../button';
+// @ts-ignore
+import ColorIndicator from '../../color-indicator';
+// @ts-ignore
+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 { DropdownProps, PopoverProps } from '../types';
+const noop = () => undefined;
+
+const BorderControlDropdown = (
+ props: WordPressComponentProps< DropdownProps, 'div' >,
+ forwardedRef: React.Ref< any >
+) => {
+ const {
+ __experimentalHasMultipleOrigins,
+ __experimentalIsRenderedInSidebar,
+ border,
+ colors,
+ disableCustomColors,
+ enableAlpha,
+ indicatorClassName,
+ onReset,
+ onColorChange,
+ onStyleChange,
+ popoverClassName,
+ popoverContentClassName,
+ popoverControlsClassName,
+ resetButtonClassName,
+ enableStyle = true,
+ ...otherProps
+ } = useBorderControlDropdown( props );
+
+ const { color, style } = border || {};
+ const fallbackColor = !! style && style !== 'none' ? '#ddd' : undefined;
+ const indicatorBorderStyles = {
+ // The border style is set to `none` when border width is zero. Forcing
+ // the solid style in this case maintains the positioning of the inner
+ // ColorIndicator.
+ borderStyle: style === 'none' ? 'solid' : style,
+ // If there is no color selected but we have a style to display, apply
+ // a border color anyway.
+ borderColor: color || fallbackColor,
+ };
+
+ const renderToggle = ( { onToggle = noop } ) => (
+
+ );
+
+ const renderContent = ( { onClose }: PopoverProps ) => (
+ <>
+
+
+ { __( 'Border color' ) }
+
+
+
+ { enableStyle && (
+
+ ) }
+
+
+ >
+ );
+
+ 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 0000000000000..84035e4794618
--- /dev/null
+++ b/packages/components/src/border-control/border-control-dropdown/hook.ts
@@ -0,0 +1,92 @@
+/**
+ * WordPress dependencies
+ */
+import { useMemo } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import * as styles from '../styles';
+import { parseUnit } 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 ] = parseUnit( 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 ] );
+
+ const indicatorClassName = useMemo( () => {
+ return cx( styles.BorderColorIndicator );
+ }, [] );
+
+ const popoverClassName = useMemo( () => {
+ return cx( styles.BorderControlPopover );
+ }, [] );
+
+ const popoverControlsClassName = useMemo( () => {
+ return cx( styles.BorderControlPopoverControls );
+ }, [] );
+
+ const popoverContentClassName = useMemo( () => {
+ return cx( styles.BorderControlPopoverContent );
+ }, [] );
+
+ const resetButtonClassName = useMemo( () => {
+ return cx( styles.ResetButton );
+ }, [] );
+
+ return {
+ ...otherProps,
+ border,
+ className: classes,
+ colors,
+ indicatorClassName,
+ 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 0000000000000..b404d7fd44a81
--- /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 0000000000000..6db2e6b4ed15a
--- /dev/null
+++ b/packages/components/src/border-control/border-control-style-picker/component.tsx
@@ -0,0 +1,86 @@
+/**
+ * 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 { 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.Ref< any >
+) => {
+ const {
+ buttonClassName,
+ hideLabelFromVision,
+ label,
+ onChange,
+ value,
+ ...otherProps
+ } = useBorderControlStylePicker( props );
+
+ return (
+
+
+
+ { BORDER_STYLES.map( ( borderStyle ) => (
+
+
+ );
+};
+
+const ConnectedBorderControlStylePicker = contextConnect(
+ BorderControlStylePicker,
+ 'BorderControlStylePicker'
+);
+
+export default ConnectedBorderControlStylePicker;
diff --git a/packages/components/src/border-control/border-control-style-picker/hook.ts b/packages/components/src/border-control/border-control-style-picker/hook.ts
new file mode 100644
index 0000000000000..759ae558344ea
--- /dev/null
+++ b/packages/components/src/border-control/border-control-style-picker/hook.ts
@@ -0,0 +1,34 @@
+/**
+ * WordPress dependencies
+ */
+import { useMemo } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import * as styles from '../styles';
+import { useContextSystem, WordPressComponentProps } from '../../ui/context';
+import { useCx } from '../../utils/hooks/use-cx';
+
+import type { StylePickerProps } from '../types';
+
+export function useBorderControlStylePicker(
+ props: WordPressComponentProps< StylePickerProps, 'div' >
+) {
+ const { className, ...otherProps } = useContextSystem(
+ props,
+ 'BorderControlStylePicker'
+ );
+
+ // Generate class names.
+ const cx = useCx();
+ const classes = useMemo( () => {
+ return cx( styles.BorderControlStylePicker, className );
+ }, [ className ] );
+
+ const buttonClassName = useMemo( () => {
+ return cx( styles.BorderStyleButton );
+ }, [] );
+
+ return { ...otherProps, className: classes, buttonClassName };
+}
diff --git a/packages/components/src/border-control/border-control-style-picker/index.ts b/packages/components/src/border-control/border-control-style-picker/index.ts
new file mode 100644
index 0000000000000..b404d7fd44a81
--- /dev/null
+++ b/packages/components/src/border-control/border-control-style-picker/index.ts
@@ -0,0 +1 @@
+export { default } from './component';
diff --git a/packages/components/src/border-control/border-control/README.md b/packages/components/src/border-control/border-control/README.md
new file mode 100644
index 0000000000000..d988d230392d3
--- /dev/null
+++ b/packages/components/src/border-control/border-control/README.md
@@ -0,0 +1,159 @@
+# BorderControl
+
+
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
+
+
+This component provides control over a border's color, style, and width.
+
+## Development guidelines
+
+The `BorderControl` brings together internal sub-components which allow users to
+set the various properties of a border. The first sub-component, a
+`BorderDropdown` contains options representing border color and style. The
+border width is controlled via a `UnitControl` and an optional `RangeControl`.
+
+Border radius is not covered by this control as it may be desired separate to
+color, style, and width. For example, the border radius may be absorbed under
+a "shape" abstraction.
+
+## Usage
+
+```jsx
+import { __experimentalBorderControl as BorderControl } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+const colors = [
+ { name: 'Blue 20', color: '#72aee6' },
+ // ...
+];
+
+const MyBorderControl = () => {
+ const [ border, setBorder ] = useState();
+ const onChange = ( newBorder ) => setBorder( newBorder );
+
+ return (
+
+ );
+};
+```
+
+## Props
+
+### `colors`: `Array`
+
+An array of color definitions. This may also be a multi-dimensional array where
+colors are organized by multiple origins.
+
+Each color may be an object containing a `name` and `color` value.
+
+- Required: No
+
+### `disableCustomColors`: `boolean`
+
+This toggles the ability to choose custom colors.
+
+- Required: No
+
+### `enableAlpha`: `boolean`
+
+This controls whether the alpha channel will be offered when selecting
+custom colors.
+
+- Required: No
+
+### `enableStyle`: `boolean`
+
+This controls whether to include border style options within the
+`BorderDropdown` sub-component.
+
+- Required: No
+- Default: `true`
+
+### `hideLabelFromVision`: `boolean`
+
+Provides control over whether the label will only be visible to screen readers.
+
+- Required: No
+
+### `isCompact`: `boolean`
+
+This flags the `BorderControl` to render with a more compact appearance. It
+restricts the width of the control and prevents it from expanding to take up
+additional space.
+
+- Required: No
+
+### `label`: `string`
+
+If provided, a label will be generated using this as the content.
+
+_Whether it is visible only to screen readers is controlled via
+`hideLabelFromVision`._
+
+- Required: No
+
+### `onChange`: `( value?: Object ) => void`
+
+A callback function invoked when the border value is changed via an interaction
+that selects or clears, border color, style, or width.
+
+_Note: the value may be `undefined` if a user clears all border properties._
+
+- Required: Yes
+
+### `shouldSanitizeBorder`: `boolean`
+
+If opted into, sanitizing the border means that if no width or color have been
+selected, the border style is also cleared and `undefined` is returned as the
+new border value.
+
+- Required: No
+
+### `value`: `Object`
+
+An object representing a border or `undefined`. Used to set the current border
+configuration for this component.
+
+Example:
+```js
+ {
+ color: '#72aee6',
+ style: 'solid',
+ width: '2px,
+}
+```
+
+- Required: No
+
+### `width`: `string`
+
+Controls the visual width of the `BorderControl`.
+
+- Required: No
+
+### `withSlider`: `boolean`
+
+Flags whether this `BorderControl` should also render a `RangeControl` for
+additional control over a border's width.
+
+- Required: No
+
+### `__experimentalHasMultipleOrigins`: `boolean`
+
+This is passed on to the color related sub-components which need to be made
+aware of whether the colors prop contains multiple origins.
+
+- Required: No
+
+### `__experimentalIsRenderedInSidebar`: `boolean`
+
+This is passed on to the color related sub-components so they may render more
+effectively when used within a sidebar.
+
+- Required: No
diff --git a/packages/components/src/border-control/border-control/component.tsx b/packages/components/src/border-control/border-control/component.tsx
new file mode 100644
index 0000000000000..2a69653530cf4
--- /dev/null
+++ b/packages/components/src/border-control/border-control/component.tsx
@@ -0,0 +1,108 @@
+/**
+ * Internal dependencies
+ */
+import BorderControlDropdown from '../border-control-dropdown';
+import UnitControl from '../../unit-control';
+import RangeControl from '../../range-control';
+import { HStack } from '../../h-stack';
+import { StyledLabel } from '../../base-control/styles/base-control-styles';
+import { View } from '../../view';
+import { VisuallyHidden } from '../../visually-hidden';
+import { contextConnect, WordPressComponentProps } from '../../ui/context';
+import { useBorderControl } from './hook';
+
+import type { BorderControlProps, LabelProps } from '../types';
+
+const BorderLabel = ( props: LabelProps ) => {
+ const { label, hideLabelFromVision } = props;
+
+ if ( ! label ) {
+ return null;
+ }
+
+ return hideLabelFromVision ? (
+ { label }
+ ) : (
+ { label }
+ );
+};
+
+const BorderControl = (
+ props: WordPressComponentProps< BorderControlProps, 'div' >,
+ forwardedRef: React.Ref< any >
+) => {
+ const {
+ colors,
+ disableCustomColors,
+ enableAlpha,
+ enableStyle = true,
+ hideLabelFromVision,
+ innerWrapperClassName,
+ label,
+ onBorderChange,
+ onSliderChange,
+ onWidthChange,
+ placeholder,
+ previousStyleSelection,
+ sliderClassName,
+ value: border,
+ widthControlClassName,
+ widthUnit,
+ widthValue,
+ withSlider,
+ __experimentalHasMultipleOrigins,
+ __experimentalIsRenderedInSidebar,
+ ...otherProps
+ } = useBorderControl( props );
+
+ return (
+
+
+
+
+
+
+
+ { withSlider && (
+
+ ) }
+
+
+ );
+};
+
+const ConnectedBorderControl = contextConnect( BorderControl, 'BorderControl' );
+
+export default ConnectedBorderControl;
diff --git a/packages/components/src/border-control/border-control/hook.ts b/packages/components/src/border-control/border-control/hook.ts
new file mode 100644
index 0000000000000..26a1cf3c9547a
--- /dev/null
+++ b/packages/components/src/border-control/border-control/hook.ts
@@ -0,0 +1,141 @@
+/**
+ * WordPress dependencies
+ */
+import { useCallback, useMemo, useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import * as styles from '../styles';
+import { parseUnit } from '../../unit-control/utils';
+import { useContextSystem, WordPressComponentProps } from '../../ui/context';
+import { useCx } from '../../utils/hooks/use-cx';
+
+import type { Border, BorderControlProps } from '../types';
+
+const sanitizeBorder = ( border?: Border ) => {
+ const hasNoWidth = border?.width === undefined || border.width === '';
+ const hasNoColor = border?.color === undefined;
+
+ // If width and color are undefined, unset any style selection as well.
+ if ( hasNoWidth && hasNoColor ) {
+ return undefined;
+ }
+
+ return border;
+};
+
+export function useBorderControl(
+ props: WordPressComponentProps< BorderControlProps, 'div' >
+) {
+ const {
+ className,
+ isCompact,
+ onChange,
+ shouldSanitizeBorder = true,
+ value: border,
+ width,
+ ...otherProps
+ } = useContextSystem( props, 'BorderControl' );
+
+ const [ widthValue, originalWidthUnit ] = parseUnit( border?.width );
+ const widthUnit = originalWidthUnit || 'px';
+ const hadPreviousZeroWidth = widthValue === 0;
+
+ const [ colorSelection, setColorSelection ] = useState< string >();
+ const [ styleSelection, setStyleSelection ] = useState< string >();
+
+ const onBorderChange = useCallback(
+ ( newBorder?: Border ) => {
+ if ( shouldSanitizeBorder ) {
+ return onChange( sanitizeBorder( newBorder ) );
+ }
+
+ onChange( newBorder );
+ },
+ [ onChange, shouldSanitizeBorder, sanitizeBorder ]
+ );
+
+ const onWidthChange = useCallback(
+ ( newWidth?: string ) => {
+ const newWidthValue = newWidth === '' ? undefined : newWidth;
+ const [ parsedValue ] = parseUnit( newWidth );
+ const hasZeroWidth = parsedValue === 0;
+
+ const updatedBorder = { ...border, width: newWidthValue };
+
+ // Setting the border width explicitly to zero will also set the
+ // border style to `none` and clear the border color.
+ if ( hasZeroWidth && ! hadPreviousZeroWidth ) {
+ // Before clearing the color and style selections, keep track of
+ // the current selections so they can be restored when the width
+ // changes to a non-zero value.
+ setColorSelection( border?.color );
+ setStyleSelection( border?.style );
+
+ // Clear the color and style border properties.
+ updatedBorder.color = undefined;
+ updatedBorder.style = 'none';
+ }
+
+ // Selection has changed from zero border width to non-zero width.
+ if ( ! hasZeroWidth && hadPreviousZeroWidth ) {
+ // Restore previous border color and style selections if width
+ // is now not zero.
+ if ( updatedBorder.color === undefined ) {
+ updatedBorder.color = colorSelection;
+ }
+ if ( updatedBorder.style === 'none' ) {
+ updatedBorder.style = styleSelection;
+ }
+ }
+
+ onBorderChange( updatedBorder );
+ },
+ [ border, hadPreviousZeroWidth, onBorderChange ]
+ );
+
+ const onSliderChange = useCallback(
+ ( value: string ) => {
+ onWidthChange( `${ value }${ widthUnit }` );
+ },
+ [ onWidthChange, widthUnit ]
+ );
+
+ // Generate class names.
+ const cx = useCx();
+ const classes = useMemo( () => {
+ return cx( styles.BorderControl, className );
+ }, [ className ] );
+
+ const innerWrapperClassName = useMemo( () => {
+ const wrapperWidth = isCompact ? '90px' : width;
+ const widthStyle =
+ !! wrapperWidth && styles.WrapperWidth( wrapperWidth );
+
+ return cx( styles.InnerWrapper, widthStyle );
+ }, [ isCompact, width ] );
+
+ const widthControlClassName = useMemo( () => {
+ return cx( styles.BorderWidthControl );
+ }, [] );
+
+ const sliderClassName = useMemo( () => {
+ return cx( styles.BorderSlider );
+ }, [] );
+
+ return {
+ ...otherProps,
+ className: classes,
+ innerWrapperClassName,
+ onBorderChange,
+ onSliderChange,
+ onWidthChange,
+ previousStyleSelection: styleSelection,
+ sliderClassName,
+ value: border,
+ widthControlClassName,
+ widthUnit,
+ widthValue,
+ };
+}
diff --git a/packages/components/src/border-control/border-control/index.ts b/packages/components/src/border-control/border-control/index.ts
new file mode 100644
index 0000000000000..7aa372d0c656b
--- /dev/null
+++ b/packages/components/src/border-control/border-control/index.ts
@@ -0,0 +1,2 @@
+export { default as BorderControl } from './component';
+export { useBorderControl } from './hook';
diff --git a/packages/components/src/border-control/index.ts b/packages/components/src/border-control/index.ts
new file mode 100644
index 0000000000000..a5eeb763a7c9c
--- /dev/null
+++ b/packages/components/src/border-control/index.ts
@@ -0,0 +1,2 @@
+export { default as BorderControl } from './border-control/component';
+export { useBorderControl } from './border-control/hook';
diff --git a/packages/components/src/border-control/stories/index.js b/packages/components/src/border-control/stories/index.js
new file mode 100644
index 0000000000000..cdbdd98ee42ea
--- /dev/null
+++ b/packages/components/src/border-control/stories/index.js
@@ -0,0 +1,70 @@
+/**
+ * External dependencies
+ */
+import styled from '@emotion/styled';
+
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { BorderControl } from '../';
+
+export default {
+ title: 'Components (Experimental)/BorderControl',
+ component: BorderControl,
+};
+
+// Available border colors.
+const colors = [
+ { name: 'Gray 0', color: '#f6f7f7' },
+ { name: 'Gray 5', color: '#dcdcde' },
+ { name: 'Gray 20', color: '#a7aaad' },
+ { name: 'Gray 70', color: '#3c434a' },
+ { name: 'Gray 100', color: '#101517' },
+ { name: 'Blue 20', color: '#72aee6' },
+ { name: 'Blue 40', color: '#3582c4' },
+ { name: 'Blue 70', color: '#0a4b78' },
+ { name: 'Red 40', color: '#e65054' },
+ { name: 'Red 70', color: '#8a2424' },
+ { name: 'Green 10', color: '#68de7c' },
+ { name: 'Green 40', color: '#00a32a' },
+ { name: 'Green 60', color: '#007017' },
+ { name: 'Yellow 10', color: '#f2d675' },
+ { name: 'Yellow 40', color: '#bd8600' },
+];
+
+const _default = ( props ) => {
+ const [ border, setBorder ] = useState();
+ const onChange = ( newBorder ) => setBorder( newBorder );
+
+ return (
+
+
+
+ );
+};
+
+export const Default = _default.bind( {} );
+Default.args = {
+ disableCustomColors: false,
+ enableAlpha: true,
+ enableStyle: true,
+ isCompact: true,
+ width: '110px',
+ withSlider: true,
+};
+
+const WrapperView = styled.div`
+ max-width: 280px;
+`;
diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts
new file mode 100644
index 0000000000000..97fef02fbfdc9
--- /dev/null
+++ b/packages/components/src/border-control/styles.ts
@@ -0,0 +1,158 @@
+/**
+ * External dependencies
+ */
+import { css } from '@emotion/react';
+
+/**
+ * Internal dependencies
+ */
+import { COLORS, CONFIG } from '../utils';
+import { space } from '../ui/utils/space';
+import { StyledLabel } from '../base-control/styles/base-control-styles';
+
+export const BorderControl = css`
+ position: relative;
+`;
+
+export const InnerWrapper = css`
+ border: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 200 ] };
+ border-radius: 2px;
+ flex: 1 0 40%;
+
+ /*
+ * Needs more thought. Aim is to prevent the border for BorderBoxControl
+ * showing through the control. Likely needs to take into account
+ * light/dark themes etc.
+ */
+ background: #fff;
+
+ /*
+ * Forces the width control to fill available space given UnitControl
+ * passes its className directly through to the input.
+ */
+ > div:last-child {
+ flex: 1;
+ margin-left: 0;
+ }
+
+ /* If arbitrary width is supplied honor it. */
+ &[style*='width'] {
+ flex: 0 0 auto;
+ }
+`;
+
+export const WrapperWidth = ( width: string ) => {
+ return css`
+ width: ${ width };
+ flex: 0 0 auto;
+ `;
+};
+
+export const BorderControlDropdown = css`
+ border-radius: 1px 0 0 1px;
+ border-right: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 200 ] };
+ background: #fff;
+
+ && > button {
+ padding: ${ space( 1 ) };
+ border-radius: inherit;
+
+ > span {
+ border-radius: 9999px;
+ border: 2px solid transparent;
+ width: 28px;
+ height: 28px;
+ padding: 2px;
+
+ & > span {
+ background: linear-gradient(
+ -45deg,
+ transparent 48%,
+ rgb( 0 0 0 / 20% ) 48%,
+ rgb( 0 0 0 / 20% ) 52%,
+ transparent 52%
+ );
+ }
+ }
+ }
+`;
+
+export const BorderControlPopover = css`
+ /* Remove padding from content, this will be re-added via inner elements*/
+ && > div > div {
+ padding: 0;
+ }
+`;
+
+export const BorderControlPopoverControls = css`
+ padding: ${ space( 4 ) };
+
+ > div:first-of-type > ${ StyledLabel } {
+ margin-bottom: 0;
+ font-weight: 500;
+ }
+
+ && ${ StyledLabel } + button:not( .has-text ) {
+ min-width: 24px;
+ padding: 0;
+ }
+`;
+
+export const BorderControlPopoverContent = css``;
+export const BorderColorIndicator = css``;
+
+export const ResetButton = css`
+ justify-content: center;
+ width: 100%;
+
+ /* Override button component styling */
+ && {
+ border-top: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 200 ] };
+ height: 46px;
+ }
+`;
+
+export const BorderWidthControl = css`
+ /* Target the UnitControl backdrop */
+ &&& > div > div {
+ border: none;
+ }
+
+ /* Specificity required to overcome UnitControl padding */
+ /* See packages/components/src/unit-control/styles/unit-control-styles.ts */
+ &&& input {
+ padding-right: 0;
+ }
+`;
+
+export const BorderControlStylePicker = css`
+ > label {
+ display: block;
+ font-weight: 500;
+ }
+
+ > div {
+ display: inline-flex;
+ }
+`;
+
+export const BorderStyleButton = css`
+ &&&&& {
+ min-width: 30px;
+ width: 30px;
+ height: 30px;
+ padding: 3px;
+ margin-right: ${ space( 1 ) };
+ }
+`;
+
+export const BorderSlider = css`
+ flex: 1 1 60%;
+ margin-right: ${ space( 3 ) };
+
+ > div {
+ margin-bottom: 0;
+ display: flex;
+ align-items: center;
+ }
+`;
diff --git a/packages/components/src/border-control/test/index.js b/packages/components/src/border-control/test/index.js
new file mode 100644
index 0000000000000..c03ab0ef141cc
--- /dev/null
+++ b/packages/components/src/border-control/test/index.js
@@ -0,0 +1,336 @@
+/**
+ * External dependencies
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+
+/**
+ * Internal dependencies
+ */
+import { BorderControl } from '../';
+
+const colors = [
+ { name: 'Gray', color: '#f6f7f7' },
+ { name: 'Blue', color: '#72aee6' },
+ { name: 'Red', color: '#e65054' },
+ { name: 'Green', color: '#00a32a' },
+ { name: 'Yellow', color: '#bd8600' },
+];
+
+const defaultBorder = {
+ color: '#72aee6',
+ style: 'solid',
+ width: '1px',
+};
+
+const props = {
+ colors,
+ label: 'Border',
+ onChange: jest.fn().mockImplementation( ( newValue ) => {
+ props.value = newValue;
+ } ),
+ value: defaultBorder,
+};
+
+const renderBorderControl = ( customProps ) => {
+ return render( );
+};
+
+const rerenderBorderControl = ( rerender, customProps ) => {
+ return rerender( );
+};
+
+const openPopover = ( dropdownToggle ) => {
+ const toggleButton =
+ dropdownToggle || screen.getByLabelText( 'Open border options' );
+ fireEvent.click( toggleButton );
+};
+
+const getButton = ( name ) => {
+ return screen.getByRole( 'button', { name } );
+};
+
+const queryButton = ( name ) => {
+ return screen.queryByRole( 'button', { name } );
+};
+
+const clickButton = ( name ) => {
+ fireEvent.click( getButton( name ) );
+};
+
+const setWidthInput = ( value ) => {
+ const widthInput = screen.getByRole( 'spinbutton' );
+ widthInput.focus();
+ fireEvent.change( widthInput, { target: { value } } );
+};
+
+const clearWidthInput = () => setWidthInput( '' );
+
+describe( 'BorderControl', () => {
+ describe( 'basic rendering', () => {
+ it( 'should render standard border control', () => {
+ renderBorderControl();
+
+ const label = screen.getByText( props.label );
+ const colorButton = screen.getByLabelText( 'Open border options' );
+ const widthInput = screen.getByRole( 'spinbutton' );
+ const unitSelect = screen.getByRole( 'combobox' );
+ const slider = screen.queryByRole( 'slider' );
+
+ expect( label ).toBeInTheDocument();
+ expect( colorButton ).toBeInTheDocument();
+ expect( widthInput ).toBeInTheDocument();
+ expect( unitSelect ).toBeInTheDocument();
+ expect( slider ).not.toBeInTheDocument();
+ } );
+
+ it( 'should hide label', () => {
+ renderBorderControl( { hideLabelFromVision: true } );
+ const label = screen.getByText( props.label );
+
+ // As visually hidden labels are still included in the document
+ // and do not have `display: none` styling, we can't rely on
+ // `.toBeInTheDocument()` or `.toBeVisible()` assertions.
+ expect( label ).toHaveAttribute(
+ 'data-wp-component',
+ 'VisuallyHidden'
+ );
+ } );
+
+ it( 'should render with slider', () => {
+ renderBorderControl( { withSlider: true } );
+
+ const slider = screen.getByRole( 'slider' );
+ expect( slider ).toBeInTheDocument();
+ } );
+
+ it( 'should render placeholder in UnitControl', () => {
+ renderBorderControl( { placeholder: 'Mixed' } );
+ const widthInput = screen.getByRole( 'spinbutton' );
+
+ expect( widthInput ).toHaveAttribute( 'placeholder', 'Mixed' );
+ } );
+
+ it( 'should render color and style popover', () => {
+ renderBorderControl();
+ openPopover();
+
+ const headerLabel = screen.getByText( 'Border color' );
+ const closeButton = getButton( 'Close border color' );
+ const customColorPicker = getButton( 'Custom color picker' );
+ const colorSwatchButtons = screen.getAllByRole( 'button', {
+ name: /^Color:/,
+ } );
+ const styleLabel = screen.getByText( 'Style' );
+ const solidButton = getButton( 'Solid' );
+ const dashedButton = getButton( 'Dashed' );
+ const dottedButton = getButton( 'Dotted' );
+ const resetButton = getButton( 'Reset to default' );
+
+ expect( headerLabel ).toBeInTheDocument();
+ expect( closeButton ).toBeInTheDocument();
+ expect( customColorPicker ).toBeInTheDocument();
+ expect( colorSwatchButtons.length ).toEqual( colors.length );
+ expect( styleLabel ).toBeInTheDocument();
+ expect( solidButton ).toBeInTheDocument();
+ expect( dashedButton ).toBeInTheDocument();
+ expect( dottedButton ).toBeInTheDocument();
+ expect( resetButton ).toBeInTheDocument();
+ } );
+
+ it( 'should not render style options when opted out of', () => {
+ renderBorderControl( { enableStyle: false } );
+ openPopover();
+
+ const styleLabel = screen.queryByText( 'Style' );
+ const solidButton = queryButton( 'Solid' );
+ const dashedButton = queryButton( 'Dashed' );
+ const dottedButton = queryButton( 'Dotted' );
+
+ expect( styleLabel ).not.toBeInTheDocument();
+ expect( solidButton ).not.toBeInTheDocument();
+ expect( dashedButton ).not.toBeInTheDocument();
+ expect( dottedButton ).not.toBeInTheDocument();
+ } );
+ } );
+
+ describe( 'onChange handling', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ props.value = defaultBorder;
+ } );
+
+ it( 'should update width with slider value', () => {
+ const { rerender } = renderBorderControl( { withSlider: true } );
+
+ const slider = screen.getByRole( 'slider' );
+ fireEvent.change( slider, { target: { value: '5' } } );
+
+ expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
+ ...defaultBorder,
+ width: '5px',
+ } );
+
+ rerenderBorderControl( rerender, { withSlider: true } );
+ const widthInput = screen.getByRole( 'spinbutton' );
+
+ expect( widthInput.value ).toEqual( '5' );
+ } );
+
+ it( 'should update color selection', () => {
+ renderBorderControl();
+ openPopover();
+ clickButton( 'Color: Green' );
+
+ expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
+ ...defaultBorder,
+ color: '#00a32a',
+ } );
+ } );
+
+ it( 'should clear color selection when toggling swatch off', () => {
+ renderBorderControl();
+ openPopover();
+ clickButton( 'Color: Blue' );
+
+ expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
+ ...defaultBorder,
+ color: undefined,
+ } );
+ } );
+
+ it( 'should update style selection', () => {
+ renderBorderControl();
+ openPopover();
+ clickButton( 'Dashed' );
+
+ expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
+ ...defaultBorder,
+ style: 'dashed',
+ } );
+ } );
+
+ it( 'should take no action when color and style popover is closed', () => {
+ renderBorderControl();
+ openPopover();
+ clickButton( 'Close border color' );
+
+ expect( props.onChange ).not.toHaveBeenCalled();
+ } );
+
+ it( 'should reset color and style only when popover reset button clicked', () => {
+ renderBorderControl();
+ openPopover();
+ clickButton( 'Reset to default' );
+
+ expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
+ color: undefined,
+ style: undefined,
+ width: defaultBorder.width,
+ } );
+ } );
+
+ it( 'should sanitize border when width and color are undefined', () => {
+ const { rerender } = renderBorderControl();
+ clearWidthInput();
+ rerenderBorderControl( rerender );
+ openPopover();
+ clickButton( 'Color: Blue' );
+
+ expect( props.onChange ).toHaveBeenCalledWith( undefined );
+ } );
+
+ it( 'should not sanitize border when requested', () => {
+ const { rerender } = renderBorderControl( {
+ shouldSanitizeBorder: false,
+ } );
+ clearWidthInput();
+ rerenderBorderControl( rerender, { shouldSanitizeBorder: false } );
+ openPopover();
+ clickButton( 'Color: Blue' );
+
+ expect( props.onChange ).toHaveBeenNthCalledWith( 2, {
+ color: undefined,
+ style: defaultBorder.style,
+ width: undefined,
+ } );
+ } );
+
+ it( 'should clear color and set style to `none` when setting zero width', () => {
+ renderBorderControl();
+ openPopover();
+ clickButton( 'Color: Green' );
+ clickButton( 'Dotted' );
+ setWidthInput( '0' );
+
+ expect( props.onChange ).toHaveBeenNthCalledWith( 3, {
+ color: undefined,
+ style: 'none',
+ width: '0px',
+ } );
+ } );
+
+ it( 'should reselect color and style selections when changing to non-zero width', () => {
+ const { rerender } = renderBorderControl();
+ openPopover();
+ clickButton( 'Color: Green' );
+ rerenderBorderControl( rerender );
+ clickButton( 'Dotted' );
+ rerenderBorderControl( rerender );
+ setWidthInput( '0' );
+ setWidthInput( '5' );
+
+ expect( props.onChange ).toHaveBeenNthCalledWith( 4, {
+ color: '#00a32a',
+ style: 'dotted',
+ width: '5px',
+ } );
+ } );
+
+ it( 'should set a non-zero width when applying color to zero width border', () => {
+ const { rerender } = renderBorderControl( { value: undefined } );
+ openPopover();
+ clickButton( 'Color: Yellow' );
+
+ expect( props.onChange ).toHaveBeenCalledWith( {
+ color: '#bd8600',
+ style: undefined,
+ width: undefined,
+ } );
+
+ setWidthInput( '0' );
+ rerenderBorderControl( rerender );
+ clickButton( 'Color: Green' );
+
+ expect( props.onChange ).toHaveBeenCalledWith( {
+ color: '#00a32a',
+ style: undefined,
+ width: '1px',
+ } );
+ } );
+
+ it( 'should set a non-zero width when applying style to zero width border', () => {
+ const { rerender } = renderBorderControl( {
+ value: undefined,
+ shouldSanitizeBorder: false,
+ } );
+ openPopover();
+ clickButton( 'Dashed' );
+
+ expect( props.onChange ).toHaveBeenCalledWith( {
+ color: undefined,
+ style: 'dashed',
+ width: undefined,
+ } );
+
+ setWidthInput( '0' );
+ rerenderBorderControl( rerender, { shouldSanitizeBorder: false } );
+ clickButton( 'Dotted' );
+
+ expect( props.onChange ).toHaveBeenCalledWith( {
+ color: undefined,
+ style: 'dotted',
+ width: '1px',
+ } );
+ } );
+ } );
+} );
diff --git a/packages/components/src/border-control/types.ts b/packages/components/src/border-control/types.ts
new file mode 100644
index 0000000000000..d64a8147e95d5
--- /dev/null
+++ b/packages/components/src/border-control/types.ts
@@ -0,0 +1,137 @@
+/**
+ * External dependencies
+ */
+import type { CSSProperties } from 'react';
+
+export type Border = {
+ color?: CSSProperties[ 'borderColor' ];
+ style?: CSSProperties[ 'borderStyle' ];
+ width?: CSSProperties[ 'borderWidth' ];
+};
+
+export type Color = {
+ name: string;
+ color: CSSProperties[ 'color' ];
+};
+
+export type ColorProps = {
+ /**
+ * An array of color definitions. This may also be a multi-dimensional array
+ * where colors are organized by multiple origins.
+ */
+ colors?: Color[];
+ /**
+ * This toggles the ability to choose custom colors.
+ */
+ disableCustomColors?: boolean;
+ /**
+ * This controls whether the alpha channel will be offered when selecting
+ * custom colors.
+ */
+ enableAlpha?: boolean;
+ /**
+ * This is passed on to the color related sub-components which need to be
+ * made aware of whether the colors prop contains multiple origins.
+ */
+ __experimentalHasMultipleOrigins?: boolean;
+ /**
+ * This is passed on to the color related sub-components so they may render
+ * more effectively when used within a sidebar.
+ */
+ __experimentalIsRenderedInSidebar?: boolean;
+};
+
+export type LabelProps = {
+ /**
+ * Provides control over whether the label will only be visible to
+ * screen readers.
+ */
+ hideLabelFromVision?: boolean;
+ /**
+ * If provided, a label will be generated using this as the content.
+ */
+ label?: string;
+};
+
+export type BorderControlProps = ColorProps &
+ LabelProps & {
+ /**
+ * This controls whether to include border style options within the
+ * `BorderDropdown` sub-component.
+ */
+ enableStyle?: boolean;
+ /**
+ * This flags the `BorderControl` to render with a more compact appearance.
+ * It restricts the width of the control and prevents it from expanding to
+ * take up additional space.
+ */
+ isCompact?: boolean;
+ /**
+ * A callback function invoked when the border value is changed via an
+ * interaction that selects or clears, border color, style, or width.
+ */
+ onChange: ( value?: Border ) => void;
+ /**
+ * If opted into, sanitizing the border means that if no width or color have
+ * been selected, the border style is also cleared and `undefined`
+ * is returned as the new border value.
+ */
+ shouldSanitizeBorder?: boolean;
+ /**
+ * An object representing a border or `undefined`. Used to set the current
+ * border configuration for this component.
+ */
+ value?: Border;
+ /**
+ * Controls the visual width of the `BorderControl`.
+ */
+ width?: string;
+ /**
+ * Flags whether this `BorderControl` should also render a `RangeControl`
+ * for additional control over a border's width.
+ */
+ withSlider?: boolean;
+ };
+
+export type DropdownProps = ColorProps & {
+ /**
+ * An object representing a border or `undefined`. This component will
+ * extract the border color and style selections from this object to use as
+ * values for its popover controls.
+ */
+ border?: Border;
+ /**
+ * This controls whether to render border style options.
+ */
+ enableStyle?: boolean;
+ /**
+ * A callback invoked when the border color or style selections change.
+ */
+ onChange: ( newBorder?: Border ) => void;
+ /**
+ * Any previous style selection made by the user. This can be used to
+ * reapply that previous selection when, for example, a zero border width is
+ * to a non-zero value.
+ */
+ previousStyleSelection?: string;
+};
+
+export type StylePickerProps = LabelProps & {
+ /**
+ * A callback function invoked when a border style is selected or cleared.
+ */
+ onChange: ( style?: string ) => void;
+ /**
+ * The currently selected border style if there is one. Styles available via
+ * this control are `solid`, `dashed` & `dotted`, however the possibility
+ * to store other valid CSS values is maintained e.g. `none`, `inherit` etc.
+ */
+ value?: string;
+};
+
+export type PopoverProps = {
+ /**
+ * Callback function to invoke when closing the border dropdown's popover.
+ */
+ onClose: () => void;
+};
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index 6331cd9b7d5d5..16a41cf614356 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -23,6 +23,7 @@ export {
useAutocompleteProps as __unstableUseAutocompleteProps,
} from './autocomplete';
export { default as BaseControl } from './base-control';
+export { BorderControl as __experimentalBorderControl } from './border-control';
export { default as __experimentalBoxControl } from './box-control';
export { default as Button } from './button';
export { default as ButtonGroup } from './button-group';
diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json
index bb38249afa624..4fee27671cfc6 100644
--- a/packages/components/tsconfig.json
+++ b/packages/components/tsconfig.json
@@ -26,6 +26,7 @@
"src/animate/**/*",
"src/base-control/**/*",
"src/base-field/**/*",
+ "src/border-control/**/*",
"src/button/**/*",
"src/card/**/*",
"src/circular-option-picker/**/*",
From 08a4aafdf542f22b61e92294b4682da388648355 Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 16:03:37 +1000
Subject: [PATCH 02/31] Improve docs and comments for width prop
---
.../components/src/border-control/border-control/README.md | 3 ++-
packages/components/src/border-control/types.ts | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/packages/components/src/border-control/border-control/README.md b/packages/components/src/border-control/border-control/README.md
index d988d230392d3..1c252cd1a9f20 100644
--- a/packages/components/src/border-control/border-control/README.md
+++ b/packages/components/src/border-control/border-control/README.md
@@ -133,7 +133,8 @@ Example:
### `width`: `string`
-Controls the visual width of the `BorderControl`.
+Controls the visual width of the `BorderControl`. It has no effect if the
+`isCompact` prop is set to `true`.
- Required: No
diff --git a/packages/components/src/border-control/types.ts b/packages/components/src/border-control/types.ts
index d64a8147e95d5..e07dc5eac3f5b 100644
--- a/packages/components/src/border-control/types.ts
+++ b/packages/components/src/border-control/types.ts
@@ -83,7 +83,8 @@ export type BorderControlProps = ColorProps &
*/
value?: Border;
/**
- * Controls the visual width of the `BorderControl`.
+ * Controls the visual width of the `BorderControl`. It has no effect if
+ * the `isCompact` prop is set to `true`.
*/
width?: string;
/**
From 45b2fd922cd7fc3e3621e38ad71727336dc47b21 Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 16:10:09 +1000
Subject: [PATCH 03/31] Target BackdropUI instead of div for removing border
---
packages/components/src/border-control/styles.ts | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts
index 97fef02fbfdc9..0424f588be3d1 100644
--- a/packages/components/src/border-control/styles.ts
+++ b/packages/components/src/border-control/styles.ts
@@ -9,6 +9,7 @@ import { css } from '@emotion/react';
import { COLORS, CONFIG } from '../utils';
import { space } from '../ui/utils/space';
import { StyledLabel } from '../base-control/styles/base-control-styles';
+import { BackdropUI } from '../input-control/styles/input-control-styles';
export const BorderControl = css`
position: relative;
@@ -113,8 +114,8 @@ export const ResetButton = css`
`;
export const BorderWidthControl = css`
- /* Target the UnitControl backdrop */
- &&& > div > div {
+ /* Target the InputControl's backdrop */
+ &&& ${ BackdropUI } {
border: none;
}
From 3c072f33b3803a06923d9fee11f0a13373f36e5a Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 16:34:22 +1000
Subject: [PATCH 04/31] Add RTL styles
---
.../components/src/border-control/styles.ts | 22 +++++++++++++------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts
index 0424f588be3d1..cd7baceea62d3 100644
--- a/packages/components/src/border-control/styles.ts
+++ b/packages/components/src/border-control/styles.ts
@@ -6,7 +6,7 @@ import { css } from '@emotion/react';
/**
* Internal dependencies
*/
-import { COLORS, CONFIG } from '../utils';
+import { COLORS, CONFIG, rtl } from '../utils';
import { space } from '../ui/utils/space';
import { StyledLabel } from '../base-control/styles/base-control-styles';
import { BackdropUI } from '../input-control/styles/input-control-styles';
@@ -33,7 +33,7 @@ export const InnerWrapper = css`
*/
> div:last-child {
flex: 1;
- margin-left: 0;
+ ${ rtl( { marginLeft: 0 } )() }
}
/* If arbitrary width is supplied honor it. */
@@ -50,9 +50,17 @@ export const WrapperWidth = ( width: string ) => {
};
export const BorderControlDropdown = css`
- border-radius: 1px 0 0 1px;
- border-right: ${ CONFIG.borderWidth } solid ${ COLORS.gray[ 200 ] };
background: #fff;
+ ${ rtl(
+ {
+ borderRadius: `1px 0 0 1px`,
+ borderRight: `${ CONFIG.borderWidth } solid ${ COLORS.gray[ 200 ] }`,
+ },
+ {
+ borderRadius: `0 1px 1px 0`,
+ borderLeft: `${ CONFIG.borderWidth } solid ${ COLORS.gray[ 200 ] }`,
+ }
+ )() }
&& > button {
padding: ${ space( 1 ) };
@@ -122,7 +130,7 @@ export const BorderWidthControl = css`
/* Specificity required to overcome UnitControl padding */
/* See packages/components/src/unit-control/styles/unit-control-styles.ts */
&&& input {
- padding-right: 0;
+ ${ rtl( { paddingRight: 0 } )() }
}
`;
@@ -143,13 +151,13 @@ export const BorderStyleButton = css`
width: 30px;
height: 30px;
padding: 3px;
- margin-right: ${ space( 1 ) };
+ ${ rtl( { marginRight: space( 1 ) } )() }
}
`;
export const BorderSlider = css`
flex: 1 1 60%;
- margin-right: ${ space( 3 ) };
+ ${ rtl( { marginRight: space( 3 ) } )() }
> div {
margin-bottom: 0;
From a6be837a238d6e16d26bcd6233063bcc1a6db49b Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 16:54:37 +1000
Subject: [PATCH 05/31] Switch BorderControlStylePicker to use inner Flex
component
---
.../border-control/border-control-style-picker/component.tsx | 5 +++--
packages/components/src/border-control/styles.ts | 5 -----
2 files changed, 3 insertions(+), 7 deletions(-)
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
index 6db2e6b4ed15a..9da99b442fe8d 100644
--- a/packages/components/src/border-control/border-control-style-picker/component.tsx
+++ b/packages/components/src/border-control/border-control-style-picker/component.tsx
@@ -10,6 +10,7 @@ import { __ } from '@wordpress/i18n';
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';
@@ -55,7 +56,7 @@ const BorderControlStylePicker = (
label={ label }
hideLabelFromVision={ hideLabelFromVision }
/>
-
+
{ BORDER_STYLES.map( ( borderStyle ) => (
) ) }
-
+
);
};
diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts
index cd7baceea62d3..5b1d08c875749 100644
--- a/packages/components/src/border-control/styles.ts
+++ b/packages/components/src/border-control/styles.ts
@@ -139,10 +139,6 @@ export const BorderControlStylePicker = css`
display: block;
font-weight: 500;
}
-
- > div {
- display: inline-flex;
- }
`;
export const BorderStyleButton = css`
@@ -151,7 +147,6 @@ export const BorderStyleButton = css`
width: 30px;
height: 30px;
padding: 3px;
- ${ rtl( { marginRight: space( 1 ) } )() }
}
`;
From 95a74a7bdabba7cb9d8149cd544965c59e13238c Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 17:00:24 +1000
Subject: [PATCH 06/31] Refactor label styles
---
packages/components/src/border-control/styles.ts | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts
index 5b1d08c875749..5f58134b0dcce 100644
--- a/packages/components/src/border-control/styles.ts
+++ b/packages/components/src/border-control/styles.ts
@@ -11,6 +11,10 @@ import { space } from '../ui/utils/space';
import { StyledLabel } from '../base-control/styles/base-control-styles';
import { BackdropUI } from '../input-control/styles/input-control-styles';
+const labelStyles = css`
+ font-weight: 500;
+`;
+
export const BorderControl = css`
position: relative;
`;
@@ -98,7 +102,7 @@ export const BorderControlPopoverControls = css`
> div:first-of-type > ${ StyledLabel } {
margin-bottom: 0;
- font-weight: 500;
+ ${ labelStyles }
}
&& ${ StyledLabel } + button:not( .has-text ) {
@@ -135,9 +139,8 @@ export const BorderWidthControl = css`
`;
export const BorderControlStylePicker = css`
- > label {
- display: block;
- font-weight: 500;
+ ${ StyledLabel } {
+ ${ labelStyles }
}
`;
From a7ecb60e301d734885bdc2ed093ab23929d6d64a Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 17:07:37 +1000
Subject: [PATCH 07/31] Add cx to useMemo deps
---
.../border-control/border-control-dropdown/hook.ts | 12 ++++++------
.../border-control-style-picker/hook.ts | 4 ++--
.../src/border-control/border-control/hook.ts | 8 ++++----
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/packages/components/src/border-control/border-control-dropdown/hook.ts b/packages/components/src/border-control/border-control-dropdown/hook.ts
index 84035e4794618..d04ebd25ce266 100644
--- a/packages/components/src/border-control/border-control-dropdown/hook.ts
+++ b/packages/components/src/border-control/border-control-dropdown/hook.ts
@@ -53,27 +53,27 @@ export function useBorderControlDropdown(
const cx = useCx();
const classes = useMemo( () => {
return cx( styles.BorderControlDropdown, className );
- }, [ className ] );
+ }, [ className, cx ] );
const indicatorClassName = useMemo( () => {
return cx( styles.BorderColorIndicator );
- }, [] );
+ }, [ 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,
diff --git a/packages/components/src/border-control/border-control-style-picker/hook.ts b/packages/components/src/border-control/border-control-style-picker/hook.ts
index 759ae558344ea..7ac54181909ed 100644
--- a/packages/components/src/border-control/border-control-style-picker/hook.ts
+++ b/packages/components/src/border-control/border-control-style-picker/hook.ts
@@ -24,11 +24,11 @@ export function useBorderControlStylePicker(
const cx = useCx();
const classes = useMemo( () => {
return cx( styles.BorderControlStylePicker, className );
- }, [ className ] );
+ }, [ className, cx ] );
const buttonClassName = useMemo( () => {
return cx( styles.BorderStyleButton );
- }, [] );
+ }, [ cx ] );
return { ...otherProps, className: classes, buttonClassName };
}
diff --git a/packages/components/src/border-control/border-control/hook.ts b/packages/components/src/border-control/border-control/hook.ts
index 26a1cf3c9547a..cfe0067aeb007 100644
--- a/packages/components/src/border-control/border-control/hook.ts
+++ b/packages/components/src/border-control/border-control/hook.ts
@@ -106,7 +106,7 @@ export function useBorderControl(
const cx = useCx();
const classes = useMemo( () => {
return cx( styles.BorderControl, className );
- }, [ className ] );
+ }, [ className, cx ] );
const innerWrapperClassName = useMemo( () => {
const wrapperWidth = isCompact ? '90px' : width;
@@ -114,15 +114,15 @@ export function useBorderControl(
!! wrapperWidth && styles.WrapperWidth( wrapperWidth );
return cx( styles.InnerWrapper, widthStyle );
- }, [ isCompact, width ] );
+ }, [ isCompact, width, cx ] );
const widthControlClassName = useMemo( () => {
return cx( styles.BorderWidthControl );
- }, [] );
+ }, [ cx ] );
const sliderClassName = useMemo( () => {
return cx( styles.BorderSlider );
- }, [] );
+ }, [ cx ] );
return {
...otherProps,
From a3fd0e02bc28645c8770f0462fd159bfe1f11b42 Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 17:08:05 +1000
Subject: [PATCH 08/31] Use StyledField to target slider margin removal
---
packages/components/src/border-control/styles.ts | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts
index 5f58134b0dcce..97a12a4e43629 100644
--- a/packages/components/src/border-control/styles.ts
+++ b/packages/components/src/border-control/styles.ts
@@ -8,7 +8,10 @@ import { css } from '@emotion/react';
*/
import { COLORS, CONFIG, rtl } from '../utils';
import { space } from '../ui/utils/space';
-import { StyledLabel } from '../base-control/styles/base-control-styles';
+import {
+ StyledField,
+ StyledLabel,
+} from '../base-control/styles/base-control-styles';
import { BackdropUI } from '../input-control/styles/input-control-styles';
const labelStyles = css`
@@ -157,7 +160,7 @@ export const BorderSlider = css`
flex: 1 1 60%;
${ rtl( { marginRight: space( 3 ) } )() }
- > div {
+ ${ StyledField } {
margin-bottom: 0;
display: flex;
align-items: center;
From f6cbe7fc9d31b64a4f36e904988c2fa227cb4ea8 Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 17:16:49 +1000
Subject: [PATCH 09/31] Fix type ignoring for color components
---
.../src/border-control/border-control-dropdown/component.tsx | 2 --
packages/components/tsconfig.json | 1 +
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx
index b89ef1ef8daca..d17a08d2691b9 100644
--- a/packages/components/src/border-control/border-control-dropdown/component.tsx
+++ b/packages/components/src/border-control/border-control-dropdown/component.tsx
@@ -9,9 +9,7 @@ import { closeSmall } from '@wordpress/icons';
*/
import BorderControlStylePicker from '../border-control-style-picker';
import Button from '../../button';
-// @ts-ignore
import ColorIndicator from '../../color-indicator';
-// @ts-ignore
import ColorPalette from '../../color-palette';
import Dropdown from '../../dropdown';
import { HStack } from '../../h-stack';
diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json
index 4fee27671cfc6..0fc06f1977183 100644
--- a/packages/components/tsconfig.json
+++ b/packages/components/tsconfig.json
@@ -30,6 +30,7 @@
"src/button/**/*",
"src/card/**/*",
"src/circular-option-picker/**/*",
+ "src/color-indicator/**/*",
"src/color-palette/**/*",
"src/color-picker/**/*",
"src/confirm-dialog/**/*",
From 8d1c5a128b0e6923cfad7d37232e9868f53aedef Mon Sep 17 00:00:00 2001
From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com>
Date: Fri, 18 Feb 2022 17:28:59 +1000
Subject: [PATCH 10/31] Move color indicator wrapper styles to dynamic class
---
.../border-control-dropdown/component.tsx | 13 +----
.../border-control-dropdown/hook.ts | 5 ++
.../src/border-control/stories/index.js | 1 +
.../components/src/border-control/styles.ts | 48 ++++++++++++-------
4 files changed, 38 insertions(+), 29 deletions(-)
diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx
index d17a08d2691b9..841d14a447951 100644
--- a/packages/components/src/border-control/border-control-dropdown/component.tsx
+++ b/packages/components/src/border-control/border-control-dropdown/component.tsx
@@ -33,6 +33,7 @@ const BorderControlDropdown = (
disableCustomColors,
enableAlpha,
indicatorClassName,
+ indicatorWrapperClassName,
onReset,
onColorChange,
onStyleChange,
@@ -45,16 +46,6 @@ const BorderControlDropdown = (
} = useBorderControlDropdown( props );
const { color, style } = border || {};
- const fallbackColor = !! style && style !== 'none' ? '#ddd' : undefined;
- const indicatorBorderStyles = {
- // The border style is set to `none` when border width is zero. Forcing
- // the solid style in this case maintains the positioning of the inner
- // ColorIndicator.
- borderStyle: style === 'none' ? 'solid' : style,
- // If there is no color selected but we have a style to display, apply
- // a border color anyway.
- borderColor: color || fallbackColor,
- };
const renderToggle = ( { onToggle = noop } ) => (