Skip to content

Commit 60b3253

Browse files
authored
Revert "Revert "Add loading prop for Button and IconButton (#3582)" (#4…"
This reverts commit c01901f.
1 parent cb5bf80 commit 60b3253

File tree

89 files changed

+1114
-292
lines changed

Some content is hidden

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

89 files changed

+1114
-292
lines changed

.changeset/lazy-jobs-pump.md

Lines changed: 5 additions & 0 deletions

e2e/components/Button.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,148 @@ test.describe('Button', () => {
479479
}
480480
})
481481

482+
test.describe('Loading', () => {
483+
for (const theme of themes) {
484+
test.describe(theme, () => {
485+
test('default @vrt', async ({page}) => {
486+
await visit(page, {
487+
id: 'components-button-features--loading',
488+
globals: {
489+
colorScheme: theme,
490+
},
491+
})
492+
493+
// Default state
494+
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(`Button.Loading.${theme}.png`)
495+
})
496+
497+
test('axe @aat', async ({page}) => {
498+
await visit(page, {
499+
id: 'components-button-features--loading',
500+
globals: {
501+
colorScheme: theme,
502+
},
503+
})
504+
await expect(page).toHaveNoViolations({
505+
rules: {
506+
'color-contrast': {
507+
enabled: theme !== 'dark_dimmed',
508+
},
509+
},
510+
})
511+
})
512+
})
513+
}
514+
})
515+
516+
test.describe('Loading Custom Announcement', () => {
517+
for (const theme of themes) {
518+
test.describe(theme, () => {
519+
test('default @vrt', async ({page}) => {
520+
await visit(page, {
521+
id: 'components-button-features--loading-custom-announcement',
522+
globals: {
523+
colorScheme: theme,
524+
},
525+
})
526+
527+
// Default state
528+
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
529+
`Button.Loading Custom Announcement.${theme}.png`,
530+
)
531+
})
532+
533+
test('axe @aat', async ({page}) => {
534+
await visit(page, {
535+
id: 'components-button-features--loading-custom-announcement',
536+
globals: {
537+
colorScheme: theme,
538+
},
539+
})
540+
await expect(page).toHaveNoViolations({
541+
rules: {
542+
'color-contrast': {
543+
enabled: theme !== 'dark_dimmed',
544+
},
545+
},
546+
})
547+
})
548+
})
549+
}
550+
})
551+
552+
test.describe('Loading With Leading Visual', () => {
553+
for (const theme of themes) {
554+
test.describe(theme, () => {
555+
test('default @vrt', async ({page}) => {
556+
await visit(page, {
557+
id: 'components-button-features--loading-with-leading-visual',
558+
globals: {
559+
colorScheme: theme,
560+
},
561+
})
562+
563+
// Default state
564+
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
565+
`Button.Loading With Leading Visual.${theme}.png`,
566+
)
567+
})
568+
569+
test('axe @aat', async ({page}) => {
570+
await visit(page, {
571+
id: 'components-button-features--loading-with-leading-visual',
572+
globals: {
573+
colorScheme: theme,
574+
},
575+
})
576+
await expect(page).toHaveNoViolations({
577+
rules: {
578+
'color-contrast': {
579+
enabled: theme !== 'dark_dimmed',
580+
},
581+
},
582+
})
583+
})
584+
})
585+
}
586+
})
587+
588+
test.describe('Loading With Trailing Visual', () => {
589+
for (const theme of themes) {
590+
test.describe(theme, () => {
591+
test('default @vrt', async ({page}) => {
592+
await visit(page, {
593+
id: 'components-button-features--loading-with-trailing-visual',
594+
globals: {
595+
colorScheme: theme,
596+
},
597+
})
598+
599+
// Default state
600+
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
601+
`Button.Loading With Trailing Visual.${theme}.png`,
602+
)
603+
})
604+
605+
test('axe @aat', async ({page}) => {
606+
await visit(page, {
607+
id: 'components-button-features--loading-with-trailing-visual',
608+
globals: {
609+
colorScheme: theme,
610+
},
611+
})
612+
await expect(page).toHaveNoViolations({
613+
rules: {
614+
'color-contrast': {
615+
enabled: theme !== 'dark_dimmed',
616+
},
617+
},
618+
})
619+
})
620+
})
621+
}
622+
})
623+
482624
test.describe('Dev: Invisible Variants', () => {
483625
for (const theme of themes) {
484626
test.describe(theme, () => {

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react/src/Button/Button.docs.json

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,9 @@
1919
"description": "For counter buttons, the number to display."
2020
},
2121
{
22-
"name": "variant",
23-
"type": "'default'\n| 'primary'\n| 'danger'\n| 'invisible'",
24-
"defaultValue": "'default'",
25-
"description": "Change the visual style of the button."
26-
},
27-
{
28-
"name": "size",
29-
"type": "'small'\n| 'medium'\n| 'large'",
30-
"defaultValue": "'medium'"
22+
"name": "inactive",
23+
"type": "boolean",
24+
"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."
3125
},
3226
{
3327
"name": "leadingIcon",
@@ -40,6 +34,22 @@
4034
"type": "React.ElementType",
4135
"description": "A visual to display before the button text."
4236
},
37+
{
38+
"name": "loading",
39+
"type": "boolean",
40+
"description": "When true, the button is in a loading state."
41+
},
42+
{
43+
"name": "loadingAnnouncement",
44+
"type": "string",
45+
"description": "The content to announce to screen readers when loading. This requires `loading` prop to be true"
46+
},
47+
48+
{
49+
"name": "size",
50+
"type": "'small'\n| 'medium'\n| 'large'",
51+
"defaultValue": "'medium'"
52+
},
4353
{
4454
"name": "trailingIcon",
4555
"type": "React.ComponentType<OcticonProps>",
@@ -52,9 +62,10 @@
5262
"description": "A visual to display after the button text."
5363
},
5464
{
55-
"name": "inactive",
56-
"type": "boolean",
57-
"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."
65+
"name": "variant",
66+
"type": "'default'\n| 'primary'\n| 'danger'\n| 'invisible'",
67+
"defaultValue": "'default'",
68+
"description": "Change the visual style of the button."
5869
},
5970
{
6071
"name": "as",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react'
2+
import type {Meta} from '@storybook/react'
3+
import {Button} from '.'
4+
import {DownloadIcon} from '@primer/octicons-react'
5+
import {VisuallyHidden} from '../internal/components/VisuallyHidden'
6+
7+
const meta: Meta<typeof Button> = {
8+
title: 'Components/Button/Examples',
9+
} as Meta<typeof Button>
10+
11+
export default meta
12+
13+
export const LoadingStatusAnnouncementSuccessful = () => {
14+
const [loading, setLoading] = React.useState(false)
15+
const [success, setSuccess] = React.useState(false)
16+
17+
const resolveAction = async () => {
18+
setLoading(true)
19+
await new Promise(resolve => setTimeout(resolve, 1500))
20+
setLoading(false)
21+
22+
return await true
23+
}
24+
25+
const onClick = (resolveType: 'error' | 'success') => async () => {
26+
const actionResult = await resolveAction()
27+
28+
if (resolveType === 'error') {
29+
setSuccess(!actionResult)
30+
return
31+
}
32+
33+
setSuccess(actionResult)
34+
}
35+
36+
return (
37+
<>
38+
<VisuallyHidden aria-live="polite">{!loading && success ? 'Export completed' : null}</VisuallyHidden>
39+
<Button loading={loading} leadingVisual={DownloadIcon} onClick={onClick('error')}>
40+
Export (success)
41+
</Button>
42+
</>
43+
)
44+
}
45+
46+
export const LoadingStatusAnnouncementError = () => {
47+
const [loading, setLoading] = React.useState(false)
48+
const [error, setError] = React.useState(false)
49+
50+
const resolveAction = async () => {
51+
setLoading(true)
52+
await new Promise(resolve => setTimeout(resolve, 1500))
53+
setLoading(false)
54+
55+
return await true
56+
}
57+
58+
const onClick = (resolveType: 'error' | 'success') => async () => {
59+
const actionResult = await resolveAction()
60+
61+
if (resolveType === 'error') {
62+
setError(actionResult)
63+
return
64+
}
65+
66+
setError(!actionResult)
67+
}
68+
69+
return (
70+
<>
71+
<VisuallyHidden aria-live="polite">{!loading && error ? 'Export failed' : null}</VisuallyHidden>
72+
73+
<Button loading={loading} leadingVisual={DownloadIcon} onClick={onClick('error')}>
74+
Export (error)
75+
</Button>
76+
</>
77+
)
78+
}

packages/react/src/Button/Button.features.stories.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {EyeIcon, TriangleDownIcon, HeartIcon} from '@primer/octicons-react'
1+
import {EyeIcon, TriangleDownIcon, HeartIcon, DownloadIcon} from '@primer/octicons-react'
22
import React, {useState} from 'react'
33
import {Button} from '.'
44

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

9898
export const Large = () => <Button size="large">Default</Button>
99+
100+
export const Loading = () => <Button loading>Default</Button>
101+
102+
export const LoadingCustomAnnouncement = () => (
103+
<Button loading loadingAnnouncement="This is a custom loading announcement">
104+
Default
105+
</Button>
106+
)
107+
108+
export const LoadingWithLeadingVisual = () => (
109+
<Button loading leadingVisual={DownloadIcon}>
110+
Export
111+
</Button>
112+
)
113+
114+
export const LoadingWithTrailingVisual = () => (
115+
<Button loading trailingVisual={DownloadIcon}>
116+
Export
117+
</Button>
118+
)
119+
120+
export const LoadingTrigger = () => {
121+
const [isLoading, setIsLoading] = useState(false)
122+
123+
const handleClick = () => {
124+
setIsLoading(true)
125+
}
126+
127+
return (
128+
<Button loading={isLoading} onClick={handleClick} leadingVisual={DownloadIcon}>
129+
Export
130+
</Button>
131+
)
132+
}

packages/react/src/Button/Button.stories.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ Playground.argTypes = {
4848
type: 'boolean',
4949
},
5050
},
51+
loading: {
52+
control: {
53+
type: 'boolean',
54+
},
55+
},
56+
count: {
57+
control: {
58+
type: 'number',
59+
},
60+
},
5161
leadingVisual: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]),
5262
trailingVisual: OcticonArgType([EyeClosedIcon, EyeIcon, SearchIcon, XIcon, HeartIcon]),
5363
trailingAction: OcticonArgType([TriangleDownIcon]),
@@ -59,6 +69,7 @@ Playground.args = {
5969
inactive: false,
6070
variant: 'default',
6171
alignContent: 'center',
72+
loading: false,
6273
trailingVisual: null,
6374
leadingVisual: null,
6475
trailingAction: null,

0 commit comments

Comments
 (0)