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
75 changes: 75 additions & 0 deletions packages/components/src/base-field/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# 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

### `error`: `boolean`
ciampo marked this conversation as resolved.
Show resolved Hide resolved

Renders an error style around the component.

### `disabled`: `boolean`

Whether the field is disabled.

### `isClickable`: `boolean`

Renders a `cursor: pointer` on hover;
ciampo marked this conversation as resolved.
Show resolved Hide resolved

### `isFocused`: `boolean`

Renders focus styles around the component.
ciampo marked this conversation as resolved.
Show resolved Hide resolved

### `isInline`: `boolean`

Renders a component that can be inlined in some text.

### `isSubtle`: `boolean`

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

/**
* `BaseField` is a primitive component used to create form element components (e.g. `TextInput`).
*/
export default createComponent( {
as: 'div',
useHook: useBaseField,
name: 'BaseField',
} );
74 changes: 74 additions & 0 deletions packages/components/src/base-field/hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* 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} [isClickable=false] Renders a `cursor: pointer` on hover.
sarayourfriend marked this conversation as resolved.
Show resolved Hide resolved
* @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,
isClickable = false,
ciampo marked this conversation as resolved.
Show resolved Hide resolved
ciampo marked this conversation as resolved.
Show resolved Hide resolved
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,
isClickable && styles.clickable,
ciampo marked this conversation as resolved.
Show resolved Hide resolved
isSubtle && styles.subtle,
hasError && styles.error,
isInline && styles.inline,
className
),
[
className,
controlGroupStyles,
hasError,
isInline,
isClickable,
ciampo marked this conversation as resolved.
Show resolved Hide resolved
isSubtle,
]
);

return {
...useFlex( { ...flexProps, className: classes } ),
disabled,
defaultValue,
};
}
3 changes: 3 additions & 0 deletions packages/components/src/base-field/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as BaseField } from './component';

export { useBaseField } from './hook';
90 changes: 90 additions & 0 deletions packages/components/src/base-field/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* 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 clickable = css`
cursor: pointer;
`;
ciampo marked this conversation as resolved.
Show resolved Hide resolved

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;
` }
`;
Loading