diff --git a/CHANGELOG.md b/CHANGELOG.md index ad0be6f557..e6216b3f10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## Added - Modernized `dcc.Tabs` +- Modernized `dcc.DatePickerSingle` and `dcc.DatePickerRange` ## Changed - `dcc.Tab` now accepts a `width` prop which can be a pixel or percentage width for an individual tab. @@ -50,7 +51,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). - [#3347](https://github.com/plotly/dash/pull/3347) Added 'api_endpoint' to `callback` to expose api endpoints at the provided path for use to be executed directly without dash. - [#3445](https://github.com/plotly/dash/pull/3445) Added API to reverse direction of slider component. - [#3460](https://github.com/plotly/dash/pull/3460) Add `/health` endpoint for server monitoring and health checks. -- [#3465](https://github.com/plotly/dash/pull/3465) Plotly cloud integrations, add devtool API, placeholder plotly cloud CLI & publish button, `dash[cloud]` extra dependencies. +- [#3465](https://github.com/plotly/dash/pull/3465) Plotly cloud integrations, add devtool API, placeholder plotly cloud CLI & publish button, `dash[cloud]` extra dependencies. ## Fixed - [#3395](https://github.com/plotly/dash/pull/3395) Fix Components added through set_props() cannot trigger related callback functions. Fix [#3316](https://github.com/plotly/dash/issues/3316) diff --git a/components/dash-core-components/jest.config.js b/components/dash-core-components/jest.config.js index c92c83b0ff..7855b5f84a 100644 --- a/components/dash-core-components/jest.config.js +++ b/components/dash-core-components/jest.config.js @@ -1,14 +1,29 @@ module.exports = { preset: 'ts-jest', - testEnvironment: 'node', + testEnvironment: 'jsdom', roots: ['/tests'], - testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], + testMatch: ['**/__tests__/**/*.{ts,tsx}', '**/?(*.)+(spec|test).{ts,tsx}'], transform: { - '^.+\\.ts$': 'ts-jest', + '^.+\\.(ts|tsx)$': ['ts-jest', { + tsconfig: { + jsx: 'react', + esModuleInterop: true, + allowSyntheticDefaultImports: true, + types: ['jest', '@testing-library/jest-dom'], + }, + }], + '^.+\\.js$': ['ts-jest', { + tsconfig: { + allowJs: true, + }, + }], }, collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'], moduleNameMapper: { '^@/(.*)$': '/src/$1', + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', }, setupFilesAfterEnv: ['/jest.setup.js'], + // Disable caching to ensure TypeScript type changes are always picked up + cache: false, }; diff --git a/components/dash-core-components/jest.setup.js b/components/dash-core-components/jest.setup.js index 71a3d440b4..48c2075115 100644 --- a/components/dash-core-components/jest.setup.js +++ b/components/dash-core-components/jest.setup.js @@ -1,2 +1,11 @@ // Jest setup file -// This file is loaded before every test file \ No newline at end of file +// This file is loaded before every test file +import '@testing-library/jest-dom'; + +// Mock window.dash_component_api for components that use Dash context +global.window = global.window || {}; +global.window.dash_component_api = { + useDashContext: () => ({ + useLoading: () => false, + }), +}; \ No newline at end of file diff --git a/components/dash-core-components/package-lock.json b/components/dash-core-components/package-lock.json index f48b534638..cc57aad0c7 100644 --- a/components/dash-core-components/package-lock.json +++ b/components/dash-core-components/package-lock.json @@ -30,10 +30,10 @@ "prop-types": "^15.8.1", "ramda": "^0.30.1", "react-addons-shallow-compare": "^15.6.3", - "react-dates": "^21.8.0", "react-docgen": "^5.4.3", "react-dropzone": "^4.1.2", "react-fast-compare": "^3.2.2", + "react-input-autosize": "^3.0.0", "react-markdown": "^4.3.1", "react-virtualized-select": "^3.1.3", "remark-math": "^3.0.1", @@ -49,6 +49,8 @@ "@babel/preset-typescript": "^7.27.1", "@plotly/dash-component-plugins": "^1.2.3", "@plotly/webpack-dash-dynamic-import": "^1.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^12.1.5", "@types/d3-format": "^3.0.4", "@types/fast-isnumeric": "^1.1.2", "@types/jest": "^29.5.0", @@ -56,6 +58,7 @@ "@types/ramda": "^0.31.0", "@types/react": "^16.14.8", "@types/react-dom": "^16.9.13", + "@types/react-input-autosize": "^2.2.4", "@types/uniqid": "^5.3.4", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", @@ -69,6 +72,7 @@ "eslint-plugin-react": "^7.32.2", "identity-obj-proxy": "^3.0.0", "jest": "^29.5.0", + "jest-environment-jsdom": "^30.2.0", "npm-run-all": "^4.1.5", "prettier": "^2.8.8", "react": "^16.14.0", @@ -99,6 +103,13 @@ "node": ">=0.10.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -111,6 +122,27 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/cli": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.28.0.tgz", @@ -2054,6 +2086,121 @@ "node": ">=0.1.90" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -2754,6 +2901,321 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", @@ -2815,6 +3277,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -4033,26 +4519,220 @@ "dev": true, "license": "MIT" }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "type-detect": "4.0.8" + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "node_modules/@testing-library/react": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.5.tgz", + "integrity": "sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.0.0", + "@types/react-dom": "<18.0.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "<18.0.0", + "react-dom": "<18.0.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4192,6 +4872,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4252,6 +4944,16 @@ "@types/react": "^16.0.0" } }, + "node_modules/@types/react-input-autosize": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@types/react-input-autosize/-/react-input-autosize-2.2.4.tgz", + "integrity": "sha512-7O028jRZHZo3mj63h3HSvB0WpvPXNWN86sajHTi0+CtjA4Ym+DFzO9RzrSbfFURe5ZWsq6P72xk7MInI6aGWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -4273,6 +4975,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -4802,26 +5511,14 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/airbnb-prop-types": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", - "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", - "dependencies": { - "array.prototype.find": "^2.1.1", - "function.prototype.name": "^1.1.2", - "is-regex": "^1.1.0", - "object-is": "^1.1.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.2", - "prop-types": "^15.7.2", - "prop-types-exact": "^1.2.0", - "react-is": "^16.13.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - }, - "peerDependencies": { - "react": "^0.14 || ^15.0.0 || ^16.0.0-alpha" + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" } }, "node_modules/ajv": { @@ -4960,10 +5657,21 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "is-array-buffer": "^3.0.1" @@ -5001,20 +5709,6 @@ "node": ">=8" } }, - "node_modules/array.prototype.find": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.2.2.tgz", - "integrity": "sha512-DRumkfW97iZGOfn+lIXbkVrXL04sfYKX+EfOodo8XboR5sxPDVvOjZTF/rysusa9lmhmSOeD6Vp6RKQP+eP4Tg==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array.prototype.findlastindex": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", @@ -5038,6 +5732,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -5086,6 +5781,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.2", @@ -5533,11 +6229,6 @@ "node": ">=8" } }, - "node_modules/brcast": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brcast/-/brcast-2.0.2.tgz", - "integrity": "sha512-Tfn5JSE7hrUlFcOoaLzVvkbgIemIorMIyoMr3TgvszWW7jFt2C9PdeMLtysYD9RU0MmU17b69+XJG1eRY2OBRg==" - }, "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -6111,11 +6802,6 @@ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" }, - "node_modules/consolidated-events": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/consolidated-events/-/consolidated-events-2.0.2.tgz", - "integrity": "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==" - }, "node_modules/constants-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", @@ -6402,6 +7088,13 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -6414,6 +7107,20 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -6424,6 +7131,20 @@ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz", "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==" }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -6441,6 +7162,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/dedent": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", @@ -6456,20 +7184,45 @@ } } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepmerge": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", - "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -6576,18 +7329,6 @@ "node": ">=8" } }, - "node_modules/direction": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", - "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==", - "bin": { - "direction": "cli.js" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6599,16 +7340,12 @@ "node": ">=6.0.0" } }, - "node_modules/document.contains": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/document.contains/-/document.contains-1.0.2.tgz", - "integrity": "sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q==", - "dependencies": { - "define-properties": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -6787,18 +7524,6 @@ "node": ">=4" } }, - "node_modules/enzyme-shallow-equal": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz", - "integrity": "sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg==", - "dependencies": { - "has": "^1.0.3", - "object-is": "^1.1.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -6812,6 +7537,7 @@ "version": "1.22.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", + "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "arraybuffer.prototype.slice": "^1.0.2", @@ -6930,6 +7656,27 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-iterator-helpers": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", @@ -6974,6 +7721,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "dev": true, "dependencies": { "get-intrinsic": "^1.2.2", "has-tostringtag": "^1.0.0", @@ -6987,6 +7735,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, "dependencies": { "hasown": "^2.0.0" } @@ -6995,6 +7744,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -7834,6 +8584,7 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -7851,6 +8602,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7944,6 +8696,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -7992,22 +8745,11 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, - "node_modules/global-cache": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/global-cache/-/global-cache-1.2.1.tgz", - "integrity": "sha512-EOeUaup5DgWKlCMhA9YFqNRIlZwoxt731jCh47WBV9fQqHgXhr3Fa55hfgIUqilIcPsfdNKN7LHjrNY+Km40KA==", - "dependencies": { - "define-properties": "^1.1.2", - "is-symbol": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, "dependencies": { "define-properties": "^1.1.3" }, @@ -8101,18 +8843,11 @@ "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", "dev": true }, - "node_modules/has": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", - "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8142,6 +8877,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8241,20 +8977,25 @@ "minimalistic-crypto-utils": "^1.0.1" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -8291,11 +9032,39 @@ "entities": "^4.5.0" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/https-browserify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -8306,6 +9075,19 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -8466,6 +9248,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -8481,13 +9273,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8543,6 +9337,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.0", @@ -8577,6 +9372,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, "dependencies": { "has-bigints": "^1.0.1" }, @@ -8602,6 +9398,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -8649,6 +9446,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -8770,6 +9568,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8791,6 +9590,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -8830,10 +9630,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -8858,6 +9666,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -8881,6 +9690,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -8900,6 +9710,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, "dependencies": { "has-symbols": "^1.0.2" }, @@ -8910,11 +9721,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-touch-device": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-touch-device/-/is-touch-device-1.0.1.tgz", - "integrity": "sha512-LAYzo9kMT1b2p19L/1ATGt2XcSilnzNlyvq6c0pbPRVisLbAPpLqr53tIJS00kvrTkj0HtR8U7+u8X0yR8lPSw==" - }, "node_modules/is-typed-array": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", @@ -8943,6 +9749,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2" }, @@ -9845,6 +10652,318 @@ "node": ">=8" } }, + "node_modules/jest-environment-jsdom": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -11079,6 +12198,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -11258,11 +12417,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -11324,6 +12478,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -11836,6 +13000,13 @@ "node": ">=8" } }, + "node_modules/nwsapi": { + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -11845,9 +13016,13 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11896,6 +13071,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -11951,6 +13127,7 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -12107,7 +13284,33 @@ "json-parse-better-errors": "^1.0.1" }, "engines": { - "node": ">=4" + "node": ">=4" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/path-browserify": { @@ -12239,11 +13442,6 @@ "inherits": "^2.0.1" } }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -12602,16 +13800,6 @@ "react-is": "^16.13.1" } }, - "node_modules/prop-types-exact": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", - "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", - "dependencies": { - "has": "^1.0.3", - "object.assign": "^4.1.0", - "reflect.ownkeys": "^0.2.0" - } - }, "node_modules/public-encrypt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", @@ -12697,14 +13885,6 @@ } ] }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dependencies": { - "performance-now": "^2.1.0" - } - }, "node_modules/ramda": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", @@ -12755,35 +13935,6 @@ "object-assign": "^4.1.0" } }, - "node_modules/react-dates": { - "version": "21.8.0", - "resolved": "https://registry.npmjs.org/react-dates/-/react-dates-21.8.0.tgz", - "integrity": "sha512-PPriGqi30CtzZmoHiGdhlA++YPYPYGCZrhydYmXXQ6RAvAsaONcPtYgXRTLozIOrsQ5mSo40+DiA5eOFHnZ6xw==", - "dependencies": { - "airbnb-prop-types": "^2.15.0", - "consolidated-events": "^1.1.1 || ^2.0.0", - "enzyme-shallow-equal": "^1.0.0", - "is-touch-device": "^1.0.1", - "lodash": "^4.1.1", - "object.assign": "^4.1.0", - "object.values": "^1.1.0", - "prop-types": "^15.7.2", - "raf": "^3.4.1", - "react-moment-proptypes": "^1.6.0", - "react-outside-click-handler": "^1.2.4", - "react-portal": "^4.2.0", - "react-with-direction": "^1.3.1", - "react-with-styles": "^4.1.0", - "react-with-styles-interface-css": "^6.0.0" - }, - "peerDependencies": { - "@babel/runtime": "^7.0.0", - "moment": "^2.18.1", - "react": "^0.14 || ^15.5.4 || ^16.1.1", - "react-dom": "^0.14 || ^15.5.4 || ^16.1.1", - "react-with-direction": "^1.3.1" - } - }, "node_modules/react-docgen": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz", @@ -12846,14 +13997,15 @@ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, "node_modules/react-input-autosize": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", - "integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-3.0.0.tgz", + "integrity": "sha512-nL9uS7jEs/zu8sqwFE5MAPx6pPkNAriACQ2rGLlqmKr2sPGtN7TXTyDdQt4lbNXVx7Uzadb40x8qotIuru6Rhg==", + "license": "MIT", "dependencies": { "prop-types": "^15.5.8" }, "peerDependencies": { - "react": "^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0" + "react": "^16.3.0 || ^17.0.0" } }, "node_modules/react-is": { @@ -12910,45 +14062,6 @@ "react": "^15.0.0 || ^16.0.0" } }, - "node_modules/react-moment-proptypes": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.8.1.tgz", - "integrity": "sha512-Er940DxWoObfIqPrZNfwXKugjxMIuk1LAuEzn23gytzV6hKS/sw108wibi9QubfMN4h+nrlje8eUCSbQRJo2fQ==", - "dependencies": { - "moment": ">=1.6.0" - }, - "peerDependencies": { - "moment": ">=1.6.0" - } - }, - "node_modules/react-outside-click-handler": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz", - "integrity": "sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ==", - "dependencies": { - "airbnb-prop-types": "^2.15.0", - "consolidated-events": "^1.1.1 || ^2.0.0", - "document.contains": "^1.0.1", - "object.values": "^1.1.0", - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "react": "^0.14 || >=15", - "react-dom": "^0.14 || >=15" - } - }, - "node_modules/react-portal": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/react-portal/-/react-portal-4.2.2.tgz", - "integrity": "sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q==", - "dependencies": { - "prop-types": "^15.5.8" - }, - "peerDependencies": { - "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0", - "react-dom": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" - } - }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", @@ -13010,6 +14123,18 @@ "react-dom": "^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0" } }, + "node_modules/react-select/node_modules/react-input-autosize": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", + "integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.8" + }, + "peerDependencies": { + "react": "^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -13064,55 +14189,6 @@ "react-dom": "^15.3.0 || ^16.0.0-alpha" } }, - "node_modules/react-with-direction": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.4.0.tgz", - "integrity": "sha512-ybHNPiAmaJpoWwugwqry9Hd1Irl2hnNXlo/2SXQBwbLn/jGMauMS2y9jw+ydyX5V9ICryCqObNSthNt5R94xpg==", - "dependencies": { - "airbnb-prop-types": "^2.16.0", - "brcast": "^2.0.2", - "deepmerge": "^1.5.2", - "direction": "^1.0.4", - "hoist-non-react-statics": "^3.3.2", - "object.assign": "^4.1.2", - "object.values": "^1.1.5", - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "react": "^0.14 || ^15 || ^16", - "react-dom": "^0.14 || ^15 || ^16" - } - }, - "node_modules/react-with-styles": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/react-with-styles/-/react-with-styles-4.2.0.tgz", - "integrity": "sha512-tZCTY27KriRNhwHIbg1NkSdTTOSfXDg6Z7s+Q37mtz0Ym7Sc7IOr3PzVt4qJhJMW6Nkvfi3g34FuhtiGAJCBQA==", - "dependencies": { - "airbnb-prop-types": "^2.14.0", - "hoist-non-react-statics": "^3.2.1", - "object.assign": "^4.1.0", - "prop-types": "^15.7.2", - "react-with-direction": "^1.3.1" - }, - "peerDependencies": { - "@babel/runtime": "^7.0.0", - "react": ">=0.14", - "react-with-direction": "^1.3.1" - } - }, - "node_modules/react-with-styles-interface-css": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/react-with-styles-interface-css/-/react-with-styles-interface-css-6.0.0.tgz", - "integrity": "sha512-6khSG1Trf4L/uXOge/ZAlBnq2O2PEXlQEqAhCRbvzaQU4sksIkdwpCPEl6d+DtP3+IdhyffTWuHDO9lhe1iYvA==", - "dependencies": { - "array.prototype.flat": "^1.2.1", - "global-cache": "^1.2.1" - }, - "peerDependencies": { - "@babel/runtime": "^7.0.0", - "react-with-styles": "^3.0.0 || ^4.0.0" - } - }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -13168,6 +14244,20 @@ "node": ">= 10.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", @@ -13188,11 +14278,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/reflect.ownkeys": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", - "integrity": "sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg==" - }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -13220,6 +14305,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -13504,6 +14590,13 @@ "inherits": "^2.0.1" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -13531,6 +14624,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "dev": true, "dependencies": { "call-bind": "^1.0.5", "get-intrinsic": "^1.2.2", @@ -13567,6 +14661,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.5", "get-intrinsic": "^1.2.2", @@ -13593,6 +14688,19 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", @@ -13696,6 +14804,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dev": true, "dependencies": { "define-data-property": "^1.0.1", "functions-have-names": "^1.2.3", @@ -13763,13 +14872,72 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13921,6 +15089,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -14058,6 +15240,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -14074,6 +15257,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -14087,6 +15271,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -14226,6 +15411,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tapable": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", @@ -14376,6 +15568,26 @@ "node": ">=0.6.0" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -14410,6 +15622,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/trim": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", @@ -14647,6 +15885,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -14664,6 +15903,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -14682,6 +15922,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -14733,6 +15974,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", @@ -15040,6 +16282,19 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -15063,6 +16318,16 @@ "node": ">=10.13.0" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/webpack": { "version": "5.101.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.0.tgz", @@ -15238,6 +16503,43 @@ "node": ">=4.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -15256,6 +16558,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -15522,11 +16825,50 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/x-is-string": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", "integrity": "sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==" }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/components/dash-core-components/package.json b/components/dash-core-components/package.json index 8eee9b6868..85b83d12fa 100644 --- a/components/dash-core-components/package.json +++ b/components/dash-core-components/package.json @@ -57,10 +57,10 @@ "prop-types": "^15.8.1", "ramda": "^0.30.1", "react-addons-shallow-compare": "^15.6.3", - "react-dates": "^21.8.0", "react-docgen": "^5.4.3", "react-dropzone": "^4.1.2", "react-fast-compare": "^3.2.2", + "react-input-autosize": "^3.0.0", "react-markdown": "^4.3.1", "react-virtualized-select": "^3.1.3", "remark-math": "^3.0.1", @@ -76,6 +76,8 @@ "@babel/preset-typescript": "^7.27.1", "@plotly/dash-component-plugins": "^1.2.3", "@plotly/webpack-dash-dynamic-import": "^1.3.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^12.1.5", "@types/d3-format": "^3.0.4", "@types/fast-isnumeric": "^1.1.2", "@types/jest": "^29.5.0", @@ -83,6 +85,7 @@ "@types/ramda": "^0.31.0", "@types/react": "^16.14.8", "@types/react-dom": "^16.9.13", + "@types/react-input-autosize": "^2.2.4", "@types/uniqid": "^5.3.4", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", @@ -96,6 +99,7 @@ "eslint-plugin-react": "^7.32.2", "identity-obj-proxy": "^3.0.0", "jest": "^29.5.0", + "jest-environment-jsdom": "^30.2.0", "npm-run-all": "^4.1.5", "prettier": "^2.8.8", "react": "^16.14.0", diff --git a/components/dash-core-components/src/components/DatePickerRange.react.js b/components/dash-core-components/src/components/DatePickerRange.react.js deleted file mode 100644 index 3cdfe83551..0000000000 --- a/components/dash-core-components/src/components/DatePickerRange.react.js +++ /dev/null @@ -1,280 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {Component, lazy, Suspense} from 'react'; -import datePickerRange from '../utils/LazyLoader/datePickerRange'; -import transformDate from '../utils/DatePickerPersistence'; - -const RealDatePickerRange = lazy(datePickerRange); - -/** - * DatePickerRange is a tailor made component designed for selecting - * timespan across multiple days off of a calendar. - * - * The DatePicker integrates well with the Python datetime module with the - * startDate and endDate being returned in a string format suitable for - * creating datetime objects. - * - * This component is based off of Airbnb's react-dates react component - * which can be found here: https://github.com/airbnb/react-dates - */ -export default class DatePickerRange extends Component { - render() { - return ( - - - - ); - } -} - -DatePickerRange.propTypes = { - /** - * Specifies the starting date for the component. - * Accepts datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - */ - start_date: PropTypes.string, - - /** - * Specifies the ending date for the component. - * Accepts datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - */ - end_date: PropTypes.string, - - /** - * Specifies the lowest selectable date for the component. - * Accepts datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - */ - min_date_allowed: PropTypes.string, - - /** - * Specifies the highest selectable date for the component. - * Accepts datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - */ - max_date_allowed: PropTypes.string, - - /** - * Specifies additional days between min_date_allowed and max_date_allowed - * that should be disabled. Accepted datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - */ - disabled_days: PropTypes.arrayOf(PropTypes.string), - - /** - * Specifies a minimum number of nights that must be selected between - * the startDate and the endDate - */ - minimum_nights: PropTypes.number, - - /** - * Determines when the component should update - * its value. If `bothdates`, then the DatePicker - * will only trigger its value when the user has - * finished picking both dates. If `singledate`, then - * the DatePicker will update its value - * as one date is picked. - */ - updatemode: PropTypes.oneOf(['singledate', 'bothdates']), - - /** - * Text that will be displayed in the first input - * box of the date picker when no date is selected. Default value is 'Start Date' - */ - start_date_placeholder_text: PropTypes.string, - - /** - * Text that will be displayed in the second input - * box of the date picker when no date is selected. Default value is 'End Date' - */ - end_date_placeholder_text: PropTypes.string, - - /** - * Specifies the month that is initially presented when the user - * opens the calendar. Accepts datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - * - */ - initial_visible_month: PropTypes.string, - - /** - * Whether or not the dropdown is "clearable", that is, whether or - * not a small "x" appears on the right of the dropdown that removes - * the selected value. - */ - clearable: PropTypes.bool, - - /** - * If True, the calendar will automatically open when cleared - */ - reopen_calendar_on_clear: PropTypes.bool, - - /** - * Specifies the format that the selected dates will be displayed - * valid formats are variations of "MM YY DD". For example: - * "MM YY DD" renders as '05 10 97' for May 10th 1997 - * "MMMM, YY" renders as 'May, 1997' for May 10th 1997 - * "M, D, YYYY" renders as '07, 10, 1997' for September 10th 1997 - * "MMMM" renders as 'May' for May 10 1997 - */ - display_format: PropTypes.string, - - /** - * Specifies the format that the month will be displayed in the calendar, - * valid formats are variations of "MM YY". For example: - * "MM YY" renders as '05 97' for May 1997 - * "MMMM, YYYY" renders as 'May, 1997' for May 1997 - * "MMM, YY" renders as 'Sep, 97' for September 1997 - */ - month_format: PropTypes.string, - - /** - * Specifies what day is the first day of the week, values must be - * from [0, ..., 6] with 0 denoting Sunday and 6 denoting Saturday - */ - first_day_of_week: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6]), - - /** - * If True the calendar will display days that rollover into - * the next month - */ - show_outside_days: PropTypes.bool, - - /** - * If True the calendar will not close when the user has selected a value - * and will wait until the user clicks off the calendar - */ - stay_open_on_select: PropTypes.bool, - - /** - * Orientation of calendar, either vertical or horizontal. - * Valid options are 'vertical' or 'horizontal'. - */ - calendar_orientation: PropTypes.oneOf(['vertical', 'horizontal']), - - /** - * Number of calendar months that are shown when calendar is opened - */ - number_of_months_shown: PropTypes.number, - - /** - * If True, calendar will open in a screen overlay portal, - * not supported on vertical calendar - */ - with_portal: PropTypes.bool, - - /** - * If True, calendar will open in a full screen overlay portal, will - * take precedent over 'withPortal' if both are set to true, - * not supported on vertical calendar - */ - with_full_screen_portal: PropTypes.bool, - - /** - * Size of rendered calendar days, higher number - * means bigger day size and larger calendar overall - */ - day_size: PropTypes.number, - - /** - * Determines whether the calendar and days operate - * from left to right or from right to left - */ - is_RTL: PropTypes.bool, - - /** - * If True, no dates can be selected. - */ - disabled: PropTypes.bool, - - /** - * The HTML element ID of the start date input field. - * Not used by Dash, only by CSS. - */ - start_date_id: PropTypes.string, - - /** - * The HTML element ID of the end date input field. - * Not used by Dash, only by CSS. - */ - end_date_id: PropTypes.string, - - /** - * CSS styles appended to wrapper div - */ - style: PropTypes.object, - - /** - * Appends a CSS class to the wrapper div component. - */ - className: PropTypes.string, - - /** - * The ID of this component, used to identify dash components - * in callbacks. The ID needs to be unique across all of the - * components in an app. - */ - id: PropTypes.string, - - /** - * Dash-assigned callback that gets fired when the value changes. - */ - setProps: PropTypes.func, - - /** - * Used to allow user interactions in this component to be persisted when - * the component - or the page - is refreshed. If `persisted` is truthy and - * hasn't changed from its previous value, any `persisted_props` that the - * user has changed while using the app will keep those changes, as long as - * the new prop value also matches what was given originally. - * Used in conjunction with `persistence_type` and `persisted_props`. - */ - persistence: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.number, - ]), - - /** - * Properties whose user interactions will persist after refreshing the - * component or the page. - */ - persisted_props: PropTypes.arrayOf( - PropTypes.oneOf(['start_date', 'end_date']) - ), - - /** - * Where persisted user changes will be stored: - * memory: only kept in memory, reset on page refresh. - * local: window.localStorage, data is kept after the browser quit. - * session: window.sessionStorage, data is cleared once the browser quit. - */ - persistence_type: PropTypes.oneOf(['local', 'session', 'memory']), -}; - -DatePickerRange.persistenceTransforms = { - end_date: transformDate, - start_date: transformDate, -}; - -DatePickerRange.defaultProps = { - calendar_orientation: 'horizontal', - is_RTL: false, - day_size: 39, - with_portal: false, - with_full_screen_portal: false, - first_day_of_week: 0, - number_of_months_shown: 1, - stay_open_on_select: false, - reopen_calendar_on_clear: false, - clearable: false, - disabled: false, - updatemode: 'singledate', - persisted_props: ['start_date', 'end_date'], - persistence_type: 'local', - disabled_days: [], -}; - -export const propTypes = DatePickerRange.propTypes; -export const defaultProps = DatePickerRange.defaultProps; diff --git a/components/dash-core-components/src/components/DatePickerRange.tsx b/components/dash-core-components/src/components/DatePickerRange.tsx new file mode 100644 index 0000000000..f1240f2daa --- /dev/null +++ b/components/dash-core-components/src/components/DatePickerRange.tsx @@ -0,0 +1,70 @@ +import React, {lazy, Suspense} from 'react'; +import datePickerRange from '../utils/LazyLoader/datePickerRange'; +import transformDate from '../utils/DatePickerPersistence'; +import {DatePickerRangeProps, PersistedProps, PersistenceTypes} from '../types'; + +const RealDatePickerRange = lazy(datePickerRange); + +/** + * DatePickerRange is a tailor made component designed for selecting + * timespan across multiple days off of a calendar. + * + * The DatePicker integrates well with the Python datetime module with the + * startDate and endDate being returned in a string format suitable for + * creating datetime objects. + * + */ +export default function DatePickerRange({ + calendar_orientation = 'horizontal', + is_RTL = false, + // eslint-disable-next-line no-magic-numbers + day_size = 34, + with_portal = false, + with_full_screen_portal = false, + first_day_of_week = 0, + number_of_months_shown = 2, + stay_open_on_select = false, + reopen_calendar_on_clear = false, + show_outside_days = false, + clearable = false, + disabled = false, + updatemode = 'singledate', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + persisted_props = [PersistedProps.start_date, PersistedProps.end_date], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + persistence_type = PersistenceTypes.local, + disabled_days = [], + ...props +}: DatePickerRangeProps) { + return ( + + + + ); +} + +DatePickerRange.dashPersistence = { + persisted_props: [PersistedProps.start_date, PersistedProps.end_date], + persistence_type: PersistenceTypes.local, +}; + +DatePickerRange.persistenceTransforms = { + end_date: transformDate, + start_date: transformDate, +}; diff --git a/components/dash-core-components/src/components/DatePickerSingle.react.js b/components/dash-core-components/src/components/DatePickerSingle.react.js deleted file mode 100644 index 1c35fa8505..0000000000 --- a/components/dash-core-components/src/components/DatePickerSingle.react.js +++ /dev/null @@ -1,237 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {Component, lazy, Suspense} from 'react'; -import datePickerSingle from '../utils/LazyLoader/datePickerSingle'; -import transformDate from '../utils/DatePickerPersistence'; - -const RealDateSingleRange = lazy(datePickerSingle); - -/** - * DatePickerSingle is a tailor made component designed for selecting - * a single day off of a calendar. - * - * The DatePicker integrates well with the Python datetime module with the - * startDate and endDate being returned in a string format suitable for - * creating datetime objects. - * - * This component is based off of Airbnb's react-dates react component - * which can be found here: https://github.com/airbnb/react-dates - */ -export default class DatePickerSingle extends Component { - render() { - return ( - - - - ); - } -} - -DatePickerSingle.propTypes = { - /** - * Specifies the starting date for the component, best practice is to pass - * value via datetime object - */ - date: PropTypes.string, - - /** - * Specifies the lowest selectable date for the component. - * Accepts datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - */ - min_date_allowed: PropTypes.string, - - /** - * Specifies the highest selectable date for the component. - * Accepts datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - */ - max_date_allowed: PropTypes.string, - - /** - * Specifies additional days between min_date_allowed and max_date_allowed - * that should be disabled. Accepted datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - */ - disabled_days: PropTypes.arrayOf(PropTypes.string), - - /** - * Text that will be displayed in the input - * box of the date picker when no date is selected. - * Default value is 'Start Date' - */ - placeholder: PropTypes.string, - - /** - * Specifies the month that is initially presented when the user - * opens the calendar. Accepts datetime.datetime objects or strings - * in the format 'YYYY-MM-DD' - * - */ - initial_visible_month: PropTypes.string, - - /** - * Whether or not the dropdown is "clearable", that is, whether or - * not a small "x" appears on the right of the dropdown that removes - * the selected value. - */ - clearable: PropTypes.bool, - - /** - * If True, the calendar will automatically open when cleared - */ - reopen_calendar_on_clear: PropTypes.bool, - - /** - * Specifies the format that the selected dates will be displayed - * valid formats are variations of "MM YY DD". For example: - * "MM YY DD" renders as '05 10 97' for May 10th 1997 - * "MMMM, YY" renders as 'May, 1997' for May 10th 1997 - * "M, D, YYYY" renders as '07, 10, 1997' for September 10th 1997 - * "MMMM" renders as 'May' for May 10 1997 - */ - display_format: PropTypes.string, - - /** - * Specifies the format that the month will be displayed in the calendar, - * valid formats are variations of "MM YY". For example: - * "MM YY" renders as '05 97' for May 1997 - * "MMMM, YYYY" renders as 'May, 1997' for May 1997 - * "MMM, YY" renders as 'Sep, 97' for September 1997 - */ - month_format: PropTypes.string, - - /** - * Specifies what day is the first day of the week, values must be - * from [0, ..., 6] with 0 denoting Sunday and 6 denoting Saturday - */ - first_day_of_week: PropTypes.oneOf([0, 1, 2, 3, 4, 5, 6]), - - /** - * If True the calendar will display days that rollover into - * the next month - */ - show_outside_days: PropTypes.bool, - - /** - * If True the calendar will not close when the user has selected a value - * and will wait until the user clicks off the calendar - */ - stay_open_on_select: PropTypes.bool, - - /** - * Orientation of calendar, either vertical or horizontal. - * Valid options are 'vertical' or 'horizontal'. - */ - calendar_orientation: PropTypes.oneOf(['vertical', 'horizontal']), - - /** - * Number of calendar months that are shown when calendar is opened - */ - number_of_months_shown: PropTypes.number, - - /** - * If True, calendar will open in a screen overlay portal, - * not supported on vertical calendar - */ - with_portal: PropTypes.bool, - - /** - * If True, calendar will open in a full screen overlay portal, will - * take precedent over 'withPortal' if both are set to True, - * not supported on vertical calendar - */ - with_full_screen_portal: PropTypes.bool, - - /** - * Size of rendered calendar days, higher number - * means bigger day size and larger calendar overall - */ - day_size: PropTypes.number, - - /** - * Determines whether the calendar and days operate - * from left to right or from right to left - */ - is_RTL: PropTypes.bool, - - /** - * If True, no dates can be selected. - */ - disabled: PropTypes.bool, - - /** - * CSS styles appended to wrapper div - */ - style: PropTypes.object, - - /** - * Appends a CSS class to the wrapper div component. - */ - className: PropTypes.string, - - /** - * The ID of this component, used to identify dash components - * in callbacks. The ID needs to be unique across all of the - * components in an app. - */ - id: PropTypes.string, - - /** - * Dash-assigned callback that gets fired when the value changes. - */ - setProps: PropTypes.func, - - /** - * Used to allow user interactions in this component to be persisted when - * the component - or the page - is refreshed. If `persisted` is truthy and - * hasn't changed from its previous value, a `date` that the user has - * changed while using the app will keep that change, as long as - * the new `date` also matches what was given originally. - * Used in conjunction with `persistence_type`. - */ - persistence: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.number, - ]), - - /** - * Properties whose user interactions will persist after refreshing the - * component or the page. Since only `date` is allowed this prop can - * normally be ignored. - */ - persisted_props: PropTypes.arrayOf(PropTypes.oneOf(['date'])), - - /** - * Where persisted user changes will be stored: - * memory: only kept in memory, reset on page refresh. - * local: window.localStorage, data is kept after the browser quit. - * session: window.sessionStorage, data is cleared once the browser quit. - */ - persistence_type: PropTypes.oneOf(['local', 'session', 'memory']), -}; - -DatePickerSingle.persistenceTransforms = { - date: transformDate, -}; - -DatePickerSingle.defaultProps = { - calendar_orientation: 'horizontal', - is_RTL: false, - day_size: 39, - with_portal: false, - with_full_screen_portal: false, - show_outside_days: true, - first_day_of_week: 0, - number_of_months_shown: 1, - stay_open_on_select: false, - reopen_calendar_on_clear: false, - clearable: false, - disabled: false, - persisted_props: ['date'], - persistence_type: 'local', - disabled_days: [], -}; - -export const propTypes = DatePickerSingle.propTypes; -export const defaultProps = DatePickerSingle.defaultProps; diff --git a/components/dash-core-components/src/components/DatePickerSingle.tsx b/components/dash-core-components/src/components/DatePickerSingle.tsx new file mode 100644 index 0000000000..388618d2c8 --- /dev/null +++ b/components/dash-core-components/src/components/DatePickerSingle.tsx @@ -0,0 +1,72 @@ +import React, {lazy, Suspense} from 'react'; +import datePickerSingle from '../utils/LazyLoader/datePickerSingle'; +import transformDate from '../utils/DatePickerPersistence'; +import { + DatePickerSingleProps, + PersistedProps, + PersistenceTypes, +} from '../types'; + +const RealDatePickerSingle = lazy(datePickerSingle); + +/** + * DatePickerSingle is a tailor made component designed for selecting + * a single day off of a calendar. + * + * The DatePicker integrates well with the Python datetime module with the + * startDate and endDate being returned in a string format suitable for + * creating datetime objects. + */ +export default function DatePickerSingle({ + placeholder = 'Select Date', + calendar_orientation = 'horizontal', + is_RTL = false, + // eslint-disable-next-line no-magic-numbers + day_size = 34, + with_portal = false, + with_full_screen_portal = false, + show_outside_days = true, + first_day_of_week = 0, + number_of_months_shown = 1, + stay_open_on_select = false, + reopen_calendar_on_clear = false, + clearable = false, + disabled = false, + disabled_days = [], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + persisted_props = [PersistedProps.date], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + persistence_type = PersistenceTypes.local, + ...props +}: DatePickerSingleProps) { + return ( + + + + ); +} + +DatePickerSingle.dashPersistence = { + persisted_props: [PersistedProps.date], + persistence_type: PersistenceTypes.local, +}; + +DatePickerSingle.persistenceTransforms = { + date: transformDate, +}; diff --git a/components/dash-core-components/src/components/Input.tsx b/components/dash-core-components/src/components/Input.tsx index 32ccdacd28..533a8e1a21 100644 --- a/components/dash-core-components/src/components/Input.tsx +++ b/components/dash-core-components/src/components/Input.tsx @@ -20,7 +20,7 @@ const convert = (val: unknown) => (isNumeric(val) ? +val : NaN); const isEquivalent = (v1: number, v2: number) => v1 === v2 || (isNaN(v1) && isNaN(v2)); -enum HTMLInputTypes { +export enum HTMLInputTypes { // Only allowing the input types with wide browser compatibility 'text' = 'text', 'number' = 'number', diff --git a/components/dash-core-components/src/components/css/calendar.css b/components/dash-core-components/src/components/css/calendar.css new file mode 100644 index 0000000000..bc100eeaf5 --- /dev/null +++ b/components/dash-core-components/src/components/css/calendar.css @@ -0,0 +1,64 @@ +.dash-datepicker-calendar { + padding: 0; + border-collapse: collapse; +} + +.dash-datepicker-calendar th > *, +.dash-datepicker-calendar td > * { + display: flex; + align-items: center; + justify-content: center; +} + +.dash-datepicker-calendar th, +.dash-datepicker-calendar td { + padding: 0; + font-weight: normal; + color: var(--Dash-Text-Weak); + text-align: center; + border-radius: 4px; + cursor: default; + user-select: none; + width: var(--day-size, 36px); + height: var(--day-size, 36px); +} + +.dash-datepicker-calendar td.dash-datepicker-calendar-date-inside { + color: var(--Dash-Text-Strong); +} + +.dash-datepicker-calendar td:hover { + cursor: pointer; + background-color: var(--Dash-Fill-Weak); +} + +.dash-datepicker-calendar td:focus { + outline: 2px solid var(--Dash-Fill-Interactive-Strong); + outline-offset: -2px; + z-index: 1; + position: relative; +} + +.dash-datepicker-calendar td.dash-datepicker-calendar-date-highlighted { + background-color: color-mix( + in srgb, + var(--Dash-Fill-Interactive-Strong) 5%, + transparent 95% + ); + color: var(--Dash-Fill-Interactive-Strong); +} + +.dash-datepicker-calendar td.dash-datepicker-calendar-date-selected { + background-color: var(--Dash-Fill-Interactive-Strong); + color: var(--Dash-Fill-Inverse-Strong); +} + +.dash-datepicker-calendar td.dash-datepicker-calendar-date-disabled { + opacity: 0.6; + cursor: not-allowed; + pointer-events: none; +} + +.dash-datepicker-calendar td input { + display: none; +} diff --git a/components/dash-core-components/src/components/css/datepickers.css b/components/dash-core-components/src/components/css/datepickers.css new file mode 100644 index 0000000000..f9a5761fa0 --- /dev/null +++ b/components/dash-core-components/src/components/css/datepickers.css @@ -0,0 +1,226 @@ +.dash-datepicker { + display: block; + flex: 1; + box-sizing: border-box; + margin: calc(var(--Dash-Spacing) * 2) 0; + padding: 0; + background: inherit; + border: none; + outline: none; + width: 100%; + font-size: inherit; + overflow: hidden; + accent-color: var(--Dash-Fill-Interactive-Strong); + outline-color: var(--Dash-Fill-Interactive-Strong); +} + +.dash-datepicker-input-wrapper { + display: grid; + grid-template-columns: auto 1fr; + justify-items: start; + align-items: center; + gap: calc(var(--Dash-Spacing) * 2); + box-sizing: border-box; + padding: 0 calc(var(--Dash-Spacing) * 2); +} + +.dash-datepicker-input-wrapper:has(:nth-child(3)) { + grid-template-columns: auto 1fr auto; +} + +.dash-datepicker-input-wrapper:has(:nth-child(4)) { + grid-template-columns: auto 1fr auto auto; +} + +.dash-datepicker-input-wrapper:has(:nth-child(5)) { + grid-template-columns: auto auto auto 1fr auto; +} + +.dash-datepicker-input-wrapper:has(:nth-child(6)) { + grid-template-columns: auto auto auto 1fr auto auto; +} + +.dash-datepicker-input-wrapper, +.dash-datepicker-content { + border-radius: var(--Dash-Spacing); + border: 1px solid var(--Dash-Stroke-Strong); + color: inherit; + text-align: left; +} + +.dash-datepicker-input { + height: 34px; + width: fit-content; + border: none; + outline: none; + border-radius: 4px; +} + +.dash-datepicker-input-wrapper:focus-within { + border: 1px solid var(--Dash-Fill-Interactive-Strong); +} + +.dash-datepicker-input-wrapper-disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.dash-datepicker-input-wrapper-disabled .dash-datepicker-input { + cursor: not-allowed; +} + +.dash-datepicker-input:focus { + outline: none; +} + +.dash-datepicker-input::placeholder { + color: var(--Dash-Text-Disabled); +} + +.dash-datepicker-trigger-button { + padding: var(--Dash-Spacing); + background: transparent; + border: none; + border-radius: 4px; + color: var(--Dash-Text-Strong); + cursor: pointer; +} + +.dash-datepicker-trigger-button:hover { + background: var(--Dash-Fill-Weak); +} + +.dash-datepicker-trigger-button:focus-visible { + outline: 2px solid var(--Dash-Fill-Interactive-Strong); + outline-offset: 2px; +} + +.dash-datepicker-trigger-button[aria-expanded='true'] { + background: var(--Dash-Fill-Weak); +} + +.dash-datepicker-trigger-icon { + width: 16px; + height: 16px; +} + +.dash-datepicker-caret-icon { + color: var(--Dash-Text-Strong); + fill: var(--Dash-Text-Strong); + white-space: nowrap; + justify-self: end; + transition: transform 0.15s; +} + +.dash-datepicker-input-wrapper[aria-expanded='true'] + .dash-datepicker-caret-icon { + transform: rotate(180deg); +} + +.dash-datepicker-clear { + background: none; + border: none; + cursor: pointer; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + justify-self: end; + color: var(--Dash-Text-Strong); + width: calc(var(--Dash-Spacing) * 3); + height: calc(var(--Dash-Spacing) * 3); +} + +.dash-datepicker-clear:hover { + color: var(--Dash-Fill-Interactive-Strong); +} + +.dash-datepicker-clear:focus { + outline: 2px solid var(--Dash-Fill-Interactive-Strong); + outline-offset: 1px; + border-radius: 2px; +} + +.dash-datepicker-content { + padding: 8px; + background: var(--Dash-Fill-Inverse-Strong); + width: fit-content; + max-width: 98vw; + overflow-y: auto; + z-index: 500; + box-shadow: 0px 10px 38px -10px var(--Dash-Shading-Strong), + 0px 10px 20px -15px var(--Dash-Shading-Weak); +} + +.dash-datepicker-calendar-wrapper { + display: flex; + flex-direction: column; + align-items: center; +} + +.dash-datepicker-calendar-container { + display: flex; + align-items: flex-start; + gap: calc(var(--Dash-Spacing) * 4); +} + +.dash-datepicker-controls { + display: flex; + align-items: center; + justify-content: center; + gap: var(--Dash-Spacing); + margin-bottom: calc(var(--Dash-Spacing) * 2); + font-size: 14px; +} + +.dash-datepicker-controls .dash-dropdown { + flex: 0 0 auto; + min-width: 122px; + width: auto; + margin: 0; +} + +.dash-datepicker-controls .dash-input { + flex-shrink: 0; + flex-grow: 0; + width: 102px; +} + +.dash-datepicker-controls .dash-input-stepper { + width: 20px; + height: 100%; +} + +.dash-datepicker-month-nav { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 32px; + padding: 0; + background: transparent; + border: none; + border-radius: 4px; + color: var(--Dash-Fill-Interactive-Strong); + cursor: pointer; + flex-shrink: 0; +} + +.dash-datepicker-month-nav:hover:not(:disabled) { + background: var(--Dash-Fill-Weak); +} + +.dash-datepicker-month-nav:focus-visible { + outline: 2px solid var(--Dash-Fill-Interactive-Strong); + outline-offset: 2px; +} + +.dash-datepicker-month-nav:disabled { + color: var(--Dash-Text-Disabled); + cursor: not-allowed; +} + +.dash-datepicker-month-nav svg { + width: 20px; + height: 20px; +} diff --git a/components/dash-core-components/src/components/css/dcc.css b/components/dash-core-components/src/components/css/dcc.css index 66579edab0..9eb28ed724 100644 --- a/components/dash-core-components/src/components/css/dcc.css +++ b/components/dash-core-components/src/components/css/dcc.css @@ -7,6 +7,7 @@ --Dash-Fill-Inverse-Strong: #fff; --Dash-Text-Primary: rgba(0, 18, 77, 0.87); --Dash-Text-Strong: rgba(0, 9, 38, 0.9); + --Dash-Text-Weak: rgba(0, 12, 51, 0.65); --Dash-Text-Disabled: rgba(0, 21, 89, 0.3); --Dash-Fill-Primary-Hover: rgba(0, 18, 77, 0.04); --Dash-Fill-Primary-Active: rgba(0, 18, 77, 0.08); diff --git a/components/dash-core-components/src/components/css/dropdown.css b/components/dash-core-components/src/components/css/dropdown.css index 7746c49a75..6ca9984c6a 100644 --- a/components/dash-core-components/src/components/css/dropdown.css +++ b/components/dash-core-components/src/components/css/dropdown.css @@ -2,7 +2,6 @@ display: block; flex: 1; box-sizing: border-box; - margin: calc(var(--Dash-Spacing) * 2) 0; padding: 0; background: inherit; border: none; diff --git a/components/dash-core-components/src/components/css/input.css b/components/dash-core-components/src/components/css/input.css index 6ca5d898fe..9de267a4ca 100644 --- a/components/dash-core-components/src/components/css/input.css +++ b/components/dash-core-components/src/components/css/input.css @@ -4,7 +4,7 @@ display: inline-flex; align-items: center; width: 170px; /* default input width */ - height: 32px; + height: 34px; background: var(--Dash-Fill-Inverse-Strong); border-radius: var(--Dash-Spacing); border: 1px solid var(--Dash-Stroke-Strong); diff --git a/components/dash-core-components/src/fragments/DatePickerRange.react.js b/components/dash-core-components/src/fragments/DatePickerRange.react.js deleted file mode 100644 index fe8fab4cc8..0000000000 --- a/components/dash-core-components/src/fragments/DatePickerRange.react.js +++ /dev/null @@ -1,221 +0,0 @@ -import 'react-dates/initialize'; -import {DateRangePicker} from 'react-dates'; -import React, {Component} from 'react'; -import uniqid from 'uniqid'; - -import {propTypes} from '../components/DatePickerRange.react'; -import convertToMoment from '../utils/convertToMoment'; -import LoadingElement from '../utils/LoadingElement'; - -export default class DatePickerRange extends Component { - constructor(props) { - super(props); - this.propsToState = this.propsToState.bind(this); - this.onDatesChange = this.onDatesChange.bind(this); - this.isOutsideRange = this.isOutsideRange.bind(this); - this.state = { - focused: false, - start_date_id: props.start_date_id || uniqid(), - end_date_id: props.end_date_id || uniqid(), - }; - } - - propsToState(newProps, force = false) { - const state = {}; - - if (force || newProps.start_date !== this.props.start_date) { - state.start_date = newProps.start_date; - } - - if (force || newProps.end_date !== this.props.end_date) { - state.end_date = newProps.end_date; - } - - if ( - force || - newProps.max_date_allowed !== this.props.max_date_allowed - ) { - state.max_date_allowed = convertToMoment(newProps, [ - 'max_date_allowed', - ]).max_date_allowed; - } - - if ( - force || - newProps.min_date_allowed !== this.props.min_date_allowed - ) { - state.min_date_allowed = convertToMoment(newProps, [ - 'min_date_allowed', - ]).min_date_allowed; - } - - if (force || newProps.disabled_days !== this.props.disabled_days) { - state.disabled_days = convertToMoment(newProps, [ - 'disabled_days', - ]).disabled_days; - } - - if (Object.keys(state).length) { - this.setState(state); - } - } - - UNSAFE_componentWillReceiveProps(newProps) { - this.propsToState(newProps); - } - - UNSAFE_componentWillMount() { - this.propsToState(this.props, true); - } - - onDatesChange({startDate: start_date, endDate: end_date}) { - const {setProps, updatemode, clearable} = this.props; - - const oldMomentDates = convertToMoment(this.state, [ - 'start_date', - 'end_date', - ]); - - if (start_date && !start_date.isSame(oldMomentDates.start_date)) { - if (updatemode === 'singledate') { - setProps({start_date: start_date.format('YYYY-MM-DD')}); - } else { - this.setState({start_date: start_date.format('YYYY-MM-DD')}); - } - } - - if (end_date && !end_date.isSame(oldMomentDates.end_date)) { - if (updatemode === 'singledate') { - setProps({end_date: end_date.format('YYYY-MM-DD')}); - } else if (updatemode === 'bothdates') { - setProps({ - start_date: this.state.start_date, - end_date: end_date.format('YYYY-MM-DD'), - }); - } - } - - if ( - clearable && - !start_date && - !end_date && - (oldMomentDates.start_date !== start_date || - oldMomentDates.end_date !== end_date) - ) { - setProps({start_date: null, end_date: null}); - } - } - - isOutsideRange(date) { - return ( - (this.state.min_date_allowed && - date.isBefore(this.state.min_date_allowed)) || - (this.state.max_date_allowed && - date.isAfter(this.state.max_date_allowed)) || - (this.state.disabled_days && - this.state.disabled_days.some(d => date.isSame(d, 'day'))) - ); - } - - render() { - const {focusedInput} = this.state; - - const { - calendar_orientation, - clearable, - day_size, - disabled, - display_format, - end_date_placeholder_text, - first_day_of_week, - is_RTL, - minimum_nights, - month_format, - number_of_months_shown, - reopen_calendar_on_clear, - show_outside_days, - start_date_placeholder_text, - stay_open_on_select, - with_full_screen_portal, - with_portal, - id, - style, - className, - start_date_id, - end_date_id, - } = this.props; - - const {initial_visible_month} = convertToMoment(this.props, [ - 'initial_visible_month', - ]); - - const {start_date, end_date} = convertToMoment(this.state, [ - 'start_date', - 'end_date', - ]); - const verticalFlag = calendar_orientation !== 'vertical'; - - const DatePickerWrapperStyles = { - position: 'relative', - display: 'inline-block', - ...style, - }; - - // the height in px of the top part of the calendar (that holds - // the name of the month) - const baselineHeight = 145; - - return ( - - { - if (initial_visible_month) { - return initial_visible_month; - } else if (end_date && focusedInput === 'endDate') { - return end_date; - } else if (start_date && focusedInput === 'startDate') { - return start_date; - } - return start_date; - }} - isOutsideRange={this.isOutsideRange} - isRTL={is_RTL} - keepOpenOnDateSelect={stay_open_on_select} - minimumNights={minimum_nights} - monthFormat={month_format} - numberOfMonths={number_of_months_shown} - onDatesChange={this.onDatesChange} - onFocusChange={focusedInput => - this.setState({focusedInput}) - } - orientation={calendar_orientation} - reopenPickerOnClearDates={reopen_calendar_on_clear} - showClearDates={clearable} - startDate={start_date} - startDatePlaceholderText={start_date_placeholder_text} - withFullScreenPortal={ - with_full_screen_portal && verticalFlag - } - withPortal={with_portal && verticalFlag} - startDateId={start_date_id || this.state.start_date_id} - endDateId={end_date_id || this.state.end_date_id} - verticalHeight={baselineHeight + day_size * 6 + 'px'} - /> - - ); - } -} - -DatePickerRange.propTypes = propTypes; diff --git a/components/dash-core-components/src/fragments/DatePickerRange.tsx b/components/dash-core-components/src/fragments/DatePickerRange.tsx new file mode 100644 index 0000000000..2e0e5109ab --- /dev/null +++ b/components/dash-core-components/src/fragments/DatePickerRange.tsx @@ -0,0 +1,431 @@ +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import * as Popover from '@radix-ui/react-popover'; +import { + CalendarIcon, + CaretDownIcon, + Cross1Icon, + ArrowLeftIcon, + ArrowRightIcon, +} from '@radix-ui/react-icons'; +import AutosizeInput from 'react-input-autosize'; +import Calendar, {CalendarHandle} from '../utils/calendar/Calendar'; +import {DatePickerRangeProps, CalendarDirection} from '../types'; +import { + dateAsStr, + strAsDate, + formatDate, + isDateDisabled, + isSameDay, +} from '../utils/calendar/helpers'; +import '../components/css/datepickers.css'; +import uuid from 'uniqid'; +import moment from 'moment'; + +const DatePickerRange = ({ + id, + className, + start_date, + end_date, + min_date_allowed, + max_date_allowed, + initial_visible_month = start_date ?? min_date_allowed ?? max_date_allowed, + disabled_days, + minimum_nights, + first_day_of_week, + show_outside_days, + clearable, + reopen_calendar_on_clear, + disabled, + display_format, + month_format = 'MMMM YYYY', + stay_open_on_select, + is_RTL = false, + setProps, + style, + // eslint-disable-next-line no-magic-numbers + day_size = 34, + number_of_months_shown = 1, + calendar_orientation, + updatemode, + start_date_id, + end_date_id, + start_date_placeholder_text = 'Start Date', + end_date_placeholder_text = 'End Date', +}: DatePickerRangeProps) => { + const [internalStartDate, setInternalStartDate] = useState( + strAsDate(start_date) + ); + const [internalEndDate, setInternalEndDate] = useState(strAsDate(end_date)); + const direction = is_RTL + ? CalendarDirection.RightToLeft + : CalendarDirection.LeftToRight; + const initialCalendarDate = + strAsDate(initial_visible_month) || + internalStartDate || + internalEndDate; + + const minDate = strAsDate(min_date_allowed); + const maxDate = strAsDate(max_date_allowed); + const disabledDates = useMemo(() => { + const baseDates = + disabled_days + ?.map(d => strAsDate(d)) + .filter((d): d is Date => d !== undefined) || []; + + // Add minimum_nights constraint: disable dates within the minimum nights range + if ( + internalStartDate && + minimum_nights && + minimum_nights > 0 && + !internalEndDate + ) { + const minimumNightsDates: Date[] = []; + for (let i = 1; i < minimum_nights; i++) { + minimumNightsDates.push( + moment(internalStartDate).add(i, 'day').toDate() + ); + minimumNightsDates.push( + moment(internalStartDate).subtract(i, 'day').toDate() + ); + } + return [...baseDates, ...minimumNightsDates]; + } + + return baseDates; + }, [disabled_days, internalStartDate, internalEndDate, minimum_nights]); + + const [isCalendarOpen, setIsCalendarOpen] = useState(false); + const [startInputValue, setStartInputValue] = useState( + formatDate(internalStartDate, display_format) + ); + const [endInputValue, setEndInputValue] = useState( + formatDate(internalEndDate, display_format) + ); + + const containerRef = useRef(null); + const startInputRef = useRef(null); + const endInputRef = useRef(null); + const calendarRef = useRef(null); + + useEffect(() => { + setInternalStartDate(strAsDate(start_date)); + }, [start_date]); + + useEffect(() => { + setInternalEndDate(strAsDate(end_date)); + }, [end_date]); + + useEffect(() => { + setStartInputValue(formatDate(internalStartDate, display_format)); + }, [internalStartDate, display_format]); + + useEffect(() => { + setEndInputValue(formatDate(internalEndDate, display_format)); + }, [internalEndDate, display_format]); + + useEffect(() => { + // Controls whether or not to call `setProps` + const startChanged = !isSameDay(start_date, internalStartDate); + const endChanged = !isSameDay(end_date, internalEndDate); + + const newDates: Partial = { + ...(startChanged && {start_date: dateAsStr(internalStartDate)}), + ...(endChanged && {end_date: dateAsStr(internalEndDate)}), + }; + + const numPropsRequiredForUpdate = updatemode === 'bothdates' ? 2 : 1; + if (Object.keys(newDates).length >= numPropsRequiredForUpdate) { + setProps(newDates); + } + }, [start_date, internalStartDate, end_date, internalEndDate, updatemode]); + + useEffect(() => { + // Keeps focus on the component when the calendar closes + if (!isCalendarOpen) { + if (!startInputValue) { + startInputRef.current?.focus(); + } else { + endInputRef.current?.focus(); + } + } + }, [isCalendarOpen, startInputValue]); + + const sendStartInputAsDate = useCallback( + (focusCalendar = false) => { + if (startInputValue) { + setInternalStartDate(undefined); + } + const parsed = strAsDate(startInputValue, display_format); + const isValid = + parsed && + !isDateDisabled(parsed, minDate, maxDate, disabledDates); + + if (isValid) { + setInternalStartDate(parsed); + if (focusCalendar) { + calendarRef.current?.focusDate(parsed); + } else { + calendarRef.current?.setVisibleDate(parsed); + } + } else { + // Invalid or disabled input: revert to previous valid date with proper formatting + const previousDate = strAsDate(start_date); + setStartInputValue( + previousDate ? formatDate(previousDate, display_format) : '' + ); + if (focusCalendar) { + calendarRef.current?.focusDate(previousDate); + } + } + }, + [ + startInputValue, + display_format, + start_date, + minDate, + maxDate, + disabledDates, + ] + ); + + const sendEndInputAsDate = useCallback( + (focusCalendar = false) => { + if (endInputValue === '') { + setInternalEndDate(undefined); + } + const parsed = strAsDate(endInputValue, display_format); + const isValid = + parsed && + !isDateDisabled(parsed, minDate, maxDate, disabledDates); + + if (isValid) { + setInternalEndDate(parsed); + if (focusCalendar) { + calendarRef.current?.focusDate(parsed); + } else { + calendarRef.current?.setVisibleDate(parsed); + } + } else { + // Invalid or disabled input: revert to previous valid date with proper formatting + const previousDate = strAsDate(end_date); + setEndInputValue( + previousDate ? formatDate(previousDate, display_format) : '' + ); + if (focusCalendar) { + calendarRef.current?.focusDate(previousDate); + } + } + }, + [ + endInputValue, + display_format, + end_date, + minDate, + maxDate, + disabledDates, + ] + ); + + const clearSelection = useCallback( + e => { + setInternalStartDate(undefined); + setInternalEndDate(undefined); + startInputRef.current?.focus(); + e.preventDefault(); + e.stopPropagation(); + if (reopen_calendar_on_clear) { + setIsCalendarOpen(true); + } + }, + [reopen_calendar_on_clear] + ); + + const handleStartInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (['ArrowUp', 'ArrowDown'].includes(e.key)) { + e.preventDefault(); + sendStartInputAsDate(true); + if (!isCalendarOpen) { + // open the calendar after resolving prop changes, so that + // it opens with the correct date showing + setTimeout(() => setIsCalendarOpen(true), 0); + } + } else if (['Enter', 'Tab'].includes(e.key)) { + sendStartInputAsDate(); + } + }, + [isCalendarOpen, sendStartInputAsDate] + ); + + const handleEndInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (['ArrowUp', 'ArrowDown'].includes(e.key)) { + e.preventDefault(); + sendEndInputAsDate(true); + if (!isCalendarOpen) { + // open the calendar after resolving prop changes, so that + // it opens with the correct date showing + setTimeout(() => setIsCalendarOpen(true), 0); + } + } else if (['Enter', 'Tab'].includes(e.key)) { + sendEndInputAsDate(); + } + }, + [isCalendarOpen, sendEndInputAsDate] + ); + + const accessibleId = id ?? uuid(); + let classNames = 'dash-datepicker-input-wrapper'; + if (disabled) { + classNames += ' dash-datepicker-input-wrapper-disabled'; + } + if (className) { + classNames += ' ' + className; + } + + const ArrowIcon = + direction === CalendarDirection.LeftToRight + ? ArrowRightIcon + : ArrowLeftIcon; + + const handleSelectionChange = useCallback( + (start?: Date, end?: Date) => { + const isNewSelection = + isSameDay(start, end) && + ((!internalStartDate && !internalEndDate) || + (internalStartDate && internalEndDate)); + + if (isNewSelection) { + setInternalStartDate(start); + setInternalEndDate(undefined); + } else { + // Normalize dates: ensure start <= end + if (start && end && start > end) { + setInternalStartDate(end); + setInternalEndDate(start); + } else { + setInternalStartDate(start); + setInternalEndDate(end); + } + + if (end && !stay_open_on_select) { + setIsCalendarOpen(false); + } + } + }, + [internalStartDate, internalEndDate, stay_open_on_select] + ); + + return ( +
+ + +
{ + e.preventDefault(); + if (!isCalendarOpen && !disabled) { + setIsCalendarOpen(true); + } + }} + > + + { + startInputRef.current = node; + }} + type="text" + id={start_date_id || accessibleId} + inputClassName="dash-datepicker-input dash-datepicker-start-date" + value={startInputValue} + onChange={e => setStartInputValue(e.target.value)} + onKeyDown={handleStartInputKeyDown} + onFocus={() => { + if (internalStartDate) { + calendarRef.current?.setVisibleDate( + internalStartDate + ); + } + }} + placeholder={start_date_placeholder_text} + disabled={disabled} + dir={direction} + aria-label={start_date_placeholder_text} + /> + + { + endInputRef.current = node; + }} + type="text" + id={end_date_id || accessibleId + '-end-date'} + inputClassName="dash-datepicker-input dash-datepicker-end-date" + value={endInputValue} + onChange={e => setEndInputValue(e.target.value)} + onKeyDown={handleEndInputKeyDown} + onFocus={() => { + if (internalEndDate) { + calendarRef.current?.setVisibleDate( + internalEndDate + ); + } + }} + placeholder={end_date_placeholder_text} + disabled={disabled} + dir={direction} + aria-label={end_date_placeholder_text} + /> + {clearable && !disabled && ( + + + + )} + +
+
+ + + e.preventDefault()} + > + + + +
+
+ ); +}; + +export default DatePickerRange; diff --git a/components/dash-core-components/src/fragments/DatePickerSingle.react.js b/components/dash-core-components/src/fragments/DatePickerSingle.react.js deleted file mode 100644 index 8cbe792588..0000000000 --- a/components/dash-core-components/src/fragments/DatePickerSingle.react.js +++ /dev/null @@ -1,157 +0,0 @@ -import 'react-dates/initialize'; - -import {SingleDatePicker} from 'react-dates'; -import moment from 'moment'; -import React, {Component} from 'react'; - -import {propTypes} from '../components/DatePickerSingle.react'; -import convertToMoment from '../utils/convertToMoment'; -import LoadingElement from '../utils/LoadingElement'; - -export default class DatePickerSingle extends Component { - constructor() { - super(); - this.propsToState = this.propsToState.bind(this); - this.isOutsideRange = this.isOutsideRange.bind(this); - this.onDateChange = this.onDateChange.bind(this); - this.state = {focused: false}; - } - - propsToState(newProps, force = false) { - const state = {}; - - if ( - force || - newProps.max_date_allowed !== this.props.max_date_allowed - ) { - state.max_date_allowed = convertToMoment(newProps, [ - 'max_date_allowed', - ]).max_date_allowed; - } - - if ( - force || - newProps.min_date_allowed !== this.props.min_date_allowed - ) { - state.min_date_allowed = convertToMoment(newProps, [ - 'min_date_allowed', - ]).min_date_allowed; - } - - if (force || newProps.disabled_days !== this.props.disabled_days) { - state.disabled_days = convertToMoment(newProps, [ - 'disabled_days', - ]).disabled_days; - } - - if (Object.keys(state).length) { - this.setState(state); - } - } - - UNSAFE_componentWillReceiveProps(newProps) { - this.propsToState(newProps); - } - - UNSAFE_componentWillMount() { - this.propsToState(this.props, true); - } - - isOutsideRange(date) { - return ( - (this.state.min_date_allowed && - date.isBefore(this.state.min_date_allowed)) || - (this.state.max_date_allowed && - date.isAfter(this.state.max_date_allowed)) || - (this.state.disabled_days && - this.state.disabled_days.some(d => date.isSame(d, 'day'))) - ); - } - - onDateChange(date) { - const {setProps} = this.props; - const payload = {date: date ? date.format('YYYY-MM-DD') : null}; - setProps(payload); - } - - render() { - const {focused} = this.state; - - const { - calendar_orientation, - clearable, - day_size, - disabled, - display_format, - first_day_of_week, - is_RTL, - month_format, - number_of_months_shown, - placeholder, - reopen_calendar_on_clear, - show_outside_days, - stay_open_on_select, - with_full_screen_portal, - with_portal, - id, - style, - className, - } = this.props; - - const {date, initial_visible_month} = convertToMoment(this.props, [ - 'date', - 'initial_visible_month', - ]); - - const verticalFlag = calendar_orientation !== 'vertical'; - - const DatePickerWrapperStyles = { - position: 'relative', - display: 'inline-block', - ...style, - }; - - // the height in px of the top part of the calendar (that holds - // the name of the month) - const baselineHeight = 145; - - return ( - - this.setState({focused})} - initialVisibleMonth={() => - date || initial_visible_month || moment() - } - isOutsideRange={this.isOutsideRange} - numberOfMonths={number_of_months_shown} - withPortal={with_portal && verticalFlag} - withFullScreenPortal={ - with_full_screen_portal && verticalFlag - } - firstDayOfWeek={first_day_of_week} - enableOutsideDays={show_outside_days} - monthFormat={month_format} - displayFormat={display_format} - placeholder={placeholder} - showClearDate={clearable} - disabled={disabled} - keepOpenOnDateSelect={stay_open_on_select} - reopenPickerOnClearDate={reopen_calendar_on_clear} - isRTL={is_RTL} - orientation={calendar_orientation} - daySize={day_size} - verticalHeight={baselineHeight + day_size * 6 + 'px'} - /> - - ); - } -} - -DatePickerSingle.propTypes = propTypes; diff --git a/components/dash-core-components/src/fragments/DatePickerSingle.tsx b/components/dash-core-components/src/fragments/DatePickerSingle.tsx new file mode 100644 index 0000000000..a3b7c1f7d3 --- /dev/null +++ b/components/dash-core-components/src/fragments/DatePickerSingle.tsx @@ -0,0 +1,243 @@ +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import * as Popover from '@radix-ui/react-popover'; +import {CalendarIcon, CaretDownIcon, Cross1Icon} from '@radix-ui/react-icons'; +import Calendar, {CalendarHandle} from '../utils/calendar/Calendar'; +import {DatePickerSingleProps, CalendarDirection} from '../types'; +import { + dateAsStr, + strAsDate, + formatDate, + isDateDisabled, + isSameDay, +} from '../utils/calendar/helpers'; +import '../components/css/datepickers.css'; +import uuid from 'uniqid'; +import AutosizeInput from 'react-input-autosize'; + +const DatePickerSingle = ({ + id, + className, + date, + min_date_allowed, + max_date_allowed, + initial_visible_month = date ?? min_date_allowed, + disabled_days, + first_day_of_week, + show_outside_days, + placeholder = 'Select Date', + clearable, + reopen_calendar_on_clear, + disabled, + display_format, + month_format = 'MMMM YYYY', + stay_open_on_select, + is_RTL = false, + setProps, + style, + // eslint-disable-next-line no-magic-numbers + day_size = 34, + number_of_months_shown = 1, + calendar_orientation, +}: DatePickerSingleProps) => { + const [internalDate, setInternalDate] = useState(strAsDate(date)); + const direction = is_RTL + ? CalendarDirection.RightToLeft + : CalendarDirection.LeftToRight; + const initialMonth = strAsDate(initial_visible_month) || internalDate; + const minDate = strAsDate(min_date_allowed); + const maxDate = strAsDate(max_date_allowed); + const disabledDates = useMemo(() => { + return disabled_days + ?.map(d => strAsDate(d)) + .filter((d): d is Date => d !== undefined); + }, [disabled_days]); + + const [isCalendarOpen, setIsCalendarOpen] = useState(false); + const [inputValue, setInputValue] = useState( + (internalDate && formatDate(internalDate, display_format)) ?? '' + ); + + const containerRef = useRef(null); + const inputRef = useRef(null); + const calendarRef = useRef(null); + + useEffect(() => { + setInternalDate(strAsDate(date)); + }, [date]); + + useEffect(() => { + setInputValue(formatDate(internalDate, display_format)); + }, [internalDate, display_format]); + + useEffect(() => { + const dateChanged = !(date && isSameDay(date, internalDate)); + + if (dateChanged) { + setProps({date: dateAsStr(internalDate)}); + } + }, [date, internalDate, setProps]); + + useEffect(() => inputRef.current?.focus(), [isCalendarOpen]); + + const parseUserInput = useCallback( + (focusCalendar = false) => { + if (inputValue === '') { + setInternalDate(undefined); + } + const parsed = strAsDate(inputValue, display_format); + const isValid = + parsed && + !isDateDisabled(parsed, minDate, maxDate, disabledDates); + + if (isValid) { + setInternalDate(parsed); + if (focusCalendar) { + calendarRef.current?.focusDate(parsed); + } else { + calendarRef.current?.setVisibleDate(parsed); + } + } else { + // Invalid or disabled input: revert to previous valid date with proper formatting + const previousDate = strAsDate(date); + setInputValue( + previousDate ? formatDate(previousDate, display_format) : '' + ); + if (focusCalendar) { + calendarRef.current?.focusDate(previousDate); + } + } + }, + [inputValue, display_format, date, minDate, maxDate, disabledDates] + ); + + const clearSelection = useCallback( + (e: React.MouseEvent) => { + setInternalDate(undefined); + inputRef.current?.focus(); + e.preventDefault(); + e.stopPropagation(); + if (reopen_calendar_on_clear) { + setIsCalendarOpen(true); + } + }, + [reopen_calendar_on_clear] + ); + + const handleInputKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (['ArrowUp', 'ArrowDown'].includes(e.key)) { + e.preventDefault(); + parseUserInput(true); + if (!isCalendarOpen) { + // open the calendar after resolving prop changes, so that + // it opens with the correct date showing + setTimeout(() => setIsCalendarOpen(true), 0); + } + } else if (['Enter', 'Tab'].includes(e.key)) { + parseUserInput(); + } + }, + [isCalendarOpen, parseUserInput] + ); + + const accessibleId = id ?? uuid(); + let classNames = 'dash-datepicker-input-wrapper'; + if (disabled) { + classNames += ' dash-datepicker-input-wrapper-disabled'; + } + if (className) { + classNames += ' ' + className; + } + + return ( +
+ + +
{ + e.preventDefault(); + if (!isCalendarOpen && !disabled) { + setIsCalendarOpen(true); + } + }} + > + + { + inputRef.current = node; + }} + type="text" + id={accessibleId} + inputClassName="dash-datepicker-input dash-datepicker-end-date" + value={inputValue} + onChange={e => setInputValue(e.target.value)} + onKeyDown={handleInputKeyDown} + placeholder={placeholder} + disabled={disabled} + dir={direction} + aria-label={placeholder} + /> + {clearable && !disabled && !!date && ( + + + + )} + + +
+
+ + + e.preventDefault()} + > + { + if (!selection) { + return; + } + setInternalDate(selection); + if (!stay_open_on_select) { + setIsCalendarOpen(false); + } + }} + /> + + +
+
+ ); +}; + +export default DatePickerSingle; diff --git a/components/dash-core-components/src/index.ts b/components/dash-core-components/src/index.ts index e95ae69301..80c5c6f0ec 100644 --- a/components/dash-core-components/src/index.ts +++ b/components/dash-core-components/src/index.ts @@ -3,8 +3,8 @@ import Checklist from './components/Checklist'; import Clipboard from './components/Clipboard.react'; import ConfirmDialog from './components/ConfirmDialog.react'; import ConfirmDialogProvider from './components/ConfirmDialogProvider.react'; -import DatePickerRange from './components/DatePickerRange.react'; -import DatePickerSingle from './components/DatePickerSingle.react'; +import DatePickerRange from './components/DatePickerRange'; +import DatePickerSingle from './components/DatePickerSingle'; import Download from './components/Download.react'; import Dropdown from './components/Dropdown'; import Geolocation from './components/Geolocation.react'; @@ -26,8 +26,6 @@ import Tooltip from './components/Tooltip'; import Upload from './components/Upload.react'; import './components/css/dcc.css'; -import 'react-dates/lib/css/_datepicker.css'; -import './components/css/react-dates@20.1.0-fix.css'; export { Checklist, diff --git a/components/dash-core-components/src/types.ts b/components/dash-core-components/src/types.ts index 7efa7cafcf..64c1ee6ef6 100644 --- a/components/dash-core-components/src/types.ts +++ b/components/dash-core-components/src/types.ts @@ -9,9 +9,13 @@ export enum PersistenceTypes { export enum PersistedProps { 'value' = 'value', + 'date' = 'date', + 'start_date' = 'start_date', + 'end_date' = 'end_date', } -export interface BaseDccProps extends BaseDashProps { +export interface BaseDccProps + extends Pick { /** * Additional CSS class for the root DOM node */ @@ -880,7 +884,7 @@ export interface TabsProps extends BaseDccProps { }; } -// Note a quirk in how this extends the BaseComponentProps: `setProps` is shared +// Note a quirk in how this extends the BaseDccProps: `setProps` is shared // with `TabsProps` (plural!) due to how tabs are implemented. This is // intentional. export interface TabProps extends BaseDccProps { @@ -945,3 +949,216 @@ export interface TabProps extends BaseDccProps { */ width?: string | number; } + +export enum DayOfWeek { + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, +} + +export enum CalendarDirection { + LeftToRight = 'ltr', + RightToLeft = 'rtl', +} + +export interface DatePickerSingleProps + extends BaseDccProps { + /** + * Specifies the starting date for the component, best practice is to pass + * value via datetime object + */ + date?: `${string}-${string}-${string}`; + + /** + * Specifies the lowest selectable date for the component. + * Accepts datetime.datetime objects or strings + * in the format 'YYYY-MM-DD' + */ + min_date_allowed?: string; + + /** + * Specifies the highest selectable date for the component. + * Accepts datetime.datetime objects or strings + * in the format 'YYYY-MM-DD' + */ + max_date_allowed?: string; + + /** + * Specifies additional days between min_date_allowed and max_date_allowed + * that should be disabled. Accepted datetime.datetime objects or strings + * in the format 'YYYY-MM-DD' + */ + disabled_days?: string[]; + + /** + * Text that will be displayed in the input + * box of the date picker when no date is selected. + */ + placeholder?: string; + + /** + * Specifies the month that is initially presented when the user + * opens the calendar. Accepts datetime.datetime objects or strings + * in the format 'YYYY-MM-DD' + * + */ + initial_visible_month?: string; + + /** + * Whether or not the dropdown is "clearable", that is, whether or + * not a small "x" appears on the right of the dropdown that removes + * the selected value. + */ + clearable?: boolean; + + /** + * If True, the calendar will automatically open when cleared + */ + reopen_calendar_on_clear?: boolean; + + /** + * Specifies the format that the selected dates will be displayed + * valid formats are variations of "MM YY DD". For example: + * "MM YY DD" renders as '05 10 97' for May 10th 1997 + * "MMMM, YY" renders as 'May, 1997' for May 10th 1997 + * "M, D, YYYY" renders as '07, 10, 1997' for September 10th 1997 + * "MMMM" renders as 'May' for May 10 1997 + */ + display_format?: string; + + /** + * Specifies the format that the month will be displayed in the calendar, + * valid formats are variations of "MM YY". For example: + * "MM YY" renders as '05 97' for May 1997 + * "MMMM, YYYY" renders as 'May, 1997' for May 1997 + * "MMM, YY" renders as 'Sep, 97' for September 1997 + */ + month_format?: string; + + /** + * Specifies what day is the first day of the week, values must be + * from [0, ..., 6] with 0 denoting Sunday and 6 denoting Saturday + */ + first_day_of_week?: DayOfWeek; + + /** + * If True the calendar will display days that rollover into + * the next month + */ + show_outside_days?: boolean; + + /** + * If True the calendar will not close when the user has selected a value + * and will wait until the user clicks off the calendar + */ + stay_open_on_select?: boolean; + + /** + * Orientation of calendar, either vertical or horizontal. + * Valid options are 'vertical' or 'horizontal'. + */ + calendar_orientation?: 'vertical' | 'horizontal'; + + /** + * Number of calendar months that are shown when calendar is opened + */ + number_of_months_shown?: number; + + /** + * If True, calendar will open in a screen overlay portal, + * not supported on vertical calendar + */ + with_portal?: boolean; + + /** + * If True, calendar will open in a full screen overlay portal, will + * take precedent over 'withPortal' if both are set to True, + * not supported on vertical calendar + */ + with_full_screen_portal?: boolean; + + /** + * Size of rendered calendar days, higher number + * means bigger day size and larger calendar overall + */ + day_size?: number; + + /** + * Determines whether the calendar and days operate + * from left to right or from right to left + */ + is_RTL?: boolean; + + /** + * If True, no dates can be selected. + */ + disabled?: boolean; + + /** + * CSS styles appended to wrapper div + */ + style?: React.CSSProperties; +} + +export interface DatePickerRangeProps + extends Omit { + /** + * Specifies the starting date for the component. + * Accepts datetime.datetime objects or strings + * in the format 'YYYY-MM-DD' + */ + start_date?: `${string}-${string}-${string}`; + + /** + * Specifies the ending date for the component. + * Accepts datetime.datetime objects or strings + * in the format 'YYYY-MM-DD' + */ + end_date?: `${string}-${string}-${string}`; + + /** + * Specifies a minimum number of nights that must be selected between + * the startDate and the endDate + */ + minimum_nights?: number; + + /** + * Determines when the component should update + * its value. If `bothdates`, then the DatePicker + * will only trigger its value when the user has + * finished picking both dates. If `singledate`, then + * the DatePicker will update its value + * as one date is picked. + */ + updatemode?: 'singledate' | 'bothdates'; + + /** + * Text that will be displayed in the first input + * box of the date picker when no date is selected. Default value is 'Start Date' + */ + start_date_placeholder_text?: string; + + /** + * Text that will be displayed in the second input + * box of the date picker when no date is selected. Default value is 'End Date' + */ + end_date_placeholder_text?: string; + + /** + * The HTML element ID of the start date input field. + * Not used by Dash, only by CSS. + */ + start_date_id?: string; + + /** + * The HTML element ID of the end date input field. + * Not used by Dash, only by CSS. + */ + end_date_id?: string; + + setProps: (props: Partial) => void; +} diff --git a/components/dash-core-components/src/utils/DatePickerPersistence.js b/components/dash-core-components/src/utils/DatePickerPersistence.ts similarity index 72% rename from components/dash-core-components/src/utils/DatePickerPersistence.js rename to components/dash-core-components/src/utils/DatePickerPersistence.ts index 7c4a157ce7..f9efdd2eda 100644 --- a/components/dash-core-components/src/utils/DatePickerPersistence.js +++ b/components/dash-core-components/src/utils/DatePickerPersistence.ts @@ -2,11 +2,11 @@ import moment from 'moment'; import {isNil} from 'ramda'; export default { - extract: propValue => { + extract: (propValue?: string) => { if (!isNil(propValue)) { return moment(propValue).startOf('day').format('YYYY-MM-DD'); } return propValue; }, - apply: storedValue => storedValue, + apply: (storedValue?: string) => storedValue, }; diff --git a/components/dash-core-components/src/utils/LazyLoader/datePickerRange.js b/components/dash-core-components/src/utils/LazyLoader/datePickerRange.js deleted file mode 100644 index c01da5e22f..0000000000 --- a/components/dash-core-components/src/utils/LazyLoader/datePickerRange.js +++ /dev/null @@ -1,4 +0,0 @@ -export default () => - import(/* webpackChunkName: "datepicker" */ '../../fragments/DatePickerRange.react'); - - diff --git a/components/dash-core-components/src/utils/LazyLoader/datePickerRange.ts b/components/dash-core-components/src/utils/LazyLoader/datePickerRange.ts new file mode 100644 index 0000000000..450a1ce66a --- /dev/null +++ b/components/dash-core-components/src/utils/LazyLoader/datePickerRange.ts @@ -0,0 +1,4 @@ +export default () => + import( + /* webpackChunkName: "datepicker" */ '../../fragments/DatePickerRange' + ); diff --git a/components/dash-core-components/src/utils/LazyLoader/datePickerSingle.js b/components/dash-core-components/src/utils/LazyLoader/datePickerSingle.js deleted file mode 100644 index 6079054e26..0000000000 --- a/components/dash-core-components/src/utils/LazyLoader/datePickerSingle.js +++ /dev/null @@ -1,2 +0,0 @@ -export default () => import(/* webpackChunkName: "datepicker" */ '../../fragments/DatePickerSingle.react'); - diff --git a/components/dash-core-components/src/utils/LazyLoader/datePickerSingle.ts b/components/dash-core-components/src/utils/LazyLoader/datePickerSingle.ts new file mode 100644 index 0000000000..d76ef5a974 --- /dev/null +++ b/components/dash-core-components/src/utils/LazyLoader/datePickerSingle.ts @@ -0,0 +1,4 @@ +export default () => + import( + /* webpackChunkName: "datepicker" */ '../../fragments/DatePickerSingle' + ); diff --git a/components/dash-core-components/src/utils/calendar/Calendar.tsx b/components/dash-core-components/src/utils/calendar/Calendar.tsx new file mode 100644 index 0000000000..bcec42cf54 --- /dev/null +++ b/components/dash-core-components/src/utils/calendar/Calendar.tsx @@ -0,0 +1,390 @@ +import React, { + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, + forwardRef, +} from 'react'; +import moment from 'moment'; +import { + ArrowUpIcon, + ArrowDownIcon, + ArrowLeftIcon, + ArrowRightIcon, +} from '@radix-ui/react-icons'; +import Input, {HTMLInputTypes} from '../../components/Input'; +import Dropdown from '../../components/Dropdown'; +import {DayOfWeek, CalendarDirection} from '../../types'; +import {CalendarMonth} from './CalendarMonth'; +import { + getMonthOptions, + formatYear, + parseYear, + isDateInRange, + isSameDay, +} from './helpers'; + +export type CalendarHandle = { + focusDate: (date?: Date) => void; + setVisibleDate: (date: Date) => void; +}; + +type CalendarProps = { + onSelectionChange: (selectionStart: Date, selectionEnd?: Date) => void; + selectionStart?: Date; + selectionEnd?: Date; + highlightStart?: Date; + highlightEnd?: Date; + initialVisibleDate?: Date; + minDateAllowed?: Date; + maxDateAllowed?: Date; + disabledDates?: Date[]; + firstDayOfWeek?: DayOfWeek; + showOutsideDays?: boolean; + monthFormat?: string; + calendarOrientation?: 'vertical' | 'horizontal'; + numberOfMonthsShown?: number; + daySize?: number; + direction?: CalendarDirection; +}; + +type CalendarPropsWithRef = CalendarProps & { + forwardedRef?: React.Ref; +}; + +const CalendarComponent = ({ + initialVisibleDate = new Date(), + onSelectionChange, + selectionStart, + selectionEnd, + highlightStart, + highlightEnd, + minDateAllowed, + maxDateAllowed, + disabledDates, + firstDayOfWeek, + showOutsideDays, + monthFormat, + calendarOrientation, + numberOfMonthsShown = 1, + daySize, + direction = CalendarDirection.LeftToRight, + forwardedRef, +}: CalendarPropsWithRef) => { + const [activeYear, setActiveYear] = useState(() => + initialVisibleDate.getFullYear() + ); + const [activeMonth, setActiveMonth] = useState(() => + initialVisibleDate.getMonth() + ); + + const [focusedDate, setFocusedDate] = useState(); + const [highlightedDates, setHighlightedDates] = useState<[Date, Date]>(); + const calendarContainerRef = useRef(document.createElement('div')); + const scrollAccumulatorRef = useRef(0); + const prevFocusedDateRef = useRef(focusedDate); + + const displayYear = useMemo(() => { + const formatted = formatYear(activeYear, monthFormat); + return parseInt(formatted, 10); + }, [activeYear, monthFormat]); + + useImperativeHandle(forwardedRef, () => ({ + focusDate: (date = moment([activeYear, activeMonth, 1]).toDate()) => { + setFocusedDate(date); + }, + setVisibleDate: (date: Date) => { + setActiveMonth(date.getMonth()); + setActiveYear(date.getFullYear()); + }, + })); + + useEffect(() => { + // Syncs activeMonth/activeYear to focusedDate when focusedDate changes + if (!focusedDate) { + return; + } + if (focusedDate.getTime() === prevFocusedDateRef.current?.getTime()) { + return; + } + prevFocusedDateRef.current = focusedDate; + + const halfRange = Math.floor((numberOfMonthsShown - 1) / 2); + const activeMonthStart = moment([activeYear, activeMonth, 1]); + const visibleStart = activeMonthStart + .clone() + .subtract(halfRange, 'months') + .toDate(); + const visibleEnd = activeMonthStart + .clone() + .add(halfRange, 'months') + .toDate(); + + const focusedMonthStart = new Date( + focusedDate.getFullYear(), + focusedDate.getMonth(), + 1 + ); + if (!isDateInRange(focusedMonthStart, visibleStart, visibleEnd)) { + setActiveMonth(focusedDate.getMonth()); + setActiveYear(focusedDate.getFullYear()); + } + }, [focusedDate, activeMonth, activeYear, numberOfMonthsShown]); + + useEffect(() => { + if (highlightStart && highlightEnd) { + setHighlightedDates([highlightStart, highlightEnd]); + } else if (highlightStart) { + setHighlightedDates([highlightStart, highlightStart]); + } else { + setHighlightedDates(undefined); + } + }, [highlightStart, highlightEnd]); + + useEffect(() => { + if (selectionStart && selectionEnd) { + setHighlightedDates([selectionStart, selectionEnd]); + } + }, [selectionStart, selectionEnd]); + + const selectedDates = useMemo((): Date[] => { + return [selectionStart, selectionEnd].filter( + (d): d is Date => d !== undefined + ); + }, [selectionStart, selectionEnd]); + + const handleSelectionStart = useCallback( + (date: Date) => { + if (!selectionStart || selectionEnd) { + // No selection yet, or previous selection is complete → start new selection + setHighlightedDates(undefined); + onSelectionChange(date, undefined); + } + }, + [selectionStart, selectionEnd, onSelectionChange] + ); + + const handleSelectionEnd = useCallback( + (date: Date) => { + // Complete the selection with an end date + if (selectionStart && !selectionEnd) { + // Incomplete selection exists (range picker mid-selection) + if (!isSameDay(selectionStart, date)) { + onSelectionChange(selectionStart, date); + } + } else { + // Complete selection exists or a single date was chosen + onSelectionChange(date, date); + } + }, + [selectionStart, selectionEnd, onSelectionChange] + ); + + const handleDaysHighlighted = useCallback( + (date: Date) => { + if (selectionStart && selectionEnd) { + setHighlightedDates([selectionStart, selectionEnd]); + } else if (selectionStart && !selectionEnd) { + setHighlightedDates([selectionStart, date]); + } else { + setHighlightedDates([date, date]); + } + }, + [selectionStart, selectionEnd] + ); + + const handleDayFocused = useCallback( + (date: Date) => { + setFocusedDate(date); + // When navigating with keyboard during range selection, + // highlight the range from start to focused date + if (selectionStart && !selectionEnd) { + setHighlightedDates([selectionStart, date]); + } + }, + [selectionStart, selectionEnd] + ); + + const monthOptions = useMemo( + () => + getMonthOptions( + activeYear, + monthFormat, + minDateAllowed, + maxDateAllowed + ), + [activeYear, monthFormat, minDateAllowed, maxDateAllowed] + ); + + const changeMonthBy = useCallback( + (months: number) => { + const currentDate = moment([activeYear, activeMonth, 1]); + + // In RTL mode, directions are reversed + const actualMonths = + direction === CalendarDirection.RightToLeft ? -months : months; + + const newDate = currentDate.clone().add(actualMonths, 'month'); + const newMonthStart = newDate.toDate(); + + if (isDateInRange(newMonthStart, minDateAllowed, maxDateAllowed)) { + setActiveYear(newDate.year()); + setActiveMonth(newDate.month()); + } + }, + [activeYear, activeMonth, minDateAllowed, maxDateAllowed, direction] + ); + + const handleWheel = useCallback( + (e: WheelEvent) => { + e.preventDefault(); + + // Accumulate scroll delta until threshold is reached, then change the active month + // This respects OS scroll speed settings and works well with trackpads + const threshold = 100; // Adjust this to control sensitivity + + scrollAccumulatorRef.current += e.deltaY; + + if (Math.abs(scrollAccumulatorRef.current) >= threshold) { + const offset = scrollAccumulatorRef.current > 0 ? 1 : -1; + changeMonthBy(offset); + scrollAccumulatorRef.current = 0; // Reset accumulator after month change + } + }, + [changeMonthBy] + ); + + useEffect(() => { + // Add listener with passive: false to allow preventDefault + calendarContainerRef.current.addEventListener('wheel', handleWheel, { + passive: false, + }); + + return () => { + calendarContainerRef.current?.removeEventListener( + 'wheel', + handleWheel + ); + }; + }, [handleWheel]); + + const canChangeMonthBy = useCallback( + (months: number) => { + const currentDate = moment([activeYear, activeMonth, 1]); + const targetMonth = currentDate + .clone() + .add(months, 'month') + .toDate(); + + return isDateInRange(targetMonth, minDateAllowed, maxDateAllowed); + }, + [activeYear, activeMonth, minDateAllowed, maxDateAllowed] + ); + + const isVertical = calendarOrientation === 'vertical'; + const PreviousMonthIcon = isVertical ? ArrowUpIcon : ArrowLeftIcon; + const NextMonthIcon = isVertical ? ArrowDownIcon : ArrowRightIcon; + + return ( +
+
+ + { + if (Number.isInteger(value)) { + setActiveMonth(value as number); + } + }} + /> + { + const parsed = parseYear(String(value)); + if (parsed !== undefined) { + setActiveYear(parsed); + } + }} + /> + +
+
+ {Array.from({length: numberOfMonthsShown}, (_, i) => { + // Center the view: start from (numberOfMonthsShown - 1) / 2 months before activeMonth + const offset = + i - Math.floor((numberOfMonthsShown - 1) / 2); + const monthDate = moment([activeYear, activeMonth, 1]).add( + offset, + 'months' + ); + return ( + 1} + direction={direction} + /> + ); + })} +
+
+ ); +}; + +const Calendar = forwardRef((props, ref) => { + return ; +}); + +Calendar.displayName = 'Calendar'; + +export default Calendar; diff --git a/components/dash-core-components/src/utils/calendar/CalendarDay.tsx b/components/dash-core-components/src/utils/calendar/CalendarDay.tsx new file mode 100644 index 0000000000..9c33072370 --- /dev/null +++ b/components/dash-core-components/src/utils/calendar/CalendarDay.tsx @@ -0,0 +1,75 @@ +import React, { + DetailedHTMLProps, + TdHTMLAttributes, + useEffect, + useRef, +} from 'react'; + +type CalendarDayProps = DetailedHTMLProps< + TdHTMLAttributes, + HTMLTableCellElement +> & { + date: Date; + isOutside: boolean; + showOutsideDays: boolean; + isSelected?: boolean; + isHighlighted?: boolean; + isFocused?: boolean; + isDisabled?: boolean; +}; + +const CalendarDay = ({ + date, + isOutside, + showOutsideDays, + isSelected = false, + isHighlighted = false, + isFocused = false, + isDisabled = false, + className, + ...passThruProps +}: CalendarDayProps): JSX.Element => { + // Compute label: show day number unless it's an outside day and we're not showing outside days + const label = !showOutsideDays && isOutside ? '' : String(date.getDate()); + + let extraClasses = ''; + + if (isOutside) { + extraClasses += ' dash-datepicker-calendar-date-outside'; + } else { + extraClasses += ' dash-datepicker-calendar-date-inside'; + } + + if (isSelected) { + extraClasses += ' dash-datepicker-calendar-date-selected'; + } + if (isHighlighted) { + extraClasses += ' dash-datepicker-calendar-date-highlighted'; + } + if (isDisabled) { + extraClasses += ' dash-datepicker-calendar-date-disabled'; + } + className = (className ?? '') + extraClasses; + + const ref = useRef(document.createElement('td')); + + useEffect(() => { + if (isFocused) { + ref.current.focus(); + } + }, [isFocused]); + + return ( + + {label} + + ); +}; + +export default CalendarDay; diff --git a/components/dash-core-components/src/utils/calendar/CalendarDayPadding.tsx b/components/dash-core-components/src/utils/calendar/CalendarDayPadding.tsx new file mode 100644 index 0000000000..8e699de448 --- /dev/null +++ b/components/dash-core-components/src/utils/calendar/CalendarDayPadding.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +/* + * Renders an "empty" cell in a calendar month, representing days that fall + * outside the month + */ +const CalendarDayPadding = (): JSX.Element => { + return ( + + + + ); +}; + +export default CalendarDayPadding; diff --git a/components/dash-core-components/src/utils/calendar/CalendarMonth.tsx b/components/dash-core-components/src/utils/calendar/CalendarMonth.tsx new file mode 100644 index 0000000000..aa31df2918 --- /dev/null +++ b/components/dash-core-components/src/utils/calendar/CalendarMonth.tsx @@ -0,0 +1,243 @@ +import React, {useCallback, useMemo} from 'react'; +import moment from 'moment'; +import CalendarDay from './CalendarDay'; +import CalendarDayPadding from './CalendarDayPadding'; +import {createMonthGrid} from './createMonthGrid'; +import {isDateInRange, isDateDisabled, isSameDay} from './helpers'; +import {CalendarDirection} from '../../types'; +import '../../components/css/calendar.css'; +import CalendarMonthHeader from './CalendarMonthHeader'; + +export enum NavigationDirection { + Backward = -1, + Forward = 1, +} + +type CalendarMonthProps = { + year: number; + month: number; // 0-11 representing January-December; + dateFocused?: Date; + selectedDates?: Date[]; + highlightedDatesRange?: [Date, Date]; + minDateAllowed?: Date; + maxDateAllowed?: Date; + disabledDates?: Date[]; + onSelectionStart?: (date: Date) => void; + onSelectionEnd?: (date: Date) => void; + onDayFocused?: (date: Date) => void; + onDaysHighlighted?: (date: Date) => void; + firstDayOfWeek?: number; // 0-7 + showOutsideDays?: boolean; + daySize?: number; + monthFormat?: string; + showMonthHeader?: boolean; + direction?: CalendarDirection; +}; + +export const CalendarMonth = ({ + year, + month, + onSelectionStart, + onSelectionEnd, + onDayFocused, + onDaysHighlighted, + selectedDates = [], + highlightedDatesRange, + minDateAllowed, + maxDateAllowed, + disabledDates, + monthFormat, + firstDayOfWeek = 0, + showOutsideDays = true, + // eslint-disable-next-line no-magic-numbers + daySize = 36, + showMonthHeader = false, + direction = CalendarDirection.LeftToRight, + ...props +}: CalendarMonthProps): JSX.Element => { + const gridDates = useMemo( + () => createMonthGrid(year, month, firstDayOfWeek, showOutsideDays), + [year, month, firstDayOfWeek, showOutsideDays] + ); + + const isDisabled = useCallback( + (date: Date): boolean => { + return isDateDisabled( + date, + minDateAllowed, + maxDateAllowed, + disabledDates + ); + }, + [minDateAllowed, maxDateAllowed, disabledDates] + ); + + const computeIsOutside = useCallback( + (date: Date): boolean => { + return date.getMonth() !== month; + }, + [month] + ); + + const daysOfTheWeek = useMemo(() => { + return Array.from({length: 7}, (_, i) => + moment() + .day((i + firstDayOfWeek) % 7) + .format('dd') + ); + }, [firstDayOfWeek]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent, date: Date) => { + const m = moment(date); + let newDate: moment.Moment | null = null; + + switch (e.key) { + case ' ': + case 'Enter': { + e.preventDefault(); + const isOutside = computeIsOutside(date); + if (!isDisabled(date) && (!isOutside || showOutsideDays)) { + // Keyboard selection: only call onSelectionEnd + // Calendar will handle completing immediately by setting both start and end + onSelectionEnd?.(date); + } + return; + } + case 'ArrowRight': + newDate = + direction === CalendarDirection.RightToLeft + ? m.subtract(1, 'day') + : m.add(1, 'day'); + break; + case 'ArrowLeft': + newDate = + direction === CalendarDirection.RightToLeft + ? m.add(1, 'day') + : m.subtract(1, 'day'); + break; + case 'ArrowDown': + newDate = m.add(1, 'week'); + break; + case 'ArrowUp': + newDate = m.subtract(1, 'week'); + break; + case 'PageDown': + newDate = m.add(1, 'month'); + break; + case 'PageUp': + newDate = m.subtract(1, 'month'); + break; + case 'Home': + // Navigate to week start (respecting firstDayOfWeek) + newDate = m.clone().day(firstDayOfWeek); + // If we went forward, adjust backward to current week + if (newDate.isAfter(m, 'day')) { + newDate.subtract(1, 'week'); + } + break; + case 'End': + // Navigate to week end (respecting firstDayOfWeek) + newDate = m.clone().day((firstDayOfWeek + 6) % 7); + // If we went backward, adjust forward to current week + if (newDate.isBefore(m, 'day')) { + newDate.add(1, 'week'); + } + break; + default: + return; + } + + if (newDate) { + e.preventDefault(); + const newDateObj = newDate.toDate(); + if (isDateInRange(newDateObj, minDateAllowed, maxDateAllowed)) { + onDayFocused?.(newDateObj); + } + } + }, + [ + onDayFocused, + onSelectionStart, + onSelectionEnd, + computeIsOutside, + isDisabled, + showOutsideDays, + minDateAllowed, + maxDateAllowed, + direction, + firstDayOfWeek, + ] + ); + + const calendarWidth = daySize * 7 + 16; // 16px for table padding + + return ( + + + {showMonthHeader && ( + + + + )} + + {daysOfTheWeek.map((day, i) => ( + + ))} + + + + {gridDates.map((week, i) => ( + + {week.map((date, j) => + date ? ( + { + onDaysHighlighted?.(date); + onSelectionStart?.(date); + }} + onMouseUp={() => onSelectionEnd?.(date)} + onMouseEnter={() => + onDaysHighlighted?.(date) + } + onKeyDown={e => handleKeyDown(e, date)} + isFocused={isSameDay( + date, + props.dateFocused + )} + isSelected={selectedDates.some(d => + isSameDay(date, d) + )} + isHighlighted={ + highlightedDatesRange !== undefined && + isDateInRange( + date, + highlightedDatesRange[0], + highlightedDatesRange[1] + ) + } + isDisabled={isDisabled(date)} + /> + ) : ( + + ) + )} + + ))} + +
+ {day} +
+ ); +}; diff --git a/components/dash-core-components/src/utils/calendar/CalendarMonthHeader.tsx b/components/dash-core-components/src/utils/calendar/CalendarMonthHeader.tsx new file mode 100644 index 0000000000..3d50fb2a3c --- /dev/null +++ b/components/dash-core-components/src/utils/calendar/CalendarMonthHeader.tsx @@ -0,0 +1,23 @@ +import React, {useMemo} from 'react'; +import {formatDate} from './helpers'; + +const CalendarMonthHeader = (props: { + year: number; + month: number; + monthFormat?: string; +}) => { + const label = useMemo(() => { + return formatDate( + new Date(props.year, props.month, 1), + props.monthFormat + ); + }, [props]); + + return ( + + {label} + + ); +}; + +export default CalendarMonthHeader; diff --git a/components/dash-core-components/src/utils/calendar/createMonthGrid.ts b/components/dash-core-components/src/utils/calendar/createMonthGrid.ts new file mode 100644 index 0000000000..d10492935f --- /dev/null +++ b/components/dash-core-components/src/utils/calendar/createMonthGrid.ts @@ -0,0 +1,39 @@ +import moment from 'moment'; + +/** + * Creates a 2D array of Date objects representing a calendar month grid. + * Always returns exactly 6 rows (weeks) to maintain consistent calendar height. + */ +export const createMonthGrid = ( + year: number, + month: number, + firstDayOfWeek: number, + showOutsideDays = true +): (Date | null)[][] => { + const firstDay = moment([year, month, 1]); + const offset = (firstDay.day() - firstDayOfWeek + 7) % 7; + const daysInMonth = firstDay.daysInMonth(); + const weeksNeeded = Math.ceil((offset + daysInMonth) / 7); + const startDate = firstDay.clone().subtract(offset, 'days'); + + const grid: (Date | null)[][] = []; + + for (let week = 0; week < weeksNeeded; week++) { + grid.push( + Array.from({length: 7}, (_, day) => { + const date = startDate.clone().add(week * 7 + day, 'days'); + if (!showOutsideDays && date.month() !== month) { + return null; + } + return date.toDate(); + }) + ); + } + + // Pad with empty rows to always have 6 rows total + while (grid.length < 6) { + grid.push(Array.from({length: 7}, () => null)); + } + + return grid; +}; diff --git a/components/dash-core-components/src/utils/calendar/helpers.ts b/components/dash-core-components/src/utils/calendar/helpers.ts new file mode 100644 index 0000000000..b35ea768e0 --- /dev/null +++ b/components/dash-core-components/src/utils/calendar/helpers.ts @@ -0,0 +1,168 @@ +import moment from 'moment'; +import {DatePickerSingleProps} from '../../types'; + +export function formatDate(date?: Date, format = 'YYYY-MM-DD'): string { + if (!date) { + return ''; + } + return moment(date).format(format); +} + +/* + * Outputs a date object in YYYY-MM-DD format, suitable for use in props + */ +export function dateAsStr( + date?: Date +): `${string}-${string}-${string}` | undefined { + if (!date) { + return undefined; + } + return formatDate(date, 'YYYY-MM-DD') as `${string}-${string}-${string}`; +} + +export function strAsDate(date?: string, format?: string): Date | undefined { + if (!date) { + return undefined; + } + const parsed = format ? moment(date, format, true) : moment(date); + if (!parsed.isValid()) { + return undefined; + } + return parsed.startOf('day').toDate(); +} + +type AnyDayFormat = string | Date | DatePickerSingleProps['date']; +export function isSameDay(day1?: AnyDayFormat, day2?: AnyDayFormat): boolean { + if (!day1 && !day2) { + return true; // Both undefined/null - considered the same + } + if (!day1 || !day2) { + return false; // Only one is defined - considered different + } + return moment(day1).isSame(day2, 'day'); +} + +export function isDateInRange( + targetDate: Date, + minDate?: Date, + maxDate?: Date +): boolean { + const target = moment(targetDate); + + // If both dates are provided, normalize them to ensure min <= max + if (minDate && maxDate) { + const min = moment(minDate); + const max = moment(maxDate); + const [actualMin, actualMax] = min.isSameOrBefore(max, 'day') + ? [min, max] + : [max, min]; + + return ( + target.isSameOrAfter(actualMin, 'day') && + target.isSameOrBefore(actualMax, 'day') + ); + } + + if (minDate && target.isBefore(moment(minDate), 'day')) { + return false; + } + + if (maxDate && target.isAfter(moment(maxDate), 'day')) { + return false; + } + + return true; +} + +/** + * Checks if a date is disabled based on min/max constraints and disabled dates array. + */ +export function isDateDisabled( + date: Date, + minDate?: Date, + maxDate?: Date, + disabledDates?: Date[] +): boolean { + // Check if date is outside min/max range + if (!isDateInRange(date, minDate, maxDate)) { + return true; + } + + // Check if date is in the disabled dates array + if (disabledDates) { + return disabledDates.some(d => isSameDay(date, d)); + } + + return false; +} + +export function formatMonth( + year: number, + month: number, + format?: string +): string { + const {monthFormat} = extractFormats(format); + return moment(new Date(year, month, 1)).format(monthFormat); +} + +/** + * Extracts separate month and year format strings from a combined month_format, e.g. "MMM YY". + */ +export function extractFormats(format?: string): { + monthFormat: string; + yearFormat: string; +} { + if (!format) { + return {monthFormat: 'MMMM', yearFormat: 'YYYY'}; + } + + // Extract month tokens (MMMM, MMM, MM, M) + const monthMatch = format.match(/M{1,4}/); + const monthFormat = monthMatch ? monthMatch[0] : 'MMMM'; + + // Extract year tokens (YYYY, YY) + const yearMatch = format.match(/Y{2,4}/); + const yearFormat = yearMatch ? yearMatch[0] : 'YYYY'; + + return {monthFormat, yearFormat}; +} + +/** + * Generates month options for a dropdown based on a format string. + */ +export function getMonthOptions( + year: number, + format?: string, + minDate?: Date, + maxDate?: Date +): Array<{label: string; value: number; disabled?: boolean}> { + const {monthFormat} = extractFormats(format); + + return Array.from({length: 12}, (_, i) => { + const monthStart = moment([year, i, 1]); + const label = monthStart.format(monthFormat); + + // Check if this month is outside the allowed range + const disabled = + (minDate && monthStart.isBefore(moment(minDate), 'month')) || + (maxDate && monthStart.isAfter(moment(maxDate), 'month')); + + return {label, value: i, disabled}; + }); +} + +/** + * Formats a year according to the year format extracted from month_format. + */ +export function formatYear(year: number, format?: string): string { + const {yearFormat} = extractFormats(format); + return moment(new Date(year, 0, 1)).format(yearFormat); +} + +/** + * Parses a year string and converts it to a full 4-digit year. + */ +export function parseYear(yearStr: string): number | undefined { + const parsed = moment(yearStr, ['YY', 'YYYY']); + return parsed.isValid() ? parsed.year() : undefined; +} diff --git a/components/dash-core-components/src/utils/convertToMoment.js b/components/dash-core-components/src/utils/convertToMoment.js deleted file mode 100644 index 4b74e1a005..0000000000 --- a/components/dash-core-components/src/utils/convertToMoment.js +++ /dev/null @@ -1,34 +0,0 @@ -import moment from 'moment'; -import {has} from 'ramda'; - -export default (newProps, momentProps) => { - const dest = {}; - - momentProps.forEach(key => { - const value = newProps[key]; - - if (value === null || value === undefined) { - dest[key] = null; - - if (key === 'initial_visible_month') { - dest[key] = moment( - newProps.start_date || - newProps.min_date_allowed || - newProps.end_date || - newProps.max_date_allowed || - undefined - ); - } - } else if (Array.isArray(value)) { - dest[key] = value.map(d => moment(d)); - } else { - dest[key] = moment(value); - - if (key === 'max_date_allowed' && has(key, dest)) { - dest[key].add(1, 'days'); - } - } - }); - - return dest; -}; diff --git a/components/dash-core-components/src/utils/optionTypes.js b/components/dash-core-components/src/utils/optionTypes.js deleted file mode 100644 index ca092ff12e..0000000000 --- a/components/dash-core-components/src/utils/optionTypes.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {type} from 'ramda'; - -export const sanitizeOptions = options => { - if (type(options) === 'Object') { - return Object.entries(options).map(([value, label]) => ({ - label: React.isValidElement(label) ? label : String(label), - value, - })); - } - - if (type(options) === 'Array') { - if ( - options.length > 0 && - ['String', 'Number', 'Bool'].includes(type(options[0])) - ) { - return options.map(option => ({ - label: String(option), - value: option, - })); - } - return options; - } - - return options; -}; diff --git a/components/dash-core-components/tests/dash_core_components_page.py b/components/dash-core-components/tests/dash_core_components_page.py index 14ee0777be..3bea980ac5 100644 --- a/components/dash-core-components/tests/dash_core_components_page.py +++ b/components/dash-core-components/tests/dash_core_components_page.py @@ -16,14 +16,15 @@ def select_date_single(self, compid, index=0, day="", outside_month=False): outside_month: used in conjunction with day. indicates if the day out the scope of current month. default False. """ - date = self.find_element(f"#{compid} input") + date = self.find_element(f"#{compid}") date.click() def is_month_valid(elem): + classes = elem.get_attribute("class") or "" return ( - "__outside" in elem.get_attribute("class") + "dash-datepicker-calendar-date-outside" in classes if outside_month - else "__outside" not in elem.get_attribute("class") + else "dash-datepicker-calendar-date-outside" not in classes ) self._wait_until_day_is_clickable() @@ -67,14 +68,14 @@ def select_date_range(self, compid, day_range, start_first=True): return prefix = "Start" if start_first else "End" - date = self.find_element(f'#{compid} input[aria-label="{prefix} Date"]') + date = self.find_element(f'#{compid}[aria-label="{prefix} Date"]') date.click() for day in day_range: self._wait_until_day_is_clickable() matched = [ _ for _ in self.find_elements(self.date_picker_day_locator) - if _.text == str(day) + if _.find_element(By.CSS_SELECTOR, "span").text == str(day) ] matched[0].click() @@ -82,7 +83,8 @@ def select_date_range(self, compid, day_range, start_first=True): def get_date_range(self, compid): return tuple( - _.get_attribute("value") for _ in self.find_elements(f"#{compid} input") + _.get_attribute("value") + for _ in self.find_elements(f"#{compid}-wrapper .dash-datepicker-input") ) def _wait_until_day_is_clickable(self, timeout=1): @@ -92,7 +94,7 @@ def _wait_until_day_is_clickable(self, timeout=1): @property def date_picker_day_locator(self): - return 'div[data-visible="true"] td.CalendarDay' + return ".dash-datepicker-calendar-date-inside, .dash-datepicker-calendar-date-outside" def click_and_hold_at_coord_fractions(self, elem_or_selector, fx, fy): elem = self._get_element(elem_or_selector) diff --git a/components/dash-core-components/tests/integration/calendar/test_a11y_date_picker_range.py b/components/dash-core-components/tests/integration/calendar/test_a11y_date_picker_range.py new file mode 100644 index 0000000000..800b610103 --- /dev/null +++ b/components/dash-core-components/tests/integration/calendar/test_a11y_date_picker_range.py @@ -0,0 +1,179 @@ +from datetime import datetime +from dash import Dash, Input, Output +from dash.dcc import DatePickerRange +from dash.html import Div +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.action_chains import ActionChains + + +def send_keys(driver, key): + """Send keyboard keys to the browser""" + actions = ActionChains(driver) + actions.send_keys(key) + actions.perform() + + +def get_focused_text(driver): + """Get the text content of the currently focused element""" + return driver.execute_script("return document.activeElement.textContent;") + + +def test_a11y_range_001_keyboard_range_selection_with_highlights(dash_dcc): + """Test keyboard-based range selection with highlight verification""" + app = Dash(__name__) + app.layout = Div( + [ + DatePickerRange( + id="date-picker-range", + initial_visible_month=datetime(2021, 1, 1), + ), + Div(id="output-dates"), + ] + ) + + @app.callback( + Output("output-dates", "children"), + Input("date-picker-range", "start_date"), + Input("date-picker-range", "end_date"), + ) + def update_output(start_date, end_date): + if start_date and end_date: + return f"{start_date} to {end_date}" + elif start_date: + return f"Start: {start_date}" + return "" + + dash_dcc.start_server(app) + + # Find the first input field and open calendar with keyboard + date_picker_input = dash_dcc.find_element(".dash-datepicker-input") + date_picker_input.click() + dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + + # Calendar opens with Jan 1 focused (first day of month since no dates selected) + # Navigate: Arrow Down (Jan 1 -> 8) + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + + # Verify focused date is Jan 8 + assert get_focused_text(dash_dcc.driver) == "8" + + # Press Space to select the first date (Jan 8) + send_keys(dash_dcc.driver, Keys.SPACE) + + # Verify first date was selected (only start_date, no end_date yet) + dash_dcc.wait_for_text_to_equal("#output-dates", "Start: 2021-01-08") + + # Navigate to another date: Arrow Down (1 week) + Arrow Right (1 day) + # Jan 8 -> Jan 15 -> Jan 16 + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + send_keys(dash_dcc.driver, Keys.ARROW_RIGHT) + + # Verify focused date is Jan 16 + assert get_focused_text(dash_dcc.driver) == "16" + + # Verify that days between Jan 8 and Jan 16 are highlighted + # The highlighted dates should have the class 'dash-datepicker-calendar-date-highlighted' + highlighted_dates = dash_dcc.driver.find_elements( + "css selector", ".dash-datepicker-calendar-date-highlighted" + ) + # Should have 9 highlighted dates (Jan 8 through Jan 16 inclusive) + assert ( + len(highlighted_dates) == 9 + ), f"Expected 9 highlighted dates, got {len(highlighted_dates)}" + + # Press Enter to select the second date + send_keys(dash_dcc.driver, Keys.ENTER) + + # Calendar should close + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=0.25) + + # Verify both dates were selected in the output + dash_dcc.wait_for_text_to_equal("#output-dates", "2021-01-08 to 2021-01-16") + + assert dash_dcc.get_logs() == [] + + +def test_a11y_range_002_keyboard_update_existing_range(dash_dcc): + """Test keyboard-based updating of an existing date range""" + app = Dash(__name__) + app.layout = Div( + [ + DatePickerRange( + id="date-picker-range", + start_date="2021-01-10", + end_date="2021-01-20", + initial_visible_month=datetime(2021, 1, 1), + ), + Div(id="output-dates"), + ] + ) + + @app.callback( + Output("output-dates", "children"), + Input("date-picker-range", "start_date"), + Input("date-picker-range", "end_date"), + ) + def update_output(start_date, end_date): + if start_date and end_date: + return f"{start_date} to {end_date}" + elif start_date: + return f"Start: {start_date}" + return "" + + dash_dcc.start_server(app) + + # Verify initial range is displayed + dash_dcc.wait_for_text_to_equal("#output-dates", "2021-01-10 to 2021-01-20") + + # Find the first input field and open calendar with keyboard + date_picker_input = dash_dcc.find_element(".dash-datepicker-input") + date_picker_input.click() + dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + + # Calendar opens with Jan 10 focused (the current start date) + # Navigate: Arrow Down (Jan 10 -> 17), then 5x Arrow Left (17 -> 16 -> 15 -> 14 -> 13 -> 12) + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + send_keys(dash_dcc.driver, Keys.ARROW_LEFT) + send_keys(dash_dcc.driver, Keys.ARROW_LEFT) + send_keys(dash_dcc.driver, Keys.ARROW_LEFT) + send_keys(dash_dcc.driver, Keys.ARROW_LEFT) + send_keys(dash_dcc.driver, Keys.ARROW_LEFT) + + # Verify focused date is Jan 12 + assert get_focused_text(dash_dcc.driver) == "12" + + # Press Space to start a NEW range selection with Jan 12 as start_date + # This should clear end_date and set only start_date + send_keys(dash_dcc.driver, Keys.SPACE) + + # Verify new start date was selected (only start_date, no end_date) + dash_dcc.wait_for_text_to_equal("#output-dates", "Start: 2021-01-12") + + # Navigate to new end date: Arrow Down + Arrow Right (Jan 12 -> 19 -> 20) + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + send_keys(dash_dcc.driver, Keys.ARROW_RIGHT) + + # Verify focused date is Jan 20 + assert get_focused_text(dash_dcc.driver) == "20" + + # Verify that days between Jan 12 and Jan 20 are highlighted + highlighted_dates = dash_dcc.driver.find_elements( + "css selector", ".dash-datepicker-calendar-date-highlighted" + ) + # Should have 9 highlighted dates (Jan 12 through 20 inclusive) + assert ( + len(highlighted_dates) == 9 + ), f"Expected 9 highlighted dates, got {len(highlighted_dates)}" + + # Press Enter to select the new end date + send_keys(dash_dcc.driver, Keys.ENTER) + + # Calendar should close + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=0.25) + + # Verify both dates were updated in the output + dash_dcc.wait_for_text_to_equal("#output-dates", "2021-01-12 to 2021-01-20") + + assert dash_dcc.get_logs() == [] diff --git a/components/dash-core-components/tests/integration/calendar/test_a11y_date_picker_single.py b/components/dash-core-components/tests/integration/calendar/test_a11y_date_picker_single.py new file mode 100644 index 0000000000..100f5b1406 --- /dev/null +++ b/components/dash-core-components/tests/integration/calendar/test_a11y_date_picker_single.py @@ -0,0 +1,388 @@ +from datetime import datetime +from dash import Dash, Input, Output +from dash.dcc import DatePickerSingle +from dash.html import Div, Label, P +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.common.action_chains import ActionChains + + +def send_keys(driver, key): + """Send keyboard keys to the browser""" + actions = ActionChains(driver) + actions.send_keys(key) + actions.perform() + + +def get_focused_text(driver): + """Get the text content of the currently focused element""" + return driver.execute_script("return document.activeElement.textContent;") + + +def create_date_picker_app(date_picker_props): + """Create a Dash app with a DatePickerSingle component and output callback""" + app = Dash(__name__) + app.layout = Div( + [DatePickerSingle(id="date-picker", **date_picker_props), Div(id="output-date")] + ) + + @app.callback(Output("output-date", "children"), Input("date-picker", "date")) + def update_output(date): + return date or "" + + return app + + +def open_calendar(dash_dcc, date_picker): + """Open the calendar and wait for it to be visible""" + date_picker.click() + dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + + # Send focus onto the calendar + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + + +def close_calendar(dash_dcc, driver): + """Close the calendar with Escape and wait for it to disappear""" + send_keys(driver, Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container") + + +def test_a11y001_label_focuses_date_picker(dash_dcc): + app = Dash(__name__) + app.layout = Label( + [ + P("Click me", id="label"), + DatePickerSingle( + id="date-picker", + initial_visible_month=datetime(2021, 1, 1), + ), + ], + ) + + dash_dcc.start_server(app) + + dash_dcc.wait_for_element("#date-picker") + + # Calendar should be closed initially + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=0.25) + + dash_dcc.find_element("#label").click() + dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + + assert dash_dcc.get_logs() == [] + + +def test_a11y002_label_with_htmlFor_can_focus_date_picker(dash_dcc): + app = Dash(__name__) + app.layout = Div( + [ + Label("Click me", htmlFor="date-picker", id="label"), + DatePickerSingle( + id="date-picker", + initial_visible_month=datetime(2021, 1, 1), + ), + ], + ) + + dash_dcc.start_server(app) + + dash_dcc.wait_for_element("#date-picker") + + # Calendar should be closed initially + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=0.25) + + dash_dcc.find_element("#label").click() + dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + + assert dash_dcc.get_logs() == [] + + +def test_a11y003_keyboard_navigation_arrows(dash_dcc): + app = create_date_picker_app( + { + "date": "2021-01-15", + "initial_visible_month": datetime(2021, 1, 1), + } + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#date-picker") + open_calendar(dash_dcc, date_picker) + + # Get the focused date element (should be Jan 15, 2021) + assert get_focused_text(dash_dcc.driver) == "15" + + # Test ArrowRight - should move to Jan 16 + send_keys(dash_dcc.driver, Keys.ARROW_RIGHT) + assert get_focused_text(dash_dcc.driver) == "16" + + # Test ArrowLeft - should move back to Jan 15 + send_keys(dash_dcc.driver, Keys.ARROW_LEFT) + assert get_focused_text(dash_dcc.driver) == "15" + + # Test ArrowDown - should move to Jan 22 (one week down) + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + assert get_focused_text(dash_dcc.driver) == "22" + + # Test ArrowUp - should move back to Jan 15 (one week up) + send_keys(dash_dcc.driver, Keys.ARROW_UP) + assert get_focused_text(dash_dcc.driver) == "15" + + # Test PageDown - should move to Feb 15 (one month forward) + send_keys(dash_dcc.driver, Keys.PAGE_DOWN) + assert get_focused_text(dash_dcc.driver) == "15" + + # Test PageUp - should move back to Jan 15 (one month back) + send_keys(dash_dcc.driver, Keys.PAGE_UP) + assert get_focused_text(dash_dcc.driver) == "15" + + # Test Enter - should select the date and close calendar + send_keys(dash_dcc.driver, Keys.ENTER) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=0.25) + + assert dash_dcc.get_logs() == [] + + +def test_a11y004_keyboard_navigation_home_end(dash_dcc): + app = create_date_picker_app( + { + "date": "2021-01-15", # Friday, Jan 15, 2021 + "initial_visible_month": datetime(2021, 1, 1), + "first_day_of_week": 0, # Sunday + } + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#date-picker") + open_calendar(dash_dcc, date_picker) + + # Get the focused date element (should be Jan 15, 2021 - Friday) + assert get_focused_text(dash_dcc.driver) == "15" + + # Test Home key - should move to week start (Sunday, Jan 10) + send_keys(dash_dcc.driver, Keys.HOME) + assert get_focused_text(dash_dcc.driver) == "10" + + # Test End key - should move to week end (Saturday, Jan 16) + send_keys(dash_dcc.driver, Keys.END) + assert get_focused_text(dash_dcc.driver) == "16" + + # Test Home key again - should move to week start (Sunday, Jan 10) + send_keys(dash_dcc.driver, Keys.HOME) + assert get_focused_text(dash_dcc.driver) == "10" + + assert dash_dcc.get_logs() == [] + + +def test_a11y005_keyboard_navigation_home_end_monday_start(dash_dcc): + app = create_date_picker_app( + { + "date": "2021-01-15", # Friday, Jan 15, 2021 + "initial_visible_month": datetime(2021, 1, 1), + "first_day_of_week": 1, # Monday + } + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#date-picker") + open_calendar(dash_dcc, date_picker) + + # Get the focused date element (should be Jan 15, 2021 - Friday) + assert get_focused_text(dash_dcc.driver) == "15" + + # Test Home key - should move to week start (Monday, Jan 11) + send_keys(dash_dcc.driver, Keys.HOME) + assert get_focused_text(dash_dcc.driver) == "11" + + # Test End key - should move to week end (Sunday, Jan 17) + send_keys(dash_dcc.driver, Keys.END) + assert get_focused_text(dash_dcc.driver) == "17" + + assert dash_dcc.get_logs() == [] + + +def test_a11y006_keyboard_navigation_rtl(dash_dcc): + app = create_date_picker_app( + { + "date": "2021-01-15", + "initial_visible_month": datetime(2021, 1, 1), + "is_RTL": True, + } + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#date-picker") + open_calendar(dash_dcc, date_picker) + + assert get_focused_text(dash_dcc.driver) == "15" + + # Moves to Jan 14 (reversed) + send_keys(dash_dcc.driver, Keys.ARROW_RIGHT) + assert get_focused_text(dash_dcc.driver) == "14" + + # Moves to Jan 15 (reversed) + send_keys(dash_dcc.driver, Keys.ARROW_LEFT) + assert get_focused_text(dash_dcc.driver) == "15" + + # Moves to week start + send_keys(dash_dcc.driver, Keys.HOME) + assert get_focused_text(dash_dcc.driver) == "10" + + # Moves to week end + send_keys(dash_dcc.driver, Keys.END) + assert get_focused_text(dash_dcc.driver) == "16" + + assert dash_dcc.get_logs() == [] + + +def test_a11y007_all_keyboard_keys_respect_min_max(dash_dcc): + app = create_date_picker_app( + { + "date": "2021-02-15", # Monday + "min_date_allowed": datetime(2021, 2, 15), # Monday - same as start date + "max_date_allowed": datetime(2021, 2, 20), # Sat + "initial_visible_month": datetime(2021, 2, 1), + "first_day_of_week": 0, # Sunday + } + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#date-picker") + initial_value = "2021-02-15" + + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test Arrow Down (would go to Feb 22, beyond max) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + send_keys(dash_dcc.driver, Keys.ENTER) + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test Arrow Up (would go to Feb 8, before min) + close_calendar(dash_dcc, dash_dcc.driver) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.ARROW_UP) + send_keys(dash_dcc.driver, Keys.ENTER) + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test Home (would go to Feb 14 Sunday, before min Feb 15) + close_calendar(dash_dcc, dash_dcc.driver) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.HOME) + send_keys(dash_dcc.driver, Keys.ENTER) + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test End (would go to Feb 20 Saturday, at max - should succeed) + close_calendar(dash_dcc, dash_dcc.driver) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.END) + send_keys(dash_dcc.driver, Keys.ENTER) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container") + dash_dcc.wait_for_text_to_equal("#output-date", "2021-02-20") + + # Reset and test PageUp (would go to Jan 20, before min) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.PAGE_UP) + send_keys(dash_dcc.driver, Keys.ENTER) + dash_dcc.wait_for_text_to_equal("#output-date", "2021-02-20") + + # Test PageDown (would go to Mar 20, after max) + send_keys(dash_dcc.driver, Keys.PAGE_DOWN) + send_keys(dash_dcc.driver, Keys.ENTER) + dash_dcc.wait_for_text_to_equal("#output-date", "2021-02-20") + + assert dash_dcc.get_logs() == [] + + +def test_a11y008_all_keyboard_keys_respect_disabled_days(dash_dcc): + initial_value = "2021-02-15" + app = create_date_picker_app( + { + "date": initial_value, # Monday + "disabled_days": [ + datetime(2021, 2, 14), # Sunday - week start + datetime(2021, 2, 16), # Tuesday - ArrowRight target + datetime(2021, 2, 20), # Saturday - week end + datetime(2021, 2, 22), # Monday - ArrowDown target + datetime(2021, 1, 15), # PageUp target + datetime(2021, 3, 15), # PageDown target + ], + "initial_visible_month": datetime(2021, 2, 1), + "first_day_of_week": 0, # Sunday + } + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#date-picker") + + # Wait for initial date to be set in output + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test Arrow Right (would go to Feb 16, disabled) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.ARROW_RIGHT) + send_keys(dash_dcc.driver, Keys.ENTER) + # Should remain at Feb 15 since Feb 16 is disabled + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test Arrow Down (would go to Feb 22, disabled) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.ARROW_DOWN) + send_keys(dash_dcc.driver, Keys.ENTER) + # Should remain at Feb 15 since Feb 22 is disabled + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test Home (would go to Feb 14 Sunday, disabled) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.HOME) + send_keys(dash_dcc.driver, Keys.ENTER) + # Should remain at Feb 15 since Feb 14 is disabled + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test End (would go to Feb 20 Saturday, disabled) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.END) + send_keys(dash_dcc.driver, Keys.ENTER) + # Should remain at Feb 15 since Feb 20 is disabled + dash_dcc.wait_for_text_to_equal("#output-date", initial_value) + + # Test PageUp (navigates to previous month, but not a disabled day within that month) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.PAGE_UP) + send_keys(dash_dcc.driver, Keys.ENTER) + output_text = dash_dcc.find_element("#output-date").text + assert output_text != "2021-01-15", "PageUp: Should not select disabled date" + + # Test PageDown (navigates to next month, but not a disabled day within that month) + open_calendar(dash_dcc, date_picker) + send_keys(dash_dcc.driver, Keys.PAGE_DOWN) + send_keys(dash_dcc.driver, Keys.ENTER) + output_text = dash_dcc.find_element("#output-date").text + assert output_text != "2021-03-15", "PageDown: Should not select disabled date" + + assert dash_dcc.get_logs() == [] + + +def test_a11y009_keyboard_space_selects_date(dash_dcc): + app = create_date_picker_app( + { + "date": "2021-01-15", + "initial_visible_month": datetime(2021, 1, 1), + } + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#date-picker") + dash_dcc.wait_for_text_to_equal("#output-date", "2021-01-15") + + open_calendar(dash_dcc, date_picker) + + send_keys(dash_dcc.driver, Keys.ARROW_RIGHT) + assert get_focused_text(dash_dcc.driver) == "16" + + send_keys(dash_dcc.driver, Keys.SPACE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=0.25) + + dash_dcc.wait_for_text_to_equal("#output-date", "2021-01-16") + + assert dash_dcc.get_logs() == [] diff --git a/components/dash-core-components/tests/integration/calendar/test_calendar_props.py b/components/dash-core-components/tests/integration/calendar/test_calendar_props.py index ab6e5a0888..8bb2710f07 100644 --- a/components/dash-core-components/tests/integration/calendar/test_calendar_props.py +++ b/components/dash-core-components/tests/integration/calendar/test_calendar_props.py @@ -1,6 +1,6 @@ import itertools import pytest - +from time import sleep from dash import Dash, Input, Output, html, dcc import dash.testing.wait as wait @@ -20,13 +20,14 @@ def test_cdpr001_date_clearable_true_works(dash_dcc): # DPR start_date, end_date = dash_dcc.select_date_range("dpr", (1, 28)) - close_btn = dash_dcc.wait_for_element('button[aria-label="Clear Dates"]') + close_btn = dash_dcc.wait_for_element("#dpr-wrapper .dash-datepicker-clear") assert ( "1" in start_date and "28" in end_date ), "both start date and end date should match the selected day" close_btn.click() + sleep(0.25) start_date, end_date = dash_dcc.get_date_range("dpr") assert not start_date and not end_date, "both start and end dates should be cleared" @@ -34,7 +35,7 @@ def test_cdpr001_date_clearable_true_works(dash_dcc): selected = dash_dcc.select_date_single("dps", day="1") assert selected, "single date should get a value" - close_btn = dash_dcc.wait_for_element("#dps button") + close_btn = dash_dcc.wait_for_element("#dps-wrapper .dash-datepicker-clear") close_btn.click() (single_date,) = dash_dcc.get_date_range("dps") assert not single_date, "date should be cleared" @@ -49,6 +50,7 @@ def test_cdpr002_updatemodes(dash_dcc): [ dcc.DatePickerRange( id="date-picker-range", + display_format="MM/DD/YYYY", start_date_id="startDate", end_date_id="endDate", start_date_placeholder_text="Select a start date!", diff --git a/components/dash-core-components/tests/integration/calendar/test_date_picker_range.py b/components/dash-core-components/tests/integration/calendar/test_date_picker_range.py index 50709fdd39..ae30ebaf5b 100644 --- a/components/dash-core-components/tests/integration/calendar/test_date_picker_range.py +++ b/components/dash-core-components/tests/integration/calendar/test_date_picker_range.py @@ -1,6 +1,11 @@ from datetime import datetime from dash import Dash, html, dcc +from selenium.common.exceptions import ( + ElementClickInterceptedException, + TimeoutException, +) +from selenium.webdriver.common.keys import Keys def test_dtpr001_initial_month_provided(dash_dcc): @@ -18,17 +23,18 @@ def test_dtpr001_initial_month_provided(dash_dcc): dash_dcc.start_server(app) - date_picker_start = dash_dcc.find_element( - '#dps-initial-month .DateInput_input.DateInput_input_1[placeholder="Start Date"]' - ) - date_picker_start.click() + date_picker = dash_dcc.find_element("#dps-initial-month") + date_picker.click() dash_dcc.wait_for_text_to_equal( - "#dps-initial-month .CalendarMonth.CalendarMonth_1[data-visible=true] strong", - "October 2019", + ".dash-datepicker .dash-dropdown-value", + "October", 1, ) + year_input = dash_dcc.find_element(".dash-datepicker .dash-input-container input") + assert year_input.get_attribute("value") == "2019" + assert dash_dcc.get_logs() == [] @@ -46,16 +52,18 @@ def test_dtpr002_no_initial_month_min_date(dash_dcc): dash_dcc.start_server(app) - date_picker_start = dash_dcc.find_element( - '#dps-initial-month .DateInput_input.DateInput_input_1[placeholder="Start Date"]' - ) - date_picker_start.click() + date_picker = dash_dcc.find_element("#dps-initial-month") + date_picker.click() dash_dcc.wait_for_text_to_equal( - "#dps-initial-month .CalendarMonth.CalendarMonth_1[data-visible=true] strong", - "January 2010", + ".dash-datepicker .dash-dropdown-value", + "January", + 1, ) + year_input = dash_dcc.find_element(".dash-datepicker .dash-input-container input") + assert year_input.get_attribute("value") == "2010" + assert dash_dcc.get_logs() == [] @@ -73,16 +81,18 @@ def test_dtpr003_no_initial_month_no_min_date_start_date(dash_dcc): dash_dcc.start_server(app) - date_picker_start = dash_dcc.find_element( - '#dps-initial-month .DateInput_input.DateInput_input_1[placeholder="Start Date"]' - ) - date_picker_start.click() + date_picker = dash_dcc.find_element("#dps-initial-month") + date_picker.click() dash_dcc.wait_for_text_to_equal( - "#dps-initial-month .CalendarMonth.CalendarMonth_1[data-visible=true] strong", - "August 2019", + ".dash-datepicker .dash-dropdown-value", + "August", + 1, ) + year_input = dash_dcc.find_element(".dash-datepicker .dash-input-container input") + assert year_input.get_attribute("value") == "2019" + assert dash_dcc.get_logs() == [] @@ -92,6 +102,7 @@ def test_dtpr004_max_and_min_dates_are_clickable(dash_dcc): [ dcc.DatePickerRange( id="dps-initial-month", + display_format="MM/DD/YYYY", start_date=datetime(2021, 1, 11), end_date=datetime(2021, 1, 19), max_date_allowed=datetime(2021, 1, 20), @@ -104,15 +115,11 @@ def test_dtpr004_max_and_min_dates_are_clickable(dash_dcc): dash_dcc.select_date_range("dps-initial-month", (10, 20)) - dash_dcc.wait_for_text_to_equal( - '#dps-initial-month .DateInput_input.DateInput_input_1[placeholder="Start Date"]', - "01/10/2021", - ) + start_date = dash_dcc.find_element(".dash-datepicker-start-date") + assert start_date.get_attribute("value") == "01/10/2021" - dash_dcc.wait_for_text_to_equal( - '#dps-initial-month .DateInput_input.DateInput_input_1[placeholder="End Date"]', - "01/20/2021", - ) + end_date = dash_dcc.find_element(".dash-datepicker-end-date") + assert end_date.get_attribute("value") == "01/20/2021" assert dash_dcc.get_logs() == [] @@ -130,20 +137,20 @@ def test_dtpr005_disabled_days_arent_clickable(dash_dcc): disabled_days=[datetime(2021, 1, 10), datetime(2021, 1, 11)], ), ], - style={ - "width": "10%", - "display": "inline-block", - "marginLeft": 10, - "marginRight": 10, - "marginBottom": 10, - }, + style={"width": "50%"}, ) dash_dcc.start_server(app) - date = dash_dcc.find_element("#dpr input") + date = dash_dcc.find_element("#dpr") assert not date.get_attribute("value") - assert not any( + + # Try to click disabled days + date.click() + try: dash_dcc.select_date_range("dpr", day_range=(10, 11)) - ), "Disabled days should not be clickable" + date.click() + except (ElementClickInterceptedException, TimeoutException): + pass # Expected - dates are disabled with pointer-events: none + assert all( dash_dcc.select_date_range("dpr", day_range=(1, 2)) ), "Other days should be clickable" @@ -151,3 +158,219 @@ def test_dtpr005_disabled_days_arent_clickable(dash_dcc): # open datepicker to take snapshot date.click() dash_dcc.percy_snapshot("dtpr005 - disabled days") + + +def test_dtpr006_minimum_nights_forward_selection(dash_dcc): + app = Dash(__name__) + app.layout = html.Div( + [ + html.Label("Booking Date"), + dcc.DatePickerRange( + id="dpr-min-nights", + min_date_allowed=datetime(2021, 1, 1), + max_date_allowed=datetime(2021, 1, 31), + initial_visible_month=datetime(2021, 1, 1), + minimum_nights=3, + display_format="MM/DD/YYYY", + ), + ], + style={"width": "50%"}, + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#dpr-min-nights") + + # Try to select dates that violate minimum_nights (Jan 10 -> Jan 11) + # This should fail because minimum_nights=3 requires at least 3 days between + date_picker.click() + try: + dash_dcc.select_date_range("dpr-min-nights", day_range=(10, 11)) + date_picker.click() + except (ElementClickInterceptedException, TimeoutException): + pass # Expected - day 11 is disabled with pointer-events: none + + # Try another invalid range (Jan 10 -> Jan 12) + date_picker.click() + try: + dash_dcc.select_date_range("dpr-min-nights", day_range=(10, 12)) + date_picker.click() + except (ElementClickInterceptedException, TimeoutException): + pass # Expected - day 12 is also disabled + + # Now select a valid range that respects minimum_nights (Jan 10 -> Jan 13) + # This should succeed because there are 3 days between them + result = dash_dcc.select_date_range("dpr-min-nights", day_range=(10, 13)) + assert result == ("01/10/2021", "01/13/2021") + + assert dash_dcc.get_logs() == [] + + +def test_dtpr007_minimum_nights_backward_selection(dash_dcc): + app = Dash(__name__) + app.layout = html.Div( + [ + html.Label("Booking Date"), + dcc.DatePickerRange( + id="dpr-min-nights-backward", + min_date_allowed=datetime(2021, 1, 1), + max_date_allowed=datetime(2021, 1, 31), + initial_visible_month=datetime(2021, 1, 1), + minimum_nights=2, + display_format="MM/DD/YYYY", + ), + ], + style={"width": "50%"}, + ) + dash_dcc.start_server(app) + + date_picker = dash_dcc.find_element("#dpr-min-nights-backward") + + # Try to select dates that violate minimum_nights backward (Jan 15 -> Jan 14) + date_picker.click() + try: + dash_dcc.select_date_range("dpr-min-nights-backward", day_range=(15, 14)) + date_picker.click() + except (ElementClickInterceptedException, TimeoutException): + pass # Expected - day 14 is disabled with pointer-events: none + + # Now select a valid backward range that respects minimum_nights (Jan 15 -> Jan 13) + # This should succeed because there are 2 days between them + # When clicking 15 then 13, the component normalizes to start=13, end=15 + result = dash_dcc.select_date_range("dpr-min-nights-backward", day_range=(15, 13)) + assert result == ("01/13/2021", "01/15/2021") + + assert dash_dcc.get_logs() == [] + + +def test_dtpr008_input_click_opens_but_keeps_focus(dash_dcc): + """Test that clicking either input opens the calendar but doesn't close it and maintains focus.""" + import time + + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerRange( + id="dpr", + start_date="2025-01-10", + end_date="2025-01-15", + display_format="MM/DD/YYYY", + ), + ] + ) + + dash_dcc.start_server(app) + + start_input = dash_dcc.find_element(".dash-datepicker-start-date") + end_input = dash_dcc.find_element(".dash-datepicker-end-date") + + # Initially, calendar should be closed + assert not dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should be closed initially" + + # Click on the start input + start_input.click() + + # Calendar should now be open + dash_dcc.wait_for_element(".dash-datepicker-calendar") + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should open after clicking start input" + + # Start input should still have focus + active_element = dash_dcc.driver.switch_to.active_element + assert "dash-datepicker-start-date" in active_element.get_attribute( + "class" + ), "Start input should maintain focus" + + # Click on the end input (switching between inputs) + end_input.click() + + # Calendar should STILL be open + time.sleep(0.2) # Give it a moment to potentially close + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should remain open when clicking end input" + + # End input should now have focus + active_element = dash_dcc.driver.switch_to.active_element + assert "dash-datepicker-end-date" in active_element.get_attribute( + "class" + ), "End input should have focus after clicking it" + + # Click on the end input again + end_input.click() + + # Calendar should STILL be open (not toggled closed) + time.sleep(0.2) + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should remain open after clicking end input again" + + # User should be able to type in end input without popup closing + # Type a date in a different month/year (August 2026) + # Select all text using keyboard (cross-platform approach) + end_input.send_keys(Keys.HOME) + end_input.send_keys(Keys.SHIFT + Keys.END) + end_input.send_keys("08/20/2026") + + # Calendar should still be open + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should remain open while typing in end input" + + # Press Tab to blur and trigger date parsing + end_input.send_keys(Keys.TAB) + time.sleep(0.3) # Give calendar time to update + + # Calendar should still be open after blur + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should remain open after blur" + + # Verify the input value was parsed correctly + assert ( + end_input.get_attribute("value") == "08/20/2026" + ), f"End input should show 08/20/2026, but shows: {end_input.get_attribute('value')}" + + # Calendar should now show August 2026 + month_dropdown = dash_dcc.find_element(".dash-datepicker .dash-dropdown-value") + assert ( + month_dropdown.text == "August" + ), f"Calendar should show August, but shows: {month_dropdown.text}" + + year_input = dash_dcc.find_element(".dash-datepicker .dash-input-container input") + assert ( + year_input.get_attribute("value") == "2026" + ), f"Calendar should show 2026, but shows: {year_input.get_attribute('value')}" + + # Click on start input and type a date in yet another month (March 2026) + start_input.click() + # Select all text using keyboard (cross-platform approach) + start_input.send_keys(Keys.HOME) + start_input.send_keys(Keys.SHIFT + Keys.END + Keys.DELETE) + start_input.send_keys("03/05/2026") + start_input.send_keys(Keys.ARROW_DOWN) + time.sleep(0.3) + + # Calendar should still be open and now show March 2026 + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should remain open while typing in start input" + + # Verify the input value was parsed correctly + assert ( + start_input.get_attribute("value") == "03/05/2026" + ), f"Start input should show 03/05/2026, but shows: {start_input.get_attribute('value')}" + + month_dropdown = dash_dcc.find_element(".dash-datepicker .dash-dropdown-value") + assert ( + month_dropdown.text == "March" + ), f"Calendar should show March, but shows: {month_dropdown.text}" + + year_input = dash_dcc.find_element(".dash-datepicker .dash-input-container input") + assert ( + year_input.get_attribute("value") == "2026" + ), f"Calendar should show 2026, but shows: {year_input.get_attribute('value')}" + + assert dash_dcc.get_logs() == [] diff --git a/components/dash-core-components/tests/integration/calendar/test_date_picker_single.py b/components/dash-core-components/tests/integration/calendar/test_date_picker_single.py index d13194511f..b1a93c6ede 100644 --- a/components/dash-core-components/tests/integration/calendar/test_date_picker_single.py +++ b/components/dash-core-components/tests/integration/calendar/test_date_picker_single.py @@ -3,6 +3,8 @@ import time import pytest +from selenium.webdriver.common.keys import Keys +from selenium.common.exceptions import ElementClickInterceptedException from dash import Dash, Input, Output, html, dcc, no_update @@ -22,15 +24,11 @@ def test_dtps001_simple_click(dash_dcc): ), ], style={ - "width": "10%", - "display": "inline-block", - "marginLeft": 10, - "marginRight": 10, - "marginBottom": 10, + "width": "50%", }, ) dash_dcc.start_server(app) - date = dash_dcc.find_element("#dps input") + date = dash_dcc.find_element("#dps") assert not date.get_attribute("value") assert dash_dcc.select_date_single( "dps", index=3 @@ -55,21 +53,19 @@ def test_dtps010_local_and_session_persistence(dash_dcc): dash_dcc.start_server(app) - assert not dash_dcc.find_element("#dps-local input").get_attribute( + assert not dash_dcc.find_element("#dps-local").get_attribute( "value" - ) and not dash_dcc.find_element("#dps-session input").get_attribute( + ) and not dash_dcc.find_element("#dps-session").get_attribute( "value" ), "component should contain no initial date" for idx in range(3): - local = dash_dcc.select_date_single("dps-local", index=idx) - session = dash_dcc.select_date_single("dps-session", index=idx) + local = dash_dcc.select_date_single("dps-local", day=idx) + session = dash_dcc.select_date_single("dps-session", day=idx) dash_dcc.wait_for_page() - assert ( - dash_dcc.find_element("#dps-local input").get_attribute("value") == local - and dash_dcc.find_element("#dps-session input").get_attribute("value") - == session - ), "the date value should be consistent after refresh" + time.sleep(0.5) + assert dash_dcc.find_element("#dps-local").get_attribute("value") == local + assert dash_dcc.find_element("#dps-session").get_attribute("value") == session assert dash_dcc.get_logs() == [] @@ -117,40 +113,98 @@ def cb(clicks): switch.click() assert dash_dcc.wait_for_text_to_equal("#out", "switched") switch.click() - assert ( - dash_dcc.find_element("#dps-memory input").get_attribute("value") == memorized - ) - switched = dash_dcc.find_element("#dps-none input").get_attribute("value") + assert dash_dcc.find_element("#dps-memory").get_attribute("value") == memorized + switched = dash_dcc.find_element("#dps-none").get_attribute("value") assert switched != amnesiaed and switched == "" assert dash_dcc.get_logs() == [] -def test_dtps012_initial_month(dash_dcc): +def test_dtps012_initial_visible_month(dash_dcc): app = Dash(__name__) app.layout = html.Div( [ dcc.DatePickerSingle( - id="dps-initial-month", - min_date_allowed=datetime(2010, 1, 1), - max_date_allowed=datetime(2099, 12, 31), + id="dps", + date="2020-06-15", + initial_visible_month=datetime(2010, 1, 1), ) ] ) dash_dcc.start_server(app) - date_picker = dash_dcc.find_element("#dps-initial-month") + date_picker = dash_dcc.find_element("#dps") date_picker.click() - dash_dcc.wait_for_text_to_equal( - "#dps-initial-month .CalendarMonth.CalendarMonth_1[data-visible=true] strong", - "January 2010", + + # Wait for calendar to open + dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + + # Check that calendar shows January 2010 (initial_visible_month), not June 2020 (date) + month_dropdown = dash_dcc.find_element(".dash-datepicker-controls .dash-dropdown") + year_input = dash_dcc.find_element(".dash-datepicker-controls input") + + assert "January" in month_dropdown.text, "Calendar should show January" + assert year_input.get_attribute("value") == "2010", "Calendar should show year 2010" + + assert dash_dcc.get_logs() == [] + + +def test_dtps013_min_max_date_allowed(dash_dcc): + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle( + id="dps", + min_date_allowed=datetime(2021, 1, 5), + max_date_allowed=datetime(2021, 1, 25), + initial_visible_month=datetime(2021, 1, 1), + ), + html.Div(id="output"), + ] ) + @app.callback( + Output("output", "children"), + Input("dps", "date"), + ) + def update_output(date): + return f"Selected: {date}" + + dash_dcc.start_server(app) + + # Initially no date selected + dash_dcc.wait_for_text_to_equal("#output", "Selected: None") + + # Try to select date before min_date_allowed - should not update + try: + dash_dcc.select_date_single("dps", day=3) + except ElementClickInterceptedException: + pass # Expected - date is disabled with pointer-events: none + dash_dcc.wait_for_text_to_equal("#output", "Selected: None") + # Close calendar + date_input = dash_dcc.find_element("#dps") + date_input.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + # Try to select date after max_date_allowed - should not update + try: + dash_dcc.select_date_single("dps", day=28) + except ElementClickInterceptedException: + pass # Expected - date is disabled with pointer-events: none + dash_dcc.wait_for_text_to_equal("#output", "Selected: None") + # Close calendar + date_input.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + # Select date within allowed range - should update + dash_dcc.select_date_single("dps", day=10) + dash_dcc.wait_for_text_to_equal("#output", "Selected: 2021-01-10") + assert dash_dcc.get_logs() == [] -def test_dtps013_disabled_days_arent_clickable(dash_dcc): +def test_dtps014_disabled_days_arent_clickable(dash_dcc): app = Dash(__name__) app.layout = html.Div( [ @@ -163,25 +217,30 @@ def test_dtps013_disabled_days_arent_clickable(dash_dcc): disabled_days=[datetime(2021, 1, 10)], ), ], - style={ - "width": "10%", - "display": "inline-block", - "marginLeft": 10, - "marginRight": 10, - "marginBottom": 10, - }, + style={"width": "50%"}, ) dash_dcc.start_server(app) - date = dash_dcc.find_element("#dps input") + date = dash_dcc.find_element("#dps") + assert not date.get_attribute("value") + + # Try to click disabled day - should fail or be intercepted + # (Selenium may throw ElementClickInterceptedException due to pointer-events: none) + try: + result = dash_dcc.select_date_single("dps", day=10) + assert not result, "Disabled days should not be clickable" + except ElementClickInterceptedException: + pass # Expected - date is disabled with pointer-events: none + + # Verify date wasn't selected assert not date.get_attribute("value") - assert not dash_dcc.select_date_single( - "dps", day=10 - ), "Disabled days should not be clickable" + + # Close calendar + date.send_keys(Keys.ESCAPE) assert dash_dcc.select_date_single("dps", day=1), "Other days should be clickable" # open datepicker to take snapshot date.click() - dash_dcc.percy_snapshot("dtps013 - disabled days") + dash_dcc.percy_snapshot("dtps014 - disabled days") def test_dtps0014_disabed_days_timeout(dash_dcc): @@ -215,5 +274,348 @@ def test_dtps0014_disabed_days_timeout(dash_dcc): date.click() assert time.time() - start_time < 5 - dash_dcc.wait_for_element(".SingleDatePicker_picker", timeout=5) + dash_dcc.wait_for_element(".dash-datepicker-calendar-container", timeout=5) + assert dash_dcc.get_logs() == [] + + +def test_dtps020_renders_date_picker(dash_dcc): + """Test that DatePickerSingle renders correctly.""" + app = Dash(__name__) + app.layout = html.Div([dcc.DatePickerSingle(id="dps")]) + + dash_dcc.start_server(app) + + # Check that the datepicker element exists + datepicker = dash_dcc.find_element(".dash-datepicker") + assert datepicker is not None, "DatePickerSingle should render" + + # Check that input exists + input_element = dash_dcc.find_element(".dash-datepicker-input") + assert input_element is not None, "DatePickerSingle should have an input element" + + assert dash_dcc.get_logs() == [] + + +def test_dtps022_custom_display_format(dash_dcc): + """Test that dates display in custom format.""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle( + id="dps", + display_format="MM/DD/YYYY", + date="2025-10-17", + ), + ] + ) + + dash_dcc.start_server(app) + + # Check that input shows the date in custom format + input_element = dash_dcc.find_element(".dash-datepicker-input") + assert ( + input_element.get_attribute("value") == "10/17/2025" + ), "Date should display in MM/DD/YYYY format" + + assert dash_dcc.get_logs() == [] + + +def test_dtps023_default_display_format(dash_dcc): + """Test that dates default to YYYY-MM-DD format and can be changed via callback.""" + app = Dash(__name__) + app.layout = html.Div( + [ + html.Button("Change Format", id="btn"), + dcc.DatePickerSingle( + id="dps", + date="2025-01-10", + ), + ] + ) + + @app.callback( + Output("dps", "display_format"), + Input("btn", "n_clicks"), + prevent_initial_call=True, + ) + def change_format(n_clicks): + return "DD/MM/YYYY" + + dash_dcc.start_server(app) + + # Check that input shows the date in default YYYY-MM-DD format + input_element = dash_dcc.find_element(".dash-datepicker-input") + assert ( + input_element.get_attribute("value") == "2025-01-10" + ), "Date should display in default YYYY-MM-DD format" + + # Click button to change format + btn = dash_dcc.find_element("#btn") + btn.click() + + # Wait for format to change and verify new format + dash_dcc.wait_for_text_to_equal(".dash-datepicker-input", "10/01/2025", timeout=4) + + assert dash_dcc.get_logs() == [] + + +def test_dtps023b_input_validation_and_blur(dash_dcc): + """Test that typing into the input and blurring validates and reformats the date.""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle( + id="dps", + display_format="MM/DD/YYYY", + ), + html.Div(id="output"), + ] + ) + + @app.callback( + Output("output", "children"), + Input("dps", "date"), + ) + def update_output(date): + return f"Date: {date}" + + dash_dcc.start_server(app) + + # Initially no date + dash_dcc.wait_for_text_to_equal("#output", "Date: None") + + input_element = dash_dcc.find_element("#dps") + + # Type a valid date in the custom format + input_element.clear() + input_element.send_keys("01/15/2025") + input_element.send_keys(Keys.TAB) # Blur the input + + # Should parse and set the date + dash_dcc.wait_for_text_to_equal("#output", "Date: 2025-01-15") + + # Input should still show in custom format after blur + assert ( + input_element.get_attribute("value") == "01/15/2025" + ), "Input should maintain custom format after blur" + + # Type an invalid date + input_element.clear() + input_element.send_keys("invalid") + input_element.send_keys(Keys.TAB) # Blur the input + + # Should revert to previous valid date + dash_dcc.wait_for_text_to_equal("#output", "Date: 2025-01-15") + assert ( + input_element.get_attribute("value") == "01/15/2025" + ), "Invalid input should revert to previous valid date in display format on blur" + + assert dash_dcc.get_logs() == [] + + +def test_dtps024_rtl_directionality(dash_dcc): + """Test that is_RTL prop applies correct directionality to input and calendar.""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle(id="dps-rtl", is_RTL=True), + dcc.DatePickerSingle(id="dps-ltr", is_RTL=False), + dcc.DatePickerSingle(id="dps-default"), + ] + ) + + dash_dcc.start_server(app) + + # Wait for components to render and check dir attribute on input elements + rtl_input = dash_dcc.wait_for_element(".dash-datepicker-input") + assert ( + rtl_input.get_attribute("dir") == "rtl" + ), "is_RTL=True should set dir='rtl' on input element" + + all_inputs = dash_dcc.find_elements(".dash-datepicker-input") + assert len(all_inputs) == 3, "Should have 3 date picker inputs" + + ltr_input = all_inputs[1] + assert ( + ltr_input.get_attribute("dir") == "ltr" + ), "is_RTL=False should set dir='ltr' on input element" + + default_input = all_inputs[2] + assert ( + default_input.get_attribute("dir") == "ltr" + ), "Default (no is_RTL) should set dir='ltr' on input element" + + # Test calendar directionality when opened + all_wrappers = dash_dcc.find_elements(".dash-datepicker-input-wrapper") + all_wrappers[0].click() + + rtl_calendar = dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + assert ( + rtl_calendar.get_attribute("dir") == "rtl" + ), "is_RTL=True should set dir='rtl' on calendar container" + + rtl_input.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + all_wrappers[1].click() + ltr_calendar = dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + assert ( + ltr_calendar.get_attribute("dir") == "ltr" + ), "is_RTL=False should set dir='ltr' on calendar container" + + ltr_input.send_keys(Keys.ESCAPE) + dash_dcc.wait_for_no_elements(".dash-datepicker-calendar-container", timeout=2) + + all_wrappers[2].click() + default_calendar = dash_dcc.wait_for_element(".dash-datepicker-calendar-container") + assert ( + default_calendar.get_attribute("dir") == "ltr" + ), "Default (no is_RTL) should set dir='ltr' on calendar container" + + assert dash_dcc.get_logs() == [] + + +def test_dtps025_typing_disabled_day_should_not_trigger_callback(dash_dcc): + """Test that manually typing a disabled day into the input does not set that date in callback.""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle( + id="dps", + date="2025-01-10", + display_format="MM/DD/YYYY", + disabled_days=[datetime(2025, 1, 15)], + ), + html.Div(id="output"), + ] + ) + + @app.callback( + Output("output", "children"), + Input("dps", "date"), + ) + def update_output(date): + return f"Date: {date}" + + dash_dcc.start_server(app) + + # Initially has a valid date + dash_dcc.wait_for_text_to_equal("#output", "Date: 2025-01-10") + + input_element = dash_dcc.find_element("#dps") + + # Verify initial display format is respected + assert ( + input_element.get_attribute("value") == "01/10/2025" + ), "Initial date should be displayed in custom format" + + # Type a disabled date (in the correct display format) + input_element.clear() + input_element.send_keys("01/15/2025") + input_element.send_keys(Keys.TAB) # Blur the input + + # The callback should NOT receive the disabled date + time.sleep(0.5) # Give it time to potentially (incorrectly) update + output_text = dash_dcc.find_element("#output").text + assert ( + output_text != "Date: 2025-01-15" + ), f"Typing a disabled day should not trigger callback with that date, but got: {output_text}" + + # The input should revert to the previous valid date in the correct display format + assert ( + input_element.get_attribute("value") == "01/10/2025" + ), f"Input should revert to previous valid date in display format (MM/DD/YYYY), but got: {input_element.get_attribute('value')}" + + assert dash_dcc.get_logs() == [] + + +def test_dtps026_input_click_opens_but_keeps_focus(dash_dcc): + """Test that clicking the input opens the calendar but doesn't close it and maintains focus.""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle( + id="dps", + date="2025-01-10", + display_format="MM/DD/YYYY", + ), + ] + ) + + dash_dcc.start_server(app) + + input_element = dash_dcc.find_element("#dps") + + # Initially, calendar should be closed + assert not dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should be closed initially" + + # Click on the input + input_element.click() + + # Calendar should now be open + dash_dcc.wait_for_element(".dash-datepicker-calendar") + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should open after clicking input" + + # Input should still have focus (user should be able to type) + active_element = dash_dcc.driver.switch_to.active_element + assert ( + active_element.get_attribute("id") == "dps" + ), "Input should maintain focus after opening calendar" + + # Click on the input again + input_element.click() + + # Calendar should STILL be open (not toggled closed) + time.sleep(0.2) # Give it a moment to potentially close + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should remain open after clicking input again" + + # Input should still have focus + active_element = dash_dcc.driver.switch_to.active_element + assert ( + active_element.get_attribute("id") == "dps" + ), "Input should maintain focus after second click" + + # User should be able to type without popup closing + # Type a date in a different month/year (June 2026) + # Select all text using keyboard (cross-platform approach) + input_element.send_keys(Keys.HOME) + input_element.send_keys(Keys.SHIFT + Keys.END) + input_element.send_keys("06/15/2026") + + # Calendar should still be open + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should remain open while typing" + + # Press Tab to blur the input and trigger date parsing + input_element.send_keys(Keys.TAB) + time.sleep(0.3) # Give calendar time to update + + # Calendar should still be open after blur + assert dash_dcc.find_elements( + ".dash-datepicker-calendar" + ), "Calendar should remain open after blur" + + # Verify the input value was parsed correctly + assert ( + input_element.get_attribute("value") == "06/15/2026" + ), f"Input should show 06/15/2026, but shows: {input_element.get_attribute('value')}" + + # Calendar should now show June 2026 + month_dropdown = dash_dcc.find_element(".dash-datepicker .dash-dropdown-value") + assert ( + month_dropdown.text == "June" + ), f"Calendar should show June, but shows: {month_dropdown.text}" + + year_input = dash_dcc.find_element(".dash-datepicker .dash-input-container input") + assert ( + year_input.get_attribute("value") == "2026" + ), f"Calendar should show 2026, but shows: {year_input.get_attribute('value')}" + assert dash_dcc.get_logs() == [] diff --git a/components/dash-core-components/tests/integration/calendar/test_multi_month_selection.py b/components/dash-core-components/tests/integration/calendar/test_multi_month_selection.py new file mode 100644 index 0000000000..6afe8a5c13 --- /dev/null +++ b/components/dash-core-components/tests/integration/calendar/test_multi_month_selection.py @@ -0,0 +1,306 @@ +from datetime import datetime +from selenium.webdriver.common.action_chains import ActionChains + +from dash import Dash, Input, Output, html, dcc + + +def test_dtps_multi_month_click_second_month(dash_dcc): + """Test clicking a date in the second month with number_of_months_shown=2""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerSingle( + id="dps", + initial_visible_month=datetime(2021, 1, 1), + number_of_months_shown=2, + stay_open_on_select=True, + ), + html.Div(id="output"), + ] + ) + + @app.callback(Output("output", "children"), Input("dps", "date")) + def update_output(date): + return date or "No date selected" + + dash_dcc.start_server(app) + + # Click the date picker to open it + date_picker = dash_dcc.find_element("#dps") + date_picker.click() + + dash_dcc._wait_until_day_is_clickable() + + # Get all visible dates across both months + days = dash_dcc.find_elements(dash_dcc.date_picker_day_locator) + + # Find a date in the second month (February 2021) + # We're looking for day "15" in the second month + second_month_days = [ + day + for day in days + if day.text == "15" + and "dash-datepicker-calendar-date-outside" not in day.get_attribute("class") + ] + + # There should be two "15"s visible (Jan 15 and Feb 15) + # We want the second one (Feb 15) + assert len(second_month_days) >= 1, "Should find at least one day 15" + + # Click on a date in the second visible month + if len(second_month_days) > 1: + second_month_days[1].click() + expected_date = "2021-02-15" + else: + # Fallback: just click the first one + second_month_days[0].click() + expected_date = "2021-01-15" + + # Check the output + output = dash_dcc.find_element("#output") + assert output.text == expected_date, f"Expected {expected_date}, got {output.text}" + + +def test_dtpr_multi_month_drag_in_second_month(dash_dcc): + """Test drag selection entirely within the second month with number_of_months_shown=2""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerRange( + id="dpr", + initial_visible_month=datetime(2021, 1, 1), + number_of_months_shown=2, + ), + html.Div(id="output-start"), + html.Div(id="output-end"), + ] + ) + + @app.callback( + Output("output-start", "children"), + Output("output-end", "children"), + Input("dpr", "start_date"), + Input("dpr", "end_date"), + ) + def update_output(start_date, end_date): + return start_date or "No start", end_date or "No end" + + dash_dcc.start_server(app) + + # Click to open the calendar + date_picker = dash_dcc.find_element("#dpr") + date_picker.click() + + dash_dcc._wait_until_day_is_clickable() + + # Get all visible dates + days = dash_dcc.find_elements(dash_dcc.date_picker_day_locator) + + # Find all day "10"s and "17"s (both should appear in Jan and Feb) + all_10s = [ + day + for day in days + if day.text == "10" + and "dash-datepicker-calendar-date-outside" not in day.get_attribute("class") + ] + all_17s = [ + day + for day in days + if day.text == "17" + and "dash-datepicker-calendar-date-outside" not in day.get_attribute("class") + ] + + # Use the last occurrence of each (should be February) + feb_10 = all_10s[-1] if len(all_10s) > 1 else all_10s[0] + feb_17 = all_17s[-1] if len(all_17s) > 1 else all_17s[0] + + # Perform drag operation: mouse down on Feb 10, drag to Feb 17, mouse up + actions = ActionChains(dash_dcc.driver) + actions.click_and_hold(feb_10).move_to_element(feb_17).release().perform() + + # Wait for the callback to fire + dash_dcc.wait_for_text_to_equal("#output-start", "2021-02-10", timeout=2) + + # Check the outputs + output_start = dash_dcc.find_element("#output-start") + output_end = dash_dcc.find_element("#output-end") + + assert ( + output_start.text == "2021-02-10" + ), f"Expected 2021-02-10 as start, got {output_start.text}" + assert ( + output_end.text == "2021-02-17" + ), f"Expected 2021-02-17 as end, got {output_end.text}" + + +def test_dtpr_multi_month_click_in_second_month(dash_dcc): + """Test click selection entirely within the second month with number_of_months_shown=2 + This should produce the same result as the drag test above""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerRange( + id="dpr", + initial_visible_month=datetime(2021, 1, 1), + number_of_months_shown=2, + stay_open_on_select=True, + ), + html.Div(id="output-start"), + html.Div(id="output-end"), + ] + ) + + @app.callback( + Output("output-start", "children"), + Output("output-end", "children"), + Input("dpr", "start_date"), + Input("dpr", "end_date"), + ) + def update_output(start_date, end_date): + return start_date or "No start", end_date or "No end" + + dash_dcc.start_server(app) + + # Open calendar + dash_dcc.find_element("#dpr").click() + dash_dcc._wait_until_day_is_clickable() + + # Find and click Feb 10 and Feb 17 + days = dash_dcc.find_elements(dash_dcc.date_picker_day_locator) + all_10s = [ + d + for d in days + if d.text == "10" + and "dash-datepicker-calendar-date-outside" not in d.get_attribute("class") + ] + all_17s = [ + d + for d in days + if d.text == "17" + and "dash-datepicker-calendar-date-outside" not in d.get_attribute("class") + ] + + all_10s[-1].click() # Feb 10 (last occurrence) + all_17s[-1].click() # Feb 17 (last occurrence) + + # Verify output + dash_dcc.wait_for_text_to_equal("#output-start", "2021-02-10", timeout=2) + dash_dcc.wait_for_text_to_equal("#output-end", "2021-02-17", timeout=2) + + +def test_dtpr_cross_month_drag_selection(dash_dcc): + """Test drag selection from 15th of first month (Jan) to 15th of second month (Feb)""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerRange( + id="dpr", + initial_visible_month=datetime(2021, 1, 1), + number_of_months_shown=2, + ), + html.Div(id="output-start"), + html.Div(id="output-end"), + ] + ) + + @app.callback( + Output("output-start", "children"), + Output("output-end", "children"), + Input("dpr", "start_date"), + Input("dpr", "end_date"), + ) + def update_output(start_date, end_date): + return start_date or "No start", end_date or "No end" + + dash_dcc.start_server(app) + + # Click to open the calendar + date_picker = dash_dcc.find_element("#dpr") + date_picker.click() + + dash_dcc._wait_until_day_is_clickable() + + # Get all visible dates + days = dash_dcc.find_elements(dash_dcc.date_picker_day_locator) + + # Find all day "15"s (both Jan 15 and Feb 15) + all_15s = [ + day + for day in days + if day.text == "15" + and "dash-datepicker-calendar-date-outside" not in day.get_attribute("class") + ] + + # Should have at least 2 instances of day 15 (Jan and Feb) + assert len(all_15s) >= 2, "Should find at least two day 15s (Jan and Feb)" + + # First occurrence is Jan 15, second is Feb 15 + jan_15 = all_15s[0] + feb_15 = all_15s[1] + + # Perform drag operation: mouse down on Jan 15, drag to Feb 15, mouse up + actions = ActionChains(dash_dcc.driver) + actions.click_and_hold(jan_15).move_to_element(feb_15).release().perform() + + # Wait for the callback to fire + dash_dcc.wait_for_text_to_equal("#output-start", "2021-01-15", timeout=2) + + # Check the outputs + output_start = dash_dcc.find_element("#output-start") + output_end = dash_dcc.find_element("#output-end") + + assert ( + output_start.text == "2021-01-15" + ), f"Expected 2021-01-15 as start, got {output_start.text}" + assert ( + output_end.text == "2021-02-15" + ), f"Expected 2021-02-15 as end, got {output_end.text}" + + +def test_dtpr_cross_month_click_selection(dash_dcc): + """Test click selection from 15th of first month (Jan) to 15th of second month (Feb) + This should produce the same result as the drag test above""" + app = Dash(__name__) + app.layout = html.Div( + [ + dcc.DatePickerRange( + id="dpr", + initial_visible_month=datetime(2021, 1, 1), + number_of_months_shown=2, + stay_open_on_select=True, + ), + html.Div(id="output-start"), + html.Div(id="output-end"), + ] + ) + + @app.callback( + Output("output-start", "children"), + Output("output-end", "children"), + Input("dpr", "start_date"), + Input("dpr", "end_date"), + ) + def update_output(start_date, end_date): + return start_date or "No start", end_date or "No end" + + dash_dcc.start_server(app) + + # Open calendar + dash_dcc.find_element("#dpr").click() + dash_dcc._wait_until_day_is_clickable() + + # Find and click Jan 15 and Feb 15 + days = dash_dcc.find_elements(dash_dcc.date_picker_day_locator) + all_15s = [ + d + for d in days + if d.text == "15" + and "dash-datepicker-calendar-date-outside" not in d.get_attribute("class") + ] + + all_15s[0].click() # Jan 15 (first occurrence) + all_15s[1].click() # Feb 15 (second occurrence) + + # Verify output + dash_dcc.wait_for_text_to_equal("#output-start", "2021-01-15", timeout=2) + dash_dcc.wait_for_text_to_equal("#output-end", "2021-02-15", timeout=2) diff --git a/components/dash-core-components/tests/unit/.eslintrc.js b/components/dash-core-components/tests/unit/.eslintrc.js new file mode 100644 index 0000000000..90eed0cc15 --- /dev/null +++ b/components/dash-core-components/tests/unit/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'no-magic-numbers': 'off', + }, +}; diff --git a/components/dash-core-components/tests/unit/calendar/Calendar.test.tsx b/components/dash-core-components/tests/unit/calendar/Calendar.test.tsx new file mode 100644 index 0000000000..4dde6da091 --- /dev/null +++ b/components/dash-core-components/tests/unit/calendar/Calendar.test.tsx @@ -0,0 +1,280 @@ +import React from 'react'; +import {render, waitFor, act} from '@testing-library/react'; +import Calendar from '../../../src/utils/calendar/Calendar'; +import {CalendarDirection} from '../../../src/types'; + +// Mock LoadingElement to avoid Dash context issues in tests +jest.mock('../../../src/utils/_LoadingElement', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const React = require('react'); + return function LoadingElement({ + children, + }: { + children: (props: any) => React.ReactNode; + }) { + return children({}); + }; +}); + +// Helper to count cells with a specific CSS class +const countCellsWithClass = ( + container: HTMLElement, + className: string +): number => { + const allCells = container.querySelectorAll('td'); + return Array.from(allCells).filter(td => td.classList.contains(className)) + .length; +}; + +describe('Calendar', () => { + let mockOnSelectionChange: jest.Mock; + + beforeEach(() => { + mockOnSelectionChange = jest.fn(); + }); + + it('renders a calendar', () => { + const {container} = render( + + ); + + const calendarWrapper = container.querySelector( + '.dash-datepicker-calendar-wrapper' + ); + expect(calendarWrapper).toBeInTheDocument(); + }); + + it('marks disabled dates correctly', () => { + const disabledDates = [new Date(2025, 0, 10), new Date(2025, 0, 15)]; + + const {container} = render( + + ); + + expect( + countCellsWithClass( + container, + 'dash-datepicker-calendar-date-disabled' + ) + ).toBeGreaterThan(0); + }); + + it('marks selected dates from selectionStart and selectionEnd', () => { + const {container} = render( + + ); + + // Should have 2 selected days (only the start and end dates, not the dates in between) + expect( + countCellsWithClass( + container, + 'dash-datepicker-calendar-date-selected' + ) + ).toBe(2); + }); + + it('marks highlighted dates from highlightStart and highlightEnd', () => { + const {container} = render( + + ); + + // Should have 6 highlighted days (Jan 5-10 inclusive) + expect( + countCellsWithClass( + container, + 'dash-datepicker-calendar-date-highlighted' + ) + ).toBe(6); + }); + + it('handles single date selection', () => { + const {container} = render( + + ); + + // Should have 1 selected day + expect( + countCellsWithClass( + container, + 'dash-datepicker-calendar-date-selected' + ) + ).toBe(1); + }); + + it.each([ + { + description: 'default format (YYYY)', + date: new Date(1997, 4, 10), + monthFormat: undefined, + expectedYear: '1997', + expectedMonth: /May/, + }, + { + description: 'YY format', + date: new Date(1997, 4, 10), + monthFormat: 'MMMM YY', + expectedYear: '97', + expectedMonth: /May/, + }, + { + description: 'YYYY format with January', + date: new Date(2023, 0, 15), + monthFormat: undefined, + expectedYear: '2023', + expectedMonth: /January/, + }, + ])( + 'formats year and month according to month_format: $description', + ({date, monthFormat, expectedYear, expectedMonth}) => { + const {container} = render( + + ); + + const yearInput = container.querySelector( + '.dash-input-element' + ) as HTMLInputElement; + expect(yearInput.value).toBe(expectedYear); + + const monthButton = container.querySelector( + '.dash-dropdown-trigger' + ); + expect(monthButton?.textContent).toMatch(expectedMonth); + } + ); + + it('parses year input with moment.js rules (1-digit, 2-digit, 4-digit)', async () => { + const mockOnSelectionChange = jest.fn(); + + const {container} = render( + + ); + + const yearInput = container.querySelector( + '.dash-input-element' + ) as HTMLInputElement; + expect(yearInput.value).toBe('2000'); + + // Test 2-digit year: "97" → 1997 + act(() => { + yearInput.value = '97'; + yearInput.dispatchEvent(new Event('change', {bubbles: true})); + }); + await waitFor(() => expect(yearInput.value).toBe('97'), { + timeout: 1000, + }); + + // Test 4-digit year: "2025" → 2025 + act(() => { + yearInput.value = '2025'; + yearInput.dispatchEvent(new Event('change', {bubbles: true})); + }); + await waitFor(() => expect(yearInput.value).toBe('2025'), { + timeout: 1000, + }); + + // Test single-digit year: "5" → 2005 + act(() => { + yearInput.value = '5'; + yearInput.dispatchEvent(new Event('change', {bubbles: true})); + }); + await waitFor(() => expect(yearInput.value).toBe('5'), {timeout: 1000}); + }); + + it.each([ + { + description: 'selected date when visible in current month', + visibleMonth: new Date(2020, 0, 1), + selectedDate: new Date(2020, 0, 23), + expectedFocusedDay: '23', + }, + { + description: 'selected date even when not initially visible', + visibleMonth: new Date(2020, 0, 1), + selectedDate: new Date(2025, 9, 17), + expectedFocusedDay: '17', // Calendar switches to October 2025 + }, + { + description: 'first day when no date is selected', + visibleMonth: new Date(2020, 0, 1), + selectedDate: undefined, + expectedFocusedDay: '1', + }, + ])( + 'focuses $description', + ({visibleMonth, selectedDate, expectedFocusedDay}) => { + const ref = React.createRef(); + render( + + ); + + // Imperatively focus the appropriate date + const dateToFocus = selectedDate || visibleMonth; + act(() => { + ref.current?.focusDate(dateToFocus); + }); + + const focusedElement = document.activeElement; + expect(focusedElement?.tagName).toBe('TD'); + expect(focusedElement?.textContent).toBe(expectedFocusedDay); + } + ); + + describe('RTL support', () => { + it('applies RTL directionality to calendar container', () => { + const {container: rtlContainer} = render( + + ); + + const {container: ltrContainer} = render( + + ); + + // dir attribute should be on calendar-container, not wrapper (to avoid reversing controls) + const rtlContainer_div = rtlContainer.querySelector( + '.dash-datepicker-calendar-container' + ); + const ltrContainer_div = ltrContainer.querySelector( + '.dash-datepicker-calendar-container' + ); + + expect(rtlContainer_div).toHaveAttribute('dir', 'rtl'); + expect(ltrContainer_div).toHaveAttribute('dir', 'ltr'); + }); + }); +}); diff --git a/components/dash-core-components/tests/unit/calendar/CalendarDay.test.tsx b/components/dash-core-components/tests/unit/calendar/CalendarDay.test.tsx new file mode 100644 index 0000000000..6a1c6a3290 --- /dev/null +++ b/components/dash-core-components/tests/unit/calendar/CalendarDay.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import CalendarDay from '../../../src/utils/calendar/CalendarDay'; + +describe('CalendarDay', () => { + const renderDay = (props: React.ComponentProps) => { + const {container} = render( + + + + + + +
+ ); + return container.querySelector('td')!; + }; + + it('renders with correct label and inside/outside classes', () => { + const insideDay = renderDay({ + date: new Date(2025, 0, 15), + isOutside: false, + showOutsideDays: true, + }); + expect(insideDay).toHaveTextContent('15'); + expect(insideDay).toHaveClass('dash-datepicker-calendar-date-inside'); + expect(insideDay).not.toHaveClass( + 'dash-datepicker-calendar-date-outside' + ); + + const outsideDay = renderDay({ + date: new Date(2024, 11, 31), + isOutside: true, + showOutsideDays: true, + }); + expect(outsideDay).toHaveTextContent('31'); + expect(outsideDay).toHaveClass('dash-datepicker-calendar-date-outside'); + expect(outsideDay).not.toHaveClass( + 'dash-datepicker-calendar-date-inside' + ); + }); + + it('marks disabled day with correct attributes', () => { + const td = renderDay({ + date: new Date(2025, 0, 10), + isOutside: false, + showOutsideDays: true, + isDisabled: true, + }); + + expect(td).toHaveClass('dash-datepicker-calendar-date-disabled'); + expect(td).toHaveAttribute('aria-disabled', 'true'); + expect(td).not.toHaveAttribute('tabIndex'); + }); + + it('hides label for outside days when showOutsideDays is false', () => { + const td = renderDay({ + date: new Date(2024, 11, 31), + isOutside: true, + showOutsideDays: false, + }); + + expect(td).toHaveTextContent(''); + expect(td).toHaveClass('dash-datepicker-calendar-date-outside'); + }); + + it('focuses element when isFocused is true', () => { + const td = renderDay({ + date: new Date(2025, 0, 15), + isOutside: false, + showOutsideDays: true, + isFocused: true, + }); + + expect(td).toBe(document.activeElement); + }); +}); diff --git a/components/dash-core-components/tests/unit/calendar/CalendarDayPadding.test.tsx b/components/dash-core-components/tests/unit/calendar/CalendarDayPadding.test.tsx new file mode 100644 index 0000000000..1664ccc580 --- /dev/null +++ b/components/dash-core-components/tests/unit/calendar/CalendarDayPadding.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import CalendarDayPadding from '../../../src/utils/calendar/CalendarDayPadding'; + +describe('CalendarDayPadding', () => { + it('renders padding cell with correct class and empty content', () => { + const {container} = render( + + + + + + +
+ ); + const td = container.querySelector('td'); + + expect(td).toBeInTheDocument(); + expect(td).toHaveClass('dash-datepicker-calendar-padding'); + expect(td).not.toHaveClass('dash-datepicker-calendar-date-inside'); + expect(td).not.toHaveClass('dash-datepicker-calendar-date-outside'); + expect(td).toHaveTextContent(''); + }); +}); diff --git a/components/dash-core-components/tests/unit/calendar/CalendarMonth.test.tsx b/components/dash-core-components/tests/unit/calendar/CalendarMonth.test.tsx new file mode 100644 index 0000000000..226473f2b3 --- /dev/null +++ b/components/dash-core-components/tests/unit/calendar/CalendarMonth.test.tsx @@ -0,0 +1,261 @@ +import React from 'react'; +import {render, fireEvent} from '@testing-library/react'; +import {CalendarMonth} from '../../../src/utils/calendar/CalendarMonth'; +import {CalendarDirection} from '../../../src/types'; + +describe('CalendarMonth', () => { + it('renders a calendar month with correct structure', () => { + const {container} = render(); + + const table = container.querySelector('table'); + expect(table).toBeInTheDocument(); + + // Should have 7 day-of-week headers + const headers = container.querySelectorAll('thead th'); + expect(headers.length).toBeGreaterThanOrEqual(7); + }); + + describe('when showOutsideDays=false', () => { + it.each([ + { + month: 5, // June + monthName: 'June', + daysInMonth: 30, + startsOnDay: 0, // Sunday + expectedFirstIndex: 0, + expectedEmptyCellsBefore: 0, + }, + { + month: 0, // January + monthName: 'January', + daysInMonth: 31, + startsOnDay: 3, // Wednesday + expectedFirstIndex: 3, + expectedEmptyCellsBefore: 3, + }, + ])( + 'renders $monthName 2025 with correct labeled and unlabeled cells', + ({ + month, + monthName, + daysInMonth, + expectedFirstIndex, + expectedEmptyCellsBefore, + }) => { + const {container} = render( + + ); + + const allCells = container.querySelectorAll('td'); + const cellTexts = Array.from(allCells).map( + td => td.textContent?.trim() || '' + ); + const labeledCells = cellTexts.filter(text => text !== ''); + + // All days in the month should be labeled in correct order + expect(labeledCells.length).toBe(daysInMonth); + const days = labeledCells.map(text => parseInt(text, 10)); + expect(days).toEqual( + Array.from({length: daysInMonth}, (_, i) => i + 1) + ); + + // First day should appear at expected position + const firstDayIndex = cellTexts.findIndex(text => text === '1'); + expect(firstDayIndex).toBe(expectedFirstIndex); + + // Cells before first day should be unlabeled + if (expectedEmptyCellsBefore > 0) { + expect( + cellTexts.slice(0, expectedEmptyCellsBefore) + ).toEqual(Array(expectedEmptyCellsBefore).fill('')); + } + + // Cells after last day in the same week should be unlabeled + const lastDayIndex = cellTexts.lastIndexOf(String(daysInMonth)); + const remainingInWeek = 6 - (lastDayIndex % 7); + for (let i = 1; i <= remainingInWeek; i++) { + expect(cellTexts[lastDayIndex + i]).toBe(''); + } + } + ); + }); + + it('shows outside day labels when showOutsideDays=true with Monday first', () => { + // January 2025: starts on Wednesday (day 3) + // With Monday as first day of week, we show: Mon Dec 30, Tue Dec 31, then Wed Jan 1 + const {container} = render( + + ); + + const allCells = container.querySelectorAll('td'); + const cellTexts = Array.from(allCells).map( + td => td.textContent?.trim() || '' + ); + + const labeledCells = cellTexts.filter(text => text !== ''); + + // First 2 cells should be December days (30, 31), then January 1 + expect(cellTexts[0]).toBe('30'); + expect(cellTexts[1]).toBe('31'); + + // 3rd cell should be January 1 + expect(cellTexts[2]).toBe('1'); + + // Verify January days continue in sequence + expect(cellTexts[3]).toBe('2'); + expect(cellTexts[4]).toBe('3'); + }); + + it('marks selected dates', () => { + const selectedDates: Date[] = [ + new Date(2025, 0, 5), + new Date(2025, 0, 10), + ]; + + const {container} = render( + + ); + + const allCells = container.querySelectorAll('td'); + const selectedCells = Array.from(allCells).filter(td => + td.classList.contains('dash-datepicker-calendar-date-selected') + ); + + // Only the two specific dates should be selected (5th and 10th) + expect(selectedCells.length).toBe(2); + }); + + it('marks highlighted dates with date range', () => { + const highlightedDates: [Date, Date] = [ + new Date(2025, 0, 10), + new Date(2025, 0, 15), + ]; + + const {container} = render( + + ); + + const allCells = container.querySelectorAll('td'); + const highlightedCells = Array.from(allCells).filter(td => + td.classList.contains('dash-datepicker-calendar-date-highlighted') + ); + + expect(highlightedCells.length).toBe(6); // Jan 10-15 = 6 days + }); + + it('handles undefined selectedDatesRange', () => { + const {container} = render( + + ); + + const allCells = container.querySelectorAll('td'); + const selectedCells = Array.from(allCells).filter(td => + td.classList.contains('dash-datepicker-calendar-date-selected') + ); + + expect(selectedCells.length).toBe(0); + }); + + it('handles undefined date props', () => { + const {container} = render( + + ); + + const table = container.querySelector('table'); + expect(table).toBeInTheDocument(); + }); + + describe('RTL support', () => { + it('reverses keyboard navigation for ArrowLeft/ArrowRight in RTL', () => { + const mockOnDayFocused = jest.fn(); + + render( + + ); + + // Use document.activeElement to find the actually focused cell + const focusedCell = document.activeElement as HTMLElement; + expect(focusedCell?.tagName).toBe('TD'); + expect(focusedCell?.textContent).toBe('15'); + + mockOnDayFocused.mockClear(); // Clear any initial focus calls + + // Press ArrowRight - in RTL this should go to January 14 (backwards) + fireEvent.keyDown(focusedCell!, {key: 'ArrowRight'}); + expect(mockOnDayFocused).toHaveBeenLastCalledWith( + new Date(2025, 0, 14) + ); + + mockOnDayFocused.mockClear(); + + // Press ArrowLeft - in RTL this should go to January 16 (forwards) + fireEvent.keyDown(focusedCell!, {key: 'ArrowLeft'}); + expect(mockOnDayFocused).toHaveBeenLastCalledWith( + new Date(2025, 0, 16) + ); + }); + + it('keeps ArrowUp/ArrowDown unchanged in RTL', () => { + const mockOnDayFocused = jest.fn(); + + render( + + ); + + const focusedCell = document.activeElement as HTMLElement; + expect(focusedCell?.tagName).toBe('TD'); + expect(focusedCell?.textContent).toBe('15'); + + mockOnDayFocused.mockClear(); // Clear any initial focus calls + + // ArrowDown should still go forward 1 week + fireEvent.keyDown(focusedCell!, {key: 'ArrowDown'}); + expect(mockOnDayFocused).toHaveBeenLastCalledWith( + new Date(2025, 0, 22) + ); + + mockOnDayFocused.mockClear(); + + // ArrowUp should still go backward 1 week + fireEvent.keyDown(focusedCell!, {key: 'ArrowUp'}); + expect(mockOnDayFocused).toHaveBeenLastCalledWith( + new Date(2025, 0, 8) + ); + }); + }); +}); diff --git a/components/dash-core-components/tests/unit/calendar/createMonthGrid.test.ts b/components/dash-core-components/tests/unit/calendar/createMonthGrid.test.ts new file mode 100644 index 0000000000..38d3b69450 --- /dev/null +++ b/components/dash-core-components/tests/unit/calendar/createMonthGrid.test.ts @@ -0,0 +1,177 @@ +import {createMonthGrid} from '../../../src/utils/calendar/createMonthGrid'; + +/** + * Helper to verify that dates are consecutive + * Once null padding starts, all subsequent values are null. + */ +const expectConsecutiveDatesUntilPadding = (dates: (Date | null)[]) => { + let foundFirstNull = false; + + for (let i = 0; i < dates.length; i++) { + const date = dates[i]; + + if (date === null) { + foundFirstNull = true; + } else if (foundFirstNull) { + // Once we hit a single padding cell, all subsequent days should be empty "padding" cells + fail('Found non-null date after null padding'); + } else if (i > 0 && dates[i - 1] !== null) { + // Verify consecutive days before padding + const dayDiff = + (date.getTime() - dates[i - 1]!.getTime()) / + (1000 * 60 * 60 * 24); + expect(dayDiff).toBe(1); + } + } +}; + +describe('createMonthGrid', () => { + describe('with showOutsideDays=true (default)', () => { + it('creates grid with exactly 6 rows and 7 columns', () => { + const grid = createMonthGrid(2025, 0, 0, true); + + expect(grid.length).toBe(6); + grid.forEach(week => { + expect(week.length).toBe(7); + }); + }); + + it('has consecutive dates until padding, then all null', () => { + const grid = createMonthGrid(2025, 0, 0, true); + expectConsecutiveDatesUntilPadding(grid.flat()); + }); + + it('adjusts for different first day of week', () => { + const sundayFirst = createMonthGrid(2025, 0, 0, true); + const mondayFirst = createMonthGrid(2025, 0, 1, true); + + expect(sundayFirst[0][0]).not.toBeNull(); + expect(mondayFirst[0][0]).not.toBeNull(); + expect(sundayFirst[0][0]).not.toEqual(mondayFirst[0][0]); + + // January 2025 starts on Wednesday + // Sunday-first shows 3 days from prev month, Monday-first shows 2 + expect(mondayFirst[0][0]!.getTime()).toBeGreaterThan( + sundayFirst[0][0]!.getTime() + ); + }); + + it('handles months starting on different weekdays', () => { + const jan2025 = createMonthGrid(2025, 0, 0, true); // Jan starts on Wednesday + const feb2025 = createMonthGrid(2025, 1, 0, true); // Feb starts on Saturday + + expect(jan2025.length).toBe(6); + expect(feb2025.length).toBe(6); + }); + + it('handles February in leap and non-leap years', () => { + const feb2024 = createMonthGrid(2024, 1, 0, true); // Leap year + const feb2025 = createMonthGrid(2025, 1, 0, true); // Non-leap year + + // Both should always have exactly 6 rows + expect(feb2024.length).toBe(6); + expect(feb2025.length).toBe(6); + + // Verify consecutive dates until padding, then all null + expectConsecutiveDatesUntilPadding(feb2024.flat()); + expectConsecutiveDatesUntilPadding(feb2025.flat()); + }); + + it('creates dates at midnight in local timezone', () => { + const grid = createMonthGrid(2025, 0, 0, true); + const firstDate = grid[0][0]; + + expect(firstDate).not.toBeNull(); + expect(firstDate!.getHours()).toBe(0); + expect(firstDate!.getMinutes()).toBe(0); + expect(firstDate!.getSeconds()).toBe(0); + expect(firstDate!.getMilliseconds()).toBe(0); + }); + + it('includes correct dates for January 2025', () => { + const grid = createMonthGrid(2025, 0, 0, true); // January 2025, Sunday first + const allDates = grid.flat(); + + // January 1, 2025 is a Wednesday + // So grid starts on Sunday, December 29, 2024 + expect(allDates[0]).toEqual(new Date(2024, 11, 29)); + + // Find January 1 (should be 4th cell: Sun, Mon, Tue, Wed) + expect(allDates[3]).toEqual(new Date(2025, 0, 1)); + + // Find January 31 + const jan31Index = allDates.findIndex( + d => d !== null && d.getMonth() === 0 && d.getDate() === 31 + ); + expect(allDates[jan31Index]).toEqual(new Date(2025, 0, 31)); + }); + }); + + describe('with showOutsideDays=false', () => { + it('creates grid with exactly 6 rows and 7 columns', () => { + const grid = createMonthGrid(2025, 0, 0, false); // January 2025, Sunday first + + // Should always have exactly 6 rows (weeks) for consistent calendar height + expect(grid.length).toBe(6); + + grid.forEach(week => { + expect(week.length).toBe(7); + }); + }); + + it('returns null for dates outside the current month', () => { + const grid = createMonthGrid(2025, 0, 0, false); // January 2025, Sunday first + const allDates = grid.flat(); + + // January 1, 2025 is a Wednesday (4th day of week when Sunday=0) + // So first 3 cells should be null + expect(allDates[0]).toBeNull(); + expect(allDates[1]).toBeNull(); + expect(allDates[2]).toBeNull(); + + // 4th cell should be January 1 + expect(allDates[3]).toEqual(new Date(2025, 0, 1)); + + // Last day of January is the 31st (Friday) + // Find where January 31 is + const jan31Index = allDates.findIndex( + d => d !== null && d.getMonth() === 0 && d.getDate() === 31 + ); + expect(allDates[jan31Index]).toEqual(new Date(2025, 0, 31)); + + // Days after January 31 in the same week should be null + for (let i = jan31Index + 1; i < allDates.length; i++) { + const d = allDates[i]; + if (d === null) { + // null is expected for outside days + expect(d).toBeNull(); + } else { + // If not null, it should be from a different month + expect(d.getMonth()).not.toBe(0); + } + } + }); + + it('only includes dates from the current month', () => { + const grid = createMonthGrid(2025, 0, 0, false); // January 2025 + const allDates = grid.flat(); + const nonNullDates = allDates.filter(d => d !== null) as Date[]; + + // All non-null dates should be in January (month 0) + nonNullDates.forEach(date => { + expect(date.getMonth()).toBe(0); + }); + + // Should have exactly 31 non-null dates (days in January) + expect(nonNullDates.length).toBe(31); + + // They should be numbered 1-31 + const dayNumbers = nonNullDates + .map(d => d.getDate()) + .sort((a, b) => a - b); + expect(dayNumbers).toEqual( + Array.from({length: 31}, (_, i) => i + 1) + ); + }); + }); +}); diff --git a/components/dash-core-components/tests/unit/calendar/helpers.test.ts b/components/dash-core-components/tests/unit/calendar/helpers.test.ts new file mode 100644 index 0000000000..ca7bae9937 --- /dev/null +++ b/components/dash-core-components/tests/unit/calendar/helpers.test.ts @@ -0,0 +1,248 @@ +import { + dateAsStr, + isDateInRange, + strAsDate, + formatDate, + formatMonth, + extractFormats, + getMonthOptions, + formatYear, + parseYear, +} from '../../../src/utils/calendar/helpers'; + +describe('strAsDate and dateAsStr', () => { + it('converts between date strings and Date objects as inverse operations', () => { + const testDates = [ + new Date(2025, 0, 15), // Jan 15, 2025 (regular date) + new Date(2025, 0, 1), // Jan 1 (start of year) + new Date(2024, 1, 29), // Feb 29, 2024 (leap year) + new Date(2025, 11, 31), // Dec 31 (end of year) + new Date(1969, 11, 31), // Dec 31, 1969 (before Unix epoch) + new Date(1900, 0, 1), // Jan 1, 1900 (far past) + new Date(2100, 5, 15), // Jun 15, 2100 (far future) + ]; + + for (const date of testDates) { + const str = dateAsStr(date); + const roundTrip = strAsDate(str); + expect(roundTrip).toEqual(date); + + // Verify proper formatting with zero-padding + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + expect(str).toBe(`${year}-${month}-${day}`); + } + }); + + it('handles undefined and invalid inputs correctly', () => { + // Strange formatting + expect(strAsDate('2025-01')).toEqual(new Date(2025, 0, 1)); + expect(strAsDate('2025/01/15')).toEqual(new Date(2025, 0, 15)); + expect(strAsDate('2025-01-15T14:30:45')).toEqual(new Date(2025, 0, 15)); + expect(strAsDate(`${new Date(2025, 0, 1)}`)).toEqual( + new Date(2025, 0, 1) + ); + + // Undefined values + expect(dateAsStr(undefined)).toBeUndefined(); + expect(strAsDate(undefined)).toBeUndefined(); + expect(strAsDate('')).toBeUndefined(); + + // Invalid formats + expect(strAsDate('invalid')).toBeUndefined(); + }); + + it('accepts Python datetime string representations', () => { + // Python datetime.datetime objects stringify with time components + // e.g., datetime(2025, 1, 15, 14, 30, 45, 123456) -> "2025-01-15 14:30:45.123456" + + // With full precision (microseconds) + const fullPrecision = strAsDate('2025-01-15 14:30:45.123456'); + expect(fullPrecision).toEqual(new Date(2025, 0, 15)); + + // With seconds only + const withSeconds = strAsDate('2025-01-15 14:30:45'); + expect(withSeconds).toEqual(new Date(2025, 0, 15)); + + // With minutes only + const withMinutes = strAsDate('2025-01-15 14:30'); + expect(withMinutes).toEqual(new Date(2025, 0, 15)); + + // Edge cases + const midnight = strAsDate('2025-01-15 00:00:00'); + expect(midnight).toEqual(new Date(2025, 0, 15)); + + const endOfDay = strAsDate('2025-01-15 23:59:59.999999'); + expect(endOfDay).toEqual(new Date(2025, 0, 15)); + }); +}); + +describe('isDateInRange', () => { + it('checks if date is within range (inclusive boundaries, normalized to midnight)', () => { + const minDate = new Date(2025, 0, 10, 14, 30, 0); // Jan 10, 2025 at 2:30 PM + const maxDate = new Date(2025, 0, 20, 9, 15, 0); // Jan 20, 2025 at 9:15 AM + + // Within range (time components ignored) + expect( + isDateInRange(new Date(2025, 0, 10, 0, 0, 1), minDate, maxDate) + ).toBe(true); // min boundary + expect( + isDateInRange(new Date(2025, 0, 15, 23, 59, 59), minDate, maxDate) + ).toBe(true); // middle + expect( + isDateInRange(new Date(2025, 0, 20, 23, 59, 59), minDate, maxDate) + ).toBe(true); // max boundary + + // Outside range + expect(isDateInRange(new Date(2025, 0, 9), minDate, maxDate)).toBe( + false + ); // before min + expect(isDateInRange(new Date(2025, 0, 21), minDate, maxDate)).toBe( + false + ); // after max + }); + + it('handles undefined min/max dates (no range restrictions)', () => { + const someDate = new Date(2025, 5, 15); + + expect(isDateInRange(someDate, undefined, undefined)).toBe(true); + + const minDate = new Date(2025, 0, 1); + const dateAfterMin = new Date(2025, 5, 15); + const dateBeforeMin = new Date(2024, 11, 31); + + expect(isDateInRange(dateAfterMin, minDate, undefined)).toBe(true); + expect(isDateInRange(dateBeforeMin, minDate, undefined)).toBe(false); + + const maxDate = new Date(2025, 11, 31); + const dateBeforeMax = new Date(2025, 5, 15); + const dateAfterMax = new Date(2026, 0, 1); + + expect(isDateInRange(dateBeforeMax, undefined, maxDate)).toBe(true); + expect(isDateInRange(dateAfterMax, undefined, maxDate)).toBe(false); + }); +}); + +describe('formatDate', () => { + const testDate = new Date(1997, 4, 10); // May 10, 1997 + + it('formats dates using moment.js format strings', () => { + expect(formatDate(testDate, 'YYYY-MM-DD')).toBe('1997-05-10'); + expect(formatDate(testDate, 'MM DD YY')).toBe('05 10 97'); + expect(formatDate(testDate, 'M, D, YYYY')).toBe('5, 10, 1997'); + expect(formatDate(testDate)).toBeTruthy(); // default format + }); +}); + +describe('formatMonth', () => { + it('extracts and formats only month tokens from combined format strings', () => { + // Accepts combined month/year formats but only returns month portion + expect(formatMonth(1997, 4, 'MM YY')).toBe('05'); + expect(formatMonth(1997, 4, 'M/YYYY')).toBe('5'); + expect(formatMonth(1997, 4, 'MMMM, YYYY')).toMatch(/May/); + + // Also works with month-only formats + expect(formatMonth(1997, 4, 'MMMM')).toMatch(/May/); + expect(formatMonth(1997, 4, 'MMM')).toMatch(/May/); + }); +}); + +describe('extractFormats', () => { + it('extracts month and year format tokens separately', () => { + expect(extractFormats('MMMM, YYYY')).toEqual({ + monthFormat: 'MMMM', + yearFormat: 'YYYY', + }); + expect(extractFormats('MM YY')).toEqual({ + monthFormat: 'MM', + yearFormat: 'YY', + }); + expect(extractFormats('M/YYYY')).toEqual({ + monthFormat: 'M', + yearFormat: 'YYYY', + }); + }); + + it('uses defaults when format not provided or tokens not found', () => { + expect(extractFormats()).toEqual({ + monthFormat: 'MMMM', + yearFormat: 'YYYY', + }); + expect(extractFormats('invalid')).toEqual({ + monthFormat: 'MMMM', + yearFormat: 'YYYY', + }); + }); +}); + +describe('getMonthOptions', () => { + it('generates 12 month options formatted according to month_format', () => { + const options = getMonthOptions(1997); + expect(options).toHaveLength(12); + expect(options[0].value).toBe(0); + expect(options[11].value).toBe(11); + + // Numeric formats + expect(getMonthOptions(1997, 'MM')[0].label).toBe('01'); + expect(getMonthOptions(1997, 'M')[0].label).toBe('1'); + + // Name formats (use regex due to locale variations) + expect(getMonthOptions(1997, 'MMMM')[0].label).toMatch(/January/); + expect(getMonthOptions(1997, 'MMM')[0].label).toMatch(/Jan/); + + // Combined format - extracts only month portion + const combined = getMonthOptions(1997, 'MMMM, YYYY'); + expect(combined[0].label).toMatch(/January/); + expect(combined[0].label).not.toMatch(/1997/); + }); +}); + +describe('formatYear', () => { + it('formats year as YYYY or YY based on extracted year format', () => { + // Default YYYY + expect(formatYear(1997)).toBe('1997'); + expect(formatYear(2023)).toBe('2023'); + + // YY format + expect(formatYear(1997, 'MMMM, YY')).toBe('97'); + expect(formatYear(2023, 'MM YY')).toBe('23'); + expect(formatYear(2005, 'M/YY')).toBe('05'); + + // YYYY format + expect(formatYear(1997, 'MMMM, YYYY')).toBe('1997'); + expect(formatYear(2023, 'MM YYYY')).toBe('2023'); + }); +}); + +describe('parseYear', () => { + it('parses 4-digit years as-is', () => { + expect(parseYear('1997')).toBe(1997); + expect(parseYear('2023')).toBe(2023); + expect(parseYear('2000')).toBe(2000); + }); + + it('parses 2-digit years using moment.js pivot (00-68 → 2000s, 69-99 → 1900s)', () => { + expect(parseYear('97')).toBe(1997); + expect(parseYear('23')).toBe(2023); + expect(parseYear('68')).toBe(2068); + expect(parseYear('69')).toBe(1969); + expect(parseYear('00')).toBe(2000); + }); + + it('handles single-digit years', () => { + expect(parseYear('5')).toBe(2005); + expect(parseYear('0')).toBe(2000); + }); + + it('returns undefined for invalid inputs', () => { + expect(parseYear('')).toBeUndefined(); + expect(parseYear(' ')).toBeUndefined(); + expect(parseYear('abc')).toBeUndefined(); + }); + + it('handles whitespace trimming', () => { + expect(parseYear(' 1997 ')).toBe(1997); + expect(parseYear(' 97 ')).toBe(1997); + }); +}); diff --git a/package-lock.json b/package-lock.json index e12968e719..9533b93ad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@lerna/filter-options": "^6.4.1", + "@types/jest": "^30.0.0", "husky": "8.0.3", "lerna": "^8.2.3", "lint-staged": "^16.1.0" @@ -179,6 +180,53 @@ "dev": true, "license": "ISC" }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -192,6 +240,45 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, "node_modules/@lerna/child-process": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-6.4.1.tgz", @@ -1329,6 +1416,92 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@types/minimatch": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", @@ -1342,6 +1515,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -1349,6 +1532,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.34", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", + "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -2917,6 +3124,24 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", @@ -4520,6 +4745,227 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -7984,6 +8430,29 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -8528,6 +8997,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", diff --git a/package.json b/package.json index e78e279c1b..6532530bd1 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "devDependencies": { "@lerna/filter-options": "^6.4.1", + "@types/jest": "^30.0.0", "husky": "8.0.3", "lerna": "^8.2.3", "lint-staged": "^16.1.0"