mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(accordion): impl in progress
This commit is contained in:
parent
a3ca1e8ed8
commit
4a274014cc
@ -71,6 +71,6 @@
|
||||
"babel-plugin-module-resolver": "^4.1.0",
|
||||
"eslint-config-next": "^11.0.0",
|
||||
"next-sitemap": "^1.6.140",
|
||||
"typescript": "4.6.2"
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
"@types/react": "^17.0.24",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"eslint": "^8.11.0",
|
||||
"typescript": "^4.5.5"
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
"@types/react": "^17.0.33",
|
||||
"@types/react-dom": "^17.0.10",
|
||||
"@vitejs/plugin-react": "^1.0.7",
|
||||
"typescript": "^4.5.4",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^2.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@
|
||||
"shelljs": "^0.8.4",
|
||||
"tsup": "6.4.0",
|
||||
"turbo": "1.6.3",
|
||||
"typescript": "^4.7.4",
|
||||
"typescript": "^5.0.2",
|
||||
"uuid": "^8.3.2",
|
||||
"webpack": "^5.53.0",
|
||||
"webpack-bundle-analyzer": "^4.4.2",
|
||||
|
||||
24
packages/components/accordion/README.md
Normal file
24
packages/components/accordion/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# @nextui-org/accordion
|
||||
|
||||
A Quick description of the component
|
||||
|
||||
> This is an internal utility, not intended for public usage.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add @nextui-org/accordion
|
||||
# or
|
||||
npm i @nextui-org/accordion
|
||||
```
|
||||
|
||||
## Contribution
|
||||
|
||||
Yes please! See the
|
||||
[contributing guidelines](https://github.com/nextui-org/nextui/blob/master/CONTRIBUTING.md)
|
||||
for details.
|
||||
|
||||
## Licence
|
||||
|
||||
This project is licensed under the terms of the
|
||||
[MIT license](https://github.com/nextui-org/nextui/blob/master/LICENSE).
|
||||
19
packages/components/accordion/__tests__/accordion.test.tsx
Normal file
19
packages/components/accordion/__tests__/accordion.test.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import {render} from "@testing-library/react";
|
||||
|
||||
import {Accordion} from "../src";
|
||||
|
||||
describe("Accordion", () => {
|
||||
it("should render correctly", () => {
|
||||
const wrapper = render(<Accordion />);
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("ref should be forwarded", () => {
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
|
||||
render(<Accordion ref={ref} />);
|
||||
expect(ref.current).not.toBeNull();
|
||||
});
|
||||
});
|
||||
11
packages/components/accordion/clean-package.config.json
Normal file
11
packages/components/accordion/clean-package.config.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"replace": {
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {"import": "./dist/index.esm.js", "require": "./dist/index.cjs.js"},
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
72
packages/components/accordion/package.json
Normal file
72
packages/components/accordion/package.json
Normal file
@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "@nextui-org/accordion",
|
||||
"version": "2.0.0-beta.1",
|
||||
"description": "Collapse display a list of high-level options that can expand/collapse to reveal more information.",
|
||||
"keywords": [
|
||||
"react",
|
||||
"accordion",
|
||||
"collapse",
|
||||
"display",
|
||||
"list",
|
||||
"expand",
|
||||
"tree view"
|
||||
],
|
||||
"author": "Junior Garcia <jrgarciadev@gmail.com>",
|
||||
"homepage": "https://nextui.org",
|
||||
"license": "MIT",
|
||||
"main": "src/index.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nextui-org/nextui.git",
|
||||
"directory": "packages/components/accordion"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/nextui-org/nextui/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src --dts",
|
||||
"build:fast": "tsup src",
|
||||
"dev": "yarn build:fast -- --watch",
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepack": "clean-package",
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/aria-utils": "workspace:*",
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "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",
|
||||
"framer-motion": "^5.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-types/accordion": "3.0.0-alpha.12",
|
||||
"@react-types/shared": "^3.17.0",
|
||||
"clean-package": "2.2.0",
|
||||
"react": "^18.0.0"
|
||||
},
|
||||
"tsup": {
|
||||
"clean": true,
|
||||
"target": "es2019",
|
||||
"format": [
|
||||
"cjs",
|
||||
"esm"
|
||||
]
|
||||
}
|
||||
}
|
||||
10
packages/components/accordion/src/accordion-context.ts
Normal file
10
packages/components/accordion/src/accordion-context.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {createContext} from "@nextui-org/shared-utils";
|
||||
|
||||
import {ContextType} from "./use-accordion";
|
||||
|
||||
export const [AccordionProvider, useAccordionContext] = createContext<ContextType>({
|
||||
name: "AccordionContext",
|
||||
strict: true,
|
||||
errorMessage:
|
||||
"useAccordionContext: `context` is undefined. Seems you forgot to wrap component within the Provider",
|
||||
});
|
||||
60
packages/components/accordion/src/accordion-item.tsx
Normal file
60
packages/components/accordion/src/accordion-item.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {useMemo, ReactNode} from "react";
|
||||
import {ChevronIcon} from "@nextui-org/shared-icons";
|
||||
import {motion, AnimatePresence} from "framer-motion";
|
||||
|
||||
import {UseAccordionItemProps, useAccordionItem} from "./use-accordion-item";
|
||||
|
||||
export interface AccordionItemProps extends Omit<UseAccordionItemProps, "ref"> {}
|
||||
|
||||
const Accordion = forwardRef<AccordionItemProps, "div">((props, ref) => {
|
||||
const {
|
||||
Component,
|
||||
item,
|
||||
isOpen,
|
||||
isDisabled,
|
||||
getBaseProps,
|
||||
getHeadingProps,
|
||||
getButtonProps,
|
||||
getTitleProps,
|
||||
getSubtitleProps,
|
||||
getContentProps,
|
||||
getIndicatorProps,
|
||||
} = useAccordionItem({ref, ...props});
|
||||
|
||||
const indicator = useMemo<ReactNode | null>(() => {
|
||||
if (typeof item.props?.indicator === "function") {
|
||||
return item.props?.indicator({indicator: <ChevronIcon />, isOpen, isDisabled});
|
||||
}
|
||||
|
||||
return <ChevronIcon />;
|
||||
}, [item.props?.indicator, isOpen, isDisabled]);
|
||||
|
||||
return (
|
||||
<Component {...getBaseProps()}>
|
||||
<h2 {...getHeadingProps()}>
|
||||
<button {...getButtonProps()}>
|
||||
{item.props?.title && <h3 {...getTitleProps()}>{item.props?.title}</h3>}
|
||||
{item.props?.subtitle && <p {...getSubtitleProps()}>{item.props?.subtitle}</p>}
|
||||
{indicator && <span {...getIndicatorProps()}>{indicator}</span>}
|
||||
</button>
|
||||
</h2>
|
||||
<AnimatePresence>
|
||||
{isOpen ? (
|
||||
<motion.div
|
||||
{...getContentProps()}
|
||||
animate={{opacity: 1}}
|
||||
exit={{opacity: 0}}
|
||||
initial={{opacity: 0}}
|
||||
>
|
||||
{item.props?.children}
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
Accordion.displayName = "NextUI.AccordionItem";
|
||||
|
||||
export default Accordion;
|
||||
32
packages/components/accordion/src/accordion.tsx
Normal file
32
packages/components/accordion/src/accordion.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
|
||||
import {UseAccordionProps, useAccordion} from "./use-accordion";
|
||||
import {AccordionProvider} from "./accordion-context";
|
||||
import AccordionItem from "./accordion-item";
|
||||
|
||||
export interface AccordionProps extends Omit<UseAccordionProps, "ref"> {}
|
||||
|
||||
const AccordionGroup = forwardRef<AccordionProps, "div">((props, ref) => {
|
||||
const {Component, context, state, getBaseProps, handleFocusChanged} = useAccordion({
|
||||
ref,
|
||||
...props,
|
||||
});
|
||||
|
||||
return (
|
||||
<Component {...getBaseProps()}>
|
||||
<AccordionProvider value={context}>
|
||||
{[...state.collection].map((item) => (
|
||||
<AccordionItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
onFocusChange={(isFocused) => handleFocusChanged(isFocused, item.key)}
|
||||
/>
|
||||
))}
|
||||
</AccordionProvider>
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
AccordionGroup.displayName = "NextUI.Accordion";
|
||||
|
||||
export default AccordionGroup;
|
||||
@ -0,0 +1,42 @@
|
||||
import {BaseItem, ItemProps} from "@nextui-org/aria-utils";
|
||||
import {FocusableProps} from "@react-types/shared";
|
||||
import {ReactNode} from "react";
|
||||
|
||||
export type AccordionItemIndicatorProps = {
|
||||
/**
|
||||
* The current indicator, usually an arrow icon.
|
||||
*/
|
||||
indicator?: ReactNode;
|
||||
/**
|
||||
* The current open status.
|
||||
*/
|
||||
isOpen?: boolean;
|
||||
/**
|
||||
* The current disabled status.
|
||||
* @default false
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export interface AccordionItemBaseProps<T extends object = {}>
|
||||
extends Omit<ItemProps<"button", T>, "children" | keyof FocusableProps>,
|
||||
FocusableProps {
|
||||
/**
|
||||
* The content of the component.
|
||||
*/
|
||||
children?: ReactNode | null;
|
||||
/**
|
||||
* The accordion item subtitle.
|
||||
*/
|
||||
subtitle?: ReactNode | string;
|
||||
/**
|
||||
* The accordion item `expanded` indicator, it's usually an arrow icon.
|
||||
* If you pass a function, NextUI will expose the current indicator and the open status,
|
||||
* In case you want to use a custom indicator or muodify the current one.
|
||||
*/
|
||||
indicator?: ReactNode | ((props: AccordionItemIndicatorProps) => ReactNode) | null;
|
||||
}
|
||||
|
||||
const AccordionItemBase = BaseItem as (props: AccordionItemBaseProps) => JSX.Element;
|
||||
|
||||
export default AccordionItemBase;
|
||||
18
packages/components/accordion/src/index.ts
Normal file
18
packages/components/accordion/src/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import AccordionItem from "./base/accordion-item-base";
|
||||
import Accordion from "./accordion";
|
||||
|
||||
// export types
|
||||
export type {AccordionProps} from "./accordion";
|
||||
export type {AccordionItemProps} from "./accordion-item";
|
||||
export type {AccordionItemIndicatorProps} from "./base/accordion-item-base";
|
||||
export type {AccordionItemBaseProps} from "./base/accordion-item-base";
|
||||
|
||||
// export hooks
|
||||
export {useAccordionItem} from "./use-accordion-item";
|
||||
export {useAccordion} from "./use-accordion";
|
||||
|
||||
// export context
|
||||
export * from "./accordion-context";
|
||||
|
||||
// export component
|
||||
export {Accordion, AccordionItem};
|
||||
221
packages/components/accordion/src/use-accordion-item.ts
Normal file
221
packages/components/accordion/src/use-accordion-item.ts
Normal file
@ -0,0 +1,221 @@
|
||||
import type {
|
||||
AccordionItemVariantProps,
|
||||
AccordionItemSlots,
|
||||
SlotsToClasses,
|
||||
} from "@nextui-org/theme";
|
||||
|
||||
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} 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";
|
||||
|
||||
import {AccordionItemBaseProps} from "./base/accordion-item-base";
|
||||
import {useAccordionContext} from "./accordion-context";
|
||||
|
||||
export interface Props<T extends object> extends HTMLNextUIProps<"div"> {
|
||||
/**
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
ref?: ReactRef<HTMLButtonElement | null>;
|
||||
/**
|
||||
* The item node.
|
||||
*/
|
||||
item: NodeWithProps<T, AccordionItemBaseProps<T>>;
|
||||
/**
|
||||
* Classname or List of classes to change the styles of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* <AccordionItem styles={{
|
||||
* base:"base-classes",
|
||||
* heading: "heading-classes",
|
||||
* indicator: "indicator-classes",
|
||||
* trigger: "trigger-classes",
|
||||
* content: "content-classes",
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
styles?: SlotsToClasses<AccordionItemSlots>;
|
||||
}
|
||||
|
||||
export type UseAccordionItemProps<T extends object = {}> = Props<T> &
|
||||
AccordionItemVariantProps &
|
||||
AccordionItemBaseProps;
|
||||
|
||||
export function useAccordionItem<T extends object = {}>(props: UseAccordionItemProps<T>) {
|
||||
const groupContext = useAccordionContext();
|
||||
|
||||
const {
|
||||
ref,
|
||||
as,
|
||||
styles,
|
||||
className,
|
||||
item,
|
||||
size = groupContext?.size ?? "md",
|
||||
radius = groupContext?.radius ?? "lg",
|
||||
isDisabled: isDisabledProp = false,
|
||||
disableAnimation = groupContext?.disableAnimation ?? false,
|
||||
onFocusChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const Component = as || "div";
|
||||
|
||||
const domRef = useDOMRef<HTMLButtonElement>(ref);
|
||||
|
||||
const state = groupContext?.state;
|
||||
|
||||
const {buttonProps: buttonCompleteProps, regionProps} = useBaseAccordion(
|
||||
{item},
|
||||
{...state, focusedKey: groupContext.focusedKey},
|
||||
domRef,
|
||||
);
|
||||
|
||||
const {onFocus: onFocusButton, onBlur: onBlurButton, ...buttonProps} = buttonCompleteProps;
|
||||
|
||||
const {isFocused, isFocusVisible, focusProps} = useFocusRing({
|
||||
autoFocus: item.props?.autoFocus,
|
||||
});
|
||||
|
||||
const isDisabled = state.disabledKeys.has(item.key) || isDisabledProp || groupContext?.isDisabled;
|
||||
const isOpen = state.selectionManager.isSelected(item.key);
|
||||
|
||||
const handleFocus = () => {
|
||||
onFocusChange?.(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
onFocusChange?.(false);
|
||||
};
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
accordionItem({
|
||||
size,
|
||||
radius,
|
||||
isDisabled,
|
||||
isFocusVisible,
|
||||
disableAnimation,
|
||||
}),
|
||||
[size, radius, isDisabled, isFocusVisible, disableAnimation],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(styles?.base, className);
|
||||
|
||||
const getBaseProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
return {
|
||||
className: slots.base({class: baseStyles}),
|
||||
...mergeProps(otherProps, props),
|
||||
};
|
||||
},
|
||||
[baseStyles, otherProps],
|
||||
);
|
||||
|
||||
const getButtonProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
return {
|
||||
ref: domRef,
|
||||
"data-focus-visible": dataAttr(isFocusVisible),
|
||||
"data-focused": dataAttr(isFocused),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
className: slots.trigger({class: styles?.trigger}),
|
||||
onFocus: callAllHandlers(
|
||||
handleFocus,
|
||||
onFocusButton,
|
||||
focusProps.onFocus,
|
||||
otherProps.onFocus,
|
||||
item.props?.onFocus,
|
||||
),
|
||||
onBlur: callAllHandlers(
|
||||
handleBlur,
|
||||
onBlurButton,
|
||||
focusProps.onBlur,
|
||||
otherProps.onBlur,
|
||||
item.props?.onBlur,
|
||||
),
|
||||
...mergeProps(buttonProps, props),
|
||||
};
|
||||
},
|
||||
[domRef, isFocusVisible, isFocused, isDisabled, buttonProps, focusProps, slots, styles],
|
||||
);
|
||||
|
||||
const getContentProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
return {
|
||||
"data-open": dataAttr(isOpen),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
className: slots.content({class: styles?.content}),
|
||||
...mergeProps(regionProps, props),
|
||||
};
|
||||
},
|
||||
[slots, styles, regionProps, isOpen, isDisabled],
|
||||
);
|
||||
|
||||
const getIndicatorProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
return {
|
||||
"aria-hidden": dataAttr(true),
|
||||
"data-open": dataAttr(isOpen),
|
||||
"data-disabled": dataAttr(isDisabled),
|
||||
className: slots.indicator({class: styles?.indicator}),
|
||||
...props,
|
||||
};
|
||||
},
|
||||
[slots, styles, isOpen, isDisabled],
|
||||
);
|
||||
|
||||
const getHeadingProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
return {
|
||||
className: slots.heading({class: styles?.heading}),
|
||||
...props,
|
||||
};
|
||||
},
|
||||
[slots, styles],
|
||||
);
|
||||
|
||||
const getTitleProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
return {
|
||||
className: slots.title({class: styles?.title}),
|
||||
...props,
|
||||
};
|
||||
},
|
||||
[slots, styles],
|
||||
);
|
||||
|
||||
const getSubtitleProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
return {
|
||||
className: slots.subtitle({class: styles?.subtitle}),
|
||||
...props,
|
||||
};
|
||||
},
|
||||
[slots, styles],
|
||||
);
|
||||
|
||||
return {
|
||||
Component,
|
||||
item,
|
||||
slots,
|
||||
styles,
|
||||
domRef,
|
||||
isOpen,
|
||||
isDisabled,
|
||||
getBaseProps,
|
||||
getHeadingProps,
|
||||
getButtonProps,
|
||||
getContentProps,
|
||||
getIndicatorProps,
|
||||
getTitleProps,
|
||||
getSubtitleProps,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseAccordionItemReturn = ReturnType<typeof useAccordionItem>;
|
||||
146
packages/components/accordion/src/use-accordion.ts
Normal file
146
packages/components/accordion/src/use-accordion.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
|
||||
import type {ReactRef} from "@nextui-org/shared-utils";
|
||||
import type {SelectionBehavior, MultipleSelection} from "@react-types/shared";
|
||||
import type {AriaAccordionProps} from "@react-types/accordion";
|
||||
|
||||
import {Key, useCallback} from "react";
|
||||
import {TreeState, useTreeState} from "@react-stately/tree";
|
||||
import {useAccordion as useReactAriaAccordion} from "@react-aria/accordion";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {accordion} from "@nextui-org/theme";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {useMemo, useState} from "react";
|
||||
|
||||
import {AccordionItemProps} from "./accordion-item";
|
||||
|
||||
interface Props extends HTMLNextUIProps<"div"> {
|
||||
/**
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
ref?: ReactRef<HTMLDivElement | null>;
|
||||
/**
|
||||
* The accordion selection behavior.
|
||||
* @default "toggle"
|
||||
*/
|
||||
selectionBehavior?: SelectionBehavior;
|
||||
}
|
||||
|
||||
export type UseAccordionProps<T extends object = {}> = Props &
|
||||
Pick<AccordionItemProps, "size" | "radius" | "isDisabled" | "disableAnimation"> &
|
||||
AriaAccordionProps<T> &
|
||||
MultipleSelection;
|
||||
|
||||
export type ContextType<T extends object = {}> = {
|
||||
state: TreeState<T>;
|
||||
focusedKey?: Key | null;
|
||||
size?: AccordionItemProps["size"];
|
||||
radius?: AccordionItemProps["radius"];
|
||||
isDisabled?: AccordionItemProps["isDisabled"];
|
||||
disableAnimation?: AccordionItemProps["disableAnimation"];
|
||||
};
|
||||
|
||||
export function useAccordion<T extends object>(props: UseAccordionProps<T>) {
|
||||
const {
|
||||
ref,
|
||||
as,
|
||||
children,
|
||||
className,
|
||||
items,
|
||||
expandedKeys,
|
||||
defaultExpandedKeys,
|
||||
disabledKeys,
|
||||
selectedKeys,
|
||||
selectionMode = "single",
|
||||
selectionBehavior = "toggle",
|
||||
disallowEmptySelection,
|
||||
defaultSelectedKeys,
|
||||
onExpandedChange,
|
||||
onSelectionChange,
|
||||
size = "md",
|
||||
radius = "lg",
|
||||
isDisabled = false,
|
||||
disableAnimation = false,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [focusedKey, setFocusedKey] = useState<Key | null>(null);
|
||||
|
||||
const Component = as || "div";
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const styles = useMemo(
|
||||
() =>
|
||||
accordion({
|
||||
className,
|
||||
}),
|
||||
[className],
|
||||
);
|
||||
|
||||
const commonProps = {
|
||||
children,
|
||||
items,
|
||||
};
|
||||
|
||||
const expandableProps = {
|
||||
expandedKeys,
|
||||
defaultExpandedKeys,
|
||||
onExpandedChange,
|
||||
};
|
||||
|
||||
const treeProps = {
|
||||
disabledKeys,
|
||||
selectedKeys,
|
||||
selectionMode,
|
||||
selectionBehavior,
|
||||
disallowEmptySelection,
|
||||
defaultSelectedKeys: defaultSelectedKeys ?? defaultExpandedKeys,
|
||||
onSelectionChange,
|
||||
...commonProps,
|
||||
...expandableProps,
|
||||
};
|
||||
|
||||
const state = useTreeState(treeProps);
|
||||
|
||||
state.selectionManager.setFocusedKey = (key: Key | null) => {
|
||||
setFocusedKey(key);
|
||||
};
|
||||
|
||||
const {accordionProps} = useReactAriaAccordion(
|
||||
{
|
||||
...commonProps,
|
||||
...expandableProps,
|
||||
},
|
||||
state,
|
||||
domRef,
|
||||
);
|
||||
|
||||
const context: ContextType<T> = useMemo(
|
||||
() => ({
|
||||
size,
|
||||
radius,
|
||||
focusedKey,
|
||||
state,
|
||||
isDisabled,
|
||||
disableAnimation,
|
||||
}),
|
||||
[size, radius, state, focusedKey, isDisabled, disableAnimation],
|
||||
);
|
||||
|
||||
const getBaseProps: PropGetter = useCallback((props = {}) => {
|
||||
return {
|
||||
ref: domRef,
|
||||
className: styles,
|
||||
"data-orientation": "vertical",
|
||||
...mergeProps(accordionProps, otherProps, props),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleFocusChanged = useCallback((isFocused: boolean, key: Key | null) => {
|
||||
isFocused && setFocusedKey(key);
|
||||
}, []);
|
||||
|
||||
return {Component, context, styles, state, focusedKey, getBaseProps, handleFocusChanged};
|
||||
}
|
||||
|
||||
export type UseAccordionReturn = ReturnType<typeof useAccordion>;
|
||||
65
packages/components/accordion/stories/accordion.stories.tsx
Normal file
65
packages/components/accordion/stories/accordion.stories.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||
import {accordion} from "@nextui-org/theme";
|
||||
|
||||
import {Accordion, AccordionProps, AccordionItem} from "../src";
|
||||
|
||||
export default {
|
||||
title: "Components/Accordion",
|
||||
component: Accordion,
|
||||
argTypes: {
|
||||
color: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
|
||||
},
|
||||
},
|
||||
radius: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["none", "base", "sm", "md", "lg", "xl", "full"],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["xs", "sm", "md", "lg", "xl"],
|
||||
},
|
||||
},
|
||||
isDisabled: {
|
||||
control: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Accordion>;
|
||||
|
||||
const defaultProps = {
|
||||
...accordion.defaultVariants,
|
||||
selectionMode: "single",
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof Accordion> = (args: AccordionProps) => (
|
||||
<Accordion {...args}>
|
||||
<AccordionItem key="1" title="Accordion 1">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" title="Accordion 2">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" title="Accordion 3">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
|
||||
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
|
||||
laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
10
packages/components/accordion/tsconfig.json
Normal file
10
packages/components/accordion/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"tailwind-variants": ["../../../node_modules/tailwind-variants"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "index.ts"]
|
||||
}
|
||||
67
packages/core/theme/src/components/accordion-item.ts
Normal file
67
packages/core/theme/src/components/accordion-item.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import {tv, type VariantProps} from "tailwind-variants";
|
||||
|
||||
import {ringClasses} from "../utils";
|
||||
/**
|
||||
* AccordionItem wrapper **Tailwind Variants** component
|
||||
*
|
||||
* const {base, heading, indicator, trigger, content } = accordionItem({...})
|
||||
*
|
||||
* @example
|
||||
* <div className={base())}>
|
||||
* <div className={heading())}>
|
||||
* <button className={trigger())}>Trigger</button>
|
||||
* <span className={indicator())}>Indicator</span>
|
||||
* </div>
|
||||
* <div className={content())}>Content</div>
|
||||
* </div>
|
||||
*/
|
||||
const accordionItem = tv({
|
||||
slots: {
|
||||
base: "py-2 border-b border-neutral",
|
||||
heading: "",
|
||||
trigger: "py-2 flex w-full outline-none items-center",
|
||||
indicator: "rotate-0 data-[open=true]:-rotate-90",
|
||||
title: "flex-1 text-left text-foreground",
|
||||
subtitle: "",
|
||||
content: "hidden data-[open=true]:block",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
xs: {},
|
||||
sm: {},
|
||||
md: {},
|
||||
lg: {},
|
||||
xl: {},
|
||||
},
|
||||
radius: {
|
||||
none: {},
|
||||
base: {},
|
||||
sm: {},
|
||||
md: {},
|
||||
lg: {},
|
||||
xl: {},
|
||||
"2xl": {},
|
||||
"3xl": {},
|
||||
full: {},
|
||||
},
|
||||
isDisabled: {
|
||||
true: {base: "opacity-50 pointer-events-none"},
|
||||
},
|
||||
isFocusVisible: {
|
||||
true: {
|
||||
trigger: [...ringClasses],
|
||||
},
|
||||
},
|
||||
disableAnimation: {
|
||||
true: {},
|
||||
false: {
|
||||
indicator: "transition-transform",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type AccordionItemVariantProps = VariantProps<typeof accordionItem>;
|
||||
export type AccordionItemSlots = keyof ReturnType<typeof accordionItem>;
|
||||
|
||||
export {accordionItem};
|
||||
20
packages/core/theme/src/components/accordion.ts
Normal file
20
packages/core/theme/src/components/accordion.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {tv, VariantProps} from "tailwind-variants";
|
||||
|
||||
/**
|
||||
* Accordion wrapper **Tailwind Variants** component
|
||||
*
|
||||
* const styles = accordion({...})
|
||||
*
|
||||
* @example
|
||||
* <div role="group" className={styles())}>
|
||||
* // avatar elements
|
||||
* </div>
|
||||
*/
|
||||
const accordion = tv({
|
||||
base: "px-2",
|
||||
variants: {},
|
||||
});
|
||||
|
||||
export type AccordionGroupVariantProps = VariantProps<typeof accordion>;
|
||||
|
||||
export {accordion};
|
||||
@ -17,3 +17,5 @@ export * from "./radio";
|
||||
export * from "./radio-group";
|
||||
export * from "./pagination";
|
||||
export * from "./toggle";
|
||||
export * from "./accordion-item";
|
||||
export * from "./accordion";
|
||||
|
||||
@ -16,6 +16,7 @@ module.exports = {
|
||||
"../../components/radio/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../components/pagination/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../components/switch/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||
"../../components/accordion/stories/*.stories.@(js|jsx|ts|tsx)",
|
||||
],
|
||||
staticDirs: ["../public"],
|
||||
addons: [
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
useId,
|
||||
useCallback,
|
||||
useEffect,
|
||||
KeyboardEventHandler,
|
||||
@ -8,7 +9,6 @@ import {
|
||||
} from "react";
|
||||
import {DOMAttributes, Node, LongPressEvent, PressEvent} from "@react-types/shared";
|
||||
import {focusSafely} from "@react-aria/focus";
|
||||
import {useId} from "@react-aria/utils";
|
||||
import {TreeState} from "@react-stately/tree";
|
||||
import {useButton} from "@react-aria/button";
|
||||
|
||||
|
||||
2374
pnpm-lock.yaml
generated
2374
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user