Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add loading prop for Button and IconButton #3582

Merged
merged 84 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
a1c03e2
draft loading state
langermank Aug 1, 2023
cfd0944
cleanup
langermank Aug 1, 2023
9408220
add story to trigger loading
langermank Aug 1, 2023
18d7954
Merge branch 'main' into button-loading-state
langermank Sep 21, 2023
1cc0465
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Sep 22, 2023
a9a7771
merge
langermank Sep 28, 2023
5c901d4
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Oct 4, 2023
fae28dd
Merge branch 'main' into button-loading-state
langermank Oct 11, 2023
72a0247
icon button
langermank Oct 11, 2023
71b747d
Create lazy-jobs-pump.md
langermank Oct 11, 2023
556aff9
add prop for loading message
langermank Oct 11, 2023
03ce69f
Merge branch 'button-loading-state' of https://github.com/primer/reac…
langermank Oct 11, 2023
f7b316d
Merge branch 'button-loading-state' of github.com:primer/react into b…
mperrotti Oct 12, 2023
6f98511
handle no visuals loading state
langermank Oct 17, 2023
5080989
Merge branch 'button-loading-state' of github.com:primer/react into b…
mperrotti Nov 28, 2023
f3e6d07
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Nov 28, 2023
ee21a63
updates snapshots after merging from main
mperrotti Nov 28, 2023
5956393
Merge branch 'main' into button-loading-state
mperrotti Nov 28, 2023
9687486
uses unique ID for loading messages, preserves aria-describedby passe…
mperrotti Nov 28, 2023
2fc7c4d
adds Storybook examples for btn loading error message
mperrotti Nov 28, 2023
7f981b0
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Nov 28, 2023
49d8853
reverts unintentional default link underlining
mperrotti Nov 28, 2023
3aa7225
changes loadingMessage to loadingAnnouncement
mperrotti Nov 28, 2023
7f7f326
updates draft Button component with loading prop
mperrotti Nov 28, 2023
73b59c7
updates legacy button counter behavior when loading
mperrotti Nov 29, 2023
fc15b72
Merge branch 'main' into button-loading-state
mperrotti Nov 29, 2023
c277957
Revert "updates draft Button component with loading prop"
mperrotti Nov 29, 2023
d556bc9
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Dec 1, 2023
ccaa4ae
moves error behavior stories to 'Examples' section
mperrotti Dec 1, 2023
30fb01d
screenreader fixes
mperrotti Dec 1, 2023
22c7c65
adds and updates unit tests
mperrotti Dec 1, 2023
787bfaa
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Dec 1, 2023
6dcf5b3
re-updates snapshots after using correct VisuallyHidden
mperrotti Dec 1, 2023
09a3199
Merge branch 'main' into button-loading-state
mperrotti Dec 5, 2023
0e276de
documents loading props
mperrotti Dec 5, 2023
e8fcd21
adds VRTs, updates loading feature stories
mperrotti Dec 5, 2023
3eee19a
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Dec 5, 2023
e63e61c
simplifies inner visual/spinner rendering logic
mperrotti Dec 5, 2023
8a901da
removes example stories (we can put them back when Flash supports foc…
mperrotti Dec 5, 2023
39f2d4e
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Dec 5, 2023
24abbcd
Merge branch 'main' into button-loading-state
mperrotti Dec 7, 2023
ffdff03
excludes loading buttons from axe contrast check
mperrotti Dec 7, 2023
9d133a0
fixes visual regression: button counter vertical alignment
mperrotti Dec 7, 2023
e069e04
Merge branch 'main' into button-loading-state
mperrotti Dec 11, 2023
611eea0
prevents double spinners when leading and trailing visuals are passed
mperrotti Dec 11, 2023
7cf14ca
test(e2e): update story ids
joshblack Dec 13, 2023
1a4a08d
test(vrt): update snapshots
joshblack Dec 13, 2023
a091125
test(e2e): disable animations in screenshots
joshblack Dec 13, 2023
e4a6f41
Merge branch 'button-loading-state' of github.com:primer/react into b…
joshblack Dec 13, 2023
c9a0743
test(vrt): update snapshots
joshblack Dec 13, 2023
4d9128e
preserves rest state styles when button is loading
mperrotti Dec 14, 2023
3c38084
adds story for success and error announcement
mperrotti Dec 14, 2023
9408882
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Dec 14, 2023
ceb5ba2
test(vrt): update snapshots
langermank Dec 14, 2023
c7501e2
Merge branch 'main' into button-loading-state
mperrotti Dec 18, 2023
ad02496
Merge branch 'main' into button-loading-state
mperrotti Dec 18, 2023
db094c5
fixes ButtonGroup regression
mperrotti Dec 18, 2023
fe2dcee
Merge branch 'main' into button-loading-state
mperrotti Dec 19, 2023
e300738
Merge branch 'main' into button-loading-state
mperrotti Dec 19, 2023
fb6d639
Merge branch 'main' into button-loading-state
mperrotti Dec 19, 2023
0911586
delete broken snapshots
langermank Dec 19, 2023
1416fcb
also targets anchor tags in ButtonGroup styles
mperrotti Dec 19, 2023
e245fe9
Merge branch 'main' into button-loading-state
mperrotti Dec 19, 2023
5e8877e
add conditional wrapper
langermank Dec 19, 2023
f2ccc8e
Merge branch 'button-loading-state' of https://github.com/primer/reac…
langermank Dec 19, 2023
f301ffe
fix group
langermank Dec 19, 2023
e7a25ef
test(vrt): update snapshots
langermank Dec 19, 2023
71de776
Merge branch 'main' into button-loading-state
langermank Dec 19, 2023
2e5ed47
lint
mperrotti Dec 20, 2023
6c425f9
Merge branch 'button-loading-state' of github.com:primer/react into b…
mperrotti Dec 20, 2023
01e07a1
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Dec 20, 2023
20d81f4
fixes unit tests, updates snapshots
mperrotti Dec 20, 2023
b3cdce6
Update src/Button/Button.docs.json
mperrotti Dec 21, 2023
c38523c
Merge branch 'main' into button-loading-state
mperrotti Dec 21, 2023
9ad96f2
fixes 'block' layout for loading buttons
mperrotti Dec 21, 2023
7e5bc66
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Mar 12, 2024
f781434
uses new internal Status component for loading announcement
mperrotti Mar 12, 2024
6f9f6ed
updates Tooltip V2 tests to account for loading messageID in button's…
mperrotti Mar 12, 2024
8809ba5
fixes BoxProps type import to new preferred syntax
mperrotti Mar 12, 2024
9649e4e
appease the linter
mperrotti Mar 12, 2024
87fa84b
rms ConditionalWrapper
mperrotti Mar 12, 2024
5acdcb9
revert back to using ConditionalWrapper with aria-describedby
mperrotti Mar 13, 2024
c0e2a3d
Merge branch 'main' of github.com:primer/react into button-loading-state
mperrotti Mar 14, 2024
a7b114b
test(vrt): update snapshots
mperrotti Mar 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lazy-jobs-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Add `loading` state to `Button` and `IconButton`
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.
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.
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.
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.
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.
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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
142 changes: 142 additions & 0 deletions e2e/components/Button.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,148 @@ test.describe('Button', () => {
}
})

