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`

Renders an error style around the component.

### `disabled`: `boolean`

Whether the field is disabled.

### `isClickable`: `boolean`

Renders a `cursor: pointer` on hover;

### `isFocused`: `boolean`

Renders focus styles around the component.

### `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.
* @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,
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,
isSubtle && styles.subtle,
hasError && styles.error,
isInline && styles.inline,
className
),
[
className,
controlGroupStyles,
hasError,
isInline,
isClickable,
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;
`;

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