From b35f9e14fef915de6868ed36bbb657e690475079 Mon Sep 17 00:00:00 2001 From: DasProffi <67233923+DasProffi@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:19:00 +0100 Subject: [PATCH] publish: version 0.9.2 --- CHANGELOG.md | 12 + jest.config.cjs | 9 +- locales/de-DE.arb | 2 + locales/en-US.arb | 2 + package-lock.json | 581 +++++++++++++++++- package.json | 4 +- .../user-interaction/Combobox/useCombobox.ts | 2 +- .../MultiSelect/useMultiSelect.ts | 3 +- .../user-interaction/Select/useSelect.ts | 3 +- .../user-interaction/data/FilterList.tsx | 77 +-- .../user-interaction/data/FilterPopUp.tsx | 16 +- .../user-interaction/input/DateTimeInput.tsx | 44 +- .../input/FlexibleDateTimeInput.tsx | 71 +++ src/utils/date.ts | 33 + .../Layout/Table/FilterListTable.stories.tsx | 1 - .../Input/FlexibleDateTimeInput.stories.tsx | 50 ++ tests/selection/useMultiSelection.test.ts | 84 +++ tests/selection/useSingleSelection.test.ts | 191 ++++++ tsconfig.json | 6 +- 19 files changed, 1110 insertions(+), 81 deletions(-) create mode 100644 src/components/user-interaction/input/FlexibleDateTimeInput.tsx create mode 100644 stories/User Interaction/Input/FlexibleDateTimeInput.stories.tsx create mode 100644 tests/selection/useMultiSelection.test.ts create mode 100644 tests/selection/useSingleSelection.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cf9b9be..3910d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.2] - 2026-03-22 + +### Added +- `FlexibleDateTimeInput` component for `DateTimeInput` for easy switching between Date and DateTime input +- actions attribute to `DateTimeInput` for displaying action buttons +- Tests for `useMultiSelection` and `useSingleSelection` hooks + +### Changed +- `FilterList` + - Height of chips and buttons is not consistent with the standard height + - Allows the addition of children + ## [0.9.1] - 2026-02-14 ### Changed diff --git a/jest.config.cjs b/jest.config.cjs index da18da5..2e493a3 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,10 +1,13 @@ module.exports = { preset: 'ts-jest/presets/default-esm', testEnvironment: 'node', - testMatch: ['**/tests/**/*.test.ts'], - extensionsToTreatAsEsm: ['.ts'], + testMatch: ['**/tests/**/*.test.ts', '**/tests/**/*.test.tsx'], + extensionsToTreatAsEsm: ['.ts', '.tsx'], + moduleNameMapper: { + '^@/(.*)$': '/$1', + }, transform: { - '^.+\\.ts$': [ + '^.+\\.tsx?$': [ 'ts-jest', { useESM: true diff --git a/locales/de-DE.arb b/locales/de-DE.arb index 35aede0..e51bf02 100644 --- a/locales/de-DE.arb +++ b/locales/de-DE.arb @@ -1,6 +1,7 @@ { "add": "Hinzufügen", "addFilter": "Filter hinzufügen", + "addTime": "Uhrzeit hinzufügen", "all": "Alle", "apply": "Anwenden", "back": "Zurück", @@ -290,6 +291,7 @@ }, "goodToSeeYou": "Schön dich zu sehen", "welcome": "Willkommen", + "withoutTime": "Ohne Uhrzeit", "rSortingOrderAfter": "Angewendet {otherSortings, plural, =0{als primäre Sortierung} =1{nach einer anderen Sortierung} other{nach # anderen Sortierungen}}", "@rSortingOrderAfter": { "placeholders": { diff --git a/locales/en-US.arb b/locales/en-US.arb index 83d344e..974c103 100644 --- a/locales/en-US.arb +++ b/locales/en-US.arb @@ -1,6 +1,7 @@ { "add": "Add", "addFilter": "Add filter", + "addTime": "Add Time", "all": "All", "apply": "Apply", "back": "Back", @@ -291,6 +292,7 @@ }, "goodToSeeYou": "Good to see you", "welcome": "Welcome", + "withoutTime": "Without Time", "rSortingOrderAfter": "Applied {otherSortings, plural, =0{as the first sorting} =1{after # other sorting} other{after # other sortings}}", "@rSortingOrderAfter": { "placeholders": { diff --git a/package-lock.json b/package-lock.json index d5143c3..926c407 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@helpwave/hightide", - "version": "0.9.0", + "version": "0.9.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@helpwave/hightide", - "version": "0.9.0", + "version": "0.9.2", "license": "MPL-2.0", "dependencies": { "@helpwave/internationalization": "0.4.0", @@ -20,7 +20,7 @@ "tailwindcss": "4.1.18" }, "bin": { - "barrel": ".dist/scripts/barrel.js" + "barrel": "dist/scripts/barrel.js" }, "devDependencies": { "@babel/core": "7.26.0", @@ -33,6 +33,7 @@ "@storybook/addon-links": "10.2.10", "@storybook/nextjs": "10.2.10", "@tailwindcss/postcss": "4.1.18", + "@testing-library/react": "16.3.0", "@types/jest": "30.0.0", "@types/node": "20.17.10", "@types/react": "19.2.3", @@ -43,6 +44,7 @@ "eslint": "9.31.0", "eslint-plugin-storybook": "10.2.10", "jest": "30.2.0", + "jest-environment-jsdom": "30.2.0", "postcss": "8.5.6", "storybook": "10.2.10", "ts-jest": "29.4.6", @@ -85,6 +87,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/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -2016,6 +2039,121 @@ "dev": true, "license": "MIT" }, + "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/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -3867,6 +4005,34 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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/expect": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", @@ -6393,6 +6559,34 @@ "dev": true, "license": "MIT" }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@testing-library/user-event": { "version": "14.6.1", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", @@ -6569,6 +6763,18 @@ "pretty-format": "^30.0.0" } }, + "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", @@ -6641,6 +6847,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/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -7551,6 +7764,16 @@ "node": ">=8.9.0" } }, + "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": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -9282,6 +9505,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.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -9289,6 +9526,20 @@ "devOptional": true, "license": "MIT" }, + "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/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -9361,6 +9612,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": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -11159,6 +11417,19 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "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-entities": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", @@ -11258,6 +11529,20 @@ "entities": "^2.0.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", @@ -11265,6 +11550,20 @@ "dev": true, "license": "MIT" }, + "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", @@ -11275,6 +11574,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", @@ -11764,6 +12076,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -12398,6 +12717,31 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "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-node": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", @@ -12932,6 +13276,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.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -14034,6 +14418,13 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "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", @@ -14370,6 +14761,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/pascal-case": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", @@ -15606,6 +16023,13 @@ "fsevents": "~2.3.2" } }, + "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-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -15719,6 +16143,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/sass-loader": { "version": "16.0.6", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.6.tgz", @@ -15760,6 +16191,19 @@ } } }, + "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.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -16611,6 +17055,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/synckit": { "version": "0.11.12", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", @@ -16839,6 +17290,26 @@ "node": ">=14.0.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", @@ -16874,6 +17345,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/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -17547,6 +18044,19 @@ "dev": true, "license": "MIT" }, + "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", @@ -17571,6 +18081,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.105.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", @@ -17709,6 +18229,44 @@ "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==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "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", @@ -17940,6 +18498,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/package.json b/package.json index 4596eec..d7a975b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "access": "public" }, "license": "MPL-2.0", - "version": "0.9.1", + "version": "0.9.2", "files": [ "dist" ], @@ -67,11 +67,13 @@ "@types/react": "19.2.3", "@types/react-dom": "19.2.3", "@types/tinycolor2": "1.4.6", + "@testing-library/react": "16.3.0", "@vitest/mocker": "4.0.16", "autoprefixer": "10.4.23", "eslint": "9.31.0", "eslint-plugin-storybook": "10.2.10", "jest": "30.2.0", + "jest-environment-jsdom": "30.2.0", "postcss": "8.5.6", "storybook": "10.2.10", "ts-jest": "29.4.6", diff --git a/src/components/user-interaction/Combobox/useCombobox.ts b/src/components/user-interaction/Combobox/useCombobox.ts index f60b588..60b53d2 100644 --- a/src/components/user-interaction/Combobox/useCombobox.ts +++ b/src/components/user-interaction/Combobox/useCombobox.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo } from 'react' import { useListNavigation } from '@/src/hooks/useListNavigation' import { useControlledState } from '@/src/hooks/useControlledState' -import { useSearch } from '@/src/hooks' +import { useSearch } from '@/src/hooks/useSearch' export interface UseComboboxOption { id: string, diff --git a/src/components/user-interaction/MultiSelect/useMultiSelect.ts b/src/components/user-interaction/MultiSelect/useMultiSelect.ts index 4aead8e..ac049c4 100644 --- a/src/components/user-interaction/MultiSelect/useMultiSelect.ts +++ b/src/components/user-interaction/MultiSelect/useMultiSelect.ts @@ -7,7 +7,8 @@ import { import { useMultiSelection } from '@/src/hooks/useMultiSelection' import { useListNavigation } from '@/src/hooks/useListNavigation' import { useEventCallbackStabilizer } from '@/src/hooks/useEventCallbackStabelizer' -import { useSearch, useTypeAheadSearch } from '@/src/hooks' +import { useSearch } from '@/src/hooks/useSearch' +import { useTypeAheadSearch } from '@/src/hooks/useTypeAheadSearch' export interface UseMultiSelectOption { id: string, diff --git a/src/components/user-interaction/Select/useSelect.ts b/src/components/user-interaction/Select/useSelect.ts index b5c0a0e..b4a3c03 100644 --- a/src/components/user-interaction/Select/useSelect.ts +++ b/src/components/user-interaction/Select/useSelect.ts @@ -7,7 +7,8 @@ import { import { useSingleSelection } from '@/src/hooks/useSingleSelection' import { useListNavigation } from '@/src/hooks/useListNavigation' import { useEventCallbackStabilizer } from '@/src/hooks/useEventCallbackStabelizer' -import { useSearch, useTypeAheadSearch } from '@/src/hooks' +import { useSearch } from '@/src/hooks/useSearch' +import { useTypeAheadSearch } from '@/src/hooks/useTypeAheadSearch' export interface UseSelectOption { id: string, diff --git a/src/components/user-interaction/data/FilterList.tsx b/src/components/user-interaction/data/FilterList.tsx index 73483e3..1c9e9bb 100644 --- a/src/components/user-interaction/data/FilterList.tsx +++ b/src/components/user-interaction/data/FilterList.tsx @@ -1,3 +1,4 @@ +import type { PropsWithChildren } from 'react' import { useMemo, useState, type ReactNode } from 'react' import type { FilterValue } from './filter-function' import { FilterValueUtils, useFilterValueTranslation } from './filter-function' @@ -37,9 +38,10 @@ export interface FilterListItem { dataType: DataType, tags: ReadonlyArray<{ tag: string, label: string, display?: ReactNode }>, popUpBuilder?: (props: FilterListPopUpBuilderProps) => ReactNode, + activeLabelBuilder?: (value: FilterValue) => ReactNode, } -export interface FilterListProps { +export interface FilterListProps extends PropsWithChildren { value: IdentifierFilterValue[], onValueChange: (value: IdentifierFilterValue[]) => void, availableItems: FilterListItem[], @@ -75,7 +77,7 @@ export const FilterList = ({ value, onValueChange, availableItems }: FilterListP {({ toggleOpen, props }) => ( - @@ -134,45 +136,46 @@ export const FilterList = ({ value, onValueChange, availableItems }: FilterListP > {({ toggleOpen, props, isOpen }) => ( - )} - {item.popUpBuilder ? ( - - {({ isOpen, setIsOpen }) => ( - item.popUpBuilder({ - value: editState?.id === columnFilter.id ? editState.value : columnFilter.value, - onValueChange: value => setEditState({ ...columnFilter, value }), - onRemove: () => { - onValueChange(value.filter(prevItem => prevItem.id !== columnFilter.id)) - setEditState(undefined) - }, - dataType: item.dataType, - tags: item.tags, - name: item.label, - isOpen, - close: () => setIsOpen(false), - }) - )} - - ) : ( - { - setEditState({ ...columnFilter, value }) - }} - onRemove={() => { - onValueChange(value.filter(prevItem => prevItem.id !== columnFilter.id)) - setEditState(undefined) - }} - /> - )} + + {({ isOpen, setIsOpen }) => item.popUpBuilder ? ( + item.popUpBuilder({ + value: editState?.id === columnFilter.id ? editState.value : columnFilter.value, + onValueChange: value => setEditState({ ...columnFilter, value }), + onRemove: () => { + onValueChange(value.filter(prevItem => prevItem.id !== columnFilter.id)) + setEditState(undefined) + }, + dataType: item.dataType, + tags: item.tags, + name: item.label, + isOpen, + close: () => setIsOpen(false), + }) + ) : ( + { + setEditState({ ...columnFilter, value }) + }} + onRemove={() => { + onValueChange(value.filter(prevItem => prevItem.id !== columnFilter.id)) + setEditState(undefined) + }} + onClose={() => setIsOpen(false)} + /> + )} + ) })} diff --git a/src/components/user-interaction/data/FilterPopUp.tsx b/src/components/user-interaction/data/FilterPopUp.tsx index 0bf4e3c..b94a0a8 100644 --- a/src/components/user-interaction/data/FilterPopUp.tsx +++ b/src/components/user-interaction/data/FilterPopUp.tsx @@ -35,7 +35,6 @@ export interface FilterPopUpBaseProps extends PopUpProps { onOperatorChange: (operator: FilterOperator) => void, onRemove: () => void, allowedOperators: FilterOperator[], - hasValue: boolean, noParameterRequired?: boolean, } @@ -46,7 +45,6 @@ export const FilterBasePopUp = forwardRef( onOperatorChange, onRemove, allowedOperators, - hasValue, noParameterRequired = false, ...props }: FilterPopUpBaseProps, ref) { @@ -78,7 +76,7 @@ export const FilterBasePopUp = forwardRef( ))} - +
( > - - ( > - +
{children} @@ -139,7 +135,6 @@ export const TextFilterPopUp = forwardRef(func onOperatorChange={(newOperator) => onValueChange({ dataType: 'text', parameter, operator: newOperator })} onRemove={onRemove} allowedOperators={FilterOperatorUtils.operatorsByCategory.text} - hasValue={!!value} noParameterRequired={!needsParameterInput} > @@ -210,7 +205,6 @@ export const NumberFilterPopUp = forwardRef(fu }} onRemove={onRemove} allowedOperators={FilterOperatorUtils.operatorsByCategory.number} - hasValue={!!value} noParameterRequired={!needsParameterInput} > @@ -303,7 +297,6 @@ export const DateFilterPopUp = forwardRef(func onOperatorChange={(newOperator) => onValueChange({ dataType: 'date', parameter, operator: newOperator })} onRemove={onRemove} allowedOperators={FilterOperatorUtils.operatorsByCategory.date} - hasValue={!!value} noParameterRequired={!needsParameterInput} > @@ -437,7 +430,6 @@ export const DatetimeFilterPopUp = forwardRef( onOperatorChange={(newOperator) => onValueChange({ dataType: 'dateTime', parameter, operator: newOperator })} onRemove={onRemove} allowedOperators={FilterOperatorUtils.operatorsByCategory.dateTime} - hasValue={!!value} > {translation('parameter')} @@ -561,7 +553,6 @@ export const BooleanFilterPopUp = forwardRef(f onOperatorChange={(newOperator) => onValueChange({ dataType: 'boolean', parameter, operator: newOperator })} onRemove={onRemove} allowedOperators={FilterOperatorUtils.operatorsByCategory.boolean} - hasValue={!!value} /> ) }) @@ -598,7 +589,6 @@ export const TagsFilterPopUp = forwardRef( onOperatorChange={(newOperator) => onValueChange({ dataType: 'multiTags', parameter, operator: newOperator })} onRemove={onRemove} allowedOperators={FilterOperatorUtils.operatorsByCategory.multiTags} - hasValue={!!value} > {translation('parameter')} @@ -663,7 +653,6 @@ export const TagsSingleFilterPopUp = forwardRef onValueChange({ dataType: 'singleTag', parameter, operator: newOperator })} onRemove={onRemove} allowedOperators={FilterOperatorUtils.operatorsByCategory.singleTag} - hasValue={!!value} > {translation('parameter')} @@ -726,7 +715,6 @@ export const GenericFilterPopUp = forwardRef(f onOperatorChange={(newOperator) => onValueChange({ ...value, operator: newOperator })} onRemove={() => onValueChange({ ...value, operator: undefined })} allowedOperators={FilterOperatorUtils.operatorsByCategory.unknownType} - hasValue={!!value} /> ) }) diff --git a/src/components/user-interaction/input/DateTimeInput.tsx b/src/components/user-interaction/input/DateTimeInput.tsx index 2280467..607481e 100644 --- a/src/components/user-interaction/input/DateTimeInput.tsx +++ b/src/components/user-interaction/input/DateTimeInput.tsx @@ -1,4 +1,4 @@ -import type { HTMLAttributes, InputHTMLAttributes } from 'react' +import type { HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react' import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from 'react' import { CalendarIcon } from 'lucide-react' import clsx from 'clsx' @@ -28,6 +28,7 @@ export interface DateTimeInputProps extends pickerProps?: Omit | 'mode' | 'initialValue' | 'start' | 'end' | 'weekStart' | 'markToday' | 'is24HourFormat' | 'minuteIncrement' | 'secondIncrement' | 'millisecondIncrement' | 'precision'>, outsideClickCloses?: boolean, onDialogOpeningChange?: (isOpen: boolean) => void, + actions?: ReactNode[], } export const DateTimeInput = forwardRef(function DateTimeInput({ @@ -55,6 +56,7 @@ export const DateTimeInput = forwardRef(fu readOnly = false, invalid = false, required = false, + actions = [], ...props }, forwardedRef) { const translation = useHightideTranslation() @@ -114,6 +116,12 @@ export const DateTimeInput = forwardRef(fu id={ids.input} value={stringInputState.state} + onClick={(event) => { + event.preventDefault() + }} + onFocus={(event) => { + event.preventDefault() + }} onChange={(event) => { const date = new Date(event.target.value ?? '') const isValid = !isNaN(date.getTime()) @@ -156,22 +164,24 @@ export const DateTimeInput = forwardRef(fu data-value={PropsUtil.dataAttributes.bool(!!state || !!stringInputState)} {...PropsUtil.aria.interactionStates({ disabled, readOnly, invalid, required }, props)} /> - - { - changeOpenWrapper(true) - }} - aria-haspopup="dialog" - aria-expanded={isOpen} - aria-controls={isOpen ? ids.popup : undefined} - > - - - +
+ {actions} + + { + changeOpenWrapper(true) + }} + aria-haspopup="dialog" + aria-expanded={isOpen} + aria-controls={isOpen ? ids.popup : undefined} + > + + + +
{ + defaultMode: Exclude, + /** Defaults to 23:59:59.999 */ + fixedTime?: Date | null, +} + +export const FlexibleDateTimeInput = forwardRef(function FlexibleDateTimeInput({ + defaultMode = 'date', + value: controlledValue, + initialValue, + onValueChange, + fixedTime: fixedTimeOverride, + actions = [], + ...props +}, forwardedRef) { + const translation = useHightideTranslation() + const [value, setValue] = useControlledState({ + value: controlledValue, + onValueChange: onValueChange, + defaultValue: initialValue, + }) + const fixedTime = useMemo(() => fixedTimeOverride ?? new Date(23, 59, 59, 999), [fixedTimeOverride]) + const [preferredMode, setPreferredMode] = useState(defaultMode) + const mode = useMemo(() => { + if(!value) return preferredMode + if(DateUtils.sameTime(value, fixedTime, true, true)) { + return 'date' + } + return 'dateTime' + }, [preferredMode, value, fixedTime]) + return ( + { + const newMode = preferredMode === 'date' ? 'dateTime' : 'date' + setPreferredMode(prev => prev === 'date' ? 'dateTime' : 'date') + if(value) { + if(newMode === 'date') { + setValue(DateUtils.withTime(value, fixedTime)) + } else { + setValue(DateUtils.isLastMillisecondOfDay(value) ? new Date(value.getTime() - 1) : new Date(value.getTime() + 1)) + } + } + }} + > + {preferredMode === 'date' ? : } + , + ]} + /> + ) +}) \ No newline at end of file diff --git a/src/utils/date.ts b/src/utils/date.ts index 46a0724..9de3dd0 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -117,6 +117,36 @@ const equalDate = (date1: Date, date2: Date) => { && date1.getDate() === date2.getDate() } +const isLastMillisecondOfDay = (date: Date): boolean => { + const next = new Date(date.getTime() + 1) + return !equalDate(date, next) +} + + +const sameTime = (a: Date, b: Date, compareSeconds: boolean = false, compareMilliseconds: boolean = false): boolean => { + if (a.getHours() !== b.getHours() || a.getMinutes() !== b.getMinutes()) { + return false + } + if (compareSeconds && a.getSeconds() !== b.getSeconds()) { + return false + } + if (compareMilliseconds && a.getMilliseconds() !== b.getMilliseconds()) { + return false + } + return true +} + +const withTime = (datePart: Date, timePart: Date): Date => { + const out = new Date(datePart) + out.setHours( + timePart.getHours(), + timePart.getMinutes(), + timePart.getSeconds(), + timePart.getMilliseconds() + ) + return out +} + const weeksForCalenderMonth = (date: Date, weekStart: WeekDay, weeks: number = 6) => { const month = date.getMonth() const year = date.getFullYear() @@ -258,6 +288,9 @@ export const DateUtils = { monthsList, weekDayList, equalDate, + isLastMillisecondOfDay, + sameTime, + withTime, formatAbsolute, formatRelative, addDuration, diff --git a/stories/Layout/Table/FilterListTable.stories.tsx b/stories/Layout/Table/FilterListTable.stories.tsx index 2b388b4..f7e1c24 100644 --- a/stories/Layout/Table/FilterListTable.stories.tsx +++ b/stories/Layout/Table/FilterListTable.stories.tsx @@ -61,7 +61,6 @@ const AgeFilterPopUp = ({ value, onValueChange, onRemove, name }: FilterListPopU onOperatorChange={(newOperator) => onValueChange({ dataType: 'number', parameter, operator: newOperator })} onRemove={onRemove} allowedOperators={FilterOperatorUtils.operatorsByCategory.number} - hasValue={!!value} noParameterRequired={!needsParameterInput} > diff --git a/stories/User Interaction/Input/FlexibleDateTimeInput.stories.tsx b/stories/User Interaction/Input/FlexibleDateTimeInput.stories.tsx new file mode 100644 index 0000000..56f6db1 --- /dev/null +++ b/stories/User Interaction/Input/FlexibleDateTimeInput.stories.tsx @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/nextjs' +import { action } from 'storybook/actions' +import { useEffect, useState } from 'react' +import { FlexibleDateTimeInput } from '@/src/components/user-interaction/input/FlexibleDateTimeInput' + +const meta: Meta = { + component: FlexibleDateTimeInput, +} + +export default meta +type Story = StoryObj; + +export const flexibleDateTimeInput: Story = { + args: { + defaultMode: 'date', + disabled: false, + invalid: false, + readOnly: false, + required: false, + precision: 'minute', + minuteIncrement: '5min', + secondIncrement: '1s', + millisecondIncrement: '100ms', + is24HourFormat: false, + initialValue: null, + value: undefined, + onValueChange: action('onValueChange'), + onEditComplete: action('onEditComplete'), + }, + render: (args) => { + const [value, setValue] = useState(args.initialValue ?? null) + useEffect(() => { + setValue(args.value ?? args.initialValue ?? null) + }, [args.value, args.initialValue]) + return ( + { + args.onValueChange?.(v) + setValue(v) + }} + onEditComplete={(v) => { + args.onEditComplete?.(v) + setValue(v) + }} + /> + ) + }, +} diff --git a/tests/selection/useMultiSelection.test.ts b/tests/selection/useMultiSelection.test.ts new file mode 100644 index 0000000..f32d5d5 --- /dev/null +++ b/tests/selection/useMultiSelection.test.ts @@ -0,0 +1,84 @@ +/** @jest-environment jsdom */ + +import { act, renderHook } from '@testing-library/react' +import { useMultiSelection } from '../../src/hooks/useMultiSelection' + +const baseOptions = [ + { id: 'a' }, + { id: 'b' }, + { id: 'c' }, +] as const + +describe('useMultiSelection', () => { + test('uses initialSelection when uncontrolled', () => { + const { result } = renderHook(() => + useMultiSelection({ + options: baseOptions, + initialSelection: ['a', 'c'], + })) + expect(result.current.selection).toEqual(['a', 'c']) + expect(result.current.isSelected('a')).toBe(true) + expect(result.current.isSelected('b')).toBe(false) + }) + + test('toggleSelection adds and removes id', () => { + const { result } = renderHook(() => + useMultiSelection({ + options: baseOptions, + initialSelection: [], + })) + act(() => { + result.current.toggleSelection('b') + }) + expect(result.current.selection).toEqual(['b']) + expect(result.current.isSelected('b')).toBe(true) + act(() => { + result.current.toggleSelection('b') + }) + expect(result.current.selection).toEqual([]) + }) + + test('toggleSelection ignores unknown or disabled id', () => { + const { result } = renderHook(() => + useMultiSelection({ + options: [{ id: 'a' }, { id: 'b', disabled: true }] as const, + initialSelection: ['a'], + })) + act(() => { + result.current.toggleSelection('missing') + }) + expect(result.current.selection).toEqual(['a']) + act(() => { + result.current.toggleSelection('b') + }) + expect(result.current.selection).toEqual(['a']) + }) + + test('setSelection replaces selection', () => { + const { result } = renderHook(() => + useMultiSelection({ + options: baseOptions, + initialSelection: ['a'], + })) + act(() => { + result.current.setSelection(['b', 'c']) + }) + expect(result.current.selection).toEqual(['b', 'c']) + }) + + test('controlled value follows prop', () => { + const onSelectionChange = jest.fn() + const { result, rerender } = renderHook( + (props: { value: readonly string[] }) => + useMultiSelection({ + options: baseOptions, + value: props.value, + onSelectionChange, + }), + { initialProps: { value: ['a'] as readonly string[] } } + ) + expect(result.current.selection).toEqual(['a']) + rerender({ value: ['b', 'c'] }) + expect(result.current.selection).toEqual(['b', 'c']) + }) +}) diff --git a/tests/selection/useSingleSelection.test.ts b/tests/selection/useSingleSelection.test.ts new file mode 100644 index 0000000..e75b358 --- /dev/null +++ b/tests/selection/useSingleSelection.test.ts @@ -0,0 +1,191 @@ +/** @jest-environment jsdom */ + +import { act, renderHook } from '@testing-library/react' +import { useSingleSelection } from '../../src/hooks/useSingleSelection' + +const baseOptions = [ + { id: 'a' }, + { id: 'b' }, + { id: 'c' }, +] as const + +describe('useSingleSelection', () => { + test('uses initialSelection when uncontrolled', () => { + const { result } = renderHook(() => + useSingleSelection({ + options: baseOptions, + initialSelection: 'b', + })) + expect(result.current.selection).toBe('b') + expect(result.current.selectedIndex).toBe(1) + }) + + test('selectValue sets enabled id and null clears', () => { + const { result } = renderHook(() => + useSingleSelection({ + options: baseOptions, + initialSelection: null, + })) + act(() => { + result.current.selectValue('c') + }) + expect(result.current.selection).toBe('c') + act(() => { + result.current.selectValue(null) + }) + expect(result.current.selection).toBeNull() + }) + + test('selectValue ignores invalid or disabled id', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}) + const options = [{ id: 'a' }, { id: 'x', disabled: true }] as const + const { result } = renderHook(() => + useSingleSelection({ + options, + initialSelection: 'a', + })) + act(() => { + result.current.selectValue('missing') + }) + expect(result.current.selection).toBe('a') + act(() => { + result.current.selectValue('x') + }) + expect(result.current.selection).toBe('a') + expect(warn).toHaveBeenCalled() + warn.mockRestore() + }) + + test('selectByIndex selects by position', () => { + const { result } = renderHook(() => + useSingleSelection({ + options: baseOptions, + initialSelection: null, + })) + act(() => { + result.current.selectByIndex(0) + }) + expect(result.current.selection).toBe('a') + }) + + test('selectByIndex ignores invalid index or disabled option', () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}) + const options = [{ id: 'a' }, { id: 'b', disabled: true }] as const + const { result } = renderHook(() => + useSingleSelection({ + options, + initialSelection: 'a', + })) + act(() => { + result.current.selectByIndex(99) + }) + expect(result.current.selection).toBe('a') + act(() => { + result.current.selectByIndex(1) + }) + expect(result.current.selection).toBe('a') + warn.mockRestore() + }) + + test('selectFirst and selectLast skip disabled', () => { + const options = [ + { id: 'a', disabled: true }, + { id: 'b' }, + { id: 'c', disabled: true }, + { id: 'd' }, + ] as const + const { result } = renderHook(() => + useSingleSelection({ + options, + initialSelection: null, + })) + act(() => { + result.current.selectFirst() + }) + expect(result.current.selection).toBe('b') + act(() => { + result.current.selectLast() + }) + expect(result.current.selection).toBe('d') + }) + + test('selectNext loops to first when isLooping is true', () => { + const { result } = renderHook(() => + useSingleSelection({ + options: baseOptions, + initialSelection: 'c', + isLooping: true, + })) + act(() => { + result.current.selectNext() + }) + expect(result.current.selection).toBe('a') + }) + + test('selectNext does not pass last when isLooping is false', () => { + const { result } = renderHook(() => + useSingleSelection({ + options: baseOptions, + initialSelection: 'c', + isLooping: false, + })) + act(() => { + result.current.selectNext() + }) + expect(result.current.selection).toBe('c') + }) + + test('selectPrevious loops from first to last when isLooping is true', () => { + const { result } = renderHook(() => + useSingleSelection({ + options: baseOptions, + initialSelection: 'a', + isLooping: true, + })) + act(() => { + result.current.selectPrevious() + }) + expect(result.current.selection).toBe('c') + }) + + test('selectPrevious stays at first when isLooping is false', () => { + const { result } = renderHook(() => + useSingleSelection({ + options: baseOptions, + initialSelection: 'a', + isLooping: false, + })) + act(() => { + result.current.selectPrevious() + }) + expect(result.current.selection).toBe('a') + }) + + test('when no selection selectNext treats index as zero then advances', () => { + const { result } = renderHook(() => + useSingleSelection({ + options: baseOptions, + initialSelection: null, + })) + act(() => { + result.current.selectNext() + }) + expect(result.current.selection).toBe('b') + }) + + test('controlled selection follows prop', () => { + const onSelectionChange = jest.fn() + const { result, rerender } = renderHook( + (props: { selection: string | null }) => + useSingleSelection({ + options: baseOptions, + selection: props.selection, + onSelectionChange, + }), + { initialProps: { selection: 'a' as string | null } } + ) + expect(result.current.selection).toBe('a') + rerender({ selection: 'b' }) + expect(result.current.selection).toBe('b') + }) +}) diff --git a/tsconfig.json b/tsconfig.json index 67da872..330be3e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,9 @@ "skipLibCheck": true, "baseUrl": ".", "paths": { - "@/*": ["./*"] + "@/*": [ + "./*" + ] } }, "include": [ @@ -24,4 +26,4 @@ "dist", "node_modules" ] -} +} \ No newline at end of file