-
-
Notifications
You must be signed in to change notification settings - Fork 428
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…#170) * refactor(component): Prepend `boolean` fields with `is` * fix(story): Update old color name in `Sidebar` story * fix(a11y): Add unique labels to `Sidebar` examples * feature(component): Allow `Sidebar`s to hide completely, resolves #81 `Sidebar`s can now be created with: ```js <Sidebar collapseBehavior="hide">...</Sidebar> ``` To completely hide the `Sidebar` when collapsed, instead of displaying icons. This is useful if you are not adding icons to yours. * test(component): Add numerous unit tests for `Sidebar`s
- Loading branch information
1 parent
2c39deb
commit 44a29e7
Showing
11 changed files
with
294 additions
and
160 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,126 +1,230 @@ | ||
import { render, waitFor } from '@testing-library/react'; | ||
import { act } from 'react-dom/test-utils'; | ||
import { HiChartPie, HiInbox, HiUser, HiShoppingBag, HiArrowSmRight, HiTable } from 'react-icons/hi'; | ||
import { render, RenderResult, waitFor } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { HiChartPie, HiInbox, HiShoppingBag } from 'react-icons/hi'; | ||
import { Sidebar, SidebarProps } from '.'; | ||
import { Badge } from '../Badge'; | ||
import { Button } from '../Button'; | ||
|
||
describe('Sidebar', () => { | ||
describe('when collapsed', () => { | ||
describe('logo', () => { | ||
it('should not display its text label', () => { | ||
const { getByTestId } = render(<SidebarTestComponent collapsed />); | ||
expect(getByTestId('sidebar-logo')).toHaveClass('sr-only'); | ||
|
||
describe('Components / Sidebar', () => { | ||
describe('A11y', () => { | ||
it('should use `aria-label` if provided', () => { | ||
const { getByLabelText } = render(<TestSidebar aria-label="My differently labelled sidebar" />); | ||
const sidebar = getByLabelText('My differently labelled sidebar'); | ||
|
||
expect(sidebar).toHaveAccessibleName('My differently labelled sidebar'); | ||
}); | ||
|
||
describe('`Sidebar.Collapse` and `Sidebar.Item`', () => { | ||
it('should use text content as accessible name', () => { | ||
const itemLabels = ['Dashboard', 'E-commerce', 'Products', 'Services', 'Inbox', 'My heading']; | ||
|
||
const { getAllByRole } = render(<TestSidebar />); | ||
const collapseButtons = getSidebarCollapseButtons({ getAllByRole }); | ||
|
||
collapseButtons.forEach((button) => userEvent.click(button)); | ||
|
||
const items = getSidebarItems({ getAllByRole }); | ||
|
||
items.forEach((item, i) => { | ||
expect(item.firstElementChild).toHaveAccessibleName(itemLabels[i]); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('items', () => { | ||
it('should not display their text content', () => { | ||
const { queryAllByTestId } = render(<SidebarTestComponent collapsed />); | ||
expect(queryAllByTestId('sidebar-item-content')).toHaveLength(0); | ||
describe('`Sidebar.Logo`', () => { | ||
it('should use text content as accessible name', () => { | ||
const logo = getSidebarLogo(render(<TestSidebar />)); | ||
|
||
expect(logo).toHaveAccessibleName('Flowbite'); | ||
}); | ||
it('should not display their label', () => { | ||
const { queryAllByTestId } = render(<SidebarTestComponent collapsed />); | ||
expect(queryAllByTestId('sidebar-item-label')).toHaveLength(0); | ||
|
||
describe('`img=".."` and `imgAlt=".."`', () => { | ||
it('should use `imgAlt` as alternative text for image', () => { | ||
const { getByAltText } = render(<TestSidebar />); | ||
const logoImg = getByAltText('Flowbite logo'); | ||
|
||
expect(logoImg).toHaveAccessibleName('Flowbite logo'); | ||
}); | ||
}); | ||
it('should display a tooltip', async () => { | ||
const { queryAllByTestId } = render(<SidebarTestComponent collapsed />); | ||
expect(queryAllByTestId('sidebar-item-tooltip')).not.toHaveLength(0); | ||
}); | ||
}); | ||
|
||
describe('Keyboard interactions', () => { | ||
describe('`Space`', () => { | ||
describe('`Sidebar.Collapse`', () => { | ||
it('should expand/collapse', () => { | ||
const { getAllByRole } = render(<TestSidebar />); | ||
const collapseButton = getSidebarCollapseButtons({ getAllByRole })[0]; | ||
|
||
userEvent.click(collapseButton); | ||
|
||
const collapse = getSidebarCollapses({ getAllByRole })[0]; | ||
|
||
expect(collapse).toBeVisible(); | ||
}); | ||
}); | ||
|
||
describe('`Sidebar.Item`', () => { | ||
describe('`href=".."`', () => { | ||
it('should follow link', () => { | ||
const { getAllByRole } = render(<TestSidebar />); | ||
const link = getAllByRole('link')[1]; | ||
|
||
expect(link).toHaveAccessibleName('Dashboard'); | ||
expect(link).toHaveAttribute('href', '#'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('CTA', () => { | ||
it('should not be displayed', () => { | ||
const { queryByTestId } = render(<SidebarTestComponent collapsed />); | ||
expect(queryByTestId('sidebar-cta')).toBeNull(); | ||
describe('`Tab`', () => { | ||
it('should be possible to `Tab` out', async () => { | ||
const { getByText } = render( | ||
<> | ||
<TestSidebar /> | ||
<button role="checkbox">Outside</button> | ||
</>, | ||
); | ||
const outside = getByText('Outside'); | ||
|
||
await waitFor(() => { | ||
userEvent.tab(); | ||
|
||
expect(outside).toHaveFocus(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('with a collapsable item', () => { | ||
describe('that is closed', () => { | ||
it('should expand when clicked', async () => { | ||
const { getByTestId } = render(<SidebarTestComponent />); | ||
act(() => { | ||
getByTestId('sidebar-collapse-button').click(); | ||
}); | ||
await waitFor(() => expect(getByTestId('sidebar-collapse')).not.toHaveClass('hidden')); | ||
describe('Props', () => { | ||
describe('`collapseBehavior="hide"`', () => { | ||
it("shouldn't display anything", () => { | ||
const { queryByLabelText } = render(<TestSidebar collapseBehavior="hide" collapsed />); | ||
const sidebar = queryByLabelText('Sidebar'); | ||
|
||
expect(sidebar).not.toBeVisible(); | ||
}); | ||
}); | ||
|
||
describe('that is open', () => { | ||
it('should collapse when clicked', async () => { | ||
const { getByTestId } = render(<SidebarTestComponent />); | ||
act(() => { | ||
getByTestId('sidebar-collapse-button').click(); | ||
describe('`collapsed={true}`', () => { | ||
describe('`Sidebar.CTA`', () => { | ||
it("shouldn't be visible", () => { | ||
const cta = getSidebarCTA(render(<TestSidebar collapsed />)); | ||
|
||
expect(cta).not.toBeVisible(); | ||
}); | ||
act(() => { | ||
getByTestId('sidebar-collapse-button').click(); | ||
}); | ||
|
||
describe('`Sidebar.Collapse` and `Sidebar.Item`', () => { | ||
it('should display tooltip', () => { | ||
const items = getSidebarItems(render(<TestSidebar collapsed />)); | ||
|
||
items.forEach((item) => { | ||
expect(item.firstElementChild).toHaveAttribute('data-testid', 'tooltip-target'); | ||
}); | ||
}); | ||
|
||
it("shouldn't display text content", () => { | ||
const items = getSidebarItemContents(render(<TestSidebar collapsed />)); | ||
|
||
items.forEach((item) => expect(item).toHaveClass('hidden')); | ||
}); | ||
await waitFor(() => expect(getByTestId('sidebar-collapse')).toHaveClass('hidden')); | ||
}); | ||
|
||
describe('`Sidebar.Item`', () => { | ||
it("shouldn't display `label`", () => { | ||
const labels = getSidebarLabels(render(<TestSidebar collapsed />)); | ||
|
||
labels.forEach((label) => expect(label).not.toBeVisible()); | ||
}); | ||
}); | ||
|
||
describe('`Sidebar.Logo`', () => { | ||
it("shouldn't display text content", () => { | ||
const logo = getSidebarLogo(render(<TestSidebar collapsed />)); | ||
|
||
expect(logo.lastElementChild).toHaveClass('hidden'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('`Sidebar.Item`', () => { | ||
describe('`as=""`', () => { | ||
it('should use that HTML element', () => { | ||
const { getByLabelText } = render(<TestSidebar />); | ||
const asItem = getByLabelText('My heading'); | ||
|
||
expect(asItem.tagName.toLocaleLowerCase()).toEqual('h3'); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('Rendering', () => { | ||
it('should render', () => { | ||
const sidebar = getSidebar(render(<TestSidebar />)); | ||
|
||
expect(sidebar).toBeInTheDocument(); | ||
}); | ||
|
||
describe('`collapseBehavior="hide"`', () => { | ||
it('should render', () => { | ||
const sidebar = getSidebar(render(<TestSidebar collapseBehavior="hide" collapsed />)); | ||
|
||
expect(sidebar).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
describe('`collapsed={true}`', () => { | ||
it('should render', () => { | ||
const sidebar = getSidebar(render(<TestSidebar collapsed />)); | ||
|
||
expect(sidebar).toBeInTheDocument(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
const SidebarTestComponent = ({ collapsed }: SidebarProps): JSX.Element => ( | ||
<Sidebar collapsed={collapsed}> | ||
const TestSidebar = ({ ...props }: SidebarProps): JSX.Element => ( | ||
<Sidebar {...props}> | ||
<Sidebar.Logo href="#" img="favicon.png" imgAlt="Flowbite logo"> | ||
Flowbite | ||
</Sidebar.Logo> | ||
<Sidebar.Items> | ||
<Sidebar.ItemGroup> | ||
<Sidebar.Item href="#" icon={HiChartPie}> | ||
<Sidebar.Item href="#" icon={HiChartPie} label="3" labelColor="success"> | ||
Dashboard | ||
</Sidebar.Item> | ||
<Sidebar.Collapse icon={HiShoppingBag} label="E-commerce"> | ||
<Sidebar.Collapse aria-label="E-commerce" icon={HiShoppingBag}> | ||
<Sidebar.Item href="#">Products</Sidebar.Item> | ||
<Sidebar.Item href="#">Services</Sidebar.Item> | ||
</Sidebar.Collapse> | ||
<Sidebar.Item href="#" icon={HiInbox}> | ||
Inbox | ||
</Sidebar.Item> | ||
<Sidebar.Item href="#" icon={HiUser}> | ||
Users | ||
</Sidebar.Item> | ||
<Sidebar.Item href="#" icon={HiShoppingBag}> | ||
Something else | ||
</Sidebar.Item> | ||
<Sidebar.Item href="#" icon={HiArrowSmRight}> | ||
Sign In | ||
</Sidebar.Item> | ||
<Sidebar.Item href="#" icon={HiTable}> | ||
Sign Up | ||
</Sidebar.Item> | ||
<Sidebar.Item as="h3">My heading</Sidebar.Item> | ||
</Sidebar.ItemGroup> | ||
</Sidebar.Items> | ||
<Sidebar.CTA> | ||
<div className="mb-3 flex items-center"> | ||
<Badge color="yellow">Beta</Badge> | ||
<Button | ||
aria-label="Close" | ||
className="-mx-1.5 -my-1.5 ml-auto !h-6 !w-6 bg-transparent !p-1 text-blue-900 hover:bg-blue-200 dark:!bg-blue-900 dark:text-blue-200 dark:hover:!bg-blue-800" | ||
data-collapse-toggle="dropdown-cta" | ||
> | ||
<span className="sr-only">Close</span> | ||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> | ||
<path | ||
fillRule="evenodd" | ||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" | ||
clipRule="evenodd" | ||
></path> | ||
</svg> | ||
</Button> | ||
</div> | ||
<p className="mb-3 text-sm text-blue-900 dark:text-blue-400"> | ||
Preview the new Flowbite dashboard navigation! You can turn the new navigation off for a limited time in your | ||
profile. | ||
</p> | ||
<a | ||
className="text-sm text-blue-900 underline hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" | ||
href="#" | ||
> | ||
Turn new navigation off | ||
</a> | ||
</Sidebar.CTA> | ||
<Sidebar.CTA>Some content</Sidebar.CTA> | ||
</Sidebar> | ||
); | ||
|
||
const getSidebar = ({ getByLabelText }: Pick<RenderResult, 'getByLabelText'>): HTMLElement => getByLabelText('Sidebar'); | ||
|
||
const getSidebarCollapseButtons = ({ getAllByRole }: Pick<RenderResult, 'getAllByRole'>): HTMLElement[] => | ||
getAllByRole('button'); | ||
|
||
const getSidebarCollapses = ({ getAllByRole }: Pick<RenderResult, 'getAllByRole'>): HTMLElement[] => | ||
getAllByRole('list').slice(1); | ||
|
||
const getSidebarCTA = ({ getByText }: Pick<RenderResult, 'getByText'>): HTMLElement => getByText('Some content'); | ||
|
||
const getSidebarItemContents = ({ getAllByTestId }: Pick<RenderResult, 'getAllByTestId'>): HTMLElement[] => | ||
getAllByTestId('flowbite-sidebar-item-content'); | ||
|
||
const getSidebarItems = ({ getAllByRole }: Pick<RenderResult, 'getAllByRole'>): HTMLElement[] => | ||
getAllByRole('listitem'); | ||
|
||
const getSidebarLabels = ({ queryAllByTestId }: Pick<RenderResult, 'queryAllByTestId'>): HTMLElement[] => | ||
queryAllByTestId('flowbite-sidebar-label'); | ||
|
||
const getSidebarLogo = ({ getByLabelText }: Pick<RenderResult, 'getByLabelText'>): HTMLElement => | ||
getByLabelText('Flowbite'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.