├── .github └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── LICENSE ├── README.md ├── __mocks__ └── fileMock.js ├── example ├── .npmignore ├── index.html ├── index.tsx ├── indicator.gif ├── package-lock.json ├── package.json ├── src │ ├── Docs.tsx │ ├── IconList.tsx │ └── Spinner.tsx ├── styles.css └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── CommonLoaderProps.ts ├── LoaderContext.tsx ├── index.tsx ├── svg-loaders │ ├── audio.svg │ ├── ball-triangle.svg │ ├── bars.svg │ ├── circles.svg │ ├── grid.svg │ ├── hearts.svg │ ├── oval.svg │ ├── puff.svg │ ├── rings.svg │ ├── spinning-circles.svg │ ├── tail-spin.svg │ └── three-dots.svg ├── svg.d.ts └── useLoading.tsx ├── test └── useLoading.test.tsx ├── tsconfig.json └── tsdx.config.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Agney 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

React Loading

2 |

3 | Simple and Accessible loading indicators with React. 4 |
5 |
6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | code style: prettier 16 | 17 | 18 | 19 | 20 | 21 | Comes bundled with React components of [Sam Herbert's animated SVG loaders](https://github.com/SamHerbert/SVG-Loaders) in a tree shakeable package. 22 | 23 | ![Example of loading indicator SVGs](./example/indicator.gif) 24 | 25 | ## Installation 26 | 27 | ```bash 28 | npm i @agney/react-loading 29 | # OR 30 | yarn add @agney/react-loading 31 | ``` 32 | 33 | [Demo](https://agneym.github.io/react-loading/) 34 | 35 | ## Features 36 | 37 | - **Small Size** 38 | 39 | The entire library is about [20kB minified](https://bundlephobia.com/result?p=@agney/react-loading). _But you would never need the whole bundle._ 40 | 41 | The library is build to be treeshakeable that when you use one or two of the bundled loaders, you would have **less than 1kB** in your bundle. 42 | 43 | No dependencies either 😇 44 | 45 | - **Accessibility** 46 | 47 | Provides accessibility attributes on your loading components and containers. 48 | 49 | `aria-busy` is set to `true` on container on loading and progress indicators have `role=progressbar`. 50 | 51 | - **Specify a Global loader** 52 | 53 | You probably don't want loader components mixed everywhere, so you can specify a `LoaderContext` that can be overridden later if necessary. 54 | 55 | - **Bring your own loader** 56 | 57 | If you decide to bring your own loading indicator, library would support that as well, keeping all your logic the same. 58 | 59 | - TypeScript support. Zero extra CSS. 60 | 61 | ## Usage 62 | 63 | ```javascript 64 | import { useLoading, Audio } from '@agney/react-loading'; 65 | 66 | function Content() { 67 | const { containerProps, indicatorEl } = useLoading({ 68 | loading: true, 69 | indicator:

75 | {indicatorEl} {/* renders only while loading */} 76 |
77 | ); 78 | } 79 | ``` 80 | [Sample](https://codesandbox.io/s/agneyreact-loading-example-9jj7c) 81 | 82 | ## Loaders 83 | 84 | This library comes bundled with React components of [Sam Herbert's animated SVG loaders](https://github.com/SamHerbert/SVG-Loaders) in a tree shakeable package. 85 | 86 | Each loader is an SVG and all props passed shall be applied to the top SVG element. All SVGs are set to inherit `currentColor` from it's parents for fill/stroke. 87 | 88 | Available loaders are: 89 | 90 | ```javascript 91 | import { 92 | Audio, 93 | BallTriangle, 94 | Bars, 95 | Circles, 96 | Grid, 97 | Hearts, 98 | Oval, 99 | Puff, 100 | Rings, 101 | SpinningCircles, 102 | TailSpin, 103 | ThreeDots, 104 | } from '@agney/react-loading'; 105 | ``` 106 | 107 | Only the ones you use will be included in your bundle when you use a bundler like Webpack/Rollup. 108 | 109 | ## Context 110 | 111 | You can specify a single loading indicator reused across hooks with the `LoaderProvider`. 112 | 113 | ```javascript 114 | import { LoaderProvider, useLoading, BallTriangle } from '@agney/react-loading'; 115 | 116 | function App() { 117 | const { containerProps, indicatorEl } = useLoading({ 118 | loading: true, 119 | }); 120 | return
{indicatorEl}
; 121 | } 122 | 123 | ReactDOM.render( 124 | }> 125 | 126 | 127 | ); 128 | ``` 129 | 130 | You can use as many `LoaderProvider` provider elements as you like and React will pick the one closest to the hook you are rendering. 131 | 132 | [More on React Context](https://reactjs.org/docs/context.html) 133 | 134 | ## Extra Props on Loader 135 | 136 | If you want to provide specific props on a loader specifically when you use the hook: 137 | 138 | ```javascript 139 | useLoading({ 140 | loading: true, 141 | loaderProps: { 142 | // Any props here would be spread on to the indicator element. 143 | style: {{ margin: '0 auto' }} 144 | } 145 | }); 146 | ``` 147 | 148 | We also a provide a special key for `valueText`, that will be used as description for indicator: 149 | 150 | ```javascript 151 | useLoading({ 152 | loading: true, 153 | loaderProps: { 154 | valueText: 'Fetching video from the Great Internet', 155 | }, 156 | }); 157 | 158 | // now this will generate: 159 | /* */ 160 | ``` 161 | 162 | `aria-valuetext` will be read by screenreaders. 163 | 164 | You could also provide `aria-valuenow` for indicators that display progress (but the prebundled ones are best for indeterminate progress indicators) 165 | 166 | [MDN for Reference on `progressbar`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_progressbar_role) 167 | 168 | ## Bring your own loader 169 | 170 | Just switch the import to your own loading indicator (just make sure that it accepts props) 171 | 172 | ```javascript 173 | import { LoaderProvider, useLoading } from '@agney/react-loading'; 174 | 175 | const Loader = ({ ...rest }) =>

Loading...

; 176 | 177 | function App() { 178 | const { containerProps, indicatorEl } = useLoading({ 179 | loading: true, 180 | }); 181 | return
{indicatorEl}
; 182 | } 183 | 184 | ReactDOM.render( 185 | }> 186 | 187 | 188 | ); 189 | ``` 190 | 191 | # Contributing 192 | 193 | All PRs welcome. 194 | 195 | ## Development 196 | 197 | We use [`tsdx`](https://github.com/formium/tsdx) for generating boilerplate. 198 | 199 | **Install:** 200 | 201 | ```bash 202 | # Library 203 | npm i 204 | 205 | # Example 206 | cd example && npm i 207 | ``` 208 | 209 | **Development:** 210 | 211 | ```bash 212 | # Running library dev 213 | npm start 214 | 215 | # Running example 216 | cd example && npm start 217 | ``` 218 | 219 | **Testing:** 220 | 221 | Testing with `react-testing-library` and `jest` 222 | 223 | ```bash 224 | npm test 225 | ``` 226 | 227 | Commands are available in detail on tsdx repository. 228 | 229 | # Credits 230 | 231 | - [Sam Herbert](http://samherbert.net/) for the amazing SVGs 232 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | 5 | import './styles.css'; 6 | import IconList from './src/IconList'; 7 | import Docs from './src/Docs'; 8 | 9 | const App = () => { 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | ) 20 | }; 21 | 22 | ReactDOM.render(, document.getElementById('root')); 23 | -------------------------------------------------------------------------------- /example/indicator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agneym/react-loading/caab0d665a47834b04bc8dd77d6d15a02bb27e2c/example/indicator.gif -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html --public-url ./" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/src/Docs.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const Docs = () => { 4 | return ( 5 | <> 6 |

Installation

7 | 8 | npm install @agney/react-loading 9 | 10 |

For more detailed docs, visit Github page

11 | 12 | ); 13 | } 14 | 15 | export default Docs; 16 | -------------------------------------------------------------------------------- /example/src/IconList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { 4 | Audio, 5 | BallTriangle, 6 | Bars, 7 | Circles, 8 | Grid, 9 | Hearts, 10 | Oval, 11 | Puff, 12 | Rings, 13 | SpinningCircles, 14 | TailSpin, 15 | ThreeDots, 16 | } from '../../.'; 17 | 18 | import SpinnerSection from './Spinner'; 19 | 20 | const spinners = [ 21 | { indicator: Audio, width: 40 }, 22 | { indicator: BallTriangle, width: 50 }, 23 | { indicator: Bars, width: 40 }, 24 | { indicator: Circles, width: 50 }, 25 | { indicator: Grid, width: 40 }, 26 | { indicator: Hearts, width: 80 }, 27 | { indicator: Oval, width: 50 }, 28 | { indicator: Puff, width: 50 }, 29 | { indicator: Rings, width: 60 }, 30 | { indicator: TailSpin, width: 50 }, 31 | { indicator: ThreeDots, width: 60 }, 32 | { indicator: SpinningCircles, width: 50 }, 33 | ]; 34 | 35 | const IconList = () => { 36 | return ( 37 | 42 | ); 43 | }; 44 | 45 | export default IconList; 46 | -------------------------------------------------------------------------------- /example/src/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useLoading } from '../../.'; 4 | 5 | const Spinner = ({ indicator }) => { 6 | const { containerProps, indicatorEl } = useLoading({ 7 | loading: true, 8 | indicator, 9 | }); 10 | return ( 11 |
  • 12 | {indicatorEl} 13 |
  • 14 | ); 15 | } 16 | 17 | export default Spinner; -------------------------------------------------------------------------------- /example/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | background: #005bea; 4 | background-image: linear-gradient(to top, #00c6fb 0%, #005bea 100%); 5 | background-attachment: fixed; 6 | } 7 | 8 | body { 9 | font-family: 'Inter', sans-serif; 10 | font-weight: 300; 11 | } 12 | 13 | h1 { 14 | font-size: 2rem; 15 | font-weight: 400; 16 | } 17 | 18 | section { 19 | display: block; 20 | width: 720px; 21 | margin: 5rem auto; 22 | } 23 | 24 | a { 25 | color: #FFFFFF; 26 | } 27 | 28 | ul { 29 | background: #FFFFFF; 30 | list-style: none; 31 | padding: 0; 32 | box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.37); 33 | display: flex; 34 | flex-wrap: wrap; 35 | } 36 | 37 | ul li { 38 | width: 180px; 39 | height: 180px; 40 | line-height: 176px; 41 | text-align: center; 42 | background-repeat: no-repeat; 43 | background-position: center; 44 | color: #fff; 45 | } 46 | 47 | ul li svg { 48 | vertical-align: middle; 49 | } 50 | 51 | li:nth-child(1) { 52 | background-color: #1ABC9C; 53 | } 54 | li:nth-child(2) { 55 | background-color: #34495E; 56 | } 57 | li:nth-child(3) { 58 | background-color: #F39C12; 59 | } 60 | li:nth-child(4) { 61 | background-color: #9B59B6; 62 | } 63 | li:nth-child(5) { 64 | background-color: #3498DB; 65 | } 66 | li:nth-child(6) { 67 | background-color: #E74C3C; 68 | } 69 | li:nth-child(7) { 70 | background-color: #2ECC71; 71 | } 72 | li:nth-child(8) { 73 | background-color: #2C3E50; 74 | } 75 | li:nth-child(9) { 76 | background-color: #F1C40F; 77 | } 78 | li:nth-child(10) { 79 | background-color: #8E44AD; 80 | } 81 | li:nth-child(11) { 82 | background-color: #2980B9; 83 | } 84 | li:nth-child(12) { 85 | background-color: #E74C3C; 86 | } 87 | 88 | code { 89 | background-color: #fff; 90 | padding: 1rem 1.5rem; 91 | display: block; 92 | } -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@agney/react-loading", 3 | "version": "0.1.2", 4 | "description": "Simple and Accessible loading indicators with React.", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "typings": "dist/index.d.ts", 8 | "files": [ 9 | "dist", 10 | "src" 11 | ], 12 | "sideEffects": false, 13 | "engines": { 14 | "node": ">=10" 15 | }, 16 | "author": { 17 | "name": "Boy with Silver Wings", 18 | "email": "agney@outlook.in" 19 | }, 20 | "scripts": { 21 | "start": "tsdx watch", 22 | "build": "tsdx build", 23 | "test": "tsdx test --passWithNoTests", 24 | "lint": "tsdx lint --fix", 25 | "prepare": "tsdx build", 26 | "size": "size-limit", 27 | "analyze": "size-limit --why", 28 | "release": "np", 29 | "postrelease": "gh-pages -d example/dist" 30 | }, 31 | "peerDependencies": { 32 | "react": ">=16" 33 | }, 34 | "prettier": { 35 | "printWidth": 80, 36 | "semi": true, 37 | "singleQuote": true, 38 | "trailingComma": "es5" 39 | }, 40 | "module": "dist/react-loading.esm.js", 41 | "size-limit": [ 42 | { 43 | "path": "dist/react-loading.cjs.production.min.js", 44 | "limit": "10 KB" 45 | }, 46 | { 47 | "path": "dist/react-loading.esm.js", 48 | "limit": "10 KB" 49 | } 50 | ], 51 | "devDependencies": { 52 | "@babel/core": "^7.11.6", 53 | "@size-limit/preset-small-lib": "^4.6.0", 54 | "@svgr/rollup": "^5.4.0", 55 | "@testing-library/jest-dom": "^5.11.4", 56 | "@testing-library/react": "^11.0.4", 57 | "@testing-library/react-hooks": "^3.4.2", 58 | "@types/react": "^16.9.49", 59 | "@types/react-dom": "^16.9.8", 60 | "babel-loader": "^8.1.0", 61 | "gh-pages": "^3.1.0", 62 | "husky": "^4.3.0", 63 | "lint-staged": "^10.4.0", 64 | "np": "^6.5.0", 65 | "prettier": "^2.1.2", 66 | "react": "^16.13.1", 67 | "react-dom": "^16.13.1", 68 | "react-is": "^16.13.1", 69 | "react-test-renderer": "^16.13.1", 70 | "size-limit": "^4.6.0", 71 | "tsdx": "^0.14.0", 72 | "tslib": "^2.0.1", 73 | "typescript": "^4.0.3" 74 | }, 75 | "husky": { 76 | "hooks": { 77 | "pre-commit": "lint-staged" 78 | } 79 | }, 80 | "lint-staged": { 81 | "*.{js,css,md}": "prettier --write" 82 | }, 83 | "dependencies": { 84 | "tiny-invariant": "^1.1.0" 85 | }, 86 | "publishConfig": { 87 | "access": "public" 88 | }, 89 | "jest": { 90 | "globals": { 91 | "__DEV__": "boolean" 92 | }, 93 | "collectCoverageFrom": [ 94 | "src/**/*.{ts,tsx}" 95 | ], 96 | "moduleNameMapper": { 97 | "\\.(svg)$": "/__mocks__/fileMock.js" 98 | } 99 | }, 100 | "repository": { 101 | "type": "git", 102 | "url": "git+https://github.com/agneym/react-loading.git" 103 | }, 104 | "keywords": [ 105 | "react", 106 | "typescript", 107 | "loader", 108 | "spinner" 109 | ], 110 | "bugs": { 111 | "url": "https://github.com/agneym/react-loading/issues" 112 | }, 113 | "homepage": "https://agneym.github.io/react-loading/" 114 | } 115 | -------------------------------------------------------------------------------- /src/CommonLoaderProps.ts: -------------------------------------------------------------------------------- 1 | export interface CommonLoaderProps { 2 | /** Width of the loader element */ 3 | width?: string | number; 4 | /** Height of loader element */ 5 | height?: string | number; 6 | /** fill or stroke of loader - set to currentColor by default */ 7 | color?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/LoaderContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | ReactElement, 4 | ReactNode, 5 | useContext, 6 | useMemo, 7 | } from 'react'; 8 | 9 | interface ContextProps { 10 | indicator: ReactElement | null; 11 | } 12 | 13 | interface ProviderProps extends ContextProps { 14 | children: ReactNode; 15 | } 16 | 17 | const defaultValue = { 18 | indicator: null, 19 | }; 20 | 21 | const LoaderContext = createContext(defaultValue); 22 | export const useLoaderContext = () => useContext(LoaderContext); 23 | 24 | export const LoaderProvider = ({ indicator, children }: ProviderProps) => { 25 | const value = useMemo( 26 | () => ({ 27 | indicator: indicator, 28 | }), 29 | [indicator] 30 | ); 31 | 32 | return ( 33 | {children} 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { useLoading } from './useLoading'; 2 | export { LoaderProvider } from './LoaderContext'; 3 | 4 | // Loaders 5 | export { default as Audio } from './svg-loaders/audio.svg'; 6 | export { default as BallTriangle } from './svg-loaders/ball-triangle.svg'; 7 | export { default as Bars } from './svg-loaders/bars.svg'; 8 | export { default as Circles } from './svg-loaders/circles.svg'; 9 | export { default as Grid } from './svg-loaders/grid.svg'; 10 | export { default as Hearts } from './svg-loaders/hearts.svg'; 11 | export { default as Oval } from './svg-loaders/oval.svg'; 12 | export { default as Puff } from './svg-loaders/puff.svg'; 13 | export { default as Rings } from './svg-loaders/rings.svg'; 14 | export { default as SpinningCircles } from './svg-loaders/spinning-circles.svg'; 15 | export { default as TailSpin } from './svg-loaders/tail-spin.svg'; 16 | export { default as ThreeDots } from './svg-loaders/three-dots.svg'; 17 | -------------------------------------------------------------------------------- /src/svg-loaders/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 15 | 16 | 17 | 21 | 22 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/svg-loaders/ball-triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 38 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/svg-loaders/bars.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 13 | 17 | 21 | 22 | 23 | 27 | 31 | 32 | 33 | 37 | 41 | 42 | 43 | 47 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/svg-loaders/circles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/svg-loaders/grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 13 | 14 | 15 | 19 | 20 | 21 | 25 | 26 | 27 | 31 | 32 | 33 | 37 | 38 | 39 | 43 | 44 | 45 | 49 | 50 | 51 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/svg-loaders/hearts.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/svg-loaders/oval.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/svg-loaders/puff.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/svg-loaders/rings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 14 | 18 | 19 | 20 | 25 | 29 | 33 | 34 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/svg-loaders/spinning-circles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 18 | 22 | 23 | 24 | 28 | 29 | 30 | 34 | 35 | 36 | 40 | 41 | 42 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/svg-loaders/tail-spin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/svg-loaders/three-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const content: any; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /src/useLoading.tsx: -------------------------------------------------------------------------------- 1 | import { cloneElement, ReactElement, isValidElement } from 'react'; 2 | import invariant from 'tiny-invariant'; 3 | 4 | import { useLoaderContext } from './LoaderContext'; 5 | 6 | interface Props { 7 | loading?: boolean; 8 | indicator?: ReactElement | null; 9 | loaderProps?: { 10 | /** Description of progressbar and what it is loading */ 11 | valueText?: string; 12 | }; 13 | } 14 | 15 | type AriaLive = 'off' | 'assertive' | 'polite' | undefined; 16 | 17 | /** 18 | * Hook returning Indicator element according to loading argument. 19 | * @example const { containerProps, indicatorEl } = useLoading({ loading: true }) 20 | */ 21 | export function useLoading({ 22 | loading = false, 23 | indicator, 24 | loaderProps = {}, 25 | }: Props) { 26 | const containerProps = { 27 | 'aria-busy': loading, 28 | 'aria-live': 'polite' as AriaLive, 29 | }; 30 | 31 | const loaderContext = useLoaderContext(); 32 | const indicatorEl = indicator ?? loaderContext?.indicator; 33 | invariant( 34 | isValidElement(indicatorEl), 35 | 'Expected a valid React element as indicator' 36 | ); 37 | 38 | const accessibleLoaderProps = (() => { 39 | const { valueText, ...rest } = loaderProps; 40 | return { 41 | role: 'progressbar', 42 | 'aria-valuetext': valueText, 43 | ...rest, 44 | }; 45 | })(); 46 | const accessibleIndicator = indicatorEl 47 | ? cloneElement(indicatorEl, accessibleLoaderProps) 48 | : null; 49 | 50 | return { 51 | containerProps, 52 | indicatorEl: loading ? accessibleIndicator : null, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /test/useLoading.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, ReactNode } from 'react'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import { render, screen } from '@testing-library/react'; 4 | import '@testing-library/jest-dom/extend-expect'; 5 | 6 | import { useLoading, LoaderProvider } from '../src'; 7 | 8 | describe('useLoading', () => { 9 | test('renders element passed in', () => { 10 | let loading = false; 11 | const { result, rerender } = renderHook(() => 12 | useLoading({ 13 | loading, 14 | indicator:

    loader

    , 15 | }) 16 | ); 17 | 18 | expect(result.current.containerProps['aria-busy']).toBe(false); 19 | expect(result.current.indicatorEl).toBeFalsy(); 20 | 21 | loading = true; 22 | rerender(); 23 | 24 | expect(result.current.containerProps['aria-busy']).toBe(true); 25 | 26 | render(result.current.indicatorEl as ReactElement); 27 | expect(screen.getByText('loader')).toBeInTheDocument(); 28 | }); 29 | 30 | test('renders element from context if no element passed in', () => { 31 | const wrapper = ({ children }: { children: ReactNode }) => ( 32 | context loader}> 33 | {children} 34 | 35 | ); 36 | let indicator: ReactElement | undefined; 37 | 38 | const { result, rerender } = renderHook( 39 | () => 40 | useLoading({ 41 | loading: true, 42 | indicator, 43 | }), 44 | { wrapper } 45 | ); 46 | 47 | const { unmount } = render(result.current.indicatorEl as ReactElement); 48 | expect(screen.getByText('context loader')).toBeInTheDocument(); 49 | unmount(); 50 | 51 | indicator =

    custom loader

    ; 52 | rerender(); 53 | 54 | render(result.current.indicatorEl as ReactElement); 55 | expect(screen.getByText('custom loader')).toBeInTheDocument(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | const svgr = require('@svgr/rollup').default; 2 | 3 | module.exports = { 4 | rollup(config) { 5 | config.plugins.push(svgr()); 6 | return config; 7 | }, 8 | }; --------------------------------------------------------------------------------