Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

components: Add BaseField #32250

Merged
merged 11 commits into from
May 28, 2021
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,12 @@
"markdown_source": "../packages/components/src/base-control/README.md",
"parent": "components"
},
{
"title": "BaseField",
"slug": "base-field",
"markdown_source": "../packages/components/src/base-field/README.md",
"parent": "components"
},
{
"title": "BoxControl",
"slug": "box-control",
Expand Down
67 changes: 67 additions & 0 deletions packages/components/src/base-field/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# BaseField

<div class="callout callout-alert">
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
</div>

`BaseField` is an internal (i.e., not exported in the `index.js`) primitive component used for building more complex fields like `TextField`. It provides error handling and focus styles for field components. It does _not_ handle layout of the component aside from wrapping the field in a `Flex` wrapper.

## Usage

`BaseField` is primarily used as a hook rather than a component:

```js
function useExampleField( props ) {
const {
as = 'input',
...baseProps,
} = useBaseField( props );

const inputProps = {
as,
// more cool stuff here
}

return { inputProps, ...baseProps };
}

function ExampleField( props, forwardRef ) {
const {
preFix,
affix,
disabled,
inputProps,
...baseProps
} = useExampleField( props );

return (
<View { ...baseProps } disabled={ disabled }>
{preFix}
<View
autocomplete="off"
{ ...inputProps }
disabled={ disabled }
/>
{affix}
</View>
);
}
```

## Props

### `hasError`: `boolean`

Renders an error style around the component.

### `disabled`: `boolean`

Whether the field is disabled.

### `isInline`: `boolean`

Renders a component that can be inlined in some text.

### `isSubtle`: `boolean`

Renders a subtle variant of the component.
64 changes: 64 additions & 0 deletions packages/components/src/base-field/hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* External dependencies
*/
import { cx } from 'emotion';

/**
* WordPress dependencies
*/
import { useMemo } from '@wordpress/element';

/**
* Internal dependencies
*/
import { useContextSystem } from '../ui/context';
import { useControlGroupContext } from '../ui/control-group';
import { useFlex } from '../flex';
import * as styles from './styles';

/**
* @typedef OwnProps
* @property {boolean} [hasError=false] Renders an error.
* @property {boolean} [disabled] Whether the field is disabled.
* @property {boolean} [isInline=false] Renders as an inline element (layout).
* @property {boolean} [isSubtle=false] Renders a subtle variant.
*/

/** @typedef {import('../flex/types').FlexProps & OwnProps} Props */

/**
* @param {import('../ui/context').PolymorphicComponentProps<Props, 'div'>} props
*/
export function useBaseField( props ) {
const {
className,
hasError = false,
isInline = false,
isSubtle = false,
// extract these because useFlex doesn't accept it
defaultValue,
disabled,
...flexProps
} = useContextSystem( props, 'BaseField' );

const { styles: controlGroupStyles } = useControlGroupContext();

const classes = useMemo(
() =>
cx(
styles.BaseField,
controlGroupStyles,
isSubtle && styles.subtle,
hasError && styles.error,
isInline && styles.inline,
className
),
[ className, controlGroupStyles, hasError, isInline, isSubtle ]
);

return {
...useFlex( { ...flexProps, className: classes } ),
disabled,
defaultValue,
};
}
1 change: 1 addition & 0 deletions packages/components/src/base-field/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useBaseField } from './hook';
86 changes: 86 additions & 0 deletions packages/components/src/base-field/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* External dependencies
*/
import { css } from 'emotion';

/**
* Internal dependencies
*/
import { CONFIG, COLORS, reduceMotion } from '../utils';
import { safariOnly } from '../utils/browsers';

export const BaseField = css`
background: ${ CONFIG.controlBackgroundColor };
border-radius: ${ CONFIG.controlBorderRadius };
border: 1px solid;
border-color: ${ CONFIG.controlBorderColor };
box-shadow: ${ CONFIG.controlBoxShadow };
display: flex;
flex: 1;
font-size: ${ CONFIG.fontSize };
outline: none;
padding: 0 8px;
position: relative;
transition: border-color ${ CONFIG.transitionDurationFastest } ease;
${ reduceMotion( 'transition' ) }
width: 100%;

&[disabled] {
opacity: 0.6;
}

&:hover {
border-color: ${ CONFIG.controlBorderColorHover };
}

&:focus,
&[data-focused='true'] {
border-color: ${ COLORS.admin.theme };
box-shadow: ${ CONFIG.controlBoxShadowFocus };
}
`;

