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

Adds inactive state to ActionList items #3913

Merged
merged 35 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
103f1b1
adds inactive state to ActionList items
mperrotti Nov 6, 2023
cd3c8f5
adds changeset
mperrotti Nov 6, 2023
4d184d1
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 6, 2023
7f2fcfc
test(vrt): update snapshots
mperrotti Nov 6, 2023
99a297e
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 7, 2023
dbb1615
render inactive indicator in trailing viz slot if no leading viz
mperrotti Nov 7, 2023
f0c0cd3
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 9, 2023
1deb129
adds a name to inactive item indicator buttons
mperrotti Nov 9, 2023
ec24b09
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 13, 2023
5691255
test(vrt): update snapshots
mperrotti Nov 13, 2023
1a8b624
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 13, 2023
611a8d9
Update src/drafts/Tooltip/Tooltip.tsx
mperrotti Nov 14, 2023
96b57d2
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 14, 2023
769618c
updates draft Tooltip import so tests pass
mperrotti Nov 14, 2023
c0f3147
updates snapshots
mperrotti Nov 14, 2023
4de4bb0
fix inactive multiselect story
mperrotti Nov 15, 2023
5cba2a8
Merge branch 'main' of github.com:primer/react into mp/inactive-actio…
mperrotti Nov 15, 2023
854a3e2
fix inactive multiselect story's aXe violation for real this time
mperrotti Nov 15, 2023
cc05edb
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 15, 2023
883b63d
adds inactive single select story
mperrotti Nov 17, 2023
0e6496c
Merge branch 'main' of github.com:primer/react into mp/inactive-actio…
mperrotti Nov 20, 2023
fddc17f
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 21, 2023
067ca35
Merge branch 'main' of github.com:primer/react into mp/inactive-actio…
mperrotti Nov 22, 2023
f0280aa
Merge branch 'main' of github.com:primer/react into mp/inactive-actio…
mperrotti Nov 27, 2023
cb1015e
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 28, 2023
5e9f183
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 28, 2023
9ea1187
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Nov 29, 2023
6f422d7
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Dec 5, 2023
53fe35f
Merge branch 'main' of github.com:primer/react into mp/inactive-actio…
mperrotti Dec 7, 2023
f5ea511
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Dec 7, 2023
0934eb3
Merge branch 'mp/inactive-actionlist-item' of github.com:primer/react…
mperrotti Dec 7, 2023
70f89d8
Merge branch 'main' of github.com:primer/react into mp/inactive-actio…
mperrotti Dec 11, 2023
22e5b57
rms unnecessary data-inactive prop
mperrotti Dec 11, 2023
e48ddc9
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Dec 11, 2023
9ef7b72
Merge branch 'main' into mp/inactive-actionlist-item
mperrotti Dec 11, 2023
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
7 changes: 7 additions & 0 deletions .changeset/smooth-tips-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@primer/react': minor
---

Supports inactive ActionList items by letting users pass the required message to the `inactiveText` prop.

<!-- Changed components: ActionList, ActionMenu, NavList -->
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.
60 changes: 60 additions & 0 deletions e2e/components/ActionList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,4 +454,64 @@ test.describe('ActionList', () => {
})
}
})

test.describe('Inactive Item', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-actionlist-features--inactive-item',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`ActionList.Inactive Item.${theme}.png`,
)
})

test('axe @aat', async ({page}) => {
await visit(page, {
id: 'components-actionlist-features--inactive-item',
globals: {
colorScheme: theme,
},
})
await expect(page).toHaveNoViolations()
})
})
}
})

test.describe('Inactive Multiselect', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-actionlist-features--inactive-multiselect',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`ActionList.Inactive Multiselect.${theme}.png`,
)
})

test('axe @aat', async ({page}) => {
await visit(page, {
id: 'components-actionlist-features--inactive-multiselect',
globals: {
colorScheme: theme,
},
})
await expect(page).toHaveNoViolations()
})
})
}
})
})
35 changes: 35 additions & 0 deletions e2e/components/ActionMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,41 @@ test.describe('ActionMenu', () => {
}
})

