Skip to content

Commit 9b03683

Browse files
committed
feat(many): add new form field error msg style + add asterisk for required fields
1 parent e250b02 commit 9b03683

File tree

44 files changed

+615
-47
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+615
-47
lines changed

docs/guides/form-errors.md

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
title: Form Errors
3+
category: Guides
4+
order: 7
5+
---
6+
7+
# Adding Error Messages to Form Components
8+
9+
InstUI offers a range of form elements and all of them have a similar API to handle error/hint/success messages. These components use the `messages` prop with the following type definition:
10+
11+
```ts
12+
---
13+
type: code
14+
---
15+
type FormMessages = {
16+
type:
17+
| 'newError'
18+
| 'error'
19+
| 'hint'
20+
| 'success'
21+
| 'screenreader-only'
22+
text: React.ReactNode
23+
}[]
24+
```
25+
26+
So a basic example would look something like this:
27+
28+
```ts
29+
---
30+
type: example
31+
---
32+
const PasswordExample = () => {
33+
const [password, setPassword] = useState('')
34+
const messages = password.length < 6
35+
? [{type: 'newError', text: 'Password have to be at least 6 characters long!'}]
36+
: []
37+
return (
38+
<TextInput
39+
renderLabel="Password"
40+
type="password"
41+
messages={messages}
42+
onChange={(event, value) => { setPassword(value) }}
43+
/>
44+
)
45+
}
46+
47+
render(<PasswordExample/>)
48+
```
49+
50+
However you might have noticed from the type definition that a message can be `error` and `newError` type. This is due to compatibility reasons. `error` is the older type and does not meet accessibility requirements, `newError` (hance the name) is the newer and more accessible format.
51+
52+
We wanted to allow users to start using the new format without making it mandatory, but after the introductory period `newError` will be deprecated and `error` type will be changed to look and behave the same way.
53+
54+
With this update we also introduced the "required asterisk" which will display an `*` character next to field labels that are required. This update is not opt-in and will apply to **all** InstUI form components so if you we relying on a custom solution for this feature before, you need to remove that to avoid having double asterisks.
55+
56+
Here are examples with different form components:
57+
58+
```ts
59+
---
60+
type: example
61+
---
62+
const Example = () => {
63+
const [showError, setShowError] = useState(true)
64+
const [showNewError, setShowNewError] = useState(true)
65+
const [showLongError, setShowLongError] = useState(false)
66+
const [isRequired, setIsRequired] = useState(true)
67+
68+
const messages = showError
69+
? [{type: showNewError ? 'newError' : 'error', text: showLongError ? 'Long error. Lorem ipsum dolor sit amet consectetur adipisicing elit. Dignissimos voluptas, esse commodi eos facilis voluptatibus harum exercitationem. Et magni est consectetur, eveniet veniam unde! Molestiae labore libero sapiente ad ratione.' : 'Short error message'}]
70+
: []
71+
72+
const handleSettingsChange = (v) => {
73+
setShowError(v.includes('showError'))
74+
setShowNewError(v.includes('showNewError'))
75+
setShowLongError(v.includes('showLongError'))
76+
setIsRequired(v.includes('isRequired'))
77+
}
78+
79+
return (
80+
<div>
81+
<CheckboxGroup
82+
name="errorOptions"
83+
description="Error message options"
84+
onChange={handleSettingsChange}
85+
defaultValue={['showError', 'showNewError', 'isRequired']}
86+
>
87+
<Checkbox label="Show error message" value="showError"/>
88+
<Checkbox label="Use the new error type" value="showNewError" />
89+
<Checkbox label="Use long message" value="showLongError" />
90+
<Checkbox label="Make fields required" value="isRequired" />
91+
</CheckboxGroup>
92+
<div style={{display: 'flex', gap: '2rem', marginTop: '3rem', flexDirection: 'column'}}>
93+
94+
<TextInput renderLabel="TextInput" messages={messages} isRequired={isRequired}/>
95+
96+
<NumberInput renderLabel="NumberInput" messages={messages} isRequired={isRequired}/>
97+
98+
<TextArea messages={messages} label="TextArea" required={isRequired}/>
99+
100+
<Checkbox label="Checkbox" isRequired={isRequired} messages={messages}/>
101+
102+
<Checkbox label={`Checkbox (variant="toggle")`} variant="toggle" isRequired={isRequired} messages={messages}/>
103+
104+
<CheckboxGroup
105+
name="CheckboxGroup"
106+
messages={messages}
107+
description="CheckboxGroup"
108+
>
109+
<Checkbox label="Checkbox 1" value="checkbox1"/>
110+
<Checkbox label="Checkbox 2" value="checkbox2"/>
111+
<Checkbox label="Checkbox 3" value="checkbox3"/>
112+
</CheckboxGroup>
113+
114+
<RadioInputGroup name="radioInputGroup" description="RadioInputGroup" messages={messages} isRequired={isRequired}>
115+
<RadioInput
116+
label="RadioInput 1"
117+
value="radioInput1"
118+
/>
119+
<RadioInput
120+
label="RadioInput 2"
121+
value="radioInput2"
122+
/>
123+
<RadioInput
124+
label="RadioInput 3"
125+
value="radioInput3"
126+
/>
127+
</RadioInputGroup>
128+
129+
<FileDrop messages={messages} renderLabel="FileDrop" />
130+
</div>
131+
</div>
132+
)
133+
}
134+
135+
render(<Example/>)
136+
```
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* The MIT License (MIT)
3+
*
4+
* Copyright (c) 2015 - present Instructure, Inc.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
import React from 'react'
26+
import {
27+
TextInput,
28+
TextArea,
29+
NumberInput,
30+
Checkbox,
31+
CheckboxGroup,
32+
RadioInput,
33+
RadioInputGroup,
34+
FileDrop,
35+
} from '@instructure/ui'
36+
37+
function FormErrors() {
38+
const renderForms = ({messages, isRequired}) => {
39+
return (
40+
<div style={{display: 'flex', gap: '2rem', marginTop: '3rem', flexDirection: 'column'}}>
41+
<TextInput renderLabel="TextInput" messages={messages} isRequired={isRequired}/>
42+
<NumberInput renderLabel="NumberInput" messages={messages} isRequired={isRequired}/>
43+
<TextArea messages={messages} label="TextArea" required={isRequired}/>
44+
<Checkbox label="Checkbox" isRequired={isRequired} messages={messages}/>
45+
<Checkbox label={`Checkbox (variant="toggle")`} variant="toggle" isRequired={isRequired} messages={messages}/>
46+
<CheckboxGroup
47+
name="CheckboxGroup"
48+
messages={messages}
49+
description="CheckboxGroup"
50+
>
51+
<Checkbox label="Checkbox 1" value="checkbox1"/>
52+
<Checkbox label="Checkbox 2" value="checkbox2"/>
53+
<Checkbox label="Checkbox 3" value="checkbox3"/>
54+
</CheckboxGroup>
55+
<RadioInputGroup name="radioInputGroup" description="RadioInputGroup" messages={messages} isRequired={isRequired}>
56+
<RadioInput
57+
label="RadioInput 1"
58+
value="radioInput1"
59+
/>
60+
<RadioInput
61+
label="RadioInput 2"
62+
value="radioInput2"
63+
/>
64+
<RadioInput
65+
label="RadioInput 3"
66+
value="radioInput3"
67+
/>
68+
</RadioInputGroup>
69+
<FileDrop messages={messages} renderLabel="FileDrop" />
70+
</div>
71+
)
72+
}
73+
74+
const exampleMessage = [{ type: 'newError', text: 'Long error. Lorem ipsum dolor sit amet consectetur adipisicing elit. Dignissimos voluptas, esse commodi eos facilis voluptatibus harum exercitationem. Et magni est consectetur, eveniet veniam unde! Molestiae labore libero sapiente ad ratione.' }]
75+
76+
const formOptions = [
77+
{
78+
messages: exampleMessage,
79+
isRequired: true
80+
},
81+
{
82+
messages: exampleMessage,
83+
isRequired: false
84+
},
85+
{
86+
messages: [],
87+
isRequired: true
88+
},
89+
{
90+
messages: [],
91+
isRequired: false
92+
},
93+
]
94+
return (
95+
<div>
96+
{formOptions.map((option) => renderForms(option))}
97+
</div>
98+
)
99+
}
100+
101+
export default FormErrors

