mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(accordion): tests added
This commit is contained in:
parent
976a3d52aa
commit
28229dc0ae
@ -1,5 +1,7 @@
|
||||
import * as React from "react";
|
||||
import {render} from "@testing-library/react";
|
||||
import {act, render} from "@testing-library/react";
|
||||
import {focus} from "@nextui-org/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import {Accordion, AccordionItem} from "../src";
|
||||
|
||||
@ -24,4 +26,221 @@ describe("Accordion", () => {
|
||||
);
|
||||
expect(ref.current).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should display the correct number of items", () => {
|
||||
const wrapper = render(
|
||||
<Accordion>
|
||||
<AccordionItem key="1">Accordion Item</AccordionItem>
|
||||
<AccordionItem key="2">Accordion Item</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
expect(wrapper.getAllByRole("button")).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("should be opened when defaultExpandedKeys is set", () => {
|
||||
const wrapper = render(
|
||||
<Accordion defaultExpandedKeys={["1"]}>
|
||||
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion Item 2">
|
||||
Accordion Item 2 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
expect(wrapper.getByTestId("item-1")).toHaveAttribute("data-open", "true");
|
||||
});
|
||||
|
||||
it("should be disabled when disabledKeys is set", () => {
|
||||
const wrapper = render(
|
||||
<Accordion disabledKeys={["1"]}>
|
||||
<AccordionItem key="1" title="Accordion Item 1">
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion Item 2">
|
||||
Accordion Item 2 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
expect(wrapper.getAllByRole("button")[0]).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should expand the accordion item when clicked", () => {
|
||||
const wrapper = render(
|
||||
<Accordion disableAnimation>
|
||||
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion Item 2">
|
||||
Accordion Item 2 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
const base = wrapper.getByTestId("item-1");
|
||||
const button = base.querySelector("button");
|
||||
|
||||
expect(button).toHaveAttribute("aria-expanded", "false");
|
||||
|
||||
act(() => {
|
||||
button?.click();
|
||||
});
|
||||
|
||||
expect(button).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
it("should support leftIndicator", () => {
|
||||
const wrapper = render(
|
||||
<Accordion>
|
||||
<AccordionItem
|
||||
key="1"
|
||||
data-testid="item-1"
|
||||
leftIndicator={<div data-testid="left-content" />}
|
||||
title="Accordion Item 1"
|
||||
>
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
expect(wrapper.getByTestId("left-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("arrow up & down moves focus to next/previous accordion item", async () => {
|
||||
const wrapper = render(
|
||||
<Accordion>
|
||||
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" data-testid="item-2" title="Accordion Item 2">
|
||||
Accordion Item 2 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
const first = wrapper.getByTestId("item-1");
|
||||
const firstButton = first.querySelector("button") as HTMLElement;
|
||||
|
||||
const second = wrapper.getByTestId("item-2");
|
||||
const secondButton = second.querySelector("button") as HTMLElement;
|
||||
|
||||
act(() => {
|
||||
focus(firstButton);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.keyboard("[ArrowDown]");
|
||||
});
|
||||
expect(secondButton).toHaveFocus();
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.keyboard("[ArrowUp]");
|
||||
});
|
||||
expect(firstButton).toHaveFocus();
|
||||
});
|
||||
|
||||
it("home & end keys moves focus to first/last accordion", async () => {
|
||||
const wrapper = render(
|
||||
<Accordion>
|
||||
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" data-testid="item-2" title="Accordion Item 2">
|
||||
Accordion Item 2 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
const first = wrapper.getByTestId("item-1");
|
||||
const firstButton = first.querySelector("button") as HTMLElement;
|
||||
|
||||
const second = wrapper.getByTestId("item-2");
|
||||
const secondButton = second.querySelector("button") as HTMLElement;
|
||||
|
||||
act(() => {
|
||||
focus(secondButton);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.keyboard("[Home]");
|
||||
});
|
||||
expect(firstButton).toHaveFocus();
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.keyboard("[End]");
|
||||
});
|
||||
expect(secondButton).toHaveFocus();
|
||||
});
|
||||
|
||||
it("tab moves focus to the next focusable element", async () => {
|
||||
const wrapper = render(
|
||||
<Accordion>
|
||||
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" data-testid="item-2" title="Accordion Item 2">
|
||||
Accordion Item 2 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
const first = wrapper.getByTestId("item-1");
|
||||
const firstButton = first.querySelector("button") as HTMLElement;
|
||||
|
||||
const second = wrapper.getByTestId("item-2");
|
||||
const secondButton = second.querySelector("button") as HTMLElement;
|
||||
|
||||
act(() => {
|
||||
focus(firstButton);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.keyboard("[Tab]");
|
||||
});
|
||||
expect(secondButton).toHaveFocus();
|
||||
});
|
||||
|
||||
it("aria-controls for button is same as id for region", async () => {
|
||||
const wrapper = render(
|
||||
<Accordion defaultExpandedKeys={["1"]}>
|
||||
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" data-testid="item-2" title="Accordion Item 2">
|
||||
Accordion Item 2 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
const base = wrapper.getByTestId("item-1");
|
||||
const button = base.querySelector("button") as HTMLElement;
|
||||
|
||||
const region = base.querySelector("[role='region']") as HTMLElement;
|
||||
|
||||
expect(button).toHaveAttribute("aria-controls", region.id);
|
||||
});
|
||||
|
||||
it("aria-expanded is true/false when accordion is open/closed", async () => {
|
||||
const wrapper = render(
|
||||
<Accordion disableAnimation>
|
||||
<AccordionItem key="1" data-testid="item-1" title="Accordion Item 1">
|
||||
Accordion Item 1 description
|
||||
</AccordionItem>
|
||||
</Accordion>,
|
||||
);
|
||||
|
||||
const base = wrapper.getByTestId("item-1");
|
||||
const button = base.querySelector("button") as HTMLElement;
|
||||
|
||||
expect(button).toHaveAttribute("aria-expanded", "false");
|
||||
|
||||
act(() => {
|
||||
button?.click();
|
||||
});
|
||||
|
||||
expect(button).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
});
|
||||
|
||||
@ -58,6 +58,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextui-org/avatar": "workspace:*",
|
||||
"@nextui-org/test-utils": "workspace:*",
|
||||
"@react-types/accordion": "3.0.0-alpha.12",
|
||||
"@react-types/shared": "^3.17.0",
|
||||
"clean-package": "2.2.0",
|
||||
|
||||
@ -10,9 +10,13 @@ export interface AccordionItemProps extends Omit<UseAccordionItemProps, "ref"> {
|
||||
const Accordion = forwardRef<AccordionItemProps, "div">((props, ref) => {
|
||||
const {
|
||||
Component,
|
||||
item,
|
||||
styles,
|
||||
slots,
|
||||
indicator,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
leftIndicator,
|
||||
isOpen,
|
||||
isDisabled,
|
||||
disableAnimation,
|
||||
@ -26,42 +30,42 @@ const Accordion = forwardRef<AccordionItemProps, "div">((props, ref) => {
|
||||
getIndicatorProps,
|
||||
} = useAccordionItem({ref, ...props});
|
||||
|
||||
const indicator = useMemo<ReactNode | null>(() => {
|
||||
if (typeof item.props?.indicator === "function") {
|
||||
return item.props?.indicator({indicator: <ChevronIcon />, isOpen, isDisabled});
|
||||
const indicatorContent = useMemo<ReactNode | null>(() => {
|
||||
if (typeof indicator === "function") {
|
||||
return indicator({indicator: <ChevronIcon />, isOpen, isDisabled});
|
||||
}
|
||||
|
||||
if (item.props?.indicator) return item.props?.indicator;
|
||||
if (indicator) return indicator;
|
||||
|
||||
return <ChevronIcon />;
|
||||
}, [item.props?.indicator, isOpen, isDisabled]);
|
||||
}, [indicator, isOpen, isDisabled]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if (disableAnimation) {
|
||||
return <div {...getContentProps()}>{item.props?.children}</div>;
|
||||
return <div {...getContentProps()}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse in={isOpen} {...motionProps}>
|
||||
<div {...getContentProps()}>{item.props?.children}</div>
|
||||
<div {...getContentProps()}>{children}</div>
|
||||
</Collapse>
|
||||
);
|
||||
}, [isOpen, disableAnimation, item.props?.children, motionProps]);
|
||||
}, [isOpen, disableAnimation, children, motionProps]);
|
||||
|
||||
return (
|
||||
<Component {...getBaseProps()}>
|
||||
<h2 {...getHeadingProps()}>
|
||||
<button {...getButtonProps()}>
|
||||
{item.props?.leftIndicator && (
|
||||
{leftIndicator && (
|
||||
<div className={slots.leftIndicator({class: styles?.leftIndicator})}>
|
||||
{item.props?.leftIndicator}
|
||||
{leftIndicator}
|
||||
</div>
|
||||
)}
|
||||
<div className={slots.titleWrapper({class: styles?.titleWrapper})}>
|
||||
{item.props?.title && <span {...getTitleProps()}>{item.props?.title}</span>}
|
||||
{item.props?.subtitle && <span {...getSubtitleProps()}>{item.props?.subtitle}</span>}
|
||||
{title && <span {...getTitleProps()}>{title}</span>}
|
||||
{subtitle && <span {...getSubtitleProps()}>{subtitle}</span>}
|
||||
</div>
|
||||
{indicator && <span {...getIndicatorProps()}>{indicator}</span>}
|
||||
{indicatorContent && <span {...getIndicatorProps()}>{indicatorContent}</span>}
|
||||
</button>
|
||||
</h2>
|
||||
{content}
|
||||
|
||||
@ -2,7 +2,7 @@ import {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {accordionItem} from "@nextui-org/theme";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, ReactRef, callAllHandlers, dataAttr, extractProperty} from "@nextui-org/shared-utils";
|
||||
import {clsx, ReactRef, callAllHandlers, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {NodeWithProps, useAccordionItem as useBaseAccordion} from "@nextui-org/aria-utils";
|
||||
import {useCallback, useMemo} from "react";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
@ -26,52 +26,25 @@ export type UseAccordionItemProps<T extends object = {}> = Props<T> & AccordionI
|
||||
export function useAccordionItem<T extends object = {}>(props: UseAccordionItemProps<T>) {
|
||||
const groupContext = useAccordionContext();
|
||||
|
||||
const {ref, as, item, onFocusChange} = props;
|
||||
|
||||
const {
|
||||
ref,
|
||||
as,
|
||||
item,
|
||||
styles = item.props?.styles,
|
||||
className = item.props?.className,
|
||||
isCompact = extractProperty(
|
||||
"isCompact",
|
||||
false,
|
||||
groupContext,
|
||||
item.props,
|
||||
) as AccordionItemBaseProps["isCompact"],
|
||||
isDisabled: isDisabledProp = extractProperty(
|
||||
"isDisabled",
|
||||
false,
|
||||
groupContext,
|
||||
item.props,
|
||||
) as AccordionItemBaseProps["isDisabled"],
|
||||
hideDivider = extractProperty(
|
||||
"hideDivider",
|
||||
false,
|
||||
groupContext,
|
||||
item.props,
|
||||
) as AccordionItemBaseProps["hideDivider"],
|
||||
hideIndicator = extractProperty(
|
||||
"hideIndicator",
|
||||
false,
|
||||
groupContext,
|
||||
item.props,
|
||||
) as AccordionItemBaseProps["hideIndicator"],
|
||||
disableAnimation = extractProperty(
|
||||
"disableAnimation",
|
||||
false,
|
||||
groupContext,
|
||||
item.props,
|
||||
) as AccordionItemBaseProps["disableAnimation"],
|
||||
disableIndicatorAnimation = extractProperty(
|
||||
"disableIndicatorAnimation",
|
||||
false,
|
||||
groupContext,
|
||||
item.props,
|
||||
) as AccordionItemBaseProps["disableIndicatorAnimation"],
|
||||
onFocusChange,
|
||||
motionProps = item.props?.motionProps ?? groupContext?.motionProps,
|
||||
styles,
|
||||
className,
|
||||
indicator,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
leftIndicator,
|
||||
motionProps = groupContext?.motionProps,
|
||||
isCompact = groupContext?.isCompact ?? false,
|
||||
isDisabled: isDisabledProp = groupContext?.isDisabled ?? false,
|
||||
hideDivider = groupContext.hideDivider ?? false,
|
||||
hideIndicator = groupContext.hideIndicator ?? false,
|
||||
disableAnimation = groupContext.disableAnimation ?? false,
|
||||
disableIndicatorAnimation = groupContext.disableIndicatorAnimation ?? false,
|
||||
...otherProps
|
||||
} = props;
|
||||
} = item.props as AccordionItemBaseProps<T>;
|
||||
|
||||
const Component = as || "div";
|
||||
|
||||
@ -134,7 +107,7 @@ export function useAccordionItem<T extends object = {}>(props: UseAccordionItemP
|
||||
...mergeProps(otherProps, props),
|
||||
};
|
||||
},
|
||||
[baseStyles, otherProps, slots, isOpen, isDisabled],
|
||||
[baseStyles, otherProps, slots, item.props, isOpen, isDisabled],
|
||||
);
|
||||
|
||||
const getButtonProps = useCallback<PropGetter>(
|
||||
@ -233,6 +206,11 @@ export function useAccordionItem<T extends object = {}>(props: UseAccordionItemP
|
||||
slots,
|
||||
styles,
|
||||
domRef,
|
||||
indicator,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
leftIndicator,
|
||||
isOpen,
|
||||
isDisabled,
|
||||
disableAnimation,
|
||||
|
||||
@ -78,7 +78,6 @@ export function useAccordion<T extends object>(props: UseAccordionProps<T>) {
|
||||
hideIndicator = false,
|
||||
disableAnimation = false,
|
||||
disableIndicatorAnimation = false,
|
||||
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
|
||||
@ -54,13 +54,13 @@ const defaultContent =
|
||||
|
||||
const Template: ComponentStory<typeof Accordion> = (args: AccordionProps) => (
|
||||
<Accordion {...args}>
|
||||
<AccordionItem key="1" title="Accordion 1">
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion 2">
|
||||
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" title="Accordion 3">
|
||||
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -68,11 +68,12 @@ const Template: ComponentStory<typeof Accordion> = (args: AccordionProps) => (
|
||||
|
||||
const TemplateWithSubtitle: ComponentStory<typeof Accordion> = (args: AccordionProps) => (
|
||||
<Accordion {...args}>
|
||||
<AccordionItem key="1" subtitle="Press to expand" title="Accordion 1">
|
||||
<AccordionItem key="1" aria-label="Accordion 1" subtitle="Press to expand" title="Accordion 1">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="2"
|
||||
aria-label="Accordion 2"
|
||||
subtitle={
|
||||
<span>
|
||||
Press to expand <strong>key 2</strong>
|
||||
@ -82,7 +83,7 @@ const TemplateWithSubtitle: ComponentStory<typeof Accordion> = (args: AccordionP
|
||||
>
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" subtitle="Press to expand" title="Accordion 3">
|
||||
<AccordionItem key="3" aria-label="Accordion 3" subtitle="Press to expand" title="Accordion 3">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -92,6 +93,7 @@ const TemplateWithLeftIndicator: ComponentStory<typeof Accordion> = (args: Accor
|
||||
<Accordion {...args} variant="shadow">
|
||||
<AccordionItem
|
||||
key="1"
|
||||
aria-label="Chung Miller"
|
||||
leftIndicator={
|
||||
<Avatar
|
||||
isBordered
|
||||
@ -107,6 +109,7 @@ const TemplateWithLeftIndicator: ComponentStory<typeof Accordion> = (args: Accor
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="2"
|
||||
aria-label="Janelle Lenard"
|
||||
leftIndicator={
|
||||
<Avatar
|
||||
isBordered
|
||||
@ -122,6 +125,7 @@ const TemplateWithLeftIndicator: ComponentStory<typeof Accordion> = (args: Accor
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="3"
|
||||
aria-label="Zoey Lang"
|
||||
leftIndicator={
|
||||
<Avatar
|
||||
isBordered
|
||||
@ -167,13 +171,13 @@ const VariantsTemplate: ComponentStory<typeof Accordion> = (args: AccordionProps
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3>Shadow</h3>
|
||||
<Accordion {...args} variant="shadow">
|
||||
<AccordionItem key="1" title="Accordion 1">
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion 2">
|
||||
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" title="Accordion 3">
|
||||
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -181,13 +185,13 @@ const VariantsTemplate: ComponentStory<typeof Accordion> = (args: AccordionProps
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3>Bordered</h3>
|
||||
<Accordion {...args} variant="bordered">
|
||||
<AccordionItem key="1" title="Accordion 1">
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion 2">
|
||||
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" title="Accordion 3">
|
||||
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -195,13 +199,13 @@ const VariantsTemplate: ComponentStory<typeof Accordion> = (args: AccordionProps
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3>Splitted</h3>
|
||||
<Accordion {...args} variant="splitted">
|
||||
<AccordionItem key="1" title="Accordion 1">
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion 2">
|
||||
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" title="Accordion 3">
|
||||
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -211,13 +215,13 @@ const VariantsTemplate: ComponentStory<typeof Accordion> = (args: AccordionProps
|
||||
|
||||
const CustomInidicatorTemplate: ComponentStory<typeof Accordion> = (args: AccordionProps) => (
|
||||
<Accordion {...args}>
|
||||
<AccordionItem key="anchor" indicator={<AnchorIcon />} title="Anchor">
|
||||
<AccordionItem key="anchor" aria-label="Anchor" indicator={<AnchorIcon />} title="Anchor">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="moon" indicator={<MoonIcon />} title="Moon">
|
||||
<AccordionItem key="moon" aria-label="Moon" indicator={<MoonIcon />} title="Moon">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="sun" indicator={<SunIcon />} title="Sun">
|
||||
<AccordionItem key="sun" aria-label="Sun" indicator={<SunIcon />} title="Sun">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -231,13 +235,13 @@ const ControlledTemplate: ComponentStory<typeof Accordion> = (args: AccordionPro
|
||||
|
||||
return (
|
||||
<Accordion {...args} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys}>
|
||||
<AccordionItem key="1" title="Accordion 1">
|
||||
<AccordionItem key="1" aria-label="Accordion 1" title="Accordion 1">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion 2">
|
||||
<AccordionItem key="2" aria-label="Accordion 2" title="Accordion 2">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" title="Accordion 3">
|
||||
<AccordionItem key="3" aria-label="Accordion 3" title="Accordion 3">
|
||||
{defaultContent}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
@ -262,6 +266,7 @@ const CustomWithStylesTemplate: ComponentStory<typeof Accordion> = (args: Accord
|
||||
>
|
||||
<AccordionItem
|
||||
key="1"
|
||||
aria-label="Connected devices"
|
||||
leftIndicator={<MonitorMobileIcon className="text-primary" />}
|
||||
styles={itemStyles}
|
||||
subtitle={
|
||||
@ -281,6 +286,7 @@ const CustomWithStylesTemplate: ComponentStory<typeof Accordion> = (args: Accord
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="2"
|
||||
aria-label="Apps Permissions"
|
||||
leftIndicator={<ShieldSecurityIcon />}
|
||||
styles={itemStyles}
|
||||
subtitle="3 apps have read permissions"
|
||||
@ -290,6 +296,7 @@ const CustomWithStylesTemplate: ComponentStory<typeof Accordion> = (args: Accord
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="3"
|
||||
aria-label="Pending tasks"
|
||||
leftIndicator={<InfoIcon className="text-warning" />}
|
||||
styles={{...itemStyles, subtitle: "text-warning"}}
|
||||
subtitle="Complete your profile"
|
||||
@ -299,6 +306,7 @@ const CustomWithStylesTemplate: ComponentStory<typeof Accordion> = (args: Accord
|
||||
</AccordionItem>
|
||||
<AccordionItem
|
||||
key="4"
|
||||
aria-label="Card expired"
|
||||
leftIndicator={<InvalidCardIcon className="text-danger" />}
|
||||
styles={{...itemStyles, subtitle: "text-danger"}}
|
||||
subtitle="Please, update now"
|
||||
|
||||
25
packages/utilities/test-utils/src/dom.ts
Normal file
25
packages/utilities/test-utils/src/dom.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export function isElement(el: any): el is Element {
|
||||
return (
|
||||
el != null && typeof el == "object" && "nodeType" in el && el.nodeType === Node.ELEMENT_NODE
|
||||
);
|
||||
}
|
||||
|
||||
export function isHTMLElement(el: any): el is HTMLElement {
|
||||
if (!isElement(el)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const win = el.ownerDocument.defaultView ?? window;
|
||||
|
||||
return el instanceof win.HTMLElement;
|
||||
}
|
||||
|
||||
export function getOwnerDocument(node?: Element | null): Document {
|
||||
return isElement(node) ? node.ownerDocument ?? document : document;
|
||||
}
|
||||
|
||||
export function getActiveElement(node?: HTMLElement) {
|
||||
const doc = getOwnerDocument(node);
|
||||
|
||||
return doc?.activeElement as HTMLElement;
|
||||
}
|
||||
21
packages/utilities/test-utils/src/focus.ts
Normal file
21
packages/utilities/test-utils/src/focus.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {act} from "@testing-library/react";
|
||||
|
||||
import {isFocusable} from "./tabbable";
|
||||
import {getActiveElement} from "./dom";
|
||||
|
||||
export function focus(el: HTMLElement) {
|
||||
if (getActiveElement(el) === el) return;
|
||||
if (!isFocusable(el)) return;
|
||||
act(() => {
|
||||
el.focus();
|
||||
});
|
||||
}
|
||||
|
||||
export function blur(el?: HTMLElement | null) {
|
||||
if (el == null) el = document.activeElement as HTMLElement;
|
||||
if (el.tagName === "BODY") return;
|
||||
if (getActiveElement(el) !== el) return;
|
||||
act(() => {
|
||||
if (el && "blur" in el) el.blur();
|
||||
});
|
||||
}
|
||||
@ -1 +1,4 @@
|
||||
export * from "./mocks";
|
||||
export * from "./focus";
|
||||
export * from "./tabbable";
|
||||
export * from "./dom";
|
||||
|
||||
82
packages/utilities/test-utils/src/tabbable.ts
Normal file
82
packages/utilities/test-utils/src/tabbable.ts
Normal file
@ -0,0 +1,82 @@
|
||||
// Really great work done by Diego Haz on this one
|
||||
// https://github.com/reakit/reakit/blob/master/packages/reakit-utils/src/tabbable.ts
|
||||
|
||||
import {getOwnerDocument, isHTMLElement} from "./dom";
|
||||
|
||||
export const hasDisplayNone = (element: HTMLElement) =>
|
||||
window.getComputedStyle(element).display === "none";
|
||||
|
||||
export const hasTabIndex = (element: HTMLElement) => element.hasAttribute("tabindex");
|
||||
|
||||
export const hasNegativeTabIndex = (element: HTMLElement) =>
|
||||
hasTabIndex(element) && element.tabIndex === -1;
|
||||
|
||||
export function isDisabled(element: HTMLElement) {
|
||||
return (
|
||||
Boolean(element.getAttribute("disabled")) === true ||
|
||||
Boolean(element.getAttribute("aria-disabled")) === true
|
||||
);
|
||||
}
|
||||
|
||||
export interface FocusableElement {
|
||||
focus(options?: FocusOptions): void;
|
||||
}
|
||||
|
||||
export function isInputElement(element: FocusableElement): element is HTMLInputElement {
|
||||
return isHTMLElement(element) && element.localName === "input" && "select" in element;
|
||||
}
|
||||
|
||||
export function isActiveElement(element: FocusableElement) {
|
||||
const doc = isHTMLElement(element) ? getOwnerDocument(element) : document;
|
||||
|
||||
return doc.activeElement === (element as HTMLElement);
|
||||
}
|
||||
|
||||
export function hasFocusWithin(element: HTMLElement) {
|
||||
if (!document.activeElement) return false;
|
||||
|
||||
return element.contains(document.activeElement);
|
||||
}
|
||||
|
||||
export function isHidden(element: HTMLElement) {
|
||||
if (element.parentElement && isHidden(element.parentElement)) return true;
|
||||
|
||||
return element.hidden;
|
||||
}
|
||||
|
||||
export function isContentEditable(element: HTMLElement) {
|
||||
const value = element.getAttribute("contenteditable");
|
||||
|
||||
return value !== "false" && value != null;
|
||||
}
|
||||
|
||||
export function isFocusable(element: HTMLElement) {
|
||||
if (!isHTMLElement(element) || isHidden(element) || isDisabled(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {localName} = element;
|
||||
const focusableTags = ["input", "select", "textarea", "button"];
|
||||
|
||||
if (focusableTags.indexOf(localName) >= 0) return true;
|
||||
|
||||
const others = {
|
||||
a: () => element.hasAttribute("href"),
|
||||
audio: () => element.hasAttribute("controls"),
|
||||
video: () => element.hasAttribute("controls"),
|
||||
};
|
||||
|
||||
if (localName in others) {
|
||||
return others[localName as keyof typeof others]();
|
||||
}
|
||||
|
||||
if (isContentEditable(element)) return true;
|
||||
|
||||
return hasTabIndex(element);
|
||||
}
|
||||
|
||||
export function isTabbable(element?: HTMLElement | null) {
|
||||
if (!element) return false;
|
||||
|
||||
return isHTMLElement(element) && isFocusable(element) && !hasNegativeTabIndex(element);
|
||||
}
|
||||
35
pnpm-lock.yaml
generated
35
pnpm-lock.yaml
generated
@ -312,6 +312,7 @@ importers:
|
||||
'@nextui-org/shared-icons': workspace:*
|
||||
'@nextui-org/shared-utils': workspace:*
|
||||
'@nextui-org/system': workspace:*
|
||||
'@nextui-org/test-utils': workspace:*
|
||||
'@nextui-org/theme': workspace:*
|
||||
'@react-aria/accordion': 3.0.0-alpha.15
|
||||
'@react-aria/focus': ^3.11.0
|
||||
@ -337,6 +338,7 @@ importers:
|
||||
framer-motion: 10.6.0_react@18.2.0
|
||||
devDependencies:
|
||||
'@nextui-org/avatar': link:../avatar
|
||||
'@nextui-org/test-utils': link:../../utilities/test-utils
|
||||
'@react-types/accordion': 3.0.0-alpha.12_react@18.2.0
|
||||
'@react-types/shared': 3.17.0_react@18.2.0
|
||||
clean-package: 2.2.0
|
||||
@ -545,39 +547,6 @@ importers:
|
||||
clean-package: 2.2.0
|
||||
react: 18.2.0
|
||||
|
||||
packages/components/collapse:
|
||||
specifiers:
|
||||
'@nextui-org/aria-utils': workspace:*
|
||||
'@nextui-org/dom-utils': workspace:*
|
||||
'@nextui-org/react-utils': workspace:*
|
||||
'@nextui-org/shared-css': workspace:*
|
||||
'@nextui-org/shared-utils': workspace:*
|
||||
'@nextui-org/system': workspace:*
|
||||
'@react-aria/accordion': 3.0.0-alpha.15
|
||||
'@react-aria/focus': ^3.11.0
|
||||
'@react-aria/utils': ^3.15.0
|
||||
'@react-stately/tree': ^3.5.0
|
||||
'@react-types/accordion': 3.0.0-alpha.12
|
||||
'@react-types/shared': ^3.15.0
|
||||
clean-package: 2.2.0
|
||||
react: ^18.2.0
|
||||
dependencies:
|
||||
'@nextui-org/aria-utils': link:../../utilities/aria-utils
|
||||
'@nextui-org/dom-utils': link:../../utilities/dom-utils
|
||||
'@nextui-org/react-utils': link:../../utilities/react-utils
|
||||
'@nextui-org/shared-css': link:../../utilities/shared-css
|
||||
'@nextui-org/shared-utils': link:../../utilities/shared-utils
|
||||
'@nextui-org/system': link:../../core/system
|
||||
'@react-aria/accordion': 3.0.0-alpha.15_react@18.2.0
|
||||
'@react-aria/focus': 3.11.0_react@18.2.0
|
||||
'@react-aria/utils': 3.15.0_react@18.2.0
|
||||
'@react-stately/tree': 3.5.0_react@18.2.0
|
||||
devDependencies:
|
||||
'@react-types/accordion': 3.0.0-alpha.12_react@18.2.0
|
||||
'@react-types/shared': 3.17.0_react@18.2.0
|
||||
clean-package: 2.2.0
|
||||
react: 18.2.0
|
||||
|
||||
packages/components/drip:
|
||||
specifiers:
|
||||
'@nextui-org/dom-utils': workspace:*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user