Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .claude/rules/frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Components are `.tsx` files. They:
element.
6. Declare `Props` in `<ComponentName>.types.ts` (extending the matching
`*HTMLAttributes` type) and export it from `index.ts`. Reuse shared unions
from `src/types` (`Color`, `Priority`, `Size`, `Layout`, `ValidationState`,
from `src/types` (`Color`, `Priority`, `Size`, `Layout`, `State`,
…); component-specific unions stay in the component directory. Every
type lives in a `.types.ts` file next to its implementation — including
context values (`<ComponentName>ContextValue`), provider props and helper
Expand Down
10 changes: 7 additions & 3 deletions src/components/CheckboxField/CheckboxField.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,20 @@
}

// States
.isRootStateChanged {
@include variants.state(changed);
}

.isRootStateInvalid {
@include variants.validation(invalid);
@include variants.state(invalid);
}

.isRootStateValid {
@include variants.validation(valid);
@include variants.state(valid);
}

.isRootStateWarning {
@include variants.validation(warning);
@include variants.state(warning);
}

// Invisible label
Expand Down
6 changes: 3 additions & 3 deletions src/components/CheckboxField/CheckboxField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useContext } from 'react';
import { withGlobalProps } from '../../providers/globalProps';
import { classNames } from '../../helpers/classNames/classNames';
import { transferProps } from '../../helpers/transferProps';
import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName';
import { getRootStateClassName } from '../_helpers/getRootStateClassName';
import { FormLayoutContext } from '../FormLayout/FormLayoutContext';
import styles from './CheckboxField.module.scss';
import type { CheckboxFieldProps } from './CheckboxField.types';
Expand All @@ -16,7 +16,7 @@ export const CheckboxField = React.forwardRef<HTMLInputElement, CheckboxFieldPro
labelPosition = 'after',
renderAsRequired = false,
required = false,
validationState,
state,
validationText,
...restProps
}, ref) => {
Expand All @@ -31,7 +31,7 @@ export const CheckboxField = React.forwardRef<HTMLInputElement, CheckboxFieldPro
labelPosition === 'before' && styles.hasRootLabelBefore,
disabled && styles.isRootDisabled,
(renderAsRequired || required) && styles.isRootRequired,
getRootValidationStateClassName(styles, validationState),
getRootStateClassName(styles, state),
)}
htmlFor={id}
id={id && `${id}__label`}
Expand Down
7 changes: 4 additions & 3 deletions src/components/CheckboxField/CheckboxField.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
} from 'react';
import type {
LabelPosition,
ValidationState,
State,
} from '../../types';