packages/__examples__/.storybook/stories/stories.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { renderPage } from './renderPage'
3535
// @ts-ignore TODO figure out why this is an error
3636
import propJSONData from '../../prop-data.json'
3737
import TooltipPositioning from './TooltipPositioning'
38+
import FormErrors from './FormErrors'
3839
import SourceCodeEditorExamples from './SourceCodeEditorExamples'
3940

4041
type AdditionalExample = {
@@ -68,6 +69,15 @@ const additionalExamples: AdditionalExample[] = [
6869
}
6970
]
7071
},
72+
{
73+
title: 'Form errors',
74+
stories: [
75+
{
76+
storyName: 'Form errors',
77+
storyFn: () => FormErrors()
78+
}
79+
]
80+
},
7181
// TODO: try to fix the editor not rendering fully on chromatic screenshot,
7282
// even with delay
7383
{

packages/shared-types/src/ComponentThemeVariables.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ export type CheckboxFacadeTheme = {
316316
iconSizeSmall: string
317317
iconSizeMedium: string
318318
iconSizeLarge: string
319+
errorBorderColor: Colors['contrasts']['red4570']
319320
}
320321

321322
export type ToggleFacadeTheme = {
@@ -343,6 +344,7 @@ export type ToggleFacadeTheme = {
343344
labelFontSizeSmall: Typography['fontSizeSmall']
344345
labelFontSizeMedium: Typography['fontSizeMedium']
345346
labelFontSizeLarge: Typography['fontSizeLarge']
347+
errorBorderColor: Colors['contrasts']['red4570']
346348
}
347349

348350
export type CodeEditorTheme = {
@@ -584,6 +586,7 @@ export type FormFieldMessageTheme = {
584586
fontWeight: Typography['fontWeightNormal']
585587
fontSize: Typography['fontSizeSmall']
586588
lineHeight: Typography['lineHeight']
589+
errorIconMarginRight: Spacing['xxSmall']
587590
}
588591

589592
export type FormFieldMessagesTheme = {
@@ -879,6 +882,7 @@ export type NumberInputTheme = {
879882
mediumHeight: Forms['inputHeightMedium']
880883
largeFontSize: Typography['fontSizeLarge']
881884
largeHeight: Forms['inputHeightLarge']
885+
requiredInvalidColor: Colors['contrasts']['red5782']
882886
}
883887

884888
export type OptionsItemTheme = {
@@ -1365,6 +1369,7 @@ export type TextAreaTheme = {
13651369
mediumHeight: Forms['inputHeightMedium']
13661370
largeFontSize: Typography['fontSizeLarge']
13671371
largeHeight: Forms['inputHeightLarge']
1372+
requiredInvalidColor: Colors['contrasts']['red5782']
13681373
}
13691374

13701375
export type TextInputTheme = {
@@ -1389,6 +1394,7 @@ export type TextInputTheme = {
13891394
mediumHeight: Forms['inputHeightMedium']
13901395
largeFontSize: Typography['fontSizeLarge']
13911396
largeHeight: Forms['inputHeightLarge']
1397+
requiredInvalidColor: Colors['contrasts']['red5782']
13921398
}
13931399

13941400
export type ToggleDetailsTheme = {
@@ -1659,6 +1665,10 @@ export type ViewTheme = {
16591665
borderStyle: string
16601666
}
16611667

1668+
export type RadioInputGroupTheme = {
1669+
invalidAsteriskColor: Colors['contrasts']['red5782']
1670+
}
1671+
16621672
export interface ThemeVariables {
16631673
Avatar: AvatarTheme
16641674
Alert: AlertTheme
@@ -1797,4 +1807,5 @@ export interface ThemeVariables {
17971807
TruncateText: TruncateTextTheme
17981808
ContextView: ContextViewTheme
17991809
View: ViewTheme
1810+
RadioInputGroup: RadioInputGroupTheme
18001811
}

packages/ui-checkbox/src/Checkbox/CheckboxFacade/props.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ type CheckboxFacadeOwnProps = {
4040
* Visual state showing that child checkboxes are a combination of checked and unchecked
4141
*/
4242
indeterminate?: boolean
43+
/**
44+
* Indicate if the parent component (`Checkbox`) is invalid to set the style accordingly.
45+
*/
46+
invalid?: boolean
4347
}
4448

4549
type PropKeys = keyof CheckboxFacadeOwnProps
@@ -57,7 +61,8 @@ const propTypes: PropValidators<PropKeys> = {
5761
focused: PropTypes.bool,
5862
hovered: PropTypes.bool,
5963
size: PropTypes.oneOf(['small', 'medium', 'large']),
60-
indeterminate: PropTypes.bool
64+
indeterminate: PropTypes.bool,
65+
invalid: PropTypes.bool
6166
}
6267

6368
const allowedProps: AllowedPropKeys = [

packages/ui-checkbox/src/Checkbox/CheckboxFacade/styles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const generateStyle = (
3939
componentTheme: CheckboxFacadeTheme,
4040
props: CheckboxFacadeProps
4141
): CheckboxFacadeStyle => {
42-
const { size, checked, focused, hovered, indeterminate } = props
42+
const { size, checked, focused, hovered, indeterminate, invalid } = props
4343

4444
const isChecked = checked || indeterminate
4545

@@ -87,7 +87,7 @@ const generateStyle = (
8787
boxSizing: 'border-box',
8888
flexShrink: 0,
8989
transition: 'all 0.2s',
90-
border: `${componentTheme.borderWidth} solid ${componentTheme.borderColor}`,
90+
border: `${componentTheme.borderWidth} solid ${invalid ? componentTheme.errorBorderColor : componentTheme.borderColor}`,
9191
borderRadius: componentTheme.borderRadius,
9292
marginInlineEnd: componentTheme.marginRight,
9393
marginInlineStart: '0',

packages/ui-checkbox/src/Checkbox/CheckboxFacade/theme.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const generateComponentTheme = (theme: Theme): CheckboxFacadeTheme => {
4848
color: colors?.contrasts?.white1010,
4949
borderWidth: borders?.widthSmall,
5050
borderColor: colors?.contrasts?.grey1214,
51+
errorBorderColor: colors?.ui?.textError,
5152
borderRadius: borders?.radiusMedium,
5253
background: colors?.contrasts?.white1010,
5354
marginRight: spacing?.xSmall,

packages/ui-checkbox/src/Checkbox/ToggleFacade/props.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ type ToggleFacadeOwnProps = {
3939
focused?: boolean
4040
size?: 'small' | 'medium' | 'large'
4141
labelPlacement?: 'top' | 'start' | 'end'
42+
/**
43+
* Indicate if the parent component (`Checkbox`) is invalid to set the style accordingly.
44+
*/
45+
invalid?: boolean
4246
}
4347

4448
type PropKeys = keyof ToggleFacadeOwnProps
@@ -59,7 +63,8 @@ const propTypes: PropValidators<PropKeys> = {
5963
readOnly: PropTypes.bool,
6064
focused: PropTypes.bool,
6165
size: PropTypes.oneOf(['small', 'medium', 'large']),
62-
labelPlacement: PropTypes.oneOf(['top', 'start', 'end'])
66+
labelPlacement: PropTypes.oneOf(['top', 'start', 'end']),
67+
invalid: PropTypes.bool
6368
}
6469

6570
const allowedProps: AllowedPropKeys = [

0 commit comments

Comments
 (0)