├── .enzyme.js
├── .eslintignore
├── .eslintrc.json
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug--documentation.md
│ ├── bug--functionality.md
│ ├── bug--styling.md
│ ├── question.md
│ ├── request--component.md
│ └── request--functionality.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── deploy.yaml
│ └── lint_and_test.yaml
├── .gitignore
├── .postcssrc.js
├── .prettierignore
├── .prettierrc.yaml
├── .storybook
├── main.ts
├── manager-head.html
├── manager.ts
├── preview.tsx
├── style.scss
├── theme.ts
└── withTheme.tsx
├── .versionrc.js
├── CHANGELOG.md
├── LICENSE
├── README.md
├── __mocks__
└── fileMock.js
├── commitlint.config.js
├── configs
├── tsbuild.json
├── tsbuild_cjs.json
├── tsbuild_esm.json
├── tsbuild_types.json
└── tsconfig.json
├── jest.config.js
├── package.json
├── public
├── favicon.ico
└── favicon.png
├── scripts
├── build_package.sh
├── make
│ ├── README.md
│ ├── constants.ts
│ ├── file-content
│ │ ├── component.ts
│ │ ├── componentIndex.ts
│ │ ├── componentReadme.ts
│ │ ├── componentRef.ts
│ │ ├── componentStory.ts
│ │ ├── componentStyle.ts
│ │ └── componentTest.ts
│ ├── generateFiles.ts
│ ├── index.ts
│ ├── optionPicker.ts
│ ├── summaryLog.ts
│ ├── types.ts
│ └── utils
│ │ ├── fileUtils.ts
│ │ ├── logUtils.ts
│ │ ├── pathUtils.ts
│ │ └── stringUtils.ts
├── parse_scss.js
└── verify-package-json.js
├── src
├── __stories__
│ ├── 1_basics
│ │ ├── 1_introduction.stories.mdx
│ │ ├── 2_getting-started.stories.mdx
│ │ └── Version.tsx
│ ├── 2_style
│ │ ├── 1_colors.stories.mdx
│ │ ├── 2_variables.stories.mdx
│ │ ├── 3_mixins.stories.mdx
│ │ ├── 4_breakpoints.stories.mdx
│ │ ├── generated
│ │ │ ├── breakpointRange.ts
│ │ │ ├── breakpoints.ts
│ │ │ ├── mixins.ts
│ │ │ └── styleVariables.ts
│ │ ├── styleBreakpoints.tsx
│ │ ├── styleComponents.tsx
│ │ ├── styles.ts
│ │ └── utils.tsx
│ ├── 3_hooks
│ │ ├── useFocusTrap.stories.mdx
│ │ ├── useFocusTrapHelpers.ts
│ │ ├── useManagedFiles.stories.mdx
│ │ ├── useManagedFilesHelpers.ts
│ │ ├── useToast.stories.mdx
│ │ └── useToastHelpers.ts
│ └── 4_data
│ │ ├── file.stories.mdx
│ │ ├── fileHelpers.ts
│ │ ├── memoized-promise.stories.mdx
│ │ └── memoizedPromiseHelpers.ts
├── components
│ ├── Button
│ │ ├── Button.stories.tsx
│ │ ├── Button.test.tsx
│ │ ├── Button.tsx
│ │ ├── README.md
│ │ ├── _Button.scss
│ │ ├── __snapshots__
│ │ │ └── Button.test.tsx.snap
│ │ └── index.ts
│ ├── ButtonGroup
│ │ ├── ButtonGroup.stories.tsx
│ │ ├── ButtonGroup.tsx
│ │ ├── README.md
│ │ ├── _ButtonGroup.scss
│ │ └── index.ts
│ ├── Choice
│ │ ├── Choice.stories.tsx
│ │ ├── Choice.test.tsx
│ │ ├── Choice.tsx
│ │ ├── README.md
│ │ ├── _Choice.scss
│ │ ├── __snapshots__
│ │ │ └── Choice.test.tsx.snap
│ │ └── index.ts
│ ├── ClickableDiv
│ │ ├── ClickableDiv.stories.tsx
│ │ ├── ClickableDiv.test.tsx
│ │ ├── ClickableDiv.tsx
│ │ ├── README.md
│ │ ├── _ClickableDiv.scss
│ │ ├── __snapshots__
│ │ │ └── ClickableDiv.test.tsx.snap
│ │ └── index.ts
│ ├── DndMultiProvider
│ │ ├── DndMultiProvider.tsx
│ │ └── index.ts
│ ├── DragLayer
│ │ ├── DragLayer.stories.tsx
│ │ ├── DragLayer.tsx
│ │ ├── README.md
│ │ ├── _DragLayer.scss
│ │ └── index.ts
│ ├── Draggable
│ │ ├── Draggable.stories.tsx
│ │ ├── Draggable.tsx
│ │ ├── README.md
│ │ ├── _Draggable.scss
│ │ └── index.ts
│ ├── EditableText
│ │ ├── EditableText.stories.tsx
│ │ ├── EditableText.test.tsx
│ │ ├── EditableText.tsx
│ │ ├── README.md
│ │ ├── _EditableText.scss
│ │ ├── __snapshots__
│ │ │ └── EditableText.test.tsx.snap
│ │ └── index.ts
│ ├── FileOrganizer
│ │ ├── FileOrganizer.stories.tsx
│ │ ├── FileOrganizer.tsx
│ │ ├── MemoAutoSizer.tsx
│ │ ├── README.md
│ │ ├── _FileOrganizer.scss
│ │ └── index.ts
│ ├── FilePicker
│ │ ├── FilePicker.stories.tsx
│ │ ├── FilePicker.test.tsx
│ │ ├── FilePicker.tsx
│ │ ├── README.md
│ │ ├── _FilePicker.scss
│ │ ├── __snapshots__
│ │ │ └── FilePicker.test.tsx.snap
│ │ └── index.ts
│ ├── FilePlaceholder
│ │ ├── FilePlaceholder.stories.tsx
│ │ ├── FilePlaceholder.test.tsx
│ │ ├── FilePlaceholder.tsx
│ │ ├── README.md
│ │ ├── _FilePlaceholder.scss
│ │ ├── __snapshots__
│ │ │ └── FilePlaceholder.test.tsx.snap
│ │ └── index.ts
│ ├── FileSkeleton
│ │ ├── FileSkeleton.stories.tsx
│ │ ├── FileSkeleton.test.tsx
│ │ ├── FileSkeleton.tsx
│ │ ├── README.md
│ │ ├── _FileSkeleton.scss
│ │ ├── __snapshots__
│ │ │ └── FileSkeleton.test.tsx.snap
│ │ └── index.ts
│ ├── FocusTrap
│ │ ├── FocusTrap.stories.scss
│ │ ├── FocusTrap.stories.tsx
│ │ ├── FocusTrap.tsx
│ │ ├── README.md
│ │ ├── _FocusTrap.scss
│ │ └── index.ts
│ ├── Icon
│ │ ├── Icon.stories.tsx
│ │ ├── Icon.test.tsx
│ │ ├── Icon.tsx
│ │ ├── README.md
│ │ ├── _Icon.scss
│ │ ├── __snapshots__
│ │ │ └── Icon.test.tsx.snap
│ │ └── index.ts
│ ├── IconButton
│ │ ├── IconButton.stories.tsx
│ │ ├── IconButton.test.tsx
│ │ ├── IconButton.tsx
│ │ ├── README.md
│ │ ├── _IconButton.scss
│ │ ├── __snapshots__
│ │ │ └── IconButton.test.tsx.snap
│ │ └── index.ts
│ ├── Image
│ │ ├── Image.stories.tsx
│ │ ├── Image.test.tsx
│ │ ├── Image.tsx
│ │ ├── README.md
│ │ ├── _Image.scss
│ │ ├── __snapshots__
│ │ │ └── Image.test.tsx.snap
│ │ └── index.ts
│ ├── Input
│ │ ├── Input.stories.tsx
│ │ ├── Input.test.tsx
│ │ ├── Input.tsx
│ │ ├── README.md
│ │ ├── _Input.scss
│ │ ├── __snapshots__
│ │ │ └── Input.test.tsx.snap
│ │ └── index.ts
│ ├── Label
│ │ ├── Label.stories.tsx
│ │ ├── Label.test.tsx
│ │ ├── Label.tsx
│ │ ├── README.md
│ │ ├── _Label.scss
│ │ ├── __snapshots__
│ │ │ └── Label.test.tsx.snap
│ │ └── index.ts
│ ├── Modal
│ │ ├── Modal.stories.tsx
│ │ ├── Modal.test.tsx
│ │ ├── Modal.tsx
│ │ ├── README.md
│ │ ├── _Modal.scss
│ │ ├── __snapshots__
│ │ │ └── Modal.test.tsx.snap
│ │ └── index.ts
│ ├── Overlay
│ │ ├── Overlay.scss
│ │ ├── Overlay.stories.tsx
│ │ ├── Overlay.tsx
│ │ ├── README.md
│ │ └── index.ts
│ ├── Spinner
│ │ ├── README.md
│ │ ├── Spinner.stories.tsx
│ │ ├── Spinner.test.tsx
│ │ ├── Spinner.tsx
│ │ ├── _Spinner.scss
│ │ ├── __snapshots__
│ │ │ └── Spinner.test.tsx.snap
│ │ └── index.ts
│ ├── Thumbnail
│ │ ├── README.md
│ │ ├── Thumbnail.stories.tsx
│ │ ├── Thumbnail.test.tsx
│ │ ├── Thumbnail.tsx
│ │ ├── _Thumbnail.scss
│ │ ├── __snapshots__
│ │ │ └── Thumbnail.test.tsx.snap
│ │ ├── index.ts
│ │ └── thumbnailFocusObservable.ts
│ ├── ThumbnailDragLayer
│ │ ├── README.md
│ │ ├── ThumbnailDragLayer.stories.tsx
│ │ ├── ThumbnailDragLayer.test.tsx
│ │ ├── ThumbnailDragLayer.tsx
│ │ ├── _ThumbnailDragLayer.scss
│ │ ├── __snapshots__
│ │ │ └── ThumbnailDragLayer.test.tsx.snap
│ │ └── index.ts
│ ├── ThumbnailSkeleton
│ │ ├── README.md
│ │ ├── ThumbnailSkeleton.stories.tsx
│ │ ├── ThumbnailSkeleton.tsx
│ │ ├── _ThumbnailSkeleton.scss
│ │ └── index.ts
│ ├── Toast
│ │ ├── README.md
│ │ ├── Toast.stories.tsx
│ │ ├── Toast.test.tsx
│ │ ├── Toast.tsx
│ │ ├── _Toast.scss
│ │ ├── __snapshots__
│ │ │ └── Toast.test.tsx.snap
│ │ └── index.ts
│ ├── ToastProvider
│ │ ├── README.md
│ │ ├── ToastProvider.stories.tsx
│ │ ├── ToastProvider.tsx
│ │ ├── _ToastProvider.scss
│ │ └── index.ts
│ ├── ToolButton
│ │ ├── README.md
│ │ ├── ToolButton.stories.tsx
│ │ ├── ToolButton.test.tsx
│ │ ├── ToolButton.tsx
│ │ ├── _ToolButton.scss
│ │ ├── __snapshots__
│ │ │ └── ToolButton.test.tsx.snap
│ │ └── index.ts
│ └── index.ts
├── data
│ ├── file.ts
│ ├── futurable.ts
│ ├── index.ts
│ └── memoizedPromise.ts
├── hooks
│ ├── index.ts
│ ├── useAccessibleFocus.ts
│ ├── useCurrentRef.ts
│ ├── useFile.ts
│ ├── useFileSubscribe.ts
│ ├── useFocus.ts
│ ├── useFocusTrap.ts
│ ├── useID.ts
│ ├── useKeyForClick.ts
│ ├── useManagedFiles.ts
│ ├── useOnClick.ts
│ ├── useToast.ts
│ └── useUnmountDelay.ts
├── icons
│ ├── Check.tsx
│ ├── ChevronDown.tsx
│ ├── Circle.tsx
│ ├── Close.tsx
│ ├── Download.tsx
│ ├── Error.tsx
│ ├── Hide.tsx
│ ├── Info.tsx
│ ├── Menu.tsx
│ ├── MultiPage.tsx
│ ├── README.md
│ ├── RotateRight.tsx
│ ├── Search.tsx
│ ├── Show.tsx
│ ├── SinglePage.tsx
│ ├── Success.tsx
│ ├── Warning.tsx
│ └── index.ts
├── index.scss
├── index.ts
├── internal.d.ts
├── storybook-helpers
│ ├── action
│ │ └── action.ts
│ ├── data
│ │ └── files.ts
│ ├── images
│ │ ├── pdf-preview-2.png
│ │ └── pdf-preview.png
│ ├── knobs
│ │ ├── forwardAction.ts
│ │ ├── integer.ts
│ │ └── selectIcon.ts
│ ├── mdx
│ │ ├── LinkTo.tsx
│ │ └── index.ts
│ └── theme
│ │ └── font.ts
├── styles
│ ├── _base.scss
│ ├── _breakpoints.scss
│ ├── _globals.scss
│ ├── _mixins.scss
│ ├── _theme.scss
│ └── _variables.scss
├── types.d.ts
├── utils
│ ├── arrayUtils.ts
│ ├── constantUtils.ts
│ ├── debugUtils.ts
│ ├── domUtils.ts
│ ├── fileUtils.ts
│ ├── gridUtils.ts
│ ├── idUtils.ts
│ ├── index.ts
│ ├── typeUtils.ts
│ └── webviewerUtils.ts
└── work
│ └── GlobalQueue.ts
├── tsconfig.json
├── tsconfig.lint.json
├── webpack.config.ts
├── webpack.style.config.ts
└── yarn.lock
/.enzyme.js:
--------------------------------------------------------------------------------
1 | const { configure } = require('enzyme');
2 | const EnzymeAdapter = require('enzyme-adapter-react-16');
3 | configure({ adapter: new EnzymeAdapter() });
4 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Ignore build folders
2 | dist
3 | lib
4 | storybook-build
5 | storybook-static
6 |
7 | # Ignore node modules folder
8 | node_modules
9 |
10 | src/types.d.ts
11 | # Those files have linting issues resulted from CRA-eject. CRA-eject can not be reversed and if there are issues specific to react-scripts versions then it's extremely difficult to fix.
12 | src/utils/arrayUtils.ts
13 | src/utils/webviewerUtils.ts
14 | src/work/GlobalQueue.ts
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "parserOptions": {
4 | "project": "./tsconfig.lint.json"
5 | },
6 | "plugins": ["react-hooks", "prettier"],
7 | "extends": [
8 | "plugin:jsx-a11y/recommended",
9 | "plugin:jest/recommended",
10 | "plugin:jest/style",
11 | "eslint:recommended",
12 | "plugin:react/recommended",
13 | "plugin:@typescript-eslint/eslint-recommended",
14 | "plugin:@typescript-eslint/recommended",
15 | // Prettier overrides
16 | "prettier",
17 | "prettier/react",
18 | "prettier/@typescript-eslint"
19 | ],
20 | "settings": {
21 | "react": {
22 | "version": "detect"
23 | }
24 | },
25 | "env": {
26 | "browser": true,
27 | "es6": true,
28 | "commonjs": true,
29 | "node": true,
30 | "jest": true
31 | },
32 | "rules": {
33 | // Off
34 | "react/prop-types": "off",
35 | "react/display-name": "off",
36 | "@typescript-eslint/no-explicit-any": "off",
37 | "@typescript-eslint/no-empty-function": "off",
38 | "@typescript-eslint/explicit-function-return-type": "off",
39 | "@typescript-eslint/no-var-requires": "off",
40 | "@typescript-eslint/no-empty-interface": "off",
41 | "@typescript-eslint/camelcase": "off",
42 | "@typescript-eslint/no-triple-slash-reference": "off",
43 | "@typescript-eslint/no-non-null-assertion": "off",
44 | "@typescript-eslint/ban-ts-comment": "off",
45 | "@typescript-eslint/no-use-before-define": "off",
46 | "@typescript-eslint/ban-ts-ignore": "off",
47 | // Consider turning on eventually... lots of warnings though.
48 | "@typescript-eslint/explicit-module-boundary-types": "off",
49 | // On.
50 | "prettier/prettier": "error",
51 | "no-empty": [
52 | "error",
53 | {
54 | "allowEmptyCatch": true
55 | }
56 | ],
57 | "prefer-const": "error",
58 | "react-hooks/rules-of-hooks": "error",
59 | "react-hooks/exhaustive-deps": "error",
60 | "@typescript-eslint/no-unused-vars": [
61 | "error",
62 | {
63 | "argsIgnorePattern": "^_"
64 | }
65 | ],
66 | "@typescript-eslint/unbound-method": "error"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Global, lowest priority (will be overridden by any below).
2 | * @liamross
3 |
4 | # GITHUB: Actions, templates, etc.
5 | /.github @liamross
6 |
7 | # BUILD: Build and bundling scripts and configs (override non-build scripts below).
8 | /scripts/ @liamross
9 | /configs/ @liamross
10 | /.postcssrc.js @liamross
11 | /webpack.config.ts @liamross
12 |
13 | # RELEASES: Commit, Changelog, and GitHub release preferences.
14 | /commitlint.config.js @liamross
15 | /.versionrc.js @liamross
16 | /CHANGELOG.md @liamross
17 |
18 | # DOCUMENTATION: Storybook configuration directories.
19 | /.storybook/ @liamross
20 | /public/ @liamross
21 |
22 | # TESTING: Testing configuration.
23 | /jest.config.js @liamross
24 | /.enzyme.js @liamross
25 |
26 | # LINTING: Linters and formatters.
27 | /.prettier* @liamross
28 | /.eslint* @liamross
29 |
30 | # SCRIPT(Make): Make script and utilities.
31 | /scripts/make/ @liamross
32 |
33 | # SCRIPT(Sass): Sass AST parser and docs generator.
34 | /scripts/parse_scss.js @liamross
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug--documentation.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Bug: Documentation'
3 | about: Report an issue with the documentation of a component
4 | title: ''
5 | labels: "\U0001F4D6 documentation bug"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Describe the bug
11 |
12 |
13 |
14 | ### To Reproduce
15 |
16 |
17 |
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | ### Expected behavior
24 |
25 |
26 |
27 | ### Screenshots
28 |
29 |
30 |
31 | ### Device
32 |
33 | **Desktop:**
34 |
35 | - OS: [e.g. iOS]
36 | - Browser [e.g. chrome, safari]
37 | - Version [e.g. 22]
38 |
39 | **Smartphone:**
40 |
41 | - Device: [e.g. iPhone6]
42 | - OS: [e.g. iOS8.1]
43 | - Browser [e.g. stock browser, safari]
44 | - Version [e.g. 22]
45 |
46 | ### Additional context
47 |
48 |
49 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug--functionality.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Bug: Functionality'
3 | about: Report an issue with props or usability of a component
4 | title: ''
5 | labels: "⚙️ functionality bug"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Describe the bug
11 |
12 |
13 |
14 | ### To Reproduce
15 |
16 |
17 |
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | ### Expected behavior
24 |
25 |
26 |
27 | ### Screenshots
28 |
29 |
30 |
31 | ### Device
32 |
33 | **Desktop:**
34 |
35 | - OS: [e.g. iOS]
36 | - Browser [e.g. chrome, safari]
37 | - Version [e.g. 22]
38 |
39 | **Smartphone:**
40 |
41 | - Device: [e.g. iPhone6]
42 | - OS: [e.g. iOS8.1]
43 | - Browser [e.g. stock browser, safari]
44 | - Version [e.g. 22]
45 |
46 | ### Additional context
47 |
48 |
49 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug--styling.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Bug: Styling'
3 | about: Report an issue with the style of a component
4 | title: ''
5 | labels: "\U0001F3A8 style bug"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### Describe the bug
11 |
12 |
13 |
14 | ### To Reproduce
15 |
16 |
17 |
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | ### Expected behavior
24 |
25 |
26 |
27 | ### Screenshots
28 |
29 |
30 |
31 | ### Device
32 |
33 | **Desktop:**
34 |
35 | - OS: [e.g. iOS]
36 | - Browser [e.g. chrome, safari]
37 | - Version [e.g. 22]
38 |
39 | **Smartphone:**
40 |
41 | - Device: [e.g. iPhone6]
42 | - OS: [e.g. iOS8.1]
43 | - Browser [e.g. stock browser, safari]
44 | - Version [e.g. 22]
45 |
46 | ### Additional context
47 |
48 |
49 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question
3 | about: Ask a question about a component, functionality, or anything
4 | title: ''
5 | labels: "\U0001F64B question"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### My question is
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/request--component.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Request: Component'
3 | about: Request a new component
4 | title: ''
5 | labels: "\U0001F9E9 component request"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### What is the component you would like
11 |
12 |
13 |
14 | ### Additional context
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/request--functionality.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Request: Functionality'
3 | about: Request functionality for an existing component or in general
4 | title: ''
5 | labels: "\U0001F69C functionality request"
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### What is the feature you would like
11 |
12 |
13 |
14 | ### Additional context
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
16 |
17 | ### Description
18 |
19 | 1. Some changes
20 | 2. Some other changes
21 |
22 | ### Related issues
23 |
24 | Fixes #SomeNumberHere
25 |
26 | ### Checklist
27 |
28 |
29 |
30 | - [ ] Added the title of this PR to the CHANGELOG
31 |
32 | - [ ] I added at least one functional test for the component
33 |
34 | - [ ] I edited the README to give more information on the component
35 |
36 | - [ ] I created knobs for every reasonable prop
37 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Lint and test components, then deploy
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'change_this_to_master_when_this_is_fixed'
7 | # - 'master'
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [12.x] # Add more if you want to test other node versions.
16 |
17 | steps:
18 | - name: Check out repository
19 | uses: actions/checkout@v1
20 |
21 | - name: Use Node version ${{ matrix.node-version }}
22 | uses: actions/setup-node@v1
23 | with:
24 | node-version: ${{ matrix.node-version }}
25 |
26 | # https://github.com/actions/cache/blob/master/examples.md#node---yarn
27 | - name: Get yarn cache directory
28 | id: yarn-cache
29 | run: echo "::set-output name=dir::$(yarn cache dir)"
30 |
31 | - name: Use yarn cache
32 | uses: actions/cache@v1
33 | with:
34 | path: ${{ steps.yarn-cache.outputs.dir }}
35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
36 | restore-keys: |
37 | ${{ runner.os }}-yarn-
38 |
39 | - name: Install dependencies
40 | run: yarn install
41 |
42 | - name: Lint components
43 | run: yarn lint
44 |
45 | - name: Test components
46 | run: yarn test
47 | env:
48 | CI: true
49 |
50 | - name: Deploy to GitHub pages
51 | run: npm run deploy-storybook -- --ci
52 | env:
53 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54 |
--------------------------------------------------------------------------------
/.github/workflows/lint_and_test.yaml:
--------------------------------------------------------------------------------
1 | name: Lint and test components
2 |
3 | on: push
4 | # Redo this once deploy script works.
5 | # on:
6 | # push:
7 | # branches:
8 | # - '*'
9 | # - '!master'
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [12.x] # Add more if you want to test other node versions.
18 |
19 | steps:
20 | - name: Check out repository
21 | uses: actions/checkout@v1
22 |
23 | - name: Use Node version ${{ matrix.node-version }}
24 | uses: actions/setup-node@v1
25 | with:
26 | node-version: ${{ matrix.node-version }}
27 |
28 | # https://github.com/actions/cache/blob/master/examples.md#node---yarn
29 | - name: Get yarn cache directory path
30 | id: yarn-cache-dir-path
31 | run: echo "::set-output name=dir::$(yarn cache dir)"
32 |
33 | - name: Use yarn cache
34 | uses: actions/cache@v1
35 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
36 | with:
37 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
38 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
39 | restore-keys: |
40 | ${{ runner.os }}-yarn-
41 |
42 | - name: Install dependencies
43 | run: yarn install
44 |
45 | - name: Lint components
46 | run: yarn lint
47 |
48 | - name: Test components
49 | run: yarn test
50 | env:
51 | CI: true
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 |
10 | # Package lock file
11 | package-lock.json
12 |
13 | # VSCode files
14 | .vscode
15 |
16 | # Builds
17 | dist
18 | lib
19 | docs
20 | .out
21 | storybook-static
22 |
23 | # Size json
24 | size.json
25 |
26 | # Runtime data
27 | pids
28 | *.pid
29 | *.seed
30 | *.pid.lock
31 |
32 | # Directory for instrumented libs generated by jscoverage/JSCover
33 | lib-cov
34 |
35 | # Coverage directory used by tools like istanbul
36 | coverage
37 |
38 | # nyc test coverage
39 | .nyc_output
40 |
41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42 | .grunt
43 |
44 | # Bower dependency directory (https://bower.io/)
45 | bower_components
46 |
47 | # node-waf configuration
48 | .lock-wscript
49 |
50 | # Compiled binary addons (https://nodejs.org/api/addons.html)
51 | build/Release
52 |
53 | # Dependency directories
54 | node_modules/
55 | jspm_packages/
56 |
57 | # TypeScript v1 declaration files
58 | typings/
59 |
60 | # Optional npm cache directory
61 | .npm
62 |
63 | # Optional eslint cache
64 | .eslintcache
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variables file
76 | .env
77 | .env.test
78 |
79 | # parcel-bundler cache (https://parceljs.org/)
80 | .cache
81 |
82 | # Other
83 | .DS_Store
84 |
85 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | module.exports = { plugins: [require('autoprefixer')] };
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore GitHub folder
2 | .github
3 |
4 | # Ignore build folders
5 | dist
6 | lib
7 | docs
8 | storybook-build
9 | storybook-static
10 |
11 | # Ignore node modules folder
12 | node_modules
13 | *.mdx
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | # Wrap markdown files at 80 chars for raw file readability.
2 | proseWrap: always
3 |
4 | # Use single quotes where possible, subjective preference.
5 | singleQuote: true
6 |
7 | # Trailing commas for improved diffs when using source control.
8 | trailingComma: all
9 |
10 | overrides:
11 | # TypeScript files benefit from more horizontal space, subjective preference.
12 | - files:
13 | - '*.ts'
14 | - '*.tsx'
15 | options:
16 | printWidth: 120
17 |
18 | # Some files don't need to be wrapped at 80 chars. List those here.
19 | - files:
20 | - CHANGELOG.md
21 | - release-logs/**/*.md
22 | options:
23 | proseWrap: never
24 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | stories: ['../src/**/*.stories.*'],
5 | addons: [
6 | '@storybook/addon-docs',
7 | '@storybook/addon-knobs',
8 | '@storybook/addon-actions',
9 | '@storybook/addon-viewport',
10 | '@storybook/addon-links',
11 | '@storybook/addon-google-analytics',
12 | ],
13 | webpackFinal: async (config: any) => {
14 | // Sass support with post-css for vendor prefixes.
15 | config.module.rules.push({
16 | test: /\.scss$/,
17 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
18 | include: path.resolve(__dirname, '../'),
19 | });
20 | return config;
21 | },
22 | presets: [
23 | {
24 | name: '@storybook/preset-typescript',
25 | options: {
26 | tsLoaderOptions: { ignoreDiagnostics: [7005] },
27 | include: [path.resolve(__dirname, '../')],
28 | },
29 | },
30 | ],
31 | };
32 |
--------------------------------------------------------------------------------
/.storybook/manager-head.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
--------------------------------------------------------------------------------
/.storybook/manager.ts:
--------------------------------------------------------------------------------
1 | import { addons } from '@storybook/addons';
2 |
3 | // Google Analytics ID
4 | // @ts-ignore
5 | window.STORYBOOK_GA_ID = 'UA-6566170-4';
6 |
7 | addons.setConfig({
8 | /**
9 | * show story component as full screen
10 | * @type {Boolean}
11 | */
12 | isFullscreen: false,
13 |
14 | /**
15 | * display panel that shows a list of stories
16 | * @type {Boolean}
17 | */
18 | showNav: true,
19 |
20 | /**
21 | * display panel that shows addon configurations
22 | * @type {Boolean}
23 | */
24 | showPanel: true,
25 |
26 | /**
27 | * where to show the addon panel
28 | * @type {('bottom'|'right')}
29 | */
30 | panelPosition: 'right',
31 |
32 | /**
33 | * sidebar tree animations
34 | * @type {Boolean}
35 | */
36 | sidebarAnimations: true,
37 |
38 | /**
39 | * enable/disable shortcuts
40 | * @type {Boolean}
41 | */
42 | enableShortcuts: true,
43 |
44 | /**
45 | * show/hide tool bar
46 | * @type {Boolean}
47 | */
48 | isToolshown: true,
49 |
50 | /**
51 | * id to select an addon panel
52 | * @type {String}
53 | */
54 | selectedPanel: undefined,
55 |
56 | previewTabs: {
57 | // the order of the tabs is configured by the order here
58 | canvas: { title: 'Playground' },
59 | 'storybook/docs/panel': { title: 'Info' },
60 | },
61 | });
62 |
--------------------------------------------------------------------------------
/.storybook/preview.tsx:
--------------------------------------------------------------------------------
1 | import { Description, Props, Subtitle, Title } from '@storybook/addon-docs/dist/blocks';
2 | import { withInfo } from '@storybook/addon-info';
3 | import { withKnobs } from '@storybook/addon-knobs';
4 | import { addDecorator, addParameters } from '@storybook/react';
5 | import React from 'react';
6 | import './style.scss';
7 | import theme from './theme';
8 | import { withTheme } from './withTheme';
9 |
10 | /* --- Add global decorators. --- */
11 |
12 | addDecorator(
13 | // TODO: Remove once features are supported in docs.
14 | // @ts-ignore
15 | withInfo({
16 | inline: true,
17 | header: false,
18 | maxPropArrayLength: 1,
19 | TableComponent: () => null,
20 | styles: (base: any) => ({
21 | ...base,
22 | // The wrapper around info.
23 | infoBody: { padding: 15, backgroundColor: 'rgb(51, 51, 51)' },
24 | // The wrapper around the story.
25 | infoStory: { padding: 40 },
26 | // Hide prop table header.
27 | propTableHead: { display: 'none' },
28 | }),
29 | }),
30 | );
31 | addDecorator(withKnobs);
32 | addDecorator(withTheme);
33 |
34 | /* --- Add global parameters. --- */
35 |
36 | addParameters({
37 | // TODO: Move `options` to `manager.js` once it's supported more.
38 | options: {
39 | theme,
40 | /**
41 | * display the top-level grouping as a "root" in the sidebar
42 | * @type {Boolean}
43 | */
44 | showRoots: true,
45 | },
46 | // Default to show "story" vs "docs" whenever story switches.
47 | viewMode: 'story',
48 | docs: {
49 | page: () => (
50 | <>
51 |
52 |
53 |
54 | {/* */}
55 |
56 | >
57 | ),
58 |
59 | // Since we do not add component description in code (instead inserting it
60 | // into a .md file) we extract it using the following.
61 | extractComponentDescription: (_c: unknown, { readme }: { readme: string }) => {
62 | if (readme) return readme;
63 | return null;
64 | },
65 |
66 | // prepareForInline: (s: StoryFn) => {s}
,
67 | },
68 | });
69 |
--------------------------------------------------------------------------------
/.storybook/theme.ts:
--------------------------------------------------------------------------------
1 | import { create } from '@storybook/theming';
2 | import font from '../src/storybook-helpers/theme/font';
3 |
4 | export default create({
5 | // Theme base
6 | base: 'dark',
7 |
8 | // Main colors
9 | colorPrimary: '#00a3e3',
10 | colorSecondary: '#00a3e3',
11 |
12 | // UI
13 | // appBg: 'white',
14 | // appContentBg: 'white',
15 | // appBorderColor: 'white',
16 | appBorderRadius: 4,
17 |
18 | // Typography
19 | fontBase: font.fontBase,
20 | fontCode: font.fontCode,
21 |
22 | // // Text colors
23 | textColor: 'rgba(240, 240, 255, 0.9)',
24 | textInverseColor: 'rgba(0, 0, 20, 0.9)',
25 |
26 | // Toolbar default and active colors
27 | barTextColor: 'rgba(240, 240, 255, 0.6)',
28 | barSelectedColor: '#00a3e3',
29 | // barBg: 'white',
30 |
31 | // // Form colors
32 | // inputBg: 'white',
33 | // inputBorder: 'white',
34 | inputTextColor: 'rgba(240, 240, 255, 0.9)',
35 | inputBorderRadius: 4,
36 |
37 | // Branding
38 | brandTitle: `${require('../package.json').version}`,
39 | brandUrl: 'https://github.com/PDFTron/webviewer-react-toolkit',
40 | brandImage: 'https://www.pdftron.com/brand-assets/pdftron-logo-blue.png',
41 | });
42 |
--------------------------------------------------------------------------------
/.storybook/withTheme.tsx:
--------------------------------------------------------------------------------
1 | import { StoryContext, StoryFn } from '@storybook/addons';
2 | import React, { useLayoutEffect, useRef, useState } from 'react';
3 |
4 | export function withTheme(storyFn: StoryFn, context: StoryContext) {
5 | return (
6 | <>
7 |
8 | {storyFn(context)}
9 | >
10 | );
11 | }
12 |
13 | export interface ThemeChangeButtonProps {
14 | className?: string;
15 | onClick?: (darkTheme: boolean) => void;
16 | id?: string;
17 | }
18 |
19 | export function isDarkThemeStored() {
20 | const storedTheme = localStorage.getItem('data-theme') || '';
21 | return storedTheme === 'dark';
22 | }
23 |
24 | export function ThemeChangeButton({ className, onClick, id }: ThemeChangeButtonProps) {
25 | const html = useRef(document.documentElement);
26 |
27 | const [darkTheme, setDarkTheme] = useState(isDarkThemeStored);
28 |
29 | useLayoutEffect(() => {
30 | const newTheme = darkTheme ? 'dark' : '';
31 | html.current.setAttribute('data-theme', newTheme);
32 | setTimeout(() => localStorage.setItem('data-theme', newTheme));
33 | }, [darkTheme]);
34 |
35 | const handleOnClick = () => {
36 | setDarkTheme((t) => {
37 | onClick?.(!t);
38 | return !t;
39 | });
40 | };
41 |
42 | return (
43 |
49 | {darkTheme ? '☀️' : '🌙'}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/.versionrc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This formats the auto-generated Changelog, and GitHub releases.
3 | */
4 | module.exports = {
5 | types: [
6 | /* --- Shown in Changelog. --- */
7 |
8 | // Affects the build system or external dependencies.
9 | { type: 'build', section: 'Build and Dependencies' },
10 | // Adds a new feature.
11 | { type: 'feat', section: 'Features' },
12 | // Solves a bug.
13 | { type: 'fix', section: 'Bug Fixes' },
14 | // Rewrites code without feature, performance or bug changes.
15 | { type: 'refactor', section: 'Refactors' },
16 | // Improves formatting, white-space.
17 | { type: 'style', section: 'Styles' },
18 |
19 | /* --- Hidden from Changelog. --- */
20 |
21 | // Other changes that don't modify src or test files.
22 | { type: 'chore', hidden: true },
23 | // Adds or alters documentation.
24 | { type: 'docs', hidden: true },
25 | // Reverts a previous commit.
26 | { type: 'revert', hidden: true },
27 | // Adds or modifies tests.
28 | { type: 'test', hidden: true },
29 | ],
30 | };
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 PDFTron Systems Inc. All rights reserved.
2 |
3 | WebViewer React Toolkit project/codebase or any derived works is only permitted
4 | in solutions with an active commercial PDFTron WebViewer license. For exact
5 | licensing terms please refer to your commercial WebViewer license. For use in
6 | other scenario, please contact sales@pdftron.com.
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WebViewer React Toolkit
2 |
3 | The WebViewer React Toolkit is a React component library contains various
4 | components that integrate with the
5 | [PDFTron WebViewer API](https://www.pdftron.com/documentation/web/).
6 |
7 | Check out the [documentation](https://pdftron.github.io/webviewer-react-toolkit)
8 | to get started with the toolkit!
9 |
10 | For a demo showcasing some of the functionality, along with step-by-step
11 | instructions on how to build it, check out the
12 | [demo repository](https://github.com/PDFTron/webviewer-react-toolkit-demo).
13 |
14 | > Note: file functionality within toolkit v7 and above requires WebViewer v7 or
15 | > higher. If you are on a previous version of WebViewer, you can use v0.6.0 of
16 | > the toolkit:
17 | >
18 | > ```sh
19 | > # Yarn
20 | > yarn add @pdftron/webviewer-react-toolkit@0.6.0
21 | >
22 | > # npm
23 | > npm install @pdftron/webviewer-react-toolkit@0.6.0
24 | > ```
25 |
26 | ## Installation
27 |
28 | You can install the toolkit from npm using your preferred package manager:
29 |
30 | ```bash
31 | # Yarn
32 | yarn add @pdftron/webviewer-react-toolkit
33 |
34 | # npm
35 | npm install @pdftron/webviewer-react-toolkit
36 | ```
37 |
38 | ## Using the toolkit
39 |
40 | Check the [introduction](https://pdftron.github.io/webviewer-react-toolkit) for
41 | information on using the toolkit.
42 |
43 | ## Contributing
44 |
45 | > Warning: There are issues building with versions of Node >=11. For now, use
46 | > [nvm](https://github.com/nvm-sh/nvm) to get latest node 10 version (works fine
47 | > with `v10.23.3`).
48 |
49 | ### To start up Storybook
50 |
51 | ```bash
52 | yarn # 1. To install dependencies (or `npm i`)
53 | yarn start # 2. To start the Storybook environment (or `npm start`)
54 | ```
55 |
56 | ### To test
57 |
58 | ```bash
59 | yarn test # Single test run (or `npm test`)
60 | yarn test --watch # Watch for changes (or `npm test -- --watch`)
61 | ```
62 |
63 | ### To lint
64 |
65 | ```bash
66 | yarn lint # Lint for errors (or `npm lint`)
67 | ```
68 |
69 | ## Goal
70 |
71 | To create a set of highly customizable components that take WebViewer documents
72 | (and other objects), and wrap them in specific functionality.
73 |
--------------------------------------------------------------------------------
/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This lints commits, and configures the `yarn commit` command.
3 | */
4 | module.exports = {
5 | extends: ['@commitlint/config-conventional'],
6 | rules: {
7 | // Allow any case for scope (ex: "FileOrganizer").
8 | 'scope-case': [0, 'always', 'lower-case'],
9 |
10 | // Limit possible types.
11 | 'type-enum': [
12 | 2,
13 | 'always',
14 | [
15 | '--- Shown in CHANGELOG: ---',
16 | 'build',
17 | 'feat',
18 | 'fix',
19 | 'refactor',
20 | 'style',
21 | '--- Hidden in CHANGELOG: ---',
22 | 'chore',
23 | 'docs',
24 | 'revert',
25 | 'test',
26 | ],
27 | ],
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/configs/tsbuild.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["../src"],
3 | "exclude": [
4 | "../node_modules",
5 | "../src/**/*.test.*",
6 | "../src/**/*.stories.*",
7 | "../src/storybook-helpers",
8 | "../src/**/__stories__"
9 | ],
10 | "extends": "./tsconfig.json"
11 | }
12 |
--------------------------------------------------------------------------------
/configs/tsbuild_cjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsbuild",
3 | "compilerOptions": {
4 | "declaration": false,
5 | "outDir": "../dist/cjs",
6 | "target": "ES5",
7 | "module": "CommonJS"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/configs/tsbuild_esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsbuild",
3 | "compilerOptions": {
4 | "declaration": false,
5 | "outDir": "../dist/esm",
6 | "target": "ES5",
7 | "module": "esnext"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/configs/tsbuild_types.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsbuild",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "outDir": "../lib",
6 | "target": "es2020",
7 | "module": "esnext"
8 | },
9 | "include": ["../src"],
10 | "exclude": [
11 | "../node_modules",
12 | "../src/**/*.test.*",
13 | "../src/**/*.stories.*",
14 | "../src/storybook-helpers",
15 | "../src/**/__stories__"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/configs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "jsx": "react",
5 | "skipLibCheck": true,
6 | "allowSyntheticDefaultImports": true,
7 | "baseUrl": "src",
8 | "emitDecoratorMetadata": true,
9 | "esModuleInterop": true,
10 | "experimentalDecorators": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "importHelpers": true,
13 | "incremental": true,
14 | "lib": ["esnext", "dom"],
15 | "moduleResolution": "node",
16 | "noImplicitAny": true,
17 | "noImplicitReturns": true,
18 | "noUnusedLocals": true,
19 | "resolveJsonModule": true,
20 | "strict": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
3 | // collectCoverage: true,
4 | // coverageDirectory: './coverage/',
5 |
6 | testMatch: ['**/?(*.)test.(ts|tsx)'],
7 |
8 | transform: {
9 | '^.+\\.(ts|tsx)$': 'ts-jest',
10 | '^.+\\.svg$': 'jest-svg-transformer',
11 | },
12 |
13 | moduleNameMapper: {
14 | '^.+\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
15 | '/__mocks__/fileMock.js',
16 | },
17 |
18 | // Setup Enzyme
19 | snapshotSerializers: ['enzyme-to-json/serializer'],
20 | setupFilesAfterEnv: ['/.enzyme.js'],
21 | };
22 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ApryseSDK/webviewer-react-toolkit/a96e3be370d02d7c9c7144eaf279c6d39f1574d2/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ApryseSDK/webviewer-react-toolkit/a96e3be370d02d7c9c7144eaf279c6d39f1574d2/public/favicon.png
--------------------------------------------------------------------------------
/scripts/build_package.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | set -e
3 | yarn tsc -p ./configs/tsbuild_types.json
4 | yarn tsc -p ./configs/tsbuild_esm.json
5 | yarn tsc -p ./configs/tsbuild_cjs.json
6 | wait
7 |
--------------------------------------------------------------------------------
/scripts/make/README.md:
--------------------------------------------------------------------------------
1 | # Make script
2 |
3 | The make script generates components along with all of their files and imports.
4 | The goal is to reduce the time spent bootstrapping and increase productivity.
5 |
6 | It's written in TypeScript in order to improve maintainability. This means it
7 | runs slower, and requires ts-node to run, but the cost is worth it.
8 |
9 | The main flow of this script is:
10 |
11 | 1. In `index.ts`, **begin the script**
12 |
13 | - This is the entry point, and it also outlines the entire flow of the
14 | script. It begins by calling the `optionPicker`.
15 |
16 | 1. In `optionPicker.ts`, **choose your options**
17 |
18 | - This is the file that collects user input and provides options for them.
19 |
20 | 1. In `summaryLog.ts`, **summarize the options**
21 |
22 | - This takes the options selected in `optionPicker.ts` and displays them.
23 |
24 | 1. In `index.ts`, **ask for approval**
25 |
26 | - Check if the user is ok with the options they selected, as well as the
27 | warnings they received. If approved, call `generateFiles`.
28 |
29 | 1. In `generateFiles.ts`, **generate all files and directories**
30 |
31 | - This builds out all of the files and directories based on the options
32 | selected. This is the most substantial file, as it needs to interact a lot
33 | with the file system.
34 |
35 | 1. It imports all of the file content strings from the `/file-content`
36 | directory in order to populate the file content.
37 | 1. It uses the `getPaths` utility to generate all of the paths to files and
38 | directories
39 | 1. It checks for any errors (ex: existing files or directories)
40 | 1. It generates files and directories, as well as modifies index files to
41 | include the imports
42 |
--------------------------------------------------------------------------------
/scripts/make/constants.ts:
--------------------------------------------------------------------------------
1 | export const STYLE_PREFIX = 'ui';
2 |
--------------------------------------------------------------------------------
/scripts/make/file-content/component.ts:
--------------------------------------------------------------------------------
1 | import { STYLE_PREFIX } from '../constants';
2 | import { pascalToCamel } from '../utils/stringUtils';
3 |
4 | export const component = (componentName: string) =>
5 | `import classnames from 'classnames';
6 | import React, { FC, ButtonHTMLAttributes } from 'react';
7 |
8 | export interface ${componentName}Props extends ButtonHTMLAttributes {
9 | // write your first prop
10 | }
11 |
12 | export const ${componentName}: FC<${componentName}Props> = ({ className, children, ...props }) => {
13 | const ${pascalToCamel(componentName)}Class = classnames(
14 | '${STYLE_PREFIX}__base ${STYLE_PREFIX}__${pascalToCamel(componentName)}',
15 | { '${STYLE_PREFIX}__${pascalToCamel(componentName)}--modifier': true },
16 | className,
17 | );
18 |
19 | return (
20 |
21 | {children}
22 |
23 | );
24 | };
25 | `;
26 |
--------------------------------------------------------------------------------
/scripts/make/file-content/componentIndex.ts:
--------------------------------------------------------------------------------
1 | export const componentIndex = (componentName: string) =>
2 | `export * from './${componentName}';
3 | `;
4 |
--------------------------------------------------------------------------------
/scripts/make/file-content/componentReadme.ts:
--------------------------------------------------------------------------------
1 | export const componentReadme = (componentName: string) =>
2 | `TODO: write a README for ${componentName}.
3 | `;
4 |
--------------------------------------------------------------------------------
/scripts/make/file-content/componentRef.ts:
--------------------------------------------------------------------------------
1 | import { STYLE_PREFIX } from '../constants';
2 | import { pascalToCamel } from '../utils/stringUtils';
3 |
4 | export const componentRef = (componentName: string) =>
5 | `import classnames from 'classnames';
6 | import React, { forwardRef, ButtonHTMLAttributes } from 'react';
7 |
8 | export interface ${componentName}Props extends ButtonHTMLAttributes {
9 | // write your first prop
10 | }
11 |
12 | export const ${componentName} = forwardRef(({ className, children, ...props }, ref) => {
13 | const ${pascalToCamel(componentName)}Class = classnames(
14 | '${STYLE_PREFIX}__base ${STYLE_PREFIX}__${pascalToCamel(componentName)}',
15 | { '${STYLE_PREFIX}__${pascalToCamel(componentName)}--modifier': true },
16 | className,
17 | );
18 |
19 | return (
20 |
21 | {children}
22 |
23 | );
24 | },
25 | );
26 | `;
27 |
--------------------------------------------------------------------------------
/scripts/make/file-content/componentStory.ts:
--------------------------------------------------------------------------------
1 | export const componentStory = (componentName: string) =>
2 | `import { action } from '../../storybook-helpers/action/action';
3 | import { boolean, text } from '@storybook/addon-knobs';
4 | import React from 'react';
5 | import { ${componentName} } from '../${componentName}';
6 | import readme from './README.md';
7 |
8 | export default { title: 'Components/${componentName}', component: ${componentName}, parameters: { readme } };
9 |
10 | export const Basic = () => <${componentName} onClick={action('onClick')}>{text('children', 'Hello, World!')}${componentName}>;
11 | `;
12 |
--------------------------------------------------------------------------------
/scripts/make/file-content/componentStyle.ts:
--------------------------------------------------------------------------------
1 | import { pascalToCamel } from '../utils/stringUtils';
2 | import { STYLE_PREFIX } from '../constants';
3 |
4 | export const componentStyle = (componentName: string) =>
5 | `.${STYLE_PREFIX}__${pascalToCamel(componentName)} {
6 | // TODO: write styles for ${componentName}.
7 | }
8 | `;
9 |
--------------------------------------------------------------------------------
/scripts/make/file-content/componentTest.ts:
--------------------------------------------------------------------------------
1 | import { STYLE_PREFIX } from '../constants';
2 | import { pascalToCamel } from '../utils/stringUtils';
3 |
4 | export const componentTest = (componentName: string) =>
5 | `import { mount, shallow } from 'enzyme';
6 | import React from 'react';
7 | import { spy } from 'sinon';
8 | import { ${componentName} } from '../${componentName}';
9 |
10 | describe('${componentName} component', () => {
11 | it('renders its contents', () => {
12 | const ${pascalToCamel(componentName)} = shallow(<${componentName} />);
13 | expect(${pascalToCamel(componentName)}.find('.${STYLE_PREFIX}__${pascalToCamel(componentName)}')).toHaveLength(1);
14 | });
15 |
16 | it('snapshot renders default ${pascalToCamel(componentName)}', () => {
17 | const ${pascalToCamel(componentName)} = shallow(<${componentName} />);
18 | expect(${pascalToCamel(componentName)}).toMatchSnapshot();
19 | });
20 |
21 | it('clicking ${pascalToCamel(componentName)} triggers onClick prop', () => {
22 | const onClick = spy();
23 | shallow(<${componentName} onClick={onClick} />).simulate('click');
24 | expect(onClick.callCount).toBe(1);
25 | });
26 |
27 | it('clicking disabled ${pascalToCamel(componentName)} does not trigger onClick prop', () => {
28 | const onClick = spy();
29 | // full DOM mount so \`${pascalToCamel(componentName)}\` element will use disabled prop
30 | mount(<${componentName} onClick={onClick} disabled />).simulate('click');
31 | expect(onClick.callCount).toBe(0);
32 | });
33 | });
34 | `;
35 |
--------------------------------------------------------------------------------
/scripts/make/index.ts:
--------------------------------------------------------------------------------
1 | import { prompt } from 'enquirer';
2 | import { generateFiles } from './generateFiles';
3 | import { optionPicker } from './optionPicker';
4 | import { summaryLog } from './summaryLog';
5 | import { clearTerminal, log, redErr } from './utils/logUtils';
6 |
7 | (async () => {
8 | clearTerminal();
9 |
10 | // eslint-disable-next-line no-constant-condition
11 | while (true) {
12 | try {
13 | /* --- Pick and display options --- */
14 |
15 | const options = await optionPicker();
16 | summaryLog(options);
17 |
18 | const { componentName, isRef } = options;
19 |
20 | /* --- Check for final approval before generating files --- */
21 |
22 | const approvedOptions = (
23 | await prompt<{ approvedOptions: 'quit' | 'no' | 'yes' }>({
24 | type: 'select',
25 | name: 'approvedOptions',
26 | message: `Do these options look ok?`,
27 | choices: [
28 | { name: 'yes', hint: ' All of your choices are correct' },
29 | { name: 'no', hint: ' Reselect your component options' },
30 | { name: 'quit', hint: 'You give up' },
31 | ],
32 | })
33 | )['approvedOptions'];
34 |
35 | // Quit
36 | if (approvedOptions === 'quit') return;
37 |
38 | // Restart (clear the console before repeating make script)
39 | if (approvedOptions === 'no') {
40 | clearTerminal();
41 | log('Restarting script...\n');
42 | }
43 |
44 | // Generate files and directories
45 | if (approvedOptions === 'yes') {
46 | log('\nAttempting to generate files...\n');
47 | generateFiles({ isRef, componentName });
48 | return;
49 | }
50 | } catch (error) {
51 | redErr(`\nScript Error:\n${error || 'No error returned.'}`);
52 | return;
53 | }
54 | }
55 | })();
56 |
--------------------------------------------------------------------------------
/scripts/make/optionPicker.ts:
--------------------------------------------------------------------------------
1 | import { prompt } from 'enquirer';
2 | import { MakeOptions } from './types';
3 |
4 | /**
5 | * Option picker function for make script.
6 | */
7 | export const optionPicker = async (): Promise => {
8 | /* --- Naming the component --- */
9 |
10 | const componentName = (
11 | await prompt<{ componentName: string }>({
12 | type: 'input',
13 | name: 'componentName',
14 | message: 'What will your component be called?',
15 | validate: value => {
16 | if (!/^[A-Z]/.test(value)) {
17 | return 'Must begin with a capital';
18 | }
19 | if (!/^[A-Za-z]+$/.test(value)) {
20 | return 'Must only contain letters';
21 | }
22 | if (value.length < 2) {
23 | return 'Must be at least 2 letters long';
24 | }
25 | return true;
26 | },
27 | })
28 | )['componentName'];
29 |
30 | /* --- Forwards ref --- */
31 |
32 | const isRef = await (async () => {
33 | const result = (
34 | await prompt<{ isRef: 'no' | 'yes' }>({
35 | type: 'select',
36 | name: 'isRef',
37 | message: 'Will your component forward a ref?',
38 | hint: 'https://reactjs.org/docs/forwarding-refs.html',
39 | choices: [
40 | { name: 'no', hint: ' Component does not require a ref (ex: Tooltip)' },
41 | { name: 'yes', hint: 'Component should forward a ref (ex: Button, Input)' },
42 | ],
43 | })
44 | )['isRef'];
45 |
46 | return result === 'yes';
47 | })();
48 |
49 | return { componentName, isRef };
50 | };
51 |
--------------------------------------------------------------------------------
/scripts/make/summaryLog.ts:
--------------------------------------------------------------------------------
1 | import c from 'ansi-colors';
2 | import { MakeOptions } from './types';
3 | import { ERROR_SYMBOL, log, SUCCESS_SYMBOL } from './utils/logUtils';
4 |
5 | const checkOrCross = (bool: boolean, ifTrue: string, ifFalse: string) => {
6 | const symbol = bool ? c.green(SUCCESS_SYMBOL) : c.red(ERROR_SYMBOL);
7 | const text = bool ? ifTrue : ifFalse;
8 | return `${symbol} ${c.dim(`- ${text}`)}`;
9 | };
10 |
11 | /**
12 | * Log a summary of your options.
13 | */
14 | export const summaryLog = ({ isRef, componentName }: MakeOptions) => {
15 | const componentInfo = `
16 | Component information:
17 | ${c.dim('Component name:')} ${c.yellow(`${componentName}`)}`;
18 |
19 | const forwardsRef = `
20 | ${c.dim('Forwards ref:')} ${checkOrCross(isRef, 'Will forward a ref to inner component', "Won't forward a ref")}`;
21 |
22 | log(componentInfo + forwardsRef + '\n');
23 | };
24 |
--------------------------------------------------------------------------------
/scripts/make/types.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * The available make options chosen and logged during the script.
3 | */
4 | export interface MakeOptions {
5 | isRef: boolean;
6 | componentName: string;
7 | }
8 |
--------------------------------------------------------------------------------
/scripts/make/utils/pathUtils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Joins segments with '/' to create a path.
3 | * @param segments The multiple segments to join.
4 | */
5 | export function pathify(...segments: string[]) {
6 | return segments.join('/');
7 | }
8 |
9 | export const getPaths = (componentName: string) => {
10 | // Directories.
11 | const rootDir = 'src';
12 | const componentCommonDir = pathify(rootDir, 'components'); // Components folder.
13 | const componentDir = pathify(componentCommonDir, componentName);
14 |
15 | // Files.
16 | const indexFile = 'index.ts';
17 | const componentFile = componentName + '.tsx';
18 | const styleFile = '_' + componentName + '.scss';
19 | const testFile = componentName + '.test.tsx';
20 | const storyFile = componentName + '.stories.tsx';
21 | const readmeFile = 'README.md';
22 | const styleIndexFile = 'index.scss';
23 | const componentIndexFile = 'index.ts';
24 |
25 | return {
26 | rootDir,
27 | componentCommonDir,
28 | componentDir,
29 | indexFile,
30 | componentFile,
31 | styleFile,
32 | testFile,
33 | storyFile,
34 | readmeFile,
35 | styleIndexFile,
36 | componentIndexFile,
37 | };
38 | };
39 |
--------------------------------------------------------------------------------
/scripts/make/utils/stringUtils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Transforms pascal case (eg. SomeString) to camel case (eg. someString).
3 | * @param s The pascal case string to transform to camel case.
4 | */
5 | export function pascalToCamel(s: string) {
6 | return s.charAt(0).toLowerCase() + s.slice(1);
7 | }
8 |
9 | /**
10 | * Transforms pascal case (eg. SomeString) to delimiter (eg. some-string).
11 | * @param string The pascal case string.
12 | * @param delimiter Defaults to '-'.
13 | */
14 | export function pascalToDelimiter(string: string, delimiter = '-') {
15 | return string
16 | .split(/(?=[A-Z])/)
17 | .join(delimiter)
18 | .toLowerCase();
19 | }
20 |
--------------------------------------------------------------------------------
/scripts/verify-package-json.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs');
3 |
4 | /**
5 | * This script makes sure that the symlink between this repo and
6 | * any local repos are destroyed before commiting
7 | */
8 | (async () => {
9 | const jsonPath = path.resolve(__dirname, '../package.json');
10 | const json = JSON.parse(fs.readFileSync(jsonPath) + '');
11 | json.main = './dist/cjs/index.js';
12 | fs.writeFileSync(jsonPath, JSON.stringify(json, null, 2) + '\n');
13 | })();
14 |
--------------------------------------------------------------------------------
/src/__stories__/1_basics/Version.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import font from '../../storybook-helpers/theme/font';
3 | const version = require('../../../package.json').version;
4 |
5 | export function Version() {
6 | return {version} ;
7 | }
8 |
9 | export function Current(props: { text: string }) {
10 | const style: CSSProperties = {
11 | fontFamily: font.fontBase,
12 | margin: 0,
13 | WebkitTapHighlightColor: 'rgba(0,0,0,0)',
14 | WebkitOverflowScrolling: 'touch',
15 | color: '#00a3e3',
16 | textDecoration: 'none',
17 | };
18 |
19 | return (
20 |
26 | {props.text || version}
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/__stories__/2_style/2_variables.stories.mdx:
--------------------------------------------------------------------------------
1 | import { Meta } from '@storybook/addon-docs/blocks';
2 | import { Theme, Groups } from './styleComponents';
3 |
4 |
5 |
6 | # Variables
7 |
8 | To allow consistency over our variables, we centralize the style by using
9 | a number of variables to control everything from font size to breakpoints. These
10 | are all configurable, and doing so will change the appearance of the toolkit.
11 |
12 | > The WebViewer React Toolkit css stylesheet contains
13 | > [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties).
14 | > It also includes a file of Sass variables that might be helpful for development
15 | > if you use Sass.
16 |
17 | ## Using
18 |
19 | To use these in your own project, you can include the variables in your code to
20 | match the visual structure outlined in the toolkit:
21 |
22 | ```scss
23 | div {
24 | /* If you are using CSS: */
25 | border-radius: var(--border-radius);
26 | /* If you are using Sass: */
27 | border-radius: $border-radius;
28 | }
29 | ```
30 |
31 | If you are using Sass, make sure you import the Sass variables file:
32 |
33 | ```scss
34 | @import "~@pdftron/webviewer-react-toolkit/dist/scss/_variables.scss";
35 | ```
36 |
37 | ## Overriding
38 |
39 | Since each value is a variable, and all Sass variables just point to the CSS
40 | variables, you can easily the appearance of the toolkit. Here's an example of
41 | overriding the border radius:
42 |
43 | ```css
44 | /* your_custom_theme.css */
45 | :root {
46 | --border-radius-small: 1px;
47 | --border-radius: 6px;
48 | --border-radius-large: 12px;
49 | }
50 | ```
51 |
52 | ## CSS and Sass Variables
53 |
54 | Below are the CSS and Sass variables. You can click any of these to copy the
55 | value to your clipboard.
56 |
57 |
58 |
--------------------------------------------------------------------------------
/src/__stories__/2_style/4_breakpoints.stories.mdx:
--------------------------------------------------------------------------------
1 | import { Meta } from '@storybook/addon-docs/blocks';
2 | import { Mixins, Breakpoints } from './styleBreakpoints';
3 | import { ToastProvider } from '../../components';
4 |
5 |
6 |
7 | # Breakpoints
8 |
9 | Unfortunately,
10 | [CSS variables do not work in media queries](https://stackoverflow.com/a/40723269),
11 | and so breakpoints are not customizable. However, we provide our breakpoints for
12 | you to use, either as Sass mixins, or just hard-coded pixel values.
13 |
14 | ## Using
15 |
16 | To use these in your own project, you can include the mixins in your code:
17 |
18 | ```scss
19 | div {
20 | @include for-tablet-up {
21 | background-color: blue;
22 | };
23 | }
24 | ```
25 |
26 | Or copy the code into your CSS files:
27 |
28 | ```css
29 | div {
30 | @media (min-width: 768px) {
31 | background-color: blue;
32 | }
33 | }
34 | ```
35 |
36 | If you are using Sass, make sure you import the Sass breakpoints file:
37 |
38 | ```scss
39 | @import "~@pdftron/webviewer-react-toolkit/dist/scss/_breakpoints.scss";
40 | ```
41 |
42 | ## Breakpoint Visualizer
43 |
44 | You can change the size of your browser window to see the breakpoints in effect.
45 |
46 |
47 |
48 | ## Mixins
49 |
50 | The following mixins show the value you can include in your code, as well as the
51 | raw values in case you wish to copy them to CSS code. You can click any of these
52 | to copy the value to your clipboard.
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | ### `for-phone-only`
61 |
62 | Try not to use this, as it breaks "mobile first" principles. Applies only to mobile devices.
63 |
64 |
65 |
66 | ```scss
67 | @media (max-width: 767px) {
68 | /* Content */
69 | }
70 | ```
71 |
72 | ### `for-tablet-up`
73 |
74 | Style applies to devices with width tablet or above.
75 |
76 |
77 |
78 | ```scss
79 | @media (min-width: 768px) {
80 | /* Content */
81 | }
82 | ```
83 |
84 | ### `for-desktop-up`
85 |
86 | Style applies to devices with width desktop or above.
87 |
88 |
89 |
90 | ```scss
91 | @media (min-width: 1024px) {
92 | /* Content */
93 | }
94 | ```
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/__stories__/2_style/generated/breakpointRange.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | params: 'for-phone-only',
4 | max: 767,
5 | prop: '$breakpoint-tablet',
6 | value: '768px',
7 | },
8 | {
9 | params: 'for-tablet-up',
10 | min: 768,
11 | prop: '$breakpoint-tablet',
12 | value: '768px',
13 | },
14 | {
15 | params: 'for-desktop-up',
16 | min: 1024,
17 | prop: '$breakpoint-desktop',
18 | value: '1024px',
19 | },
20 | ];
21 |
--------------------------------------------------------------------------------
/src/__stories__/2_style/generated/breakpoints.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | '@include for-phone-only {\n // Content\n};',
3 | '@include for-tablet-up {\n // Content\n};',
4 | '@include for-desktop-up {\n // Content\n};',
5 | ];
6 |
--------------------------------------------------------------------------------
/src/__stories__/2_style/generated/mixins.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | '@include focus;',
3 | '@include focus-inset;',
4 | '@include absolute-fill;',
5 | '@include fixed-fill;',
6 | '@include flex-center;',
7 | '@include padding-bottom($paddingBottom, $grid-row-gap: 0px);',
8 | '@include skeleton;',
9 | '@include spinner;',
10 | '@include slide-in;',
11 | '@include opacity-transition;',
12 | '@include opacity-transition-fast;',
13 | ];
14 |
--------------------------------------------------------------------------------
/src/__stories__/2_style/styles.ts:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from 'react';
2 | import font from '../../storybook-helpers/theme/font';
3 |
4 | /* --- Styles. --- */
5 |
6 | export const style: CSSProperties = {
7 | userSelect: 'none',
8 | padding: 8,
9 | backgroundColor: 'rgba(255, 255, 255, 0.7)',
10 | borderRadius: 4,
11 | cursor: 'pointer',
12 | display: 'flex',
13 | alignItems: 'center',
14 | fontFamily: font.fontCode,
15 | fontSize: 12,
16 | overflow: 'hidden',
17 | };
18 |
19 | export const dividerStyle: CSSProperties = {
20 | ...style,
21 | cursor: 'default',
22 | backgroundColor: 'transparent',
23 | color: style.backgroundColor,
24 | fontFamily: font.fontBase,
25 | justifyContent: 'center',
26 | };
27 |
28 | export const prefixStyle: CSSProperties = {
29 | ...dividerStyle,
30 | paddingLeft: 0,
31 | paddingRight: 0,
32 | marginRight: -8,
33 | justifyContent: 'flex-end',
34 | };
35 |
36 | export const titleStyle: CSSProperties = {
37 | fontFamily: font.fontBase,
38 | margin: '20px 0 8px',
39 | padding: 0,
40 | position: 'relative',
41 | color: 'rgba(240,240,255,0.9)',
42 | fontSize: 20,
43 | };
44 |
--------------------------------------------------------------------------------
/src/__stories__/2_style/utils.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEvent } from 'react';
2 | import { useToast } from '../../hooks';
3 |
4 | async function fallbackCopyTextToClipboard(text: string) {
5 | const textArea = document.createElement('textarea');
6 | textArea.value = text;
7 | textArea.style.position = 'fixed'; // avoid scrolling to bottom
8 | document.body.appendChild(textArea);
9 | textArea.focus();
10 | textArea.select();
11 |
12 | const successful = document.execCommand('copy');
13 | if (!successful) throw new Error();
14 |
15 | document.body.removeChild(textArea);
16 | }
17 |
18 | export function useCopy() {
19 | const toast = useToast();
20 |
21 | return (text: string) => {
22 | return (e: MouseEvent) => {
23 | e.preventDefault();
24 | e.stopPropagation();
25 | const copyFunc = navigator.clipboard
26 | ? () => navigator.clipboard.writeText(text)
27 | : () => fallbackCopyTextToClipboard(text);
28 | copyFunc().then(
29 | () => {
30 | toast.remove();
31 | toast.add({
32 | heading: 'Copied Value',
33 | children: {text}
,
34 | message: 'success',
35 | timeout: 2000,
36 | });
37 | },
38 | err => {
39 | toast.add({
40 | heading: 'Error Copying Text',
41 | children: {err}
,
42 | message: 'error',
43 | timeout: 2000,
44 | });
45 | },
46 | );
47 | };
48 | };
49 | }
50 |
51 | export function getTitle(group: string) {
52 | let words = group.split(/(?=[A-Z])/);
53 | words = words.map(w => w.charAt(0).toUpperCase() + w.slice(1));
54 | return words.join(' ');
55 | }
56 |
--------------------------------------------------------------------------------
/src/__stories__/3_hooks/useFocusTrap.stories.mdx:
--------------------------------------------------------------------------------
1 | import { options, output } from './useFocusTrapHelpers';
2 | import { Meta, Props } from '@storybook/addon-docs/blocks';
3 | import { LinkTo } from '../../storybook-helpers/mdx';
4 |
5 |
6 |
7 | # useFocusTrap
8 |
9 | A hook that allows you to conditionally lock focus within an element. This is
10 | valuable for trapping focus inside modals, or any other use case which requires
11 | you to prevent the user from tabbing out of a context.
12 |
13 | The first argument is `locked`, a boolean determining whether focus is locked
14 | within the element you pass the ref to.
15 |
16 | This functionality is wrapped by the
17 | component FocusTrap if you prefer
18 | to use a component rather than a hook.
19 |
20 | ```tsx
21 | const focusRef = useFocusTrap(locked, { /* UseFocusTrapOptions */ });
22 |
23 | return (
24 |
25 |
26 | Some button
27 |
28 | );
29 | ```
30 |
31 | ## `UseFocusTrapOptions`
32 |
33 |
34 |
35 | ## Returns
36 |
37 | ```tsx
38 | RefObject
39 | ```
--------------------------------------------------------------------------------
/src/__stories__/3_hooks/useFocusTrapHelpers.ts:
--------------------------------------------------------------------------------
1 | import { UseFocusTrapOptions } from '../../hooks/useFocusTrap';
2 |
3 | /* eslint-disable @typescript-eslint/no-unused-vars */
4 |
5 | export function options(x: UseFocusTrapOptions) {}
6 |
--------------------------------------------------------------------------------
/src/__stories__/3_hooks/useManagedFiles.stories.mdx:
--------------------------------------------------------------------------------
1 | import { options, output } from './useManagedFilesHelpers';
2 | import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
3 | import { LinkTo } from '../../storybook-helpers/mdx';
4 |
5 |
6 |
7 | # useManagedFiles
8 |
9 | A hook that provides complex managing behavior for files. It is designed for use with `FileOrganizer`, but can be used anywhere you need to manage an array of files. The basic functionality it provides is:
10 |
11 | 1. File selection
12 | 1. File movement
13 | 1. Moving multiple selected files
14 |
15 | ```tsx
16 | const { fileOrganizerProps, getThumbnailSelectionProps } = useManagedFiles({ /* UseManagedFilesOptions */ });
17 |
18 | return (
19 | (
22 |
26 | )}
27 | />
28 | );
29 | ```
30 |
31 | ## `UseManagedFilesOptions`
32 |
33 |
34 |
35 | ## `UseManagedFilesOutput` (return value)
36 |
37 |
38 |
39 | ## Example
40 |
41 | Here's the hook in action:
42 |
43 | > Note: See FileOrganizer for more demos
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/__stories__/3_hooks/useManagedFilesHelpers.ts:
--------------------------------------------------------------------------------
1 | import { UseManagedFilesOptions, UseManagedFilesOutput } from '../../hooks/useManagedFiles';
2 |
3 | /* eslint-disable @typescript-eslint/no-unused-vars */
4 |
5 | export function options(x: UseManagedFilesOptions) {}
6 | export function output(x: UseManagedFilesOutput) {}
7 |
--------------------------------------------------------------------------------
/src/__stories__/3_hooks/useToast.stories.mdx:
--------------------------------------------------------------------------------
1 | import { options, output } from './useToastHelpers';
2 | import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
3 | import { LinkTo } from '../../storybook-helpers/mdx';
4 |
5 |
6 |
7 | # useToast
8 |
9 | A hook that provides the context value
10 | of ToastProvider in
11 | order to add or remove Toast notifications. Make sure that you have a
12 | `ToastProvider` somewhere further up in your React project, or this will not
13 | work.
14 |
15 | ```ts
16 | const { add, remove } = useToast();
17 |
18 | const toastId = add({
19 | heading: 'You are not able to do that',
20 | children: 'The action you are attempting to do is not possible.',
21 | message: 'warning',
22 | action: { text: 'Try Again', onClick: tryAgain },
23 | closable: true,
24 | timeout: 4000,
25 | });
26 |
27 | // Later, if you want to remove:
28 | remove(toastId);
29 | ```
30 |
31 | ## `ToastContextValue` (return value)
32 |
33 |
34 |
35 | ## `AddToast`
36 |
37 |
38 |
39 | ## Example
40 |
41 | Here's the hook in action:
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/__stories__/3_hooks/useToastHelpers.ts:
--------------------------------------------------------------------------------
1 | import { ToastProps } from '../../components';
2 | import { AddToast, ToastContextValue } from '../../hooks/useToast';
3 | import { Include } from '../../utils';
4 |
5 | /* eslint-disable @typescript-eslint/no-unused-vars */
6 |
7 | interface Test extends AddToast, Include {}
8 |
9 | export function options(x: Test) {}
10 | export function output(x: ToastContextValue) {}
11 |
--------------------------------------------------------------------------------
/src/__stories__/4_data/file.stories.mdx:
--------------------------------------------------------------------------------
1 | import { options, output } from './fileHelpers';
2 | import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
3 | import { LinkTo } from '../../storybook-helpers/mdx';
4 |
5 |
6 |
7 | # File
8 |
9 | The `File` class is a utility that encapsulates useful functionality from the
10 | WebViewer API. It exposes various properties and methods for manipulating the
11 | `File`.
12 |
13 | ```ts
14 | const file = new File({ /* FileDetails */ });
15 |
16 | await file.rotate();
17 | setThumbnail(file.thumbnail.get());
18 | ```
19 |
20 | ## Providing a License Key
21 |
22 | Since file uses [WebViewer APIs](https://www.pdftron.com/documentation/web/)
23 | internally, you must provide a license key in order to remove watermarking and
24 | enable the full functionality. This can be done by either setting a global
25 | license, or individually including licenses for files during initialization:
26 |
27 | ```ts
28 | // Globally set the license for all files (this is the best method)
29 | File.setGlobalLicense('your-license-key');
30 |
31 | // Individually set license for a file instance (this will take priority if set)
32 | const file = new File({license: 'your-license-key'})
33 | ```
34 |
35 | ## `FileDetails`
36 |
37 |
38 |
39 | ## `File` (class)
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/__stories__/4_data/fileHelpers.ts:
--------------------------------------------------------------------------------
1 | import { File, FileDetails } from '../../data/file';
2 | import { Include } from '../../utils';
3 |
4 | type Public = Include;
5 |
6 | /* eslint-disable @typescript-eslint/no-unused-vars */
7 |
8 | export function options(x: FileDetails) {}
9 | export function output(x: Public) {}
10 |
--------------------------------------------------------------------------------
/src/__stories__/4_data/memoized-promise.stories.mdx:
--------------------------------------------------------------------------------
1 | import { options, output } from './memoizedPromiseHelpers';
2 | import { Meta, Props, Story, Preview } from '@storybook/addon-docs/blocks';
3 | import { LinkTo } from '../../storybook-helpers/mdx';
4 |
5 |
6 |
7 | # MemoizedPromise
8 |
9 | The `MemoizedPromise` class is a utility that allows for caching of lazy
10 | promises. This is used internally in File in
11 | order to allow for lazy retrieval of potentially expensive properties. It allows
12 | for throttling, for example
13 | in FileOrganizer when
14 | lazy `Thumbnails` are not fetched until 500ms after entering view.
15 |
16 | ```ts
17 | const memoizedPromise = new MemoizedPromise(value, { /* MemoizeOptions */ });
18 |
19 | if (memoizedPromise.done) {
20 | setValue(memoizedPromise.get());
21 | } else {
22 | throttle(() => setValue(memoizedPromise.get()));
23 | }
24 | ```
25 |
26 | ## `MemoizeOptions`
27 |
28 |
29 |
30 | ## `MemoizedPromise` (class)
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/__stories__/4_data/memoizedPromiseHelpers.ts:
--------------------------------------------------------------------------------
1 | import { MemoizedPromise, MemoizeOptions } from '../../data/memoizedPromise';
2 | import { Include } from '../../utils';
3 |
4 | type Public = Include, any>;
5 |
6 | /* eslint-disable @typescript-eslint/no-unused-vars */
7 |
8 | export function options(x: MemoizeOptions) {}
9 | export function output(x: Public) {}
10 |
--------------------------------------------------------------------------------
/src/components/Button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean, select, text } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { action } from '../../storybook-helpers/action/action';
4 | import { Button } from '../Button';
5 | import readme from './README.md';
6 |
7 | export default { title: 'Components/Button', component: Button, parameters: { readme } };
8 |
9 | export const Basic = () => (
10 |
16 | {text('children', 'Button content')}
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/components/Button/Button.test.tsx:
--------------------------------------------------------------------------------
1 | import { mount, shallow } from 'enzyme';
2 | import React from 'react';
3 | import { spy } from 'sinon';
4 | import { Button } from '../Button';
5 |
6 | describe('Button component', () => {
7 | it('renders its contents', () => {
8 | const button = shallow( );
9 | expect(button.find('.ui__button')).toHaveLength(1);
10 | });
11 |
12 | it('snapshot renders default button', () => {
13 | const button = shallow( );
14 | expect(button).toMatchSnapshot();
15 | });
16 |
17 | it('clicking button triggers onClick prop', () => {
18 | const onClick = spy();
19 | shallow( ).simulate('click');
20 | expect(onClick.callCount).toBe(1);
21 | });
22 |
23 | it('clicking disabled button does not trigger onClick prop', () => {
24 | const onClick = spy();
25 | mount( ).simulate('click');
26 | expect(onClick.callCount).toBe(0);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/Button/Button.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { ButtonHTMLAttributes, forwardRef } from 'react';
3 | import { useAccessibleFocus } from '../../hooks';
4 |
5 | export interface ButtonProps extends ButtonHTMLAttributes {
6 | /**
7 | * Sets the visual appearance of the button.
8 | * @default "default"
9 | */
10 | buttonStyle?: 'default' | 'borderless' | 'outline';
11 | /**
12 | * Sets the size of the button.
13 | * @default "default"
14 | */
15 | buttonSize?: 'small' | 'default' | 'large';
16 | /**
17 | * Defaults to 'button' instead of 'submit' to prevent accidental submissions.
18 | * @default "button"
19 | */
20 | type?: ButtonHTMLAttributes['type'];
21 | }
22 |
23 | export const Button = forwardRef(
24 | ({ buttonStyle = 'default', buttonSize = 'default', type = 'button', className, children, ...buttonProps }, ref) => {
25 | const isUserTabbing = useAccessibleFocus();
26 |
27 | const buttonClass = classnames(
28 | 'ui__base ui__button',
29 | `ui__button--style-${buttonStyle}`,
30 | `ui__button--size-${buttonSize}`,
31 | {
32 | 'ui__button--disabled': buttonProps.disabled,
33 | 'ui__button--tabbing': isUserTabbing,
34 | },
35 | className,
36 | );
37 |
38 | return (
39 |
40 | {children}
41 |
42 | );
43 | },
44 | );
45 |
--------------------------------------------------------------------------------
/src/components/Button/README.md:
--------------------------------------------------------------------------------
1 | A versatile button component that can adapt to various styles and sizes.
2 |
--------------------------------------------------------------------------------
/src/components/Button/__snapshots__/Button.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Button component snapshot renders default button 1`] = `
4 |
8 |
11 |
12 | `;
13 |
--------------------------------------------------------------------------------
/src/components/Button/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Button';
2 |
--------------------------------------------------------------------------------
/src/components/ButtonGroup/ButtonGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean, number, select } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { Button } from '../Button';
4 | import { ButtonProps } from '../Button/Button';
5 | import { ButtonGroup } from '../ButtonGroup';
6 | import readme from './README.md';
7 |
8 | export default { title: 'Components/ButtonGroup', component: ButtonGroup, parameters: { readme } };
9 |
10 | export const Basic = () => {
11 | const numButtons = number('numButtons', 2, { min: 1, max: 5, range: true, step: 1 });
12 |
13 | return (
14 |
19 | {Array.from({ length: numButtons }).map((_, index) => {
20 | if (index === 0) {
21 | return Accept ;
22 | }
23 | if (index === 1) {
24 | return (
25 |
26 | Cancel
27 |
28 | );
29 | }
30 | return (
31 |
35 | Button {index + 1}
36 |
37 | );
38 | })}
39 |
40 | );
41 | };
42 |
43 | export const Nested = () => {
44 | return (
45 |
46 | Other
47 |
48 |
49 | Accept
50 | Cancel
51 |
52 |
53 | );
54 | };
55 |
56 | export const WithLargeButtons = () => {
57 | return (
58 |
63 | Accept But With Long Name So You Can See It Wrap
64 | Cancel But With Long Name So You Can See It Wrap
65 |
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/ButtonGroup/ButtonGroup.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { FC, HTMLAttributes } from 'react';
3 |
4 | export interface ButtonGroupProps extends HTMLAttributes {
5 | /**
6 | * Position the buttons within the group.
7 | * @default "right"
8 | */
9 | position?: 'left' | 'center' | 'right' | 'space-between' | 'space-around';
10 | /**
11 | * If given, will wrap the buttons in reverse order. This is valuable if you
12 | * have an accept button on the left, but want it on the bottom when wrapped.
13 | */
14 | reverseWrap?: boolean;
15 | /**
16 | * Center the buttons at mobile widths.
17 | */
18 | centerMobile?: boolean;
19 | }
20 |
21 | export const ButtonGroup: FC = ({
22 | position = 'right',
23 | reverseWrap,
24 | centerMobile,
25 | children,
26 | className,
27 | ...props
28 | }) => {
29 | const buttonGroupClass = classnames(
30 | 'ui__base ui__buttonGroup',
31 | `ui__buttonGroup--position-${position}`,
32 | {
33 | 'ui__buttonGroup--reverse': reverseWrap,
34 | 'ui__buttonGroup--centerMobile': centerMobile,
35 | },
36 | className,
37 | );
38 |
39 | return (
40 |
41 | {children}
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/ButtonGroup/README.md:
--------------------------------------------------------------------------------
1 | Allows you to easily group buttons, with the ability to position them within the
2 | group.
3 |
--------------------------------------------------------------------------------
/src/components/ButtonGroup/_ButtonGroup.scss:
--------------------------------------------------------------------------------
1 | .ui__buttonGroup {
2 | // do not mulply by -1 as it seems to cause issues for other libraries that uses this library
3 | margin: calc(1px - #{$padding-tiny} - 1px);
4 | display: flex;
5 | flex-wrap: wrap;
6 |
7 | // Undo negative margin on nested groups.
8 | > .ui__buttonGroup {
9 | margin: 0;
10 | }
11 |
12 | // Buttons inside are kept apart by padding.
13 | > button {
14 | margin: $padding-tiny;
15 | }
16 |
17 | &--reverse {
18 | flex-wrap: wrap-reverse;
19 | }
20 |
21 | &--centerMobile.ui__buttonGroup {
22 | @include for-phone-only {
23 | justify-content: center;
24 | }
25 | }
26 |
27 | &--position {
28 | &-left {
29 | justify-content: flex-start;
30 | }
31 |
32 | &-center {
33 | justify-content: center;
34 | }
35 |
36 | &-right {
37 | justify-content: flex-end;
38 | }
39 |
40 | &-space-between {
41 | justify-content: space-between;
42 | }
43 |
44 | &-space-around {
45 | justify-content: space-around;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/ButtonGroup/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ButtonGroup';
2 |
--------------------------------------------------------------------------------
/src/components/Choice/Choice.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { Choice } from '../Choice';
4 |
5 | describe('Choice component', () => {
6 | it('renders its contents', () => {
7 | const choice = shallow( );
8 | expect(choice.find('.ui__choice')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default choice', () => {
12 | const choice = shallow( );
13 | expect(choice).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Choice/README.md:
--------------------------------------------------------------------------------
1 | Choice provides a component for either checkboxes or radio selection buttons.
2 | These function how native radio or checkbox components would.
3 |
--------------------------------------------------------------------------------
/src/components/Choice/__snapshots__/Choice.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Choice component snapshot renders default choice 1`] = `
4 |
7 |
10 |
13 |
20 |
21 |
25 | Label
26 |
27 |
28 | `;
29 |
--------------------------------------------------------------------------------
/src/components/Choice/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Choice';
2 |
--------------------------------------------------------------------------------
/src/components/ClickableDiv/ClickableDiv.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean, text } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { action } from '../../storybook-helpers/action/action';
4 | import { ClickableDiv } from '../ClickableDiv';
5 | import readme from './README.md';
6 |
7 | export default { title: 'Components/ClickableDiv', component: ClickableDiv, parameters: { readme } };
8 |
9 | export const Basic = () => (
10 |
16 | {text('children', 'This is a clickable div!')}
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/src/components/ClickableDiv/ClickableDiv.test.tsx:
--------------------------------------------------------------------------------
1 | import { mount, shallow } from 'enzyme';
2 | import React from 'react';
3 | import { spy } from 'sinon';
4 | import { ClickableDiv } from '../ClickableDiv';
5 |
6 | describe('ClickableDiv component', () => {
7 | it('renders its contents', () => {
8 | const clickableDiv = shallow( );
9 | expect(clickableDiv.find('.ui__clickableDiv')).toHaveLength(1);
10 | });
11 |
12 | it('snapshot renders default clickableDiv', () => {
13 | const clickableDiv = shallow( );
14 | expect(clickableDiv).toMatchSnapshot();
15 | });
16 |
17 | it('clicking clickableDiv triggers onClick prop', () => {
18 | const onClick = spy();
19 | mount( ).simulate('click');
20 | expect(onClick.callCount).toBe(1);
21 | });
22 |
23 | it('clicking disabled clickableDiv does not trigger onClick prop', () => {
24 | const onClick = spy();
25 | mount( ).simulate('click');
26 | expect(onClick.callCount).toBe(0);
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/ClickableDiv/ClickableDiv.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { forwardRef, HTMLAttributes, useImperativeHandle, useRef } from 'react';
3 | import { useAccessibleFocus, useKeyForClick, useOnClick } from '../../hooks';
4 | import { Remove } from '../../utils';
5 |
6 | export interface ClickableDivProps extends Remove, 'role'> {
7 | /**
8 | * Is the clickable div disabled. Disabled will stop the onClick callback from
9 | * firing (similar to a button).
10 | */
11 | disabled?: boolean;
12 | /**
13 | * No style when focused. If true will have no focus outline.
14 | */
15 | noFocusStyle?: boolean;
16 | /**
17 | * Specify whether clickable div uses a pointer cursor. Otherwise is default.
18 | */
19 | usePointer?: boolean;
20 | }
21 |
22 | export const ClickableDiv = forwardRef(
23 | ({ onClick, onKeyPress, disabled, noFocusStyle, usePointer, className, children, tabIndex, ...divProps }, ref) => {
24 | const clickableDivRef = useRef(null);
25 | useImperativeHandle(ref, () => clickableDivRef.current as HTMLDivElement);
26 |
27 | const handleOnClick = useOnClick(onClick, { disabled, stopPropagation: true });
28 |
29 | const handleKeyPress = useKeyForClick(onKeyPress, clickableDivRef);
30 |
31 | const isUserTabbing = useAccessibleFocus();
32 |
33 | const clickableDivClass = classnames(
34 | 'ui__base ui__clickableDiv',
35 | {
36 | 'ui__clickableDiv--disabled': disabled,
37 | 'ui__clickableDiv--tabbing': isUserTabbing,
38 | 'ui__clickableDiv--noFocusStyle': noFocusStyle,
39 | 'ui__clickableDiv--usePointer': usePointer && !disabled,
40 | },
41 | className,
42 | );
43 |
44 | return (
45 |
54 | {children}
55 |
56 | );
57 | },
58 | );
59 |
--------------------------------------------------------------------------------
/src/components/ClickableDiv/README.md:
--------------------------------------------------------------------------------
1 | A div that acts like a focusable button, but without appearing as a traditional
2 | button.
3 |
--------------------------------------------------------------------------------
/src/components/ClickableDiv/_ClickableDiv.scss:
--------------------------------------------------------------------------------
1 | .ui__clickableDiv {
2 | display: inline-block;
3 | outline: none;
4 | border: none;
5 | transition: $focus-transition;
6 |
7 | // Don't show focus shadow if no focus style or disabled.
8 | &:focus.ui__clickableDiv--tabbing:not(.ui__clickableDiv--noFocusStyle):not(.ui__clickableDiv--disabled) {
9 | @include focus;
10 | }
11 |
12 | &--usePointer {
13 | cursor: pointer;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/ClickableDiv/__snapshots__/ClickableDiv.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ClickableDiv component snapshot renders default clickableDiv 1`] = `
4 |
11 | `;
12 |
--------------------------------------------------------------------------------
/src/components/ClickableDiv/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ClickableDiv';
2 |
--------------------------------------------------------------------------------
/src/components/DndMultiProvider/DndMultiProvider.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from 'react';
2 | import { DndProvider } from 'react-dnd';
3 | import MultiBackend from 'react-dnd-multi-backend';
4 | import HTML5toTouch from 'react-dnd-multi-backend/dist/cjs/HTML5toTouch';
5 |
6 | export const DndMultiProvider: FC = ({ children }) => {
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/DndMultiProvider/index.ts:
--------------------------------------------------------------------------------
1 | export * from './DndMultiProvider';
2 |
--------------------------------------------------------------------------------
/src/components/DragLayer/DragLayer.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { DndMultiProvider } from '../DndMultiProvider';
3 | import { Draggable } from '../Draggable';
4 | import { DragLayer } from '../DragLayer';
5 | import readme from './README.md';
6 |
7 | export default { title: 'Components/DragLayer', component: DragLayer, parameters: { readme } };
8 |
9 | const WIDTH = 200;
10 | const HEIGHT = 50;
11 |
12 | const commonStyle: CSSProperties = {
13 | padding: 16,
14 | background: 'lightgray',
15 | color: 'black',
16 | width: WIDTH,
17 | height: HEIGHT,
18 | textAlign: 'center',
19 | };
20 |
21 | export const Basic = () => (
22 |
23 |
24 | This div is draggable!
25 |
26 |
27 |
28 | Custom preview!
29 |
30 |
31 |
32 | );
33 |
34 | export const WithCustomTranslate = () => (
35 |
36 |
37 | This div is draggable!
38 |
39 | {
41 | const x = mousePosition.x - WIDTH / 2;
42 | const y = mousePosition.y - HEIGHT / 2;
43 | return { x, y };
44 | }}
45 | >
46 |
47 | Custom preview!
48 |
49 |
50 |
51 | );
52 |
--------------------------------------------------------------------------------
/src/components/DragLayer/DragLayer.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { CSSProperties, FC, HTMLAttributes, ReactNode, useMemo } from 'react';
3 | import { useDragLayer, XYCoord } from 'react-dnd';
4 |
5 | export interface DragLayerProps extends HTMLAttributes {
6 | /**
7 | * The children passed to the drag layer will be rendered whenever an item is
8 | * drag-and-dropped. You may have to disable their image preview or else you
9 | * will see both.
10 | */
11 | children?: ReactNode;
12 | /**
13 | * If not given, will use `currentOffset.x` and `currentOffset.y`. Can return
14 | * custom translate coordinates. This allows you to always center on mouse
15 | * position, or translate to any coordinates.
16 | */
17 | customTranslate?: (params: { currentOffset: XYCoord; mousePosition: XYCoord }) => { x: number; y: number };
18 | }
19 |
20 | export const DragLayer: FC = ({ children, customTranslate, className, ...divProps }) => {
21 | const { currentOffset, isDragging, mousePosition } = useDragLayer(monitor => ({
22 | currentOffset: monitor.getSourceClientOffset(),
23 | isDragging: monitor.isDragging(),
24 | mousePosition: monitor.getClientOffset(),
25 | }));
26 |
27 | const style = useMemo(() => {
28 | if (!currentOffset || !mousePosition) return { display: 'none' };
29 | const { x, y } = customTranslate?.({ currentOffset, mousePosition }) ?? currentOffset;
30 | const transform = `translate(${x}px, ${y}px)`;
31 | return { transform, WebkitTransform: transform };
32 | }, [currentOffset, customTranslate, mousePosition]);
33 |
34 | if (!isDragging) return null;
35 |
36 | const dragLayerClass = classnames('ui__base ui__dragLayer', className);
37 |
38 | return (
39 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/DragLayer/README.md:
--------------------------------------------------------------------------------
1 | This is a drag layer that implements
2 | [react-dnd](https://react-dnd.github.io/react-dnd) internally. It allows you to
3 | have a custom drag preview for any drag-and-drop container. In this component
4 | library, it is used to generate the page drag icons in `FileOrganizer`. It must
5 | be within a
6 | [DndProvider](https://react-dnd.github.io/react-dnd/docs/api/dnd-provider) with
7 | a backend (in our example we are using our own internal component called
8 | `DndMultiProvider` which bundles together mouse and touch handlers).
9 |
--------------------------------------------------------------------------------
/src/components/DragLayer/_DragLayer.scss:
--------------------------------------------------------------------------------
1 | .ui__dragLayer {
2 | pointer-events: none;
3 | @include fixed-fill;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/DragLayer/index.ts:
--------------------------------------------------------------------------------
1 | export * from './DragLayer';
2 |
--------------------------------------------------------------------------------
/src/components/Draggable/Draggable.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean } from '@storybook/addon-knobs';
2 | import React, { CSSProperties } from 'react';
3 | import { action } from '../../storybook-helpers/action/action';
4 | import { DndMultiProvider } from '../DndMultiProvider';
5 | import { Draggable } from '../Draggable';
6 | import readme from './README.md';
7 |
8 | export default { title: 'Components/Draggable', component: Draggable, parameters: { readme } };
9 |
10 | const commonStyle: CSSProperties = {
11 | padding: 16,
12 | background: 'lightgray',
13 | color: 'black',
14 | width: 200,
15 | height: 50,
16 | textAlign: 'center',
17 | };
18 |
19 | export const Basic = () => (
20 |
21 |
27 | This div is draggable!
28 |
29 |
30 | );
31 |
32 | export const WithOnRenderChildren = () => (
33 |
34 | (
37 |
38 | {isDragging ? 'This div is being dragged!' : 'This div is draggable!'}
39 |
40 | )}
41 | onDragChange={action('onDragChange')}
42 | disableDrag={boolean('disableDrag', false)}
43 | hideDragPreview={boolean('hideDragPreview', false)}
44 | />
45 |
46 | );
47 |
--------------------------------------------------------------------------------
/src/components/Draggable/README.md:
--------------------------------------------------------------------------------
1 | This is a draggable wrapper that implements
2 | [`react-dnd`](https://react-dnd.github.io/react-dnd) internally. It allows you
3 | to have drag-and-drop-able elements. In this component library, it is used to
4 | wrap the thumbnails of `FileOrganizer`. It must be within a
5 | [DndProvider](https://react-dnd.github.io/react-dnd/docs/api/dnd-provider) with
6 | a backend (in our example we are using our own internal component called
7 | `DndMultiProvider` which bundles together mouse and touch handlers).
8 |
--------------------------------------------------------------------------------
/src/components/Draggable/_Draggable.scss:
--------------------------------------------------------------------------------
1 | .ui__draggable {
2 | display: inline-block;
3 |
4 | &__animated {
5 | &--inMotion {
6 | pointer-events: none;
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Draggable/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Draggable';
2 |
--------------------------------------------------------------------------------
/src/components/EditableText/EditableText.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { EditableText } from '../EditableText';
4 |
5 | describe('EditableText component', () => {
6 | it('renders its contents', () => {
7 | const editableText = shallow( );
8 | expect(editableText.find('.ui__editableText')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default editableText', () => {
12 | const editableText = shallow( );
13 | expect(editableText).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/EditableText/README.md:
--------------------------------------------------------------------------------
1 | An editable text block that allows in-line editing of values.
2 |
--------------------------------------------------------------------------------
/src/components/EditableText/_EditableText.scss:
--------------------------------------------------------------------------------
1 | .ui__editableText {
2 | &--disabled {
3 | .ui__editableText__button {
4 | > span {
5 | opacity: 0.7;
6 | }
7 | }
8 | }
9 |
10 | &--centerText {
11 | .ui__editableText__button {
12 | justify-content: center;
13 | }
14 |
15 | .ui__editableText__field {
16 | text-align: center;
17 | }
18 | }
19 |
20 | &--bordered {
21 | .ui__editableText__button {
22 | border: 1px solid $color-blue-gray-2;
23 | }
24 |
25 | &:not(.ui__editableText--disabled) .ui__editableText__button {
26 | &:hover {
27 | background-color: $color-blue-gray-2;
28 | }
29 |
30 | &--placeholder {
31 | color: $color-blue-gray-3;
32 | }
33 | }
34 | }
35 |
36 | .ui__editableText__field {
37 | line-height: 1;
38 | }
39 |
40 | &__button,
41 | &__field {
42 | min-height: 28px;
43 | min-width: 100%;
44 | color: $color-font-primary;
45 | padding: $padding-tiny;
46 | border-radius: $border-radius;
47 | border: 1px solid transparent;
48 | }
49 |
50 | &__button {
51 | display: flex;
52 | align-items: center;
53 | transition: 0.1s background-color;
54 | transition: $focus-transition;
55 | line-height: 1.15; // FIX: match the normalize.css for input.
56 |
57 | > span {
58 | line-height: 1.15rem;
59 | white-space: nowrap;
60 | text-overflow: ellipsis;
61 | overflow: hidden;
62 | }
63 |
64 | &--noFocusTransition {
65 | // Prevents focus ring "stutter". Remove if transition is removed from focus.
66 | transition: none;
67 | }
68 | }
69 |
70 | &:not(.ui__editableText--disabled):not(.ui__editableText--locked)
71 | .ui__editableText__button {
72 | &:hover {
73 | border: 1px solid $color-blue-gray-2;
74 | }
75 |
76 | &--placeholder {
77 | color: $color-blue-gray-3;
78 | }
79 | }
80 |
81 | &__field {
82 | background-color: $color-blue-gray-2;
83 | outline: none;
84 | transition: $focus-transition;
85 |
86 | &:focus.ui__editableText__field--tabbing {
87 | @include focus;
88 | // Prevents focus ring "stutter". Remove if transition is removed from focus.
89 | transition: none;
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/components/EditableText/__snapshots__/EditableText.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`EditableText component snapshot renders default editableText 1`] = `
4 |
7 |
12 |
13 |
14 |
15 | `;
16 |
--------------------------------------------------------------------------------
/src/components/EditableText/index.ts:
--------------------------------------------------------------------------------
1 | export * from './EditableText';
2 |
--------------------------------------------------------------------------------
/src/components/FileOrganizer/README.md:
--------------------------------------------------------------------------------
1 | The file organizer allows you to view multiple page files and use drag-and-drop
2 | to re-order them. You can also use the arrow keys while focusing any part of the
3 | file thumbnail in order to focus on a new file, or tab to move through them one
4 | by one (tab will also focus internal elements, such as the editable text within
5 | `Thumbnail`). The `FileOrganizer` is virtualized, so there will be no
6 | performance issues even with thousands of thumbnails (see the stress test
7 | playground).
8 |
9 | ## Moving files
10 |
11 | You can click and drag to move items. If you are using `useManagedFiles` hook,
12 | you can hold Shift to multi-select items and then move them together.
13 | Unless `preventArrowsToMove` is set to true, you can hold the ⌘
14 | Command key on macOS, or the Ctrl key on Windows and use the
15 | arrow keys, `onMove` will be fired.
16 |
17 | ## Rendering Files
18 |
19 | The easiest way to get started would be to use the `Thumbnail` element as the
20 | item that is rendered in the `FileOrganizer`. The `Thumbnail` component includes
21 | many default props that integrate well with the `FileOrganizer`.
22 |
23 | ## Managing Files
24 |
25 | It is recommended that you use the `useManagedFiles` hook, as it exposes many
26 | valuable tools to save you effort when managing a list of files.
27 |
--------------------------------------------------------------------------------
/src/components/FileOrganizer/_FileOrganizer.scss:
--------------------------------------------------------------------------------
1 | .ui__fileOrganizer {
2 | position: relative;
3 | outline: 0;
4 | height: 100%;
5 |
6 | &__draglayer {
7 | overflow: visible;
8 | @include flex-center;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/FileOrganizer/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FileOrganizer';
2 |
--------------------------------------------------------------------------------
/src/components/FilePicker/FilePicker.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { action } from '../../storybook-helpers/action/action';
4 | import { FilePicker } from '../FilePicker';
5 | import readme from './README.md';
6 |
7 | export default { title: 'Components/FilePicker', component: FilePicker, parameters: { readme } };
8 |
9 | export const Basic = () => {
10 | const onRename = boolean('has onRename', false);
11 | const onDelete = boolean('has onDelete', false);
12 | const long = boolean('has long name', false);
13 |
14 | return (
15 |
16 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/FilePicker/FilePicker.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { FilePicker } from '../FilePicker';
4 |
5 | describe('FilePicker component', () => {
6 | it('renders its contents', () => {
7 | const filePicker = shallow( );
8 | expect(filePicker.find('.ui__filePicker')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default filePicker', () => {
12 | const filePicker = shallow( );
13 | expect(filePicker).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/FilePicker/FilePicker.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { FC, HTMLAttributes, ReactText } from 'react';
3 | import { EditableTextProps, EditableText } from '../EditableText';
4 | import { IconButton } from '../IconButton';
5 | import { Icon } from '../Icon';
6 |
7 | export interface FilePickerItem {
8 | key: ReactText;
9 | name: string;
10 | onRename?: EditableTextProps['onSave'];
11 | onDelete?: () => void;
12 | className?: string;
13 | }
14 |
15 | export interface FilePickerProps extends HTMLAttributes {
16 | items: FilePickerItem[];
17 | }
18 |
19 | export const FilePicker: FC = ({ items, className, ...props }) => {
20 | const filePickerClass = classnames('ui__base ui__filePicker', className);
21 |
22 | return (
23 |
24 | {items.map(item => (
25 |
26 |
32 | {item.onDelete ? (
33 |
34 |
35 |
36 | ) : (
37 | undefined
38 | )}
39 |
40 | ))}
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/components/FilePicker/README.md:
--------------------------------------------------------------------------------
1 | Organizes multiple files in a column, with options to rename or delete.
2 |
--------------------------------------------------------------------------------
/src/components/FilePicker/_FilePicker.scss:
--------------------------------------------------------------------------------
1 | .ui__filePicker {
2 | &__file {
3 | display: flex;
4 | align-items: center;
5 | min-height: 28px;
6 |
7 | &__text {
8 | flex: 1 1 auto;
9 | overflow: hidden;
10 | }
11 |
12 | &__delete.ui__button.ui__iconButton {
13 | height: 28px;
14 | width: 28px;
15 | min-height: 28px;
16 | min-width: 28px;
17 | margin-left: $padding-tiny;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/FilePicker/__snapshots__/FilePicker.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`FilePicker component snapshot renders default filePicker 1`] = `
4 |
7 | `;
8 |
--------------------------------------------------------------------------------
/src/components/FilePicker/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FilePicker';
2 |
--------------------------------------------------------------------------------
/src/components/FilePlaceholder/FilePlaceholder.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FilePlaceholder } from '../FilePlaceholder';
3 | import readme from './README.md';
4 | import { text } from '@storybook/addon-knobs';
5 |
6 | export default { title: 'Components/FilePlaceholder', component: FilePlaceholder, parameters: { readme } };
7 |
8 | export const Basic = () => ;
9 |
--------------------------------------------------------------------------------
/src/components/FilePlaceholder/FilePlaceholder.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { FilePlaceholder } from '../FilePlaceholder';
4 |
5 | describe('FilePlaceholder component', () => {
6 | it('renders its contents', () => {
7 | const filePlaceholder = shallow( );
8 | expect(filePlaceholder.find('.ui__filePlaceholder')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default filePlaceholder', () => {
12 | const filePlaceholder = shallow( );
13 | expect(filePlaceholder).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/FilePlaceholder/FilePlaceholder.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { FC } from 'react';
3 |
4 | export interface FilePlaceholderProps {
5 | /**
6 | * Classname of the placeholder wrapper.
7 | */
8 | className?: string;
9 | /**
10 | * The file extension to display on the placeholder.
11 | */
12 | extension?: string;
13 | }
14 |
15 | export const FilePlaceholder: FC = ({ className, extension }) => {
16 | const filePlaceholderClass = classnames('ui__base ui__filePlaceholder', className);
17 |
18 | const formattedExtension = extension && `.${extension.replace(/^\./, '')}`;
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {formattedExtension ?
{formattedExtension}
: undefined}
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/FilePlaceholder/README.md:
--------------------------------------------------------------------------------
1 | Placeholder for files. You can include an extension to add it to the bottom of
2 | the placeholder. These should be used when you are unable to generate thumbnails
3 | for a given file, for example an unsupported file type.
4 |
--------------------------------------------------------------------------------
/src/components/FilePlaceholder/_FilePlaceholder.scss:
--------------------------------------------------------------------------------
1 | .ui__filePlaceholder {
2 | background-color: white;
3 | padding: 10px;
4 | height: 164px;
5 | width: 128px;
6 | position: relative;
7 |
8 | &__block {
9 | background-color: #ccc;
10 |
11 | &--thumbnail {
12 | width: 40px;
13 | height: 40px;
14 | float: left;
15 | }
16 |
17 | &--line-sm {
18 | margin-left: 50px;
19 | margin-top: 10px;
20 | height: 6px;
21 | }
22 |
23 | &--line-xs {
24 | margin-left: 50px;
25 | margin-top: 10px;
26 | width: 40px;
27 | height: 6px;
28 | }
29 |
30 | &--line-df {
31 | margin-top: 20px;
32 | width: 90px;
33 | height: 6px;
34 | }
35 |
36 | &--line-lg {
37 | margin-top: 10px;
38 | width: 90px;
39 | height: 6px;
40 | }
41 |
42 | &--line-lgx {
43 | margin-top: 10px;
44 | height: 6px;
45 | }
46 | }
47 |
48 | &__extension {
49 | position: absolute;
50 | color: #ccc;
51 | text-align: center;
52 | font-size: $font-size-large;
53 | right: 10px;
54 | left: 10px;
55 | bottom: 10px;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/FilePlaceholder/__snapshots__/FilePlaceholder.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`FilePlaceholder component snapshot renders default filePlaceholder 1`] = `
4 |
7 |
10 |
13 |
16 |
19 |
22 |
25 |
28 |
29 | `;
30 |
--------------------------------------------------------------------------------
/src/components/FilePlaceholder/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FilePlaceholder';
2 |
--------------------------------------------------------------------------------
/src/components/FileSkeleton/FileSkeleton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FileSkeleton } from '../FileSkeleton';
3 | import readme from './README.md';
4 |
5 | export default { title: 'Components/FileSkeleton', component: FileSkeleton, parameters: { readme } };
6 |
7 | export const Basic = () => ;
8 |
--------------------------------------------------------------------------------
/src/components/FileSkeleton/FileSkeleton.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { FileSkeleton } from '../FileSkeleton';
4 |
5 | describe('FileSkeleton component', () => {
6 | it('renders its contents', () => {
7 | const fileSkeleton = shallow( );
8 | expect(fileSkeleton.find('.ui__fileSkeleton')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default fileSkeleton', () => {
12 | const fileSkeleton = shallow( );
13 | expect(fileSkeleton).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/FileSkeleton/FileSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React from 'react';
3 |
4 | export interface FileSkeletonProps {
5 | /**
6 | * Classname of the skeleton wrapper.
7 | */
8 | className?: string;
9 | }
10 |
11 | export const FileSkeleton = ({ className }: FileSkeletonProps) => {
12 | const fileSkeletonClass = classnames('ui__base ui__fileSkeleton', className);
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/FileSkeleton/README.md:
--------------------------------------------------------------------------------
1 | Skeleton loading placeholder for files or pages.
2 |
--------------------------------------------------------------------------------
/src/components/FileSkeleton/_FileSkeleton.scss:
--------------------------------------------------------------------------------
1 | .ui__fileSkeleton {
2 | background-color: white;
3 | padding: 10px;
4 | height: 164px;
5 | width: 128px;
6 |
7 | &__block {
8 | @include skeleton;
9 |
10 | &--thumbnail {
11 | width: 40px;
12 | height: 40px;
13 | float: left;
14 | }
15 |
16 | &--line-sm {
17 | margin-left: 50px;
18 | margin-top: 10px;
19 | height: 6px;
20 | }
21 |
22 | &--line-xs {
23 | margin-left: 50px;
24 | margin-top: 10px;
25 | width: 40px;
26 | height: 6px;
27 | }
28 |
29 | &--line-df {
30 | margin-top: 20px;
31 | width: 90px;
32 | height: 6px;
33 | }
34 |
35 | &--line-lg {
36 | margin-top: 10px;
37 | width: 90px;
38 | height: 6px;
39 | }
40 |
41 | &--line-lgx {
42 | margin-top: 10px;
43 | height: 6px;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/FileSkeleton/__snapshots__/FileSkeleton.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`FileSkeleton component snapshot renders default fileSkeleton 1`] = `
4 |
7 |
10 |
13 |
16 |
19 |
22 |
25 |
28 |
29 | `;
30 |
--------------------------------------------------------------------------------
/src/components/FileSkeleton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FileSkeleton';
2 |
--------------------------------------------------------------------------------
/src/components/FocusTrap/FocusTrap.stories.scss:
--------------------------------------------------------------------------------
1 | .App {
2 | font-family: sans-serif;
3 | text-align: center;
4 |
5 | &__lockzone {
6 | border: 1px solid gray;
7 | background-color: lightgray;
8 | padding: 16px;
9 | margin-top: 16px;
10 | }
11 |
12 | &__lockzone--locked {
13 | background-color: lightblue;
14 | }
15 |
16 | input {
17 | margin-right: 8px;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/FocusTrap/FocusTrap.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean } from '@storybook/addon-knobs';
2 | import React, { useState } from 'react';
3 | import { useFocusTrap } from '../../hooks';
4 | import { FocusTrap } from '../FocusTrap';
5 | import readme from './README.md';
6 |
7 | export default { title: 'Components/FocusTrap', component: FocusTrap, parameters: { readme } };
8 |
9 | export const Basic = () => {
10 | const [showLock, setShowLock] = useState(false);
11 | return (
12 |
13 |
14 |
setShowLock(true)}>Lock from outside
15 |
16 |
17 |
Zone is {showLock ? 'locked' : 'unlocked'}
18 |
19 |
setShowLock(prev => !prev)}>{showLock ? 'Unlock' : 'Lock from inside'}
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export const JustUseFocusTrapHook = () => {
27 | const [showLock, setShowLock] = useState(false);
28 | const focusRef = useFocusTrap(showLock, { focusLastOnUnlock: boolean('focusLastOnUnlock', false) });
29 | return (
30 |
31 |
32 |
setShowLock(true)}>Lock from outside
33 |
34 |
Zone is {showLock ? 'locked' : 'unlocked'}
35 |
36 |
setShowLock(prev => !prev)}>{showLock ? 'Unlock' : 'Lock from inside'}
37 |
38 |
39 | );
40 | };
41 |
42 | JustUseFocusTrapHook.story = { name: 'Just useFocusTrap Hook' };
43 |
--------------------------------------------------------------------------------
/src/components/FocusTrap/FocusTrap.tsx:
--------------------------------------------------------------------------------
1 | import { cloneElement, FC, ReactElement, RefAttributes } from 'react';
2 | import { useFocusTrap, UseFocusTrapOptions } from '../../hooks';
3 |
4 | export interface FocusLockProps extends UseFocusTrapOptions {
5 | /**
6 | * When true, focus will be locked within the child element.
7 | */
8 | locked?: boolean;
9 | /**
10 | * A child with a targetable ref to lock focus on. This component must accept
11 | * the `ref` prop in order to work with `FocusTrap`.
12 | */
13 | children: ReactElement>;
14 | }
15 |
16 | export const FocusTrap: FC = ({ locked, focusLastOnUnlock, children }) => {
17 | const focusRef = useFocusTrap(locked, { focusLastOnUnlock });
18 |
19 | return cloneElement(children, { ref: focusRef });
20 | };
21 |
--------------------------------------------------------------------------------
/src/components/FocusTrap/README.md:
--------------------------------------------------------------------------------
1 | Traps the focus within the child component when `locked` is true. This wraps the
2 | hook `useFocusTrap`, so you can just use that if you'd prefer a hook.
3 |
4 |
34 |
--------------------------------------------------------------------------------
/src/components/FocusTrap/_FocusTrap.scss:
--------------------------------------------------------------------------------
1 | .ui__focusTrap {
2 | color: blue;
3 |
4 | &--disabled {
5 | color: red;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/FocusTrap/index.ts:
--------------------------------------------------------------------------------
1 | export * from './FocusTrap';
2 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { selectIcon } from '../../storybook-helpers/knobs/selectIcon';
3 | import { Icon } from '../Icon';
4 | import readme from './README.md';
5 |
6 | export default { title: 'Components/Icon', component: Icon, parameters: { readme } };
7 |
8 | export const Basic = () => ;
9 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { Icon } from '../Icon';
4 |
5 | describe('Icon component', () => {
6 | it('renders its contents', () => {
7 | const icon = shallow( );
8 | expect(icon.find('.ui__icon')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default icon', () => {
12 | const icon = shallow( );
13 | expect(icon).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { forwardRef, HTMLAttributes, ReactNode, SVGProps, useMemo } from 'react';
3 | import * as icons from './../../icons';
4 |
5 | export type AvailableIcons = keyof typeof icons;
6 |
7 | export interface IconProps extends HTMLAttributes {
8 | /**
9 | * Specify one of the included icons from the toolkit. If provided, do not add
10 | * `children` or they will override this.
11 | */
12 | icon?: AvailableIcons;
13 | /**
14 | * Props that will be passed to the included icons. This will not be used if
15 | * `children` is provided.
16 | */
17 | svgProps?: SVGProps;
18 | /**
19 | * Provide a custom child instead of a provided icon. Will override `icon` if
20 | * provided.
21 | */
22 | children?: ReactNode;
23 | }
24 |
25 | export const Icon = forwardRef(
26 | ({ icon, svgProps, className, children, ...props }, ref) => {
27 | const iconClass = classnames('ui__base ui__icon', className);
28 |
29 | const child = useMemo(() => {
30 | if (children !== undefined) return children;
31 | if (icons === undefined) return undefined;
32 | const IconChild = icons[icon!];
33 | return ;
34 | }, [children, icon, svgProps]);
35 |
36 | return (
37 |
38 | {child}
39 |
40 | );
41 | },
42 | );
43 |
--------------------------------------------------------------------------------
/src/components/Icon/README.md:
--------------------------------------------------------------------------------
1 | Icon allows you to specify one of the included icons using the `icon` prop, or
2 | add your own custom child icon.
3 |
--------------------------------------------------------------------------------
/src/components/Icon/_Icon.scss:
--------------------------------------------------------------------------------
1 | .ui__icon {
2 | display: inline-flex;
3 | justify-content: center;
4 | align-items: center;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/Icon/__snapshots__/Icon.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Icon component snapshot renders default icon 1`] = `
4 |
7 |
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/src/components/Icon/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Icon';
2 |
--------------------------------------------------------------------------------
/src/components/IconButton/IconButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { action } from '../../storybook-helpers/action/action';
4 | import { Icon } from '../Icon/Icon';
5 | import { IconButton } from '../IconButton';
6 | import readme from './README.md';
7 |
8 | export default { title: 'Components/IconButton', component: IconButton, parameters: { readme } };
9 |
10 | export const Basic = () => (
11 |
12 |
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/components/IconButton/IconButton.test.tsx:
--------------------------------------------------------------------------------
1 | import { mount, shallow } from 'enzyme';
2 | import React from 'react';
3 | import { spy } from 'sinon';
4 | import { IconButton } from '../IconButton';
5 |
6 | describe('IconButton component', () => {
7 | it('renders its contents', () => {
8 | const iconButton = shallow( );
9 | expect(iconButton.find('.ui__iconButton')).toHaveLength(1);
10 | });
11 |
12 | it('snapshot renders default iconButton', () => {
13 | const iconButton = shallow( );
14 | expect(iconButton).toMatchSnapshot();
15 | });
16 |
17 | it('clicking iconButton triggers onClick prop', () => {
18 | const onClick = spy();
19 | shallow( ).simulate('click');
20 | expect(onClick.callCount).toBe(1);
21 | });
22 |
23 | it('clicking disabled iconButton does not trigger onClick prop', () => {
24 | const onClick = spy();
25 | // full DOM mount so `iconButton` element will use disabled prop
26 | mount( ).simulate('click');
27 | expect(onClick.callCount).toBe(0);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/src/components/IconButton/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { ButtonHTMLAttributes, forwardRef } from 'react';
3 | import { Button } from '../Button';
4 |
5 | export interface IconButtonProps extends ButtonHTMLAttributes {
6 | /**
7 | * Defaults to 'button' instead of 'submit' to prevent accidental submissions.
8 | * @default "button"
9 | */
10 | type?: ButtonHTMLAttributes['type'];
11 | }
12 |
13 | export const IconButton = forwardRef(({ children, className, ...props }, ref) => {
14 | const iconButtonClass = classnames('ui__base ui__iconButton', className);
15 |
16 | return (
17 |
18 | {children}
19 |
20 | );
21 | });
22 |
--------------------------------------------------------------------------------
/src/components/IconButton/README.md:
--------------------------------------------------------------------------------
1 | A wrapper for `Button` with custom styles to optimize it for icons.
2 |
--------------------------------------------------------------------------------
/src/components/IconButton/_IconButton.scss:
--------------------------------------------------------------------------------
1 | .ui__button.ui__iconButton {
2 | width: auto;
3 | min-width: auto;
4 | padding: 0 3px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/components/IconButton/__snapshots__/IconButton.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`IconButton component snapshot renders default iconButton 1`] = `
4 |
8 | `;
9 |
--------------------------------------------------------------------------------
/src/components/IconButton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './IconButton';
2 |
--------------------------------------------------------------------------------
/src/components/Image/Image.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { CSSProperties } from 'react';
2 | import { Image } from '../Image';
3 | import { Spinner } from '../Spinner';
4 | import readme from './README.md';
5 | import { boolean } from '@storybook/addon-knobs';
6 |
7 | export default { title: 'Components/Image', component: Image, parameters: { readme } };
8 |
9 | const style: CSSProperties = {
10 | height: 250,
11 | width: 250,
12 | display: 'flex',
13 | justifyContent: 'center',
14 | alignItems: 'center',
15 | backgroundColor: 'lightgray',
16 | };
17 |
18 | const IMAGE =
19 | 'https://www.webfx.com/blog/images/cdn.designinstruct.com/files/582-how-to-image-placeholders/generic-image-placeholder.png';
20 |
21 | export const Basic = () => (
22 |
23 | } pending={boolean('pending', false)} />
24 |
25 | );
26 |
27 | export const WithSrcPromise = () => (
28 |
29 | setTimeout(() => res(IMAGE), 500))} onRenderLoading={() => } />
30 |
31 | );
32 |
33 | export const SrcPromiseRejects = () => (
34 |
35 | setTimeout(() => rej(), 500))}
37 | onRenderLoading={() => }
38 | onRenderFallback={() => 'Rejected source promise'}
39 | pending={boolean('pending', false)}
40 | />
41 |
42 | );
43 |
44 | export const SrcPromiseReturnsFalsy = () => (
45 |
46 | setTimeout(() => res(''), 500))}
48 | onRenderLoading={() => }
49 | onRenderFallback={() => 'Falsy source'}
50 | pending={boolean('pending', false)}
51 | />
52 |
53 | );
54 |
--------------------------------------------------------------------------------
/src/components/Image/Image.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { Image } from '../Image';
4 |
5 | describe('Image component', () => {
6 | it('snapshot renders default image', () => {
7 | const image = shallow( );
8 | expect(image).toMatchSnapshot();
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/src/components/Image/Image.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { forwardRef, ImgHTMLAttributes, ReactNode, useCallback, useEffect, useState } from 'react';
3 | import { FuturableOrLazy, futureableOrLazyToFuturable } from '../../data';
4 | import { Remove } from '../../utils';
5 |
6 | export interface ImageProps extends Remove, 'src'> {
7 | /**
8 | * The image source can be a `Futurable` or `LazyFuturable`, or undefined. If
9 | * undefined or if a promise will display as loading.
10 | */
11 | src?: FuturableOrLazy;
12 | /**
13 | * Manually set whether image should show loading state.
14 | */
15 | pending?: boolean;
16 | /**
17 | * Render out an element to be shown while src is loading.
18 | */
19 | onRenderLoading?(): ReactNode;
20 | /**
21 | * Render out an element to be shown if the image fails to load src, or src
22 | * is falsy.
23 | */
24 | onRenderFallback?(): ReactNode;
25 | }
26 |
27 | export const Image = forwardRef(
28 | ({ src, pending, onRenderLoading, onRenderFallback, alt, className, ...imgProps }, ref) => {
29 | const sourceIsNotPromise = typeof src === 'string' || !src;
30 | const [loading, setLoading] = useState(!sourceIsNotPromise);
31 | const [source, setSource] = useState(
32 | sourceIsNotPromise ? (src as string | undefined) : undefined,
33 | );
34 |
35 | const getSource = useCallback(async (srcGetter: FuturableOrLazy) => {
36 | setLoading(true);
37 | let fetchedSource = undefined;
38 | try {
39 | fetchedSource = await futureableOrLazyToFuturable(srcGetter);
40 | } catch {}
41 | setLoading(false);
42 | setSource(fetchedSource || undefined);
43 | }, []);
44 |
45 | useEffect(() => {
46 | if (sourceIsNotPromise) {
47 | setLoading(false);
48 | setSource((src as string | undefined) || undefined);
49 | return;
50 | }
51 | getSource(src);
52 | }, [getSource, sourceIsNotPromise, src]);
53 |
54 | const imageClass = classnames('ui__image', className);
55 |
56 | if (loading || pending) return <>{onRenderLoading?.()}>;
57 | if (!source) return <>{onRenderFallback?.()}>;
58 | return ;
59 | },
60 | );
61 |
--------------------------------------------------------------------------------
/src/components/Image/README.md:
--------------------------------------------------------------------------------
1 | This is an image that handles multiple `src` parameters. Specifically, it can
2 | accept promises, as well as functions that return the `src` string or a promise
3 | for the string. If you want to throttle the fetching of the image, you can do so
4 | by giving a function for `src`. You can provide a loading placeholder using the
5 | `onRenderLoading` prop.
6 |
--------------------------------------------------------------------------------
/src/components/Image/_Image.scss:
--------------------------------------------------------------------------------
1 | .ui__image {
2 | max-width: 100%;
3 | max-height: 100%;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/Image/__snapshots__/Image.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Image component snapshot renders default image 1`] = ` `;
4 |
--------------------------------------------------------------------------------
/src/components/Image/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Image';
2 |
--------------------------------------------------------------------------------
/src/components/Input/Input.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { Input } from '../Input';
4 |
5 | describe('Input component', () => {
6 | it('renders its contents', () => {
7 | const input = shallow( );
8 | expect(input.find('.ui__input')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default input', () => {
12 | const input = shallow( );
13 | expect(input).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Input/README.md:
--------------------------------------------------------------------------------
1 | A simple styled input with the ability to show error or warning feedback.
2 |
--------------------------------------------------------------------------------
/src/components/Input/_Input.scss:
--------------------------------------------------------------------------------
1 | .ui__input__wrapper {
2 | width: 220px;
3 |
4 | &--fill {
5 | width: 100%;
6 | }
7 |
8 | &--pad {
9 | padding-bottom: 20px;
10 | }
11 | }
12 |
13 | .ui__input {
14 | border-radius: $border-radius;
15 | transition: $focus-transition;
16 | background-color: $color-gray-1;
17 | border: 1px solid $color-gray-4;
18 | display: inline-flex;
19 | align-items: center;
20 | width: 100%;
21 |
22 | &--message {
23 | &-default {
24 | &.ui__input--focused {
25 | border: 1px solid $color-theme-primary;
26 | }
27 |
28 | &.ui__input--focused {
29 | @include focus;
30 | }
31 | }
32 |
33 | &-warning {
34 | border-color: $color-message-warning;
35 |
36 | i {
37 | fill: $color-message-warning;
38 | }
39 |
40 | &.ui__input--focused {
41 | box-shadow: 0 0 0 2px var(--color-message-warning-focus-shadow);
42 | }
43 | }
44 |
45 | &-error {
46 | border-color: $color-message-error;
47 |
48 | i {
49 | fill: $color-message-error;
50 | }
51 |
52 | &.ui__input--focused {
53 | box-shadow: 0 0 0 2px var(--color-message-error-focus-shadow);
54 | }
55 | }
56 | }
57 |
58 | .ui__input__input {
59 | flex: 1 1 auto;
60 | padding: $padding-tiny;
61 | height: 28px;
62 | outline: none;
63 | border: none;
64 | background-color: transparent;
65 | color: $color-font-primary;
66 | min-height: 24px;
67 |
68 | &--disabled {
69 | opacity: 0.7;
70 | }
71 | }
72 |
73 | &__messageText {
74 | width: 100%;
75 | font-size: $font-size-small;
76 | margin-top: $padding-tiny;
77 | }
78 |
79 | // Override any icon buttons so they have no padding.
80 | .ui__iconButton {
81 | border: none;
82 | padding: 0;
83 | min-height: 0;
84 | }
85 |
86 | // Override any icons so that they have an internal padding.
87 | .ui__icon {
88 | padding: 0 2px;
89 | }
90 |
91 | svg {
92 | fill: $color-blue-gray-5;
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/components/Input/__snapshots__/Input.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Input component snapshot renders default input 1`] = `
4 |
18 | `;
19 |
--------------------------------------------------------------------------------
/src/components/Input/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Input';
2 |
--------------------------------------------------------------------------------
/src/components/Label/Label.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean, text } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { Input } from '../Input';
4 | import { Label } from '../Label';
5 | import readme from './README.md';
6 |
7 | export default { title: 'Components/Label', component: Label, parameters: { readme } };
8 |
9 | export const Basic = () => (
10 |
14 |
15 |
16 | );
17 |
18 | export const WithCustomId = () => (
19 |
23 |
24 |
25 | );
26 |
27 | export const WithDisabledFormElement = () => (
28 |
32 |
33 |
34 | );
35 |
36 | export const Detached = () => (
37 | <>
38 |
43 | Some other stuff
44 |
45 | >
46 | );
47 |
--------------------------------------------------------------------------------
/src/components/Label/Label.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { Label } from '../Label';
4 |
5 | describe('Label component', () => {
6 | it('renders its contents', () => {
7 | const label = shallow( );
8 | expect(label.find('.ui__label')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default label', () => {
12 | const label = shallow( );
13 | expect(label).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Label/Label.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { cloneElement, FC, LabelHTMLAttributes, ReactElement, ReactNode } from 'react';
3 | import { useID } from '../../hooks/useID';
4 |
5 | export interface LabelProps extends LabelHTMLAttributes {
6 | /**
7 | * The label to apply to any form element.
8 | */
9 | label: ReactNode;
10 | /**
11 | * Provide a string specifying that this field is optional.
12 | */
13 | optionalText?: string;
14 | /**
15 | * Pass a child element which can accept an `id` prop. If you don't specify
16 | * the `id` prop, one will be generated to link the label to the element. If
17 | * you wish to link this label to a form field without passing children, you
18 | * should specify the `htmlFor` prop with the `id` of your form field.
19 | */
20 | children?: ReactElement;
21 | }
22 |
23 | export const Label: FC = ({ label, optionalText, children, className, htmlFor, ...props }) => {
24 | const childrenId = children?.props.id;
25 | const id = useID(childrenId);
26 |
27 | const labelClass = classnames(
28 | 'ui__base ui__label',
29 | { 'ui__label--disabled': children?.props.disabled, 'ui__label--attached': children },
30 | className,
31 | );
32 |
33 | return (
34 | <>
35 |
36 | {label}
37 | {optionalText ? {optionalText} : undefined}
38 |
39 | {children ? cloneElement(children, { id }) : undefined}
40 | >
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/Label/README.md:
--------------------------------------------------------------------------------
1 | A generic label for labelling form fields. If the child form field has an `id`
2 | specified, that will be automatically parsed and used in the label's `htmlFor`
3 | attribute to link the two. If no `id` is specified for the child, one will be
4 | automatically generated.
5 |
6 | You can also link the label to a form field without passing it as a child by
7 | giving the field an `id` and passing the same `id` to the `htmlFor` prop for the
8 | input. This is useful if the two elements do not sit near each other on the
9 | page.
10 |
11 | If the child element is disabled, the label will automatically change appearance
12 | to a disabled state. This will not happen if the form field is not the child.
13 |
--------------------------------------------------------------------------------
/src/components/Label/_Label.scss:
--------------------------------------------------------------------------------
1 | .ui__label {
2 | &--attached::after {
3 | height: 2px;
4 | width: 1px;
5 | content: '';
6 | display: block;
7 | }
8 |
9 | &--disabled {
10 | opacity: 0.7;
11 | }
12 |
13 | &__optional {
14 | font-size: $font-size-small;
15 | margin-left: $padding-tiny;
16 | color: $color-font-secondary;
17 | font-style: italic;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Label/__snapshots__/Label.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Label component snapshot renders default label 1`] = `
4 |
5 |
9 |
10 | `;
11 |
--------------------------------------------------------------------------------
/src/components/Label/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Label';
2 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { Modal } from '../Modal';
4 |
5 | describe('Modal component', () => {
6 | it('renders its contents when open', () => {
7 | const modal = shallow(
8 |
9 | children
10 | ,
11 | );
12 | expect(modal.find('.ui__modal')).toHaveLength(1);
13 | });
14 |
15 | it('hides its contents when closed', () => {
16 | const modal = shallow(children );
17 | expect(modal.find('.ui__modal')).toHaveLength(0);
18 | });
19 |
20 | it('snapshot renders default modal', () => {
21 | const modal = shallow(children );
22 | expect(modal).toMatchSnapshot();
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/components/Modal/README.md:
--------------------------------------------------------------------------------
1 | Modals provide the ability to add confirmation or display important information
2 | to the user. These can be easily dismissed by adding an `onClose`, as well as
3 | `closeOnBackgroundClick` and `closeOnEscape` at your discretion.
4 |
5 | Alternatively, they can enforce that the user completes the required action by
6 | removing the `onClose` and adding your own confirmation buttons to the
7 | `buttonGroup`.
8 |
--------------------------------------------------------------------------------
/src/components/Modal/_Modal.scss:
--------------------------------------------------------------------------------
1 | .ui__modal__wrapper {
2 | @include absolute-fill;
3 | @include opacity-transition;
4 | opacity: 1;
5 | background-color: $color-overlay-canvas;
6 | overflow: auto;
7 | display: flex;
8 |
9 | padding: $padding $padding 0;
10 |
11 | @include for-tablet-up {
12 | padding: $padding-huge $padding-huge 0;
13 | }
14 |
15 | &--closed {
16 | opacity: 0;
17 | pointer-events: none !important;
18 | }
19 |
20 | &--fullWidth {
21 | .ui__modal__paddingFix {
22 | max-width: none !important;
23 | }
24 | }
25 | }
26 |
27 | // This fixes the firefox bottom padding bug.
28 | .ui__modal__paddingFix {
29 | padding: 0 0 $padding;
30 | margin: auto;
31 | width: 100%;
32 |
33 | @include for-tablet-up {
34 | padding: 0 0 $padding-huge;
35 | max-width: 500px;
36 | }
37 | }
38 |
39 | .ui__modal {
40 | background-color: $color-gray-2;
41 | border-radius: $border-radius;
42 | overflow: hidden;
43 | width: 100%;
44 |
45 | &--hidden {
46 | pointer-events: none;
47 | visibility: hidden;
48 | }
49 |
50 | &__top {
51 | display: flex;
52 |
53 | &__heading {
54 | font-size: $font-size-large;
55 | flex: 1 1 auto;
56 | padding: $padding-medium $padding;
57 | }
58 |
59 | &__close.ui__button {
60 | padding: 0 calc(#{$padding-medium} - 2px);
61 | margin: 2px;
62 | }
63 | }
64 |
65 | &__body {
66 | padding: $padding-medium $padding;
67 | background-color: $color-gray-1;
68 | border-top: 1px solid $color-gray-4;
69 | border-bottom: 1px solid $color-gray-4;
70 |
71 | &--noButton {
72 | border-bottom: none;
73 | }
74 | }
75 |
76 | &__buttonGroup {
77 | padding: $padding-medium $padding;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/Modal/__snapshots__/Modal.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Modal component snapshot renders default modal 1`] = `
4 |
5 |
9 |
10 | `;
11 |
--------------------------------------------------------------------------------
/src/components/Modal/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Modal';
2 |
--------------------------------------------------------------------------------
/src/components/Overlay/Overlay.scss:
--------------------------------------------------------------------------------
1 | .ui__overlay {
2 | @include fixed-fill;
3 | z-index: $z-index-overlay;
4 | pointer-events: none;
5 |
6 | > * {
7 | pointer-events: all;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/Overlay/Overlay.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean } from '@storybook/addon-knobs';
2 | import React, { CSSProperties } from 'react';
3 | import { Overlay } from '../Overlay';
4 | import readme from './README.md';
5 |
6 | export default { title: 'Components/Overlay', component: Overlay, parameters: { readme } };
7 |
8 | const style: CSSProperties = {
9 | backgroundColor: 'lightblue',
10 | color: 'black',
11 | margin: 32,
12 | padding: 32,
13 | textAlign: 'center',
14 | };
15 |
16 | export const Basic = () => {
17 | const mounted = boolean('mounted', true);
18 | return (
19 | <>
20 | {mounted && (
21 |
22 | Overlay #1
23 |
24 | )}
25 | >
26 | );
27 | };
28 |
29 | export const Multiple = () => {
30 | const mounted1 = boolean('#1 mounted', true);
31 |
32 | const mounted2 = boolean('#2 mounted', true);
33 |
34 | return (
35 | <>
36 | {mounted1 ? (
37 |
38 | Overlay #1
39 |
40 | ) : (
41 | undefined
42 | )}
43 | {mounted2 ? (
44 |
45 | Overlay #2
46 |
47 | ) : (
48 | undefined
49 | )}
50 | >
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/components/Overlay/Overlay.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect } from 'react';
2 | import { createPortal } from 'react-dom';
3 |
4 | export interface OverlayProps {
5 | /**
6 | * Optional className for the overlay container.
7 | */
8 | className?: string;
9 | }
10 |
11 | function generateOverlayLayer() {
12 | let currentId = 1;
13 | const elements = new Set();
14 | const classes: { [className: string]: number } = {};
15 |
16 | const overlayRoot = document.createElement('div');
17 | overlayRoot.classList.add('ui__base', 'ui__overlay');
18 |
19 | const appendElement = () => document.body.appendChild(overlayRoot);
20 | const removeElement = () => document.body.removeChild(overlayRoot);
21 |
22 | const addClass = (className?: string) => {
23 | if (!className) return;
24 | overlayRoot.classList.add(className);
25 | classes[className] = (classes[className] || 0) + 1;
26 | };
27 |
28 | const removeClass = (className?: string) => {
29 | if (!className) return;
30 | classes[className] = (classes[className] || 0) - 1;
31 | if (classes[className] <= 0) {
32 | delete classes[className];
33 | overlayRoot.classList.remove(className);
34 | }
35 | };
36 |
37 | const add = (props: OverlayProps) => {
38 | const id = currentId++;
39 |
40 | addClass(props.className);
41 | elements.add(id);
42 | if (elements.size === 1) appendElement();
43 |
44 | return () => {
45 | elements.delete(id);
46 | removeClass(props.className);
47 | if (elements.size === 0) removeElement();
48 | };
49 | };
50 |
51 | return (({ children, className }) => {
52 | useEffect(() => add({ className }), [className]);
53 | return createPortal(children, overlayRoot);
54 | }) as FC;
55 | }
56 |
57 | let Overlay: FC;
58 |
59 | if (typeof window !== 'undefined') {
60 | Overlay = generateOverlayLayer();
61 | } else {
62 | Overlay = () => null;
63 | }
64 |
65 | export { Overlay };
66 |
--------------------------------------------------------------------------------
/src/components/Overlay/README.md:
--------------------------------------------------------------------------------
1 | Passing children to `Overlay` will cause them to be rendered inside of an
2 | overlay on top of any other components. This can be used for modals, tooltips,
3 | or any other kind of overlay component.
4 |
5 | ## Multiple Components
6 |
7 | Since components use [React portals](https://reactjs.org/docs/portals.html) to
8 | mount into the overlay root, they are inserted in the order they are mounted.
9 | Therefore, if you un-mount and remount a component, it will be inserted as the
10 | last child, regardless of it's position in the React tree.
11 |
--------------------------------------------------------------------------------
/src/components/Overlay/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Overlay';
2 |
--------------------------------------------------------------------------------
/src/components/Spinner/README.md:
--------------------------------------------------------------------------------
1 | Shows non-specific progress. Best used if there is an unknown amount of time
2 | before the intended action completes.
3 |
--------------------------------------------------------------------------------
/src/components/Spinner/Spinner.stories.tsx:
--------------------------------------------------------------------------------
1 | import { select } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { Spinner } from '../Spinner';
4 | import readme from './README.md';
5 |
6 | export default { title: 'Components/Spinner', component: Spinner, parameters: { readme } };
7 |
8 | export const Basic = () => (
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/components/Spinner/Spinner.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { Spinner } from '../Spinner';
4 |
5 | describe('Spinner component', () => {
6 | it('renders its contents', () => {
7 | const spinner = shallow( );
8 | expect(spinner.find('.ui__spinner')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default spinner', () => {
12 | const spinner = shallow( );
13 | expect(spinner).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Spinner/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React from 'react';
3 |
4 | export interface SpinnerProps {
5 | /**
6 | * Set the size of the spinner.
7 | * @default "default"
8 | */
9 | spinnerSize?: 'tiny' | 'small' | 'default' | 'large';
10 | /**
11 | * Classname for the container div.
12 | */
13 | className?: string;
14 | }
15 |
16 | export const Spinner = ({ spinnerSize = 'default', className }: SpinnerProps) => {
17 | const spinnerClass = classnames('ui__base ui__spinner', `ui__spinner--size-${spinnerSize}`, className);
18 |
19 | return (
20 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/Spinner/_Spinner.scss:
--------------------------------------------------------------------------------
1 | .ui__spinner {
2 | display: inline-block;
3 |
4 | &__animated {
5 | @include spinner;
6 | width: 100%;
7 | height: 100%;
8 | border-radius: 50%;
9 | border: solid;
10 | border-color: $color-blue-gray-4 $color-blue-gray-1 $color-blue-gray-1;
11 | }
12 |
13 | /* --- Spinner sizes. --- */
14 |
15 | &--size {
16 | &-tiny {
17 | width: 12px;
18 | height: 12px;
19 |
20 | .ui__spinner__animated {
21 | border-width: 1px;
22 | }
23 | }
24 |
25 | &-small {
26 | width: 16px;
27 | height: 16px;
28 |
29 | .ui__spinner__animated {
30 | border-width: 2px;
31 | }
32 | }
33 |
34 | &-default {
35 | width: 24px;
36 | height: 24px;
37 |
38 | .ui__spinner__animated {
39 | border-width: 2px;
40 | }
41 | }
42 |
43 | &-large {
44 | width: 32px;
45 | height: 32px;
46 |
47 | .ui__spinner__animated {
48 | border-width: 2px;
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/Spinner/__snapshots__/Spinner.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Spinner component snapshot renders default spinner 1`] = `
4 |
11 | `;
12 |
--------------------------------------------------------------------------------
/src/components/Spinner/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Spinner';
2 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/README.md:
--------------------------------------------------------------------------------
1 | A thumbnail representation of a file. This can be used as a good default for
2 | `FileOrganizer`.
3 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/Thumbnail.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean, text } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { action } from '../../storybook-helpers/action/action';
4 | import { createFile, CreateFileOptions, FakeFile } from '../../storybook-helpers/data/files';
5 | import { Icon } from '../Icon';
6 | import { Thumbnail, ThumbnailProps } from '../Thumbnail';
7 | import readme from './README.md';
8 |
9 | export default { title: 'Components/Thumbnail', component: Thumbnail, parameters: { readme } };
10 |
11 | const defaultProps = (options?: CreateFileOptions, index = 0, withToolButtons?: boolean): ThumbnailProps => ({
12 | file: createFile(index, options),
13 | selected: boolean('selected', false),
14 | disabled: boolean('disabled', false),
15 | dragging: boolean('dragging', false),
16 | otherDragging: boolean('otherDragging', false),
17 | onClick: boolean('has onClick', true) ? action('onClick') : undefined,
18 | onRename: boolean('has onRename', true) ? action('onRename') : undefined,
19 | buttonProps: withToolButtons
20 | ? [
21 | { children: , onClick: action('rotate onClick'), key: 0 },
22 | { children: , onClick: action('close onClick'), key: 1 },
23 | ]
24 | : undefined,
25 | });
26 |
27 | export const Basic = () => ;
28 |
29 | export const Expensive = () => ;
30 |
31 | export const Rejected = () => ;
32 |
33 | export const WithToolButtons = () => ;
34 |
35 | export const WithLabel = () => ;
36 |
37 | export const SelectedIcon = () => (
38 | {text('selectedIcon', 'CUSTOM!')}}
41 | />
42 | );
43 |
44 | export const Rotated = () => ;
45 |
46 | export const RotatedThrottled = () => ;
47 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/Thumbnail.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { spy } from 'sinon';
4 | import { createFile } from '../../storybook-helpers/data/files';
5 | import { Thumbnail } from '../Thumbnail';
6 |
7 | const testFile = createFile(0);
8 |
9 | describe('Thumbnail component', () => {
10 | it('renders its contents', () => {
11 | const thumbnail = shallow( );
12 | expect(thumbnail.find('.ui__thumbnail')).toHaveLength(1);
13 | });
14 |
15 | it('snapshot renders default thumbnail', () => {
16 | const thumbnail = shallow( );
17 | expect(thumbnail).toMatchSnapshot();
18 | });
19 |
20 | it('clicking thumbnail triggers onClick prop', () => {
21 | const onClick = spy();
22 | shallow( ).simulate('click');
23 | expect(onClick.callCount).toBe(1);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/__snapshots__/Thumbnail.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Thumbnail component snapshot renders default thumbnail 1`] = `
4 |
10 |
13 |
19 |
20 |
23 |
32 |
33 | `;
34 |
--------------------------------------------------------------------------------
/src/components/Thumbnail/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Thumbnail';
2 |
--------------------------------------------------------------------------------
/src/components/ThumbnailDragLayer/README.md:
--------------------------------------------------------------------------------
1 | This is a thumbnail drag preview which displays the number of pages that are
2 | selected.
3 |
--------------------------------------------------------------------------------
/src/components/ThumbnailDragLayer/ThumbnailDragLayer.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { integer } from '../../storybook-helpers/knobs/integer';
3 | import { ThumbnailDragLayer } from '../ThumbnailDragLayer';
4 | import readme from './README.md';
5 |
6 | export default { title: 'Components/ThumbnailDragLayer', component: ThumbnailDragLayer, parameters: { readme } };
7 |
8 | export const Basic = () => ;
9 |
--------------------------------------------------------------------------------
/src/components/ThumbnailDragLayer/ThumbnailDragLayer.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { ThumbnailDragLayer } from '../ThumbnailDragLayer';
4 |
5 | describe('ThumbnailDragLayer component', () => {
6 | it('renders its contents', () => {
7 | const thumbnailDragLayer = shallow( );
8 | expect(thumbnailDragLayer.find('.ui__thumbnailDragLayer')).toHaveLength(1);
9 | });
10 |
11 | it('snapshot renders default thumbnailDragLayer', () => {
12 | const thumbnailDragLayer = shallow( );
13 | expect(thumbnailDragLayer).toMatchSnapshot();
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/ThumbnailDragLayer/ThumbnailDragLayer.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { HTMLAttributes, useEffect } from 'react';
3 | import { MultiPage, SinglePage } from '../../icons';
4 |
5 | export interface ThumbnailDragLayerProps extends HTMLAttributes {
6 | /**
7 | * Must be a positive integer (1, 2, 3...) or falsy to default.
8 | * @default 1
9 | */
10 | numFiles?: number;
11 | }
12 |
13 | export const ThumbnailDragLayer = ({ numFiles = 1, className, ...divProps }: ThumbnailDragLayerProps) => {
14 | numFiles = numFiles || 1;
15 | useEffect(() => {
16 | if (!Number.isInteger(numFiles)) throw new RangeError('numFiles must be an integer');
17 | if (!Number.isFinite(numFiles)) throw new RangeError('numFiles must not be infinite');
18 | if (numFiles <= 0) throw new RangeError('numFiles must be a positive integer');
19 | }, [numFiles]);
20 |
21 | const thumbnailDragLayerClass = classnames('ui__base ui__thumbnailDragLayer', className);
22 |
23 | return (
24 |
25 |
26 | {numFiles === 1 ? (
27 |
28 | ) : (
29 |
30 | )}
31 | {numFiles > 1 ? (
32 |
33 | {numFiles}
34 |
35 | ) : (
36 | undefined
37 | )}
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/ThumbnailDragLayer/_ThumbnailDragLayer.scss:
--------------------------------------------------------------------------------
1 | .ui__thumbnailDragLayer {
2 | width: $thumbnail-dimensions;
3 | height: $thumbnail-dimensions;
4 | @include flex-center;
5 |
6 | &__icon {
7 | height: $thumbnail-dimensions / 4;
8 | width: $thumbnail-dimensions / 4;
9 | fill: $color-blue-gray-3;
10 | }
11 |
12 | &__numFiles {
13 | @include absolute-fill;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: flex-end;
17 | font-size: $font-size-default;
18 |
19 | &__wrapper {
20 | position: relative;
21 | top: -10px;
22 | right: -10px;
23 | padding: $padding-tiny;
24 | border-radius: $border-radius-large;
25 | background-color: $color-blue-gray-3;
26 | min-width: 20px;
27 | @include flex-center;
28 | }
29 | }
30 |
31 | &__wrapper {
32 | position: relative;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/ThumbnailDragLayer/__snapshots__/ThumbnailDragLayer.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ThumbnailDragLayer component snapshot renders default thumbnailDragLayer 1`] = `
4 |
15 | `;
16 |
--------------------------------------------------------------------------------
/src/components/ThumbnailDragLayer/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ThumbnailDragLayer';
2 |
--------------------------------------------------------------------------------
/src/components/ThumbnailSkeleton/README.md:
--------------------------------------------------------------------------------
1 | A loading placeholder for `Thumbnail`.
2 |
--------------------------------------------------------------------------------
/src/components/ThumbnailSkeleton/ThumbnailSkeleton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ThumbnailSkeleton } from '../ThumbnailSkeleton';
3 | import readme from './README.md';
4 |
5 | export default { title: 'Components/ThumbnailSkeleton', component: ThumbnailSkeleton, parameters: { readme } };
6 |
7 | export const Basic = () => ;
8 |
--------------------------------------------------------------------------------
/src/components/ThumbnailSkeleton/ThumbnailSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import classnames from 'classnames';
2 | import React, { HTMLAttributes } from 'react';
3 | import { FileSkeleton } from '../FileSkeleton';
4 |
5 | export function ThumbnailSkeleton({ className, ...divProps }: HTMLAttributes) {
6 | const thumbnailClass = classnames('ui__base ui__thumbnailSkeleton', className);
7 |
8 | return (
9 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/ThumbnailSkeleton/_ThumbnailSkeleton.scss:
--------------------------------------------------------------------------------
1 | .ui__thumbnailSkeleton {
2 | padding: $padding;
3 | position: relative;
4 | outline: none;
5 | width: $thumbnail-dimensions;
6 | height: $thumbnail-dimensions;
7 | transition: 0.1s background-color;
8 | display: flex;
9 | flex-direction: column;
10 |
11 | &:hover {
12 | background-color: $color-gray-4;
13 | }
14 |
15 | &__image {
16 | @include flex-center;
17 | flex-direction: column;
18 | height: 0;
19 | flex: 1 1 auto;
20 |
21 | .ui__thumbnailSkeleton__image__skeleton {
22 | border: 1px solid $color-blue-gray-1;
23 | transition: $focus-transition;
24 | }
25 | }
26 |
27 | &__label {
28 | margin-top: $padding;
29 | height: 28px;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/ThumbnailSkeleton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ThumbnailSkeleton';
2 |
--------------------------------------------------------------------------------
/src/components/Toast/README.md:
--------------------------------------------------------------------------------
1 | This is a configurable Toast, but it has no inherent animations or timers. In
2 | order to use the Toast as a notification system, you should use the `useToast`
3 | hook.
4 |
--------------------------------------------------------------------------------
/src/components/Toast/Toast.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean, select, text } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { action } from '../../storybook-helpers/action/action';
4 | import { Toast } from '../Toast';
5 | import readme from './README.md';
6 |
7 | export default { title: 'Components/Toast', component: Toast, parameters: { readme } };
8 |
9 | export const Basic = () => (
10 |
11 |
17 | {text('children', '')}
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/components/Toast/Toast.test.tsx:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme';
2 | import React from 'react';
3 | import { Toast } from '../Toast';
4 |
5 | jest.mock('../../utils', () => ({ getStringId: (prefix: string) => `${prefix}_1234` }));
6 |
7 | describe('Toast component', () => {
8 | it('renders its contents', () => {
9 | const toast = shallow( );
10 | expect(toast.find('.ui__toast')).toHaveLength(1);
11 | });
12 |
13 | it('snapshot renders default toast', () => {
14 | const toast = shallow( );
15 | expect(toast).toMatchSnapshot();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/Toast/__snapshots__/Toast.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Toast component snapshot renders default toast 1`] = `
4 |
21 | `;
22 |
--------------------------------------------------------------------------------
/src/components/Toast/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Toast';
2 |
--------------------------------------------------------------------------------
/src/components/ToastProvider/README.md:
--------------------------------------------------------------------------------
1 | A provider to allow you to manage notification toasts in your application. Use
2 | in combination with the `useToast` hook.
3 |
4 | ## Using
5 |
6 | 1. Make sure that you have the `ToastProvider` placed at the root of your
7 | project
8 | 1. Use the `useToast` hook to access the `ToastContext`
9 |
10 | ## Details
11 |
12 | When hovering a toast, any existing timeout will be cancelled, and will restart
13 | once the mouse leaves the toast.
14 |
--------------------------------------------------------------------------------
/src/components/ToastProvider/_ToastProvider.scss:
--------------------------------------------------------------------------------
1 | %toastProvider-bottom {
2 | position: absolute;
3 | bottom: 0;
4 | left: 0;
5 | right: 0;
6 | transform: translateY(100%);
7 | }
8 |
9 | .ui__toastProvider {
10 | opacity: 1;
11 | @include opacity-transition;
12 | position: relative;
13 |
14 | &--closing {
15 | opacity: 0;
16 | }
17 |
18 | &__toast {
19 | display: flex;
20 | padding: $padding;
21 | pointer-events: none !important;
22 |
23 | > * {
24 | pointer-events: all;
25 | }
26 |
27 | @include slide-in;
28 |
29 | &--position {
30 | &-top-left {
31 | transform: translateY(-100%);
32 | justify-content: flex-start;
33 | }
34 |
35 | &-top {
36 | transform: translateY(-100%);
37 | justify-content: center;
38 | }
39 |
40 | &-top-right {
41 | transform: translateY(-100%);
42 | justify-content: flex-end;
43 | }
44 |
45 | &-bottom-left {
46 | @extend %toastProvider-bottom;
47 | justify-content: flex-start;
48 | }
49 |
50 | &-bottom {
51 | @extend %toastProvider-bottom;
52 | justify-content: center;
53 | }
54 |
55 | &-bottom-right {
56 | @extend %toastProvider-bottom;
57 | justify-content: flex-end;
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/ToastProvider/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ToastProvider';
2 |
--------------------------------------------------------------------------------
/src/components/ToolButton/README.md:
--------------------------------------------------------------------------------
1 | A button designed to be used in small toolbars. This button would most likely
2 | take an icon as its child.
3 |
--------------------------------------------------------------------------------
/src/components/ToolButton/ToolButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import { boolean, select } from '@storybook/addon-knobs';
2 | import React from 'react';
3 | import { action } from '../../storybook-helpers/action/action';
4 | import { Icon } from '../Icon';
5 | import { ToolButton } from '../ToolButton';
6 | import readme from './README.md';
7 |
8 | export default { title: 'Components/ToolButton', component: ToolButton, parameters: { readme } };
9 |
10 | export const Basic = () => (
11 |
23 |
24 |
25 | );
26 |
--------------------------------------------------------------------------------
/src/components/ToolButton/ToolButton.test.tsx:
--------------------------------------------------------------------------------
1 | import { mount, shallow } from 'enzyme';
2 | import React from 'react';
3 | import { spy } from 'sinon';
4 | import { ToolButton } from '../ToolButton';
5 |
6 | describe('ToolButton component', () => {
7 | it('renders its contents', () => {
8 | const toolButton = shallow( );
9 | expect(toolButton.find('.ui__toolButton')).toHaveLength(1);
10 | });
11 |
12 | it('snapshot renders default toolButton', () => {
13 | const toolButton = shallow( );
14 | expect(toolButton).toMatchSnapshot();
15 | });
16 |
17 | it('clicking toolButton triggers onClick prop', () => {
18 | const onClick = spy();
19 | mount( )
20 | .find('button.ui__toolButton__action')
21 | .simulate('click');
22 | expect(onClick.callCount).toBe(1);
23 | });
24 |
25 | it('clicking disabled toolButton does not trigger onClick prop', () => {
26 | const onClick = spy();
27 | mount( )
28 | .find('button.ui__toolButton__action')
29 | .simulate('click');
30 | expect(onClick.callCount).toBe(0);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/components/ToolButton/_ToolButton.scss:
--------------------------------------------------------------------------------
1 | $tool-button-background: rgba(175, 175, 175, 0.3);
2 |
3 | %tool-button-background {
4 | &:hover:not(.ui__toolButton--disabled),
5 | &:focus.ui__toolButton--tabbing:not(.ui__toolButton--disabled),
6 | &:active:not(.ui__toolButton--disabled) {
7 | background-color: $tool-button-background;
8 | }
9 | }
10 |
11 | .ui__toolButton {
12 | display: inline-flex;
13 | flex-direction: column;
14 |
15 | &--expanded {
16 | &.ui__toolButton--right {
17 | flex-direction: row;
18 | }
19 |
20 | &.ui__toolButton--bottom {
21 | flex-direction: column;
22 | }
23 | }
24 | }
25 |
26 | .ui__toolButton__action {
27 | @extend %tool-button-background;
28 |
29 | &.ui__toolButton--expanded {
30 | &.ui__toolButton--right {
31 | border-radius: $border-radius 0 0 $border-radius;
32 | }
33 |
34 | &.ui__toolButton--bottom {
35 | border-radius: $border-radius $border-radius 0 0;
36 | }
37 | }
38 |
39 | &__internals {
40 | width: 100%;
41 | height: 100%;
42 | @include flex-center;
43 | }
44 | }
45 |
46 | .ui__toolButton__expand.ui__button {
47 | padding: 0;
48 | @include flex-center;
49 | min-height: 0;
50 |
51 | &.ui__toolButton--right {
52 | border-radius: 0 $border-radius $border-radius 0;
53 | }
54 |
55 | &.ui__toolButton--bottom {
56 | border-radius: 0 0 $border-radius $border-radius;
57 | }
58 |
59 | @extend %tool-button-background;
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/ToolButton/__snapshots__/ToolButton.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ToolButton component snapshot renders default toolButton 1`] = `
4 |
7 |
11 |
12 | `;
13 |
--------------------------------------------------------------------------------
/src/components/ToolButton/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ToolButton';
2 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Component imports: DO NOT MODIFY.
3 | */
4 |
5 | //
6 | export * from './Button';
7 | export * from './ButtonGroup';
8 | export * from './Choice';
9 | export * from './ClickableDiv';
10 | export * from './DndMultiProvider';
11 | export * from './DragLayer';
12 | export * from './Draggable';
13 | export * from './EditableText';
14 | export * from './FileOrganizer';
15 | export * from './FilePicker';
16 | export * from './FilePlaceholder';
17 | export * from './FileSkeleton';
18 | export * from './FocusTrap';
19 | export * from './Icon';
20 | export * from './IconButton';
21 | export * from './Image';
22 | export * from './Input';
23 | export * from './Label';
24 | export * from './Modal';
25 | export * from './Overlay';
26 | export * from './Spinner';
27 | export * from './Thumbnail';
28 | export * from './ThumbnailDragLayer';
29 | export * from './ThumbnailSkeleton';
30 | export * from './Toast';
31 | export * from './ToastProvider';
32 | export * from './ToolButton';
33 | //
34 |
--------------------------------------------------------------------------------
/src/data/futurable.ts:
--------------------------------------------------------------------------------
1 | import { MemoizedPromise } from './memoizedPromise';
2 |
3 | /** Either a promise to return a type `T`, or `T` itself. */
4 | export type Futurable = Promise | T;
5 |
6 | /** Function that returns a promise of `T`, or `T` itself. */
7 | export type LazyFuturable = () => Futurable;
8 |
9 | /** A promise of `T`, or `T` itself, or a function to return either. */
10 | export type FuturableOrLazy = Futurable | LazyFuturable;
11 |
12 | /**
13 | * Returns a futurable from a futurable or a lazy futurable. If lazy, will call
14 | * to convert to futurable. Use this at evaluation time only, as any lazy
15 | * futurables will be called at this point.
16 | * @param futurableOrLazy A `Futurable` or a `LazyFuturable`.
17 | */
18 | export function futureableOrLazyToFuturable(futurableOrLazy: FuturableOrLazy): Futurable {
19 | return futurableOrLazy instanceof Function ? futurableOrLazy() : futurableOrLazy;
20 | }
21 |
22 | /**
23 | * If the MemoizedPromise is done, will turn into Promise, otherwise will turn
24 | * into lazy Promise
25 | * @param memoizedPromise The memoized promise to convert.
26 | */
27 | export function memoizedPromiseToFuturableOrLazy(memoizedPromise: MemoizedPromise): FuturableOrLazy {
28 | if (memoizedPromise.done) return memoizedPromise.get();
29 | return memoizedPromise.get;
30 | }
31 |
--------------------------------------------------------------------------------
/src/data/index.ts:
--------------------------------------------------------------------------------
1 | export * from './file';
2 | export * from './futurable';
3 | export * from './memoizedPromise';
4 |
--------------------------------------------------------------------------------
/src/data/memoizedPromise.ts:
--------------------------------------------------------------------------------
1 | import { Futurable, FuturableOrLazy, futureableOrLazyToFuturable, memoizedPromiseToFuturableOrLazy } from './futurable';
2 |
3 | export interface MemoizeOptions {
4 | /**
5 | * If true, will immediately process the input, even if it's a lazy promise (
6 | * a function to retrieve a promise).
7 | */
8 | preprocess?: boolean;
9 | }
10 |
11 | /**
12 | * This class is responsible for wrapping tasks in a promise that won't be
13 | * executed until the result is actually required. Calling .get() on the class
14 | * will start the task, and resolve with the result. If the task has already
15 | * been executed once, it will resolve immediatly with the last result.
16 | */
17 | export class MemoizedPromise {
18 | private _futurableOrLazy: FuturableOrLazy;
19 | private _result?: Futurable;
20 | private _done: boolean;
21 |
22 | constructor(futurableOrLazy: MemoizedPromise | FuturableOrLazy, options: MemoizeOptions = {}) {
23 | if (futurableOrLazy instanceof MemoizedPromise) {
24 | this._futurableOrLazy = memoizedPromiseToFuturableOrLazy(futurableOrLazy);
25 | } else {
26 | this._futurableOrLazy = futurableOrLazy;
27 | }
28 |
29 | this._result = undefined;
30 | this._done = false;
31 |
32 | if (options.preprocess || typeof this._futurableOrLazy !== 'function') {
33 | this._result = futureableOrLazyToFuturable(this._futurableOrLazy);
34 | this._done = true;
35 | }
36 | }
37 |
38 | /** Is true if the value is memoized. */
39 | get done() {
40 | return this._done;
41 | }
42 |
43 | /** Resolves with a promise for the value. */
44 | get = async () => {
45 | if (this._done) return this._result as Futurable;
46 | this._result = futureableOrLazyToFuturable(this._futurableOrLazy);
47 | this._done = true;
48 | return this._result;
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useAccessibleFocus';
2 | export * from './useCurrentRef';
3 | export * from './useFile';
4 | export * from './useFileSubscribe';
5 | export * from './useFocus';
6 | export * from './useFocusTrap';
7 | export * from './useKeyForClick';
8 | export * from './useManagedFiles';
9 | export * from './useOnClick';
10 | export * from './useToast';
11 | export * from './useUnmountDelay';
12 |
--------------------------------------------------------------------------------
/src/hooks/useCurrentRef.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 |
3 | export function useCurrentRef(toRef: T) {
4 | const toRefRef = useRef(toRef);
5 | useEffect(() => {
6 | toRefRef.current = toRef;
7 | });
8 | return toRefRef;
9 | }
10 |
--------------------------------------------------------------------------------
/src/hooks/useFile.ts:
--------------------------------------------------------------------------------
1 | import { Core } from '@pdftron/webviewer';
2 | import { useMemo } from 'react';
3 | import { FileLike } from '../data';
4 | import { useFileSubscribe } from './useFileSubscribe';
5 |
6 | /** The output of this hook is an object representing a file. */
7 | interface FileHook {
8 | /** The entire file class. */
9 | file: F;
10 | /** The file id. */
11 | id: string;
12 | /** The file originalName. */
13 | originalName: string;
14 | /** The file extension. */
15 | extension: string;
16 | /** The file name. */
17 | name?: string;
18 | /** The resolved file thumbnail or undefined until it is resolved. */
19 | thumbnail?: string;
20 | /** The resolved file fileObj or undefined until it is resolved. */
21 | fileObj?: Blob;
22 | /** The resolved file documentObj or undefined until it is resolved. */
23 | documentObj?: Core.Document;
24 | errors: {
25 | name?: any;
26 | thumbnail?: any;
27 | fileObj?: any;
28 | documentObj?: any;
29 | };
30 | }
31 |
32 | /**
33 | * This hook converts a file class with async values into a React-friendly hook
34 | * with async values set to undefined until they are fetched.
35 | * @param file The file to convert to react observable values.
36 | */
37 | export function useFile(file: F): FileHook {
38 | const [name, nameErr] = useFileSubscribe(file, f => f.name, 'onnamechange');
39 | const [thumbnail, thumbnailErr] = useFileSubscribe(file, f => f.thumbnail, 'onthumbnailchange');
40 | const [fileObj, fileObjErr] = useFileSubscribe(file, f => f.fileObj, 'onfileobjchange');
41 | const [documentObj, documentObjErr] = useFileSubscribe(file, f => f.documentObj, 'ondocumentobjchange');
42 |
43 | const fileValue = useMemo>(
44 | () => ({
45 | file,
46 | id: file.id,
47 | originalName: file.originalName,
48 | extension: file.extension,
49 | name,
50 | thumbnail,
51 | fileObj,
52 | documentObj,
53 | errors: {
54 | name: nameErr,
55 | thumbnail: thumbnailErr,
56 | fileObj: fileObjErr,
57 | documentObj: documentObjErr,
58 | },
59 | }),
60 | [documentObj, documentObjErr, file, fileObj, fileObjErr, name, nameErr, thumbnail, thumbnailErr],
61 | );
62 |
63 | return fileValue;
64 | }
65 |
--------------------------------------------------------------------------------
/src/hooks/useFocus.ts:
--------------------------------------------------------------------------------
1 | import { FocusEvent, FocusEventHandler, useCallback, useState } from 'react';
2 |
3 | /**
4 | * Returns handlers for onFocus and onBlur, as well as a property focused which
5 | * is true if the component or any child is being focused.
6 | * @param onFocus The onFocus prop if it's available.
7 | * @param onBlur The onBlur prop if it's available.
8 | */
9 | export function useFocus(onFocus?: FocusEventHandler, onBlur?: FocusEventHandler) {
10 | const [focused, setFocused] = useState(false);
11 |
12 | const handleOnFocus = useCallback(
13 | (event: FocusEvent) => {
14 | setFocused(true);
15 | onFocus?.(event);
16 | },
17 | [onFocus],
18 | );
19 |
20 | const handleOnBlur = useCallback(
21 | (event: FocusEvent) => {
22 | setFocused(false);
23 | onBlur?.(event);
24 | },
25 | [onBlur],
26 | );
27 |
28 | return { focused, handleOnFocus, handleOnBlur };
29 | }
30 |
--------------------------------------------------------------------------------
/src/hooks/useID.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { getStringId } from '../utils';
3 |
4 | /**
5 | * If an ID is not given, will generate and memoize an ID to use for a11y
6 | * or any other purpose.
7 | * @param id The optional ID provided by props.
8 | */
9 | export function useID(id?: string) {
10 | return useMemo(() => id || getStringId('label'), [id]);
11 | }
12 |
--------------------------------------------------------------------------------
/src/hooks/useKeyForClick.ts:
--------------------------------------------------------------------------------
1 | import { KeyboardEvent, KeyboardEventHandler, RefObject, useCallback } from 'react';
2 | import { generateClickEventFromKeyboardEvent } from '../utils';
3 |
4 | /**
5 | * Returns the handler for onKeyPress. If it hears a space or Enter key, it will
6 | * fire onClick. If you provide a ref, will compare the target and make sure it
7 | * is the same as the ref, then will fire onClick on the ref. Otherwise will
8 | * call it on the event target.
9 | * @param onKeyPress The onKeyPress prop if it's available.
10 | * @param ref If given, will compare event target to prevent any bubbling events.
11 | */
12 | export function useKeyForClick(onKeyPress?: KeyboardEventHandler, ref?: RefObject) {
13 | const handler = useCallback(
14 | (event: KeyboardEvent) => {
15 | // Fire click on space or enter press.
16 | if (event.key === ' ' || event.key === 'Enter') {
17 | const clickEvent = generateClickEventFromKeyboardEvent(event);
18 |
19 | // If ref is provided and it matches the event target, click ref.
20 | if (ref && event.target === ref.current) {
21 | ref.current.dispatchEvent(clickEvent);
22 | // Stop scrolling if space is pressed.
23 | if (event.key === ' ') event.preventDefault();
24 | } else if (!ref) {
25 | (event.target as HTMLElement).dispatchEvent(clickEvent);
26 | }
27 | }
28 | onKeyPress?.(event);
29 | },
30 | [ref, onKeyPress],
31 | );
32 |
33 | return handler;
34 | }
35 |
--------------------------------------------------------------------------------
/src/hooks/useOnClick.ts:
--------------------------------------------------------------------------------
1 | import { MouseEvent, MouseEventHandler, useCallback } from 'react';
2 |
3 | export interface UseOnClickOptions {
4 | stopPropagation?: boolean;
5 | preventDefault?: boolean;
6 | blurOnClick?: boolean;
7 | disabled?: boolean;
8 | }
9 |
10 | type UseOnClickOutput = (event: MouseEvent) => void;
11 |
12 | /**
13 | * Returns the handler for onClick. Allows you to add specific options to the
14 | * event before passing it through to the default onClick.
15 | * @param onClick The onClick prop if it's available.
16 | * @param options UseOnClickOptions for the hook.
17 | */
18 | export function useOnClick(onClick?: MouseEventHandler, options: UseOnClickOptions = {}) {
19 | const stopPropagation = !!options.stopPropagation;
20 | const preventDefault = !!options.preventDefault;
21 | const blurOnClick = !!options.blurOnClick;
22 | const disabled = !!options.disabled;
23 |
24 | const handler = useCallback>(
25 | event => {
26 | if (preventDefault) event.preventDefault();
27 | if (stopPropagation) event.stopPropagation();
28 | if (disabled) return;
29 | if (blurOnClick) {
30 | const focused = document.activeElement as HTMLElement | null;
31 | focused?.blur();
32 | }
33 | onClick?.(event);
34 | },
35 | [preventDefault, stopPropagation, blurOnClick, onClick, disabled],
36 | );
37 |
38 | return handler;
39 | }
40 |
--------------------------------------------------------------------------------
/src/hooks/useUnmountDelay.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo, useState } from 'react';
2 |
3 | export function useUnmountDelay(on = false, delay = 250) {
4 | const [mounted, setMounted] = useState(on);
5 |
6 | useEffect(() => {
7 | if (!on) {
8 | const timeoutId = window.setTimeout(() => {
9 | setMounted(false);
10 | }, delay);
11 |
12 | return () => window.clearTimeout(timeoutId);
13 | }
14 | setMounted(true);
15 | return;
16 | }, [delay, on]);
17 |
18 | const value = useMemo(
19 | () => ({
20 | mounted,
21 | unmounting: mounted && !on,
22 | }),
23 | [mounted, on],
24 | );
25 |
26 | return value;
27 | }
28 |
--------------------------------------------------------------------------------
/src/icons/Check.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Check(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/ChevronDown.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function ChevronDown(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/Circle.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Circle(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/Close.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Close(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/Download.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Download(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/Error.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Error(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/icons/Hide.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Hide(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/Info.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Info(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/icons/Menu.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Menu(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/MultiPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function MultiPage(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/README.md:
--------------------------------------------------------------------------------
1 | # Icons
2 |
3 | Many of the icons are modified from [Boxicons](https://boxicons.com/).
4 |
5 | They are licensed under CC 4.0. For license information, see
6 | [Boxicons license](https://github.com/atisawd/boxicons#license).
7 |
--------------------------------------------------------------------------------
/src/icons/RotateRight.tsx:
--------------------------------------------------------------------------------
1 | //
2 |
3 | import React, { SVGProps } from 'react';
4 |
5 | export function RotateRight(props: SVGProps) {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/icons/Search.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Search(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/Show.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Show(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/icons/SinglePage.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function SinglePage(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/src/icons/Success.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Success(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/icons/Warning.tsx:
--------------------------------------------------------------------------------
1 | import React, { SVGProps } from 'react';
2 |
3 | export function Warning(props: SVGProps) {
4 | return (
5 |
6 |
7 |
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/icons/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Check';
2 | export * from './ChevronDown';
3 | export * from './Circle';
4 | export * from './Close';
5 | export * from './Download';
6 | export * from './Error';
7 | export * from './Hide';
8 | export * from './Info';
9 | export * from './Menu';
10 | export * from './MultiPage';
11 | export * from './RotateRight';
12 | export * from './Search';
13 | export * from './Show';
14 | export * from './SinglePage';
15 | export * from './Success';
16 | export * from './Warning';
17 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | // Theme variables.
2 | @import './styles/theme';
3 |
4 | // Style variables.
5 | @import './styles/variables';
6 |
7 | // Mixins.
8 | @import './styles/mixins';
9 |
10 | // Mixins.
11 | @import './styles/breakpoints';
12 |
13 | // Base style.
14 | @import './styles/base';
15 |
16 | // Global constants style.
17 | @import './styles/globals';
18 |
19 | /**
20 | * Component style imports: DO NOT MODIFY.
21 | */
22 |
23 | //
24 | @import './components/Button/Button';
25 | @import './components/ButtonGroup/ButtonGroup';
26 | @import './components/Choice/Choice';
27 | @import './components/ClickableDiv/ClickableDiv';
28 | @import './components/DragLayer/DragLayer';
29 | @import './components/Draggable/Draggable';
30 | @import './components/EditableText/EditableText';
31 | @import './components/FileOrganizer/FileOrganizer';
32 | @import './components/FilePicker/FilePicker';
33 | @import './components/FilePlaceholder/FilePlaceholder';
34 | @import './components/FileSkeleton/FileSkeleton';
35 | @import './components/FocusTrap/FocusTrap';
36 | @import './components/Icon/Icon';
37 | @import './components/IconButton/IconButton';
38 | @import './components/Image/Image';
39 | @import './components/Input/Input';
40 | @import './components/Label/Label';
41 | @import './components/Modal/Modal';
42 | @import './components/Overlay/Overlay';
43 | @import './components/Spinner/Spinner';
44 | @import './components/Thumbnail/Thumbnail';
45 | @import './components/ThumbnailDragLayer/ThumbnailDragLayer';
46 | @import './components/ThumbnailSkeleton/ThumbnailSkeleton';
47 | @import './components/Toast/Toast';
48 | @import './components/ToastProvider/ToastProvider';
49 | @import './components/ToolButton/ToolButton';
50 | //
51 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components';
2 | export * from './data';
3 | export * from './hooks';
4 | export * from './icons';
5 | export * from './utils';
6 |
--------------------------------------------------------------------------------
/src/internal.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: string;
3 | export default content;
4 | }
5 |
6 | declare module '*.md' {
7 | const content: string;
8 | export default content;
9 | }
10 |
11 | declare module '*.png' {
12 | const content: string;
13 | export default content;
14 | }
15 |
--------------------------------------------------------------------------------
/src/storybook-helpers/action/action.ts:
--------------------------------------------------------------------------------
1 | import { action as _action, ActionOptions } from '@storybook/addon-actions';
2 | import { SyntheticEvent } from 'react';
3 |
4 | /**
5 | * Async actions for increased performance. Doesn't block thread.
6 | * @param name Action name. Should match your prop name.
7 | * @param options Options for the action.
8 | */
9 | export function action(name: string, options?: ActionOptions) {
10 | const primedAction = _action(name, options);
11 | return (event?: SyntheticEvent | any, ...args: any[]) => {
12 | if (event && event.nativeEvent) event = event.nativeEvent;
13 | setTimeout(() => primedAction(event, ...args));
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/src/storybook-helpers/data/files.ts:
--------------------------------------------------------------------------------
1 | import { Core } from '@pdftron/webviewer';
2 | import { FileLike } from '../../data/file';
3 | import { FuturableOrLazy } from '../../data/futurable';
4 | import { MemoizedPromise } from '../../data/memoizedPromise';
5 | import testPdfThumbnailRotated from '../images/pdf-preview-2.png';
6 | import testPdfThumbnail from '../images/pdf-preview.png';
7 |
8 | export interface CreateFileOptions {
9 | lazy?: boolean; // Expensive operation
10 | error?: boolean;
11 | }
12 |
13 | export function createFile(index: number, options: CreateFileOptions = {}) {
14 | return new FakeFile(index, options);
15 | }
16 |
17 | export class FakeFile implements FileLike {
18 | id: string;
19 | name: string;
20 | originalName: string;
21 | extension: string;
22 | thumbnail: MemoizedPromise;
23 | fileObj: MemoizedPromise;
24 | documentObj: MemoizedPromise;
25 | fullDocumentObj: Core.Document | undefined;
26 | pageNumber: number | undefined;
27 |
28 | constructor(index: number, options: CreateFileOptions = {}) {
29 | this.id = `file_${index + 1}`;
30 | this.name = `file_${index + 1}.pdf`;
31 | this.originalName = `file_${index + 1}`;
32 | this.extension = 'pdf';
33 | this.thumbnail = this._getParameter(index % 2 ? testPdfThumbnailRotated : testPdfThumbnail, options);
34 | this.fileObj = new MemoizedPromise(new Blob());
35 | this.documentObj = new MemoizedPromise(('' as unknown) as Core.Document);
36 | }
37 |
38 | private _getParameter(parameter: T, options: CreateFileOptions) {
39 | const internals = (() => {
40 | if (options.lazy) return () => new Promise(res => setTimeout(() => res(parameter), 500));
41 | if (options.error) return () => new Promise((_res, rej) => setTimeout(() => rej('Some error.'), 500));
42 | return parameter;
43 | })();
44 | return new MemoizedPromise(internals as FuturableOrLazy);
45 | }
46 |
47 | subscribe() {
48 | return () => {};
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/storybook-helpers/images/pdf-preview-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ApryseSDK/webviewer-react-toolkit/a96e3be370d02d7c9c7144eaf279c6d39f1574d2/src/storybook-helpers/images/pdf-preview-2.png
--------------------------------------------------------------------------------
/src/storybook-helpers/images/pdf-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ApryseSDK/webviewer-react-toolkit/a96e3be370d02d7c9c7144eaf279c6d39f1574d2/src/storybook-helpers/images/pdf-preview.png
--------------------------------------------------------------------------------
/src/storybook-helpers/knobs/forwardAction.ts:
--------------------------------------------------------------------------------
1 | import { action, ActionOptions } from '@storybook/addon-actions';
2 |
3 | export function forwardAction(name: string, callback: (...args: T) => any, options?: ActionOptions) {
4 | return (...args: T) => {
5 | action(name, options)(...args);
6 | return callback(...args);
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/storybook-helpers/knobs/integer.ts:
--------------------------------------------------------------------------------
1 | import { number } from '@storybook/addon-knobs';
2 |
3 | export function integer(name: string, value: number, groupId?: string) {
4 | const num = number(name, value, undefined, groupId);
5 | if (!Number.isInteger(num) || !Number.isFinite(num) || num < 0) return undefined;
6 | return num;
7 | }
8 |
--------------------------------------------------------------------------------
/src/storybook-helpers/knobs/selectIcon.ts:
--------------------------------------------------------------------------------
1 | import { select } from '@storybook/addon-knobs';
2 | import { AvailableIcons } from '../../components';
3 | import * as icons from '../../icons';
4 |
5 | const availableIcons = Object.keys(icons) as AvailableIcons[];
6 |
7 | export function selectIcon(name: string, value: AvailableIcons, groupId?: string): AvailableIcons {
8 | return select(name, availableIcons, value, groupId);
9 | }
10 |
--------------------------------------------------------------------------------
/src/storybook-helpers/mdx/LinkTo.tsx:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import Link from '@storybook/addon-links/react';
3 | import React, { FC } from 'react';
4 |
5 | export const LinkTo: FC<{ title: string; story?: string }> = ({ children, title, story = 'page' }) => {
6 | return (
7 |
12 | {children}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/storybook-helpers/mdx/index.ts:
--------------------------------------------------------------------------------
1 | export { LinkTo } from './LinkTo';
2 |
--------------------------------------------------------------------------------
/src/storybook-helpers/theme/font.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | fontBase: 'Lato,Tahoma,sans-serif',
3 | fontCode: '"IBM Plex Mono","Fira Code Retina","Fira Code","FiraCode-Retina",monospace',
4 | };
5 |
--------------------------------------------------------------------------------
/src/styles/_base.scss:
--------------------------------------------------------------------------------
1 | // Scoped to base class at the root of each component.
2 | .ui__base {
3 | // Scope normalize css into the base.
4 | @import '~normalize.css';
5 |
6 | box-sizing: border-box;
7 | font-size: $font-size-default;
8 | line-height: 1.25;
9 | font-family: inherit;
10 | color: $color-font-primary;
11 | fill: $color-font-primary;
12 | font-weight: $font-weight-normal;
13 | font-family: $font-family;
14 | -webkit-font-smoothing: auto;
15 | -moz-osx-font-smoothing: grayscale;
16 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
17 | -webkit-overflow-scrolling: touch;
18 |
19 | *,
20 | *:before,
21 | *:after {
22 | box-sizing: inherit;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/_breakpoints.scss:
--------------------------------------------------------------------------------
1 | $breakpoint-tablet: 768px;
2 | $breakpoint-desktop: 1024px;
3 |
4 | // Try not to use this, as it breaks "mobile first" principles. Applies only to mobile devices.
5 | @mixin for-phone-only {
6 | @media (max-width: $breakpoint-tablet - 1) {
7 | @content;
8 | }
9 | }
10 |
11 | // Style applies to devices with width tablet or above.
12 | @mixin for-tablet-up {
13 | @media (min-width: $breakpoint-tablet) {
14 | @content;
15 | }
16 | }
17 |
18 | // Style applies to devices with width desktop or above.
19 | @mixin for-desktop-up {
20 | @media (min-width: $breakpoint-desktop) {
21 | @content;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/styles/_globals.scss:
--------------------------------------------------------------------------------
1 | $thumbnail-dimensions: 240px;
2 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | import { Core } from '@pdftron/webviewer';
2 |
3 | export {}; // Required to indicate that the file is a module.
4 |
5 | declare global {
6 | interface Window {
7 | Core: typeof Core;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/constantUtils.ts:
--------------------------------------------------------------------------------
1 | /** The default throttle timeout for lazy async items. */
2 | export const DEFAULT_THROTTLE_TIMEOUT = 500;
3 |
4 | /** The width of a thumbnail element. */
5 | export const THUMBNAIL_WIDTH = 240;
6 |
7 | /** An object with a string `id` attribute. */
8 | export type ObjectWithId = { id: string };
9 |
--------------------------------------------------------------------------------
/src/utils/debugUtils.ts:
--------------------------------------------------------------------------------
1 | export function logExecTime(tag: string): () => number {
2 | const now = performance.now();
3 | return () => {
4 | const now2 = performance.now();
5 | console.log(`${tag} took ${now2 - now}ms`);
6 | return now2 - now;
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/fileUtils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns the extension of a filename.
3 | */
4 | export const getExtension = (filename = '') => {
5 | const split = filename.split('.');
6 | return split.pop() as string;
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/gridUtils.ts:
--------------------------------------------------------------------------------
1 | export function getItemIndex(rowIndex: number, columnIndex: number, numColumns: number) {
2 | return rowIndex * numColumns + columnIndex;
3 | }
4 |
5 | export function getRowAndColumnIndex(index: number, numColumns: number) {
6 | return {
7 | rowIndex: Math.floor(index / numColumns),
8 | columnIndex: index % numColumns,
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/idUtils.ts:
--------------------------------------------------------------------------------
1 | let currentId = 0;
2 |
3 | /**
4 | * Generates a sequential string to use as a unique identifier. This should be
5 | * used over `getId` if you need to use it as a DOM id, or a React key.
6 | *
7 | * @param prefix Optional. Prefix for the string id.
8 | */
9 | export function getStringId(prefix = 'id'): string {
10 | return `${prefix}_${(currentId++).toString(16)}`;
11 | }
12 |
13 | export type UniqueIdentifier = symbol | string;
14 |
15 | /**
16 | * Returns a Symbol to uniquely identify something. Will fallback to using
17 | * string.
18 | *
19 | * @param description Description of the Symbol. Used as string prefix if not supported.
20 | */
21 | export function getId(description?: string | number): UniqueIdentifier {
22 | if (typeof Symbol === 'function') return Symbol(description);
23 | return `${description}_${getStringId()}`;
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './arrayUtils';
2 | export * from './constantUtils';
3 | export * from './domUtils';
4 | export * from './fileUtils';
5 | export * from './gridUtils';
6 | export * from './idUtils';
7 | export * from './typeUtils';
8 | export * from './webviewerUtils';
9 |
--------------------------------------------------------------------------------
/src/utils/typeUtils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Remove the specified Keys of T. Like Omit, but with autocompletion.
3 | *
4 | * @example
5 | * type SomeProps = {field1: string; field2: string};
6 | * const x: Remove = {field1: 'Hello, World!'};
7 | */
8 | export type Remove = Pick>;
9 |
10 | /**
11 | * Include only the specified Keys of T.
12 | *
13 | * @example
14 | * type SomeProps = {field1: string; field2: string};
15 | * const x: Include = {field2: 'Hello, World!'};
16 | */
17 | export type Include = Pick>;
18 |
19 | /**
20 | * Extract the common item type in array of similar items T.
21 | *
22 | * @example
23 | * const x = [{a: 'a', b: 'b'}, {a: 'aa', b: 'bb'}];
24 | * const y: ItemOf = {a: 'Hello'; b: 'World'};
25 | */
26 | export type ItemOf = T[number];
27 |
28 | /**
29 | * Extracts an array of argument types of T.
30 | *
31 | * @example
32 | * const x = (a: string, b: number) => a + b;
33 | * const x: ArgumentTypes = ['Hello, World!', 100];
34 | */
35 | // eslint-disable-next-line @typescript-eslint/ban-types
36 | export type ArgumentTypes = T extends (...args: infer A) => any ? A : never;
37 |
38 | /**
39 | * Requires at least one of the specified Keys of T. If no keys provided, will
40 | * require at least one of any keyof T.
41 | *
42 | * @example
43 | * type SomeProps = {field1?: string; field2?: string};
44 | * const x: RequireAtLeastOne = {field1: 'Hello, World!'};
45 | */
46 | export type RequireAtLeastOne = Remove &
47 | { [K in Keys]-?: Required> & Partial>> }[Keys];
48 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./configs/tsconfig",
3 | "include": ["./src"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.lint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./configs/tsconfig",
3 | "include": ["."]
4 | }
5 |
--------------------------------------------------------------------------------
/webpack.style.config.ts:
--------------------------------------------------------------------------------
1 | import { BASE_URL } from './webpack.config';
2 | import webpack from 'webpack';
3 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
4 |
5 | const config: webpack.Configuration = {
6 | mode: 'production',
7 | watch: true,
8 | entry: [`./${BASE_URL}/index.scss`],
9 | plugins: [new MiniCssExtractPlugin({ filename: `css/style.css` })],
10 | resolve: {
11 | extensions: ['.scss'],
12 | },
13 | module: {
14 | rules: [
15 | {
16 | test: /\.scss$/,
17 | use: [
18 | { loader: MiniCssExtractPlugin.loader },
19 | { loader: require.resolve('css-loader') },
20 | { loader: require.resolve('postcss-loader') },
21 | { loader: require.resolve('sass-loader') },
22 | ],
23 | },
24 | ],
25 | },
26 | };
27 |
28 | export default config;
29 |
--------------------------------------------------------------------------------