{
single?: boolean;
/** If true, the option list will be hidden if there is only one option and it is an exact match to the input value. */
hideIfExactMatch?: boolean;
+ /**
+ * Set the text to display with read-only state when there is no data. Default to 'No data'
+ */
+ noDataFallbackText?: string;
}
diff --git a/packages/react-components/src/components/Checkbox/Checkbox.module.scss b/packages/react-components/src/components/Checkbox/Checkbox.module.scss
index e54e7ee46..ec3e80440 100644
--- a/packages/react-components/src/components/Checkbox/Checkbox.module.scss
+++ b/packages/react-components/src/components/Checkbox/Checkbox.module.scss
@@ -98,6 +98,27 @@
}
}
+ &--read-only {
+ .checkbox {
+ &__input {
+ border-color: var(--border-basic-secondary);
+ background-color: var(--surface-primary-default);
+
+ &::after {
+ background-color: var(--content-basic-primary);
+ }
+ }
+
+ &__label,
+ &__input,
+ &__helper,
+ &__text {
+ cursor: default;
+ pointer-events: none;
+ }
+ }
+ }
+
&--selected {
&:hover {
.checkbox__input:checked + .checkbox__square {
diff --git a/packages/react-components/src/components/Checkbox/Checkbox.stories.tsx b/packages/react-components/src/components/Checkbox/Checkbox.stories.tsx
index 3dcb0c7c1..9f8fa6a84 100644
--- a/packages/react-components/src/components/Checkbox/Checkbox.stories.tsx
+++ b/packages/react-components/src/components/Checkbox/Checkbox.stories.tsx
@@ -38,6 +38,9 @@ export const States = (): React.ReactElement => (
Checkbox label
+
+ Checkbox label
+
@@ -46,6 +49,9 @@ export const States = (): React.ReactElement => (
Checkbox label
+
+ Checkbox label
+
(
>
Checkbox label
+
+ Checkbox label
+
>
);
diff --git a/packages/react-components/src/components/Checkbox/Checkbox.tsx b/packages/react-components/src/components/Checkbox/Checkbox.tsx
index 5d294663a..2b8473625 100644
--- a/packages/react-components/src/components/Checkbox/Checkbox.tsx
+++ b/packages/react-components/src/components/Checkbox/Checkbox.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import cx from 'clsx';
+import { useReadOnlyFormFieldContext } from '../../providers/ReadOnlyFormFieldProvider';
import { FieldDescription } from '../FieldDescription';
import { Text } from '../Typography';
@@ -12,6 +13,10 @@ export interface CheckboxProps extends React.HTMLAttributes
{
* Specify whether the checkbox should be disabled
*/
disabled?: boolean;
+ /**
+ * Specify whether the checkbox should be read only
+ */
+ readOnly?: boolean;
/**
* Specify whether the checkbox should be checked
*/
@@ -41,12 +46,16 @@ export const Checkbox = React.forwardRef(
},
ref
) => {
+ const { readOnly } = useReadOnlyFormFieldContext();
+ const computedReadOnly = readOnly || restInputProps.readOnly;
+
return (
);
};
+
+export const FormField: React.FC> = ({
+ readOnly,
+ ...props
+}) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/react-components/src/components/Input/Input.module.scss b/packages/react-components/src/components/Input/Input.module.scss
index 1fc294b39..e2958b012 100644
--- a/packages/react-components/src/components/Input/Input.module.scss
+++ b/packages/react-components/src/components/Input/Input.module.scss
@@ -81,11 +81,6 @@ $base-class: 'input';
}
}
- &--read-only {
- border-color: var(--border-basic-disabled);
- background-color: var(--surface-primary-disabled);
- }
-
&--error,
&--error:hover {
border-color: var(--border-basic-negative);
diff --git a/packages/react-components/src/components/Input/Input.stories.tsx b/packages/react-components/src/components/Input/Input.stories.tsx
index b2a01c16b..f18fcdc34 100644
--- a/packages/react-components/src/components/Input/Input.stories.tsx
+++ b/packages/react-components/src/components/Input/Input.stories.tsx
@@ -63,6 +63,9 @@ export const States = (): React.ReactElement => (
+
+
+
>
);
@@ -135,6 +138,13 @@ export const InputPromoStates = (): React.ReactElement => (
+
+
+
>
);
diff --git a/packages/react-components/src/components/Input/Input.tsx b/packages/react-components/src/components/Input/Input.tsx
index d9c9a3126..c77fffb93 100644
--- a/packages/react-components/src/components/Input/Input.tsx
+++ b/packages/react-components/src/components/Input/Input.tsx
@@ -6,8 +6,10 @@ import {
} from '@livechat/design-system-icons';
import cx from 'clsx';
+import { useReadOnlyFormFieldContext } from '../../providers/ReadOnlyFormFieldProvider';
import { Button } from '../Button';
import { Icon } from '../Icon';
+import { ReadOnlyText } from '../ReadOnlyText';
import { Text } from '../Typography';
import {
@@ -46,11 +48,14 @@ export const InputComponent = React.forwardRef<
mainClassName,
isPromo = false,
cropOnBlur = true,
+ noDataFallbackText = 'No data',
...inputProps
},
ref
) => {
const innerRef = React.useRef(null);
+ const { readOnly } = useReadOnlyFormFieldContext();
+ const computedReadOnly = readOnly || inputProps.readOnly;
React.useImperativeHandle(ref, () => innerRef.current!, []);
const [isFocused, setIsFocused] = React.useState(false);
@@ -63,7 +68,6 @@ export const InputComponent = React.forwardRef<
[styles[`${baseClass}--focused`]]: isFocused,
[styles[`${baseClass}--error`]]: error,
[styles[`${baseClass}--crop`]]: cropOnBlur,
- [styles[`${baseClass}--read-only`]]: inputProps.readOnly,
},
className
);
@@ -79,6 +83,15 @@ export const InputComponent = React.forwardRef<
innerRef.current?.focus();
};
+ if (computedReadOnly) {
+ return (
+
+ );
+ }
+
return (
& {
disabled?: boolean;
noControls?: boolean;
onChange: (value: string) => void;
+ noDataFallbackText?: string;
};
export const NumericInput: React.FC<
@@ -33,9 +36,12 @@ export const NumericInput: React.FC<
noControls,
style,
onChange,
+ noDataFallbackText = 'No data',
...restProps
}) => {
const inputRef = React.useRef(null);
+ const { readOnly } = useReadOnlyFormFieldContext();
+ const computedReadOnly = readOnly || restProps.readOnly;
const mergedClassNames = cx(
styles[baseClass],
@@ -111,6 +117,15 @@ export const NumericInput: React.FC<
return updateValue(-1);
};
+ if (computedReadOnly) {
+ return (
+
+ );
+ }
+
return (
(
disabled
/>
+
+
+
diff --git a/packages/react-components/src/components/Picker/Picker.tsx b/packages/react-components/src/components/Picker/Picker.tsx
index 3b7016903..ffc277e0f 100644
--- a/packages/react-components/src/components/Picker/Picker.tsx
+++ b/packages/react-components/src/components/Picker/Picker.tsx
@@ -3,6 +3,9 @@ import * as React from 'react';
import { FloatingNode, FloatingPortal } from '@floating-ui/react';
import cx from 'clsx';
+import { useReadOnlyFormFieldContext } from '../../providers/ReadOnlyFormFieldProvider';
+import { ReadOnlyText } from '../ReadOnlyText';
+
import { PickerList } from './components/PickerList';
import { PickerTrigger } from './components/PickerTrigger';
import { PickerTriggerBody } from './components/PickerTriggerBody';
@@ -44,6 +47,7 @@ export const Picker: React.FC
= ({
useClickHookProps,
virtuosoProps,
inputProps,
+ noDataFallbackText = 'No data',
...props
}) => {
const [open, setOpen] = React.useState(openedOnInit);
@@ -51,6 +55,8 @@ export const Picker: React.FC = ({
const isControlled = isVisible !== undefined;
const isOpen = isControlled ? isVisible : open;
const inputRef = React.useRef(null);
+ const { readOnly } = useReadOnlyFormFieldContext();
+ const computedReadOnly = readOnly || inputProps?.readOnly;
const handleVisibilityChange = (newValue: boolean, event?: Event) => {
if (newValue) {
@@ -111,6 +117,18 @@ export const Picker: React.FC = ({
searchPhrase,
});
+ if (computedReadOnly) {
+ return (
+ (!option.groupHeader ? option.name : null))
+ .filter(Boolean)
+ .join(', ')}
+ noDataFallbackText={noDataFallbackText}
+ />
+ );
+ }
+
return (
| Record;
+ /**
+ * Set the text to display with read-only state when there is no data. Default to 'No data'
+ */
+ noDataFallbackText?: string;
}
diff --git a/packages/react-components/src/components/RadioButton/RadioButton.module.scss b/packages/react-components/src/components/RadioButton/RadioButton.module.scss
index 1f23c6c85..05eb3eaa0 100644
--- a/packages/react-components/src/components/RadioButton/RadioButton.module.scss
+++ b/packages/react-components/src/components/RadioButton/RadioButton.module.scss
@@ -59,9 +59,25 @@
cursor: default;
}
- &--selected {
+ &--read-only {
+ cursor: default;
+ pointer-events: none;
+
.radio-button__circle {
- border-color: var(--action-primary-default);
+ border-color: var(--content-basic-primary);
+ background-color: var(--surface-primary-default);
+ }
+
+ .radio-button__inner-circle {
+ background-color: var(--content-basic-primary);
+ }
+ }
+
+ &--selected {
+ &:not(.radio-button--read-only) {
+ .radio-button__circle {
+ border-color: var(--action-primary-default);
+ }
}
.radio-button__inner-circle {
diff --git a/packages/react-components/src/components/RadioButton/RadioButton.stories.tsx b/packages/react-components/src/components/RadioButton/RadioButton.stories.tsx
index 2561e8861..6fbf5ba68 100644
--- a/packages/react-components/src/components/RadioButton/RadioButton.stories.tsx
+++ b/packages/react-components/src/components/RadioButton/RadioButton.stories.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
import { Meta } from '@storybook/react';
+import { StoryDescriptor } from '../../stories/components/StoryDescriptor';
+
import {
RadioButton as RadioButtonComponent,
RadioButtonProps,
@@ -33,3 +35,24 @@ RadioButton.args = {
description: 'Help text',
children: 'Radio label',
};
+
+export const RadioButtonReadOnly = ({
+ children,
+ ...args
+}: RadioButtonProps): React.ReactElement => (
+ <>
+
+
+ {children}
+
+
+
+ {children}
+
+ >
+);
+
+RadioButtonReadOnly.args = {
+ ...RadioButton.args,
+ readOnly: true,
+};
diff --git a/packages/react-components/src/components/RadioButton/RadioButton.tsx b/packages/react-components/src/components/RadioButton/RadioButton.tsx
index 606966c3e..802cdb3f3 100644
--- a/packages/react-components/src/components/RadioButton/RadioButton.tsx
+++ b/packages/react-components/src/components/RadioButton/RadioButton.tsx
@@ -2,6 +2,7 @@ import * as React from 'react';
import cx from 'clsx';
+import { useReadOnlyFormFieldContext } from '../../providers/ReadOnlyFormFieldProvider';
import { FieldDescription } from '../FieldDescription';
import { Text } from '../Typography';
@@ -12,6 +13,7 @@ export interface RadioButtonProps
description?: React.ReactNode;
checked?: boolean;
disabled?: boolean;
+ readOnly?: boolean;
}
const baseClass = 'radio-button';
@@ -21,9 +23,13 @@ export const RadioButton = React.forwardRef(
{ children, className = '', description, checked, disabled, ...props },
ref
) => {
+ const { readOnly } = useReadOnlyFormFieldContext();
+ const computedReadOnly = readOnly || props.readOnly;
+
const mergedClassNames = cx(styles[baseClass], className, {
[styles[`${baseClass}--selected`]]: checked,
[styles[`${baseClass}--disabled`]]: disabled,
+ [styles[`${baseClass}--read-only`]]: computedReadOnly,
});
return (
diff --git a/packages/react-components/src/components/ReadOnlyText/ReadOnlyText.tsx b/packages/react-components/src/components/ReadOnlyText/ReadOnlyText.tsx
new file mode 100644
index 000000000..d74e2ba74
--- /dev/null
+++ b/packages/react-components/src/components/ReadOnlyText/ReadOnlyText.tsx
@@ -0,0 +1,38 @@
+import { useEffect } from 'react';
+
+import { useReadOnlyFormFieldContext } from '../../providers/ReadOnlyFormFieldProvider';
+import { Text } from '../Typography';
+
+export interface ReadOnlyTextProps {
+ /**
+ * The value to display
+ */
+ value?: string;
+ /**
+ * Text to show when no value is provided
+ */
+ noDataFallbackText?: string;
+}
+
+export const ReadOnlyText = ({
+ value,
+ noDataFallbackText = 'No data',
+}: ReadOnlyTextProps) => {
+ const { setIsEmpty } = useReadOnlyFormFieldContext();
+
+ useEffect(() => {
+ setIsEmpty(!value);
+ }, [value, setIsEmpty]);
+
+ return (
+
+ {value || noDataFallbackText}
+
+ );
+};
diff --git a/packages/react-components/src/components/ReadOnlyText/index.ts b/packages/react-components/src/components/ReadOnlyText/index.ts
new file mode 100644
index 000000000..fa4b890d9
--- /dev/null
+++ b/packages/react-components/src/components/ReadOnlyText/index.ts
@@ -0,0 +1,2 @@
+export { ReadOnlyText } from './ReadOnlyText';
+export type { ReadOnlyTextProps } from './ReadOnlyText';
diff --git a/packages/react-components/src/components/TagInput/TagInput.stories.tsx b/packages/react-components/src/components/TagInput/TagInput.stories.tsx
index c67266682..ab2dca039 100644
--- a/packages/react-components/src/components/TagInput/TagInput.stories.tsx
+++ b/packages/react-components/src/components/TagInput/TagInput.stories.tsx
@@ -99,3 +99,9 @@ export const Sizes = (): React.ReactElement => (
>
);
+
+export const ReadOnlyTagInput = ({ ...args }) => {
+ const [tags, setTags] = React.useState(['tag1', 'tag2']);
+
+ return ;
+};
diff --git a/packages/react-components/src/components/TagInput/TagInput.tsx b/packages/react-components/src/components/TagInput/TagInput.tsx
index 9831f0034..69117cbf1 100644
--- a/packages/react-components/src/components/TagInput/TagInput.tsx
+++ b/packages/react-components/src/components/TagInput/TagInput.tsx
@@ -2,8 +2,10 @@ import * as React from 'react';
import cx from 'clsx';
+import { useReadOnlyFormFieldContext } from '../../providers/ReadOnlyFormFieldProvider';
import { KeyCodes } from '../../utils/keyCodes';
import { FieldError } from '../FieldError';
+import { ReadOnlyText } from '../ReadOnlyText';
import { Text } from '../Typography';
import { EditableTag } from './components/EditableTag';
@@ -34,6 +36,8 @@ export const TagInput = ({
inputClassName,
onBlur,
addOnBlur = true,
+ readOnly,
+ noDataFallbackText = 'No data',
...props
}: TagInputProps): React.ReactElement => {
const mergedClassNames = cx(
@@ -51,6 +55,8 @@ export const TagInput = ({
const [inputValue, setInputValue] = React.useState('');
const inputRef = React.useRef(null);
+ const { readOnly: readOnlyContext } = useReadOnlyFormFieldContext();
+ const computedReadOnly = readOnlyContext || readOnly;
const addTag = (value: string) => {
if (value.trim() !== '') {
@@ -113,6 +119,19 @@ export const TagInput = ({
onChange([...(tags || []), ...newTags] as T[]);
};
+ const getTagValue = (tag: T): string => {
+ if (typeof tag === 'string') return tag;
+ return tag.value || (typeof tag.children === 'string' ? tag.children : '');
+ };
+ if (computedReadOnly) {
+ return (
+
+ );
+ }
+
const renderTag = (tag: T, index: number) => {
if (typeof tag === 'string') {
return (
diff --git a/packages/react-components/src/components/TagInput/types.ts b/packages/react-components/src/components/TagInput/types.ts
index 79125ef88..bd212dcf3 100644
--- a/packages/react-components/src/components/TagInput/types.ts
+++ b/packages/react-components/src/components/TagInput/types.ts
@@ -43,6 +43,10 @@ export interface TagInputProps
* Add Tag on blur
*/
addOnBlur?: boolean;
+ /**
+ * Set the text to display with read-only state when there is no data. Default to 'No data'
+ */
+ noDataFallbackText?: string;
}
export type TagInputValues =
diff --git a/packages/react-components/src/components/Textarea/Textarea.stories.tsx b/packages/react-components/src/components/Textarea/Textarea.stories.tsx
index 75be22f69..0187b8647 100644
--- a/packages/react-components/src/components/Textarea/Textarea.stories.tsx
+++ b/packages/react-components/src/components/Textarea/Textarea.stories.tsx
@@ -40,5 +40,8 @@ export const States = (): React.ReactElement => (
placeholder={placeholderText}
/>
+
+
+
>
);
diff --git a/packages/react-components/src/components/Textarea/Textarea.tsx b/packages/react-components/src/components/Textarea/Textarea.tsx
index 824d7b01a..f2da47127 100644
--- a/packages/react-components/src/components/Textarea/Textarea.tsx
+++ b/packages/react-components/src/components/Textarea/Textarea.tsx
@@ -2,6 +2,8 @@ import * as React from 'react';
import cx from 'clsx';
+import { useReadOnlyFormFieldContext } from '../../providers/ReadOnlyFormFieldProvider';
+import { ReadOnlyText } from '../ReadOnlyText';
import { Text } from '../Typography';
import styles from './Textarea.module.scss';
@@ -12,11 +14,17 @@ export interface TextareaProps
extends React.TextareaHTMLAttributes {
className?: string;
error?: boolean;
+ noDataFallbackText?: string;
}
export const Textarea = React.forwardRef(
- ({ className, error, ...textareaProps }, ref) => {
+ (
+ { className, noDataFallbackText = 'No data', error, ...textareaProps },
+ ref
+ ) => {
const { disabled, onBlur, onFocus } = textareaProps;
+ const { readOnly } = useReadOnlyFormFieldContext();
+ const computedReadOnly = readOnly || textareaProps.readOnly;
const [isFocused, setIsFocused] = React.useState(false);
const mergedClassNames = cx(className, styles[baseClass], {
[styles[`${baseClass}--disabled`]]: disabled,
@@ -38,6 +46,15 @@ export const Textarea = React.forwardRef(
onFocus?.(e);
};
+ if (computedReadOnly) {
+ return (
+
+ );
+ }
+
return (