|
1 | 1 | import React, {useState} from 'react' |
2 | 2 | import {render, cleanup, fireEvent, waitFor} from '@testing-library/react' |
3 | 3 | import '@testing-library/jest-dom' |
| 4 | +import userEvent from '@testing-library/user-event' |
4 | 5 | import {axe, toHaveNoViolations} from 'jest-axe' |
5 | 6 |
|
6 | 7 | import {ActionMenu} from './ActionMenu' |
@@ -111,6 +112,81 @@ describe('ActionMenu', () => { |
111 | 112 | ) |
112 | 113 | }) |
113 | 114 |
|
| 115 | + it("should forward the user's onClick callback and toggle the menu", () => { |
| 116 | + const mockOnClick = jest.fn() |
| 117 | + const {getByRole, queryByLabelText} = render( |
| 118 | + <ActionMenu> |
| 119 | + <ActionMenu.Button onClick={mockOnClick}>Open menu</ActionMenu.Button> |
| 120 | + <ActionMenu.Overlay aria-label="Actions"> |
| 121 | + <ActionMenu.Item value="Copy link">Copy link</ActionMenu.Item> |
| 122 | + </ActionMenu.Overlay> |
| 123 | + </ActionMenu>, |
| 124 | + ) |
| 125 | + |
| 126 | + fireEvent.click(getByRole('button', {name: 'Open menu'})) |
| 127 | + |
| 128 | + expect(mockOnClick).toHaveBeenCalledTimes(1) |
| 129 | + expect(queryByLabelText('Actions')).toBeInTheDocument() |
| 130 | + }) |
| 131 | + |
| 132 | + it("should not toggle the menu when the user's onClick callback prevents the default action", () => { |
| 133 | + const mockOnClick = jest.fn(event => event.preventDefault()) |
| 134 | + const {getByRole, queryByLabelText} = render( |
| 135 | + <ActionMenu> |
| 136 | + <ActionMenu.Button onClick={mockOnClick}>Open menu</ActionMenu.Button> |
| 137 | + <ActionMenu.Overlay aria-label="Actions"> |
| 138 | + <ActionMenu.Item value="Copy link">Copy link</ActionMenu.Item> |
| 139 | + </ActionMenu.Overlay> |
| 140 | + </ActionMenu>, |
| 141 | + ) |
| 142 | + |
| 143 | + fireEvent.click(getByRole('button', {name: 'Open menu'})) |
| 144 | + |
| 145 | + expect(mockOnClick).toHaveBeenCalledTimes(1) |
| 146 | + expect(queryByLabelText('Actions')).not.toBeInTheDocument() |
| 147 | + }) |
| 148 | + |
| 149 | + it('should apply the button variant in default mode', () => { |
| 150 | + const {getByRole} = render( |
| 151 | + <ActionMenu> |
| 152 | + <ActionMenu.Button variant="subtle">Open menu</ActionMenu.Button> |
| 153 | + <ActionMenu.Overlay aria-label="Actions"> |
| 154 | + <ActionMenu.Item value="Copy link">Copy link</ActionMenu.Item> |
| 155 | + </ActionMenu.Overlay> |
| 156 | + </ActionMenu>, |
| 157 | + ) |
| 158 | + |
| 159 | + expect(getByRole('button', {name: 'Open menu'})).toHaveClass('Button--subtle') |
| 160 | + }) |
| 161 | + |
| 162 | + it('should use the secondary button variant by default in default mode', () => { |
| 163 | + const {getByRole} = render( |
| 164 | + <ActionMenu> |
| 165 | + <ActionMenu.Button>Open menu</ActionMenu.Button> |
| 166 | + <ActionMenu.Overlay aria-label="Actions"> |
| 167 | + <ActionMenu.Item value="Copy link">Copy link</ActionMenu.Item> |
| 168 | + </ActionMenu.Overlay> |
| 169 | + </ActionMenu>, |
| 170 | + ) |
| 171 | + |
| 172 | + expect(getByRole('button', {name: 'Open menu'})).toHaveClass('Button--secondary') |
| 173 | + }) |
| 174 | + |
| 175 | + it('should render the leading visual in default mode', () => { |
| 176 | + const accessibleText = 'Test icon' |
| 177 | + const TestIcon = () => <svg aria-label={accessibleText} /> |
| 178 | + const {getByLabelText} = render( |
| 179 | + <ActionMenu> |
| 180 | + <ActionMenu.Button leadingVisual={<TestIcon />}>Open menu</ActionMenu.Button> |
| 181 | + <ActionMenu.Overlay aria-label="Actions"> |
| 182 | + <ActionMenu.Item value="Copy link">Copy link</ActionMenu.Item> |
| 183 | + </ActionMenu.Overlay> |
| 184 | + </ActionMenu>, |
| 185 | + ) |
| 186 | + |
| 187 | + expect(getByLabelText(accessibleText)).toBeInTheDocument() |
| 188 | + }) |
| 189 | + |
114 | 190 | it("should set aria-haspopup to 'true' and aria-expanded to 'false' by default", () => { |
115 | 191 | const {getByRole} = render( |
116 | 192 | <ActionMenu onSelect={jest.fn()}> |
@@ -455,6 +531,24 @@ describe('ActionMenu', () => { |
455 | 531 | expect(mainButton).toHaveAttribute('href', '#option1') |
456 | 532 | }) |
457 | 533 |
|
| 534 | + it('should forward custom attributes to the primary action in split-button mode', () => { |
| 535 | + const {getByRole, getByLabelText} = render( |
| 536 | + <ActionMenu mode="split-button"> |
| 537 | + <ActionMenu.Button as="a" href="#option1" data-attribute="test"> |
| 538 | + Primary Action |
| 539 | + </ActionMenu.Button> |
| 540 | + <ActionMenu.Overlay aria-label="Additional options"> |
| 541 | + <ActionMenu.Item as="a" href="#option1"> |
| 542 | + Option 1 |
| 543 | + </ActionMenu.Item> |
| 544 | + </ActionMenu.Overlay> |
| 545 | + </ActionMenu>, |
| 546 | + ) |
| 547 | + |
| 548 | + expect(getByRole('link', {name: 'Primary Action'})).toHaveAttribute('data-attribute', 'test') |
| 549 | + expect(getByLabelText('Menu')).not.toHaveAttribute('data-attribute') |
| 550 | + }) |
| 551 | + |
458 | 552 | it('should toggle menu when clicking the chevron button', async () => { |
459 | 553 | const {getByLabelText, queryByLabelText} = render( |
460 | 554 | <ActionMenu mode="split-button"> |
@@ -501,6 +595,40 @@ describe('ActionMenu', () => { |
501 | 595 | ) |
502 | 596 | }) |
503 | 597 |
|
| 598 | + it('should keep the primary and menu actions independent in split-button mode', async () => { |
| 599 | + const mockOnClick = jest.fn() |
| 600 | + const user = userEvent.setup() |
| 601 | + const {getByRole, getByLabelText, queryByLabelText} = render( |
| 602 | + <ActionMenu mode="split-button"> |
| 603 | + <ActionMenu.Button onClick={mockOnClick}>Primary Action</ActionMenu.Button> |
| 604 | + <ActionMenu.Overlay aria-label="Additional options"> |
| 605 | + <ActionMenu.Item as="a" href="#option1"> |
| 606 | + Option 1 |
| 607 | + </ActionMenu.Item> |
| 608 | + </ActionMenu.Overlay> |
| 609 | + </ActionMenu>, |
| 610 | + ) |
| 611 | + |
| 612 | + const primaryButton = getByRole('button', {name: 'Primary Action'}) |
| 613 | + fireEvent.click(primaryButton) |
| 614 | + |
| 615 | + expect(mockOnClick).toHaveBeenCalledTimes(1) |
| 616 | + expect(queryByLabelText('Additional options')).not.toBeInTheDocument() |
| 617 | + |
| 618 | + await user.tab() |
| 619 | + expect(primaryButton).toHaveFocus() |
| 620 | + await user.keyboard('{Enter}') |
| 621 | + await user.keyboard(' ') |
| 622 | + |
| 623 | + expect(mockOnClick).toHaveBeenCalledTimes(3) |
| 624 | + expect(queryByLabelText('Additional options')).not.toBeInTheDocument() |
| 625 | + |
| 626 | + fireEvent.click(getByLabelText('Menu')) |
| 627 | + |
| 628 | + expect(mockOnClick).toHaveBeenCalledTimes(3) |
| 629 | + expect(queryByLabelText('Additional options')).toBeInTheDocument() |
| 630 | + }) |
| 631 | + |
504 | 632 | it('should render items as links with correct href attribute', async () => { |
505 | 633 | const {getByLabelText, getAllByRole} = render( |
506 | 634 | <ActionMenu mode="split-button" open> |
@@ -574,6 +702,24 @@ describe('ActionMenu', () => { |
574 | 702 | expect(variantButton).toBeInTheDocument() |
575 | 703 | }) |
576 | 704 |
|
| 705 | + it('should use the primary button variant by default in split-button mode', () => { |
| 706 | + const {getByRole, getByLabelText} = render( |
| 707 | + <ActionMenu mode="split-button"> |
| 708 | + <ActionMenu.Button as="a" href="#option1"> |
| 709 | + Primary Action |
| 710 | + </ActionMenu.Button> |
| 711 | + <ActionMenu.Overlay aria-label="Additional options"> |
| 712 | + <ActionMenu.Item as="a" href="#option1"> |
| 713 | + Option 1 |
| 714 | + </ActionMenu.Item> |
| 715 | + </ActionMenu.Overlay> |
| 716 | + </ActionMenu>, |
| 717 | + ) |
| 718 | + |
| 719 | + expect(getByRole('link', {name: 'Primary Action'})).toHaveClass('Button--primary') |
| 720 | + expect(getByLabelText('Menu')).toHaveClass('Button--primary') |
| 721 | + }) |
| 722 | + |
577 | 723 | it('should not change main button href when menu is toggled', async () => { |
578 | 724 | const {getByText, getByLabelText} = render( |
579 | 725 | <ActionMenu mode="split-button"> |
|
0 commit comments