export type CheckboxFieldLabelPosition = LabelPosition;
Expand Down Expand Up @@ -53,9 +53,10 @@ export type CheckboxFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'ty
*/
required?: boolean;
/**
* Alter the field to provide feedback based on validation result.
* Alter the field to reflect the state of its data, e.g. a validation result or a change
* against the saved value.
*/
validationState?: ValidationState;
state?: State;
/**
* Validation message to be displayed.
*/
Expand Down
28 changes: 25 additions & 3 deletions src/components/CheckboxField/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ React.createElement(() => {
</>
)}
onChange={() => setAgree(!agree)}
validationState="valid"
state="valid"
/>
<CheckboxField
checked={agree}
Expand All @@ -158,7 +158,7 @@ React.createElement(() => {
</>
)}
onChange={() => setAgree(!agree)}
validationState="warning"
state="warning"
validationText={(
<>
Please wait 10 minutes until we verify your data.
Expand All @@ -178,14 +178,36 @@ React.createElement(() => {
)}
onChange={() => setAgree(!agree)}
required
validationState="invalid"
state="invalid"
validationText="You must agree to be able to proceed."
/>
</>
);
});
```

### Changed State

Changed state highlights fields whose value differs from the saved one, so users
can review their edits before saving.

A field shows one state at a time. When a changed value is also invalid, it's
up to you to pick the state that matters more to the user.

```docoff-react-preview
React.createElement(() => {
const [newsletter, setNewsletter] = React.useState(true);
return (
<CheckboxField
checked={newsletter}
label="Subscribe to newsletter"
onChange={() => setNewsletter(!newsletter)}
state="changed"
/>
);
});
```

### Required State

The required state indicates that the input is mandatory. Required fields
Expand Down
6 changes: 3 additions & 3 deletions src/components/CheckboxField/__tests__/CheckboxField.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@ test.describe('CheckboxField', () => {
...mixPropTests([
propTests.disabledPropTest,
propTests.checkedPropTest,
propTests.validationStatePropTest,
propTests.statePropTest,
]),
...mixPropTests([
propTests.checkedPropTest,
propTests.requiredPropTest,
propTests.validationStatePropTest,
propTests.statePropTest,
]),
...mixPropTests([
propTests.checkedPropTest,
propTests.renderAsRequiredPropTest,
propTests.validationStatePropTest,
propTests.statePropTest,
]),
...mixPropTests([
propTests.checkedPropTest,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
10 changes: 7 additions & 3 deletions src/components/FileInputField/FileInputField.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,20 @@
cursor: inherit;
}

.isRootStateChanged {
@include variants.state(changed);
}

.isRootStateInvalid {
@include variants.validation(invalid);
@include variants.state(invalid);
}

.isRootStateValid {
@include variants.validation(valid);
@include variants.state(valid);
}

.isRootStateWarning {
@include variants.validation(warning);
@include variants.state(warning);
}

// Invisible label
Expand Down
6 changes: 3 additions & 3 deletions src/components/FileInputField/FileInputField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { classNames } from '../../helpers/classNames';
import { transferProps } from '../../helpers/transferProps';
import { TranslationsContext } from '../../providers/translations';
import { getRootSizeClassName } from '../_helpers/getRootSizeClassName';
import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName';
import { getRootStateClassName } from '../_helpers/getRootStateClassName';
import { resolveContextOrProp } from '../_helpers/resolveContextOrProp';
import { InputGroupContext } from '../InputGroup/InputGroupContext';
import { Text } from '../Text';
Expand All @@ -42,7 +42,7 @@ export const FileInputField = React.forwardRef<FileInputFieldRef, FileInputField
onFilesChanged,
required = false,
size = 'medium',
validationState,
state,
validationText,
...restProps
}, ref) => {
Expand Down Expand Up @@ -170,7 +170,7 @@ export const FileInputField = React.forwardRef<FileInputFieldRef, FileInputField
styles,
resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
),
getRootValidationStateClassName(styles, validationState),
getRootStateClassName(styles, state),
)}
id={`${id}__root`}
onDragLeave={!disabled ? handleDragLeave : undefined}
Expand Down
7 changes: 4 additions & 3 deletions src/components/FileInputField/FileInputField.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
import type {
Layout,
Size,
ValidationState,
State,
} from '../../types';

export type FileInputFieldFilesChangedEvent = ChangeEvent<HTMLInputElement> | DragEvent<HTMLDivElement> | Event | null;
Expand Down Expand Up @@ -87,9 +87,10 @@ export type FileInputFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'i
*/
size?: Size;
/**
* Alter the field to provide feedback based on validation result.
* Alter the field to reflect the state of its data, e.g. a validation result or a change
* against the saved value.
*/
validationState?: ValidationState;
state?: State;
/**
* Validation message to be displayed.
*
Expand Down
23 changes: 20 additions & 3 deletions src/components/FileInputField/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,28 +166,45 @@ have.
id="my-file-valid"
label="Attachment"
onFilesChanged={() => {}}
validationState="valid"
state="valid"
validationText="Looks good!"
/>
<FileInputField
id="my-file-invalid"
label="Attachment"
onFilesChanged={() => {}}
validationState="invalid"
state="invalid"
validationText="Your file is too big. Please select something smaller."
/>
<FileInputField
id="my-file-warning"
label="Attachment"
onFilesChanged={() => {}}
validationState="warning"
state="warning"
validationText={`
You selected more than 10 files.
It may take some time before we process them.
`}
/>
```

### Changed State

Changed state highlights fields whose value differs from the saved one, so users
can review their edits before saving.