test.describe('Loading', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-button-features--loading',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(`Button.Loading.${theme}.png`)
})

test('axe @aat', async ({page}) => {
await visit(page, {
id: 'components-button-features--loading',
globals: {
colorScheme: theme,
},
})
await expect(page).toHaveNoViolations({
rules: {
'color-contrast': {
enabled: theme !== 'dark_dimmed',
},
},
})
})
})
}
})

test.describe('Loading Custom Announcement', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-button-features--loading-custom-announcement',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`Button.Loading Custom Announcement.${theme}.png`,
)
})

test('axe @aat', async ({page}) => {
await visit(page, {
id: 'components-button-features--loading-custom-announcement',
globals: {
colorScheme: theme,
},
})
await expect(page).toHaveNoViolations({
rules: {
'color-contrast': {
enabled: theme !== 'dark_dimmed',
},
},
})
})
})
}
})

test.describe('Loading With Leading Visual', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-button-features--loading-with-leading-visual',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`Button.Loading With Leading Visual.${theme}.png`,
)
})

test('axe @aat', async ({page}) => {
await visit(page, {
id: 'components-button-features--loading-with-leading-visual',
globals: {
colorScheme: theme,
},
})
await expect(page).toHaveNoViolations({
rules: {
'color-contrast': {
enabled: theme !== 'dark_dimmed',
},
},
})
})
})
}
})

