@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 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}}
@@ -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
+ [![semantic-release](https://img.shields.io/badge/semantic--release-react-e10079?logo=semantic-release)](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
+ };
@@ -0,0 +1,2 @@
1
+ import "@testing-library/jest-dom";
2
+ import "jest-canvas-mock";
package/empty.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -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,5 @@
1
+ // jest-dom adds custom jest matchers for asserting on DOM nodes.
2
+ // allows you to do things like:
3
+ // expect(element).toHaveTextContent(/react/i)
4
+ // learn more: https://github.com/testing-library/jest-dom
5
+ import "@testing-library/jest-dom";
@@ -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,15 @@
1
+ dialog > div {
2
+ padding: 20px;
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 16px;
6
+
7
+ div {
8
+ display: flex;
9
+ gap: 10px;
10
+ }
11
+
12
+ button {
13
+ flex: 1;
14
+ }
15
+ }
@@ -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
+ }