A field shows one state at a time. When a changed value is also invalid, it's
up to you to pick the state that matters more to the user.

```docoff-react-preview
<FileInputField
id="my-file-changed"
label="Attachment"
onFilesChanged={() => {}}
state="changed"
/>
```

### Disabled State

It's possible to disable the whole input.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test.describe('FileInputField', () => {
...propTests.defaultComponentPropTest,
...mixPropTests([
propTests.disabledPropTest,
propTests.validationStatePropTest,
propTests.statePropTest,
]),
...fileSelectedPropTest,
...mixPropTests([
Expand Down
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const fileSelectedPropTest: PropTests = [
onBeforeSnapshot,
props,
})),
...propTests.validationStatePropTest.map(({
...propTests.statePropTest.map(({
name,
props,
}) => ({
Expand Down
10 changes: 7 additions & 3 deletions src/components/FormLayout/FormLayoutCustomField.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,20 @@
}

// States
.isRootStateChanged {
@include variants.state(changed);
}

.isRootStateInvalid {
@include variants.validation(invalid);
@include variants.state(invalid);
}

.isRootStateValid {
@include variants.validation(valid);
@include variants.state(valid);
}

.isRootStateWarning {
@include variants.validation(warning);
@include variants.state(warning);
}

// Layouts
Expand Down
6 changes: 3 additions & 3 deletions src/components/FormLayout/FormLayoutCustomField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { withGlobalProps } from '../../providers/globalProps';
import { classNames } from '../../helpers/classNames/classNames';
import { transferProps } from '../../helpers/transferProps';
import { getRootSizeClassName } from '../_helpers/getRootSizeClassName';
import { getRootValidationStateClassName } from '../_helpers/getRootValidationStateClassName';
import { getRootStateClassName } from '../_helpers/getRootStateClassName';
import { isChildrenEmpty } from '../../helpers/isChildrenEmpty/isChildrenEmpty';
import { FormLayoutContext } from './FormLayoutContext';
import { FormLayoutCustomFieldContext } from './FormLayoutCustomFieldContext';
Expand Down Expand Up @@ -46,7 +46,7 @@ export const FormLayoutCustomField: React.FunctionComponent<FormLayoutCustomFiel
label,
labelForId,
required = false,
validationState,
state,
...restProps
}: FormLayoutCustomFieldProps) => {
const context = useContext(FormLayoutContext);
Expand All @@ -65,7 +65,7 @@ export const FormLayoutCustomField: React.FunctionComponent<FormLayoutCustomFiel
disabled && styles.isRootDisabled,
required && styles.isRootRequired,
getRootSizeClassName(styles, innerFieldSize),
getRootValidationStateClassName(styles, validationState),
getRootStateClassName(styles, state),
)}
id={id}
>
Expand Down
7 changes: 4 additions & 3 deletions src/components/FormLayout/FormLayoutCustomField.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
} from 'react';
import type {
Size,
ValidationState,
State,
} from '../../types';

/**
Expand Down Expand Up @@ -48,9 +48,10 @@ export type FormLayoutCustomFieldProps = HTMLAttributes<HTMLDivElement> & {
*/
required?: boolean;
/**
* Alter the field to provide feedback based on validation result.
* Alter the field to reflect the state of its data, e.g. a validation result or a change
* against the saved value.
*/
validationState?: ValidationState;
state?: State;
};

/**
Expand Down
4 changes: 2 additions & 2 deletions src/components/FormLayout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ React.createElement(() => {
<TextField
inputSize={6}
label="ZIP"
validationState="invalid"
state="invalid"
validationText="ZIP should be 5 to 6 digits long code."
/>
<FormLayoutCustomField label="Country">
Expand Down Expand Up @@ -383,7 +383,7 @@ provide labels with optional feedback style.
<FormLayout fieldLayout="horizontal" labelWidth="auto">
<FormLayoutCustomField
label="Custom field label in valid state"
validationState="valid"
state="valid"
>
<docoff-placeholder bordered>Custom field content</docoff-placeholder>
</FormLayoutCustomField>
Expand Down
Loading
Loading