export const subtle = css`
background-color: transparent;

&:hover,
&:active,
&:focus,
&[data-focused='true'] {
background: ${ CONFIG.controlBackgroundColor };
}
`;

export const error = css`
border-color: ${ CONFIG.controlDestructiveBorderColor };

&:hover,
&:active {
border-color: ${ CONFIG.controlDestructiveBorderColor };
}

&:focus,
&[data-focused='true'] {
border-color: ${ CONFIG.controlDestructiveBorderColor };
box-shadow: 0 0 0, 0.5px, ${ CONFIG.controlDestructiveBorderColor };
}
`;

export const errorFocus = css`
border-color: ${ CONFIG.controlDestructiveBorderColor };
box-shadow: 0 0 0, 0.5px, ${ CONFIG.controlDestructiveBorderColor };

&:hover {
border-color: ${ CONFIG.controlDestructiveBorderColor };
}
`;

export const inline = css`
display: inline-flex;
vertical-align: baseline;
width: auto;

${ safariOnly`
vertical-align: middle;
` }
`;
143 changes: 143 additions & 0 deletions packages/components/src/base-field/test/__snapshots__/index.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`base field props should render error styles 1`] = `
Snapshot Diff:
- Received styles
+ Base styles

@@ -12,11 +12,11 @@
"-webkit-justify-content": "space-between",
"-webkit-transition": "border-color 100ms ease",
"align-items": "center",
"background": "#fff",
"border": "1px solid",
- "border-color": "#d94f4f",
+ "border-color": "#757575",
"border-radius": "2px",
"box-shadow": "transparent",
"display": "flex",
"flex": "1",
"flex-direction": "row",
`;

exports[`base field props should render inline styles 1`] = `
Snapshot Diff:
- Received styles
+ Base styles

@@ -15,18 +15,17 @@
"background": "#fff",
"border": "1px solid",
"border-color": "#757575",
"border-radius": "2px",
"box-shadow": "transparent",
- "display": "inline-flex",
+ "display": "flex",
"flex": "1",
"flex-direction": "row",
"font-size": "13px",
"justify-content": "space-between",
"outline": "none",
"padding": "0 8px",
"position": "relative",
"transition": "border-color 100ms ease",
- "vertical-align": "baseline",
- "width": "auto",
+ "width": "100%",
},
]
`;

exports[`base field props should render subtle styles 1`] = `
Snapshot Diff:
- Received styles
+ Base styles

@@ -11,11 +11,10 @@
"-webkit-flex-direction": "row",
"-webkit-justify-content": "space-between",
"-webkit-transition": "border-color 100ms ease",
"align-items": "center",
"background": "#fff",
- "background-color": "transparent",
"border": "1px solid",
"border-color": "#757575",
"border-radius": "2px",
"box-shadow": "transparent",
"display": "flex",
`;

exports[`base field should render correctly 1`] = `
.emotion-0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
width: 100%;
background: #fff;
border-radius: 2px;
border: 1px solid;
border-color: #757575;
box-shadow: transparent;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
font-size: 13px;
outline: none;
padding: 0 8px;
position: relative;
-webkit-transition: border-color 100ms ease;
transition: border-color 100ms ease;
width: 100%;
}

.emotion-0 > * + *:not(marquee) {
margin-left: calc(4px * 2);
}

.emotion-0 > * {
min-width: 0;
}

@media ( prefers-reduced-motion:reduce ) {
.emotion-0 {
-webkit-transition-duration: 0ms;
transition-duration: 0ms;
}
}

.emotion-0[disabled] {
opacity: 0.6;
}

.emotion-0:hover {
border-color: #757575;
}

.emotion-0:focus,
.emotion-0[data-focused='true'] {
border-color: var( --wp-admin-theme-color,#00669b);
box-shadow: 0 0 0,0.5px,[object Object];
}

<div
class="components-flex components-base-field emotion-0 emotion-1 emotion-2"
data-wp-c16t="true"
data-wp-component="BaseField"
/>
`;
Loading