├── lib ├── src │ ├── utilities │ │ ├── index.ts │ │ ├── clsx.ts │ │ ├── useReachUI.ts │ │ ├── useStylesheet.ts │ │ ├── deepMerge.ts │ │ ├── styled.ts │ │ ├── theme.ts │ │ ├── systemColumns.ts │ │ └── generateStatus.ts │ ├── index.ts │ ├── context.ts │ ├── typings.d.ts │ ├── types │ │ ├── TableOptions.ts │ │ ├── DataType.ts │ │ ├── OtherOptions.ts │ │ ├── index.ts │ │ ├── RowStateOptions.ts │ │ ├── RowSelectOptions.ts │ │ ├── LocaleOptions.ts │ │ ├── ExpandedOptions.ts │ │ ├── ActionOptions.ts │ │ ├── StyleOptions.ts │ │ ├── ColumnOptions.ts │ │ ├── GlobalFilterOptions.ts │ │ ├── FiltersOptions.ts │ │ ├── ReactTable.ts │ │ ├── PaginationOptions.ts │ │ ├── SortingOptions.ts │ │ └── ReactTableUIProps.ts │ ├── styles │ │ ├── index.ts │ │ ├── StatusBarStyle.ts │ │ ├── menuStyle.ts │ │ ├── commonStyle.ts │ │ ├── containerStyle.ts │ │ ├── TitleBarStyle.ts │ │ └── TableStyle.ts │ ├── logic │ │ ├── useCreateDefaultColumns.ts │ │ ├── useManualPagination.ts │ │ ├── useScrollPosition.ts │ │ ├── generateUseExpandedColumn.tsx │ │ ├── useModal.tsx │ │ ├── generateUseRowActionColumn.tsx │ │ ├── generateUseRowSelectColumn.tsx │ │ ├── useFullscreenAction.tsx │ │ ├── useTheme.ts │ │ ├── useVisibleFilters.tsx │ │ ├── useHandleStateChange.ts │ │ └── useReactTableUI.tsx │ ├── common │ │ ├── Cell.tsx │ │ ├── Button.tsx │ │ ├── Icon.tsx │ │ └── FocusTrap.tsx │ ├── components │ │ ├── TitleBar │ │ │ ├── index.tsx │ │ │ ├── VisibleFilterAction.tsx │ │ │ ├── userDefinedActions.tsx │ │ │ ├── PreferencesAction.tsx │ │ │ └── GlobalFilter.tsx │ │ ├── Modal.tsx │ │ ├── ReactTableUI.tsx │ │ ├── Foot.tsx │ │ ├── Table.tsx │ │ ├── Body.tsx │ │ ├── StatusBar.tsx │ │ ├── Filters.tsx │ │ └── Head.tsx │ └── react-table-config.d.ts ├── tsconfig.json ├── scripts │ ├── build.js │ ├── postInstall.js │ └── publish.js ├── package.json └── README.md ├── example ├── src │ ├── vite-env.d.ts │ ├── routes │ │ ├── _router.tsx │ │ ├── Basic.tsx │ │ └── Pagination.tsx │ ├── index.tsx │ ├── index.css │ ├── App.tsx │ └── react-table-config.d.ts ├── vite.config.ts ├── tsconfig.node.json ├── .gitignore ├── index.html ├── package.json └── tsconfig.json ├── .vscode └── settings.json ├── assets ├── Logo.png ├── RTUI.jpg └── hero.png ├── .prettierrc ├── .gitignore ├── README.md ├── typedoc.json ├── tsconfig.json ├── .eslintrc ├── LICENSE ├── .github └── workflows │ └── publish.yml └── package.json /lib/src/utilities/index.ts: -------------------------------------------------------------------------------- 1 | export const NOOP = () => {} 2 | -------------------------------------------------------------------------------- /example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "rtui" 4 | ] 5 | } -------------------------------------------------------------------------------- /assets/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuptaSiddhant/react-table-ui/HEAD/assets/Logo.png -------------------------------------------------------------------------------- /assets/RTUI.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuptaSiddhant/react-table-ui/HEAD/assets/RTUI.jpg -------------------------------------------------------------------------------- /assets/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuptaSiddhant/react-table-ui/HEAD/assets/hero.png -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src"], 7 | "exclude": ["node_modules", "dist", "docs", "example"] 8 | } 9 | -------------------------------------------------------------------------------- /example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "semi": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "jsxBracketSameLine": false, 8 | "arrowParens": "always", 9 | "trailingComma": "none" 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | // Import components 2 | import ReactTableUI from './components/ReactTableUI' 3 | 4 | // ----------------- 5 | 6 | // Export components 7 | 8 | export default ReactTableUI 9 | 10 | // Export types 11 | export * from './types' 12 | 13 | // ----------------- 14 | -------------------------------------------------------------------------------- /example/src/routes/_router.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Basic = React.lazy(() => import('./Basic')) 4 | const Pagination = React.lazy(() => import('./Pagination')) 5 | 6 | const router = { 7 | Basic: , 8 | Pagination: 9 | } 10 | 11 | export default router 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # builds 7 | build 8 | dist 9 | .rpt2_cache 10 | docs/.generated-docs 11 | docs 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | .env.* 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Table UI Monorepo 2 | 3 | [![NPM](https://img.shields.io/npm/v/react-table-ui.svg)](https://www.npmjs.com/package/react-table-ui) 4 | [![GitHub](https://img.shields.io/badge/GitHub-Repo-black)](https://github.com/GuptaSiddhant/react-table-ui) 5 | 6 | Out-of-the-box UI for [React-Table 7](https://react-table-v7.tanstack.com). 7 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { BrowserRouter } from 'react-router-dom' 4 | 5 | import App from './App' 6 | import './index.css' 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Vite + React + TS 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "name": "React-Table UI Docs", 4 | "includeVersion": false, 5 | "excludeExternals": false, 6 | "excludeInternal": true, 7 | "categoryOrder": ["Important", "Component", "Options", "State", "*"], 8 | "includes": "./lib", 9 | 10 | "entryPoints": ["./lib/src/index.ts"], 11 | "readme": "./lib/README.md", 12 | "skipErrorChecking": true 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["dom", "esnext"], 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "strict": true, 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "emitDeclarationOnly": true 13 | }, 14 | "exclude": ["node_modules", "dist", "docs", "examples/*"] 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/context.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import type { DataType, TableContext } from './types' 3 | 4 | export const tableContext = React.createContext | undefined>( 5 | undefined 6 | ) 7 | 8 | export default function useTableContext< 9 | Data extends DataType 10 | >(): TableContext { 11 | const ctx = React.useContext(tableContext) 12 | if (!ctx) 13 | throw new Error( 14 | 'useTableContext cannot be used outside ReactTableUI component.' 15 | ) 16 | 17 | return ctx 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default CSS definition for typescript, 3 | * will be overridden with file-specific definitions by rollup 4 | */ 5 | declare module '*.css' { 6 | const content: { [className: string]: string } 7 | export default content 8 | } 9 | 10 | interface SvgrComponent 11 | extends React.FunctionComponent> {} 12 | 13 | declare module '*.svg' { 14 | const svgUrl: string 15 | const svgComponent: SvgrComponent 16 | export default svgUrl 17 | export { svgComponent as ReactComponent } 18 | } 19 | -------------------------------------------------------------------------------- /lib/src/types/TableOptions.ts: -------------------------------------------------------------------------------- 1 | import type { UseTableOptions, TableState } from './ReactTable' 2 | 3 | import type { DataType, StateChangeHandler } from './DataType' 4 | 5 | /** Type interface of Table specific options. 6 | * @category Options */ 7 | export interface TableOptions 8 | extends Omit, 'columns' | 'data'> { 9 | /** Callback executed when table's state is updated. 10 | * The function must be wrapped in useCallback hook. */ 11 | onStateChange?: StateChangeHandler> 12 | } 13 | 14 | export default TableOptions 15 | -------------------------------------------------------------------------------- /lib/src/styles/index.ts: -------------------------------------------------------------------------------- 1 | import styled from '../utilities/styled' 2 | 3 | import { ButtonStyle } from '../common/Button' 4 | import { CellStyle } from '../common/Cell' 5 | 6 | import commonStyle from './commonStyle' 7 | import containerStyle from './containerStyle' 8 | import TableStyle from './TableStyle' 9 | import TitleBarStyle from './TitleBarStyle' 10 | import menuStyle from './menuStyle' 11 | 12 | const styles = styled.rtui` 13 | ${containerStyle} 14 | ${commonStyle} 15 | ${menuStyle} 16 | ${ButtonStyle} 17 | ${TitleBarStyle} 18 | ${TableStyle} 19 | ${CellStyle} 20 | ` 21 | 22 | export default styles 23 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-table-ui-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "namor": "^2.0.4", 13 | "react": "*", 14 | "react-dom": "*", 15 | "react-router-dom": "^6.3.0", 16 | "react-table-ui": "*" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "*", 20 | "@types/react-dom": "*", 21 | "@vitejs/plugin-react": "^2.1.0", 22 | "typescript": "*", 23 | "vite": "^3.1.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/types/DataType.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | 3 | export interface DataType extends Record { 4 | /** Select data-row by default. */ 5 | isSelected?: boolean 6 | 7 | /** Always expanded data-row, regardless of state. */ 8 | expanded?: boolean 9 | 10 | /** Sub rows visible when expanded. */ 11 | subRows?: DataType[] 12 | 13 | /** Custom component shown when row is expanded. 14 | * @category Custom Component */ 15 | subComponent?: ReactNode 16 | } 17 | 18 | /** Type for Callback to handle state changes. */ 19 | export type StateChangeHandler = (state: State) => void 20 | 21 | export default DataType 22 | -------------------------------------------------------------------------------- /lib/src/utilities/clsx.ts: -------------------------------------------------------------------------------- 1 | export const commonClassName = 'RTUI' 2 | 3 | type FalsyValue = undefined | null | false | 0 | '' 4 | type ClassName = string | FalsyValue | ClassName[] | Record 5 | 6 | export default function clsx(...classNames: ClassName[]): string { 7 | return classNames 8 | .map((name) => { 9 | if (!name) return null 10 | if (typeof name !== 'object') return name 11 | if (Array.isArray(name)) return clsx(name) 12 | return Object.entries(name) 13 | .filter(([, v]) => Boolean(v)) 14 | .map(([k]) => k) 15 | .join(' ') 16 | }) 17 | .filter(Boolean) 18 | .join(' ') 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/logic/useCreateDefaultColumns.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import type { DataType, Column } from '../types' 3 | 4 | const toTitleCase = (str: string) => 5 | str.slice(0, 1).toUpperCase() + str.slice(1) 6 | 7 | const useCreateDefaultColumns = ( 8 | data: Data[] 9 | ): Column[] => 10 | useMemo(() => { 11 | const uniqueKeys = Object.keys(Object.assign({}, ...data)) 12 | return uniqueKeys 13 | .filter((key) => !['subRows', 'subComponent'].includes(key)) 14 | .map((key) => ({ 15 | accessor: key, 16 | Header: toTitleCase(key) 17 | })) 18 | }, [data]) 19 | 20 | export default useCreateDefaultColumns 21 | -------------------------------------------------------------------------------- /lib/src/common/Cell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { color, spacing } from '../utilities/theme' 3 | import clsx from '../utilities/clsx' 4 | import styled from '../utilities/styled' 5 | 6 | export const CellStyle = styled('td')` 7 | &, 8 | .th { 9 | padding: ${spacing.sm} ${spacing.md}; 10 | background: ${color.background.primary}; 11 | overflow: hidden; 12 | } 13 | ` 14 | 15 | const Cell = React.forwardRef< 16 | HTMLDivElement, 17 | React.HTMLAttributes 18 | >(({ className, ...props }, ref) => { 19 | return ( 20 |
26 | ) 27 | }) 28 | 29 | export default Cell 30 | -------------------------------------------------------------------------------- /lib/src/utilities/useReachUI.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | const useReachUI = (type: 'menu') => { 4 | const [reach, setReach] = useState(null) 5 | 6 | useEffect(() => { 7 | let isMounted = true 8 | const handleSetReach = (mod: any) => isMounted && setReach(mod) 9 | switch (type) { 10 | case 'menu': 11 | import('@reach/menu-button').then(handleSetReach).catch(console.error) 12 | break 13 | } 14 | 15 | return () => { 16 | isMounted = false 17 | } 18 | }, [type]) 19 | 20 | switch (type) { 21 | case 'menu': 22 | return reach as typeof import('@reach/menu-button/dist/declarations/src') 23 | default: 24 | return null 25 | } 26 | } 27 | 28 | export default useReachUI 29 | -------------------------------------------------------------------------------- /lib/src/logic/useManualPagination.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import type { DataType, TableContext } from '../types' 3 | 4 | /** Execute callback to fetch data 5 | * when working with manual pagination */ 6 | const useManualPagination = ({ 7 | tableInstance: { 8 | state: { pageSize, pageIndex } 9 | }, 10 | tableProps: { paginationOptions = {} } 11 | }: TableContext) => { 12 | const { disablePagination, manualPagination, fetchData } = paginationOptions 13 | 14 | React.useEffect(() => { 15 | if (!disablePagination && manualPagination && fetchData) { 16 | fetchData({ pageIndex, pageSize }) 17 | } 18 | }, [fetchData, pageIndex, pageSize, disablePagination, manualPagination]) 19 | } 20 | 21 | export default useManualPagination 22 | -------------------------------------------------------------------------------- /example/src/routes/Basic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactTableUI from 'react-table-ui' 3 | import type { TableInstance, DataType } from 'react-table-ui' 4 | 5 | /** Structure of data provided foe each row. */ 6 | interface User extends DataType { 7 | name: string 8 | age: number 9 | } 10 | 11 | export default function App() { 12 | const data: User[] = React.useMemo( 13 | () => [ 14 | { name: 'Abc Xyx', age: 20 }, 15 | { name: 'Def Uvw', age: 25 }, 16 | { name: 'Ghi Rst', age: 23 }, 17 | { name: 'Jklm Nopq', age: 30 } 18 | ], 19 | [] 20 | ) 21 | 22 | // Create an instance ref that will hold the reference to React Table instance. 23 | const tableInstanceRef = React.useRef>(null) 24 | 25 | return ( 26 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/utilities/useStylesheet.ts: -------------------------------------------------------------------------------- 1 | import { useInsertionEffect } from 'react' 2 | import clsx from './clsx' 3 | import styles from '../styles' 4 | 5 | /** Place Styles in DOM */ 6 | const useStylesheet = (cssString: string = ''): void => { 7 | useInsertionEffect(() => { 8 | const styleTagID = clsx('styles') 9 | const existingStyleTag = document.getElementById( 10 | styleTagID 11 | ) as HTMLStyleElement | null 12 | 13 | const newStyleTag = document.createElement('style') 14 | newStyleTag.id = styleTagID 15 | 16 | const styleTag: HTMLStyleElement = existingStyleTag || newStyleTag 17 | styleTag.innerHTML = cssString + ' ' + styles 18 | 19 | if (!existingStyleTag) 20 | document.head.insertAdjacentElement('afterbegin', styleTag) 21 | 22 | return () => { 23 | if (styleTag) document.head.removeChild(styleTag) 24 | } 25 | }, [cssString]) 26 | } 27 | 28 | export default useStylesheet 29 | -------------------------------------------------------------------------------- /lib/src/utilities/deepMerge.ts: -------------------------------------------------------------------------------- 1 | /** Deep merge objects */ 2 | const deepMerge = >(...objects: T[]): T => { 3 | // Variables 4 | let target: Record = {} 5 | 6 | // Merge the object into the target object 7 | let merger = (obj: T) => { 8 | for (let prop in obj) { 9 | if (obj.hasOwnProperty(prop)) { 10 | if (Object.prototype.toString.call(obj[prop]) === '[object Object]') { 11 | // If we're doing a deep merge and the property is an object 12 | target[prop] = deepMerge(target[prop], obj[prop]) 13 | } else { 14 | // Otherwise, do a regular merge 15 | target[prop] = obj[prop] 16 | } 17 | } 18 | } 19 | } 20 | 21 | //Loop through each object and conduct a merge 22 | for (let i = 0; i < objects.length; i++) { 23 | merger(objects[i]) 24 | } 25 | 26 | return target as T 27 | } 28 | 29 | export default deepMerge 30 | -------------------------------------------------------------------------------- /lib/src/utilities/styled.ts: -------------------------------------------------------------------------------- 1 | import { commonClassName } from './clsx' 2 | 3 | /** Mock "styled-components" function to trick JS-in-CSS Intellisense */ 4 | const styled = (element: string) => ( 5 | literals: TemplateStringsArray, 6 | ...args: string[] 7 | ): string => { 8 | let result = '' 9 | 10 | // interleave the literals with the placeholders 11 | for (let i = 0; i < args.length; i++) { 12 | result += literals[i] 13 | result += args[i] 14 | } 15 | // add the last literal 16 | result += literals[literals.length - 1] 17 | 18 | // Replace "&" with common className 19 | const replacementClassName = [commonClassName, element] 20 | .filter((str) => !!str) 21 | .map((c) => `.${c}`) 22 | .join(' ') 23 | return result.replaceAll('&', replacementClassName) 24 | } 25 | 26 | /** Add element shorthands */ 27 | styled.div = styled('') 28 | styled.rtui = styled('RTUI') 29 | styled.table = styled('Table') 30 | 31 | export default styled 32 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "plugin:prettier/recommended", 7 | "prettier/standard", 8 | "prettier/react", 9 | "plugin:@typescript-eslint/eslint-recommended" 10 | ], 11 | "plugins": [ 12 | "@typescript-eslint", 13 | "@typescript-eslint/eslint-plugin", 14 | "eslint-plugin-tsdoc" 15 | ], 16 | "env": { 17 | "node": true 18 | }, 19 | "parserOptions": { 20 | "ecmaVersion": 2020, 21 | "ecmaFeatures": { 22 | "legacyDecorators": true, 23 | "jsx": true 24 | } 25 | }, 26 | "settings": { 27 | "react": { 28 | "version": "18" 29 | } 30 | }, 31 | "rules": { 32 | "space-before-function-paren": 0, 33 | "react/prop-types": 0, 34 | "react/jsx-handler-names": 0, 35 | "react/jsx-fragments": 0, 36 | "react/no-unused-prop-types": 0, 37 | "import/export": 0, 38 | "no-unused-vars": "off", 39 | "@typescript-eslint/no-unused-vars": ["error"] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/types/OtherOptions.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | 3 | /** Type interface for loading options. 4 | * @category Options */ 5 | export interface LoadingOptions { 6 | /** Loading state. 7 | * @default false */ 8 | loading?: boolean 9 | 10 | /** If true, loading is done in background. 11 | * Loading indicator is not shown if there is data on screen. 12 | * @default true */ 13 | backgroundLoading?: boolean 14 | 15 | /** Loading status is shown in status bar. 16 | * @default true */ 17 | showLoadingStatus?: boolean 18 | 19 | // ---------- 20 | // Components 21 | 22 | /** Component rendered during loading. 23 | * @default Spinner 24 | * @category Custom Component */ 25 | Component?: ReactNode 26 | } 27 | 28 | /** Type interface for setting header/footer freeze. 29 | * @category Options */ 30 | export interface FreezeOptions { 31 | /** @default true */ 32 | columns?: boolean 33 | 34 | /** @default true */ 35 | header?: boolean 36 | 37 | /** @default true */ 38 | footer?: boolean 39 | } 40 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | height: 100vh; 5 | font-family: sans-serif; 6 | overflow: hidden; 7 | } 8 | 9 | #root { 10 | height: calc(100% - 1rem); 11 | margin: 0.5rem; 12 | display: flex; 13 | flex-direction: column; 14 | gap: 0.5rem; 15 | } 16 | 17 | header { 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | padding: 0.5rem 1rem; 22 | background: #eee; 23 | border-radius: 0.25rem; 24 | } 25 | 26 | h1, 27 | h2 { 28 | margin: 0; 29 | font-size: 1.5rem; 30 | } 31 | 32 | h1 a, 33 | h2 a { 34 | text-decoration: none; 35 | color: black; 36 | } 37 | 38 | nav { 39 | flex: 1; 40 | display: flex; 41 | justify-content: flex-end; 42 | gap: 0.5rem; 43 | list-style: none; 44 | flex-wrap: wrap; 45 | } 46 | 47 | a.active { 48 | font-weight: bold; 49 | } 50 | 51 | main { 52 | flex: 1; 53 | overflow: hidden; 54 | } 55 | 56 | #homepage { 57 | padding: 1rem; 58 | } 59 | #homepage ul { 60 | display: flex; 61 | flex-direction: column; 62 | gap: 0.25rem; 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/logic/useScrollPosition.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | interface ScrollPosition { 4 | x: number 5 | y: number 6 | } 7 | 8 | export const useScrollPosition = () => { 9 | const elementRef = React.useRef(null) 10 | const [scrollPosition, setScrollPosition] = React.useState({ 11 | y: 0, 12 | x: 0 13 | }) 14 | 15 | const handleElementScroll = React.useCallback(() => { 16 | const element = elementRef.current 17 | const newScrollPos: ScrollPosition = { 18 | y: element?.scrollTop || 0, 19 | x: element?.scrollLeft || 0 20 | } 21 | setScrollPosition(newScrollPos) 22 | }, []) 23 | 24 | React.useEffect(() => { 25 | elementRef.current?.addEventListener('scroll', handleElementScroll) 26 | 27 | return () => 28 | elementRef.current?.removeEventListener('scroll', handleElementScroll) 29 | }, [handleElementScroll]) 30 | 31 | return [ 32 | elementRef, 33 | { 34 | scrollPosY: scrollPosition.y, 35 | scrollPosX: scrollPosition.x 36 | } 37 | ] as const 38 | } 39 | 40 | export default useScrollPosition 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Siddhant Gupta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/styles/StatusBarStyle.ts: -------------------------------------------------------------------------------- 1 | import styled from '../utilities/styled' 2 | import { border, pxToEm, spacing, color } from '../utilities/theme' 3 | 4 | export default styled('StatusBar')` 5 | & { 6 | display: flex; 7 | flex-wrap: wrap; 8 | justify-content: space-between; 9 | align-items: center; 10 | min-height: ${pxToEm(56)}; 11 | width: 100%; 12 | border-top: ${border.default}; 13 | background-color: ${color.background.primary}; 14 | } 15 | 16 | & .Status { 17 | flex-basis: 1; 18 | flex-grow: 1; 19 | min-height: ${pxToEm(40)}; 20 | padding: 0 ${spacing.xl}; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | min-width: max-content; 25 | max-width: 100%; 26 | } 27 | 28 | & .spacer { 29 | flex-basis: 1; 30 | flex-grow: 999; 31 | } 32 | 33 | & .Pagination { 34 | display: flex; 35 | flex-basis: 1; 36 | flex-grow: 1; 37 | align-items: center; 38 | justify-content: space-between; 39 | min-width: max-content; 40 | max-width: 100%; 41 | } 42 | & .Pagination input { 43 | margin: 0 ${spacing.md}; 44 | text-align: center; 45 | } 46 | ` 47 | -------------------------------------------------------------------------------- /lib/src/components/TitleBar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import type { DataType } from '../../types' 4 | import clsx from '../../utilities/clsx' 5 | 6 | import GlobalFilter from './GlobalFilter' 7 | import VisibleFilterAction from './VisibleFilterAction' 8 | 9 | import { TableActions, MultiRowActions } from './userDefinedActions' 10 | import PreferencesAction from './PreferencesAction' 11 | import useTableContext from '../../context' 12 | 13 | /** 14 | * Title component of RTUI. 15 | * It contains table title and actions. 16 | */ 17 | export default function TitleBar(): JSX.Element | null { 18 | const context = useTableContext() 19 | const { titleBar = true } = context.tableProps.styleOptions || {} 20 | 21 | return titleBar ? ( 22 |
23 | 24 | 25 | 26 | 27 |
28 | ) : ( 29 |
30 | ) 31 | } 32 | 33 | function SystemActions(): JSX.Element | null { 34 | return ( 35 |
36 | 37 | {/* */} 38 | 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /lib/scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | 4 | const args = process.argv.slice(2) 5 | const isWatchMode = args.includes('watch') 6 | 7 | /** @type import('esbuild').WatchMode */ 8 | const watchMode = { 9 | onRebuild(error, result) { 10 | if (error) console.error('Build failed:', error) 11 | else { 12 | console.log('Build succeeded') 13 | ;(result?.warnings || []).forEach(console.log) 14 | } 15 | } 16 | } 17 | 18 | /** @type import('esbuild').BuildOptions */ 19 | const esbuildOptions = { 20 | watch: isWatchMode && watchMode, 21 | entryPoints: ['./src/index.ts'], 22 | bundle: true, 23 | outdir: 'dist', 24 | format: 'esm', 25 | minify: true, 26 | platform: 'neutral', 27 | sourcemap: 'external', 28 | splitting: true, 29 | external: ['react', 'react-*', '@reach/*'], 30 | treeShaking: true, 31 | chunkNames: 'chunks/[name]-[hash]', 32 | color: true, 33 | logLevel: 'info', 34 | logLimit: 5 35 | } 36 | 37 | // Execute ESBuild 38 | console.log('Building with ESBuild...') 39 | require('esbuild') 40 | .build({ ...esbuildOptions }) 41 | .catch(() => process.exit(1)) 42 | 43 | console.log('Building types...') 44 | require('child_process').spawnSync( 45 | 'npx', 46 | isWatchMode ? ['tsc', '--watch'] : ['tsc'] 47 | ) 48 | -------------------------------------------------------------------------------- /lib/scripts/postInstall.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | let isTSProject = false 7 | 8 | const rootPath = process.env.INIT_CWD 9 | if (!rootPath) { 10 | console.log('Skipping post-install.') 11 | process.exit(0) 12 | } 13 | 14 | // Check TS 15 | const pathTSConfig = path.join(rootPath, 'tsconfig.json') 16 | const isTSConfigExist = fs.existsSync(pathTSConfig) 17 | if (!isTSConfigExist) { 18 | console.log('Not a TS project.') 19 | process.exit(0) 20 | } 21 | isTSProject = true 22 | 23 | if (isTSProject) { 24 | // Check RTUI config 25 | const rtConfigFilename = 'react-table-config.d.ts' 26 | const pathRTConfig = path.join( 27 | rootPath, 28 | 'node_modules', 29 | 'react-table-ui', 30 | rtConfigFilename 31 | ) 32 | const isRTConfigExist = fs.existsSync(pathRTConfig) 33 | if (!isRTConfigExist) { 34 | console.log('RTUI is not installed.') 35 | process.exit(0) 36 | } 37 | 38 | const pathRTConfigDestination = path.join(rootPath, 'src', rtConfigFilename) 39 | try { 40 | fs.copyFileSync(pathRTConfig, pathRTConfigDestination) 41 | console.log('SUCCESS: React Table configuration added.') 42 | process.exit(0) 43 | } catch { 44 | console.log(`FAILED: React Table configuration couldn't be added.`) 45 | process.exit(1) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/components/TitleBar/VisibleFilterAction.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import type { DataType } from '../../types' 4 | import Button from '../../common/Button' 5 | import Icon from '../../common/Icon' 6 | import useTableContext from '../../context' 7 | 8 | export default function VisibleFilterAction< 9 | Data extends DataType 10 | >(): JSX.Element | null { 11 | const { tableProps, tableInstance } = useTableContext() 12 | const { 13 | state: { filtersVisible }, 14 | toggleFiltersVisible 15 | } = tableInstance 16 | const { 17 | alwaysShowFilters, 18 | disableFilters, 19 | showFiltersActionIndicator, 20 | hideFiltersActionIndicator 21 | } = tableProps.filtersOptions || {} 22 | const text = tableProps.localeOptions?.text 23 | 24 | if (disableFilters || alwaysShowFilters) return null 25 | 26 | return ( 27 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /lib/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { RefObject } from 'react' 2 | import type { TableInstance } from './ReactTable' 3 | import type DataType from './DataType' 4 | import type ReactTableUIProps from './ReactTableUIProps' 5 | 6 | /** 7 | * Context of the Table, 8 | * made available to each component and helper. 9 | */ 10 | interface TableContext { 11 | /** Initiated instance of react-table. 12 | * @see [React Table Docs](https://react-table-v7.tanstack.com/docs/api/useTable#instance-properties) */ 13 | tableInstance: TableInstance 14 | 15 | /** Props passed by user to react-table. */ 16 | tableProps: ReactTableUIProps 17 | 18 | /** Ref to React Table UI component. */ 19 | tableRef?: RefObject 20 | } 21 | 22 | // export default TableContext 23 | 24 | export type { DataType, TableContext } 25 | export * from './ReactTableUIProps' 26 | export * from './SortingOptions' 27 | export * from './FiltersOptions' 28 | export * from './GlobalFilterOptions' 29 | export * from './ExpandedOptions' 30 | export * from './RowSelectOptions' 31 | export * from './PaginationOptions' 32 | export * from './OtherOptions' 33 | export * from './RowStateOptions' 34 | export * from './TableOptions' 35 | export * from './ActionOptions' 36 | export * from './StyleOptions' 37 | export * from './LocaleOptions' 38 | export * from './ReactTable' 39 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - lib/** 8 | - typedoc.json 9 | - package.json 10 | - .github/workflows/publish.yaml 11 | 12 | env: 13 | INPUT_TOKEN: ${{ secrets.NPM_TOKEN }} 14 | 15 | jobs: 16 | publish-lib: 17 | name: Publish library 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | working-directory: './lib' 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: actions/setup-node@v3 25 | with: 26 | node-version: 16 27 | cache: 'yarn' 28 | 29 | - name: Setup NPM TOKEN 30 | run: echo "//registry.npmjs.org/:_authToken=$INPUT_TOKEN" > ~/.npmrc 31 | 32 | - run: yarn 33 | - run: node scripts/publish.js 34 | 35 | - name: Push changes to repo 36 | run: git push "https://${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" --follow-tags 37 | 38 | docs: 39 | name: Publish Docs 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: actions/setup-node@v3 44 | with: 45 | node-version: 16 46 | cache: 'yarn' 47 | - run: yarn 48 | - run: yarn build:docs 49 | 50 | - name: Deploy 🚀 51 | uses: JamesIves/github-pages-deploy-action@4.1.3 52 | with: 53 | branch: docs 54 | folder: docs 55 | -------------------------------------------------------------------------------- /lib/src/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Icon from '../common/Icon' 4 | import Button from '../common/Button' 5 | import FocusTrap from '../common/FocusTrap' 6 | import type { DataType } from '../types' 7 | import useTableContext from '../context' 8 | 9 | export default function Modal(): JSX.Element | null { 10 | const context = useTableContext() 11 | const { 12 | state: { modal }, 13 | resetModal 14 | } = context.tableInstance 15 | const text = context.tableProps.localeOptions?.text 16 | 17 | const handleSave = () => { 18 | modal?.onSave && modal.onSave() 19 | resetModal() 20 | } 21 | 22 | return modal ? ( 23 |
24 | e.stopPropagation()}> 25 |
26 | {modal.title} 27 |
28 | {modal.onSave && ( 29 | 35 | )} 36 | 39 |
40 |
41 |
{modal.children}
42 |
43 |
44 | ) : null 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/types/RowStateOptions.ts: -------------------------------------------------------------------------------- 1 | // import type { FC } from 'react' 2 | import type { 3 | UseRowStateOptions, 4 | UseRowStateState, 5 | UseRowStateLocalState, 6 | Row, 7 | Cell 8 | } from './ReactTable' 9 | import type { DataType, StateChangeHandler } from './DataType' 10 | 11 | /** Type interface for RowState options. 12 | * @category Options */ 13 | export interface RowStateOptions 14 | extends UseRowStateOptions { 15 | /** Initial settings of row-select. 16 | * @example 17 | * ``` 18 | * { rowState: { [rowId]: { cellState: { [columnId]: {} } } } } 19 | * ``` 20 | */ 21 | initialState?: Partial> 22 | 23 | /** Callback executed when rows are selected or deselected. 24 | * The function must be wrapped in useCallback hook. */ 25 | onStateChange?: StateChangeHandler> 26 | 27 | /** This function should return the initial state for a row. */ 28 | initialRowStateAccessor?: (row: Row) => UseRowStateLocalState 29 | 30 | /** This function should return the initial state for a cell. */ 31 | initialCellStateAccessor?: (cell: Cell) => UseRowStateLocalState 32 | 33 | /** Disable row-state management table. 34 | * @default false */ 35 | disableRowState?: boolean 36 | 37 | /** Reset row-state when data changes. 38 | * @default true */ 39 | autoResetRowState?: boolean 40 | 41 | // getResetRowStateDeps: (instance: TableInstance) => any[] 42 | } 43 | 44 | export default RowStateOptions 45 | -------------------------------------------------------------------------------- /lib/src/types/RowSelectOptions.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import type { 3 | UseRowSelectOptions, 4 | UseRowSelectState, 5 | TableToggleCommonProps, 6 | IdType 7 | } from './ReactTable' 8 | import type { DataType, StateChangeHandler } from './DataType' 9 | 10 | /** Type interface for RowSelect options. 11 | * @category Options */ 12 | export interface RowSelectOptions 13 | extends UseRowSelectOptions { 14 | /** Initial settings of row-select. 15 | * @example 16 | * ``` 17 | * { selectedRowIds: { [rowId]: boolean } } 18 | * ``` 19 | */ 20 | initialState?: Partial> 21 | 22 | /** Disable row selection. 23 | * @default false */ 24 | disableRowSelect?: boolean 25 | 26 | /** Key is found on the original data row, and it is true, 27 | * this row will be manually selected. 28 | * @default 'isSelected' */ 29 | manualRowSelectedKey?: IdType 30 | 31 | /** Reset row-selection when data changes. 32 | * @default true */ 33 | autoResetSelectedRows?: boolean 34 | 35 | /** Allow sub-rows to be selected. 36 | * @default false */ 37 | selectSubRows?: boolean 38 | 39 | /** Callback executed when rows are selected or deselected. 40 | * The function must be wrapped in useCallback hook. */ 41 | onStateChange?: StateChangeHandler> 42 | 43 | // ---------- 44 | // Components 45 | 46 | /** Component to render to denote row selection 47 | * @category Custom Component */ 48 | Component?: RowSelectComponent 49 | } 50 | 51 | export type RowSelectComponent = FC 52 | 53 | export default RowSelectOptions 54 | -------------------------------------------------------------------------------- /lib/src/styles/menuStyle.ts: -------------------------------------------------------------------------------- 1 | import styled from '../utilities/styled' 2 | import { color, radius, spacing } from '../utilities/theme' 3 | 4 | export default styled.div` 5 | :root { 6 | --reach-menu-button: 1; 7 | } 8 | 9 | [data-reach-menu], 10 | [data-reach-menu-popover] { 11 | display: block; 12 | position: absolute; 13 | } 14 | 15 | [data-reach-menu][hidden], 16 | [data-reach-menu-popover][hidden] { 17 | display: none; 18 | } 19 | 20 | [data-reach-menu-list], 21 | [data-reach-menu-items] { 22 | display: block; 23 | white-space: nowrap; 24 | border: solid 1px ${color.border.primary}; 25 | background: hsla(0, 100%, 100%, 0.99); 26 | outline: none; 27 | overflow: hidden; 28 | padding: ${spacing.sm} 0; 29 | font-size: 85%; 30 | border-radius: ${radius.sm}; 31 | box-shadow: 0 2px 8px 0 rgba(0,0,0,0.25); 32 | } 33 | 34 | [data-reach-menu-item] { 35 | display: block; 36 | user-select: none; 37 | 38 | /* reach-menu-item */ 39 | cursor: pointer; 40 | 41 | /* a */ 42 | display: block; 43 | color: inherit; 44 | font: inherit; 45 | text-decoration: initial; 46 | 47 | /* both */ 48 | padding: ${spacing.md} ${spacing.xl}; 49 | 50 | display: flex; 51 | justify-content: space-between; 52 | align-items: center; 53 | gap: ${spacing.md}; 54 | } 55 | 56 | /* pseudo pseudo selector */ 57 | [data-reach-menu-item][data-selected] { 58 | background: ${color.background.selected}; 59 | color: ${color.text.selected}; 60 | outline: none; 61 | } 62 | 63 | [data-reach-menu-item][aria-disabled] { 64 | opacity: 0.5; 65 | cursor: not-allowed; 66 | } 67 | ` 68 | -------------------------------------------------------------------------------- /lib/src/components/ReactTableUI.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { tableContext } from '../context' 4 | import useReactTableUI from '../logic/useReactTableUI' 5 | import useTheme from '../logic/useTheme' 6 | import clsx, { commonClassName } from '../utilities/clsx' 7 | import type { DataType, ReactTableUIProps, TableInstance } from '../types' 8 | 9 | import TitleBar from './TitleBar' 10 | import Table from './Table' 11 | import StatusBar from './StatusBar' 12 | import Modal from './Modal' 13 | 14 | /** 15 | * React Table UI 16 | * --- 17 | * 18 | * Styled table with all things configured. 19 | * 20 | * @category Component 21 | */ 22 | export default function ReactTableUI({ 23 | tableInstanceRef, 24 | ...tableProps 25 | }: ReactTableUIProps & { 26 | tableInstanceRef?: React.RefObject> 27 | }): JSX.Element { 28 | // Internal ref for table 29 | const tableRef = React.useRef(null) 30 | 31 | // Add styles to DOM 32 | useTheme(tableProps) 33 | 34 | // Create Table's context 35 | const context = useReactTableUI(tableProps, tableRef) 36 | 37 | // Set TableInstance to tableInstanceRef 38 | React.useImperativeHandle(tableInstanceRef, () => context.tableInstance) 39 | 40 | const { borderless = false } = context.tableProps.styleOptions || {} 41 | 42 | return ( 43 | 44 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/utilities/theme.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeColors, ThemeColor, ThemeSpacing } from '../types' 2 | import { commonClassName } from './clsx' 3 | 4 | const getThemeColorObject = (category: Omit) => 5 | ['primary', 'secondary', 'disabled', 'inverse', 'selected', 'none'].reduce( 6 | (obj, name) => ({ 7 | ...obj, 8 | [name]: `var(--${commonClassName}-color-${category}-${name})` 9 | }), 10 | {} as ThemeColor 11 | ) 12 | 13 | const getThemeSpacingObject = (category: string) => 14 | ['none', 'xs', 'sm', 'md', 'lg', 'xl'].reduce( 15 | (obj, name) => ({ 16 | ...obj, 17 | [name]: `var(--${commonClassName}-${category}-${name})` 18 | }), 19 | {} as ThemeSpacing 20 | ) 21 | 22 | export const pxToEm = (px: number) => px / 16 + 'em' 23 | 24 | export const color: ThemeColors = { 25 | text: getThemeColorObject('text'), 26 | background: getThemeColorObject('background'), 27 | border: getThemeColorObject('border'), 28 | accent: { 29 | default: `var(--${commonClassName}-color-accent-default)`, 30 | lighter: `var(--${commonClassName}-color-accent-lighter)`, 31 | darker: `var(--${commonClassName}-color-accent-darker)` 32 | } 33 | } as const 34 | 35 | export const spacing = getThemeSpacingObject('spacing') 36 | 37 | export const radius = getThemeSpacingObject('radius') 38 | 39 | export const mediaQueries = { 40 | mobile: `@media (max-width: 600px)` 41 | } 42 | 43 | export const border = { 44 | default: `1px solid ${color.border.primary}` 45 | } as const 46 | 47 | export const CenterStyle = ` 48 | width: 100%; 49 | height: 100%; 50 | display: flex; 51 | align-items: center; 52 | justify-content: center; 53 | ` 54 | -------------------------------------------------------------------------------- /lib/src/common/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes, forwardRef } from 'react' 2 | import clsx from '../utilities/clsx' 3 | import styled from '../utilities/styled' 4 | import { color, radius, spacing, pxToEm } from '../utilities/theme' 5 | 6 | export interface IconButtonProps 7 | extends ButtonHTMLAttributes {} 8 | 9 | export const ButtonStyle = styled('Button')` 10 | & { 11 | background: none; 12 | border: none; 13 | line-height: 1; 14 | border-radius: ${radius.sm}; 15 | cursor: pointer; 16 | padding: ${spacing.none}; 17 | } 18 | 19 | & .button-content { 20 | background: none; 21 | margin: ${spacing.sm}; 22 | padding: ${spacing.sm}; 23 | min-width: ${pxToEm(40)}; 24 | min-height: ${pxToEm(40)}; 25 | border-radius: ${radius.xs}; 26 | 27 | display: flex; 28 | align-items: center; 29 | justify-content: center; 30 | } 31 | 32 | &:hover .button-content { 33 | background: ${color.background.selected}; 34 | } 35 | 36 | &:disabled, 37 | &:disabled .button-content { 38 | cursor: not-allowed; 39 | background: none; 40 | } 41 | 42 | & .iconWithLabel { 43 | display: grid; 44 | align-items: center; 45 | grid-template-columns: max-content max-content; 46 | gap: ${spacing.md}; 47 | padding: 0 ${spacing.md}; 48 | } 49 | ` 50 | 51 | function Button( 52 | { children, className = '', ...props }: IconButtonProps, 53 | ref: React.ForwardedRef 54 | ) { 55 | return ( 56 | 59 | ) 60 | } 61 | 62 | export default forwardRef(Button) 63 | -------------------------------------------------------------------------------- /lib/src/styles/commonStyle.ts: -------------------------------------------------------------------------------- 1 | import styled from '../utilities/styled' 2 | import { border, color, radius, pxToEm } from '../utilities/theme' 3 | 4 | const checkboxStyle = styled('checkbox')` 5 | & { 6 | height: 1em; 7 | width: 1em; 8 | -webkit-appearance: none; 9 | -moz-appearance: none; 10 | -o-appearance: none; 11 | appearance: none; 12 | 13 | outline: none; 14 | border: 1px solid ${color.border.primary}; 15 | border-radius: ${radius.sm}; 16 | background-color: ${color.background.primary}; 17 | cursor: pointer; 18 | } 19 | 20 | &:hover, 21 | &:focus { 22 | outline: 1px solid ${color.accent.default}; 23 | } 24 | 25 | &:checked, 26 | &:indeterminate { 27 | border-color: ${color.accent.default}; 28 | background-color: ${color.accent.default}; 29 | box-shadow: inset 0 0 0 2px ${color.background.primary}; 30 | } 31 | 32 | &:indeterminate { 33 | box-shadow: inset 0 ${pxToEm(4)} 0 2px ${color.background.primary}, 34 | inset 0 -${pxToEm(4)} 0 2px ${color.background.primary}; 35 | } 36 | ` 37 | 38 | export default styled.div` 39 | & input[type='search'], 40 | & input[type='text'], 41 | & input[type='number'], 42 | & input[type='checkbox'] { 43 | font: inherit; 44 | border-radius: ${radius.xs}; 45 | border: ${border.default}; 46 | color: ${color.text.primary}; 47 | } 48 | 49 | & input[type='search'], 50 | & input[type='text'] { 51 | width: 100%; 52 | padding: ${radius.sm} ${radius.md}; 53 | } 54 | 55 | & input[type='search'] { 56 | -webkit-appearance: none; 57 | } 58 | 59 | & input[type='search']::-webkit-search-decoration { 60 | -webkit-appearance: none; 61 | } 62 | 63 | ${checkboxStyle} 64 | ` 65 | -------------------------------------------------------------------------------- /lib/src/logic/generateUseExpandedColumn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import systemColumns from '../utilities/systemColumns' 4 | import type { 5 | DataType, 6 | ReactTableUIProps, 7 | Hooks, 8 | CellProps, 9 | HeaderProps, 10 | Column 11 | } from '../types' 12 | 13 | const generateUseExpandedColumn = ( 14 | props: ReactTableUIProps 15 | ) => { 16 | const { collapsedIndicator = '→', expandedIndicator = '↓' } = 17 | props.expandedOptions || {} 18 | 19 | const disableExpander = !props.data.some( 20 | (d) => d.subRows?.length || d.subComponent 21 | ) 22 | 23 | const Header = ({ 24 | getToggleAllRowsExpandedProps, 25 | isAllRowsExpanded 26 | }: HeaderProps) => ( 27 | 28 | {isAllRowsExpanded ? expandedIndicator : collapsedIndicator} 29 | 30 | ) 31 | 32 | const Cell = ({ row }: CellProps) => 33 | row.canExpand || row.original.subComponent ? ( 34 | 39 | {row.isExpanded ? expandedIndicator : collapsedIndicator} 40 | 41 | ) : null 42 | 43 | const expanderColumn: Column = { 44 | id: systemColumns.expander.id, 45 | sticky: systemColumns.expander.order < 0 ? 'left' : 'right', 46 | minWidth: 50, 47 | maxWidth: 50, 48 | Header, 49 | Cell, 50 | disableResizing: true 51 | } 52 | 53 | return { 54 | useExpandedColumn: (hooks: Hooks): void => { 55 | hooks.visibleColumns.push((columns) => [expanderColumn, ...columns]) 56 | }, 57 | disableExpander 58 | } 59 | } 60 | export default generateUseExpandedColumn 61 | -------------------------------------------------------------------------------- /lib/src/styles/containerStyle.ts: -------------------------------------------------------------------------------- 1 | import styled from '../utilities/styled' 2 | import { border, radius, color, spacing } from '../utilities/theme' 3 | import StatusBarStyle from './StatusBarStyle' 4 | 5 | const ModalStyles = styled('Modal')` 6 | & { 7 | min-width: 300px; 8 | min-height: 200px; 9 | background-color: ${color.background.secondary}; 10 | border-radius: ${radius.md}; 11 | overflow: hidden; 12 | } 13 | 14 | & .TitleBar { 15 | width: 100%; 16 | min-height: 3rem; 17 | height: auto; 18 | padding-left: ${spacing.xl}; 19 | 20 | display: flex; 21 | align-items: center; 22 | justify-content: space-between; 23 | 24 | font-weight: bold; 25 | border-bottom: ${border.default}; 26 | background-color: ${color.background.primary}; 27 | } 28 | 29 | & .Modal-Content { 30 | padding: ${spacing.xl}; 31 | } 32 | ` 33 | 34 | export default styled.div` 35 | &, 36 | & * { 37 | box-sizing: border-box; 38 | font-size: 16px; 39 | line-height: 1.5; 40 | } 41 | 42 | & { 43 | position: relative; 44 | display: grid; 45 | grid-template-rows: max-content 1fr max-content; 46 | height: 100%; 47 | width: 100%; 48 | color: ${color.text.primary}; 49 | } 50 | 51 | &.withBorder { 52 | border: ${border.default}; 53 | border-radius: ${radius.md}; 54 | overflow: hidden; 55 | } 56 | 57 | &:fullscreen { 58 | border-radius: 0; 59 | } 60 | 61 | & .Modal-Wrapper { 62 | position: absolute; 63 | top: 0; 64 | left: 0; 65 | right: 0; 66 | bottom: 0; 67 | background-color: rgba(0, 0, 0, 0.25); 68 | z-index: 20; 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | } 73 | 74 | ${StatusBarStyle} 75 | ${ModalStyles} 76 | ` 77 | -------------------------------------------------------------------------------- /lib/src/types/LocaleOptions.ts: -------------------------------------------------------------------------------- 1 | // import type { UseTableOptions, TableState } from './ReactTable' 2 | 3 | /** Type interface of locale specific options. 4 | * @category Options */ 5 | export interface LocaleOptions { 6 | /** 7 | * Locale represented by BCP-47 Language tag. Used for number and date formatting. 8 | * @see [list of language tags](https://www.techonthenet.com/js/language_tags.php) 9 | * @see [MDN - Intl locales argument](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locales_argument) 10 | * @default browser-locale 11 | * @example `en-GB`, `en-US`, `fi-FI` 12 | */ 13 | locale?: string | string[] 14 | 15 | /** 16 | * Map of all static text used in the Table UI. 17 | * Provide a local/translated string value to a key 18 | * to override the original text in the table. 19 | */ 20 | text?: Partial 21 | } 22 | 23 | /** 24 | * Map of all static text used in the Table UI. 25 | * Provide a local/translated string value to a key 26 | * to override the original text in the table. 27 | */ 28 | export interface LocaleText extends Record { 29 | // Common 30 | loading: string 31 | of: string 32 | // Status 33 | noRecords: string 34 | total: string 35 | records: string 36 | showing: string 37 | // Pagination 38 | page: string 39 | firstPage: string 40 | previousPage: string 41 | nextPage: string 42 | lastPage: string 43 | // Prefs 44 | tablePreferences: string 45 | save: string 46 | pageSize: string 47 | // Search 48 | filter: string 49 | search: string 50 | closeSearch: string 51 | hideColumnFilters: string 52 | showColumnFilters: string 53 | toggleColumnFilters: string 54 | // Actions 55 | toggleFullscreen: string 56 | } 57 | 58 | export default LocaleOptions 59 | -------------------------------------------------------------------------------- /lib/src/types/ExpandedOptions.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | 3 | import type { UseExpandedOptions, UseExpandedState, IdType } from './ReactTable' 4 | import type { DataType, StateChangeHandler } from './DataType' 5 | 6 | /** Type interface for expanded rows options. 7 | * @category Options */ 8 | export interface ExpandedOptions 9 | extends UseExpandedOptions { 10 | /** Initial settings of expanded rows. */ 11 | initialState?: Partial> 12 | 13 | /** This string is used as the key to detect 14 | * manual expanded state on any given row. 15 | * @default "expanded" 16 | * 17 | * The row with ('expanded': true) will always be expanded. */ 18 | manualExpandedKey?: IdType 19 | 20 | /** Count expanded sub-rows while calculating rows on a page. 21 | * @default true */ 22 | paginateExpandedRows?: boolean 23 | 24 | /** Expanded rows are rendered like normal rows. 25 | * @default true */ 26 | expandSubRows?: boolean 27 | 28 | /** Reset expanded rows when data changes. 29 | * @default true */ 30 | autoResetExpanded?: boolean 31 | 32 | /** Custom method to extract sub-rows from a given row. */ 33 | getSubRows?: (rowData: Data, relativeIndex: number) => Data[] 34 | 35 | /** Callback executed when rows are expanded or collapsed. 36 | * The function must be wrapped in useCallback hook. */ 37 | onStateChange?: StateChangeHandler> 38 | 39 | // ---------- 40 | // Components 41 | 42 | /** Indicator for collapsed row. 43 | * @default → 44 | * @category Custom Component */ 45 | collapsedIndicator?: ReactNode 46 | 47 | /** Indicator for expanded row. 48 | * @default ↓ 49 | * @category Custom Component */ 50 | expandedIndicator?: ReactNode 51 | } 52 | 53 | export default ExpandedOptions 54 | -------------------------------------------------------------------------------- /lib/src/components/TitleBar/userDefinedActions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import type { DataType } from '../../types' 4 | import clsx from '../../utilities/clsx' 5 | import Button from '../../common/Button' 6 | import useTableContext from '../../context' 7 | 8 | export function TableActions(): JSX.Element | null { 9 | const context = useTableContext() 10 | const { actionOptions: { tableActions = [] } = {} } = context.tableProps 11 | 12 | if (tableActions.length === 0) return null 13 | 14 | return ( 15 |
16 | {tableActions.map((action) => ( 17 | 24 | ))} 25 |
26 |
27 | ) 28 | } 29 | 30 | export function MultiRowActions(): JSX.Element | null { 31 | const context = useTableContext() 32 | const { actionOptions: { multiRowActions = [] } = {} } = context.tableProps 33 | const { selectedFlatRows } = context.tableInstance 34 | 35 | if (multiRowActions.length === 0) return null 36 | 37 | const data: Data[] = selectedFlatRows.map((row) => row.original) 38 | const disabled = selectedFlatRows.length === 0 39 | 40 | return ( 41 |
42 | {multiRowActions.map((action) => ( 43 | 51 | ))} 52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-table-ui", 3 | "version": "0.1.6", 4 | "description": "Out-of-the-box UI for React-Table 7.", 5 | "author": { 6 | "name": "Siddhant Gupta", 7 | "email": "me@guptasiddhant.com", 8 | "url": "https://guptsiddhant.com" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://react-table-ui.js.org", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/GuptaSiddhant/react-table-ui/", 15 | "directory": "packages/react-table-ui" 16 | }, 17 | "bugs": "https://github.com/GuptaSiddhant/react-table-ui/issues", 18 | "browser": "./dist/index.js", 19 | "main": "./dist/index.js", 20 | "module": "./dist/index.js", 21 | "types": "./dist/index.d.ts", 22 | "source": "./src/index.ts", 23 | "engines": { 24 | "node": ">=16" 25 | }, 26 | "files": [ 27 | "dist", 28 | "scripts" 29 | ], 30 | "dependencies": { 31 | "@reach/menu-button": "^0.17.0", 32 | "react-table": "^7.8.0", 33 | "react-table-sticky": "^1.1.3", 34 | "@types/react-table": "^7.7.12" 35 | }, 36 | "peerDependencies": { 37 | "react": "^17 || ^18" 38 | }, 39 | "scripts": { 40 | "prebuild": "rm -rf dist", 41 | "build": "node scripts/build.js", 42 | "postbuild": "cp src/react-table-config.d.ts dist/react-table-config.d.ts", 43 | "dev": "node scripts/build watch", 44 | "prepublishOnly": "yarn build", 45 | "postinstall": "node scripts/postInstall.js" 46 | }, 47 | "devDependencies": { 48 | "@jsdevtools/npm-publish": "^1.4.3", 49 | "esbuild": "^0.15.7", 50 | "react": "*", 51 | "react-dom": "*", 52 | "typescript": "^4.8.3" 53 | }, 54 | "keywords": [ 55 | "react", 56 | "table", 57 | "grid", 58 | "ui", 59 | "interface", 60 | "accessible", 61 | "ready-made", 62 | "react-table", 63 | "typescript", 64 | "customise", 65 | "style" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/components/Foot.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import clsx from '../utilities/clsx' 4 | import Cell from '../common/Cell' 5 | import type { DataType, HeaderGroup } from '../types' 6 | import useTableContext from '../context' 7 | 8 | export default function Foot(): JSX.Element | null { 9 | const { 10 | tableInstance, 11 | tableProps: { freezeOptions, columns } 12 | } = useTableContext() 13 | 14 | const showFooter = columns?.some(function hasFooter(column): boolean { 15 | return !!column.Footer || !!column.columns?.some((c) => hasFooter(c)) 16 | }) 17 | 18 | const { headerGroups } = tableInstance 19 | const footerGroups = headerGroups.slice().reverse() 20 | 21 | const freezeFoot = freezeOptions?.footer !== false 22 | 23 | return ( 24 |
28 | {showFooter && 29 | footerGroups.map((group) => { 30 | const rowHasFooter = group.headers.some(({ Footer }) => 31 | typeof Footer === 'function' 32 | ? (Footer as any).name !== 'emptyRenderer' 33 | : !!Footer 34 | ) 35 | return rowHasFooter ? ( 36 |
37 | {group.headers.map((column) => ( 38 | 39 | {renderFooterCellContent(column)} 40 | 41 | ))} 42 |
43 | ) : null 44 | })} 45 |
46 | ) 47 | } 48 | 49 | function renderFooterCellContent( 50 | column: HeaderGroup 51 | ): React.ReactNode { 52 | try { 53 | return column.render('Footer') as React.ReactNode 54 | } catch { 55 | return null 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/components/Table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from '../utilities/clsx' 3 | import type { DataType } from '../types' 4 | import Head from '../components/Head' 5 | import Body from '../components/Body' 6 | import Foot from '../components/Foot' 7 | import useScrollPosition from '../logic/useScrollPosition' 8 | import useTableContext from '../context' 9 | 10 | /** 11 | * Table 12 | * 13 | * @category Component 14 | */ 15 | export default function Table( 16 | props: React.HTMLAttributes 17 | ) { 18 | const { tableInstance, tableProps } = useTableContext() 19 | const { className = '' } = props 20 | const { getTableProps, rows } = tableInstance 21 | const { 22 | loadingOptions = {}, 23 | data = [], 24 | localeOptions: { text } = {} 25 | } = tableProps 26 | 27 | const { 28 | loading = false, 29 | Component: LoadingComponent = , 30 | backgroundLoading = true 31 | } = loadingOptions 32 | 33 | const showLoading = React.useMemo( 34 | () => (backgroundLoading ? (data.length === 0 ? loading : false) : loading), 35 | [data, loading, backgroundLoading] 36 | ) 37 | 38 | const [ref, { scrollPosX, scrollPosY }] = useScrollPosition() 39 | 40 | return ( 41 |
0 && 'scrollX', 49 | scrollPosY > 0 && 'scrollY' 50 | )} 51 | ref={ref} 52 | > 53 | {showLoading ? ( 54 | <>{LoadingComponent} 55 | ) : ( 56 | <> 57 | 58 | 59 | 60 | 61 | )} 62 |
63 | ) 64 | } 65 | 66 | function Loader({ text }: { text?: string }) { 67 | return
{text || 'Loading'}...
68 | } 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-table-ui-monorepo", 3 | "private": true, 4 | "description": "Out-of-the-box UI for React-Table 7", 5 | "author": { 6 | "name": "Siddhant Gupta", 7 | "email": "me@guptasiddhant.com", 8 | "url": "https://guptsiddhant.com" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://react-table-ui.js.org", 12 | "repository": "GuptaSiddhant/react-table-ui", 13 | "bugs": "https://github.com/GuptaSiddhant/react-table-ui/issues", 14 | "workspaces": [ 15 | "lib", 16 | "docs", 17 | "example" 18 | ], 19 | "scripts": { 20 | "build:lib": "yarn workspace react-table-ui build", 21 | "dev:lib": "yarn workspace react-table-ui dev", 22 | "build:docs": "typedoc --emit docs", 23 | "dev:docs": "typedoc --emit docs --watch", 24 | "build:example": "yarn workspace react-table-ui-example build", 25 | "dev:example": "yarn workspace react-table-ui-example dev" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^12.12.38", 29 | "@types/react": "^18.0.3", 30 | "@types/react-dom": "^18.0.3", 31 | "@typescript-eslint/eslint-plugin": "^2.26.0", 32 | "@typescript-eslint/parser": "^2.26.0", 33 | "babel-eslint": "^10.0.3", 34 | "cross-env": "^7.0.2", 35 | "eslint": "^6.8.0", 36 | "eslint-config-prettier": "^6.7.0", 37 | "eslint-config-standard": "^14.1.0", 38 | "eslint-config-standard-react": "^9.2.0", 39 | "eslint-plugin-import": "^2.18.2", 40 | "eslint-plugin-node": "^11.0.0", 41 | "eslint-plugin-prettier": "^3.1.1", 42 | "eslint-plugin-promise": "^4.2.1", 43 | "eslint-plugin-react": "^7.17.0", 44 | "eslint-plugin-standard": "^4.0.1", 45 | "eslint-plugin-tsdoc": "^0.2.11", 46 | "gh-pages": "^2.2.0", 47 | "namor": "^2.0.2", 48 | "npm-run-all": "^4.1.5", 49 | "prettier": "^2.0.4", 50 | "react": "18", 51 | "react-dom": "18", 52 | "typedoc": "^0.23.0", 53 | "typescript": "^4.8.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/logic/useModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ensurePluginOrder } from 'react-table' 3 | import type { ModalProps } from 'react-table' 4 | 5 | import type { 6 | DataType, 7 | Hooks, 8 | ReducerTableState, 9 | TableState, 10 | ActionType, 11 | TableInstance 12 | } from '../types' 13 | 14 | const pluginName = 'useModal' 15 | 16 | const actions = { 17 | init: 'init', 18 | setModal: 'setModal', 19 | resetModal: 'resetModal' 20 | } 21 | 22 | const reducer = ( 23 | state: TableState, 24 | action: ActionType, 25 | _previousState?: TableState, 26 | instance?: TableInstance 27 | ): ReducerTableState => { 28 | switch (action.type) { 29 | case actions.init: 30 | return { 31 | modal: instance?.initialState?.modal, 32 | ...state 33 | } 34 | case actions.resetModal: 35 | return { 36 | ...state, 37 | modal: undefined 38 | } 39 | 40 | case actions.setModal: 41 | return { 42 | ...state, 43 | modal: action['modal'] 44 | } 45 | 46 | default: 47 | return state 48 | } 49 | } 50 | 51 | const useInstance = ( 52 | instance: TableInstance 53 | ): void => { 54 | const { dispatch, plugins } = instance 55 | 56 | ensurePluginOrder(plugins, ['useFilters'], pluginName) 57 | 58 | const setModal = React.useCallback( 59 | (modal: ModalProps) => { 60 | dispatch({ type: actions.setModal, modal }) 61 | }, 62 | [dispatch] 63 | ) 64 | 65 | const resetModal = React.useCallback(() => { 66 | dispatch({ type: actions.resetModal }) 67 | }, [dispatch]) 68 | 69 | Object.assign(instance, { 70 | setModal, 71 | resetModal 72 | }) 73 | } 74 | 75 | function useModal(hooks: Hooks): void { 76 | hooks.stateReducers.push(reducer) 77 | hooks.useInstance.push(useInstance) 78 | } 79 | 80 | useModal.pluginName = pluginName 81 | 82 | export default useModal 83 | -------------------------------------------------------------------------------- /lib/src/logic/generateUseRowActionColumn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button' 3 | 4 | import systemColumns from '../utilities/systemColumns' 5 | 6 | import type { 7 | DataType, 8 | ReactTableUIProps, 9 | SingleRowAction, 10 | Hooks, 11 | CellProps, 12 | Row 13 | } from '../types' 14 | 15 | export default function generateUseRowActionColumn( 16 | props: ReactTableUIProps 17 | ) { 18 | const { singleRowActions = [] } = props.actionOptions || {} 19 | 20 | const Cell = ({ row }: CellProps) => ( 21 | 22 | ) 23 | 24 | return (hooks: Hooks): void => { 25 | if (singleRowActions.length > 0) 26 | hooks.visibleColumns.push((columns) => [ 27 | ...columns, 28 | { 29 | id: systemColumns.action.id, 30 | sticky: systemColumns.action.order < 0 ? 'left' : 'right', 31 | minWidth: 48, 32 | maxWidth: 48, 33 | Header: '', 34 | Cell, 35 | disableResizing: true, 36 | disableFilters: true, 37 | disableGlobalFilter: true, 38 | disableGroupBy: true, 39 | disableSortBy: true 40 | } 41 | ]) 42 | } 43 | } 44 | 45 | function ActionMenu({ 46 | actions, 47 | row 48 | }: { 49 | actions: SingleRowAction[] 50 | row: Row 51 | }) { 52 | return ( 53 | 54 | 55 |
•••
56 |
57 | 58 | {actions.map((action) => ( 59 | action.onClick(row.original, row)} 62 | about={action.tooltip} 63 | > 64 | {action.children} 65 | 66 | ))} 67 | 68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { Routes, Route, NavLink, Link } from 'react-router-dom' 3 | 4 | import router from './routes/_router' 5 | import './index.css' 6 | 7 | const routes = Object.entries(router) 8 | 9 | export default function App() { 10 | return ( 11 | <> 12 |
13 |
14 | Loading...
}> 15 | 16 | } /> 17 | {routes.map(([name, element]) => ( 18 | 19 | ))} 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | function Header() { 28 | return ( 29 |
30 |

31 | React Table UI 32 |

33 | 65 |
66 | ) 67 | } 68 | 69 | function Home() { 70 | return ( 71 |
72 |

Examples

73 |
    74 | {routes.map(([name]) => ( 75 |
  • 76 | {name} 77 |
  • 78 | ))} 79 |
80 |
81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/types/ActionOptions.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | 3 | import type { TableContext, DataType, Row } from '.' 4 | 5 | /** Type interface of actions in table. 6 | * @category Options */ 7 | export interface ActionOptions { 8 | /** Actions available for each row. */ 9 | singleRowActions?: SingleRowAction[] 10 | 11 | /** Actions available when multiple rows are selected. */ 12 | multiRowActions?: MultiRowAction[] 13 | 14 | /** Actions available for the whole table. */ 15 | tableActions?: TableAction[] 16 | 17 | /** Action to take table in fullscreen mode. 18 | * @default true */ 19 | fullscreenAction?: 20 | | boolean 21 | | { 22 | /** Indicator when fullscreen can be requested for table. 23 | * @category Component */ 24 | enterFullscreenIndicator?: ReactNode 25 | /** Indicator when table can exit the fullscreen mode. 26 | * @category Component */ 27 | exitFullscreenIndicator?: ReactNode 28 | } 29 | } 30 | 31 | interface CommonTableAction { 32 | /** Unique ID of each action. */ 33 | id: string 34 | /** Content of the action. Preferably an icon / icon+label / label. */ 35 | children: ReactNode 36 | /** Tooltip of the action */ 37 | tooltip?: string 38 | /** Action is disabled. 39 | * @default false */ 40 | disabled?: boolean 41 | } 42 | export interface SingleRowAction 43 | extends CommonTableAction { 44 | /** Callback executed when action is clicked. */ 45 | onClick: (data: Data, row: Row) => void 46 | } 47 | 48 | export interface MultiRowAction 49 | extends CommonTableAction { 50 | /** Callback executed when action is clicked. */ 51 | onClick: (data: Data[], rows: Row[]) => void 52 | } 53 | 54 | export interface TableAction extends CommonTableAction { 55 | /** Callback executed when action is clicked. */ 56 | onClick: (instance: TableContext) => void 57 | } 58 | 59 | export default ActionOptions 60 | -------------------------------------------------------------------------------- /lib/src/types/StyleOptions.ts: -------------------------------------------------------------------------------- 1 | /** Type interface of Table-style specific options. 2 | * @category Options */ 3 | export interface StyleOptions { 4 | /** All theme related options like colors and spacing */ 5 | theme?: ThemeOptions 6 | 7 | /** The table is rendered without border styling. 8 | * Good for embedding in other containers. 9 | * @default false */ 10 | borderless?: boolean 11 | 12 | /** If enabled, table and its components will have 13 | * rounded corners calculated from theme's spacing. 14 | * @default true */ 15 | roundedCorners?: boolean 16 | 17 | /** Settings for title bar (top bar). 18 | * Set to false, to hide the title bar. 19 | * @default true */ 20 | titleBar?: boolean 21 | 22 | /** Settings for status bar (bottom bar). 23 | * Set to false, to hide the status bar. 24 | * @default true */ 25 | statusBar?: boolean 26 | } 27 | 28 | export default StyleOptions 29 | 30 | /** @internal */ 31 | type DeepPartial = { 32 | [P in keyof T]?: DeepPartial 33 | } 34 | 35 | /** Type interface of Theme specific options. This overrides the internal CSS. 36 | * @category Options */ 37 | export interface ThemeOptions { 38 | /** Colors for the Table */ 39 | colors?: DeepPartial 40 | /** Spacing used throughout the table 41 | * to determine padding, margins and radii. */ 42 | spacing?: Partial 43 | } 44 | 45 | export interface ThemeColors { 46 | text: ThemeColor 47 | background: ThemeColor 48 | border: ThemeColor 49 | accent: { 50 | default: string 51 | lighter: string 52 | darker: string 53 | } 54 | } 55 | 56 | export interface ThemeColor { 57 | primary: string 58 | secondary: string 59 | disabled: string 60 | inverse: string 61 | selected: string 62 | none: string 63 | } 64 | 65 | export interface ThemeSpacing { 66 | /** @default 0 */ 67 | none: T 68 | /** @default 2 */ 69 | xs: T 70 | /** @default 4 */ 71 | sm: T 72 | /** @default 8 */ 73 | md: T 74 | /** @default 12 */ 75 | lg: T 76 | /** @default 16 */ 77 | xl: T 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/logic/generateUseRowSelectColumn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import systemColumns from '../utilities/systemColumns' 4 | import type { 5 | DataType, 6 | ReactTableUIProps, 7 | Hooks, 8 | CellProps, 9 | HeaderProps, 10 | TableToggleCommonProps 11 | } from '../types' 12 | 13 | const IndeterminateCheckbox = React.forwardRef< 14 | HTMLInputElement, 15 | TableToggleCommonProps 16 | >(({ indeterminate = false, ...rest }, ref) => { 17 | const defaultRef = React.useRef(null) 18 | const resolvedRef = ref || defaultRef 19 | 20 | React.useEffect(() => { 21 | if (typeof resolvedRef !== 'function' && resolvedRef?.current) 22 | resolvedRef.current.indeterminate = indeterminate 23 | }, [resolvedRef, indeterminate]) 24 | 25 | return ( 26 |
27 | 28 |
29 | ) 30 | }) 31 | 32 | const generateUseRowSelectColumn = ( 33 | props: ReactTableUIProps 34 | ) => { 35 | const { Component = IndeterminateCheckbox, selectSubRows = false } = 36 | props.rowSelectOptions || {} 37 | 38 | const Header = ({ getToggleAllRowsSelectedProps }: HeaderProps) => ( 39 | 40 | ) 41 | 42 | const Cell = ({ row }: CellProps) => 43 | row.depth > 0 && !selectSubRows ? null : ( 44 | 45 | ) 46 | 47 | return (hooks: Hooks): void => { 48 | hooks.visibleColumns.push((columns) => [ 49 | { 50 | id: systemColumns.selection.id, 51 | sticky: systemColumns.selection.order < 0 ? 'left' : 'right', 52 | minWidth: 48, 53 | maxWidth: 48, 54 | Header, 55 | Cell, 56 | disableResizing: true, 57 | disableFilters: true, 58 | disableGlobalFilter: true, 59 | disableGroupBy: true, 60 | disableSortBy: true 61 | }, 62 | ...columns 63 | ]) 64 | } 65 | } 66 | export default generateUseRowSelectColumn 67 | -------------------------------------------------------------------------------- /lib/src/utilities/systemColumns.ts: -------------------------------------------------------------------------------- 1 | // Column order 2 | // Left sticky // Table columns // Right sticky 3 | // -4 -3 -2 -1 // 0 0 0 0 0 0 0 // 1 2 3 4 5 6 4 | 5 | import { Column, HeaderGroup, IdType } from 'react-table' 6 | import { DataType } from '../types' 7 | 8 | interface SystemColumn { 9 | id: string 10 | order: number 11 | } 12 | 13 | const selectionColumn: SystemColumn = { 14 | id: 'selection', 15 | order: -2 16 | } 17 | const expanderColumn: SystemColumn = { 18 | id: 'expander', 19 | order: -1 20 | } 21 | const actionColumn: SystemColumn = { 22 | id: 'action', 23 | order: 1 24 | } 25 | 26 | const systemColumns = { 27 | selection: selectionColumn, 28 | expander: expanderColumn, 29 | action: actionColumn 30 | } as const 31 | 32 | const allSystemColumnsId = Object.values(systemColumns).map((col) => col.id) 33 | 34 | const leftSystemColumnsId = Object.values(systemColumns) 35 | .filter(({ order }) => order < 0) 36 | .sort((a, b) => a.order - b.order) 37 | .map(({ id }) => id) 38 | 39 | const rightSystemColumnsId = Object.values(systemColumns) 40 | .filter(({ order }) => order > 0) 41 | .sort((a, b) => a.order - b.order) 42 | .map(({ id }) => id) 43 | 44 | export const checkIfSystemColumn = ( 45 | column: HeaderGroup | string 46 | ) => 47 | allSystemColumnsId.includes( 48 | typeof column === 'string' ? column : column.placeholderOf?.id || column.id 49 | ) 50 | 51 | export const fixColumnOrder = ( 52 | columnOrder: IdType[] = [], 53 | allColumns: Column[] 54 | ): IdType[] => { 55 | const middleColumnsId = columnOrder.filter( 56 | (id) => !allSystemColumnsId.includes(id) 57 | ) 58 | const missingColumnId = allColumns 59 | .filter( 60 | ({ id, accessor }) => 61 | !middleColumnsId.some((cId) => cId === id || cId === accessor) 62 | ) 63 | .map(({ id, accessor }) => id || accessor) as string[] 64 | 65 | return [ 66 | ...leftSystemColumnsId, 67 | ...middleColumnsId, 68 | ...missingColumnId, 69 | ...rightSystemColumnsId 70 | ] 71 | } 72 | 73 | export default systemColumns 74 | -------------------------------------------------------------------------------- /lib/src/logic/useFullscreenAction.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import type { DataType, TableContext } from '../types' 4 | import Icon from '../common/Icon' 5 | 6 | const defaultEnterFullscreenIndicator = 7 | const defaultExitFullscreenIndicator = 8 | 9 | export default function useFullscreenAction({ 10 | tableRef, 11 | tableProps 12 | }: TableContext): { 13 | title: string 14 | onClick: () => void 15 | children: React.ReactNode 16 | } | null { 17 | const { 18 | actionOptions: { fullscreenAction = true } = {}, 19 | localeOptions: { text } = {} 20 | } = tableProps 21 | const [isFullscreen, setIsFullscreen] = React.useState(false) 22 | 23 | const handleEnterFullscreen = React.useCallback(() => { 24 | setIsFullscreen(true) 25 | if (tableRef?.current) tableRef.current.requestFullscreen() 26 | }, [tableRef]) 27 | 28 | const handleExitFullscreen = React.useCallback(() => { 29 | if (window.document.fullscreenElement) window.document.exitFullscreen() 30 | }, []) 31 | 32 | React.useEffect(() => { 33 | const callback = () => { 34 | if (!window.document.fullscreenElement) setIsFullscreen(false) 35 | } 36 | 37 | window.document.addEventListener('fullscreenchange', callback) 38 | return () => { 39 | window.document.removeEventListener('fullscreenchange', callback) 40 | } 41 | }) 42 | 43 | const enterIndicator: React.ReactNode = 44 | (typeof fullscreenAction !== 'boolean' && 45 | fullscreenAction.enterFullscreenIndicator) || 46 | defaultEnterFullscreenIndicator 47 | 48 | const exitIndicator: React.ReactNode = 49 | (typeof fullscreenAction !== 'boolean' && 50 | fullscreenAction.exitFullscreenIndicator) || 51 | defaultExitFullscreenIndicator 52 | 53 | const iconButtonProps = fullscreenAction 54 | ? { 55 | title: text?.toggleFullscreen || 'Toggle fullscreen', 56 | onClick: isFullscreen ? handleExitFullscreen : handleEnterFullscreen, 57 | children: isFullscreen ? exitIndicator : enterIndicator 58 | } 59 | : null 60 | 61 | return iconButtonProps 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/utilities/generateStatus.ts: -------------------------------------------------------------------------------- 1 | import type { DataType, TableContext } from '../types' 2 | 3 | const generateStatus = ({ 4 | tableProps, 5 | tableInstance 6 | }: TableContext) => { 7 | const { loading = false, showLoadingStatus = true } = 8 | tableProps.loadingOptions || {} 9 | const { disablePagination = false, manualPagination = false, recordCount } = 10 | tableProps.paginationOptions || {} 11 | const { 12 | page, 13 | rows, 14 | pageCount, 15 | selectedFlatRows, 16 | state: { pageSize, pageIndex } 17 | } = tableInstance 18 | const locale = tableProps.localeOptions?.locale 19 | const { 20 | loading: loadingText = 'Loading', 21 | noRecords: noRecordsText = 'No records', 22 | total: totalText = 'Total', 23 | records: recordsText = 'records', 24 | showing: showingText = 'Showing', 25 | of: ofText = 'of' 26 | } = tableProps.localeOptions?.text || {} 27 | 28 | const selectedRowCount = Object.keys(selectedFlatRows).length 29 | 30 | if (showLoadingStatus && loading) return loadingText + '...' 31 | 32 | const statuses: string[] = [] 33 | 34 | // row count 35 | let rowCountStatus = '' 36 | if (rows.length === 0) { 37 | rowCountStatus = noRecordsText 38 | } else if (disablePagination) { 39 | rowCountStatus = `${totalText} ${rows.length.toLocaleString( 40 | locale 41 | )} ${recordsText}` 42 | } else { 43 | const totalResults = manualPagination 44 | ? recordCount?.toLocaleString(locale) || 45 | `~${(pageCount * pageSize).toLocaleString(locale)}` 46 | : rows.length.toLocaleString(locale) 47 | const startRow = (pageIndex * pageSize + 1).toLocaleString(locale) 48 | const endRow = (manualPagination 49 | ? (pageIndex + 1) * pageSize 50 | : pageIndex * pageSize + page.length 51 | ).toLocaleString(locale) 52 | 53 | rowCountStatus = `${showingText} ${startRow}-${endRow} ${ofText} ${totalResults} ${recordsText}` 54 | } 55 | statuses.push(rowCountStatus) 56 | 57 | if (selectedRowCount > 0) { 58 | statuses.push(`${selectedRowCount} selected`) 59 | } 60 | 61 | return statuses.join(' • ') 62 | } 63 | 64 | export default generateStatus 65 | -------------------------------------------------------------------------------- /lib/src/types/ColumnOptions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IdType, 3 | UseColumnOrderState, 4 | UseResizeColumnsOptions, 5 | UseResizeColumnsState 6 | } from './ReactTable' 7 | import type { DataType, StateChangeHandler } from './DataType' 8 | 9 | /** Type interface for Column options. 10 | * @category Options */ 11 | export interface ColumnOptions 12 | extends UseResizeColumnsOptions { 13 | /** Initial settings of row-select. */ 14 | initialState?: Partial> 15 | 16 | /** Disable columns ordering. 17 | * @default false */ 18 | disableOrdering?: boolean 19 | 20 | /** Disable columns resizing. 21 | * @default false */ 22 | disableResizing?: boolean 23 | 24 | /** Reset hidden columns when columns is changed. 25 | * @default true */ 26 | autoResetHiddenColumns?: boolean 27 | 28 | /** Callback executed when column's state is changed . 29 | * The function must be wrapped in useCallback hook. */ 30 | onStateChange?: StateChangeHandler> 31 | 32 | /** Callback executed when column order is changed . 33 | * The function must be wrapped in useCallback hook. */ 34 | onOrderStateChange?: StateChangeHandler['columnOrder']> 35 | 36 | /** Callback executed when column size is changed . 37 | * The function must be wrapped in useCallback hook. */ 38 | onResizeStateChange?: StateChangeHandler['columnResizing']> 39 | 40 | /** Callback executed when column's visibility is changed . 41 | * The function must be wrapped in useCallback hook. */ 42 | onVisibilityStateChange?: StateChangeHandler< 43 | ColumnState['hiddenColumns'] 44 | > 45 | 46 | // ---------- 47 | // Components 48 | } 49 | 50 | /** Type interface for Column options state 51 | * { 52 | * columnResizing: { 53 | startX?: number; 54 | columnWidth: number; 55 | headerIdWidths: Record; 56 | columnWidths: any; 57 | isResizingColumn?: string; 58 | }, 59 | columnOrder: Array>, 60 | hiddenColumns: Array>, 61 | * } 62 | */ 63 | export interface ColumnState 64 | extends UseColumnOrderState, 65 | UseResizeColumnsState { 66 | /** Hide columns by providing column's ID in an array. */ 67 | hiddenColumns: IdType[] 68 | } 69 | 70 | export default ColumnOptions 71 | -------------------------------------------------------------------------------- /lib/src/types/GlobalFilterOptions.ts: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import type { 3 | UseGlobalFiltersOptions, 4 | UseGlobalFiltersState, 5 | FilterType, 6 | Row, 7 | IdType, 8 | UseGlobalFiltersInstanceProps 9 | } from './ReactTable' 10 | import type { DataType, StateChangeHandler } from './DataType' 11 | 12 | /** Type interface for filtering options. 13 | * @category Options */ 14 | export interface GlobalFilterOptions 15 | extends UseGlobalFiltersOptions { 16 | /** Initial settings of global filter. 17 | * @example 18 | * ``` 19 | * { globalFilter: {} } 20 | * ``` */ 21 | initialState?: UseGlobalFiltersState 22 | 23 | /** Callback executed when columns are sorted. 24 | * The function must be wrapped in useCallback hook. */ 25 | onStateChange?: StateChangeHandler> 26 | 27 | /** Disable global filtering for table. 28 | * @default false */ 29 | disableGlobalFilter?: boolean 30 | 31 | /** Should the Global Filter be visible by default. 32 | * @default false */ 33 | defaultVisibleGlobalFilter?: boolean 34 | 35 | /** Reset filtering when data is changed. 36 | * @default true */ 37 | autoResetGlobalFilter?: boolean 38 | 39 | /** Manual filtering with custom logic, eg. server-side. 40 | * Enables filter detection functionality, 41 | * but does not automatically perform row filtering 42 | * @default false */ 43 | manualGlobalFilter?: boolean 44 | 45 | /** Allows overriding or adding additional filter types. 46 | * If "globalFilter" type isn't found on this object, 47 | * it will default to using the built-in filter types. Must be memoised. */ 48 | filterTypes?: Record> 49 | 50 | /** @default "text" */ 51 | globalFilter?: 52 | | string 53 | | (( 54 | rows: Row[], 55 | columnIds: IdType[], 56 | globalFilterValue: string 57 | ) => Row[]) 58 | 59 | // ---------- 60 | // Components 61 | 62 | /** Global filter component rendered for in table. 63 | * This overrides the fallback text-input-field. 64 | * @category Custom Component */ 65 | Component?: GlobalFilterComponent 66 | } 67 | 68 | export type GlobalFilterComponent = FC< 69 | UseGlobalFiltersInstanceProps & { globalFilterValue: string } 70 | > 71 | 72 | export default GlobalFilterOptions 73 | -------------------------------------------------------------------------------- /lib/src/logic/useTheme.ts: -------------------------------------------------------------------------------- 1 | import type { ReactTableUIProps, ThemeOptions, DataType } from '../types' 2 | import useStylesheet from '../utilities/useStylesheet' 3 | import { pxToEm } from '../utilities/theme' 4 | import deepMerge from '../utilities/deepMerge' 5 | import { commonClassName } from '../utilities/clsx' 6 | 7 | const accent = { 8 | default: '#096ED1', 9 | lighter: '#F2F9FF', 10 | darker: '' 11 | } 12 | 13 | const defaultThemeOptions: ThemeOptions = { 14 | colors: { 15 | accent, 16 | text: { 17 | primary: '#1a1a1a', 18 | secondary: '#404040', 19 | disabled: '#808080', 20 | inverse: '#ffffff', 21 | selected: accent.default, 22 | none: 'transparent' 23 | }, 24 | background: { 25 | primary: '#ffffff', 26 | secondary: '#f7f7f7', 27 | disabled: '#eeeeee', 28 | selected: accent.lighter, 29 | inverse: '#000000', 30 | none: 'transparent' 31 | }, 32 | border: { 33 | primary: '#bfbfbf', 34 | selected: accent.default, 35 | inverse: '#1a1a1a', 36 | none: 'transparent' 37 | } 38 | }, 39 | spacing: { 40 | none: 0, 41 | xs: 2, 42 | sm: 4, 43 | md: 8, 44 | lg: 12, 45 | xl: 16 46 | } 47 | } 48 | 49 | const useTheme = ({ 50 | styleOptions: { roundedCorners = true, theme: themeOptions = {} } = {} 51 | }: ReactTableUIProps) => { 52 | const mergedThemeOptions = deepMerge(defaultThemeOptions, themeOptions) 53 | 54 | // Colors 55 | let colorsCSS = '' 56 | Object.entries(mergedThemeOptions.colors || {}).forEach( 57 | ([category, colors]) => { 58 | Object.entries(colors || {}).forEach(([name, color]) => { 59 | if (color) { 60 | colorsCSS += `--${commonClassName}-color-${category}-${name}: ${color};\n` 61 | } 62 | }) 63 | } 64 | ) 65 | 66 | // Spacing 67 | let spacingCSS = '' 68 | const disableRadius = !roundedCorners 69 | Object.entries(mergedThemeOptions.spacing || {}).forEach(([type, val]) => { 70 | if (val) { 71 | spacingCSS += `--${commonClassName}-spacing-${type}: ${pxToEm(val)};\n` 72 | spacingCSS += `--${commonClassName}-radius-${type}: ${ 73 | disableRadius ? 0 : pxToEm(val) 74 | };\n` 75 | } 76 | }) 77 | 78 | const themeCSS = `:root { 79 | ${colorsCSS} 80 | ${spacingCSS} 81 | }` 82 | 83 | useStylesheet(themeCSS) 84 | } 85 | 86 | export default useTheme 87 | -------------------------------------------------------------------------------- /lib/scripts/publish.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { npmPublish } = require('@jsdevtools/npm-publish') 4 | const { spawnSync } = require('child_process') 5 | const { readFileSync, writeFileSync } = require('fs') 6 | const manifestPath = './package.json' 7 | 8 | publishToNpm().then(handleSuccess).catch(handleError) 9 | 10 | async function publishToNpm() { 11 | console.log('Publishing package to NPM...') 12 | 13 | const manifest = getPackageJson() 14 | const { name, version: currentVersion } = manifest 15 | const publishedVersion = getPublishedVersion(name) 16 | 17 | console.log('Current published version:', currentVersion) 18 | 19 | if (publishedVersion === currentVersion) { 20 | return await publishCanary(manifest) 21 | } 22 | 23 | return await publishLatest(currentVersion) 24 | } 25 | 26 | async function publishLatest(version) { 27 | const result = await npmPublish({ checkVersion: true, access: 'public' }) 28 | gitTag(version) 29 | 30 | return result 31 | } 32 | 33 | async function publishCanary(manifest, tag = 'canary') { 34 | const canaryVersion = `${manifest.version}-${tag}.${Date.now().valueOf()}` 35 | const canaryManifest = { ...manifest, version: canaryVersion } 36 | writeFileSync(manifestPath, JSON.stringify(canaryManifest, null, 2)) 37 | 38 | console.log('Publishing', tag, 'version:', canaryVersion) 39 | return npmPublish({ tag, access: 'public' }) 40 | } 41 | 42 | /** @param {import("@jsdevtools/npm-publish").Results} results */ 43 | function handleSuccess({ package: name, tag, version }) { 44 | console.log() 45 | console.log(`Usage: npm install ${name}@${tag}`) 46 | console.log(`Link : https://www.npmjs.com/package/${name}/v/${version}`) 47 | } 48 | 49 | function handleError(error) { 50 | console.error(error.message) 51 | process.exit(1) 52 | } 53 | 54 | function getPackageJson() { 55 | const packageJson = readFileSync(manifestPath, 'utf8') 56 | return JSON.parse(packageJson) 57 | } 58 | 59 | function getPublishedVersion(name) { 60 | const result = spawnSync('npm', ['view', name, 'version', '--silent'], { 61 | encoding: 'utf-8' 62 | }) 63 | return result.stdout.trim().split('\\')[0] 64 | } 65 | 66 | /** @param {string} version */ 67 | function gitTag(version) { 68 | spawnSync('git', ['config', '--local', 'user.email', `"action@github.com"`]) 69 | spawnSync('git', ['config', '--local', 'user.name', `"GitHub Action"`]) 70 | 71 | spawnSync('git', ['tag', '-a', `v${version}`, '-m', `Release v${version}`]) 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/components/Body.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from '../utilities/clsx' 3 | import Cell from '../common/Cell' 4 | import { checkIfSystemColumn } from '../utilities/systemColumns' 5 | import type { DataType, Row } from '../types' 6 | import useTableContext from '../context' 7 | 8 | export default function Body(): JSX.Element { 9 | const context = useTableContext() 10 | const { 11 | rows, 12 | page, 13 | getTableBodyProps, 14 | prepareRow, 15 | totalColumnsWidth 16 | } = context.tableInstance 17 | 18 | return ( 19 |
20 | {(page || rows).map((row) => { 21 | prepareRow(row) 22 | 23 | return ( 24 | 25 | 26 | {row.isExpanded && ( 27 | 28 | )} 29 | 30 | ) 31 | })} 32 |
33 | ) 34 | } 35 | 36 | function SubComponent({ 37 | row, 38 | totalColumnsWidth 39 | }: { 40 | row: Row 41 | totalColumnsWidth: number 42 | }): JSX.Element | null { 43 | const subComponent: React.ReactNode = row.original.subComponent 44 | const rowProps = row.getRowProps() 45 | 46 | return subComponent ? ( 47 |
52 |
{subComponent}
53 |
54 | ) : null 55 | } 56 | 57 | function BodyRow({ 58 | row 59 | }: { 60 | row: Row 61 | }): JSX.Element { 62 | const rowProps = row.getRowProps() 63 | const isRowSelected = row.isSelected 64 | 65 | return ( 66 |
71 | {row.cells.map((cell) => { 72 | const isSystemColumn = checkIfSystemColumn(cell.column.id) 73 | const cellContent = cell.render('Cell') as React.ReactNode 74 | 75 | return ( 76 | 81 | {cellContent} 82 | 83 | ) 84 | })} 85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/components/TitleBar/PreferencesAction.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button' 3 | 4 | import type { DataType } from '../../types' 5 | import useFullscreenAction from '../../logic/useFullscreenAction' 6 | import Icon from '../../common/Icon' 7 | import useTableContext from '../../context' 8 | 9 | const defaultPreferencesIndicator = 10 | 11 | export default function PreferencesAction< 12 | Data extends DataType 13 | >(): JSX.Element | null { 14 | const context = useTableContext() 15 | const { setModal } = context.tableInstance 16 | const text = context.tableProps.localeOptions?.text 17 | const fullScreenAction = useFullscreenAction(context) 18 | 19 | const handleOpenPreferences = () => 20 | setModal({ 21 | title: text?.tablePreferences || 'Table preferences', 22 | children: ( 23 |
24 | 25 |
26 | ) 27 | }) 28 | 29 | return ( 30 | 31 | 32 |
•••
33 |
34 | 35 | {fullScreenAction && ( 36 | fullScreenAction.onClick!()}> 37 | {fullScreenAction.children} 38 | {fullScreenAction.title} 39 | 40 | )} 41 | 42 | {defaultPreferencesIndicator} 43 | {text?.tablePreferences || 'Table preferences'} 44 | 45 | 46 |
47 | ) 48 | } 49 | 50 | function PageSizePreference(): JSX.Element | null { 51 | const { tableInstance, tableProps } = useTableContext() 52 | const { 53 | state: { pageSize }, 54 | setPageSize 55 | } = tableInstance 56 | const { locale, text } = tableProps.localeOptions || {} 57 | const { pageSizes = [10, 20, 50, 75, 100, 150] } = 58 | tableProps.paginationOptions || {} 59 | 60 | return ( 61 |
62 | {text?.pageSize || 'Page size'} 63 | 74 |
75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/styles/TitleBarStyle.ts: -------------------------------------------------------------------------------- 1 | import styled from '../utilities/styled' 2 | import { 3 | border, 4 | color, 5 | mediaQueries, 6 | pxToEm, 7 | spacing 8 | } from '../utilities/theme' 9 | 10 | /* TitleBar */ 11 | export default styled('TitleBar')` 12 | & { 13 | width: 100%; 14 | height: 3.5em; 15 | 16 | display: grid; 17 | grid-template-columns: 1fr max-content; 18 | grid-template-rows: 1fr max-content; 19 | grid-template-areas: 'titleSearch rowActions actions systemActions'; 20 | align-items: center; 21 | 22 | border-bottom: ${border.default}; 23 | background-color: ${color.background.primary}; 24 | } 25 | 26 | ${mediaQueries.mobile} { 27 | & { 28 | height: auto; 29 | grid-template-areas: 'titleSearch systemActions' 'rowActions actions'; 30 | } 31 | } 32 | 33 | & .titleSearch { 34 | grid-area: titleSearch; 35 | position: relative; 36 | display: flex; 37 | justify-content: space-between; 38 | align-items: center; 39 | padding-inline-start: 0.5em; 40 | gap: 0.5em; 41 | } 42 | 43 | & .separator { 44 | position: absolute; 45 | right: 0; 46 | width: 1px; 47 | background-color: ${color.border.primary}; 48 | top: 50%; 49 | height: ${pxToEm(24)}; 50 | transform: translateY(-50%); 51 | } 52 | 53 | & .systemActions { 54 | grid-area: systemActions; 55 | display: flex; 56 | justify-content: flex-end; 57 | align-items: center; 58 | } 59 | 60 | & .TableActions { 61 | position: relative; 62 | grid-area: actions; 63 | display: flex; 64 | justify-content: flex-end; 65 | align-items: center; 66 | } 67 | 68 | & .MultiRowActions { 69 | position: relative; 70 | grid-area: rowActions; 71 | display: flex; 72 | justify-content: flex-end; 73 | align-items: center; 74 | } 75 | 76 | ${mediaQueries.mobile} { 77 | & .TableActions { 78 | position: initial; 79 | } 80 | 81 | & .MultiRowActions { 82 | position: initial; 83 | justify-content: flex-start; 84 | } 85 | } 86 | 87 | & .title { 88 | font-weight: 700; 89 | max-height: 3.5em; 90 | width: 100%; 91 | padding-inline-start: 0.5em; 92 | text-align: left; 93 | text-align: start; 94 | } 95 | 96 | & .Search { 97 | width: 100%; 98 | } 99 | 100 | & .titleSearch input { 101 | min-height: ${pxToEm(32)}; 102 | padding: ${spacing.sm} ${spacing.md}; 103 | } 104 | 105 | & .titleSearch input::placeholder { 106 | font-weight: bold; 107 | } 108 | ` 109 | -------------------------------------------------------------------------------- /lib/src/types/FiltersOptions.ts: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react' 2 | import type { 3 | UseFiltersOptions, 4 | UseFiltersState, 5 | FilterType, 6 | HeaderProps 7 | } from './ReactTable' 8 | import type { DataType, StateChangeHandler } from './DataType' 9 | 10 | /** Type interface for filtering options. 11 | * @category Options */ 12 | export interface FiltersOptions 13 | extends UseFiltersOptions { 14 | /** Initial settings of filters. 15 | * List of objects containing column id and value. 16 | * @example 17 | * ``` 18 | * { filters: [{ id: 'columnsId', value: 'filterValue' }], visible: true } 19 | * ``` */ 20 | initialState?: FiltersState 21 | 22 | /** Callback executed when columns are sorted. 23 | * The function must be wrapped in useCallback hook. */ 24 | onStateChange?: StateChangeHandler> 25 | 26 | /** Disable column filtering for table. 27 | * @default false */ 28 | disableFilters?: boolean 29 | 30 | /** Enable filtering for all columns, 31 | * regardless if they have a valid accessor. 32 | * @default false */ 33 | defaultCanFilter?: boolean 34 | 35 | /** Reset filtering when data is changed. 36 | * @default true */ 37 | autoResetFilters?: boolean 38 | 39 | /** Filters are always visible. Removes the button to show/hide filters. 40 | * @default false */ 41 | alwaysShowFilters?: boolean 42 | 43 | /** Manual filtering with custom logic, eg. server-side. 44 | * @default false */ 45 | manualFilters?: boolean 46 | 47 | /** Allows overriding or adding additional filter types for columns to use. 48 | * If a column's filter type isn't found on this object, 49 | * it will default to using the built-in filter types. Must be memoised. */ 50 | filterTypes?: Record> 51 | 52 | // ---------- 53 | // Components 54 | 55 | /** Default filter component rendered for columns. 56 | * This overrides the fallback text-input-field. 57 | * Column specific components will override this component. 58 | * @category Custom Component */ 59 | DefaultComponent?: FilterComponent 60 | 61 | /** Indicator/icon used for action/button to show the Filters Row. 62 | * @category Custom Component */ 63 | showFiltersActionIndicator?: ReactNode 64 | 65 | /** Indicator/icon used for action/button to hide the Filters Row. 66 | * @category Custom Component */ 67 | hideFiltersActionIndicator?: ReactNode 68 | } 69 | 70 | export type FilterComponent = FC> 71 | 72 | /** @category State */ 73 | export interface FiltersState 74 | extends Partial> { 75 | /** Set visibility of filters in the table. 76 | * @default false */ 77 | filtersVisible?: boolean 78 | } 79 | 80 | export default FiltersOptions 81 | -------------------------------------------------------------------------------- /lib/src/logic/useVisibleFilters.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ensurePluginOrder } from 'react-table' 3 | 4 | import type { 5 | DataType, 6 | Hooks, 7 | ReducerTableState, 8 | TableState, 9 | ActionType, 10 | TableInstance 11 | } from '../types' 12 | 13 | const pluginName = 'useFiltersVisible' 14 | 15 | const actions = { 16 | init: 'init', 17 | setFiltersVisible: 'setFiltersVisible', 18 | resetFiltersVisible: 'resetFiltersVisible', 19 | toggleFiltersVisible: 'toggleFiltersVisible' 20 | } 21 | 22 | const reducer = ( 23 | state: TableState, 24 | action: ActionType, 25 | previousState?: TableState, 26 | instance?: TableInstance 27 | ): ReducerTableState => { 28 | if (instance?.disableFilters) 29 | return { 30 | ...state, 31 | filtersVisible: false 32 | } 33 | 34 | if (instance?.alwaysShowFilters) 35 | return { 36 | ...state, 37 | filtersVisible: true 38 | } 39 | 40 | switch (action.type) { 41 | case actions.init: 42 | case actions.resetFiltersVisible: 43 | return { 44 | filtersVisible: instance?.initialState?.filtersVisible || false, 45 | ...state 46 | } 47 | 48 | case actions.setFiltersVisible: 49 | return { 50 | ...state, 51 | filtersVisible: !!action['filtersVisible'] 52 | } 53 | 54 | case actions.toggleFiltersVisible: 55 | return { 56 | ...state, 57 | filtersVisible: !previousState?.filtersVisible 58 | } 59 | 60 | default: 61 | return state 62 | } 63 | } 64 | 65 | const useInstance = ( 66 | instance: TableInstance 67 | ): void => { 68 | const { dispatch, plugins } = instance 69 | 70 | ensurePluginOrder(plugins, ['useFilters'], pluginName) 71 | 72 | const setFiltersVisible = React.useCallback( 73 | (filtersVisible: boolean) => { 74 | dispatch({ type: actions.setFiltersVisible, filtersVisible }) 75 | dispatch({ type: 'resetFilters' }) 76 | }, 77 | [dispatch] 78 | ) 79 | 80 | const resetFiltersVisible = React.useCallback(() => { 81 | dispatch({ type: actions.resetFiltersVisible }) 82 | dispatch({ type: 'resetFilters' }) 83 | }, [dispatch]) 84 | 85 | const toggleFiltersVisible = React.useCallback(() => { 86 | dispatch({ type: actions.toggleFiltersVisible }) 87 | dispatch({ type: 'resetFilters' }) 88 | }, [dispatch]) 89 | 90 | Object.assign(instance, { 91 | setFiltersVisible, 92 | resetFiltersVisible, 93 | toggleFiltersVisible 94 | }) 95 | } 96 | 97 | function useFiltersVisible(hooks: Hooks): void { 98 | hooks.stateReducers.push(reducer) 99 | hooks.useInstance.push(useInstance) 100 | } 101 | 102 | useFiltersVisible.pluginName = pluginName 103 | 104 | export default useFiltersVisible 105 | -------------------------------------------------------------------------------- /lib/src/types/ReactTable.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | Accessor, 3 | AggregatedValue, 4 | AggregatorFn, 5 | ActionType, 6 | Aggregator, 7 | Cell, 8 | CellPropGetter, 9 | CellProps, 10 | CellValue, 11 | Column, 12 | ColumnGroup, 13 | ColumnGroupInterface, 14 | ColumnInstance, 15 | ColumnInterface, 16 | ColumnInterfaceBasedOnValue, 17 | ColumnWithLooseAccessor, 18 | ColumnWithStrictAccessor, 19 | DefaultAggregators, 20 | DefaultFilterTypes, 21 | DefaultSortTypes, 22 | FilterProps, 23 | FilterType, 24 | FilterTypes, 25 | FilterValue, 26 | Filters, 27 | FooterGroupPropGetter, 28 | FooterPropGetter, 29 | HeaderGroup, 30 | HeaderGroupPropGetter, 31 | HeaderPropGetter, 32 | HeaderProps, 33 | Hooks, 34 | IdType, 35 | Meta, 36 | // ModalProps, 37 | MetaBase, 38 | PluginHook, 39 | PropGetter, 40 | ReducerTableState, 41 | Renderer, 42 | Row, 43 | RowPropGetter, 44 | SortByFn, 45 | SortingRule, 46 | StringKey, 47 | TableInstance, 48 | TablePropGetter, 49 | TableProps, 50 | TableResizerProps, 51 | TableRowProps, 52 | TableSortByToggleProps, 53 | TableState, 54 | TableToggleAllRowsSelectedProps, 55 | TableToggleCommonProps, 56 | TableToggleHideAllColumnProps, 57 | TableToggleRowsSelectedProps, 58 | UseColumnOrderInstanceProps, 59 | UseColumnOrderState, 60 | UseExpandedHooks, 61 | UseExpandedInstanceProps, 62 | UseExpandedOptions, 63 | UseExpandedRowProps, 64 | UseExpandedState, 65 | UseFiltersColumnOptions, 66 | UseFiltersColumnProps, 67 | UseFiltersInstanceProps, 68 | UseFiltersOptions, 69 | UseFiltersState, 70 | // UseFiltersVisibleProps, 71 | // UseFiltersVisibleState, 72 | UseGlobalFiltersColumnOptions, 73 | UseGlobalFiltersInstanceProps, 74 | UseGlobalFiltersOptions, 75 | UseGlobalFiltersState, 76 | UseGroupByCellProps, 77 | UseGroupByColumnOptions, 78 | UseGroupByColumnProps, 79 | UseGroupByHooks, 80 | UseGroupByInstanceProps, 81 | UseGroupByOptions, 82 | UseGroupByRowProps, 83 | UseGroupByState, 84 | // UseModalProps, 85 | // UseModalState, 86 | UsePaginationInstanceProps, 87 | UsePaginationOptions, 88 | UsePaginationState, 89 | UseResizeColumnsColumnOptions, 90 | UseResizeColumnsColumnProps, 91 | UseResizeColumnsOptions, 92 | UseResizeColumnsState, 93 | UseRowSelectHooks, 94 | UseRowSelectInstanceProps, 95 | UseRowSelectOptions, 96 | UseRowSelectRowProps, 97 | UseRowSelectState, 98 | UseRowStateCellProps, 99 | UseRowStateInstanceProps, 100 | UseRowStateLocalState, 101 | UseRowStateOptions, 102 | UseRowStateRowProps, 103 | UseRowStateState, 104 | UseRowUpdater, 105 | UseSortByColumnOptions, 106 | UseSortByColumnProps, 107 | UseSortByHooks, 108 | UseSortByInstanceProps, 109 | UseSortByOptions, 110 | UseSortByState, 111 | UseTableCellProps, 112 | UseTableColumnOptions, 113 | UseTableColumnProps, 114 | UseTableHeaderGroupProps, 115 | UseTableHooks, 116 | UseTableInstanceProps, 117 | UseTableOptions, 118 | UseTableRowProps 119 | } from 'react-table' 120 | -------------------------------------------------------------------------------- /lib/src/common/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Icon({ name }: { name: string }) { 4 | return ( 5 | 17 | {getIconContent(name)} 18 | 19 | ) 20 | } 21 | 22 | function getIconContent(name: string): JSX.Element | null { 23 | switch (name) { 24 | case 'maximize': 25 | case 'maximise': 26 | return ( 27 | 28 | ) 29 | case 'minimize': 30 | case 'minimise': 31 | return ( 32 | 33 | ) 34 | case 'filter': 35 | return 36 | case 'search': 37 | return ( 38 | 39 | 40 | 41 | 42 | ) 43 | case 'x': 44 | case 'close': 45 | case 'cancel': 46 | return ( 47 | 48 | 49 | 50 | 51 | ) 52 | case 'chevron-right': 53 | return 54 | case 'chevron-left': 55 | return 56 | case 'chevrons-right': 57 | return ( 58 | 59 | 60 | 61 | 62 | ) 63 | case 'chevrons-left': 64 | return ( 65 | 66 | 67 | 68 | 69 | ) 70 | case 'sliders': 71 | return ( 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | ) 84 | case 'save': 85 | return ( 86 | 87 | 88 | 89 | 90 | 91 | ) 92 | 93 | default: 94 | return null 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/logic/useHandleStateChange.ts: -------------------------------------------------------------------------------- 1 | // eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] 2 | 3 | import { useEffect } from 'react' 4 | import type { 5 | TableContext, 6 | DataType, 7 | UseRowStateState, 8 | UseGroupByState 9 | } from '../types' 10 | 11 | const useHandleStateChange = ({ 12 | tableInstance: { state }, 13 | tableProps: { 14 | tableOptions, 15 | sortByOptions, 16 | filtersOptions, 17 | expandedOptions, 18 | rowSelectOptions, 19 | paginationOptions, 20 | columnOptions, 21 | globalFilterOptions 22 | } 23 | }: TableContext) => { 24 | const { 25 | expanded, 26 | selectedRowIds, 27 | sortBy, 28 | filters, 29 | pageIndex, 30 | pageSize, 31 | rowState, 32 | columnOrder, 33 | globalFilter, 34 | groupBy, 35 | columnResizing, 36 | hiddenColumns = [], 37 | filtersVisible 38 | } = state 39 | 40 | /** @todo Remove */ 41 | const rowStateOptions = { 42 | onStateChange: (_: UseRowStateState) => {} 43 | } 44 | /** @todo Remove */ 45 | const groupingOptions = { 46 | onStateChange: (_: UseGroupByState) => {} 47 | } 48 | 49 | // Table 50 | useEffect(() => { 51 | tableOptions?.onStateChange?.(state) 52 | }, [tableOptions?.onStateChange, state]) 53 | 54 | // Sorting 55 | useEffect(() => { 56 | if (sortByOptions?.onStateChange) sortByOptions?.onStateChange({ sortBy }) 57 | }, [sortByOptions?.onStateChange, sortBy]) 58 | 59 | // Filtering 60 | useEffect(() => { 61 | filtersOptions?.onStateChange?.({ filters, filtersVisible }) 62 | }, [filtersOptions?.onStateChange, filters, filtersVisible]) 63 | 64 | // Global filtering 65 | useEffect(() => { 66 | globalFilterOptions?.onStateChange?.({ globalFilter }) 67 | }, [globalFilterOptions?.onStateChange, globalFilter]) 68 | 69 | // Expanded row 70 | useEffect(() => { 71 | expandedOptions?.onStateChange?.({ expanded }) 72 | }, [expandedOptions?.onStateChange, expanded]) 73 | 74 | // Row-select 75 | useEffect(() => { 76 | rowSelectOptions?.onStateChange?.({ selectedRowIds }) 77 | }, [rowSelectOptions?.onStateChange, selectedRowIds]) 78 | 79 | // Row-state 80 | useEffect(() => { 81 | rowStateOptions?.onStateChange?.({ rowState }) 82 | }, [rowStateOptions?.onStateChange, rowState]) 83 | 84 | // Group by 85 | useEffect(() => { 86 | groupingOptions?.onStateChange?.({ groupBy }) 87 | }, [groupingOptions?.onStateChange, groupBy]) 88 | 89 | // Pagination 90 | useEffect(() => { 91 | paginationOptions?.onStateChange?.({ pageIndex, pageSize }) 92 | }, [paginationOptions?.onStateChange, pageIndex, pageSize]) 93 | 94 | // Column order, resize, hidden 95 | useEffect(() => { 96 | columnOptions?.onStateChange?.({ 97 | columnOrder, 98 | columnResizing, 99 | hiddenColumns 100 | }) 101 | }, [columnOptions?.onStateChange, columnOrder, columnResizing, hiddenColumns]) 102 | 103 | // Column order 104 | useEffect(() => { 105 | columnOptions?.onOrderStateChange?.(columnOrder) 106 | }, [columnOptions?.onOrderStateChange, columnOrder]) 107 | 108 | // Column resize 109 | useEffect(() => { 110 | columnOptions?.onResizeStateChange?.(columnResizing) 111 | }, [columnOptions?.onResizeStateChange, columnResizing]) 112 | 113 | // Column resize 114 | useEffect(() => { 115 | columnOptions?.onVisibilityStateChange?.(hiddenColumns) 116 | }, [columnOptions?.onVisibilityStateChange, hiddenColumns]) 117 | } 118 | 119 | export default useHandleStateChange 120 | -------------------------------------------------------------------------------- /lib/src/types/PaginationOptions.ts: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react' 2 | import type { 3 | UsePaginationOptions, 4 | UsePaginationState, 5 | UsePaginationInstanceProps 6 | } from './ReactTable' 7 | import type { DataType, StateChangeHandler } from './DataType' 8 | 9 | export type PaginationOptions = 10 | | ManualPaginationOptions 11 | | AutoPaginationOptions 12 | 13 | export interface ManualPaginationOptions 14 | extends CommonPaginationOptions { 15 | /** Manually paginate data using external methods. */ 16 | manualPagination?: true 17 | 18 | /** Number of pages available. Required for manual pagination. */ 19 | pageCount: number 20 | 21 | /** Number of total records available across all pages. 22 | * If not provided, an estimation is used in the Status. */ 23 | recordCount?: number 24 | 25 | /** 26 | * Callback to fetch more data when `manualPagination` is enabled. 27 | * It receives current pagination state as parameter. 28 | * It should be wrapped in `React.useCallback`. 29 | */ 30 | fetchData: (paginationState: UsePaginationState) => void 31 | } 32 | 33 | export interface AutoPaginationOptions 34 | extends CommonPaginationOptions { 35 | manualPagination?: undefined 36 | pageCount?: undefined 37 | fetchData?: undefined 38 | recordCount?: undefined 39 | } 40 | 41 | /** Type interface for pagination options. 42 | * @category Options */ 43 | interface CommonPaginationOptions 44 | extends UsePaginationOptions { 45 | /** Initial settings of pagination */ 46 | initialState?: Partial> 47 | 48 | /** Disable pagination. 49 | * @default false */ 50 | disablePagination?: boolean 51 | 52 | /** Reset pagination when data changes (sorting, filtering, etc.). 53 | * @default true for client-side pagination 54 | * @default false for manual-pagination. */ 55 | autoResetPage?: boolean 56 | 57 | /** Count expanded sub-rows while calculating rows on a page. 58 | * @default true */ 59 | paginateExpandedRows?: boolean 60 | 61 | /** Callback executed when page changes (all pagination changes). 62 | * The function must be wrapped in useCallback hook. */ 63 | onStateChange?: StateChangeHandler> 64 | 65 | /** Show a list of page-size options in Preference panel. 66 | * @default "[10, 20, 50, 75, 100, 150]" */ 67 | pageSizes?: Array 68 | 69 | // ---------- 70 | // Components 71 | 72 | /** Custom component to be rendered for pagination. 73 | * Overrides all indicators. 74 | * @category Custom Component */ 75 | Component?: PaginationComponent 76 | 77 | /** Indicator/icon used in action/button to 78 | * navigate to the next page. 79 | * @category Custom Component */ 80 | nextPageIndicator?: ReactNode 81 | 82 | /** Indicator/icon used in action/button to 83 | * navigate to the previous page. 84 | * @category Custom Component */ 85 | previousPageIndicator?: ReactNode 86 | 87 | /** Indicator/icon used in action/button to 88 | * navigate to the first page. 89 | * @category Custom Component */ 90 | firstPageIndicator?: ReactNode 91 | 92 | /** Indicator/icon used in action/button to 93 | * navigate to the last page. 94 | * @category Custom Component */ 95 | lastPageIndicator?: ReactNode 96 | } 97 | 98 | export type PaginationComponent = FC< 99 | UsePaginationInstanceProps & 100 | UsePaginationState & { 101 | loading: boolean 102 | } 103 | > 104 | 105 | export default PaginationOptions 106 | -------------------------------------------------------------------------------- /lib/src/components/TitleBar/GlobalFilter.tsx: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime' 2 | import * as React from 'react' 3 | import { useAsyncDebounce } from 'react-table' 4 | 5 | import Button from '../../common/Button' 6 | import Icon from '../../common/Icon' 7 | import type { DataType, UseGlobalFiltersInstanceProps } from '../../types' 8 | import useTableContext from '../../context' 9 | 10 | export default function GlobalFilter(): JSX.Element { 11 | const context = useTableContext() 12 | const { 13 | globalFilterOptions = {}, 14 | title = 'Table', 15 | localeOptions: { text } = {} 16 | } = context.tableProps 17 | 18 | const { 19 | rows, 20 | state: { globalFilter }, 21 | setGlobalFilter, 22 | flatRows, 23 | globalFilteredFlatRows, 24 | globalFilteredRows, 25 | globalFilteredRowsById, 26 | preGlobalFilteredFlatRows, 27 | preGlobalFilteredRows, 28 | preGlobalFilteredRowsById, 29 | rowsById 30 | } = context.tableInstance 31 | 32 | const { 33 | Component: CustomGlobalFilter = DefaultGlobalFilter, 34 | disableGlobalFilter = false, 35 | defaultVisibleGlobalFilter = false 36 | } = globalFilterOptions 37 | 38 | const [isGlobalFilterVisible, setIsGlobalFilterComponent] = React.useState( 39 | defaultVisibleGlobalFilter 40 | ) 41 | 42 | const showGlobalFilterComponent = React.useCallback( 43 | () => setIsGlobalFilterComponent(true), 44 | [] 45 | ) 46 | const hideGlobalFilterComponent = React.useCallback(() => { 47 | setGlobalFilter(undefined) 48 | setIsGlobalFilterComponent(false) 49 | }, []) 50 | 51 | const searchText = `${text?.search || 'Search'} ${ 52 | typeof title === 'string' ? title : '' 53 | }` 54 | 55 | const globalFilterProps: GlobalFilterProps = { 56 | globalFilterValue: globalFilter, 57 | flatRows, 58 | globalFilteredFlatRows, 59 | globalFilteredRows, 60 | globalFilteredRowsById, 61 | preGlobalFilteredFlatRows, 62 | preGlobalFilteredRows, 63 | preGlobalFilteredRowsById, 64 | rows, 65 | rowsById, 66 | setGlobalFilter, 67 | placeholder: searchText 68 | } 69 | 70 | return ( 71 |
72 | {isGlobalFilterVisible ? ( 73 | 74 | 75 | 81 | 82 | ) : ( 83 | 84 |
{title}
85 | {!disableGlobalFilter && ( 86 | 89 | )} 90 |
91 | )} 92 |
93 |
94 | ) 95 | } 96 | 97 | export interface GlobalFilterProps 98 | extends UseGlobalFiltersInstanceProps { 99 | globalFilterValue: string 100 | placeholder: string 101 | } 102 | 103 | // Define a default UI for filtering 104 | export function DefaultGlobalFilter({ 105 | globalFilterValue = '', 106 | setGlobalFilter, 107 | placeholder 108 | }: GlobalFilterProps) { 109 | const onChange = useAsyncDebounce((value) => { 110 | setGlobalFilter(value || undefined) 111 | }, 200) 112 | 113 | return ( 114 | onChange(e.target.value)} 119 | placeholder={placeholder} 120 | /> 121 | ) 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/types/SortingOptions.ts: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode, MouseEvent } from 'react' 2 | import type { 3 | UseSortByOptions, 4 | UseSortByState, 5 | Row, 6 | SortByFn, 7 | UseSortByColumnProps, 8 | HeaderGroup 9 | } from './ReactTable' 10 | import type { DataType, StateChangeHandler } from './DataType' 11 | 12 | /** Type interface for sorting options. 13 | * @category Options */ 14 | export interface SortingOptions 15 | extends UseSortByOptions { 16 | /** Initial settings of sorting. 17 | * List of objects containing column id and order preference. 18 | * @example 19 | * ``` 20 | * { sortBy: [{ id: 'columnsId', desc: false }] } 21 | * ``` */ 22 | initialState?: UseSortByState 23 | 24 | /** Disable sorting for table. 25 | * @default false */ 26 | disableSortBy?: boolean 27 | 28 | /** Enable sorting for all columns, 29 | * regardless if they have a valid accessor. 30 | * @default false */ 31 | defaultCanSort?: boolean 32 | 33 | /** If true, the un-sorted state will not be available 34 | * to columns once they have been sorted. 35 | * @default false */ 36 | disableSortRemove?: boolean 37 | 38 | /** Reset sorting when data is changed. 39 | * @default true */ 40 | autoResetSortBy?: boolean 41 | 42 | /** Manual sorting with custom logic, eg. server-side. 43 | * @default false @see orderByFn */ 44 | manualSortBy?: boolean 45 | 46 | /** Allows overriding or adding additional sort types for columns to use. 47 | * If a column's sort type isn't found on this object, 48 | * it will default to using the built-in sort types. Must be memoised. */ 49 | sortTypes?: Record> 50 | 51 | // ---------- 52 | // Multi-sort - Sorting with "shift" key pressed. 53 | 54 | /** Disables multi-sorting for the entire table. 55 | * @default false */ 56 | disableMultiSort?: boolean 57 | 58 | /** Limit on max number of columns for multi-sort. 59 | * @default Infinite */ 60 | maxMultiSortColCount?: number 61 | 62 | /** If true, the un-sorted state will not be available 63 | * to multi-sorted columns. 64 | * @default false */ 65 | disabledMultiRemove?: boolean 66 | 67 | /** Allows to override default multi-sort detection behaviour. 68 | * Receives mouse-event as param. */ 69 | isMultiSortEvent?: (e: MouseEvent) => boolean 70 | 71 | /** Composing multiple sorting functions together for multi-sorting */ 72 | orderByFn?: ( 73 | rows: Array>, 74 | sortFns: Array>, 75 | directions: boolean[] 76 | ) => Array> 77 | 78 | /** Callback executed when columns are sorted. 79 | * The function must be wrapped in useCallback hook. */ 80 | onStateChange?: StateChangeHandler> 81 | 82 | // ---------- 83 | // Components 84 | 85 | /** Custom component to manage all sorting buttons. 86 | * It overrides all sorting indicators. 87 | * @category Custom Component */ 88 | Component?: SortingComponent 89 | 90 | /** Indicator when column is not sorted. 91 | * Used in default sorting component. 92 | * @default '⇅' 93 | * @category Custom Component */ 94 | defaultIndicator?: ReactNode 95 | 96 | /** Indicator when column is sorted in ascending order. 97 | * Used in default sorting component. 98 | * @default '↓' 99 | * @category Custom Component */ 100 | ascendingIndicator?: ReactNode 101 | 102 | /** Indicator when column is sorted in descending order. 103 | * Used in default sorting component. 104 | * @default '↑' 105 | * @category Custom Component */ 106 | descendingIndicator?: ReactNode 107 | } 108 | 109 | export type SortingComponent = FC< 110 | UseSortByColumnProps & { 111 | onClick: (e: MouseEvent) => void 112 | column: HeaderGroup 113 | title: string 114 | } 115 | > 116 | 117 | export default SortingOptions 118 | -------------------------------------------------------------------------------- /lib/src/components/StatusBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import clsx from '../utilities/clsx' 3 | import type { DataType } from '../types' 4 | import Button from '../common/Button' 5 | import generateStatus from '../utilities/generateStatus' 6 | import Icon from '../common/Icon' 7 | import useTableContext from '../context' 8 | 9 | /** 10 | * StatusBar 11 | * @category Component 12 | */ 13 | export default function StatusBar(): JSX.Element | null { 14 | const context = useTableContext() 15 | const { styleOptions, paginationOptions, localeOptions } = context.tableProps 16 | const { statusBar = true } = styleOptions || {} 17 | const { 18 | Component, 19 | firstPageIndicator = , 20 | previousPageIndicator = , 21 | nextPageIndicator = , 22 | lastPageIndicator = 23 | } = paginationOptions || {} 24 | const { locale, text = {} } = localeOptions || {} 25 | 26 | const status = generateStatus(context) 27 | const { 28 | page, 29 | canPreviousPage, 30 | canNextPage, 31 | pageOptions, 32 | pageCount, 33 | gotoPage, 34 | nextPage, 35 | previousPage, 36 | setPageSize, 37 | state: { pageIndex, pageSize } 38 | } = context.tableInstance 39 | 40 | const loading: boolean = !!context.tableProps.loadingOptions?.loading 41 | const showPagination = pageOptions.length > 1 42 | 43 | if (!statusBar) return null 44 | return ( 45 |
46 | {status &&
{status}
} 47 |
48 | {showPagination ? ( 49 | Component ? ( 50 | 66 | ) : ( 67 |
68 | 75 | 82 |
83 | {text.page || 'Page'} 84 | { 88 | const page = e.target.value ? Number(e.target.value) - 1 : 0 89 | gotoPage(page) 90 | }} 91 | style={{ width: '60px' }} 92 | disabled={pageOptions.length <= 1} 93 | /> 94 | 95 | {text.of || 'of'} {pageOptions.length.toLocaleString(locale)} 96 | 97 |
98 | 105 | 112 |
113 | ) 114 | ) : null} 115 |
116 | ) 117 | } 118 | -------------------------------------------------------------------------------- /example/src/routes/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Column, UsePaginationState, TableInstance } from 'react-table' 3 | import ReactTableUI, { MultiRowAction, TableAction } from 'react-table-ui' 4 | import type { DataType, SingleRowAction } from 'react-table-ui' 5 | 6 | interface Passenger extends DataType { 7 | _id: string 8 | name: string 9 | trips: number 10 | airline: { 11 | id: number 12 | name: string 13 | country: string 14 | logo: string 15 | slogan: string 16 | website: string 17 | established: string 18 | }[] 19 | } 20 | 21 | export default function App() { 22 | const { 23 | fetchData, 24 | data, 25 | columns, 26 | loading, 27 | pageCount, 28 | recordCount 29 | } = usePassengersAPI() 30 | 31 | const tableInstanceRef = React.useRef>(null) 32 | 33 | const singleRowActions: SingleRowAction[] = [ 34 | { 35 | id: 'log', 36 | tooltip: 'Console log', 37 | onClick: console.log, 38 | children:
🪵 Console log a long message
39 | } 40 | ] 41 | 42 | const multiRowActions: MultiRowAction[] = [ 43 | { 44 | id: 'log', 45 | tooltip: 'Console log', 46 | onClick: console.log, 47 | children: '🪵' 48 | } 49 | ] 50 | 51 | const tableActions: TableAction[] = [ 52 | { 53 | id: 'load', 54 | tooltip: 'Reload', 55 | onClick: () => { 56 | const state = tableInstanceRef.current?.state 57 | if (state) { 58 | fetchData({ pageIndex: state.pageIndex, pageSize: state.pageSize }) 59 | } 60 | }, 61 | children: '🔄' 62 | } 63 | ] 64 | 65 | return ( 66 | 87 | ) 88 | } 89 | 90 | function usePassengersAPI() { 91 | const [passengers, setPassengers] = React.useState([]) 92 | const [pageCount, setPageCount] = React.useState(0) 93 | const [loading, setLoading] = React.useState(false) 94 | const [totalPassengers, setTotalPassengers] = React.useState(0) 95 | 96 | const fetchData = React.useCallback( 97 | async ({ pageSize, pageIndex }: UsePaginationState) => { 98 | setLoading(true) 99 | const jsonRes = await fetch( 100 | `https://api.instantwebtools.net/v1/passenger?page=${pageIndex}&size=${pageSize}` 101 | ) 102 | .then((res) => res.json()) 103 | .catch(console.error) 104 | .finally(() => setLoading(false)) 105 | 106 | setPassengers(jsonRes?.data || []) 107 | setPageCount(jsonRes?.totalPages || 0) 108 | setTotalPassengers(jsonRes?.totalPassengers || 0) 109 | }, 110 | [] 111 | ) 112 | 113 | const columns: Column[] = React.useMemo( 114 | () => [ 115 | { 116 | Header: 'Name', 117 | sticky: 'left', 118 | accessor: 'name' 119 | }, 120 | { 121 | Header: 'Trips', 122 | accessor: 'trips' 123 | }, 124 | 125 | { 126 | Header: 'Name', 127 | accessor: 'airline.name' 128 | }, 129 | { 130 | Header: 'Country', 131 | accessor: 'airline.country' 132 | }, 133 | { 134 | Header: 'Established', 135 | accessor: 'airline.established' 136 | } 137 | ], 138 | [] 139 | ) 140 | 141 | return { 142 | fetchData, 143 | pageCount, 144 | columns, 145 | data: passengers, 146 | loading, 147 | recordCount: totalPassengers 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/src/types/ReactTableUIProps.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | import type { Column } from './ReactTable' 3 | import type DataType from './DataType' 4 | import type SortingOptions from './SortingOptions' 5 | import type FiltersOptions from './FiltersOptions' 6 | import type GlobalFilterOptions from './GlobalFilterOptions' 7 | import type ExpandedOptions from './ExpandedOptions' 8 | import type RowSelectOptions from './RowSelectOptions' 9 | import type PaginationOptions from './PaginationOptions' 10 | import type ColumnOptions from './ColumnOptions' 11 | import type RowStateOptions from './RowStateOptions' 12 | import type TableOptions from './TableOptions' 13 | import type ActionOptions from './ActionOptions' 14 | import type StyleOptions from './StyleOptions' 15 | import type LocaleOptions from './LocaleOptions' 16 | import type { LoadingOptions, FreezeOptions } from './OtherOptions' 17 | 18 | /** 19 | * Props supported by React Table UI. 20 | * @category Important 21 | */ 22 | export interface ReactTableUIProps { 23 | /** Memoised data-array of the table. 24 | * The data object should always extends {@link DataType} interface. */ 25 | data: Data[] 26 | 27 | /** Memoised column definitions of the table. (Optional, can be generated from keys in `data` object). 28 | * Each column object can define its data accessor, properties and behavior. 29 | * Optional - Columns can be auto-generated based on provided dataset. */ 30 | columns?: Column[] 31 | 32 | /** Title of the table. 33 | * @default 'Table' */ 34 | title?: ReactNode 35 | 36 | /** Manage loading state of table. */ 37 | loadingOptions?: LoadingOptions 38 | 39 | /** Add and manage actions in that Table. */ 40 | actionOptions?: ActionOptions 41 | 42 | /** useTable Table options 43 | * @see [RT useTable API - Table options](https://react-table-v7.tanstack.com/docs/api/useTable#table-options) */ 44 | tableOptions?: TableOptions 45 | 46 | /** Manages sorting of the table columns. 47 | * @see [RT useSortBy API - Table options](https://react-table-v7.tanstack.com/docs/api/useSortBy#table-options) */ 48 | sortByOptions?: SortingOptions 49 | 50 | /** Manages filtering of the table columns. 51 | * @see [RT useFilters API - Table options](https://react-table-v7.tanstack.com/docs/api/useFilters#table-options) */ 52 | filtersOptions?: FiltersOptions 53 | 54 | /** Manages global filtering of the table. 55 | * @see [RT useGlobalFilter API - Table options](https://react-table-v7.tanstack.com/docs/api/useGlobalFilter#table-options) */ 56 | globalFilterOptions?: GlobalFilterOptions 57 | 58 | /** Manages pagination of the table. 59 | * @see [RT usePagination API - Table options](https://react-table-v7.tanstack.com/docs/api/usePagination#table-options) */ 60 | paginationOptions?: PaginationOptions 61 | 62 | /** Manages pagination of the table. 63 | * @see [RT usePagination API - Table options](https://react-table-v7.tanstack.com/docs/api/usePagination#table-options) */ 64 | expandedOptions?: ExpandedOptions 65 | 66 | /** Manages row-selection of the table. 67 | * @see [RT useRowSelect API - Table options](https://react-table-v7.tanstack.com/docs/api/useRowSelect#table-options) */ 68 | rowSelectOptions?: RowSelectOptions 69 | 70 | /** Manage options for column order, resize and visibility. 71 | * @see [RT useColumnOrder API - Table options](https://react-table-v7.tanstack.com/docs/api/useColumnOrder#table-options) 72 | * @see [RT useResizeColumns API - Table options](https://react-table-v7.tanstack.com/docs/api/useResizeColumns#table-options) */ 73 | columnOptions?: ColumnOptions 74 | 75 | /** Manage options for row state. 76 | * @see [RT useRowState API - Table options](https://react-table-v7.tanstack.com/docs/api/useRowState#table-options) */ 77 | rowStateOptions?: RowStateOptions 78 | 79 | /** Freeze headers to the top and footers to the bottom while scrolling. */ 80 | freezeOptions?: FreezeOptions 81 | 82 | /** Manage all style preferences related options. */ 83 | styleOptions?: StyleOptions 84 | 85 | /** Manage all locale preferences and translations. */ 86 | localeOptions?: LocaleOptions 87 | } 88 | 89 | export default ReactTableUIProps 90 | -------------------------------------------------------------------------------- /lib/src/styles/TableStyle.ts: -------------------------------------------------------------------------------- 1 | import styled from '../utilities/styled' 2 | import { border, CenterStyle, color, pxToEm, spacing } from '../utilities/theme' 3 | 4 | const THeadStyle = styled('THead')` 5 | & { 6 | position: relative; 7 | z-index: 5; 8 | width: 100%; 9 | top: 0; 10 | } 11 | 12 | & .Row { 13 | min-height: ${pxToEm(48)}; 14 | background-color: ${color.background.secondary}; 15 | } 16 | 17 | .Table.scrollY .THead .Row { 18 | box-shadow: 0px 2px 8px 0 rgba(0, 0, 0, 0.2); 19 | } 20 | 21 | & .Cell { 22 | cursor: inherit; 23 | display: flex; 24 | align-items: center; 25 | background-color: inherit; 26 | gap: ${spacing.md}; 27 | border-bottom: ${border.default}; 28 | color: ${color.text.secondary}; 29 | } 30 | 31 | & .Sort { 32 | visibility: hidden; 33 | } 34 | 35 | & .Cell:hover .Sort { 36 | visibility: visible; 37 | } 38 | 39 | & .Filter { 40 | width: 100%; 41 | } 42 | 43 | & .FilterRow { 44 | height: ${pxToEm(48)}; 45 | background-color: ${color.background.disabled}; 46 | } 47 | 48 | & .Filter input { 49 | position: relative; 50 | width: calc(100% + ${spacing.xl}); 51 | padding: ${spacing.sm} ${spacing.md}; 52 | left: calc(-1 * ${spacing.md}); 53 | font-weight: normal; 54 | } 55 | 56 | & .resizer { 57 | display: inline-block; 58 | background: ${color.border.primary}; 59 | width: 2px; 60 | height: ${pxToEm(24)}; 61 | position: absolute; 62 | right: 0; 63 | top: 50%; 64 | transform: translateY(-50%); 65 | z-index: 1; 66 | touch-action: none; 67 | } 68 | & .resizer:hover, 69 | & .resizer.isResizing { 70 | background: ${color.border.selected}; 71 | } 72 | ` 73 | 74 | const TBodyStyle = styled('TBody')` 75 | & { 76 | height: max-content; 77 | position: relative; 78 | z-index: 0; 79 | } 80 | 81 | & .Row { 82 | min-height: ${pxToEm(48)}; 83 | background: ${color.background.primary}; 84 | } 85 | 86 | & .Row.selected { 87 | background: ${color.background.selected}; 88 | } 89 | 90 | & .Row:focus, 91 | & .Row:focus .Cell { 92 | border-top: 1px solid ${color.border.selected}; 93 | border-bottom: 1px solid ${color.border.selected}; 94 | outline: none; 95 | } 96 | 97 | & .Cell { 98 | display: flex; 99 | align-items: center; 100 | background-color: inherit; 101 | border-bottom: ${border.default}; 102 | } 103 | 104 | & .RowAction { 105 | opacity: 0; 106 | } 107 | 108 | & .Row:hover .RowAction, 109 | & .Row:focus-within .RowAction { 110 | opacity: 1; 111 | } 112 | 113 | & .subComponent { 114 | box-shadow: inset 0 0 8px 0 #0002; 115 | background-color: ${color.background.secondary}; 116 | } 117 | & .subComponent .content { 118 | position: sticky; 119 | left: 0px; 120 | width: max-content; /* TODO */ 121 | padding: ${pxToEm(16)}; 122 | } 123 | ` 124 | 125 | export default styled.table` 126 | & { 127 | position: relative; 128 | z-index: 0; 129 | height: 100%; 130 | min-width: 100%; 131 | overflow: auto; 132 | background-color: ${color.background.secondary}; 133 | } 134 | 135 | & .sticky { 136 | position: sticky; 137 | } 138 | 139 | ${THeadStyle} 140 | ${TBodyStyle} 141 | 142 | & .RowSelectCheckbox { 143 | ${CenterStyle} 144 | } 145 | 146 | & [data-sticky-td] { 147 | position: sticky; 148 | } 149 | 150 | &.scrollX [data-sticky-last-left-td] { 151 | box-shadow: 1px 0px 2px ${color.border.primary}; 152 | } 153 | 154 | &.scrollX [data-sticky-first-right-td] { 155 | box-shadow: -1px 0px 2px ${color.border.primary}; 156 | } 157 | 158 | & .Cell { 159 | padding: ${spacing.md} ${spacing.xl}; 160 | } 161 | 162 | & .Cell.noSpacing { 163 | flex-direction: row; 164 | align-items: center; 165 | padding: 0; 166 | } 167 | 168 | & .loader { 169 | display: flex; 170 | justify-content: center; 171 | align-items: center; 172 | width: 100%; 173 | height: 100%; 174 | } 175 | 176 | & .TFoot { 177 | position: sticky; 178 | bottom: 0; 179 | z-index: 5; 180 | width: 100%; 181 | background: ${color.background.secondary}; 182 | } 183 | ` 184 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # React-Table UI 2 | 3 | [![NPM](https://img.shields.io/npm/v/react-table-ui.svg)](https://www.npmjs.com/package/react-table-ui) 4 | [![GitHub](https://img.shields.io/badge/GitHub-Repo-black)](https://github.com/GuptaSiddhant/react-table-ui) 5 | 6 | --- 7 | 8 | Out-of-the-box UI for [React-Table 7](https://react-table-v7.tanstack.com). 9 | 10 | - Customisable UI with custom theme and components. 11 | - Build with Typescript. Extends community types for React-Table. 12 | - Supports accessibility and keyboard interaction. 13 | - Allows all native React-Table configurations and extends on it. 14 | 15 | > The project is dedicated to the awesome work done at React-Table by [Tanner Linsley](https://twitter.com/tannerlinsley) as it wouldn't have been possible without his great library. I have personally use the library and wanted to contribute back to it somehow. 16 | 17 | ![RTUI](https://raw.githubusercontent.com/GuptaSiddhant/react-table-ui/main/assets/RTUI.jpg) 18 | 19 | --- 20 | 21 | ## Get started 22 | 23 | ### Install 24 | 25 | 1. Install the dependency 26 | 27 | ```bash 28 | npm install react-table-ui 29 | --- 30 | yarn add react-table-ui 31 | ``` 32 | 33 | The package size for production usage (with styles and without types) is ~36 KB (unzipped). The ~200 KB size of the complete package contains helpful TypeScript typings that makes using React-Table-UI a bliss. 34 | 35 | 1. For Typescript support, add `react-table-config.d.ts` file to your source (src) folder, if not added automatically. 36 | 37 | - [Preferred] Copy the file from your project's node_modules - 38 | `./node_modules/react-table-ui/dist/react-table-config.d.ts` 39 | to your source folder. 40 | - [Fallback] Get the file from [GitHub](https://github.com/GuptaSiddhant/react-table-ui/blob/main/src/react-table-config.d.ts). It may not match the exact version of library that you are using. 41 | 42 | ### Usage 43 | 44 | 45 | 46 | ```tsx 47 | /** React Table UI - Basic example (TypeScript) */ 48 | 49 | import ReactTableUI from 'react-table-ui' 50 | import { useMemo, useRef } from 'react' 51 | import type { TableInstance, DataType } from 'react-table-ui' 52 | 53 | /** Structure of data provided foe each row. */ 54 | interface User extends DataType { 55 | name: string 56 | age: number 57 | } 58 | 59 | const App = () => { 60 | // Provide data for the table 61 | const data: User[] = useMemo( 62 | () => [ 63 | { name: 'Abc Xyx', age: 20 }, 64 | { name: 'Def Uvw', age: 25 }, 65 | { name: 'Ghi Rst', age: 23 }, 66 | { name: 'Jklm Nopq', age: 30 } 67 | ], 68 | [] 69 | ) 70 | 71 | // Create an instance ref that will hold the reference to React Table instance. 72 | const tableInstanceRef = useRef>(null) 73 | 74 | return ( 75 | 80 | ) 81 | } 82 | ``` 83 | 84 |
85 | JavaScript (Basic example) 86 | 87 | ```jsx 88 | /** React Table UI - Basic example (JavaScript) */ 89 | 90 | import ReactTableUI from 'react-table-ui' 91 | import { useMemo, useRef } from 'react' 92 | 93 | const App = () => { 94 | // Provide data for the table 95 | const data = useMemo( 96 | () => [ 97 | { name: 'Abc Xyx', age: 20 }, 98 | { name: 'Def Uvw', age: 25 }, 99 | { name: 'Ghi Rst', age: 23 }, 100 | { name: 'Jklm Nopq', age: 30 } 101 | ], 102 | [] 103 | ) 104 | 105 | // Create an instance ref that will hold the reference to React Table instance. 106 | const tableInstanceRef = useRef(null) 107 | 108 | return ( 109 | 114 | ) 115 | } 116 | ``` 117 | 118 |
119 | 120 | ### API Documentation 121 | 122 | All options and properties available for ReactTableUI component are listed [here](https://react-table-ui.js.org/interfaces/reacttableuiprops.html). 123 | 124 | ### Tutorial 125 | 126 | - [Blog](http://guptasiddhant.com/projects/react-table-ui/) 127 | 128 | ### Examples 129 | 130 | - [Server pagination](https://codesandbox.io/s/react-table-ui-basic-8ukxd) 131 | 132 | ## License 133 | 134 | MIT © [GuptaSiddhant](https://github.com/GuptaSiddhant) 135 | -------------------------------------------------------------------------------- /lib/src/components/Filters.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import type { DataType, HeaderProps } from '../types' 4 | 5 | export function DefaultColumnFilter({ 6 | column: { filterValue, setFilter, render } 7 | }: HeaderProps) { 8 | const renderHeader = render('Header') 9 | const renderHeaderString = 10 | typeof renderHeader === 'string' ? renderHeader : '' 11 | 12 | return ( 13 | setFilter(e.target.value || undefined)} 18 | placeholder={`Filter ${renderHeaderString}`} 19 | /> 20 | ) 21 | } 22 | 23 | // // This is a custom filter UI for selecting 24 | // // a unique option from a list 25 | // function SelectColumnFilter({ 26 | // column: { filterValue, setFilter, preFilteredRows, id } 27 | // }) { 28 | // // Calculate the options for filtering 29 | // // using the preFilteredRows 30 | // const options = React.useMemo(() => { 31 | // const options = new Set() 32 | // preFilteredRows.forEach((row) => { 33 | // options.add(row.values[id]) 34 | // }) 35 | // return [...options.values()] 36 | // }, [id, preFilteredRows]) 37 | 38 | // // Render a multi-select box 39 | // return ( 40 | // 53 | // ) 54 | // } 55 | 56 | // // This is a custom filter UI that uses a 57 | // // slider to set the filter value between a column's 58 | // // min and max values 59 | // function SliderColumnFilter({ 60 | // column: { filterValue, setFilter, preFilteredRows, id } 61 | // }) { 62 | // // Calculate the min and max 63 | // // using the preFilteredRows 64 | 65 | // const [min, max] = React.useMemo(() => { 66 | // let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 67 | // let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 68 | // preFilteredRows.forEach((row) => { 69 | // min = Math.min(row.values[id], min) 70 | // max = Math.max(row.values[id], max) 71 | // }) 72 | // return [min, max] 73 | // }, [id, preFilteredRows]) 74 | 75 | // return ( 76 | // <> 77 | // { 83 | // setFilter(parseInt(e.target.value, 10)) 84 | // }} 85 | // /> 86 | // 87 | // 88 | // ) 89 | // } 90 | 91 | // // This is a custom UI for our 'between' or number range 92 | // // filter. It uses two number boxes and filters rows to 93 | // // ones that have values between the two 94 | // function NumberRangeColumnFilter({ 95 | // column: { filterValue = [], preFilteredRows, setFilter, id } 96 | // }) { 97 | // const [min, max] = React.useMemo(() => { 98 | // let min = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 99 | // let max = preFilteredRows.length ? preFilteredRows[0].values[id] : 0 100 | // preFilteredRows.forEach((row) => { 101 | // min = Math.min(row.values[id], min) 102 | // max = Math.max(row.values[id], max) 103 | // }) 104 | // return [min, max] 105 | // }, [id, preFilteredRows]) 106 | 107 | // return ( 108 | //
113 | // { 117 | // const val = e.target.value 118 | // setFilter((old = []) => [val ? parseInt(val, 10) : undefined, old[1]]) 119 | // }} 120 | // placeholder={`Min (${min})`} 121 | // style={{ 122 | // width: '70px', 123 | // marginRight: '0.5rem' 124 | // }} 125 | // /> 126 | // to 127 | // { 131 | // const val = e.target.value 132 | // setFilter((old = []) => [old[0], val ? parseInt(val, 10) : undefined]) 133 | // }} 134 | // placeholder={`Max (${max})`} 135 | // style={{ 136 | // width: '70px', 137 | // marginLeft: '0.5rem' 138 | // }} 139 | // /> 140 | //
141 | // ) 142 | // } 143 | -------------------------------------------------------------------------------- /lib/src/react-table-config.d.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | import type { 3 | UseColumnOrderInstanceProps, 4 | UseColumnOrderState, 5 | UseExpandedHooks, 6 | UseExpandedInstanceProps, 7 | UseExpandedOptions, 8 | UseExpandedRowProps, 9 | UseExpandedState, 10 | UseFiltersColumnOptions, 11 | UseFiltersColumnProps, 12 | UseFiltersInstanceProps, 13 | UseFiltersOptions, 14 | UseFiltersState, 15 | UseGlobalFiltersColumnOptions, 16 | UseGlobalFiltersInstanceProps, 17 | UseGlobalFiltersOptions, 18 | UseGlobalFiltersState, 19 | UseGroupByCellProps, 20 | UseGroupByColumnOptions, 21 | UseGroupByColumnProps, 22 | UseGroupByHooks, 23 | UseGroupByInstanceProps, 24 | UseGroupByOptions, 25 | UseGroupByRowProps, 26 | UseGroupByState, 27 | UsePaginationInstanceProps, 28 | UsePaginationOptions, 29 | UsePaginationState, 30 | UseResizeColumnsColumnOptions, 31 | UseResizeColumnsColumnProps, 32 | UseResizeColumnsOptions, 33 | UseResizeColumnsState, 34 | UseRowSelectHooks, 35 | UseRowSelectInstanceProps, 36 | UseRowSelectOptions, 37 | UseRowSelectRowProps, 38 | UseRowSelectState, 39 | UseRowStateCellProps, 40 | UseRowStateInstanceProps, 41 | UseRowStateOptions, 42 | UseRowStateRowProps, 43 | UseRowStateState, 44 | UseSortByColumnOptions, 45 | UseSortByColumnProps, 46 | UseSortByHooks, 47 | UseSortByInstanceProps, 48 | UseSortByOptions, 49 | UseSortByState 50 | } from 'react-table' 51 | 52 | declare module 'react-table' { 53 | // take this file as-is, or comment out the sections that don't apply to your plugin configuration 54 | 55 | export interface TableOptions 56 | extends UseExpandedOptions, 57 | UseFiltersOptions, 58 | UseGlobalFiltersOptions, 59 | UseGroupByOptions, 60 | UsePaginationOptions, 61 | UseResizeColumnsOptions, 62 | UseRowSelectOptions, 63 | UseRowStateOptions, 64 | UseSortByOptions, 65 | // note that having Record here allows you to add anything to the options, this matches the spirit of the 66 | // underlying js library, but might be cleaner if it's replaced by a more specific type that matches your 67 | // feature set, this is a safe default. 68 | Record {} 69 | 70 | export interface Hooks 71 | extends UseExpandedHooks, 72 | UseGroupByHooks, 73 | UseRowSelectHooks, 74 | UseSortByHooks {} 75 | 76 | export interface TableInstance 77 | extends UseColumnOrderInstanceProps, 78 | UseExpandedInstanceProps, 79 | UseFiltersInstanceProps, 80 | UseGlobalFiltersInstanceProps, 81 | UseGroupByInstanceProps, 82 | UsePaginationInstanceProps, 83 | UseRowSelectInstanceProps, 84 | UseRowStateInstanceProps, 85 | UseSortByInstanceProps, 86 | UseFiltersVisibleProps, 87 | UseModalProps {} 88 | 89 | export interface TableState 90 | extends UseColumnOrderState, 91 | UseExpandedState, 92 | UseFiltersState, 93 | UseGlobalFiltersState, 94 | UseGroupByState, 95 | UsePaginationState, 96 | UseResizeColumnsState, 97 | UseRowSelectState, 98 | UseRowStateState, 99 | UseSortByState, 100 | UseFiltersVisibleState, 101 | UseModalState {} 102 | 103 | export interface ColumnInterface 104 | extends UseFiltersColumnOptions, 105 | UseGlobalFiltersColumnOptions, 106 | UseGroupByColumnOptions, 107 | UseResizeColumnsColumnOptions, 108 | UseSortByColumnOptions { 109 | Footer?: ReactNode 110 | columns?: ColumnInstance[] 111 | sticky?: 'left' | 'right' 112 | align?: 'left' | 'right' | 'center' 113 | } 114 | 115 | export interface ColumnInstance 116 | extends UseFiltersColumnProps, 117 | UseGroupByColumnProps, 118 | UseResizeColumnsColumnProps, 119 | UseSortByColumnProps {} 120 | 121 | export interface Cell 122 | extends UseGroupByCellProps, 123 | UseRowStateCellProps {} 124 | 125 | export interface Row 126 | extends UseExpandedRowProps, 127 | UseGroupByRowProps, 128 | UseRowSelectRowProps, 129 | UseRowStateRowProps {} 130 | 131 | export interface UseFiltersVisibleState { 132 | filtersVisible?: boolean 133 | } 134 | export interface UseFiltersVisibleProps { 135 | setFiltersVisible: (filtersVisible: boolean) => void 136 | resetFiltersVisible: () => void 137 | toggleFiltersVisible: () => void 138 | } 139 | 140 | export interface UseModalState { 141 | modal?: ModalProps 142 | } 143 | 144 | export interface UseModalProps { 145 | setModal: (modalProps: ModalProps) => void 146 | resetModal: () => void 147 | } 148 | 149 | /** Model props */ 150 | export interface ModalProps { 151 | title: string 152 | onSave?: () => void 153 | children: React.ReactNode 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /example/src/react-table-config.d.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react' 2 | import type { 3 | UseColumnOrderInstanceProps, 4 | UseColumnOrderState, 5 | UseExpandedHooks, 6 | UseExpandedInstanceProps, 7 | UseExpandedOptions, 8 | UseExpandedRowProps, 9 | UseExpandedState, 10 | UseFiltersColumnOptions, 11 | UseFiltersColumnProps, 12 | UseFiltersInstanceProps, 13 | UseFiltersOptions, 14 | UseFiltersState, 15 | UseGlobalFiltersColumnOptions, 16 | UseGlobalFiltersInstanceProps, 17 | UseGlobalFiltersOptions, 18 | UseGlobalFiltersState, 19 | UseGroupByCellProps, 20 | UseGroupByColumnOptions, 21 | UseGroupByColumnProps, 22 | UseGroupByHooks, 23 | UseGroupByInstanceProps, 24 | UseGroupByOptions, 25 | UseGroupByRowProps, 26 | UseGroupByState, 27 | UsePaginationInstanceProps, 28 | UsePaginationOptions, 29 | UsePaginationState, 30 | UseResizeColumnsColumnOptions, 31 | UseResizeColumnsColumnProps, 32 | UseResizeColumnsOptions, 33 | UseResizeColumnsState, 34 | UseRowSelectHooks, 35 | UseRowSelectInstanceProps, 36 | UseRowSelectOptions, 37 | UseRowSelectRowProps, 38 | UseRowSelectState, 39 | UseRowStateCellProps, 40 | UseRowStateInstanceProps, 41 | UseRowStateOptions, 42 | UseRowStateRowProps, 43 | UseRowStateState, 44 | UseSortByColumnOptions, 45 | UseSortByColumnProps, 46 | UseSortByHooks, 47 | UseSortByInstanceProps, 48 | UseSortByOptions, 49 | UseSortByState 50 | } from 'react-table' 51 | 52 | declare module 'react-table' { 53 | // take this file as-is, or comment out the sections that don't apply to your plugin configuration 54 | 55 | export interface TableOptions 56 | extends UseExpandedOptions, 57 | UseFiltersOptions, 58 | UseGlobalFiltersOptions, 59 | UseGroupByOptions, 60 | UsePaginationOptions, 61 | UseResizeColumnsOptions, 62 | UseRowSelectOptions, 63 | UseRowStateOptions, 64 | UseSortByOptions, 65 | // note that having Record here allows you to add anything to the options, this matches the spirit of the 66 | // underlying js library, but might be cleaner if it's replaced by a more specific type that matches your 67 | // feature set, this is a safe default. 68 | Record {} 69 | 70 | export interface Hooks 71 | extends UseExpandedHooks, 72 | UseGroupByHooks, 73 | UseRowSelectHooks, 74 | UseSortByHooks {} 75 | 76 | export interface TableInstance 77 | extends UseColumnOrderInstanceProps, 78 | UseExpandedInstanceProps, 79 | UseFiltersInstanceProps, 80 | UseGlobalFiltersInstanceProps, 81 | UseGroupByInstanceProps, 82 | UsePaginationInstanceProps, 83 | UseRowSelectInstanceProps, 84 | UseRowStateInstanceProps, 85 | UseSortByInstanceProps, 86 | UseFiltersVisibleProps, 87 | UseModalProps {} 88 | 89 | export interface TableState 90 | extends UseColumnOrderState, 91 | UseExpandedState, 92 | UseFiltersState, 93 | UseGlobalFiltersState, 94 | UseGroupByState, 95 | UsePaginationState, 96 | UseResizeColumnsState, 97 | UseRowSelectState, 98 | UseRowStateState, 99 | UseSortByState, 100 | UseFiltersVisibleState, 101 | UseModalState {} 102 | 103 | export interface ColumnInterface 104 | extends UseFiltersColumnOptions, 105 | UseGlobalFiltersColumnOptions, 106 | UseGroupByColumnOptions, 107 | UseResizeColumnsColumnOptions, 108 | UseSortByColumnOptions { 109 | Footer?: ReactNode 110 | columns?: ColumnInstance[] 111 | sticky?: 'left' | 'right' 112 | align?: 'left' | 'right' | 'center' 113 | } 114 | 115 | export interface ColumnInstance 116 | extends UseFiltersColumnProps, 117 | UseGroupByColumnProps, 118 | UseResizeColumnsColumnProps, 119 | UseSortByColumnProps {} 120 | 121 | export interface Cell 122 | extends UseGroupByCellProps, 123 | UseRowStateCellProps {} 124 | 125 | export interface Row 126 | extends UseExpandedRowProps, 127 | UseGroupByRowProps, 128 | UseRowSelectRowProps, 129 | UseRowStateRowProps {} 130 | 131 | export interface UseFiltersVisibleState { 132 | filtersVisible?: boolean 133 | } 134 | export interface UseFiltersVisibleProps { 135 | setFiltersVisible: (filtersVisible: boolean) => void 136 | resetFiltersVisible: () => void 137 | toggleFiltersVisible: () => void 138 | } 139 | 140 | export interface UseModalState { 141 | modal?: ModalProps 142 | } 143 | 144 | export interface UseModalProps { 145 | setModal: (modalProps: ModalProps) => void 146 | resetModal: () => void 147 | } 148 | 149 | /** Model props */ 150 | export interface ModalProps { 151 | title: string 152 | onSave?: () => void 153 | children: React.ReactNode 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/src/common/FocusTrap.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | /** List of `css` selectors for elements that can be focussed. */ 4 | const focusableElementSelectors = [ 5 | 'button', 6 | '[href]', 7 | 'input', 8 | 'select', 9 | 'textarea', 10 | '[tabindex]:not([tabindex="-1"])' 11 | ] 12 | 13 | type FocusTrapElement = HTMLElement 14 | 15 | /** Type definition for props supported by FocusTrap component. */ 16 | export interface FocusTrapProps 17 | extends React.HTMLAttributes { 18 | /** Render FocusTrap as different HTML element. @default 'div' */ 19 | element?: keyof JSX.IntrinsicElements 20 | /** Add additional CSS selectors to query focusable children. */ 21 | selectors?: string[] 22 | /** Focus the first element when it is visible. @default true */ 23 | autoFocusFirstElement?: boolean 24 | } 25 | 26 | /** 27 | * FocusTrap 28 | * --- 29 | * Wrapper to trap focus among its children. 30 | * Can be used as a base for Styled components. 31 | * 32 | * @author Siddhant Gupta 33 | */ 34 | export default function FocusTrap({ 35 | element = 'div', 36 | selectors = [], 37 | autoFocusFirstElement, 38 | children, 39 | ...props 40 | }: FocusTrapProps): JSX.Element { 41 | const containerRef = React.useRef(null) 42 | const firstElementRef = React.useRef() 43 | const lastElementRef = React.useRef() 44 | 45 | React.useEffect(() => { 46 | const focusableElements = containerRef.current?.querySelectorAll( 47 | [...focusableElementSelectors, ...selectors].join(', ') 48 | ) 49 | firstElementRef.current = focusableElements?.item(0) as FocusTrapElement 50 | lastElementRef.current = focusableElements?.item( 51 | focusableElements.length - 1 52 | ) as FocusTrapElement 53 | }, [containerRef.current, children, selectors]) 54 | 55 | useFocusTrap( 56 | firstElementRef as React.RefObject, 57 | lastElementRef as React.RefObject, 58 | autoFocusFirstElement 59 | ) 60 | 61 | const Element = element as React.ElementType 62 | return ( 63 | 64 | {children} 65 | 66 | ) 67 | } 68 | 69 | type UseFocusTrapParam = React.RefObject 70 | 71 | /** 72 | * Hook to trap focus between two elements. 73 | * @param firstElementRef Reference for first focusable element. 74 | * @param lastElementRef Reference for last focusable element. 75 | * @param autoFocusFirstElement Focus the first element when it is visible (default: true) 76 | * 77 | * @author Siddhant Gupta 78 | */ 79 | function useFocusTrap( 80 | firstElementRef: UseFocusTrapParam, 81 | lastElementRef: UseFocusTrapParam, 82 | autoFocusFirstElement: boolean = true 83 | ) { 84 | const firstElement = firstElementRef.current 85 | const lastElement = lastElementRef.current 86 | 87 | const isFirstElementVisible = useOnScreen(firstElementRef, {}) 88 | 89 | const handleKeyDown = React.useCallback( 90 | (e: KeyboardEvent) => { 91 | const isTabPressed = e.key === 'Tab' 92 | const isShiftPressed = e.shiftKey 93 | if (!firstElement) { 94 | console.warn('No element found with firstElementRef.') 95 | return 96 | } 97 | if (!lastElement) { 98 | console.warn('No element found with lastElementRef') 99 | return 100 | } 101 | // Cycle focus from first to last element 102 | if ( 103 | isTabPressed && 104 | isShiftPressed && 105 | document.activeElement === firstElement 106 | ) { 107 | e.preventDefault() 108 | lastElement.focus() 109 | } 110 | // Cycle focus from last to first element 111 | if ( 112 | isTabPressed && 113 | !isShiftPressed && 114 | document.activeElement === lastElement 115 | ) { 116 | e.preventDefault() 117 | firstElement.focus() 118 | } 119 | }, 120 | [firstElement, lastElement] 121 | ) 122 | React.useEffect(() => { 123 | document.addEventListener('keydown', handleKeyDown) 124 | return () => document.removeEventListener('keydown', handleKeyDown) 125 | }, [handleKeyDown]) 126 | 127 | React.useEffect(() => { 128 | autoFocusFirstElement && 129 | isFirstElementVisible && 130 | firstElement && 131 | firstElement.focus() 132 | }, [autoFocusFirstElement, isFirstElementVisible]) 133 | } 134 | 135 | function useOnScreen( 136 | ref: React.RefObject, 137 | options: any, 138 | screenSizeLimit = 960 139 | ) { 140 | const [visible, setVisible] = React.useState(false) 141 | 142 | React.useEffect(() => { 143 | if (window.innerWidth >= screenSizeLimit) { 144 | const observer = new IntersectionObserver(([entry]) => { 145 | setVisible(entry.isIntersecting) 146 | }, options) 147 | 148 | if (ref.current) { 149 | observer.observe(ref.current) 150 | } 151 | 152 | return () => { 153 | if (ref.current) { 154 | observer.unobserve(ref.current) 155 | } 156 | } 157 | } 158 | }, [ref, options]) 159 | 160 | return visible 161 | } 162 | -------------------------------------------------------------------------------- /lib/src/logic/useReactTableUI.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { 3 | useTable, 4 | useSortBy, 5 | useFilters, 6 | usePagination, 7 | useRowSelect, 8 | useExpanded, 9 | useColumnOrder, 10 | useResizeColumns, 11 | useGlobalFilter, 12 | useRowState, 13 | useFlexLayout 14 | } from 'react-table' 15 | import { useSticky } from 'react-table-sticky' 16 | 17 | import { DefaultColumnFilter } from '../components/Filters' 18 | import { fixColumnOrder } from '../utilities/systemColumns' 19 | import { NOOP } from '../utilities' 20 | import type { 21 | TableContext, 22 | DataType, 23 | ReactTableUIProps, 24 | Column 25 | } from '../types' 26 | 27 | import generateUseExpandedColumn from './generateUseExpandedColumn' 28 | import generateUseRowSelectColumn from './generateUseRowSelectColumn' 29 | import generateUseRowActionColumn from './generateUseRowActionColumn' 30 | 31 | import useManualPagination from './useManualPagination' 32 | import useHandleStateChange from './useHandleStateChange' 33 | import useCreateDefaultColumns from './useCreateDefaultColumns' 34 | import useVisibleFilters from './useVisibleFilters' 35 | import useModal from './useModal' 36 | 37 | const useReactTableUI = ( 38 | tableProps: ReactTableUIProps, 39 | tableRef: React.RefObject 40 | ): TableContext => { 41 | const { 42 | data = [], 43 | columns = useCreateDefaultColumns(data), 44 | tableOptions = {}, 45 | filtersOptions = {}, 46 | globalFilterOptions = {}, 47 | sortByOptions = {}, 48 | paginationOptions = {}, 49 | expandedOptions = {}, 50 | rowSelectOptions = {}, 51 | columnOptions = {}, 52 | rowStateOptions = {}, 53 | freezeOptions: { columns: freezeColumns = true } = {} 54 | } = tableProps 55 | 56 | const { 57 | initialState: initialFiltersState, 58 | DefaultComponent: DefaultFilterComponent, 59 | ...filtersTableOptions 60 | } = filtersOptions 61 | 62 | const { 63 | initialState: initialGlobalFilterState, 64 | ...globalFilterTableOptions 65 | } = globalFilterOptions 66 | 67 | const { 68 | initialState: initialSortByState, 69 | ...sortByTableOptions 70 | } = sortByOptions 71 | 72 | const { 73 | initialState: initialExpandedState, 74 | ...expandedTableOptions 75 | } = expandedOptions 76 | 77 | const { 78 | initialState: initialRowSelectState, 79 | ...rowSelectTableOptions 80 | } = rowSelectOptions 81 | 82 | const { 83 | initialState: initialPaginationState, 84 | ...paginationTableOptions 85 | } = paginationOptions 86 | 87 | const { 88 | initialState: initialColumnState, 89 | ...columnTableOptions 90 | } = columnOptions 91 | 92 | const { 93 | initialState: initialRowStateState, 94 | ...rowStateTableOptions 95 | } = rowStateOptions 96 | 97 | const defaultColumn: Partial> = useMemo( 98 | () => ({ 99 | Filter: DefaultFilterComponent || DefaultColumnFilter 100 | }), 101 | [] 102 | ) 103 | 104 | const useRowSelectColumn = generateUseRowSelectColumn(tableProps) 105 | const useRowActionColumn = generateUseRowActionColumn(tableProps) 106 | const { useExpandedColumn, disableExpander } = generateUseExpandedColumn( 107 | tableProps 108 | ) 109 | 110 | // Main hook call 111 | const tableInstance = useTable( 112 | { 113 | data, 114 | columns, 115 | defaultColumn, 116 | ...tableOptions, 117 | ...filtersTableOptions, 118 | ...globalFilterTableOptions, 119 | ...sortByTableOptions, 120 | ...{ 121 | autoResetPage: !paginationTableOptions.manualPagination, 122 | ...paginationTableOptions 123 | }, 124 | ...expandedTableOptions, 125 | ...rowSelectTableOptions, 126 | ...columnTableOptions, 127 | ...rowStateTableOptions, 128 | initialState: { 129 | ...(tableOptions?.initialState || {}), 130 | ...initialSortByState, 131 | ...initialFiltersState, 132 | ...initialGlobalFilterState, 133 | ...initialExpandedState, 134 | ...initialRowSelectState, 135 | ...initialRowStateState, 136 | ...{ 137 | pageSize: paginationTableOptions.pageSizes 138 | ? paginationTableOptions.pageSizes[0] 139 | : 50, 140 | pageIndex: 0, 141 | ...initialPaginationState 142 | }, 143 | ...{ 144 | ...initialColumnState, 145 | columnOrder: fixColumnOrder(initialColumnState?.columnOrder, columns) 146 | } 147 | } 148 | }, 149 | filtersOptions.disableFilters ? NOOP : useFilters, 150 | globalFilterOptions.disableGlobalFilter ? NOOP : useGlobalFilter, 151 | sortByOptions.disableSortBy ? NOOP : useSortBy, 152 | disableExpander ? NOOP : useExpanded, 153 | disableExpander ? NOOP : useExpandedColumn, 154 | paginationOptions.disablePagination ? NOOP : usePagination, 155 | rowSelectOptions.disableRowSelect ? NOOP : useRowSelect, 156 | rowSelectOptions.disableRowSelect ? NOOP : useRowSelectColumn, 157 | rowStateOptions.disableRowState ? NOOP : useRowState, 158 | columnOptions.disableOrdering ? NOOP : useColumnOrder, 159 | columnOptions.disableResizing ? NOOP : useResizeColumns, 160 | useRowActionColumn, 161 | useFlexLayout, 162 | useVisibleFilters, 163 | useModal, 164 | freezeColumns ? useSticky : NOOP 165 | ) 166 | 167 | const tableContext: TableContext = { 168 | tableInstance, 169 | tableProps, 170 | tableRef 171 | } 172 | 173 | useManualPagination(tableContext) 174 | useHandleStateChange(tableContext) 175 | 176 | return tableContext 177 | } 178 | 179 | export default useReactTableUI 180 | -------------------------------------------------------------------------------- /lib/src/components/Head.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import clsx from '../utilities/clsx' 4 | import Cell from '../common/Cell' 5 | import { checkIfSystemColumn } from '../utilities/systemColumns' 6 | import type { 7 | DataType, 8 | HeaderGroup, 9 | ReactTableUIProps, 10 | SortingComponent 11 | } from '../types' 12 | import useTableContext from '../context' 13 | 14 | export default function Head(): JSX.Element { 15 | const context = useTableContext() 16 | const { tableInstance, tableProps } = context 17 | const { headerGroups, state } = tableInstance 18 | const { freezeOptions } = tableProps 19 | const freezeHead = freezeOptions?.header !== false 20 | 21 | const filtersHeaderGroup = headerGroups[headerGroups.length - 1] 22 | const { filtersVisible } = state 23 | 24 | return ( 25 |
29 | {headerGroups.map((headerGroup, index) => ( 30 | 31 | ))} 32 | {filtersVisible && ( 33 | 38 | )} 39 |
40 | ) 41 | } 42 | 43 | interface HeadCellProps { 44 | column: HeaderGroup 45 | } 46 | 47 | function HeadFilterCell({ 48 | column 49 | }: HeadCellProps): JSX.Element | null { 50 | const renderContent = column.render('Header') 51 | const renderContentString = 52 | typeof renderContent === 'string' 53 | ? renderContent 54 | : renderContent?.toString() || '' 55 | const headCellProps = column.getHeaderProps(column.getSortByToggleProps()) 56 | const isSystemColumn = checkIfSystemColumn(column) 57 | const columnFilter = column.render('Filter') as React.ReactNode 58 | 59 | return ( 60 | 69 | {!column.canFilter ? null :
{columnFilter}
} 70 |
71 | ) 72 | } 73 | 74 | function HeadCell({ 75 | column 76 | }: HeadCellProps): JSX.Element | null { 77 | const { tableProps } = useTableContext() 78 | const { disableResizing = false } = tableProps.columnOptions || {} 79 | 80 | const SortComponent = createSortComponent(tableProps) 81 | const renderContent = column.render('Header') 82 | const renderContentString = 83 | typeof renderContent === 'string' 84 | ? renderContent 85 | : renderContent?.toString() || '' 86 | const headCellProps = column.getHeaderProps(column.getSortByToggleProps()) 87 | const isSystemColumn = checkIfSystemColumn(column) 88 | 89 | return ( 90 | 99 | <> 100 | {renderContent} 101 | 107 | {!disableResizing && !column.disableResizing && !isSystemColumn && ( 108 |
113 | )} 114 | 115 | 116 | ) 117 | } 118 | 119 | function HeaderRow(props: { 120 | headerGroup: HeaderGroup 121 | }): JSX.Element { 122 | const { getHeaderGroupProps, headers } = props.headerGroup 123 | 124 | return ( 125 |
126 | {headers.map((column) => ( 127 | 128 | ))} 129 |
130 | ) 131 | } 132 | 133 | function FilterRow(props: { 134 | headerGroup: HeaderGroup 135 | }): JSX.Element { 136 | const { getHeaderGroupProps, headers } = props.headerGroup 137 | 138 | return ( 139 |
140 | {headers.map((column) => ( 141 | 145 | ))} 146 |
147 | ) 148 | } 149 | 150 | function createSortComponent( 151 | tableProps: ReactTableUIProps 152 | ): SortingComponent { 153 | const { 154 | Component, 155 | descendingIndicator = '↑', 156 | ascendingIndicator = '↓', 157 | defaultIndicator = '⇅' 158 | } = tableProps.sortByOptions || {} 159 | 160 | const DefaultSortComponent: SortingComponent = ({ 161 | canSort, 162 | isSorted, 163 | isSortedDesc, 164 | onClick, 165 | title 166 | }) => 167 | canSort ? ( 168 |
174 | {isSorted 175 | ? isSortedDesc 176 | ? descendingIndicator 177 | : ascendingIndicator 178 | : defaultIndicator} 179 |
180 | ) : null 181 | 182 | return Component || DefaultSortComponent 183 | } 184 | --------------------------------------------------------------------------------