mirror of
https://github.com/nextui-org/nextui.git
synced 2025-12-08 19:26:11 +00:00
feat(table): sortable and load-more examples added
This commit is contained in:
parent
8500329f9c
commit
316acb48c5
@ -95,7 +95,7 @@ export function useAccordionItem<T extends object = {}>(props: UseAccordionItemP
|
||||
],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getBaseProps = useCallback<PropGetter>(
|
||||
(props = {}) => {
|
||||
|
||||
@ -150,7 +150,7 @@ export function useAvatar(props: UseAvatarProps) {
|
||||
[color, radius, size, isBordered, isDisabled, isInGroup, groupContext?.isGrid],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const canBeFocused = useMemo(() => {
|
||||
return isFocusable || as === "button";
|
||||
|
||||
@ -77,7 +77,7 @@ export function useCard(originalProps: UseCardProps) {
|
||||
const domRef = useDOMRef<HTMLDivElement>(ref);
|
||||
const Component = as || (originalProps.isPressable ? "button" : "div");
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const {onClick: onDripClickHandler, drips} = useDrip();
|
||||
|
||||
|
||||
@ -105,7 +105,7 @@ export function useCheckboxGroup(props: UseCheckboxGroupProps) {
|
||||
|
||||
const slots = useMemo(() => checkboxGroup(), []);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getGroupProps: PropGetter = () => {
|
||||
return {
|
||||
|
||||
@ -219,7 +219,7 @@ export function useCheckbox(props: UseCheckboxProps) {
|
||||
[color, size, radius, lineThrough, isDisabled, disableAnimation],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getBaseProps: PropGetter = () => {
|
||||
return {
|
||||
|
||||
@ -76,7 +76,7 @@ export function useChip(originalProps: UseChipProps) {
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const isCloseable = !!onClose;
|
||||
const isDotVariant = originalProps.variant === "dot";
|
||||
|
||||
@ -113,7 +113,7 @@ export function useDropdownItem<T extends object>(originalProps: UseDropdownItem
|
||||
[...Object.values(variantProps), isDisabled, disableAnimation],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getItemProps: PropGetter = (props = {}) => ({
|
||||
ref: domRef,
|
||||
|
||||
@ -142,7 +142,7 @@ export function useModal(originalProps: UseModalProps) {
|
||||
focusProps: closeButtonFocusProps,
|
||||
} = useFocusRing();
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const slots = useMemo(
|
||||
() =>
|
||||
|
||||
@ -155,7 +155,7 @@ export function useNavbar(originalProps: UseNavbarProps) {
|
||||
[...Object.values(variantProps), shouldHideOnScroll],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
useScrollPosition({
|
||||
elementRef: parentRef,
|
||||
|
||||
@ -185,7 +185,7 @@ export function usePagination(originalProps: UsePaginationProps) {
|
||||
[...Object.values(variantProps)],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const onNext = () => {
|
||||
if (loop && activePage === total) {
|
||||
|
||||
@ -179,7 +179,7 @@ export function usePopover(originalProps: UsePopoverProps) {
|
||||
[...Object.values(variantProps)],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getPopoverProps: PropGetter = (props = {}) => ({
|
||||
ref: popoverRef,
|
||||
|
||||
@ -72,7 +72,7 @@ export function useCircularProgress(originalProps: UseCircularProgressProps) {
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
const [, isMounted] = useIsMounted({
|
||||
rerender: true,
|
||||
delay: 100,
|
||||
|
||||
@ -68,7 +68,7 @@ export function useProgress(originalProps: UseProgressProps) {
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
const [, isMounted] = useIsMounted({
|
||||
rerender: true,
|
||||
delay: 100,
|
||||
|
||||
@ -131,7 +131,7 @@ export function useRadioGroup(props: UseRadioGroupProps) {
|
||||
|
||||
const slots = useMemo(() => radioGroup(), []);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getGroupProps: PropGetter = () => {
|
||||
return {
|
||||
|
||||
@ -164,7 +164,7 @@ export function useRadio(props: UseRadioProps) {
|
||||
[color, size, radius, isDisabled, isInvalid, disableAnimation],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getBaseProps: PropGetter = (props = {}) => {
|
||||
return {
|
||||
|
||||
@ -154,7 +154,7 @@ export function useSnippet(originalProps: UseSnippetProps) {
|
||||
return str ? `${str} ` : "";
|
||||
}, [symbol]);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getSnippetProps = useCallback<PropGetter>(
|
||||
() => ({
|
||||
|
||||
@ -41,7 +41,7 @@ export function useSpinner(originalProps: UseSpinnerProps) {
|
||||
|
||||
const slots = useMemo(() => spinner({...variantProps}), [...Object.values(variantProps)]);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const label = labelProp || children;
|
||||
|
||||
|
||||
@ -180,7 +180,7 @@ export function useSwitch(originalProps: UseSwitchProps) {
|
||||
[...Object.values(variantProps)],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getBaseProps: PropGetter = (props) => {
|
||||
return {
|
||||
|
||||
@ -38,23 +38,27 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nextui-org/checkbox": "workspace:*",
|
||||
"@nextui-org/spacer": "workspace:*",
|
||||
"@nextui-org/dom-utils": "workspace:*",
|
||||
"@nextui-org/shared-utils": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"@nextui-org/spacer": "workspace:*",
|
||||
"@nextui-org/system": "workspace:*",
|
||||
"@nextui-org/theme": "workspace:*",
|
||||
"@react-aria/focus": "^3.12.0",
|
||||
"@react-aria/interactions": "^3.15.0",
|
||||
"@react-aria/table": "^3.9.0",
|
||||
"@react-aria/utils": "^3.16.0",
|
||||
"@react-aria/visually-hidden": "^3.8.0",
|
||||
"@react-aria/interactions": "^3.15.0",
|
||||
"@react-stately/table": "^3.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextui-org/user": "workspace:*",
|
||||
"@nextui-org/tooltip": "workspace:*",
|
||||
"@nextui-org/chip": "workspace:*",
|
||||
"@nextui-org/shared-icons": "workspace:*",
|
||||
"@nextui-org/button": "workspace:*",
|
||||
"@nextui-org/spinner": "workspace:*",
|
||||
"@nextui-org/pagination": "workspace:*",
|
||||
"@nextui-org/tooltip": "workspace:*",
|
||||
"@nextui-org/user": "workspace:*",
|
||||
"@react-stately/data": "^3.9.1",
|
||||
"@react-types/grid": "^3.1.7",
|
||||
"@react-types/table": "^3.6.0",
|
||||
"clean-package": "2.2.0",
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import {HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {TableBody as TableBodyBase} from "@react-stately/table";
|
||||
import {TableBodyProps as TableBodyBaseProps} from "@react-types/table";
|
||||
import {ReactNode} from "react";
|
||||
|
||||
export type TableBodyProps<T> = TableBodyBaseProps<T> &
|
||||
Omit<HTMLNextUIProps<"tbody">, keyof TableBodyBaseProps<T>>;
|
||||
export interface TableBodyProps<T>
|
||||
extends TableBodyBaseProps<T>,
|
||||
Omit<HTMLNextUIProps<"tbody">, keyof TableBodyBaseProps<T>> {
|
||||
/** Provides content to display when there are no rows in the table. */
|
||||
renderEmptyState?: () => ReactNode;
|
||||
}
|
||||
|
||||
const TableBody = TableBodyBase as <T>(props: TableBodyProps<T>) => JSX.Element;
|
||||
|
||||
|
||||
@ -1,23 +1,26 @@
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {useTableRowGroup} from "@react-aria/table";
|
||||
import {useMemo} from "react";
|
||||
import {filterDOMProps, mergeProps} from "@react-aria/utils";
|
||||
|
||||
import TableRow from "./table-row";
|
||||
import TableCell from "./table-cell";
|
||||
import TableRowGroup from "./table-row-group";
|
||||
import TableCheckboxCell from "./table-checkbox-cell";
|
||||
import {useTableContext} from "./table-context";
|
||||
|
||||
const TableBody = forwardRef<HTMLNextUIProps, "tbody">((props, ref) => {
|
||||
const {as: asProp, className, ...otherProps} = props;
|
||||
const {as, className, ...otherProps} = props;
|
||||
|
||||
const as = asProp || "tbody";
|
||||
const Component = as || "tbody";
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, collection, classNames} = useTableContext();
|
||||
const {rowGroupProps} = useTableRowGroup();
|
||||
|
||||
const tbodyStyles = clsx(classNames?.tbody, className);
|
||||
const bodyProps = collection.body.props;
|
||||
|
||||
const renderRows = useMemo(() => {
|
||||
return [...collection.body.childNodes].map((row) => (
|
||||
@ -33,15 +36,32 @@ const TableBody = forwardRef<HTMLNextUIProps, "tbody">((props, ref) => {
|
||||
));
|
||||
}, [collection.body.childNodes]);
|
||||
|
||||
let emptyState;
|
||||
|
||||
if (collection.size === 0 && bodyProps.renderEmptyState) {
|
||||
emptyState = (
|
||||
<tr role="row">
|
||||
<td
|
||||
className={slots?.emptyWrapper({class: classNames?.emptyWrapper})}
|
||||
colSpan={collection.columnCount}
|
||||
role="gridcell"
|
||||
>
|
||||
{bodyProps.renderEmptyState()}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRowGroup
|
||||
<Component
|
||||
ref={domRef}
|
||||
as={as}
|
||||
{...otherProps}
|
||||
{...mergeProps(rowGroupProps, filterDOMProps(bodyProps, {labelable: true}), otherProps)}
|
||||
className={slots.tbody?.({class: tbodyStyles})}
|
||||
data-empty={dataAttr(collection.size === 0)}
|
||||
>
|
||||
{renderRows}
|
||||
</TableRowGroup>
|
||||
{emptyState}
|
||||
</Component>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -2,11 +2,13 @@ import type {GridNode} from "@react-types/grid";
|
||||
|
||||
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
|
||||
import {useDOMRef} from "@nextui-org/dom-utils";
|
||||
import {clsx} from "@nextui-org/shared-utils";
|
||||
import {clsx, dataAttr} from "@nextui-org/shared-utils";
|
||||
import {useTableColumnHeader} from "@react-aria/table";
|
||||
import {filterDOMProps, mergeProps} from "@react-aria/utils";
|
||||
import {ChevronDownIcon} from "@nextui-org/shared-icons";
|
||||
import {useFocusRing} from "@react-aria/focus";
|
||||
import {VisuallyHidden} from "@react-aria/visually-hidden";
|
||||
import {useHover} from "@react-aria/interactions";
|
||||
|
||||
import {useTableContext} from "./table-context";
|
||||
|
||||
@ -30,43 +32,36 @@ const TableColumnHeader = forwardRef<TableColumnHeaderProps, "th">((props, ref)
|
||||
const thStyles = clsx(classNames?.th, className, node.props?.className);
|
||||
|
||||
const {isFocusVisible, focusProps} = useFocusRing();
|
||||
const {isHovered, hoverProps} = useHover({});
|
||||
const {hideHeader, ...columnProps} = node.props;
|
||||
|
||||
let arrowIcon = state.sortDescriptor?.direction === "ascending" ? "▲" : "▼";
|
||||
const allowsSorting = columnProps.allowsSorting;
|
||||
|
||||
return (
|
||||
<Component
|
||||
ref={domRef}
|
||||
colSpan={node.colspan}
|
||||
data-focus-visible={isFocusVisible}
|
||||
data-focus-visible={dataAttr(isFocusVisible)}
|
||||
data-hover={dataAttr(isHovered)}
|
||||
data-sortable={dataAttr(allowsSorting)}
|
||||
{...mergeProps(
|
||||
columnHeaderProps,
|
||||
focusProps,
|
||||
filterDOMProps(columnProps, {labelable: true}),
|
||||
allowsSorting ? hoverProps : {},
|
||||
otherProps,
|
||||
)}
|
||||
className={slots.th?.({class: thStyles})}
|
||||
>
|
||||
{hideHeader ? <VisuallyHidden>{node.rendered}</VisuallyHidden> : node.rendered}
|
||||
{columnProps.allowsSorting && (
|
||||
<span
|
||||
{allowsSorting && (
|
||||
<ChevronDownIcon
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
padding: "0 2px",
|
||||
visibility: state.sortDescriptor?.column === node.key ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{arrowIcon}
|
||||
</span>
|
||||
// <TableSortIcon
|
||||
// ascending={state.sortDescriptor?.direction === "ascending"}
|
||||
// css={{
|
||||
// position: "absolute",
|
||||
// m: "0 $2",
|
||||
// bottom: `calc(50% - ${ICON_SIZE / 2}px)`,
|
||||
// }}
|
||||
// visible={state.sortDescriptor?.column === column.key}
|
||||
// />
|
||||
className={slots.sortIcon?.({class: classNames?.sortIcon})}
|
||||
data-direction={state.sortDescriptor?.direction}
|
||||
data-visible={dataAttr(state.sortDescriptor?.column === node.key)}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
)}
|
||||
</Component>
|
||||
);
|
||||
|
||||
@ -22,6 +22,7 @@ const TableRow = forwardRef<TableRowProps, "tr">((props, ref) => {
|
||||
const {as, className, children, node, ...otherProps} = props;
|
||||
|
||||
const Component = as || "tr";
|
||||
|
||||
const domRef = useDOMRef(ref);
|
||||
|
||||
const {slots, state, isSelectable, classNames} = useTableContext();
|
||||
|
||||
@ -14,43 +14,57 @@ export interface TableProps
|
||||
extends Omit<UseTableProps, "ref" | "isSelectable" | "isMultiSelectable"> {}
|
||||
|
||||
const Table = forwardRef<TableProps, "table">((props, ref) => {
|
||||
const {Component, collection, context, removeWrapper, getBaseProps, getTableProps} = useTable({
|
||||
const {
|
||||
BaseComponent,
|
||||
Component,
|
||||
collection,
|
||||
context,
|
||||
topContent,
|
||||
bottomContent,
|
||||
removeWrapper,
|
||||
getBaseProps,
|
||||
getTableProps,
|
||||
} = useTable({
|
||||
ref,
|
||||
...props,
|
||||
});
|
||||
|
||||
const Wrapper = useCallback(
|
||||
const BaseWrapper = useCallback(
|
||||
({children}: {children: JSX.Element}) => {
|
||||
if (removeWrapper) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return <div {...getBaseProps()}>{children}</div>;
|
||||
return <BaseComponent {...getBaseProps()}>{children}</BaseComponent>;
|
||||
},
|
||||
[removeWrapper, getBaseProps],
|
||||
);
|
||||
|
||||
return (
|
||||
<TableProvider value={context}>
|
||||
<Wrapper>
|
||||
<Component {...getTableProps()}>
|
||||
<TableRowGroup>
|
||||
{collection.headerRows.map((headerRow) => (
|
||||
<TableHeaderRow key={headerRow?.key} node={headerRow}>
|
||||
{[...headerRow.childNodes].map((column) =>
|
||||
column?.props?.isSelectionCell ? (
|
||||
<TableSelectAllCheckbox key={column?.key} node={column} />
|
||||
) : (
|
||||
<TableColumnHeader key={column?.key} node={column} />
|
||||
),
|
||||
)}
|
||||
</TableHeaderRow>
|
||||
))}
|
||||
<Spacer as="tr" y={0.4} />
|
||||
</TableRowGroup>
|
||||
<TableBody />
|
||||
</Component>
|
||||
</Wrapper>
|
||||
<BaseWrapper>
|
||||
<>
|
||||
{topContent}
|
||||
<Component {...getTableProps()}>
|
||||
<TableRowGroup>
|
||||
{collection.headerRows.map((headerRow) => (
|
||||
<TableHeaderRow key={headerRow?.key} node={headerRow}>
|
||||
{[...headerRow.childNodes].map((column) =>
|
||||
column?.props?.isSelectionCell ? (
|
||||
<TableSelectAllCheckbox key={column?.key} node={column} />
|
||||
) : (
|
||||
<TableColumnHeader key={column?.key} node={column} />
|
||||
),
|
||||
)}
|
||||
</TableHeaderRow>
|
||||
))}
|
||||
<Spacer as="tr" y={0.4} />
|
||||
</TableRowGroup>
|
||||
<TableBody />
|
||||
</Component>
|
||||
{bottomContent}
|
||||
</>
|
||||
</BaseWrapper>
|
||||
</TableProvider>
|
||||
);
|
||||
});
|
||||
|
||||
@ -23,32 +23,28 @@ interface Props extends HTMLNextUIProps<"table"> {
|
||||
* Ref to the DOM node.
|
||||
*/
|
||||
ref?: ReactRef<HTMLElement | null>;
|
||||
/** The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. */
|
||||
/*
|
||||
* The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows.
|
||||
*/
|
||||
children?: ReactNode;
|
||||
/**
|
||||
* Whether the table container should not be rendered.
|
||||
* A custom wrapper component for the table.
|
||||
* @default "div"
|
||||
*/
|
||||
BaseComponent?: React.ComponentType<any>;
|
||||
/**
|
||||
* A property to include a component in the top of the table.
|
||||
*/
|
||||
topContent?: ReactNode;
|
||||
/**
|
||||
* A property to include a component in the bottom of the table.
|
||||
*/
|
||||
bottomContent?: ReactNode;
|
||||
/**
|
||||
* Whether the table base container should not be rendered.
|
||||
* @default false
|
||||
*/
|
||||
removeWrapper?: boolean;
|
||||
/**
|
||||
* Classname or List of classes to change the classNames of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* <Table classNames={{
|
||||
* base:"base-classes", // table wrapper
|
||||
* table: "table-classes",
|
||||
* thead: "thead-classes",
|
||||
* tbody: "tbody-classes",
|
||||
* tr: "tr-classes",
|
||||
* th: "th-classes",
|
||||
* td: "td-classes",
|
||||
* tfoot: "tfoot-classes",
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
classNames?: SlotsToClasses<TableSlots>;
|
||||
/**
|
||||
* How multiple selection should behave in the collection.
|
||||
* The selection behavior for the table. If selectionMode is `"none"`, this will be `null`.
|
||||
@ -70,6 +66,27 @@ interface Props extends HTMLNextUIProps<"table"> {
|
||||
onRowAction?: (key: Key) => void;
|
||||
/** Handler that is called when a user performs an action on the cell. */
|
||||
onCellAction?: (key: Key) => void;
|
||||
/**
|
||||
* Classname or List of classes to change the classNames of the element.
|
||||
* if `className` is passed, it will be added to the base slot.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* <Table classNames={{
|
||||
* base:"base-classes", // table wrapper
|
||||
* table: "table-classes",
|
||||
* thead: "thead-classes",
|
||||
* tbody: "tbody-classes",
|
||||
* tr: "tr-classes",
|
||||
* th: "th-classes",
|
||||
* td: "td-classes",
|
||||
* tfoot: "tfoot-classes",
|
||||
* sortIcon: "sort-icon-classes",
|
||||
* emptyWrapper: "empty-wrapper-classes",
|
||||
* }} />
|
||||
* ```
|
||||
*/
|
||||
classNames?: SlotsToClasses<TableSlots>;
|
||||
}
|
||||
|
||||
export type UseTableProps<T = object> = Props &
|
||||
@ -108,6 +125,9 @@ export function useTable<T extends object>(originalProps: UseTableProps<T>) {
|
||||
disabledBehavior = "selection",
|
||||
showSelectionCheckboxes = selectionMode === "multiple" && selectionBehavior !== "replace",
|
||||
disableAnimation = false,
|
||||
BaseComponent = "div",
|
||||
topContent,
|
||||
bottomContent,
|
||||
onRowAction,
|
||||
onCellAction,
|
||||
...otherProps
|
||||
@ -140,7 +160,7 @@ export function useTable<T extends object>(originalProps: UseTableProps<T>) {
|
||||
[...Object.values(variantProps), isSelectable, isMultiSelectable],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const context = useMemo<ContextType<T>>(
|
||||
() => ({
|
||||
@ -190,11 +210,14 @@ export function useTable<T extends object>(originalProps: UseTableProps<T>) {
|
||||
});
|
||||
|
||||
return {
|
||||
BaseComponent,
|
||||
Component,
|
||||
children,
|
||||
state,
|
||||
collection,
|
||||
context,
|
||||
topContent,
|
||||
bottomContent,
|
||||
removeWrapper,
|
||||
selectionMode,
|
||||
getBaseProps,
|
||||
|
||||
@ -3,8 +3,11 @@ import {ComponentStory, ComponentMeta} from "@storybook/react";
|
||||
import {table} from "@nextui-org/theme";
|
||||
import {User} from "@nextui-org/user";
|
||||
import {Chip, ChipProps} from "@nextui-org/chip";
|
||||
import {Button} from "@nextui-org/button";
|
||||
import {Spinner} from "@nextui-org/spinner";
|
||||
import {Tooltip} from "@nextui-org/tooltip";
|
||||
import {EditIcon, DeleteIcon, EyeIcon} from "@nextui-org/shared-icons";
|
||||
import {useAsyncList} from "@react-stately/data";
|
||||
|
||||
import {Table, TableHeader, TableColumn, TableBody, TableCell, TableRow, TableProps} from "../src";
|
||||
|
||||
@ -104,6 +107,13 @@ const columns = [
|
||||
},
|
||||
];
|
||||
|
||||
type SWCharacter = {
|
||||
name: string;
|
||||
height: string;
|
||||
mass: string;
|
||||
birth_year: string;
|
||||
};
|
||||
|
||||
const isObject = (target: unknown) => target && typeof target === "object";
|
||||
|
||||
const getKeyValue = (obj: any, key: Key) => {
|
||||
@ -145,6 +155,17 @@ const StaticTemplate: ComponentStory<typeof Table> = (args: TableProps) => (
|
||||
</Table>
|
||||
);
|
||||
|
||||
const EmptyTemplate: ComponentStory<typeof Table> = (args: TableProps) => (
|
||||
<Table aria-label="Example empty table" {...args}>
|
||||
<TableHeader>
|
||||
<TableColumn>NAME</TableColumn>
|
||||
<TableColumn>ROLE</TableColumn>
|
||||
<TableColumn>STATUS</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody renderEmptyState={() => "No rows to display."}>{[]}</TableBody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
const DynamicTemplate: ComponentStory<typeof Table> = (args: TableProps) => (
|
||||
<Table aria-label="Example table with dynamic content" {...args}>
|
||||
<TableHeader columns={columns}>
|
||||
@ -220,14 +241,16 @@ const CustomCellTemplate: ComponentStory<typeof Table> = (args: TableProps) => {
|
||||
},
|
||||
];
|
||||
|
||||
type User = typeof users[0];
|
||||
|
||||
const statusColorMap: Record<string, ChipProps["color"]> = {
|
||||
active: "success",
|
||||
paused: "danger",
|
||||
vacation: "warning",
|
||||
};
|
||||
|
||||
const renderCell = React.useCallback((user, columnKey) => {
|
||||
const cellValue = user[columnKey];
|
||||
const renderCell = React.useCallback((user: User, columnKey: React.Key) => {
|
||||
const cellValue = user[columnKey as keyof User];
|
||||
|
||||
switch (columnKey) {
|
||||
case "name":
|
||||
@ -253,7 +276,6 @@ const CustomCellTemplate: ComponentStory<typeof Table> = (args: TableProps) => {
|
||||
{cellValue}
|
||||
</Chip>
|
||||
);
|
||||
|
||||
case "actions":
|
||||
return (
|
||||
<div className="relative flex items-center gap-2">
|
||||
@ -299,6 +321,125 @@ const CustomCellTemplate: ComponentStory<typeof Table> = (args: TableProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SortableTemplate: ComponentStory<typeof Table> = (args: TableProps) => {
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal}) {
|
||||
let res = await fetch(`https://swapi.py4e.com/api/people/?search`, {
|
||||
signal,
|
||||
});
|
||||
let json = await res.json();
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
};
|
||||
},
|
||||
async sort({items, sortDescriptor}) {
|
||||
return {
|
||||
items: items.sort((a, b) => {
|
||||
let first = a[sortDescriptor.column!];
|
||||
let second = b[sortDescriptor.column!];
|
||||
let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1;
|
||||
|
||||
if (sortDescriptor.direction === "descending") {
|
||||
cmp *= -1;
|
||||
}
|
||||
|
||||
return cmp;
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Table
|
||||
aria-label="Example table with client side sorting"
|
||||
sortDescriptor={list.sortDescriptor}
|
||||
onSortChange={list.sort}
|
||||
{...args}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name" allowsSorting>
|
||||
Name
|
||||
</TableColumn>
|
||||
<TableColumn key="height" allowsSorting>
|
||||
Height
|
||||
</TableColumn>
|
||||
<TableColumn key="mass" allowsSorting>
|
||||
Mass
|
||||
</TableColumn>
|
||||
<TableColumn key="birth_year" allowsSorting>
|
||||
Birth year
|
||||
</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody items={list.items}>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
const LoadMoreTemplate: ComponentStory<typeof Table> = (args: TableProps) => {
|
||||
const [page, setPage] = React.useState(1);
|
||||
|
||||
let list = useAsyncList<SWCharacter>({
|
||||
async load({signal, cursor}) {
|
||||
if (cursor) {
|
||||
// write this /^http:\/\//i using RegExp
|
||||
const regex = "/^http:///i";
|
||||
|
||||
cursor = cursor.replace(regex, "https://");
|
||||
|
||||
setPage((prev) => prev + 1);
|
||||
}
|
||||
|
||||
let res = await fetch(cursor || "https://swapi.py4e.com/api/people/?search=", {signal});
|
||||
let json = await res.json();
|
||||
|
||||
return {
|
||||
items: json.results,
|
||||
cursor: json.next,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const hasMore = page < 9;
|
||||
|
||||
return (
|
||||
<Table
|
||||
aria-label="Example table with client side sorting"
|
||||
bottomContent={
|
||||
hasMore ? (
|
||||
<Button isDisabled={list.isLoading} variant="flat" onPress={list.loadMore}>
|
||||
{list.isLoading && <Spinner color="white" size="sm" />}
|
||||
Load More
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
sortDescriptor={list.sortDescriptor}
|
||||
onSortChange={list.sort}
|
||||
{...args}
|
||||
>
|
||||
<TableHeader>
|
||||
<TableColumn key="name">Name</TableColumn>
|
||||
<TableColumn key="height">Height</TableColumn>
|
||||
<TableColumn key="mass">Mass</TableColumn>
|
||||
<TableColumn key="birth_year">Birth year</TableColumn>
|
||||
</TableHeader>
|
||||
<TableBody items={list.items}>
|
||||
{(item) => (
|
||||
<TableRow key={item.name}>
|
||||
{(columnKey) => <TableCell>{getKeyValue(item, columnKey)}</TableCell>}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export const Static = StaticTemplate.bind({});
|
||||
Static.args = {
|
||||
...defaultProps,
|
||||
@ -309,6 +450,17 @@ Dynamic.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const EmptyState = EmptyTemplate.bind({});
|
||||
EmptyState.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const NoHeader = StaticTemplate.bind({});
|
||||
NoHeader.args = {
|
||||
...defaultProps,
|
||||
hideHeader: true,
|
||||
};
|
||||
|
||||
export const CustomCells = CustomCellTemplate.bind({});
|
||||
CustomCells.args = {
|
||||
...defaultProps,
|
||||
@ -360,6 +512,17 @@ DisallowEmptySelection.args = {
|
||||
selectionMode: "multiple",
|
||||
};
|
||||
|
||||
export const Sortable = SortableTemplate.bind({});
|
||||
Sortable.args = {
|
||||
...defaultProps,
|
||||
};
|
||||
|
||||
export const LoadMore = LoadMoreTemplate.bind({});
|
||||
LoadMore.args = {
|
||||
...defaultProps,
|
||||
className: "max-w-3xl max-h-auto",
|
||||
};
|
||||
|
||||
export const DisableAnimation = StaticTemplate.bind({});
|
||||
DisableAnimation.args = {
|
||||
...defaultProps,
|
||||
|
||||
@ -190,7 +190,7 @@ export function useTooltip(originalProps: UseTooltipProps) {
|
||||
[...Object.values(variantProps)],
|
||||
);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getTriggerProps = useCallback<PropGetter>(
|
||||
(props = {}, _ref: Ref<any> | null | undefined = null) => ({
|
||||
|
||||
@ -76,7 +76,7 @@ export function useUser(props: UseUserProps) {
|
||||
|
||||
const slots = useMemo(() => user(), []);
|
||||
|
||||
const baseStyles = clsx(classNames?.base, className);
|
||||
const baseStyles = clsx(className, classNames?.base);
|
||||
|
||||
const getUserProps = useCallback<PropGetter>(
|
||||
() => ({
|
||||
|
||||
@ -27,6 +27,7 @@ const button = tv({
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"max-w-fit",
|
||||
"box-border",
|
||||
"appearance-none",
|
||||
"outline-none",
|
||||
|
||||
@ -48,12 +48,13 @@ const focusRing = [
|
||||
*/
|
||||
const table = tv({
|
||||
slots: {
|
||||
base: "p-4 border border-neutral-100",
|
||||
base: "flex flex-col items-center p-4 gap-4 border border-neutral-100 overflow-auto",
|
||||
table: "",
|
||||
thead: "",
|
||||
tbody: "",
|
||||
tr: ["group", "outline-none", ...focusRing],
|
||||
th: [
|
||||
"group",
|
||||
"px-3",
|
||||
"h-10",
|
||||
"text-left",
|
||||
@ -65,6 +66,9 @@ const table = tv({
|
||||
"first:rounded-l-lg",
|
||||
"last:rounded-r-lg",
|
||||
"outline-none",
|
||||
"data-[sortable=true]:transition-colors",
|
||||
"data-[sortable=true]:cursor-pointer",
|
||||
"data-[hover=true]:text-neutral-400",
|
||||
...focusRing,
|
||||
],
|
||||
td: [
|
||||
@ -88,6 +92,19 @@ const table = tv({
|
||||
"group-data-[disabled=true]:text-neutral-300",
|
||||
],
|
||||
tfoot: "",
|
||||
sortIcon: [
|
||||
"ml-2",
|
||||
"mb-px",
|
||||
"opacity-0",
|
||||
"text-inherit",
|
||||
"inline-block",
|
||||
"transition-transform-opacity",
|
||||
"data-[visible=true]:opacity-100",
|
||||
"group-data-[hover=true]:opacity-100",
|
||||
"data-[direction=descending]:rotate-0",
|
||||
"data-[direction=ascending]:rotate-180",
|
||||
],
|
||||
emptyWrapper: "text-neutral-300 align-middle text-center h-36",
|
||||
},
|
||||
variants: {
|
||||
color: {
|
||||
@ -165,6 +182,11 @@ const table = tv({
|
||||
base: "shadow-inner",
|
||||
},
|
||||
},
|
||||
hideHeader: {
|
||||
true: {
|
||||
thead: "hidden",
|
||||
},
|
||||
},
|
||||
isStriped: {
|
||||
true: {
|
||||
td: [
|
||||
@ -208,6 +230,7 @@ const table = tv({
|
||||
shadow: "lg",
|
||||
radius: "xl",
|
||||
color: "neutral",
|
||||
hideHeader: false,
|
||||
isStriped: false,
|
||||
fullWidth: true,
|
||||
},
|
||||
|
||||
@ -136,5 +136,5 @@ import {link, button} from "@nextui-org/theme";
|
||||
<br />
|
||||
|
||||
<div class="block text-xs text-neutral-400">
|
||||
Last updated on <time datetime="2023-03-07">April 19, 2023</time>
|
||||
Last updated on <time datetime="2023-03-07">April 22, 2023</time>
|
||||
</div>
|
||||
|
||||
23
packages/utilities/shared-icons/src/chevron-down.tsx
Normal file
23
packages/utilities/shared-icons/src/chevron-down.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import {IconSvgProps} from "./types";
|
||||
|
||||
export const ChevronDownIcon = ({strokeWidth = 1.5, ...otherProps}: IconSvgProps) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="1em"
|
||||
{...otherProps}
|
||||
>
|
||||
<path
|
||||
d="m19.92 8.95-6.52 6.52c-.77.77-2.03.77-2.8 0L4.08 8.95"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeMiterlimit={10}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@ -5,6 +5,7 @@ export * from "./avatar";
|
||||
export * from "./close";
|
||||
export * from "./close-filled";
|
||||
export * from "./chevron";
|
||||
export * from "./chevron-down";
|
||||
export * from "./ellipsis";
|
||||
export * from "./forward";
|
||||
export * from "./sun";
|
||||
|
||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@ -1481,6 +1481,9 @@ importers:
|
||||
'@nextui-org/dom-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/dom-utils
|
||||
'@nextui-org/shared-icons':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-icons
|
||||
'@nextui-org/shared-utils':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-utils
|
||||
@ -1512,18 +1515,27 @@ importers:
|
||||
specifier: ^3.9.0
|
||||
version: 3.9.0(react@18.2.0)
|
||||
devDependencies:
|
||||
'@nextui-org/button':
|
||||
specifier: workspace:*
|
||||
version: link:../button
|
||||
'@nextui-org/chip':
|
||||
specifier: workspace:*
|
||||
version: link:../chip
|
||||
'@nextui-org/shared-icons':
|
||||
'@nextui-org/pagination':
|
||||
specifier: workspace:*
|
||||
version: link:../../utilities/shared-icons
|
||||
version: link:../pagination
|
||||
'@nextui-org/spinner':
|
||||
specifier: workspace:*
|
||||
version: link:../spinner
|
||||
'@nextui-org/tooltip':
|
||||
specifier: workspace:*
|
||||
version: link:../tooltip
|
||||
'@nextui-org/user':
|
||||
specifier: workspace:*
|
||||
version: link:../user
|
||||
'@react-stately/data':
|
||||
specifier: ^3.9.1
|
||||
version: 3.9.1(react@18.2.0)
|
||||
'@react-types/grid':
|
||||
specifier: ^3.1.7
|
||||
version: 3.1.7(react@18.2.0)
|
||||
@ -6702,6 +6714,16 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@react-stately/data@3.9.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-UClgI8jQTF3hVR/WLa2ht7Gjd2x2PRnYycDmfY+mfbd+ONBD7rX/m3KWGgrR8AvO05qSpQoSlab8D+cfLXvgWA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
|
||||
dependencies:
|
||||
'@react-types/shared': 3.18.0(react@18.2.0)
|
||||
'@swc/helpers': 0.4.14
|
||||
react: 18.2.0
|
||||
dev: true
|
||||
|
||||
/@react-stately/grid@3.6.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-Sq/ivfq9Kskghoe6rYh2PfhB9/jBGfoj8wUZ4bqHcalTrBjfUvkcWMSFosibYPNZFDkA7r00bbJPDJVf1VLhuw==}
|
||||
peerDependencies:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user