test.describe('Loading With Trailing Visual', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-button-features--loading-with-trailing-visual',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`Button.Loading With Trailing Visual.${theme}.png`,
)
})

test('axe @aat', async ({page}) => {
await visit(page, {
id: 'components-button-features--loading-with-trailing-visual',
globals: {
colorScheme: theme,
},
})
await expect(page).toHaveNoViolations({
rules: {
'color-contrast': {
enabled: theme !== 'dark_dimmed',
},
},
})
})
})
}
})

test.describe('Dev: Invisible Variants', () => {
for (const theme of themes) {
test.describe(theme, () => {
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

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

35 changes: 23 additions & 12 deletions packages/react/src/Button/Button.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,9 @@
"description": "For counter buttons, the number to display."
},
{
"name": "variant",
"type": "'default'\n| 'primary'\n| 'danger'\n| 'invisible'",
"defaultValue": "'default'",
"description": "Change the visual style of the button."
},
{
"name": "size",
"type": "'small'\n| 'medium'\n| 'large'",
"defaultValue": "'medium'"
"name": "inactive",
"type": "boolean",
"description": "Whether the button looks visually disabled, but can still accept all the same interactions as an enabled button.\n This is intended to be used when a system error such as an outage prevents the button from performing its usual action.\n Inactive styles are slightly different from disabled styles because inactive buttons need to have an accessible color contrast ratio. This is because inactive buttons can have tooltips or perform an action such as opening a dialog explaining why it's inactive.\n If both `disabled` and `inactive` are true, `disabled` takes precedence."
},
{
"name": "leadingIcon",
Expand All @@ -39,6 +33,22 @@
"type": "React.ElementType",
"description": "A visual to display before the button text."
},
{
"name": "loading",
"type": "boolean",
"description": "When true, the button is in a loading state."
},
{
"name": "loadingAnnouncement",
"type": "string",
"description": "The content to announce to screen readers when loading. This requires `loading` prop to be true"
},

{
"name": "size",
"type": "'small'\n| 'medium'\n| 'large'",
"defaultValue": "'medium'"
},
{
"name": "trailingIcon",
"type": "React.ComponentType<OcticonProps>",
Expand All @@ -51,9 +61,10 @@
"description": "A visual to display after the button text."
},
{
"name": "inactive",
"type": "boolean",
"description": "Whether the button looks visually disabled, but can still accept all the same interactions as an enabled button.\n This is intended to be used when a system error such as an outage prevents the button from performing its usual action.\n Inactive styles are slightly different from disabled styles because inactive buttons need to have an accessible color contrast ratio. This is because inactive buttons can have tooltips or perform an action such as opening a dialog explaining why it's inactive.\n If both `disabled` and `inactive` are true, `disabled` takes precedence."
"name": "variant",
"type": "'default'\n| 'primary'\n| 'danger'\n| 'invisible'",
"defaultValue": "'default'",
"description": "Change the visual style of the button."
},
{
"name": "as",
Expand Down
78 changes: 78 additions & 0 deletions packages/react/src/Button/Button.examples.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react'
import type {Meta} from '@storybook/react'
import {Button} from '.'
import {DownloadIcon} from '@primer/octicons-react'
import {VisuallyHidden} from '../internal/components/VisuallyHidden'

const meta: Meta<typeof Button> = {
title: 'Components/Button/Examples',
} as Meta<typeof Button>

export default meta

export const LoadingStatusAnnouncementSuccessful = () => {
const [loading, setLoading] = React.useState(false)
const [success, setSuccess] = React.useState(false)

const resolveAction = async () => {
setLoading(true)
await new Promise(resolve => setTimeout(resolve, 1500))
setLoading(false)

return await true
}

const onClick = (resolveType: 'error' | 'success') => async () => {
const actionResult = await resolveAction()

if (resolveType === 'error') {
setSuccess(!actionResult)
return
}

setSuccess(actionResult)
}

return (
<>
<VisuallyHidden aria-live="polite">{!loading && success ? 'Export completed' : null}</VisuallyHidden>
<Button loading={loading} leadingVisual={DownloadIcon} onClick={onClick('error')}>
Export (success)
</Button>
</>
)
}

export const LoadingStatusAnnouncementError = () => {
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(false)

const resolveAction = async () => {
setLoading(true)
await new Promise(resolve => setTimeout(resolve, 1500))
setLoading(false)

return await true
}

const onClick = (resolveType: 'error' | 'success') => async () => {
const actionResult = await resolveAction()

if (resolveType === 'error') {
setError(actionResult)
return
}

setError(!actionResult)
}

return (
<>
<VisuallyHidden aria-live="polite">{!loading && error ? 'Export failed' : null}</VisuallyHidden>

<Button loading={loading} leadingVisual={DownloadIcon} onClick={onClick('error')}>
Export (error)
</Button>
</>
)
}
36 changes: 35 additions & 1 deletion packages/react/src/Button/Button.features.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {EyeIcon, TriangleDownIcon, HeartIcon} from '@primer/octicons-react'
import {EyeIcon, TriangleDownIcon, HeartIcon, DownloadIcon} from '@primer/octicons-react'
import React, {useState} from 'react'
import {Button} from '.'

Expand Down Expand Up @@ -96,3 +96,37 @@ export const Small = () => <Button size="small">Default</Button>
export const Medium = () => <Button size="medium">Default</Button>

export const Large = () => <Button size="large">Default</Button>

export const Loading = () => <Button loading>Default</Button>

export const LoadingCustomAnnouncement = () => (
<Button loading loadingAnnouncement="This is a custom loading announcement">
Default
</Button>
)

export const LoadingWithLeadingVisual = () => (
<Button loading leadingVisual={DownloadIcon}>
Export
</Button>
)

export const LoadingWithTrailingVisual = () => (
<Button loading trailingVisual={DownloadIcon}>
Export
</Button>
)

export const LoadingTrigger = () => {
const [isLoading, setIsLoading] = useState(false)

const handleClick = () => {
setIsLoading(true)
}

return (
<Button loading={isLoading} onClick={handleClick} leadingVisual={DownloadIcon}>
Export
</Button>
)
}
11 changes: 11 additions & 0 deletions packages/react/src/Button/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ Playground.argTypes = {
type: 'boolean',
},
},
loading: {
control: {
type: 'boolean',
},
},
count: {
control: {
type: 'number',
},
},
leadingVisual: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]),
trailingVisual: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]),
trailingAction: OcticonArgType([TriangleDownIcon]),
Expand All @@ -59,6 +69,7 @@ Playground.args = {
inactive: false,
variant: 'default',
alignContent: 'center',
loading: false,
trailingVisual: null,
leadingVisual: null,
trailingAction: null,
Expand Down
Loading
Loading