From 51f1140398701a2b936bbd9efaee6657034a6021 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Wed, 26 May 2021 12:09:57 -0700 Subject: [PATCH] components: Add `BaseField` --- docs/manifest.json | 6 ++ packages/components/src/base-field/README.md | 75 ++++++++++++++ .../components/src/base-field/component.js | 14 +++ packages/components/src/base-field/hook.js | 74 ++++++++++++++ packages/components/src/base-field/index.js | 3 + packages/components/src/base-field/styles.js | 99 +++++++++++++++++++ .../test/__snapshots__/index.js.snap | 77 +++++++++++++++ .../components/src/base-field/test/index.js | 16 +++ packages/components/src/utils/browsers.js | 51 ++++++++++ .../components/src/utils/config-values.js | 7 ++ 10 files changed, 422 insertions(+) create mode 100644 packages/components/src/base-field/README.md create mode 100644 packages/components/src/base-field/component.js create mode 100644 packages/components/src/base-field/hook.js create mode 100644 packages/components/src/base-field/index.js create mode 100644 packages/components/src/base-field/styles.js create mode 100644 packages/components/src/base-field/test/__snapshots__/index.js.snap create mode 100644 packages/components/src/base-field/test/index.js create mode 100644 packages/components/src/utils/browsers.js diff --git a/docs/manifest.json b/docs/manifest.json index 27aaef43b208b..ac735f7c9e4b4 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", diff --git a/packages/components/src/base-field/README.md b/packages/components/src/base-field/README.md new file mode 100644 index 0000000000000..32b4e3a849ddd --- /dev/null +++ b/packages/components/src/base-field/README.md @@ -0,0 +1,75 @@ +# BaseField + +
+This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. +
+ +`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 ( + + {preFix} + + {affix} + + ); +} +``` + +## 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. diff --git a/packages/components/src/base-field/component.js b/packages/components/src/base-field/component.js new file mode 100644 index 0000000000000..1fb0fcacb612b --- /dev/null +++ b/packages/components/src/base-field/component.js @@ -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', +} ); diff --git a/packages/components/src/base-field/hook.js b/packages/components/src/base-field/hook.js new file mode 100644 index 0000000000000..ea8e106555d98 --- /dev/null +++ b/packages/components/src/base-field/hook.js @@ -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} [error=false] Renders an error. + * @property {boolean} [disabled] Whether the field is disabled. + * @property {boolean} [isClickable=false] Renders a `cursor: pointer` on hover. + * @property {boolean} [isFocused=false] Renders focus styles. + * @property {boolean} [isInline=false] Renders as an inline element (layout). + * @property {boolean} [isSubtle=false] Renders a subtle variant. + */ + +/** @typedef {import('../Flex/useFlex').FlexProps & OwnProps} Props */ + +/** + * @param {import('@wp-g2/create-styles').ViewOwnProps} props + */ +export function useBaseField( props ) { + const { + className, + error = false, + isClickable = false, + isFocused = false, + isInline = false, + isSubtle = false, + // eslint-disable-next-line no-unused-vars + defaultValue, // extract this because useFlex doesn't accept it + ...flexProps + } = useContextSystem( props, 'BaseField' ); + + const { styles: controlGroupStyles } = useControlGroupContext(); + + const classes = useMemo( + () => + cx( + styles.BaseField, + controlGroupStyles, + isClickable && styles.clickable, + isFocused && styles.focus, + isSubtle && styles.subtle, + error && styles.error, + error && isFocused && styles.errorFocus, + isInline && styles.inline, + className + ), + [ + className, + controlGroupStyles, + error, + isInline, + isClickable, + isFocused, + isSubtle, + ] + ); + + return useFlex( { ...flexProps, className: classes } ); +} diff --git a/packages/components/src/base-field/index.js b/packages/components/src/base-field/index.js new file mode 100644 index 0000000000000..6ceae5a2da465 --- /dev/null +++ b/packages/components/src/base-field/index.js @@ -0,0 +1,3 @@ +export { default as BaseField } from './component'; + +export { useBaseField } from './hook'; diff --git a/packages/components/src/base-field/styles.js b/packages/components/src/base-field/styles.js new file mode 100644 index 0000000000000..1856aa2b91c27 --- /dev/null +++ b/packages/components/src/base-field/styles.js @@ -0,0 +1,99 @@ +/** + * 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 }; + box-shadow: ${ CONFIG.controlBoxShadowFocus }; + } +`; + +export const clickable = css` + cursor: pointer; +`; + +export const focus = css` + border-color: ${ COLORS.admin }; + box-shadow: ${ CONFIG.controlBoxShadowFocus }; + + &:hover { + border-color: ${ COLORS.admin }; + } +`; + +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; + ` } +`; diff --git a/packages/components/src/base-field/test/__snapshots__/index.js.snap b/packages/components/src/base-field/test/__snapshots__/index.js.snap new file mode 100644 index 0000000000000..1466895943f24 --- /dev/null +++ b/packages/components/src/base-field/test/__snapshots__/index.js.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`props 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: theme:var( --wp-admin-theme-color,#00669b); + theme-dark10: var( --wp-admin-theme-color-darker-10,#007cba); + box-shadow: 0 0 0,0.5px,[object Object]; +} + +
+`; diff --git a/packages/components/src/base-field/test/index.js b/packages/components/src/base-field/test/index.js new file mode 100644 index 0000000000000..713d4a4f4efbd --- /dev/null +++ b/packages/components/src/base-field/test/index.js @@ -0,0 +1,16 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { BaseField } from '../index'; + +describe( 'props', () => { + test( 'should render correctly', () => { + const { container } = render( ); + expect( container.firstChild ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/components/src/utils/browsers.js b/packages/components/src/utils/browsers.js new file mode 100644 index 0000000000000..f8e3cae9b4629 --- /dev/null +++ b/packages/components/src/utils/browsers.js @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import { css } from 'emotion'; + +/* eslint-disable jsdoc/no-undefined-types */ +/** + * @param {TemplateStringsArray} strings + * @param {import('create-emotion').Interpolation[]} interpolations + */ +export function ieOnly( strings, ...interpolations ) { + const interpolatedStyles = css( strings, ...interpolations ); + + return css` + @media screen and ( -ms-high-contrast: active ), + ( -ms-high-contrast: none ) { + ${ interpolatedStyles }; + } + `; +} + +/** + * @param {TemplateStringsArray} strings + * @param {import('create-emotion').Interpolation[]} interpolations + */ +export function firefoxOnly( strings, ...interpolations ) { + const interpolatedStyles = css( strings, ...interpolations ); + + return css` + @-moz-document url-prefix() { + ${ interpolatedStyles }; + } + `; +} + +/** + * @param {TemplateStringsArray} strings + * @param {import('create-emotion').Interpolation[]} interpolations + */ +export function safariOnly( strings, ...interpolations ) { + const interpolatedStyles = css( strings, ...interpolations ); + + return css` + @media not all and ( min-resolution: 0.001dpcm ) { + @supports ( -webkit-appearance: none ) { + ${ interpolatedStyles } + } + } + `; +} +/* eslint-enable jsdoc/no-undefined-types */ diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js index 9275354b8987e..359c391cf2556 100644 --- a/packages/components/src/utils/config-values.js +++ b/packages/components/src/utils/config-values.js @@ -34,6 +34,13 @@ export default { fontWeight: 'normal', fontWeightHeading: '600', gridBase: '4px', + controlBackgroundColor: COLORS.white, + controlBorderRadius: '2px', + controlBorderColor: COLORS.gray[ 700 ], + controlBoxShadow: 'transparent', + controlBorderColorHover: COLORS.gray[ 700 ], + controlBoxShadowFocus: `0 0 0, 0.5px, ${ COLORS.admin }`, + controlDestructiveBorderColor: COLORS.alert.red, controlHeight: CONTROL_HEIGHT, controlHeightLarge: `calc( ${ CONTROL_HEIGHT } * 1.2 )`, controlHeightSmall: `calc( ${ CONTROL_HEIGHT } * 0.8 )`,