Skip to content

Commit

Permalink
feat(many): backport new error messages from v10
Browse files Browse the repository at this point in the history
  • Loading branch information
balzss committed Nov 12, 2024
1 parent dabbe23 commit 9003d19
Show file tree
Hide file tree
Showing 53 changed files with 654 additions and 101 deletions.
35 changes: 32 additions & 3 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ on:
- v9_maintenance
workflow_dispatch:
jobs:
deploy-docs:
deploy-release:
if: startsWith(github.event.head_commit.message, 'chore(release)')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install Node 22
uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci && npm run bootstrap
- name: Set build directory and deployment path based on branch
id: set-build-dir
Expand All @@ -35,5 +40,29 @@ jobs:
folder: ${{ steps.set-build-dir.outputs.build_dir }}
branch: gh-pages
target-folder: ${{ steps.set-build-dir.outputs.deploy_dir }}
clean-exclude: pr-preview
clean-exclude: |
pr-preview
v7
v8
v9
latest
force: false
deploy-latest:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
- name: Install Node 22
uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci && npm run bootstrap
- name: Build docs-app
run: npm run build:docs
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: ./packages/__docs__/__build__
branch: gh-pages
target-folder: latest
force: false
144 changes: 144 additions & 0 deletions docs/guides/form-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
---
title: Form Errors
category: Guides
order: 7
---

# Adding Error Messages to Form Components

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:

```ts
---
type: code
---
type FormMessages = {
type:
| 'newError'
| 'error'
| 'hint'
| 'success'
| 'screenreader-only'
text: React.ReactNode
}[]
```
So a basic example would look something like this:
```ts
---
type: example
---
const PasswordExample = () => {
const [password, setPassword] = useState('')
const messages = password.length < 6
? [{type: 'newError', text: 'Password have to be at least 6 characters long!'}]
: []
return (
<TextInput
renderLabel="Password"
type="password"
messages={messages}
onChange={(event, value) => { setPassword(value) }}
/>
)
}

render(<PasswordExample/>)
```

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.

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.

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 were relying on a custom solution for this feature before, you need to remove that to avoid having double asterisks.

Here are examples with different form components:

```ts
---
type: example
---
const Example = () => {
const [showError, setShowError] = useState(true)
const [showNewError, setShowNewError] = useState(true)
const [showLongError, setShowLongError] = useState(false)
const [isRequired, setIsRequired] = useState(true)

const messages = showError
? [{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'}]
: []

const handleSettingsChange = (v) => {
setShowError(v.includes('showError'))
setShowNewError(v.includes('showNewError'))
setShowLongError(v.includes('showLongError'))
setIsRequired(v.includes('isRequired'))
}

return (
<div>
<CheckboxGroup
name="errorOptions"
description="Error message options"
onChange={handleSettingsChange}
defaultValue={['showError', 'showNewError', 'isRequired']}
>
<Checkbox label="Show error message" value="showError"/>
<Checkbox label="Use the new error type" value="showNewError" />
<Checkbox label="Use long message" value="showLongError" />
<Checkbox label="Make fields required" value="isRequired" />
</CheckboxGroup>
<div style={{display: 'flex', gap: '2rem', marginTop: '3rem', flexDirection: 'column'}}>

<TextInput renderLabel="TextInput" messages={messages} isRequired={isRequired}/>

<NumberInput renderLabel="NumberInput" messages={messages} isRequired={isRequired}/>

<TextArea messages={messages} label="TextArea" required={isRequired}/>

<Checkbox label="Checkbox" isRequired={isRequired} messages={messages}/>

<Checkbox label={`Checkbox (variant="toggle")`} variant="toggle" isRequired={isRequired} messages={messages}/>

<CheckboxGroup
name="CheckboxGroup"
messages={messages}
description="CheckboxGroup"
>
<Checkbox label="Checkbox 1" value="checkbox1"/>
<Checkbox label="Checkbox 2" value="checkbox2"/>
<Checkbox label="Checkbox 3" value="checkbox3"/>
</CheckboxGroup>

<RadioInputGroup name="radioInputGroup" description="RadioInputGroup" messages={messages} isRequired={isRequired}>
<RadioInput
label="RadioInput 1"
value="radioInput1"
/>
<RadioInput
label="RadioInput 2"
value="radioInput2"
/>
<RadioInput
label="RadioInput 3"
value="radioInput3"
/>
</RadioInputGroup>

<FileDrop messages={messages} renderLabel="FileDrop" />

<ColorPicker
label="ColorPicker"
placeholderText="Enter HEX"
isRequired={isRequired}
renderMessages={() => messages}
/>

</div>
</div>
)
}

render(<Example/>)
```
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions packages/shared-types/src/ComponentThemeVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ export type CheckboxFacadeTheme = {
iconSizeSmall: string
iconSizeMedium: string
iconSizeLarge: string
errorBorderColor: Colors['borderDanger']
}

