diff --git a/packages/clay-css/src/scss/cadmin/variables/_forms.scss b/packages/clay-css/src/scss/cadmin/variables/_forms.scss index d7cf64feb5..de3f19ce81 100644 --- a/packages/clay-css/src/scss/cadmin/variables/_forms.scss +++ b/packages/clay-css/src/scss/cadmin/variables/_forms.scss @@ -129,6 +129,59 @@ $cadmin-input-palette: map-deep-merge( white-space: nowrap, width: min-content, ), + '.form-control-fit-content': ( + max-width: 100%, + '.form-control': ( + display: inline-flex, + width: auto, + ), + '.form-control-inset': ( + margin: 0, + min-height: 0, + min-width: 50px, + width: auto, + '&:empty:before': ( + color: $cadmin-secondary, + content: unquote("'\\FEFF' attr(placeholder)"), + cursor: text, + pointer-events: none, + ), + '&:focus:empty:before': ( + content: none, + ), + ), + '.form-control-hidden': ( + width: 0, + ), + ), + '.inline-text-input': ( + '.form-control': ( + background-color: transparent, + border-color: transparent, + font-size: 28px, + font-weight: $cadmin-font-weight-semi-bold, + line-height: 1, + padding: 5px 0 5px 16px, + '&:hover': ( + border-color: $cadmin-secondary-l3, + ), + ), + '.form-control-inset': ( + padding-right: 16px, + '&:focus ~ .form-control-indicator': ( + display: none, + ), + '&[contenteditable="false"] ~ .form-control-indicator': ( + display: none, + ), + ), + '.form-control-item': ( + padding-right: 16px, + ), + '.lexicon-icon': ( + font-size: 16px, + ), + ), ), $cadmin-input-palette ); @@ -421,9 +474,10 @@ $cadmin-form-control-inset: map-deep-merge( margin-bottom: $cadmin-form-control-inset-margin-y, margin-left: 8px, margin-top: $cadmin-form-control-inset-margin-y, + max-width: 100%, min-height: $cadmin-form-control-inset-min-height, + min-width: 50px, padding: 0, - width: 50px, focus: ( outline: 0, ), @@ -840,6 +894,14 @@ $cadmin-has-error: map-deep-merge( ), ), ), + '.inline-text-input': ( + '.form-control': ( + border-color: $cadmin-input-danger-border-color, + ), + '.form-control-indicator': ( + display: none, + ), + ), ), $cadmin-has-error ); @@ -1010,6 +1072,14 @@ $cadmin-has-warning: map-deep-merge( ), ), ), + '.inline-text-input': ( + '.form-control': ( + border-color: $cadmin-input-warning-border-color, + ), + '.form-control-indicator': ( + display: none, + ), + ), ), $cadmin-has-warning ); @@ -1180,6 +1250,14 @@ $cadmin-has-success: map-deep-merge( ), ), ), + '.inline-text-input': ( + '.form-control': ( + border-color: $cadmin-input-success-border-color, + ), + '.form-control-indicator': ( + display: none, + ), + ), ), $cadmin-has-success ); diff --git a/packages/clay-css/src/scss/mixins/_forms.scss b/packages/clay-css/src/scss/mixins/_forms.scss index 302b382496..3598c00605 100644 --- a/packages/clay-css/src/scss/mixins/_forms.scss +++ b/packages/clay-css/src/scss/mixins/_forms.scss @@ -607,8 +607,11 @@ $_focus-within: map-get($map, focus-within); @if ($_focus-within) { - &:focus-within:has(input:focus) { - @include clay-form-control-variant($_focus-within); + &:focus-within { + &:has(input:focus), + &:has([contenteditable]:focus) { + @include clay-form-control-variant($_focus-within); + } } } @@ -644,6 +647,14 @@ } } + $_readonly: map-get($map, readonly); + + @if ($_readonly) { + &[readonly] { + @include clay-form-control-variant($_readonly); + } + } + // Disabled // HTML5 says that controls under a fieldset > legend:first-child won't be // disabled if the fieldset is disabled. Due to implementation difficulty, we @@ -700,6 +711,22 @@ } } + @each $key, $value in $map { + @if not clay-is-map-unset($value) { + $selector: if( + starts-with($key, '.') or starts-with($key, '#'), + $key, + false + ); + + @if ($selector) { + #{$selector} { + @include clay-map-to-css($value); + } + } + } + } + @include clay-generate-media-breakpoints($map); } } @@ -1605,6 +1632,22 @@ } } } + + @each $key, $value in $map { + @if not clay-is-map-unset($value) { + $selector: if( + starts-with($key, '.') or starts-with($key, '#'), + $key, + false + ); + + @if ($selector) { + #{$selector} { + @include clay-map-to-css($value); + } + } + } + } } } } diff --git a/packages/clay-css/src/scss/variables/_forms.scss b/packages/clay-css/src/scss/variables/_forms.scss index f165126241..685cbf2575 100644 --- a/packages/clay-css/src/scss/variables/_forms.scss +++ b/packages/clay-css/src/scss/variables/_forms.scss @@ -127,6 +127,59 @@ $input-palette: map-deep-merge( white-space: nowrap, width: min-content, ), + '.form-control-fit-content': ( + max-width: 100%, + '.form-control': ( + display: inline-flex, + width: auto, + ), + '.form-control-inset': ( + margin: 0, + min-height: 0, + min-width: 50px, + width: auto, + '&:empty:before': ( + color: $secondary, + content: unquote("'\\FEFF' attr(placeholder)"), + cursor: text, + pointer-events: none, + ), + '&:focus:empty:before': ( + content: none, + ), + ), + '.form-control-hidden': ( + width: 0, + ), + ), + '.inline-text-input': ( + '.form-control': ( + background-color: transparent, + border-color: transparent, + font-size: 1.75rem, + font-weight: $font-weight-semi-bold, + line-height: 1, + padding: 0.3125rem 0 0.3125rem 1rem, + '&:hover': ( + border-color: $secondary-l3, + ), + ), + '.form-control-inset': ( + padding-right: 1rem, + '&:focus ~ .form-control-indicator': ( + display: none, + ), + '&[contenteditable="false"] ~ .form-control-indicator': ( + display: none, + ), + ), + '.form-control-item': ( + padding-right: 1rem, + ), + '.lexicon-icon': ( + font-size: 1rem, + ), + ), ), $input-palette ); @@ -468,9 +521,10 @@ $form-control-inset: map-deep-merge( margin-bottom: $form-control-inset-margin-y, margin-left: 0.25rem, margin-top: $form-control-inset-margin-y, + max-width: 100%, min-height: $form-control-inset-min-height, + min-width: 50px, padding: 0, - width: 50px, focus: ( outline: 0, ), @@ -922,6 +976,14 @@ $has-error: map-deep-merge( ), ), ), + '.inline-text-input': ( + '.form-control': ( + border-color: $input-danger-border-color, + ), + '.form-control-indicator': ( + display: none, + ), + ), ), $has-error ); @@ -1075,6 +1137,14 @@ $has-warning: map-deep-merge( ), ), ), + '.inline-text-input': ( + '.form-control': ( + border-color: $input-warning-border-color, + ), + '.form-control-indicator': ( + display: none, + ), + ), ), $has-warning ); @@ -1228,6 +1298,14 @@ $has-success: map-deep-merge( ), ), ), + '.inline-text-input': ( + '.form-control': ( + border-color: $input-success-border-color, + ), + '.form-control-indicator': ( + display: none, + ), + ), ), $has-success ); diff --git a/packages/clay-form/docs/form.mdx b/packages/clay-form/docs/form.mdx index 82a0decd17..7af858016d 100644 --- a/packages/clay-form/docs/form.mdx +++ b/packages/clay-form/docs/form.mdx @@ -41,6 +41,53 @@ export default function App() { } ``` +## Inline Text Input + +A text input that grows along with its content. + +```jsx preview +import {Provider} from '@clayui/core'; +import Form, {ClayInput} from '@clayui/form'; +import React, {useState} from 'react'; + +import '@clayui/css/lib/css/atlas.css'; + +export default function App() { + return ( + +
+
+ + + + + + + + + + + +
+
+
+ ); +} +``` + ## Validation `.has-error`, `.has-success` and `.has-warning` classes are used in `ClayForm.Group` to identifier validation state of the form group items. diff --git a/packages/clay-form/src/Input.tsx b/packages/clay-form/src/Input.tsx index 0e0c672130..3b98c9112e 100644 --- a/packages/clay-form/src/Input.tsx +++ b/packages/clay-form/src/Input.tsx @@ -3,6 +3,8 @@ * SPDX-License-Identifier: BSD-3-Clause */ +import ClayIcon from '@clayui/icon'; +import {InternalDispatch, useControlledState} from '@clayui/shared'; import classNames from 'classnames'; import React from 'react'; @@ -155,6 +157,123 @@ const GroupInsetItem = React.forwardRef< GroupInsetItem.displayName = 'ClayInputGroupInsetItem'; +interface IInlineTextProps + extends React.HTMLAttributes { + /** + * The initial value of the input (uncontrolled). + */ + defaultInputValue?: string; + + /** + * The id of the component + */ + id?: string; + + /** + * The current value of the input (controlled). + */ + inputValue?: string; + + /** + * Callback called when input value changes (controlled). + */ + onInputValueChange?: InternalDispatch; + + /** + * Text that hints at the expected data type to be entered. + */ + placeholder?: string; + + /** + * A flag that sets the component as noneditable. + */ + readOnly?: boolean; + + /** + * Path to the spritemap that Icon should use when referencing symbols. + */ + spritemap?: string; +} + +const InlineText = React.forwardRef( + ( + { + children, + className, + defaultValue, + onValueChange, + placeholder, + spritemap, + value: externalValue, + ...otherProps + }: IInlineTextProps, + ref: React.Ref + ) => { + const inputRef = React.useRef(null); + + const [value = '', setValue] = useControlledState({ + defaultName: 'defaultValue', + defaultValue: defaultValue, + handleName: 'onValueChange', + name: 'value', + onChange: onValueChange, + value: externalValue, + }); + + const initialValue = value; + + return ( +
+
+ { + inputRef.current?.focus(); + }} + type="text" + value={value} + {...otherProps} + /> + { + if (value.trim() === '') { + const input = event.target as HTMLElement; + + input.innerHTML = ''; + } + }} + onInput={(event) => { + const input = event.target as HTMLElement; + + setValue(input.innerText); + }} + placeholder={placeholder} + ref={inputRef} + role="textbox" + suppressContentEditableWarning={true} + > + {defaultValue} + + + + + {children} +
+
+ ); + } +); + +InlineText.displayName = 'ClayInlineText'; + interface IProps extends React.InputHTMLAttributes { /** * Input component to render. Can either be a string like 'input' or 'textarea' or a component. @@ -183,6 +302,7 @@ interface IForwardRef GroupInsetItem: typeof GroupInsetItem; GroupItem: typeof GroupItem; GroupText: typeof GroupText; + InlineText: typeof InlineText; } function forwardRef(component: React.RefForwardingComponent) { @@ -221,5 +341,6 @@ Input.Group = Group; Input.GroupInsetItem = GroupInsetItem; Input.GroupItem = GroupItem; Input.GroupText = GroupText; +Input.InlineText = InlineText; export default Input; diff --git a/packages/clay-form/src/__tests__/__snapshots__/index.tsx.snap b/packages/clay-form/src/__tests__/__snapshots__/index.tsx.snap index 200b0449be..3dde9d751d 100644 --- a/packages/clay-form/src/__tests__/__snapshots__/index.tsx.snap +++ b/packages/clay-form/src/__tests__/__snapshots__/index.tsx.snap @@ -39,12 +39,56 @@ exports[`ClayForm Input.Group renders 1`] = ` /> `; +exports[`ClayForm Input.GroupInsetItem renders 1`] = ` +
+`; + exports[`ClayForm Input.GroupItem renders 1`] = `
`; +exports[`ClayForm Input.InlineText renders 1`] = ` +
+
+ + + + + + + +
+
+`; + exports[`ClayForm Text renders 1`] = `
{ expect(testRenderer.toJSON()).toMatchSnapshot(); }); + + it('Input.GroupInsetItem renders', () => { + const testRenderer = TestRenderer.create(); + + expect(testRenderer.toJSON()).toMatchSnapshot(); + }); + + it('Input.InlineText renders', () => { + const testRenderer = TestRenderer.create(); + + expect(testRenderer.toJSON()).toMatchSnapshot(); + }); }); diff --git a/packages/clay-form/stories/Input.stories.tsx b/packages/clay-form/stories/Input.stories.tsx index 07171b28e0..5e869ed158 100644 --- a/packages/clay-form/stories/Input.stories.tsx +++ b/packages/clay-form/stories/Input.stories.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: BSD-3-Clause */ +import classNames from 'classnames'; import React from 'react'; import {ClayInput} from '../src'; @@ -86,3 +87,32 @@ Textarea.args = { readOnly: false, sizing: undefined, }; + +export const InlineText = (args: any) => ( +
+ + + + +
+); + +InlineText.args = { + hasError: false, + hasSuccess: false, + hasWarning: false, + readonly: false, +};