mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(components): collapse component added, styles, tests & stories still missing , other refactors
This commit is contained in:
parent
37c3049133
commit
462597b822
@ -10,7 +10,7 @@ import {useState, useEffect, useMemo} from "react";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {HTMLNextUIProps, forwardRef} from "@nextui-org/system";
|
||||
import {useDOMRef, IFocusRingAria} from "@nextui-org/dom-utils";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx, safeText, __DEV__} from "@nextui-org/shared-utils";
|
||||
|
||||
import {StyledAvatar} from "./avatar.styles";
|
||||
@ -67,7 +67,7 @@ const Avatar = forwardRef<AvatarProps, "span", CompundAvatar>((props, ref) => {
|
||||
const showText = !src;
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const {isFocusVisible, focusProps}: IFocusRingAria<AvatarProps> = useFocusRing();
|
||||
const {isFocusVisible, focusProps} = useFocusRing();
|
||||
|
||||
useEffect(() => {
|
||||
imgRef?.current?.complete && setReady(true);
|
||||
|
||||
@ -9,7 +9,7 @@ import {useFocusRing} from "@react-aria/focus";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {useDrip} from "@nextui-org/drip";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
import {IFocusRingAria, useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {__DEV__, warn} from "@nextui-org/shared-utils";
|
||||
|
||||
import {getColors} from "./button-utils";
|
||||
@ -213,7 +213,7 @@ export function useButton(props: UseButtonProps) {
|
||||
|
||||
const {hoverProps, isHovered} = useHover({isDisabled: disabled});
|
||||
|
||||
const {isFocused, isFocusVisible, focusProps}: IFocusRingAria<UseButtonProps> = useFocusRing({
|
||||
const {isFocused, isFocusVisible, focusProps} = useFocusRing({
|
||||
autoFocus,
|
||||
});
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import {useFocusRing} from "@react-aria/focus";
|
||||
import {usePress, useHover} from "@react-aria/interactions";
|
||||
import {HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {NormalWeights, ReactRef, warn} from "@nextui-org/shared-utils";
|
||||
import {useDOMRef, IFocusRingAria} from "@nextui-org/dom-utils";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {useDrip} from "@nextui-org/drip";
|
||||
|
||||
export interface UseCardProps extends HTMLNextUIProps<"div", PressEvents & FocusableProps> {
|
||||
@ -106,7 +106,7 @@ export function useCard(props: UseCardProps) {
|
||||
...otherProps,
|
||||
});
|
||||
|
||||
const {isFocusVisible, focusProps}: IFocusRingAria<Pick<UseCardProps, "css">> = useFocusRing({
|
||||
const {isFocusVisible, focusProps} = useFocusRing({
|
||||
autoFocus,
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type {AriaCheckboxProps} from "@react-types/checkbox";
|
||||
import type {HTMLNextUIProps, CSS} from "@nextui-org/system";
|
||||
import type {IPressResult, IFocusRingAria} from "@nextui-org/dom-utils";
|
||||
|
||||
import {useMemo, useRef} from "react";
|
||||
import {useToggleState} from "@react-stately/toggle";
|
||||
@ -125,11 +124,11 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
});
|
||||
|
||||
// TODO: Event's propagation wasn't stopped https://github.com/adobe/react-spectrum/issues/2383
|
||||
const {pressProps}: IPressResult<UseCheckboxProps> = usePress({
|
||||
const {pressProps} = usePress({
|
||||
isDisabled: inputProps.disabled,
|
||||
});
|
||||
|
||||
const {focusProps, isFocusVisible}: IFocusRingAria<UseCheckboxProps> = useFocusRing({
|
||||
const {focusProps, isFocusVisible} = useFocusRing({
|
||||
autoFocus: inputProps.autoFocus,
|
||||
});
|
||||
|
||||
|
||||
24
packages/components/collapse/README.md
Normal file
24
packages/components/collapse/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# @nextui-org/collapse
|
||||
|
||||
A Quick description of the component
|
||||
|
||||
> This is an internal utility, not intended for public usage.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add @nextui-org/collapse
|
||||
# or
|
||||
npm i @nextui-org/collapse
|
||||
```
|
||||
|
||||
## 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/collapse/__tests__/collapse.test.tsx
Normal file
19
packages/components/collapse/__tests__/collapse.test.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import {render} from "@testing-library/react";
|
||||
|
||||
import { Collapse } from "../src";
|
||||
|
||||
describe("Collapse", () => {
|
||||
it("should render correctly", () => {
|
||||
const wrapper = render(<Collapse />);
|
||||
|
||||
expect(() => wrapper.unmount()).not.toThrow();
|
||||
});
|
||||
|
||||
it("ref should be forwarded", () => {
|
||||
const ref = React.createRef<HTMLDivElement>();
|
||||
|
||||
render(<Collapse ref={ref} />);
|
||||
expect(ref.current).not.toBeNull();
|
||||
});
|
||||
});
|
||||
3
packages/components/collapse/clean-package.config.json
Normal file
3
packages/components/collapse/clean-package.config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{ "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" } } }
|
||||
56
packages/components/collapse/package.json
Normal file
56
packages/components/collapse/package.json
Normal file
@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@nextui-org/collapse",
|
||||
"version": "1.0.0-beta.11",
|
||||
"description": "Collapse display a list of high-level options that can expand/collapse to reveal more information.",
|
||||
"keywords": [
|
||||
"collapse"
|
||||
],
|
||||
"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/collapse"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/nextui-org/nextui/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format=esm,cjs --dts",
|
||||
"dev": "yarn build:fast -- --watch",
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:fast": "tsup src/index.ts --format=esm,cjs",
|
||||
"prepack": "clean-package",
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@nextui-org/shared-css": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/aria-utils": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@react-aria/accordion": "3.0.0-alpha.12",
|
||||
"@react-aria/focus": "^3.9.0",
|
||||
"@react-aria/utils": "^3.14.0",
|
||||
"@react-stately/tree": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-types/shared": "^3.15.0",
|
||||
"@react-types/accordion": "3.0.0-alpha.10",
|
||||
"clean-package": "2.1.1",
|
||||
"react": "^17.0.2"
|
||||
}
|
||||
}
|
||||
16
packages/components/collapse/src/base/collapse-item-base.tsx
Normal file
16
packages/components/collapse/src/base/collapse-item-base.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import {BaseItem, ItemProps} from "@nextui-org/aria-utils";
|
||||
import {FocusableProps} from "@react-types/shared";
|
||||
import {ReactNode} from "react";
|
||||
|
||||
export type CollapseItemBaseProps<T extends object = {}> = Omit<
|
||||
ItemProps<"button", T>,
|
||||
"children"
|
||||
> & {
|
||||
children?: ReactNode | null;
|
||||
} & FocusableProps;
|
||||
|
||||
const CollapseItem = BaseItem as (props: CollapseItemBaseProps) => JSX.Element;
|
||||
|
||||
CollapseItem.toString = () => ".nextui-collapse-item-base";
|
||||
|
||||
export default CollapseItem;
|
||||
67
packages/components/collapse/src/collapse-item.tsx
Normal file
67
packages/components/collapse/src/collapse-item.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {clsx, __DEV__} from "@nextui-org/shared-utils";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
|
||||
import {StyledCollapseItem, StyledCollapseItemButton} from "./collapse.styles";
|
||||
import {UseCollapseItemProps, useCollapseItem} from "./use-collapse-item";
|
||||
|
||||
export interface CollapseItemProps<T extends object = {}>
|
||||
extends Omit<UseCollapseItemProps<T>, "ref"> {}
|
||||
|
||||
const CollapseItem = forwardRef<CollapseItemProps, "div">((props, ref) => {
|
||||
const {
|
||||
className,
|
||||
domRef,
|
||||
item,
|
||||
isOpen,
|
||||
isDisabled,
|
||||
isFocusVisible,
|
||||
buttonProps,
|
||||
regionProps,
|
||||
focusProps,
|
||||
...otherProps
|
||||
} = useCollapseItem({
|
||||
ref,
|
||||
...props,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledCollapseItem
|
||||
className={clsx("nextui-collapse-item", className)}
|
||||
isDisabled={isDisabled}
|
||||
isOpen={isOpen}
|
||||
{...otherProps}
|
||||
>
|
||||
<h2 className="collapse-item-heading">
|
||||
<StyledCollapseItemButton
|
||||
{...mergeProps(buttonProps, focusProps)}
|
||||
ref={domRef}
|
||||
className="collapse-item-button"
|
||||
disabled={isDisabled}
|
||||
isFocusVisible={isFocusVisible}
|
||||
>
|
||||
{item.props.title}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
aria-label="collapse item indicator"
|
||||
className="collapse-item-indicator"
|
||||
role="img"
|
||||
>
|
||||
{isOpen ? "🔽" : "▶️"}️
|
||||
</span>
|
||||
</StyledCollapseItemButton>
|
||||
</h2>
|
||||
<div {...regionProps} className="collapse-item-content">
|
||||
{item.props.children}
|
||||
</div>
|
||||
</StyledCollapseItem>
|
||||
);
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
CollapseItem.displayName = "NextUI.CollapseItem";
|
||||
}
|
||||
|
||||
CollapseItem.toString = () => ".nextui-collapse-item";
|
||||
|
||||
export default CollapseItem;
|
||||
56
packages/components/collapse/src/collapse.styles.ts
Normal file
56
packages/components/collapse/src/collapse.styles.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {styled} from "@nextui-org/system";
|
||||
import {cssFocusVisible} from "@nextui-org/shared-css";
|
||||
|
||||
export const StyledCollapse = styled("div", {
|
||||
width: "100%",
|
||||
maxWidth: "400px",
|
||||
display: "block",
|
||||
listStyle: "none",
|
||||
padding: "0",
|
||||
margin: "0",
|
||||
});
|
||||
|
||||
export const StyledCollapseItem = styled("div", {
|
||||
zIndex: "inherit",
|
||||
position: "relative",
|
||||
display: "list-item",
|
||||
margin: "0",
|
||||
marginBottom: "$4",
|
||||
borderBottom: "#333333 solid transparent",
|
||||
"&:first-of-type": {
|
||||
borderTop: "#333333 solid transparent",
|
||||
},
|
||||
".collapse-item-heading": {
|
||||
margin: 0,
|
||||
},
|
||||
".collapse-item-indicator": {
|
||||
marginLeft: "8px",
|
||||
},
|
||||
".collapse-item-content": {
|
||||
display: "none",
|
||||
background: "red",
|
||||
padding: "6px 10px 10px",
|
||||
},
|
||||
variants: {
|
||||
isOpen: {
|
||||
true: {
|
||||
".collapse-item-content": {
|
||||
display: "block",
|
||||
},
|
||||
},
|
||||
},
|
||||
isSelectable: {},
|
||||
isDisabled: {
|
||||
true: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const StyledCollapseItemButton = styled(
|
||||
"button",
|
||||
{
|
||||
textAlign: "start",
|
||||
width: "100%",
|
||||
},
|
||||
cssFocusVisible,
|
||||
);
|
||||
47
packages/components/collapse/src/collapse.tsx
Normal file
47
packages/components/collapse/src/collapse.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
import {clsx, __DEV__} from "@nextui-org/shared-utils";
|
||||
|
||||
import CollapseItemBase from "./base/collapse-item-base";
|
||||
import CollapseItem from "./collapse-item";
|
||||
import {StyledCollapse} from "./collapse.styles";
|
||||
import {UseCollapseProps, useCollapse} from "./use-collapse";
|
||||
|
||||
export interface CollapseProps<T extends object = {}> extends Omit<UseCollapseProps<T>, "ref"> {}
|
||||
|
||||
type CompoundCollapse = {
|
||||
Item: typeof CollapseItemBase;
|
||||
};
|
||||
|
||||
const Collapse = forwardRef<CollapseProps, "div", CompoundCollapse>((props, ref) => {
|
||||
const {className, domRef, state, focusedKey, setFocusedKey, collapseProps, ...otherProps} =
|
||||
useCollapse({ref, ...props});
|
||||
|
||||
return (
|
||||
<StyledCollapse
|
||||
ref={domRef}
|
||||
className={clsx("nextui-collapse", className)}
|
||||
{...mergeProps(collapseProps, otherProps)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<CollapseItem
|
||||
key={item.key}
|
||||
focusedKey={focusedKey}
|
||||
item={item}
|
||||
state={state}
|
||||
onFocusChange={(isFocused) => isFocused && setFocusedKey(item.key)}
|
||||
/>
|
||||
))}
|
||||
</StyledCollapse>
|
||||
);
|
||||
});
|
||||
|
||||
Collapse.Item = CollapseItemBase;
|
||||
|
||||
if (__DEV__) {
|
||||
Collapse.displayName = "NextUI.Collapse";
|
||||
}
|
||||
|
||||
Collapse.toString = () => ".nextui-collapse";
|
||||
|
||||
export default Collapse;
|
||||
5
packages/components/collapse/src/index.ts
Normal file
5
packages/components/collapse/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
// export types
|
||||
export type {CollapseProps} from "./collapse";
|
||||
|
||||
// export component
|
||||
export {default as Collapse} from "./collapse";
|
||||
74
packages/components/collapse/src/use-collapse-item.ts
Normal file
74
packages/components/collapse/src/use-collapse-item.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import type {CollapseItemBaseProps} from "./base/collapse-item-base";
|
||||
|
||||
import {Node} from "@react-types/shared";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {TreeState} from "@react-stately/tree";
|
||||
import {callAllHandlers, ReactRef} from "@nextui-org/shared-utils";
|
||||
import {useAccordionItem} from "@nextui-org/aria-utils";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {Key} from "react";
|
||||
|
||||
export interface UseCollapseItemProps<T extends object> extends CollapseItemBaseProps<T> {
|
||||
/**
|
||||
* The collapse item ref.
|
||||
*/
|
||||
ref?: ReactRef<HTMLButtonElement | null>;
|
||||
/**
|
||||
* The current collapse focused key.
|
||||
* @internal
|
||||
* @info This prop is necessary for the focus to work properly.
|
||||
* Due to the fact that the selectionManager focusedKey is no a state.
|
||||
*/
|
||||
focusedKey?: Key | null;
|
||||
/**
|
||||
* The item node.
|
||||
*/
|
||||
item: Node<T>;
|
||||
/**
|
||||
* The tree state.
|
||||
*/
|
||||
state: TreeState<T>;
|
||||
}
|
||||
|
||||
export function useCollapseItem<T extends object>(props: UseCollapseItemProps<T>) {
|
||||
const {ref, state, item, focusedKey, onFocusChange, ...otherProps} = props;
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {buttonProps, regionProps} = useAccordionItem({item}, {...state, focusedKey}, domRef);
|
||||
const {isFocusVisible, focusProps} = useFocusRing({
|
||||
autoFocus: item.props.autoFocus,
|
||||
});
|
||||
|
||||
const isDisabled = state.disabledKeys.has(item.key);
|
||||
const isOpen = state.selectionManager.isSelected(item.key);
|
||||
|
||||
const handleFocus = () => {
|
||||
onFocusChange?.(true);
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
onFocusChange?.(false);
|
||||
};
|
||||
|
||||
return {
|
||||
domRef,
|
||||
item,
|
||||
isDisabled,
|
||||
isOpen,
|
||||
isFocusVisible,
|
||||
buttonProps,
|
||||
regionProps,
|
||||
focusProps,
|
||||
onFocus: callAllHandlers(
|
||||
handleFocus,
|
||||
focusProps.onFocus,
|
||||
otherProps.onFocus,
|
||||
item.props.onFocus,
|
||||
),
|
||||
onBlur: callAllHandlers(handleBlur, focusProps.onBlur, otherProps.onBlur, item.props.onBlur),
|
||||
...otherProps,
|
||||
};
|
||||
}
|
||||
|
||||
export type UseCollapseItemReturn = ReturnType<typeof useCollapseItem>;
|
||||
82
packages/components/collapse/src/use-collapse.ts
Normal file
82
packages/components/collapse/src/use-collapse.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import type {HTMLNextUIProps} from "@nextui-org/system";
|
||||
import type {MultipleSelection, SelectionBehavior} from "@react-types/shared";
|
||||
import type {ReactRef} from "@nextui-org/shared-utils";
|
||||
|
||||
import {useState, Key} from "react";
|
||||
import {AriaAccordionProps} from "@react-types/accordion";
|
||||
import {useTreeState} from "@react-stately/tree";
|
||||
import {useAccordion} from "@react-aria/accordion";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
|
||||
interface Props extends HTMLNextUIProps<"div"> {
|
||||
/**
|
||||
* The collapse ref.
|
||||
*/
|
||||
ref?: ReactRef<HTMLDivElement | null>;
|
||||
selectionBehavior?: SelectionBehavior;
|
||||
}
|
||||
|
||||
export type UseCollapseProps<T extends object> = Props & AriaAccordionProps<T> & MultipleSelection;
|
||||
|
||||
export function useCollapse<T extends object>(props: UseCollapseProps<T>) {
|
||||
const {
|
||||
ref,
|
||||
children,
|
||||
items,
|
||||
expandedKeys,
|
||||
defaultExpandedKeys,
|
||||
disabledKeys,
|
||||
selectedKeys,
|
||||
selectionMode = "single",
|
||||
selectionBehavior = "toggle",
|
||||
disallowEmptySelection,
|
||||
defaultSelectedKeys,
|
||||
onExpandedChange,
|
||||
onSelectionChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const [focusedKey, setFocusedKey] = useState<Key | null>(null);
|
||||
|
||||
const commonProps = {
|
||||
children,
|
||||
items,
|
||||
};
|
||||
|
||||
const expandableProps = {
|
||||
expandedKeys,
|
||||
defaultExpandedKeys,
|
||||
onExpandedChange,
|
||||
};
|
||||
|
||||
const treeProps = {
|
||||
disabledKeys,
|
||||
selectedKeys,
|
||||
selectionMode,
|
||||
selectionBehavior,
|
||||
disallowEmptySelection,
|
||||
defaultSelectedKeys: defaultSelectedKeys ?? defaultExpandedKeys,
|
||||
onSelectionChange,
|
||||
...commonProps,
|
||||
...expandableProps,
|
||||
};
|
||||
|
||||
const accordionProps = {
|
||||
...commonProps,
|
||||
...expandableProps,
|
||||
};
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const state = useTreeState(treeProps);
|
||||
|
||||
state.selectionManager.setFocusedKey = (key: Key | null) => {
|
||||
setFocusedKey(key);
|
||||
};
|
||||
|
||||
const {accordionProps: collapseProps} = useAccordion(accordionProps, state, domRef);
|
||||
|
||||
return {domRef, state, focusedKey, setFocusedKey, collapseProps, ...otherProps};
|
||||
}
|
||||
|
||||
export type UseCollapseReturn = ReturnType<typeof useCollapse>;
|
||||
53
packages/components/collapse/stories/collapse.stories.tsx
Normal file
53
packages/components/collapse/stories/collapse.stories.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import {Meta} from "@storybook/react";
|
||||
|
||||
import {Collapse} from "../src";
|
||||
|
||||
export default {
|
||||
title: "Surfaces/Collapse",
|
||||
component: Collapse,
|
||||
} as Meta;
|
||||
|
||||
export const Default = () => (
|
||||
<Collapse selectionMode="single">
|
||||
<Collapse.Item key="1" title="Your files">
|
||||
file
|
||||
</Collapse.Item>
|
||||
<Collapse.Item key="2" title="Shared with you">
|
||||
shared
|
||||
</Collapse.Item>
|
||||
<Collapse.Item key="3" title="Last item">
|
||||
last
|
||||
</Collapse.Item>
|
||||
<Collapse.Item key="4" title="Four item">
|
||||
four
|
||||
</Collapse.Item>
|
||||
</Collapse>
|
||||
);
|
||||
|
||||
export const Controlled = () => {
|
||||
const [expandedKeys, setExpandedKeys] = React.useState<Set<React.Key>>(new Set([]));
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
defaultExpandedKeys={["1"]}
|
||||
disabledKeys={["3"]}
|
||||
expandedKeys={expandedKeys}
|
||||
selectionMode="single"
|
||||
onExpandedChange={setExpandedKeys}
|
||||
>
|
||||
<Collapse.Item key="1" title="Your files">
|
||||
file
|
||||
</Collapse.Item>
|
||||
<Collapse.Item key="2" title="Shared with you">
|
||||
shared
|
||||
</Collapse.Item>
|
||||
<Collapse.Item key="3" title="Last item">
|
||||
last
|
||||
</Collapse.Item>
|
||||
<Collapse.Item key="4" title="Four item">
|
||||
four
|
||||
</Collapse.Item>
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
9
packages/components/collapse/tsconfig.json
Normal file
9
packages/components/collapse/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@stitches/react": ["../../../node_modules/@stitches/react"]
|
||||
}
|
||||
},
|
||||
"include": ["src", "index.ts"]
|
||||
}
|
||||
13
packages/components/collapse/tsup.config.ts
Normal file
13
packages/components/collapse/tsup.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {defineConfig} from "tsup";
|
||||
import {findUpSync} from "find-up";
|
||||
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
minify: false,
|
||||
treeshake: true,
|
||||
format: ["cjs", "esm"],
|
||||
outExtension(ctx) {
|
||||
return {js: `.${ctx.format}.js`};
|
||||
},
|
||||
inject: process.env.JSX ? [findUpSync("react-shim.js")!] : undefined,
|
||||
});
|
||||
@ -1,6 +1,7 @@
|
||||
import {useState, useEffect, useMemo} from "react";
|
||||
import {HTMLNextUIProps, CSS} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {ReactRef} from "@nextui-org/shared-utils";
|
||||
import {useRefState} from "@nextui-org/use-ref-state";
|
||||
import {useRealShape} from "@nextui-org/use-real-shape";
|
||||
import {useResize} from "@nextui-org/use-resize";
|
||||
@ -9,7 +10,7 @@ export interface UseImageProps extends Omit<HTMLNextUIProps<"img">, "height" | "
|
||||
/**
|
||||
* The image ref.
|
||||
*/
|
||||
ref?: React.Ref<HTMLImageElement>;
|
||||
ref?: ReactRef<HTMLImageElement | null>;
|
||||
/**
|
||||
* The image source (local or remote)
|
||||
*/
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import {HTMLAttributes} from "react";
|
||||
import {useLink as useAriaLink} from "@react-aria/link";
|
||||
import {mergeProps} from "@react-aria/utils";
|
||||
import {forwardRef} from "@nextui-org/system";
|
||||
@ -11,20 +10,13 @@ import {LinkIcon} from "./link-icon";
|
||||
|
||||
export interface LinkProps extends UseLinkProps {}
|
||||
|
||||
interface ILinkAria {
|
||||
/** Props for the link element. */
|
||||
linkProps: Omit<HTMLAttributes<HTMLElement>, keyof UseLinkProps>;
|
||||
/** Whether the link is currently pressed. */
|
||||
isPressed: boolean;
|
||||
}
|
||||
|
||||
const Link = forwardRef<LinkProps, "a">((props, ref) => {
|
||||
const {children, as, css, linkCss, isExternal, focusProps, className, ...otherProps} =
|
||||
useLink(props);
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {linkProps}: ILinkAria = useAriaLink({...otherProps, elementType: `${as}`}, domRef);
|
||||
const {linkProps} = useAriaLink({...otherProps, elementType: `${as}`}, domRef);
|
||||
|
||||
return (
|
||||
<StyledLink
|
||||
|
||||
@ -3,7 +3,6 @@ import type {AriaLinkProps} from "@react-types/link";
|
||||
import {useMemo} from "react";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {HTMLNextUIProps, CSS, getTokenValue} from "@nextui-org/system";
|
||||
import {IFocusRingAria} from "@nextui-org/dom-utils";
|
||||
import {isNormalColor} from "@nextui-org//shared-utils";
|
||||
|
||||
export interface Props extends HTMLNextUIProps<"a"> {
|
||||
@ -45,7 +44,7 @@ export function useLink(props: UseLinkProps) {
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {isFocusVisible, focusProps}: IFocusRingAria<UseLinkProps> = useFocusRing({autoFocus});
|
||||
const {isFocusVisible, focusProps} = useFocusRing({autoFocus});
|
||||
|
||||
const linkCss = useMemo(() => {
|
||||
const isNormal = isNormalColor(color as string);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type {AriaRadioProps} from "@react-types/radio";
|
||||
import type {IFocusRingAria} from "@nextui-org/dom-utils";
|
||||
|
||||
import {useMemo, useRef} from "react";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
@ -92,7 +91,7 @@ export function useRadio(props: UseRadioProps) {
|
||||
|
||||
const {hoverProps, isHovered} = useHover({isDisabled});
|
||||
|
||||
const {focusProps, isFocusVisible}: IFocusRingAria<UseRadioProps> = useFocusRing({
|
||||
const {focusProps, isFocusVisible} = useFocusRing({
|
||||
autoFocus,
|
||||
});
|
||||
|
||||
|
||||
@ -3,7 +3,6 @@ import type {AvatarProps} from "@nextui-org/avatar";
|
||||
import {ReactNode, useMemo} from "react";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {IFocusRingAria} from "@nextui-org/dom-utils";
|
||||
|
||||
export interface UseUserProps extends HTMLNextUIProps<"div", AvatarProps> {
|
||||
/**
|
||||
@ -19,7 +18,7 @@ export interface UseUserProps extends HTMLNextUIProps<"div", AvatarProps> {
|
||||
export function useUser(props: UseUserProps) {
|
||||
const {as, className, css, name, description, ...otherProps} = props;
|
||||
|
||||
const {isFocusVisible, focusProps}: IFocusRingAria<UseUserProps> = useFocusRing();
|
||||
const {isFocusVisible, focusProps} = useFocusRing();
|
||||
|
||||
const userCss = useMemo(() => {
|
||||
if (as === "button") {
|
||||
|
||||
@ -66,7 +66,7 @@ export type PropsOf<T extends As> = React.ComponentPropsWithoutRef<T> & {
|
||||
};
|
||||
|
||||
export type HTMLNextUIProps<T extends As = "div", K extends object = {}> = Omit<
|
||||
Omit<PropsOf<T>, "ref" | "color"> & NextUIProps,
|
||||
Omit<PropsOf<T>, "ref" | "color" | "slot"> & NextUIProps,
|
||||
keyof K
|
||||
> &
|
||||
K;
|
||||
|
||||
24
packages/utilities/aria-utils/README.md
Normal file
24
packages/utilities/aria-utils/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# @nextui-org/aria-utils
|
||||
|
||||
A Quick description of the component
|
||||
|
||||
> This is an internal utility, not intended for public usage.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add @nextui-org/aria-utils
|
||||
# or
|
||||
npm i @nextui-org/aria-utils
|
||||
```
|
||||
|
||||
## 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).
|
||||
3
packages/utilities/aria-utils/clean-package.config.json
Normal file
3
packages/utilities/aria-utils/clean-package.config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{ "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" } } }
|
||||
55
packages/utilities/aria-utils/package.json
Normal file
55
packages/utilities/aria-utils/package.json
Normal file
@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@nextui-org/aria-utils",
|
||||
"version": "1.0.0-beta.11",
|
||||
"description": "A package for managing @react-aria nextui utils.",
|
||||
"keywords": [
|
||||
"aria-utils"
|
||||
],
|
||||
"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/utilities/aria-utils"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/nextui-org/nextui/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format=esm,cjs --dts",
|
||||
"dev": "yarn build:fast -- --watch",
|
||||
"clean": "rimraf dist .turbo",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build:fast": "tsup src/index.ts --format=esm,cjs",
|
||||
"prepack": "clean-package",
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@react-aria/button": "^3.6.2",
|
||||
"@react-aria/selection": "^3.11.0",
|
||||
"@react-aria/utils": "^3.14.0",
|
||||
"@react-stately/collections": "^3.4.4",
|
||||
"@react-aria/interactions": "^3.12.0",
|
||||
"@react-aria/focus": "^3.9.0",
|
||||
"@react-stately/tree": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-types/button": "^3.6.2",
|
||||
"@react-types/shared": "^3.15.0",
|
||||
"react": "^17.0.2",
|
||||
"clean-package": "2.1.1"
|
||||
}
|
||||
}
|
||||
1
packages/utilities/aria-utils/src/accordion/index.ts
Normal file
1
packages/utilities/aria-utils/src/accordion/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./use-accordion-item";
|
||||
@ -0,0 +1,147 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
KeyboardEventHandler,
|
||||
ButtonHTMLAttributes,
|
||||
RefObject,
|
||||
Key,
|
||||
} 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";
|
||||
|
||||
export interface AccordionItemAriaProps<T> {
|
||||
item: Node<T>;
|
||||
}
|
||||
|
||||
export interface AccordionItemAria {
|
||||
/** Props for the accordion item button. */
|
||||
buttonProps: ButtonHTMLAttributes<HTMLElement>;
|
||||
/** Props for the accordion item content element. */
|
||||
regionProps: DOMAttributes;
|
||||
}
|
||||
|
||||
export function useAccordionItem<T>(
|
||||
props: AccordionItemAriaProps<T>,
|
||||
state: TreeState<T> & {
|
||||
focusedKey?: Key | null;
|
||||
},
|
||||
ref: RefObject<HTMLButtonElement>,
|
||||
): AccordionItemAria {
|
||||
let {item} = props;
|
||||
let key = item.key;
|
||||
let manager = state.selectionManager;
|
||||
let buttonId = useId();
|
||||
let regionId = useId();
|
||||
let isDisabled = state.disabledKeys.has(item.key);
|
||||
|
||||
// Focus the associated DOM node when this item becomes the focusedKey
|
||||
useEffect(() => {
|
||||
let isFocused = key === state.focusedKey;
|
||||
|
||||
if (isFocused && document.activeElement !== ref.current) {
|
||||
ref.current && focusSafely(ref.current);
|
||||
}
|
||||
}, [ref, key, state.focusedKey]);
|
||||
|
||||
let onSelect = useCallback(
|
||||
(e: PressEvent | LongPressEvent | PointerEvent) => {
|
||||
if (!manager.canSelectItem(key)) {
|
||||
return;
|
||||
}
|
||||
manager.select(key, e);
|
||||
state.toggleKey(key);
|
||||
},
|
||||
[key, manager],
|
||||
);
|
||||
|
||||
const extendFocusSelection = useCallback(
|
||||
(toKey: Key) => {
|
||||
if (manager.selectionBehavior === "replace") {
|
||||
manager.extendSelection(toKey);
|
||||
}
|
||||
manager.setFocusedKey(toKey);
|
||||
},
|
||||
[manager],
|
||||
);
|
||||
|
||||
/**
|
||||
* Manage keyboard navigation between accordion items.
|
||||
*/
|
||||
const onKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
const keyMap: Record<string, KeyboardEventHandler> = {
|
||||
ArrowDown: () => {
|
||||
const nextKey = state.collection.getKeyAfter(key);
|
||||
|
||||
if (nextKey && state.disabledKeys.has(nextKey)) {
|
||||
const nextEnabledKey = state.collection.getKeyAfter(nextKey);
|
||||
|
||||
nextEnabledKey && extendFocusSelection(nextEnabledKey);
|
||||
} else {
|
||||
nextKey && extendFocusSelection(nextKey);
|
||||
}
|
||||
},
|
||||
ArrowUp: () => {
|
||||
const prevKey = state.collection.getKeyBefore(key);
|
||||
|
||||
if (prevKey && state.disabledKeys.has(prevKey)) {
|
||||
const prevEnabledKey = state.collection.getKeyBefore(prevKey);
|
||||
|
||||
prevEnabledKey && extendFocusSelection(prevEnabledKey);
|
||||
} else {
|
||||
prevKey && extendFocusSelection(prevKey);
|
||||
}
|
||||
},
|
||||
Home: () => {
|
||||
const firstKey = state.collection.getFirstKey();
|
||||
|
||||
firstKey && extendFocusSelection(firstKey);
|
||||
},
|
||||
End: () => {
|
||||
const lastKey = state.collection.getLastKey();
|
||||
|
||||
lastKey && extendFocusSelection(lastKey);
|
||||
},
|
||||
};
|
||||
|
||||
const action = keyMap[event.key];
|
||||
|
||||
if (action) {
|
||||
event.preventDefault();
|
||||
if (manager.canSelectItem(key)) {
|
||||
action(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
[key, manager],
|
||||
);
|
||||
|
||||
let {buttonProps} = useButton(
|
||||
{
|
||||
id: buttonId,
|
||||
elementType: "button",
|
||||
isDisabled,
|
||||
onKeyDown,
|
||||
onPress: onSelect,
|
||||
},
|
||||
ref,
|
||||
);
|
||||
|
||||
let isExpanded = state.selectionManager.isSelected(item.key);
|
||||
|
||||
return {
|
||||
buttonProps: {
|
||||
...buttonProps,
|
||||
"aria-expanded": isExpanded,
|
||||
"aria-controls": isExpanded ? regionId : undefined,
|
||||
},
|
||||
regionProps: {
|
||||
id: regionId,
|
||||
role: "region",
|
||||
"aria-labelledby": buttonId,
|
||||
},
|
||||
};
|
||||
}
|
||||
1
packages/utilities/aria-utils/src/collections/index.ts
Normal file
1
packages/utilities/aria-utils/src/collections/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./item";
|
||||
10
packages/utilities/aria-utils/src/collections/item.ts
Normal file
10
packages/utilities/aria-utils/src/collections/item.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export {Item as BaseItem} from "@react-stately/collections";
|
||||
import {ItemProps as BaseItemProps} from "@react-types/shared";
|
||||
import {HTMLNextUIProps, As} from "@nextui-org/system";
|
||||
|
||||
/**
|
||||
* A modified version of the ItemProps from @react-types/shared, with the addition of the NextUI props.
|
||||
*
|
||||
*/
|
||||
export type ItemProps<Type extends As = "div", T extends object = {}> = BaseItemProps<T> &
|
||||
HTMLNextUIProps<Type>;
|
||||
4
packages/utilities/aria-utils/src/index.ts
Normal file
4
packages/utilities/aria-utils/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./accordion";
|
||||
export * from "./collections";
|
||||
export * from "./utils";
|
||||
export * from "./interactions";
|
||||
22
packages/utilities/aria-utils/src/utils/index.ts
Normal file
22
packages/utilities/aria-utils/src/utils/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import {isAppleDevice} from "@react-aria/utils";
|
||||
import {isMac} from "@react-aria/utils";
|
||||
|
||||
interface Event {
|
||||
altKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
}
|
||||
|
||||
export function isNonContiguousSelectionModifier(e: Event) {
|
||||
// Ctrl + Arrow Up/Arrow Down has a system wide meaning on macOS, so use Alt instead.
|
||||
// On Windows and Ubuntu, Alt + Space has a system wide meaning.
|
||||
return isAppleDevice() ? e.altKey : e.ctrlKey;
|
||||
}
|
||||
|
||||
export function isCtrlKeyPressed(e: Event) {
|
||||
if (isMac()) {
|
||||
return e.metaKey;
|
||||
}
|
||||
|
||||
return e.ctrlKey;
|
||||
}
|
||||
4
packages/utilities/aria-utils/tsconfig.json
Normal file
4
packages/utilities/aria-utils/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": ["src", "index.ts"]
|
||||
}
|
||||
13
packages/utilities/aria-utils/tsup.config.ts
Normal file
13
packages/utilities/aria-utils/tsup.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import {defineConfig} from "tsup";
|
||||
import {findUpSync} from "find-up";
|
||||
|
||||
export default defineConfig({
|
||||
clean: true,
|
||||
minify: false,
|
||||
treeshake: true,
|
||||
format: ["cjs", "esm"],
|
||||
outExtension(ctx) {
|
||||
return {js: `.${ctx.format}.js`};
|
||||
},
|
||||
inject: process.env.JSX ? [findUpSync("react-shim.js")!] : undefined,
|
||||
});
|
||||
@ -33,11 +33,6 @@
|
||||
"prepack": "clean-package",
|
||||
"postpack": "clean-package restore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-types/shared": "^3.15.0",
|
||||
"@react-aria/interactions": "^3.12.0",
|
||||
"@react-aria/focus": "^3.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
},
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
export * from "./dom";
|
||||
export * from "./aria";
|
||||
export * from "./dimensions";
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
type Args<T extends Function> = T extends (...args: infer R) => any ? R : never;
|
||||
|
||||
type AnyFunction<T = any> = (...args: T[]) => any;
|
||||
|
||||
/**
|
||||
* Capitalizes the first letter of a string
|
||||
* @param {string} text
|
||||
@ -6,3 +10,21 @@
|
||||
export const capitalize = (text: string) => {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
};
|
||||
|
||||
export function callAllHandlers<T extends (event: any) => void>(...fns: (T | undefined)[]) {
|
||||
return function func(event: Args<T>[0]) {
|
||||
fns.some((fn) => {
|
||||
fn?.(event);
|
||||
|
||||
return event?.defaultPrevented;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function callAll<T extends AnyFunction>(...fns: (T | undefined)[]) {
|
||||
return function mergedFn(arg: Args<T>[0]) {
|
||||
fns.forEach((fn) => {
|
||||
fn?.(arg);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
1212
pnpm-lock.yaml
generated
1212
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user