export type ToggleFacadeTheme = {
Expand Down Expand Up @@ -343,6 +344,7 @@ export type ToggleFacadeTheme = {
labelFontSizeSmall: Typography['fontSizeSmall']
labelFontSizeMedium: Typography['fontSizeMedium']
labelFontSizeLarge: Typography['fontSizeLarge']
errorBorderColor: Colors['borderDanger']
}

export type CodeEditorTheme = {
Expand Down Expand Up @@ -584,6 +586,7 @@ export type FormFieldMessageTheme = {
fontWeight: Typography['fontWeightNormal']
fontSize: Typography['fontSizeSmall']
lineHeight: Typography['lineHeight']
errorIconMarginRight: Spacing['xxSmall']
}

export type FormFieldMessagesTheme = {
Expand Down Expand Up @@ -879,6 +882,7 @@ export type NumberInputTheme = {
mediumHeight: Forms['inputHeightMedium']
largeFontSize: Typography['fontSizeLarge']
largeHeight: Forms['inputHeightLarge']
requiredInvalidColor: Colors['textDanger']
}

export type OptionsItemTheme = {
Expand Down Expand Up @@ -1086,6 +1090,10 @@ export type RadioInputTheme = {
toggleLargeFontSize: Typography['fontSizeMedium']
}

export type RadioInputGroupTheme = {
invalidAsteriskColor: Colors['textDanger']
}

export type RangeInputTheme = {
minWidth: string | 0
handleSize: string | 0
Expand Down Expand Up @@ -1364,6 +1372,7 @@ export type TextAreaTheme = {
mediumHeight: Forms['inputHeightMedium']
largeFontSize: Typography['fontSizeLarge']
largeHeight: Forms['inputHeightLarge']
requiredInvalidColor: Colors['textDanger']
}

export type TextInputTheme = {
Expand All @@ -1388,6 +1397,7 @@ export type TextInputTheme = {
mediumHeight: Forms['inputHeightMedium']
largeFontSize: Typography['fontSizeLarge']
largeHeight: Forms['inputHeightLarge']
requiredInvalidColor: Colors['textDanger']
}

export type ToggleDetailsTheme = {
Expand Down Expand Up @@ -1744,6 +1754,7 @@ export interface ThemeVariables {
ProgressCircle: ProgressCircleTheme
RangeInput: RangeInputTheme
RadioInput: RadioInputTheme
RadioInputGroup: RadioInputGroupTheme
RatingIcon: RatingIconTheme
'Rating.Icon': RatingIconTheme
Select: SelectTheme
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-checkbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ const MyCheckbox = () => {
[npm]: https://img.shields.io/npm/v/@instructure/ui-checkbox.svg
[npm-url]: https://npmjs.com/package/@instructure/ui-checkbox
[license-badge]: https://img.shields.io/npm/l/instructure-ui.svg?style=flat-square
[license]: https://github.com/instructure/instructure-ui/blob/master/LICENSE
[license]: https://github.com/instructure/instructure-ui/blob/master/LICENSE.md
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
[coc]: https://github.com/instructure/instructure-ui/blob/master/CODE_OF_CONDUCT.md
7 changes: 6 additions & 1 deletion packages/ui-checkbox/src/Checkbox/CheckboxFacade/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type CheckboxFacadeOwnProps = {
* Visual state showing that child checkboxes are a combination of checked and unchecked
*/
indeterminate?: boolean
/**
* Indicate if the parent component (`Checkbox`) is invalid to set the style accordingly.
*/
invalid?: boolean
}

type PropKeys = keyof CheckboxFacadeOwnProps
Expand All @@ -57,7 +61,8 @@ const propTypes: PropValidators<PropKeys> = {
focused: PropTypes.bool,
hovered: PropTypes.bool,
size: PropTypes.oneOf(['small', 'medium', 'large']),
indeterminate: PropTypes.bool
indeterminate: PropTypes.bool,
invalid: PropTypes.bool
}

const allowedProps: AllowedPropKeys = [
Expand Down
6 changes: 4 additions & 2 deletions packages/ui-checkbox/src/Checkbox/CheckboxFacade/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const generateStyle = (
componentTheme: CheckboxFacadeTheme,
props: CheckboxFacadeProps
): CheckboxFacadeStyle => {
const { size, checked, focused, hovered, indeterminate } = props
const { size, checked, focused, hovered, indeterminate, invalid } = props

const isChecked = checked || indeterminate

Expand Down Expand Up @@ -87,7 +87,9 @@ const generateStyle = (
boxSizing: 'border-box',
flexShrink: 0,
transition: 'all 0.2s',
border: `${componentTheme.borderWidth} solid ${componentTheme.borderColor}`,
border: `${componentTheme.borderWidth} solid ${
invalid ? componentTheme.errorBorderColor : componentTheme.borderColor
}`,
borderRadius: componentTheme.borderRadius,
marginInlineEnd: componentTheme.marginRight,
marginInlineStart: '0',
Expand Down
1 change: 1 addition & 0 deletions packages/ui-checkbox/src/Checkbox/CheckboxFacade/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const generateComponentTheme = (theme: Theme): CheckboxFacadeTheme => {
checkedBorderColor: colors?.borderDarkest,

hoverBorderColor: colors?.borderDarkest,
errorBorderColor: colors?.borderDanger,

focusBorderColor: colors?.borderBrand,
focusBorderWidth: borders?.widthMedium,
Expand Down
11 changes: 11 additions & 0 deletions packages/ui-checkbox/src/Checkbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,14 @@ type: embed
</Figure>
</Guidelines>
```

```js
---
type: embed
---
<Guidelines>
<Figure recommendation="a11y" title="Accessibility">
<Figure.Item>Do not add business logic to `onMouseOver` or `onMouseOut` events. These events are not triggered by keyboard navigation</Figure.Item>
</Figure>
</Guidelines>
```
7 changes: 6 additions & 1 deletion packages/ui-checkbox/src/Checkbox/ToggleFacade/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type ToggleFacadeOwnProps = {
focused?: boolean
size?: 'small' | 'medium' | 'large'
labelPlacement?: 'top' | 'start' | 'end'
/**
* Indicate if the parent component (`Checkbox`) is invalid to set the style accordingly.
*/
invalid?: boolean
}

type PropKeys = keyof ToggleFacadeOwnProps
Expand All @@ -59,7 +63,8 @@ const propTypes: PropValidators<PropKeys> = {
readOnly: PropTypes.bool,
focused: PropTypes.bool,
size: PropTypes.oneOf(['small', 'medium', 'large']),
labelPlacement: PropTypes.oneOf(['top', 'start', 'end'])
labelPlacement: PropTypes.oneOf(['top', 'start', 'end']),
invalid: PropTypes.bool
}

const allowedProps: AllowedPropKeys = [
Expand Down
Loading

0 comments on commit 9003d19

Please sign in to comment.