Skip to content

Commit

Permalink
feat(FormField): add a base for read-only support
Browse files Browse the repository at this point in the history
  • Loading branch information
JoannaSikora committed Jan 23, 2025
1 parent fa5db09 commit 0934708
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,9 @@ export const TextFieldWithError: StoryFn<FormFieldProps> = () => (
<ExampleInput error />
</FormFieldComponent>
);

export const TextFieldReadOnlyWithInput: StoryFn<FormFieldProps> = () => (
<FormFieldComponent labelText="Username" readonly>
<ExampleInput />
</FormFieldComponent>
);
166 changes: 88 additions & 78 deletions packages/react-components/src/components/FormField/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';

import cx from 'clsx';

import { ReadOnlyFormFieldContextProvider } from '../../providers/ReadOnlyFormFieldProvider';
import { FieldDescription } from '../FieldDescription';
import { FieldError } from '../FieldError';
import { Text } from '../Typography';
Expand Down Expand Up @@ -47,6 +48,10 @@ export interface FormFieldProps {
* Renders given element above the filed
*/
labelRightNode?: React.ReactNode;
/**
* Whether the form field is read-only
*/
readonly?: boolean;
}

export const FormField: React.FC<React.PropsWithChildren<FormFieldProps>> = ({
Expand All @@ -59,9 +64,11 @@ export const FormField: React.FC<React.PropsWithChildren<FormFieldProps>> = ({
labelFor,
children,
labelRightNode,
readonly = false,
}) => {
const childrenRef = React.useRef<HTMLDivElement>(null);
const [labelHeight, setLabelHeight] = React.useState('auto');

const mergedClassNames = cx(
styles[baseClass],
{
Expand All @@ -81,91 +88,94 @@ export const FormField: React.FC<React.PropsWithChildren<FormFieldProps>> = ({
});

return (
<div className={mergedClassNames}>
{labelRightNode && inline && (
<React.Fragment>
<Text
as="div"
size="sm"
className={cx(
styles[`${baseClass}__label-right-node`],
styles[`${baseClass}__label-right-node--inline`]
)}
>
{labelRightNode}
</Text>
<div className={styles[`${baseClass}__row-break`]} />
</React.Fragment>
)}
<div
className={cx(
styles[`${baseClass}__wrapper`],
inline && styles[`${baseClass}__wrapper--inline`]
<ReadOnlyFormFieldContextProvider readonly={readonly}>
<div className={mergedClassNames}>
{labelRightNode && inline && (
<React.Fragment>
<Text
as="div"
size="sm"
className={cx(
styles[`${baseClass}__label-right-node`],
styles[`${baseClass}__label-right-node--inline`]
)}
>
{labelRightNode}
</Text>
<div className={styles[`${baseClass}__row-break`]} />
</React.Fragment>
)}
>
{(labelText || labelRightNode) && (
<div
className={cx(
styles[`${baseClass}__label`],
inline && styles[`${baseClass}__label--inline`],
!labelText && styles[`${baseClass}__label--no-text`]
)}
>
{labelText && (
<div
className={cx(
styles[`${baseClass}__label-wrapper`],
inline && styles[`${baseClass}__label-wrapper--inline`]
)}
style={{
height: labelHeight,
}}
>
<label
className={styles[`${baseClass}__label-left-node`]}
htmlFor={labelFor}
<div
className={cx(
styles[`${baseClass}__wrapper`],
inline && styles[`${baseClass}__wrapper--inline`]
)}
>
{(labelText || labelRightNode) && (
<div
className={cx(
styles[`${baseClass}__label`],
inline && styles[`${baseClass}__label--inline`],
!labelText && styles[`${baseClass}__label--no-text`]
)}
>
{labelText && (
<div
className={cx(
styles[`${baseClass}__label-wrapper`],
inline && styles[`${baseClass}__label-wrapper--inline`]
)}
style={{
height: labelHeight,
}}
>
<Text as="span" size="sm">
{labelText}
</Text>
</label>
{labelAdornment && (
<Text
as="div"
size="sm"
className={cx(
styles[`${baseClass}__label-adornment`],
inline && styles[`${baseClass}__label-adornment--inline`]
)}
<label
className={styles[`${baseClass}__label-left-node`]}
htmlFor={labelFor}
>
{labelAdornment}
</Text>
)}
</div>
)}
{labelRightNode && !inline && (
<Text
as="div"
size="sm"
className={cx(styles[`${baseClass}__label-right-node`])}
<Text as="span" size="sm" bold={readonly}>
{labelText}
</Text>
</label>
{labelAdornment && (
<Text
as="div"
size="sm"
className={cx(
styles[`${baseClass}__label-adornment`],
inline &&
styles[`${baseClass}__label-adornment--inline`]
)}
>
{labelAdornment}
</Text>
)}
</div>
)}
{labelRightNode && !inline && (
<Text
as="div"
size="sm"
className={cx(styles[`${baseClass}__label-right-node`])}
>
{labelRightNode}
</Text>
)}
</div>
)}
<div className={cx(styles[`${baseClass}__content`])}>
<div ref={childrenRef}>{children}</div>
{error && <FieldError>{error}</FieldError>}
{!error && description && (
<FieldDescription
className={cx(styles[`${baseClass}__content__description`])}
>
{labelRightNode}
</Text>
{description}
</FieldDescription>
)}
</div>
)}
<div className={cx(styles[`${baseClass}__content`])}>
<div ref={childrenRef}>{children}</div>
{error && <FieldError>{error}</FieldError>}
{!error && description && (
<FieldDescription
className={cx(styles[`${baseClass}__content__description`])}
>
{description}
</FieldDescription>
)}
</div>
</div>
</div>
</ReadOnlyFormFieldContextProvider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ $base-class: 'input';
}

&--read-only {
border-color: var(--border-basic-disabled);
background-color: var(--surface-primary-disabled);
// TODO styles
border: 0;
background: none;
}

&--error,
Expand Down
5 changes: 4 additions & 1 deletion packages/react-components/src/components/Input/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@livechat/design-system-icons';
import cx from 'clsx';

import { useReadOnlyFormFieldContext } from '../../providers/ReadOnlyFormFieldProvider';
import { Button } from '../Button';
import { Icon } from '../Icon';
import { Text } from '../Typography';
Expand Down Expand Up @@ -51,6 +52,7 @@ export const InputComponent = React.forwardRef<
ref
) => {
const innerRef = React.useRef<HTMLInputElement>(null);
const { readonly } = useReadOnlyFormFieldContext();

React.useImperativeHandle(ref, () => innerRef.current!, []);
const [isFocused, setIsFocused] = React.useState(false);
Expand All @@ -63,7 +65,7 @@ export const InputComponent = React.forwardRef<
[styles[`${baseClass}--focused`]]: isFocused,
[styles[`${baseClass}--error`]]: error,
[styles[`${baseClass}--crop`]]: cropOnBlur,
[styles[`${baseClass}--read-only`]]: inputProps.readOnly,
[styles[`${baseClass}--read-only`]]: readonly || inputProps.readOnly,
},
className
);
Expand Down Expand Up @@ -91,6 +93,7 @@ export const InputComponent = React.forwardRef<
{shouldRenderLeftIcon && renderIcon(icon, disabled)}
<input
{...inputProps}
readOnly={readonly}
ref={innerRef}
onFocus={(e) => {
setIsFocused(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ReactNode, createContext, useContext } from 'react';
import type { FC } from 'react';

interface ReadOnlyFormFieldContextValue {
readonly: boolean;
}

const ReadOnlyFormFieldContext = createContext<ReadOnlyFormFieldContextValue>({
readonly: false,
});

export interface ReadOnlyFormFieldContextProps {
readonly?: boolean;
children?: ReactNode;
}

export const ReadOnlyFormFieldContextProvider: FC<
ReadOnlyFormFieldContextProps
> = ({ children, readonly = false }) => {
return (
<ReadOnlyFormFieldContext.Provider
value={{
readonly,
}}
>
{children}
</ReadOnlyFormFieldContext.Provider>
);
};

export const useReadOnlyFormFieldContext = () =>
useContext(ReadOnlyFormFieldContext);

export default ReadOnlyFormFieldContext;

0 comments on commit 0934708

Please sign in to comment.