@linzjs/windows 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +103 -0
- package/.github/dependabot.yml +12 -0
- package/.github/pull_request_template.md +29 -0
- package/.github/workflows/chromatic.yml +23 -0
- package/.github/workflows/deploy-storybook.yml +86 -0
- package/.prettierrc.cjs +12 -0
- package/.storybook/main.ts +17 -0
- package/.storybook/preview.ts +15 -0
- package/.stylelintrc.json +68 -0
- package/README.md +10 -0
- package/config/jest/babelTransform.cjs +29 -0
- package/config/jest/cssTransform.cjs +14 -0
- package/config/jest/fileTransform.cjs +38 -0
- package/config/jest/setup.js +2 -0
- package/empty.js +1 -0
- package/jest.config.cjs +19 -0
- package/package.json +124 -0
- package/rollup.config.cjs +26 -0
- package/src/modal/Modal.tsx +32 -0
- package/src/modal/ModalContext.tsx +27 -0
- package/src/modal/ModalContextProvider.tsx +137 -0
- package/src/modal/ModalInstanceContext.ts +16 -0
- package/src/modal/useShowModal.ts +16 -0
- package/src/setupTests.ts +5 -0
- package/src/stories/modal/Modal.mdx +20 -0
- package/src/stories/modal/Modal.stories.tsx +27 -0
- package/src/stories/modal/TestModal.scss +15 -0
- package/src/stories/modal/TestModal.tsx +57 -0
- package/src/util/useInterval.test.tsx +26 -0
- package/src/util/useInterval.ts +26 -0
- package/tsconfig.json +37 -0
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// This config will run in a way that fails a build
|
|
2
|
+
module.exports = {
|
|
3
|
+
env: {
|
|
4
|
+
commonjs: true,
|
|
5
|
+
es2020: true,
|
|
6
|
+
node: true
|
|
7
|
+
},
|
|
8
|
+
plugins: ["react", "react-hooks", "jest", "jsx-a11y", "testing-library"],
|
|
9
|
+
settings: {
|
|
10
|
+
react: {
|
|
11
|
+
version: "detect"
|
|
12
|
+
},
|
|
13
|
+
jest: {
|
|
14
|
+
version: 26
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
parserOptions: {
|
|
18
|
+
ecmaVersion: 2020,
|
|
19
|
+
sourceType: "module"
|
|
20
|
+
},
|
|
21
|
+
extends: ["eslint:recommended", "plugin:react/recommended", "plugin:react-hooks/recommended", "plugin:jest/recommended", "plugin:jest/style", "plugin:testing-library/react", "plugin:prettier/recommended", "plugin:storybook/recommended"],
|
|
22
|
+
ignorePatterns: ["react-app-env.d.ts"],
|
|
23
|
+
rules: {
|
|
24
|
+
// testing-library - to fix
|
|
25
|
+
"testing-library/no-dom-import": "off",
|
|
26
|
+
// Fix these
|
|
27
|
+
"jest/no-conditional-expect": "off",
|
|
28
|
+
"jest/no-standalone-expect": "off",
|
|
29
|
+
"jest/valid-expect": "off",
|
|
30
|
+
"jest/prefer-to-be": "error",
|
|
31
|
+
"testing-library/no-unnecessary-act": "off",
|
|
32
|
+
"testing-library/prefer-presence-queries": "off",
|
|
33
|
+
"testing-library/no-wait-for-multiple-assertions": "off",
|
|
34
|
+
"testing-library/no-render-in-setup": "off",
|
|
35
|
+
"testing-library/no-node-access": "off",
|
|
36
|
+
"testing-library/prefer-screen-queries": "off",
|
|
37
|
+
"testing-library/prefer-find-by": "off",
|
|
38
|
+
"testing-library/prefer-query-by-disappearance": "off",
|
|
39
|
+
"testing-library/no-debugging-utils": "warn",
|
|
40
|
+
"testing-library/render-result-naming-convention": "off",
|
|
41
|
+
// customized rules
|
|
42
|
+
"react/no-unescaped-entities": ["error", {
|
|
43
|
+
forbid: [">", '"', "}"]
|
|
44
|
+
}],
|
|
45
|
+
// ' is ok, don't want to escape this
|
|
46
|
+
"react/react-in-jsx-scope": "off",
|
|
47
|
+
// TS config takes care of this
|
|
48
|
+
"linebreak-style": ["error", "unix"],
|
|
49
|
+
// prevent crlf from getting pushed
|
|
50
|
+
"react/prop-types": "off",
|
|
51
|
+
// Doesn't seem pick up React.FC<Props> typing
|
|
52
|
+
"no-console": ["error", {
|
|
53
|
+
allow: ["warn", "error"]
|
|
54
|
+
}],
|
|
55
|
+
// error on push/codacy
|
|
56
|
+
"jest/no-disabled-tests": "warn",
|
|
57
|
+
// we have some disabled tests
|
|
58
|
+
"no-unused-vars": "off",
|
|
59
|
+
// duplicate of typescript rule
|
|
60
|
+
"react/jsx-no-target-blank": "off",
|
|
61
|
+
"jest/expect-expect": "off",
|
|
62
|
+
// sometimes the assertions are in other functions called from a test
|
|
63
|
+
|
|
64
|
+
"react-hooks/rules-of-hooks": "error",
|
|
65
|
+
"react-hooks/exhaustive-deps": ["warn", {
|
|
66
|
+
additionalHooks: "(useWorkflowEffect|useWorkflowSidePanelHook)"
|
|
67
|
+
}]
|
|
68
|
+
},
|
|
69
|
+
overrides: [{
|
|
70
|
+
/** Overrides for typescript */
|
|
71
|
+
files: ["**/*.ts", "**/*.tsx"],
|
|
72
|
+
plugins: ["@typescript-eslint"],
|
|
73
|
+
parser: "@typescript-eslint/parser",
|
|
74
|
+
parserOptions: {
|
|
75
|
+
project: "./tsconfig.json",
|
|
76
|
+
tsconfigRootDir: __dirname
|
|
77
|
+
},
|
|
78
|
+
extends: ["plugin:@typescript-eslint/recommended"],
|
|
79
|
+
rules: {
|
|
80
|
+
"@typescript-eslint/await-thenable": "error",
|
|
81
|
+
"@typescript-eslint/no-unnecessary-type-constraint": "off",
|
|
82
|
+
"@typescript-eslint/explicit-function-return-type": "off",
|
|
83
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
84
|
+
"@typescript-eslint/no-empty-function": ["warn", {
|
|
85
|
+
allow: ["arrowFunctions"]
|
|
86
|
+
}],
|
|
87
|
+
"@typescript-eslint/no-unused-vars": ["error", {
|
|
88
|
+
argsIgnorePattern: "^_"
|
|
89
|
+
}],
|
|
90
|
+
// prepend var with _ (e.g.. _myVar) to ignore this pattern
|
|
91
|
+
"@typescript-eslint/no-use-before-define": "off",
|
|
92
|
+
// We will want to use before define to keep exports at the top
|
|
93
|
+
"@typescript-eslint/ban-ts-comment": "off",
|
|
94
|
+
// We use explicit overrides
|
|
95
|
+
"@typescript-eslint/naming-convention": "off" // React's convention is to use CamelCase for component file names
|
|
96
|
+
}
|
|
97
|
+
}, {
|
|
98
|
+
files: ["*.test.js", "*.test.ts", "*.test.tsx", "scripts/*"],
|
|
99
|
+
rules: {
|
|
100
|
+
"no-console": "off"
|
|
101
|
+
}
|
|
102
|
+
}]
|
|
103
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Basic dependabot.yml file with
|
|
2
|
+
# minimum configuration for two package managers
|
|
3
|
+
|
|
4
|
+
version: 2
|
|
5
|
+
updates:
|
|
6
|
+
# Enable version updates for npm
|
|
7
|
+
- package-ecosystem: 'npm'
|
|
8
|
+
# Look for `package.json` and `lock` files in the `root` directory
|
|
9
|
+
directory: '/'
|
|
10
|
+
# Check the npm registry for updates every day (weekdays)
|
|
11
|
+
schedule:
|
|
12
|
+
interval: 'daily'
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
DESCRIPTION of the task, STORY/TASK number and link if applicable (e.g. SURVEY-100)
|
|
2
|
+
|
|
3
|
+
Author Checklist
|
|
4
|
+
|
|
5
|
+
- [ ] appropriate description or links provided to provide context on the PR
|
|
6
|
+
- [ ] self reviewed, seems easy to understand and follow
|
|
7
|
+
- [ ] reasonable code test coverage
|
|
8
|
+
- [ ] change is documented in Storybook and/or markdown files
|
|
9
|
+
|
|
10
|
+
Reviewer Checklist
|
|
11
|
+
|
|
12
|
+
- Follows convention
|
|
13
|
+
- Does what the author says it will do
|
|
14
|
+
- Does not appear to cause side effects and breaking changes
|
|
15
|
+
- if it does cause breaking changes, those are appropriately referenced
|
|
16
|
+
|
|
17
|
+
Post merge
|
|
18
|
+
|
|
19
|
+
- [ ] Post about the change in #lui-cop
|
|
20
|
+
|
|
21
|
+
Conventional Commit Cheat Sheet:
|
|
22
|
+
**build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
|
|
23
|
+
**ci**: Changes to our CI configuration files and scripts (example scopes: Circle, BrowserStack, SauceLabs)
|
|
24
|
+
**docs**: Documentation only changes
|
|
25
|
+
**feat**: A new feature
|
|
26
|
+
**fix**: A bug fix
|
|
27
|
+
**perf**: A code change that improves performance
|
|
28
|
+
**refactor**: A code change that neither fixes a bug nor adds a feature
|
|
29
|
+
**test**: Adding missing tests or correcting existing tests
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: 'Chromatic'
|
|
2
|
+
on: push
|
|
3
|
+
|
|
4
|
+
jobs:
|
|
5
|
+
chromatic-deployment:
|
|
6
|
+
runs-on: ubuntu-latest
|
|
7
|
+
steps:
|
|
8
|
+
- uses: actions/checkout@v1
|
|
9
|
+
- name: Install dependencies
|
|
10
|
+
run: npm i --legacy-peer-deps && npm run build-storybook
|
|
11
|
+
- name: Publish to non-master non-beta to Chromatic
|
|
12
|
+
if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/beta'
|
|
13
|
+
uses: chromaui/action@v1
|
|
14
|
+
with:
|
|
15
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
16
|
+
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
|
17
|
+
- name: Publish Chromatic and auto accept changes for master and beta
|
|
18
|
+
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/beta'
|
|
19
|
+
uses: chromaui/action@v1
|
|
20
|
+
with:
|
|
21
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
22
|
+
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
|
23
|
+
autoAcceptChanges: true # 👈 Option to accept all changes
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on: [push]
|
|
3
|
+
jobs:
|
|
4
|
+
build:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
|
|
7
|
+
steps:
|
|
8
|
+
- name: Begin CI...
|
|
9
|
+
uses: actions/checkout@v2
|
|
10
|
+
with:
|
|
11
|
+
persist-credentials: false
|
|
12
|
+
|
|
13
|
+
- name: Use Node 16
|
|
14
|
+
uses: actions/setup-node@v2
|
|
15
|
+
with:
|
|
16
|
+
node-version: 16.x
|
|
17
|
+
|
|
18
|
+
- name: Install dependencies
|
|
19
|
+
run: npm ci --legacy-peer-deps
|
|
20
|
+
|
|
21
|
+
- name: Test
|
|
22
|
+
run: npm run test --ci --coverage --maxWorkers=2
|
|
23
|
+
|
|
24
|
+
- name: Build
|
|
25
|
+
run: npm run build
|
|
26
|
+
|
|
27
|
+
- name: Release
|
|
28
|
+
if: github.ref == 'refs/heads/master'
|
|
29
|
+
run: npx semantic-release
|
|
30
|
+
env:
|
|
31
|
+
GH_TOKEN: ${{secrets.STEP_ENABLEMENT_SERVICE_PAT}}
|
|
32
|
+
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
33
|
+
NPM_TOKEN: ${{secrets.NPM_AUTH_TOKEN_LINZJS}}
|
|
34
|
+
|
|
35
|
+
publish-beta-npm:
|
|
36
|
+
if: startsWith(github.ref, 'refs/heads/beta')
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v2
|
|
41
|
+
with:
|
|
42
|
+
token: ${{secrets.STEP_ENABLEMENT_SERVICE_PAT}}
|
|
43
|
+
|
|
44
|
+
- uses: actions/setup-node@v2
|
|
45
|
+
with:
|
|
46
|
+
node-version: 16
|
|
47
|
+
registry-url: https://registry.npmjs.org/
|
|
48
|
+
|
|
49
|
+
- name: Install dependencies
|
|
50
|
+
run: npm ci --legacy-peer-deps
|
|
51
|
+
|
|
52
|
+
- name: Build
|
|
53
|
+
run: npm run build
|
|
54
|
+
|
|
55
|
+
- run: npm version prerelease -m "%s [skip ci]" && git push
|
|
56
|
+
env:
|
|
57
|
+
GIT_AUTHOR_EMAIL: STEPEnablementService@linz.govt.nz
|
|
58
|
+
GIT_AUTHOR_NAME: STEP Enablement Service
|
|
59
|
+
GIT_COMMITTER_EMAIL: STEPEnablementService@linz.govt.nz
|
|
60
|
+
GIT_COMMITTER_NAME: STEP Enablement Service
|
|
61
|
+
|
|
62
|
+
- run: npm publish --access public --tag beta
|
|
63
|
+
env:
|
|
64
|
+
NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN_LINZJS}}
|
|
65
|
+
|
|
66
|
+
publish-storybook:
|
|
67
|
+
if: github.ref == 'refs/heads/master'
|
|
68
|
+
needs: build
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
|
|
72
|
+
- uses: actions/checkout@v2
|
|
73
|
+
|
|
74
|
+
- uses: actions/setup-node@v2
|
|
75
|
+
with:
|
|
76
|
+
node-version: 16
|
|
77
|
+
|
|
78
|
+
- name: Install dependencies
|
|
79
|
+
run: npm ci --legacy-peer-deps
|
|
80
|
+
|
|
81
|
+
- name: Build
|
|
82
|
+
run: npm run build
|
|
83
|
+
|
|
84
|
+
- run: npm run deploy-storybook -- --ci
|
|
85
|
+
env:
|
|
86
|
+
GH_TOKEN: github-actions:${{secrets.GITHUB_TOKEN}}
|
package/.prettierrc.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
semi: true,
|
|
3
|
+
trailingComma: "all",
|
|
4
|
+
printWidth: 120,
|
|
5
|
+
useTabs: false,
|
|
6
|
+
tabWidth: 2,
|
|
7
|
+
singleQuote: false,
|
|
8
|
+
endOfLine: "lf",
|
|
9
|
+
importOrder: [".(css|scss)$", "<THIRD_PARTY_MODULES>", "^@linzjs/(.*)$", "^@step-ag-grid"],
|
|
10
|
+
importOrderSeparation: true,
|
|
11
|
+
importOrderSortSpecifiers: true,
|
|
12
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StorybookConfig } from "@storybook/react-vite";
|
|
2
|
+
const config: StorybookConfig = {
|
|
3
|
+
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
|
|
4
|
+
addons: [
|
|
5
|
+
"@storybook/addon-links",
|
|
6
|
+
"@storybook/addon-essentials",
|
|
7
|
+
"@storybook/addon-interactions",
|
|
8
|
+
],
|
|
9
|
+
framework: {
|
|
10
|
+
name: "@storybook/react-vite",
|
|
11
|
+
options: {},
|
|
12
|
+
},
|
|
13
|
+
docs: {
|
|
14
|
+
autodocs: "tag",
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export default config;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Preview } from "@storybook/react";
|
|
2
|
+
|
|
3
|
+
const preview: Preview = {
|
|
4
|
+
parameters: {
|
|
5
|
+
actions: { argTypesRegex: "^on[A-Z].*" },
|
|
6
|
+
controls: {
|
|
7
|
+
matchers: {
|
|
8
|
+
color: /(background|color)$/i,
|
|
9
|
+
date: /Date$/,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default preview;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": ["stylelint-config-standard", "stylelint-config-prettier", "stylelint-config-recommended-scss"],
|
|
3
|
+
"plugins": ["stylelint-scss"],
|
|
4
|
+
"customSyntax": "postcss-scss",
|
|
5
|
+
"rules": {
|
|
6
|
+
"at-rule-no-unknown": null,
|
|
7
|
+
"at-rule-empty-line-before": null,
|
|
8
|
+
"block-closing-brace-newline-before": "always-multi-line",
|
|
9
|
+
"block-closing-brace-newline-after": [
|
|
10
|
+
"always-multi-line",
|
|
11
|
+
{
|
|
12
|
+
"ignoreAtRules": ["if", "else"]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"block-opening-brace-space-before": "always",
|
|
16
|
+
"block-opening-brace-space-after": "always-single-line",
|
|
17
|
+
"color-hex-case": null,
|
|
18
|
+
"color-hex-length": null,
|
|
19
|
+
"declaration-empty-line-before": null,
|
|
20
|
+
"declaration-block-single-line-max-declarations": 2,
|
|
21
|
+
"declaration-block-semicolon-newline-after": "always-multi-line",
|
|
22
|
+
"declaration-block-semicolon-space-after": "always-single-line",
|
|
23
|
+
"declaration-block-semicolon-space-before": "never",
|
|
24
|
+
"function-parentheses-space-inside": "never-single-line",
|
|
25
|
+
"function-max-empty-lines": 1,
|
|
26
|
+
"length-zero-no-unit": null,
|
|
27
|
+
"max-empty-lines": [
|
|
28
|
+
2,
|
|
29
|
+
{
|
|
30
|
+
"ignore": ["comments"]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"no-duplicate-selectors": null,
|
|
34
|
+
"no-invalid-position-at-import-rule": null,
|
|
35
|
+
"rule-empty-line-before": [
|
|
36
|
+
"always-multi-line",
|
|
37
|
+
{
|
|
38
|
+
"except": ["after-single-line-comment", "first-nested"],
|
|
39
|
+
"ignore": ["after-comment", "first-nested"]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"selector-list-comma-newline-after": "always-multi-line",
|
|
43
|
+
"selector-pseudo-element-colon-notation": "double",
|
|
44
|
+
"value-list-max-empty-lines": 1,
|
|
45
|
+
|
|
46
|
+
"scss/at-rule-no-unknown": true,
|
|
47
|
+
"scss/at-else-closing-brace-newline-after": "always-last-in-chain",
|
|
48
|
+
"scss/at-else-if-parentheses-space-before": "always",
|
|
49
|
+
"scss/no-duplicate-dollar-variables": [
|
|
50
|
+
true,
|
|
51
|
+
{
|
|
52
|
+
"ignoreInsideAtRules": ["if", "else", "mixin", "function", "while"]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"scss/at-function-parentheses-space-before": "never",
|
|
56
|
+
"scss/at-if-closing-brace-newline-after": "always-last-in-chain",
|
|
57
|
+
"scss/at-mixin-argumentless-call-parentheses": "always",
|
|
58
|
+
"scss/at-mixin-parentheses-space-before": "never",
|
|
59
|
+
"scss/dollar-variable-colon-space-after": "at-least-one-space",
|
|
60
|
+
"scss/dollar-variable-colon-newline-after": "always-multi-line",
|
|
61
|
+
"scss/dollar-variable-no-missing-interpolation": true,
|
|
62
|
+
"scss/double-slash-comment-whitespace-inside": "always",
|
|
63
|
+
"scss/declaration-nested-properties-no-divided-groups": true,
|
|
64
|
+
"scss/operator-no-unspaced": true,
|
|
65
|
+
"scss/selector-no-redundant-nesting-selector": true,
|
|
66
|
+
"selector-class-pattern": "^[a-zA-Z-_]+$"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# linz/windows
|
|
2
|
+
|
|
3
|
+
[](https://github.com/semantic-release/semantic-release)
|
|
4
|
+
|
|
5
|
+
> Reusable windowing component for LINZ / Toitū te whenua.
|
|
6
|
+
>
|
|
7
|
+
## Features
|
|
8
|
+
- Async React Modal dialogs
|
|
9
|
+
|
|
10
|
+
See [Chromatic](https://64a2356b80885af35510b627-gsvwsgdsde.chromatic.com/) for usage.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const babelJest = require("babel-jest");
|
|
4
|
+
|
|
5
|
+
const hasJsxRuntime = (() => {
|
|
6
|
+
if (process.env.DISABLE_NEW_JSX_TRANSFORM === "true") {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
require.resolve("react/jsx-runtime");
|
|
12
|
+
return true;
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
module.exports = babelJest.default.createTransformer({
|
|
19
|
+
presets: [
|
|
20
|
+
[
|
|
21
|
+
require.resolve("babel-preset-react-app"),
|
|
22
|
+
{
|
|
23
|
+
runtime: hasJsxRuntime ? "automatic" : "classic",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
],
|
|
27
|
+
babelrc: false,
|
|
28
|
+
configFile: false,
|
|
29
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// This is a custom Jest transformer turning style imports into empty objects.
|
|
4
|
+
// http://facebook.github.io/jest/docs/en/webpack.html
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
process() {
|
|
8
|
+
return "module.exports = {};";
|
|
9
|
+
},
|
|
10
|
+
getCacheKey() {
|
|
11
|
+
// The output is always the same.
|
|
12
|
+
return "cssTransform";
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const lodash = require("lodash");
|
|
5
|
+
|
|
6
|
+
// This is a custom Jest transformer turning file imports into filenames.
|
|
7
|
+
// http://facebook.github.io/jest/docs/en/webpack.html
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
process(src, filename) {
|
|
11
|
+
const assetFilename = JSON.stringify(path.basename(filename));
|
|
12
|
+
|
|
13
|
+
if (filename.match(/\.svg$/)) {
|
|
14
|
+
// Based on how SVGR generates a component name:
|
|
15
|
+
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
|
|
16
|
+
const pascalCaseFilename = lodash.upperFirst(lodash.camelCase(path.parse(filename).name));
|
|
17
|
+
const componentName = `Svg${pascalCaseFilename}`;
|
|
18
|
+
return `const React = require('react');
|
|
19
|
+
module.exports = {
|
|
20
|
+
__esModule: true,
|
|
21
|
+
default: ${assetFilename},
|
|
22
|
+
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
|
|
23
|
+
return {
|
|
24
|
+
$$typeof: Symbol.for('react.element'),
|
|
25
|
+
type: 'svg',
|
|
26
|
+
ref: ref,
|
|
27
|
+
key: null,
|
|
28
|
+
props: Object.assign({}, props, {
|
|
29
|
+
children: ${assetFilename}
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
};`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `module.exports = ${assetFilename};`;
|
|
37
|
+
},
|
|
38
|
+
};
|
package/empty.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
roots: ["<rootDir>/src"],
|
|
3
|
+
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts"],
|
|
4
|
+
setupFiles: ["react-app-polyfill/jsdom"],
|
|
5
|
+
setupFilesAfterEnv: ["jest-expect-message"],
|
|
6
|
+
testMatch: ["<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"],
|
|
7
|
+
testEnvironment: "jsdom",
|
|
8
|
+
transform: {
|
|
9
|
+
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.cjs",
|
|
10
|
+
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.cjs",
|
|
11
|
+
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.cjs",
|
|
12
|
+
},
|
|
13
|
+
transformIgnorePatterns: ["node_modules/(?!(ol|@geoblocks/ol-maplibre-layer|geotiff|quick-lru)|lodash-es|lodash|escape-string-regexp|matcher/)"],
|
|
14
|
+
moduleNameMapper: {
|
|
15
|
+
"^@components/(.*)$": "<rootDir>/src/components/$1",
|
|
16
|
+
},
|
|
17
|
+
resetMocks: true,
|
|
18
|
+
coverageReporters: ["text", "cobertura"],
|
|
19
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linzjs/windows",
|
|
3
|
+
"repository": "github:linz/windows.git",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"peerDependencies": {
|
|
7
|
+
"lodash-es": ">=4",
|
|
8
|
+
"react": ">=17",
|
|
9
|
+
"react-dom": ">=17"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=16"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"lodash-es": ">=4",
|
|
20
|
+
"react": ">=17",
|
|
21
|
+
"react-dom": ">=17",
|
|
22
|
+
"uuid": "^9.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@rollup/plugin-commonjs": "^25.0.2",
|
|
26
|
+
"@rollup/plugin-json": "^6.0.0",
|
|
27
|
+
"@rollup/plugin-node-resolve": "^15.1.0",
|
|
28
|
+
"@storybook/addon-docs": "^7.0.24",
|
|
29
|
+
"@storybook/addon-essentials": "^7.0.24",
|
|
30
|
+
"@storybook/addon-interactions": "^7.0.24",
|
|
31
|
+
"@storybook/addon-links": "^7.0.24",
|
|
32
|
+
"@storybook/blocks": "^7.0.24",
|
|
33
|
+
"@storybook/builder-webpack5": "^7.0.24",
|
|
34
|
+
"@storybook/jest": "^0.1.0",
|
|
35
|
+
"@storybook/preset-create-react-app": "^7.0.24",
|
|
36
|
+
"@storybook/react": "^7.0.24",
|
|
37
|
+
"@storybook/react-vite": "^7.0.24",
|
|
38
|
+
"@storybook/test-runner": "^0.11.0",
|
|
39
|
+
"@storybook/testing-library": "^0.2.0",
|
|
40
|
+
"@testing-library/jest-dom": "^5.16.5",
|
|
41
|
+
"@testing-library/react": "^14.0.0",
|
|
42
|
+
"@testing-library/user-event": "^14.4.3",
|
|
43
|
+
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
|
|
44
|
+
"@types/jest": "^29.5.2",
|
|
45
|
+
"@types/lodash-es": "^4.17.7",
|
|
46
|
+
"@types/node": "^20.3.3",
|
|
47
|
+
"@types/react": "^18.2.14",
|
|
48
|
+
"@types/react-dom": "^18.2.6",
|
|
49
|
+
"@types/uuid": "^9.0.2",
|
|
50
|
+
"eslint": "^8.44.0",
|
|
51
|
+
"eslint-config-prettier": "^8.8.0",
|
|
52
|
+
"eslint-config-react-app": "^7.0.1",
|
|
53
|
+
"eslint-plugin-deprecation": "^1.4.1",
|
|
54
|
+
"eslint-plugin-import": "^2.27.5",
|
|
55
|
+
"eslint-plugin-jest": "^27.2.2",
|
|
56
|
+
"eslint-plugin-jsx-a11y": "^6.7.1",
|
|
57
|
+
"eslint-plugin-prettier": "^4.2.1",
|
|
58
|
+
"eslint-plugin-react": "^7.32.2",
|
|
59
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
60
|
+
"eslint-plugin-storybook": "^0.6.12",
|
|
61
|
+
"eslint-plugin-testing-library": "^5.11.0",
|
|
62
|
+
"jest": "^29.5.0",
|
|
63
|
+
"jest-canvas-mock": "^2.5.2",
|
|
64
|
+
"jest-environment-jsdom": "^29.5.0",
|
|
65
|
+
"jest-expect-message": "^1.1.3",
|
|
66
|
+
"mkdirp": "^3.0.1",
|
|
67
|
+
"npm-run-all": "^4.1.5",
|
|
68
|
+
"prettier": "^2.8.8",
|
|
69
|
+
"prop-types": "^15.8.1",
|
|
70
|
+
"react-scripts": "5.0.1",
|
|
71
|
+
"rollup": "^3.23.0",
|
|
72
|
+
"rollup-plugin-copy": "^3.4.0",
|
|
73
|
+
"sass": "^1.63.6",
|
|
74
|
+
"sass-loader": "^13.3.2",
|
|
75
|
+
"semantic-release": "^19.0.5",
|
|
76
|
+
"storybook": "^7.0.24",
|
|
77
|
+
"style-loader": "^3.3.3",
|
|
78
|
+
"stylelint": "^14.16.1",
|
|
79
|
+
"stylelint-config-prettier": "^9.0.5",
|
|
80
|
+
"stylelint-config-recommended-scss": "^8.0.0",
|
|
81
|
+
"stylelint-config-standard": "^29.0.0",
|
|
82
|
+
"stylelint-prettier": "3.0.0",
|
|
83
|
+
"stylelint-scss": "5.0.1",
|
|
84
|
+
"typescript": "^4.9.5",
|
|
85
|
+
"vite": "^4.3.9"
|
|
86
|
+
},
|
|
87
|
+
"scripts": {
|
|
88
|
+
"build": "run-s clean stylelint lint bundle",
|
|
89
|
+
"yalc": "run-s clean css bundle && yalc publish",
|
|
90
|
+
"clean": "rimraf dist && mkdirp ./dist",
|
|
91
|
+
"bundle": "rollup -c",
|
|
92
|
+
"test": "jest",
|
|
93
|
+
"stylelint": "stylelint src/**/*.scss src/**/*.css --fix",
|
|
94
|
+
"lint": "eslint ./src --ext .js,.ts,.tsx --fix --cache --ignore-path .gitignore",
|
|
95
|
+
"storybook": "storybook dev -p 6006",
|
|
96
|
+
"build-storybook": "storybook build",
|
|
97
|
+
"deploy-storybook": "npx --yes -p @storybook/storybook-deployer storybook-to-ghpages",
|
|
98
|
+
"chromatic": "chromatic --exit-zero-on-changes",
|
|
99
|
+
"semantic-release": "semantic-release"
|
|
100
|
+
},
|
|
101
|
+
"eslintConfig": {
|
|
102
|
+
"extends": [
|
|
103
|
+
"react-app",
|
|
104
|
+
"react-app/jest"
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
"browserslist": {
|
|
108
|
+
"production": [
|
|
109
|
+
">0.2%",
|
|
110
|
+
"not dead",
|
|
111
|
+
"not op_mini all"
|
|
112
|
+
],
|
|
113
|
+
"development": [
|
|
114
|
+
"last 1 chrome version",
|
|
115
|
+
"last 1 firefox version",
|
|
116
|
+
"last 1 safari version"
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
"husky": {
|
|
120
|
+
"hooks": {
|
|
121
|
+
"pre-commit": "npm run lint"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const copy = require("rollup-plugin-copy");
|
|
2
|
+
|
|
3
|
+
const outputDir = "dist";
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
input: "./empty.js",
|
|
7
|
+
plugins: [
|
|
8
|
+
copy({
|
|
9
|
+
targets: [
|
|
10
|
+
{
|
|
11
|
+
src: ["src/modal/*", "!**/*.test.ts*"],
|
|
12
|
+
dest: outputDir,
|
|
13
|
+
expandDirectories: true,
|
|
14
|
+
onlyFiles: true,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
src: ["src/util/*", "!**/*.test.ts*"],
|
|
18
|
+
dest: outputDir,
|
|
19
|
+
expandDirectories: true,
|
|
20
|
+
onlyFiles: true,
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
flatten: false,
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ModalInstanceContext } from "./ModalInstanceContext";
|
|
2
|
+
import { defer } from "lodash-es";
|
|
3
|
+
import { ReactElement, useContext, useEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
export interface ModalProps {
|
|
6
|
+
selectFirstInput?: boolean;
|
|
7
|
+
children: ReactElement | ReactElement[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Modal = ({ selectFirstInput = true, children }: ModalProps): ReactElement => {
|
|
11
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
12
|
+
|
|
13
|
+
const { close } = useContext(ModalInstanceContext);
|
|
14
|
+
|
|
15
|
+
// The only way to create a modal dialog is to call showModal, open attribute will not work
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// Check if it's open already to support .vite hot deploys
|
|
18
|
+
if (!dialogRef.current?.open) {
|
|
19
|
+
dialogRef.current?.showModal();
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
selectFirstInput && defer(() => dialogRef.current?.querySelector("input")?.select());
|
|
25
|
+
}, [selectFirstInput]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<dialog ref={dialogRef} onClick={(e) => e.target === e.currentTarget && close()} style={{ padding: 0 }}>
|
|
29
|
+
{children}
|
|
30
|
+
</dialog>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ComponentProps, MutableRefObject, ReactElement, createContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type ComponentType = (props: any) => ReactElement<any, any>;
|
|
4
|
+
|
|
5
|
+
export interface ModalCallback<R> {
|
|
6
|
+
resolve: (result: R | undefined) => void;
|
|
7
|
+
close: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ModalContextType {
|
|
11
|
+
showModal: <
|
|
12
|
+
OR extends HTMLElement | null,
|
|
13
|
+
PROPS extends ModalCallback<any>,
|
|
14
|
+
CT extends (props: PROPS) => ReactElement<any, any>,
|
|
15
|
+
RT = Parameters<Parameters<CT>[0]["resolve"]>[0],
|
|
16
|
+
>(
|
|
17
|
+
ownerRef: MutableRefObject<OR>,
|
|
18
|
+
component: CT,
|
|
19
|
+
args: Omit<ComponentProps<CT>, "resolve" | "close">,
|
|
20
|
+
) => Promise<RT>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ModalContext = createContext<ModalContextType>({
|
|
24
|
+
showModal: (async () => {
|
|
25
|
+
console.error("Missing ModalContext Provider");
|
|
26
|
+
}) as ModalContextType["showModal"],
|
|
27
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useInterval } from "../util/useInterval";
|
|
2
|
+
import { ComponentType, ModalContext } from "./ModalContext";
|
|
3
|
+
import { ModalInstanceContext } from "./ModalInstanceContext";
|
|
4
|
+
import { Fragment, MutableRefObject, ReactElement, useState } from "react";
|
|
5
|
+
import * as ReactDOM from "react-dom";
|
|
6
|
+
import { v4 as uuid } from "uuid";
|
|
7
|
+
|
|
8
|
+
export interface ModalInstance {
|
|
9
|
+
uuid: string;
|
|
10
|
+
ownerElement: Element | undefined;
|
|
11
|
+
componentInstance: ReactElement;
|
|
12
|
+
resolve: (result: any) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Provides the ability to show modals using react components without needing useState boilerplate and inline dialogs.
|
|
17
|
+
*
|
|
18
|
+
* To use:
|
|
19
|
+
* <ol>
|
|
20
|
+
* <li>Add this Provider somewhere in your standard providers
|
|
21
|
+
* <pre>
|
|
22
|
+
* <ModalContextProvider>
|
|
23
|
+
* ...children
|
|
24
|
+
* </ModalContextProvider>
|
|
25
|
+
* </pre>
|
|
26
|
+
* </li>
|
|
27
|
+
* <li>Add the modal return type to the element as below, and use the props resolve/close from ModalProps for results.
|
|
28
|
+
* <pre>
|
|
29
|
+
* export interface SomeModalProps extends ModalProps<number> {
|
|
30
|
+
* someProp?: number; // user props
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* export const SomeModal = ({initialState, resolve, close}: SomeModalProps): ReactElement => {
|
|
34
|
+
* return Modal(
|
|
35
|
+
* <div>
|
|
36
|
+
* Itsa me, I'm modal
|
|
37
|
+
* <button onClick={close}>Cancel</button>
|
|
38
|
+
* <button onClick={()=>resolve(someProp)}>Save</button>
|
|
39
|
+
* </div>
|
|
40
|
+
* )}
|
|
41
|
+
* }
|
|
42
|
+
* </pre>
|
|
43
|
+
* </li>
|
|
44
|
+
* <li> To show the dialog and get the result...
|
|
45
|
+
* <pre>
|
|
46
|
+
* // Note: modalOwnerRef is only required if you need to support popout windows
|
|
47
|
+
* const { showModal, modalOwnerRef } = useContext(ModalContext);
|
|
48
|
+
* ...
|
|
49
|
+
* const showModal = () => {
|
|
50
|
+
* const result = await showModal(SomeModal, { someProp: 1 });
|
|
51
|
+
* if (!result) return; // modal cancelled
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* return <button onClick={showModal} ref={modalOwnerRef}>Show Modal!</button>
|
|
55
|
+
* </pre>
|
|
56
|
+
* </li>
|
|
57
|
+
* </ol>
|
|
58
|
+
*/
|
|
59
|
+
export const ModalContextProvider = ({ children }: { children: ReactElement }): ReactElement => {
|
|
60
|
+
const [modals, setModals] = useState<ModalInstance[]>([]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Inserts the modal into the page, and removes once modal has a result.
|
|
64
|
+
* Note: full generic types are provided by the interface. Function def has been simplified here.
|
|
65
|
+
*
|
|
66
|
+
* @param ownerRef Reference to div that opened this dialog such that it works for popout windows.
|
|
67
|
+
* @param Component React component.
|
|
68
|
+
* @param args Arguments for react component.
|
|
69
|
+
*/
|
|
70
|
+
const showModal = async (
|
|
71
|
+
ownerRef: MutableRefObject<HTMLElement | null>,
|
|
72
|
+
Component: ComponentType,
|
|
73
|
+
args: any,
|
|
74
|
+
): Promise<any> => {
|
|
75
|
+
let componentInstance: ReactElement | undefined;
|
|
76
|
+
const promise = new Promise((resolve) => {
|
|
77
|
+
try {
|
|
78
|
+
// If there are any exceptions the modal won't show
|
|
79
|
+
setModals([
|
|
80
|
+
...modals,
|
|
81
|
+
{
|
|
82
|
+
uuid: uuid(),
|
|
83
|
+
ownerElement: ownerRef.current ?? document.body,
|
|
84
|
+
componentInstance: <Component {...args} resolve={resolve} close={() => resolve(undefined)} />,
|
|
85
|
+
resolve,
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error(e);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Wait for modal to complete
|
|
95
|
+
const result = await promise;
|
|
96
|
+
|
|
97
|
+
// Close modal
|
|
98
|
+
setModals(modals.filter((e) => e.componentInstance !== componentInstance));
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const modalHasView = (modalInstance: ModalInstance): boolean =>
|
|
104
|
+
!!modalInstance.ownerElement?.ownerDocument?.defaultView;
|
|
105
|
+
|
|
106
|
+
// Tidy up modals that have closed because of an external window closing
|
|
107
|
+
useInterval(() => {
|
|
108
|
+
const newModals = modals.filter(modalHasView);
|
|
109
|
+
if (newModals.length !== modals.length) {
|
|
110
|
+
setModals(newModals);
|
|
111
|
+
}
|
|
112
|
+
}, 1000);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<ModalContext.Provider
|
|
116
|
+
value={{
|
|
117
|
+
showModal,
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<>
|
|
121
|
+
<Fragment key={"modals"}>
|
|
122
|
+
{modals
|
|
123
|
+
.filter(modalHasView)
|
|
124
|
+
.map((modalInstance) =>
|
|
125
|
+
ReactDOM.createPortal(
|
|
126
|
+
<ModalInstanceContext.Provider value={{ close: () => modalInstance.resolve(undefined) }}>
|
|
127
|
+
{modalInstance.componentInstance}
|
|
128
|
+
</ModalInstanceContext.Provider>,
|
|
129
|
+
(modalInstance.ownerElement?.ownerDocument ?? document).body,
|
|
130
|
+
),
|
|
131
|
+
)}
|
|
132
|
+
</Fragment>
|
|
133
|
+
<Fragment key={"children"}>{children}</Fragment>
|
|
134
|
+
</>
|
|
135
|
+
</ModalContext.Provider>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
|
|
3
|
+
export interface ModalInstanceContextType {
|
|
4
|
+
close: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const NoContextError = () => {
|
|
8
|
+
console.error("Missing ModalInstanceContext Provider");
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Provides access to resolving/closing to modal elements.
|
|
13
|
+
*/
|
|
14
|
+
export const ModalInstanceContext = createContext<ModalInstanceContextType>({
|
|
15
|
+
close: NoContextError,
|
|
16
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ComponentType, ModalContext } from "./ModalContext";
|
|
2
|
+
import { ComponentProps, useContext, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
export const useShowModal = () => {
|
|
5
|
+
const { showModal } = useContext(ModalContext);
|
|
6
|
+
|
|
7
|
+
const modalOwnerRef = useRef<any>(null);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
showModal: <CT extends ComponentType>(
|
|
11
|
+
component: CT,
|
|
12
|
+
args: Omit<ComponentProps<CT>, "resolve" | "close">,
|
|
13
|
+
): Promise<Parameters<Parameters<CT>[0]["resolve"]>[0]> => showModal(modalOwnerRef, component, args),
|
|
14
|
+
modalOwnerRef,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {Meta, Source, Story} from "@storybook/blocks";
|
|
2
|
+
import * as ModalStories from "./Modal.stories"
|
|
3
|
+
|
|
4
|
+
import myModule from './TestModal?raw';
|
|
5
|
+
|
|
6
|
+
<Meta title="Modal/Primary" of={ModalStories}/>
|
|
7
|
+
# Show Modal
|
|
8
|
+
## Example
|
|
9
|
+
Click to show the modal:
|
|
10
|
+
<Story of={ModalStories.ShowModal}/>
|
|
11
|
+
|
|
12
|
+
## Code
|
|
13
|
+
<br/>
|
|
14
|
+
{myModule.toString().split("// #").splice(1).map(block => (
|
|
15
|
+
<>
|
|
16
|
+
<h3>{block.split("\n")[0]}</h3>
|
|
17
|
+
<Source code={block.split("\n").splice(1).join("\n")} language='typescript'/>
|
|
18
|
+
</>
|
|
19
|
+
))}
|
|
20
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ModalContextProvider } from "../../modal/ModalContextProvider";
|
|
2
|
+
import { TestModalUsage } from "./TestModal";
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof TestModalUsage> = {
|
|
6
|
+
title: "Modal",
|
|
7
|
+
component: TestModalUsage,
|
|
8
|
+
argTypes: {
|
|
9
|
+
backgroundColor: { control: "color" },
|
|
10
|
+
},
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story: any) => (
|
|
13
|
+
<div>
|
|
14
|
+
<ModalContextProvider>
|
|
15
|
+
<Story />
|
|
16
|
+
</ModalContextProvider>
|
|
17
|
+
</div>
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
export const ShowModal: Story = {
|
|
26
|
+
args: {},
|
|
27
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import "./TestModal.scss";
|
|
2
|
+
|
|
3
|
+
import { Modal } from "../../modal/Modal";
|
|
4
|
+
import { ModalCallback } from "../../modal/ModalContext";
|
|
5
|
+
import { ModalContextProvider } from "../../modal/ModalContextProvider";
|
|
6
|
+
import { useShowModal } from "../../modal/useShowModal";
|
|
7
|
+
|
|
8
|
+
// #Example: Modal Context Provider
|
|
9
|
+
// Don't forget to add a ModalContextProvider at the root of your project
|
|
10
|
+
export const App = () => (
|
|
11
|
+
<ModalContextProvider>
|
|
12
|
+
<div>...the rest of your app...</div>
|
|
13
|
+
</ModalContextProvider>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// #Example: Modal Component
|
|
17
|
+
// Extend your props with ModalCallback<RESULT_TYPE> to add a return type, and enable close/resolve
|
|
18
|
+
export interface TestModalProps extends ModalCallback<number> {
|
|
19
|
+
text: string; // A user property
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Close and resolve will be passed to your props magically!
|
|
23
|
+
export const TestModal = ({ text, close, resolve }: TestModalProps) => (
|
|
24
|
+
<Modal>
|
|
25
|
+
<div>
|
|
26
|
+
<div>This is the modal text: '{text}'</div>
|
|
27
|
+
<div>
|
|
28
|
+
<button onClick={close}>Close</button>
|
|
29
|
+
<button onClick={() => resolve(1)}>Return 1</button>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</Modal>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// #Example: Modal Invocation
|
|
36
|
+
export const TestModalUsage = () => {
|
|
37
|
+
// showModal to show modal, modalOwnerRef is only required if you have popout windows
|
|
38
|
+
const { showModal, modalOwnerRef } = useShowModal();
|
|
39
|
+
|
|
40
|
+
const showModalHandler = async () => {
|
|
41
|
+
// Show modal and await result
|
|
42
|
+
const result = await showModal(TestModal, { text: "Text text" });
|
|
43
|
+
|
|
44
|
+
// If result is undefined the modal was closed
|
|
45
|
+
if (!result) return alert("Modal closed");
|
|
46
|
+
|
|
47
|
+
// Otherwise we have a result
|
|
48
|
+
alert(`Modal result is: ${result}`);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Remember to add the modalOwnerRef!
|
|
52
|
+
return (
|
|
53
|
+
<div ref={modalOwnerRef}>
|
|
54
|
+
<button onClick={showModalHandler}>Show modal</button>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useInterval } from "./useInterval";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
describe("useInterval", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
jest.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
jest.useRealTimers();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const TestComponent = (props: { callback: () => void }) => {
|
|
14
|
+
useInterval(props.callback, 1000);
|
|
15
|
+
return <div />;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
test("invokes on timeout", async () => {
|
|
19
|
+
const callback = jest.fn();
|
|
20
|
+
|
|
21
|
+
render(<TestComponent callback={callback} />);
|
|
22
|
+
expect(callback).toHaveBeenCalledTimes(0);
|
|
23
|
+
jest.runOnlyPendingTimers();
|
|
24
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
type Callback = () => void | Promise<void>;
|
|
4
|
+
|
|
5
|
+
export const useInterval = (callback: Callback, delay: number | null) => {
|
|
6
|
+
const savedCallback = useRef(callback);
|
|
7
|
+
const callbackInProgress = useRef(false);
|
|
8
|
+
savedCallback.current = callback;
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (delay == null) return;
|
|
12
|
+
|
|
13
|
+
const id = setInterval(async () => {
|
|
14
|
+
// Since this is an interval, then next tick could occur before the previous has finished
|
|
15
|
+
if (!callbackInProgress.current && savedCallback.current) {
|
|
16
|
+
callbackInProgress.current = true;
|
|
17
|
+
try {
|
|
18
|
+
await savedCallback.current();
|
|
19
|
+
} finally {
|
|
20
|
+
callbackInProgress.current = false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}, delay);
|
|
24
|
+
return () => clearInterval(id);
|
|
25
|
+
}, [delay]);
|
|
26
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
// see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
|
|
3
|
+
"include": ["src"],
|
|
4
|
+
"files": ["node_modules/jest-expect-message/types/index.d.ts"],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"baseUrl": "./src",
|
|
7
|
+
"module": "esnext",
|
|
8
|
+
"lib": ["dom", "esnext"],
|
|
9
|
+
"target": "ESNext",
|
|
10
|
+
"importHelpers": true,
|
|
11
|
+
// output .d.ts declaration files for consumers
|
|
12
|
+
"declaration": true,
|
|
13
|
+
// output .js.map sourcemap files for consumers
|
|
14
|
+
"sourceMap": true,
|
|
15
|
+
// match output dir to input dir. e.g. dist/index instead of dist/src/index
|
|
16
|
+
"rootDir": ".",
|
|
17
|
+
// stricter type-checking for stronger correctness. Recommended by TS
|
|
18
|
+
"strict": true,
|
|
19
|
+
// linter checks for common issues
|
|
20
|
+
"noImplicitReturns": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
// noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
|
|
23
|
+
"noUnusedLocals": false,
|
|
24
|
+
"noUnusedParameters": true,
|
|
25
|
+
// use Node's module resolution algorithm, instead of the legacy TS one
|
|
26
|
+
"moduleResolution": "node",
|
|
27
|
+
// transpile JSX to React.createElement
|
|
28
|
+
"jsx": "react-jsx",
|
|
29
|
+
// interop between ESM and CJS modules. Recommended by TS
|
|
30
|
+
"esModuleInterop": true,
|
|
31
|
+
// significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
|
|
32
|
+
"skipLibCheck": true,
|
|
33
|
+
// error out if import and file system have a casing mismatch. Recommended by TS
|
|
34
|
+
"forceConsistentCasingInFileNames": true,
|
|
35
|
+
"declarationDir":"./declaration"
|
|
36
|
+
}
|
|
37
|
+
}
|