test.describe('Inactive Items', () => {
for (const theme of themes) {
test.describe(theme, () => {
test('default @vrt', async ({page}) => {
await visit(page, {
id: 'components-actionmenu-features--links-and-actions',
globals: {
colorScheme: theme,
},
})

// Default state
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot(
`ActionMenu.Links And Actions.${theme}.png`,
)

// Open menu
await page.locator('button', {hasText: 'Open menu'}).waitFor()
await page.getByRole('button', {name: 'Open menu'}).click()
expect(await page.screenshot({animations: 'disabled'})).toMatchSnapshot()
})

test('axe @aat', async ({page}) => {
await visit(page, {
id: 'components-actionmenu-features--links-and-actions',
globals: {
colorScheme: theme,
},
})
await expect(page).toHaveNoViolations()
})
})
}
})

test.describe('Links And Actions', () => {
for (const theme of themes) {
test.describe(theme, () => {
Expand Down
8 changes: 7 additions & 1 deletion src/ActionList/ActionList.docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"name": "onSelect",
"type": "(event: React.MouseEvent<HTMLLIElement> | React.KeyboardEvent<HTMLLIElement>) => void",
"defaultValue": "",
"description": "Callback that is called when the item is selected using either the mouse or keyboard. `event.preventDefault()` will prevent a menu from closing when within an `<ActionMenu />`"
"description": "Callback that is called when the item is selected using either the mouse or keyboard. `event.preventDefault()` will prevent a menu from closing when within an `<ActionMenu />`. This is not called for disabled or inactive items."
},
{
"name": "selected",
Expand All @@ -82,6 +82,12 @@
"defaultValue": "false",
"description": "Items that are disabled can not be clicked, selected, or navigated to."
},
{
"name": "inactiveText",
"type": "string",
"defaultValue": "",
"description": "Text describing why the item is inactive. This may be used when an item's usual functionality is unavailable due to a system error such as a database outage."
},
{
"name": "role",
"type": "AriaRole",
Expand Down
37 changes: 37 additions & 0 deletions src/ActionList/ActionList.examples.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,43 @@ export function AllCombinations(): JSX.Element {
<StarIcon />
</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item inactiveText="Unavailable due to an outage">
L + B + T<ActionList.Description variant="block">Block description</ActionList.Description>
</ActionList.Item>
<ActionList.Item inactiveText="Unavailable due to an outage">
L + B + T<ActionList.Description variant="inline">Inline description</ActionList.Description>
</ActionList.Item>
<ActionList.Item inactiveText="Unavailable due to an outage">
<ActionList.LeadingVisual>
<StarIcon />
</ActionList.LeadingVisual>
L + I + T<ActionList.Description variant="inline">inline description</ActionList.Description>
<ActionList.TrailingVisual>
<StarIcon />
</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item inactiveText="Unavailable due to an outage">
<ActionList.LeadingVisual>
<StarIcon />
</ActionList.LeadingVisual>
L + B + T<ActionList.Description variant="block">Block description</ActionList.Description>
<ActionList.TrailingVisual>
<StarIcon />
</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item inactiveText="Unavailable due to an outage">
L + B + T<ActionList.Description variant="block">Block description</ActionList.Description>
<ActionList.TrailingVisual>
<StarIcon />
</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item inactiveText="Unavailable due to an outage">
I + B + T<ActionList.Description variant="inline">inline description</ActionList.Description>
<ActionList.Description variant="block">Block description</ActionList.Description>
<ActionList.TrailingVisual>
<StarIcon />
</ActionList.TrailingVisual>
</ActionList.Item>
<ActionList.Item variant="danger">
<ActionList.LeadingVisual>
<StarIcon />
Expand Down
47 changes: 47 additions & 0 deletions src/ActionList/ActionList.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,37 @@ export const DisabledMultiselect = () => (
</ActionList>
)

export const InactiveMultiselect = () => {
const [selectedIndices, setSelectedIndices] = React.useState<number[]>([0])
const handleSelect = (index: number) => {
if (selectedIndices.includes(index)) {
setSelectedIndices(selectedIndices.filter(i => i !== index))
} else {
setSelectedIndices([...selectedIndices, index])
}
}
return (
<ActionList selectionVariant="multiple" role="menu" aria-label="Project">
<ActionList.Item
role="menuitemcheckbox"
selected={false}
aria-checked={false}
inactiveText="Unavailable due to an outage"
>
Inactive item
</ActionList.Item>
<ActionList.Item
role="menuitemcheckbox"
selected={selectedIndices.includes(1)}
aria-checked={selectedIndices.includes(1)}
onSelect={() => handleSelect(1)}
>
Item 2
</ActionList.Item>
</ActionList>
)
}

export const DisabledItem = () => {
const [selectedIndex, setSelectedIndex] = React.useState(0)
return (
Expand All @@ -346,6 +377,22 @@ export const DisabledItem = () => {
)
}

export const InactiveItem = () => {
return (
<ActionList aria-label="Project">
{projects.map((project, index) => (
<ActionList.Item key={index} inactiveText={index === 1 ? 'Unavailable due to an outage' : undefined}>
<ActionList.LeadingVisual>
<TableIcon />
</ActionList.LeadingVisual>
{project.name}
<ActionList.Description variant="block">{project.scope}</ActionList.Description>
</ActionList.Item>
))}
</ActionList>
)
}

export const Links = () => (
<>
<Heading as="h1" id="list-heading" sx={{fontSize: 1}}>
Expand Down
6 changes: 6 additions & 0 deletions src/ActionList/ActionList.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ ItemPlayground.argTypes = {
type: 'boolean',
},
},
inactiveText: {
control: {
type: 'text',
},
},
variant: {
control: 'radio',
options: ['default', 'danger'],
Expand Down Expand Up @@ -155,6 +160,7 @@ ItemPlayground.args = {
selected: false,
active: false,
disabled: false,
inactiveText: '',
variant: 'default',
role: 'listitem',
id: 'item-1',
Expand Down
35 changes: 35 additions & 0 deletions src/ActionList/ActionList.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {render as HTMLRender, waitFor, fireEvent} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {axe} from 'jest-axe'
import React from 'react'
import theme from '../theme'
Expand Down Expand Up @@ -31,6 +32,7 @@ const projects = [
{name: 'Primer Backlog', scope: 'GitHub'},
{name: 'Primer React', scope: 'github/primer'},
{name: 'Disabled Project', scope: 'github/primer', disabled: true},
{name: 'Inactive Project', scope: 'github/primer', inactiveText: 'Unavailable due to an outage'},
]
function SingleSelectListStory(): JSX.Element {
const [selectedIndex, setSelectedIndex] = React.useState(0)
Expand All @@ -45,6 +47,7 @@ function SingleSelectListStory(): JSX.Element {
aria-selected={index === selectedIndex}
onSelect={() => setSelectedIndex(index)}
disabled={project.disabled}
inactiveText={project.inactiveText}
>
{project.name}
</ActionList.Item>
Expand Down Expand Up @@ -123,6 +126,24 @@ describe('ActionList', () => {
expect(options[2]).toHaveAttribute('aria-selected', 'false')
})

it('should skip onSelect on inactive items', async () => {
const component = HTMLRender(<SingleSelectListStory />)
const options = await waitFor(() => component.getAllByRole('option'))

expect(options[0]).toHaveAttribute('aria-selected', 'true')
expect(options[3]).toHaveAttribute('aria-selected', 'false')

fireEvent.click(options[3])

expect(options[0]).toHaveAttribute('aria-selected', 'true')
expect(options[3]).toHaveAttribute('aria-selected', 'false')

fireEvent.keyPress(options[3], {key: 'Enter', charCode: 13})

expect(options[0]).toHaveAttribute('aria-selected', 'true')
expect(options[3]).toHaveAttribute('aria-selected', 'false')
})

it('should throw when selected is provided without a selectionVariant on parent', async () => {
// we expect console.error to be called, so we suppress that in the test
const mockError = jest.spyOn(console, 'error').mockImplementation(() => jest.fn())
Expand Down Expand Up @@ -154,6 +175,20 @@ describe('ActionList', () => {
expect(option).toBeInTheDocument()
})

it('should focus the button around the leading visual when tabbing to an inactive item', async () => {
const component = HTMLRender(<SingleSelectListStory />)
const inactiveOptionButton = await waitFor(() =>
component.getByRole('button', {description: projects[3].inactiveText}),
)
const inactiveIndex = projects.findIndex(project => 'inactiveText' in project)

for (let i = 0; i < inactiveIndex; i++) {
await userEvent.tab()
}

expect(inactiveOptionButton).toHaveFocus()
})

it('should call onClick for a link item', async () => {
const onClick = jest.fn()
const component = HTMLRender(
Expand Down
Loading
Loading