diff --git a/.husky/post-commit b/.husky/post-commit new file mode 100755 index 0000000..6bff3d4 --- /dev/null +++ b/.husky/post-commit @@ -0,0 +1,6 @@ +#!/usr/bin/env sh +[ -n "$CI" ] && exit 0 + +. "$(dirname -- "$0")/_/husky.sh" + +npm run docs \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push index 858630c..115cd65 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npm run docs && npm run test:ci +npm run test:ci diff --git a/commitlint.config.js b/commitlint.config.js index 4698a12..e40d319 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,6 +1,6 @@ export default { extends: ['@commitlint/config-conventional'], rules: { - 'footer-max-length': [2, 'never'] + 'footer-max-length': [0] } }; diff --git a/docs/components.md b/docs/components.md index d573de4..3396852 100644 --- a/docs/components.md +++ b/docs/components.md @@ -9,6 +9,9 @@ - [Form](../src/components/Form//README.md) - [Header](../src/components/Header//README.md) - [Input](../src/components/Input//README.md) +- [InputDatePicker](../src/components/InputDatePicker//README.md) +- [InputRadio](../src/components/InputRadio//README.md) +- [InputSelect](../src/components/InputSelect//README.md) - [Nav](../src/components/Nav//README.md) - [QuickSearchForm](../src/components/QuickSearchForm//README.md) - [Spinner](../src/components/Spinner//README.md) diff --git a/package-lock.json b/package-lock.json index bbacb0f..044c247 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,14 @@ "eslint-config" ], "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", + "@fortawesome/free-regular-svg-icons": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/vue-fontawesome": "^3.0.8", "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", + "@vuepic/vue-datepicker": "^10.0.0", "fs-extra": "^11.1.1", "happy-dom": "^6.0.4", "minimist": "^1.2.8", @@ -27,6 +33,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/release-notes-generator": "^12.1.0", + "@types/lodash": "^4.17.13", "@types/node": "^20.8.9", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", @@ -1339,6 +1346,67 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", + "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", + "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/vue-fontawesome": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz", + "integrity": "sha512-yyHHAj4G8pQIDfaIsMvQpwKMboIZtcHTUvPqXjOHyldh1O1vZfH4W03VDPv5RvI9P6DLTzJQlmVgj9wCf7c2Fw==", + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "vue": ">= 3.0.0 < 4" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -2623,6 +2691,12 @@ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true + }, "node_modules/@types/minimist": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.4.tgz", @@ -3321,6 +3395,20 @@ } } }, + "node_modules/@vuepic/vue-datepicker": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-10.0.0.tgz", + "integrity": "sha512-ujlk3ahftVQpyCJ8hq7TmOOHrf/XFJI1ZcAh/FRB5Ci62Vq5HmHf6xux5KVi5SPUFRTJY78m+uDhYy1M+8RZ9w==", + "dependencies": { + "date-fns": "^4.1.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "vue": ">=3.2.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3935,9 +4023,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001625", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", - "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "dev": true, "funding": [ { @@ -4996,6 +5084,15 @@ "node": ">=8" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -18038,6 +18135,49 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==" }, + "@fortawesome/fontawesome-common-types": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz", + "integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==" + }, + "@fortawesome/fontawesome-svg-core": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz", + "integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.6.0" + } + }, + "@fortawesome/free-brands-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz", + "integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.6.0" + } + }, + "@fortawesome/free-regular-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz", + "integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.6.0" + } + }, + "@fortawesome/free-solid-svg-icons": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz", + "integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==", + "requires": { + "@fortawesome/fontawesome-common-types": "6.6.0" + } + }, + "@fortawesome/vue-fontawesome": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.8.tgz", + "integrity": "sha512-yyHHAj4G8pQIDfaIsMvQpwKMboIZtcHTUvPqXjOHyldh1O1vZfH4W03VDPv5RvI9P6DLTzJQlmVgj9wCf7c2Fw==", + "requires": {} + }, "@humanwhocodes/config-array": { "version": "0.11.13", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", @@ -19004,6 +19144,12 @@ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, + "@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true + }, "@types/minimist": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.4.tgz", @@ -19503,6 +19649,14 @@ "vue-demi": "^0.13.11" } }, + "@vuepic/vue-datepicker": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-10.0.0.tgz", + "integrity": "sha512-ujlk3ahftVQpyCJ8hq7TmOOHrf/XFJI1ZcAh/FRB5Ci62Vq5HmHf6xux5KVi5SPUFRTJY78m+uDhYy1M+8RZ9w==", + "requires": { + "date-fns": "^4.1.0" + } + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -19919,9 +20073,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001625", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", - "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "version": "1.0.30001680", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001680.tgz", + "integrity": "sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==", "dev": true }, "cardinal": { @@ -20691,6 +20845,11 @@ "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", "dev": true }, + "date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==" + }, "de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", diff --git a/package.json b/package.json index acac0a1..de0871b 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@semantic-release/release-notes-generator": "^12.1.0", + "@types/lodash": "^4.17.13", "@types/node": "^20.8.9", "@typescript-eslint/eslint-plugin": "^6.9.1", "@typescript-eslint/parser": "^6.9.1", @@ -85,8 +86,14 @@ "vue-tsc": "^1.8.22" }, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", + "@fortawesome/free-regular-svg-icons": "^6.6.0", + "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@fortawesome/vue-fontawesome": "^3.0.8", "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", + "@vuepic/vue-datepicker": "^10.0.0", "fs-extra": "^11.1.1", "happy-dom": "^6.0.4", "minimist": "^1.2.8", diff --git a/scripts/update-docs.sh b/scripts/update-docs.sh index c17de7f..e00374f 100644 --- a/scripts/update-docs.sh +++ b/scripts/update-docs.sh @@ -20,11 +20,11 @@ done # create a commit, only if there are changes if git diff --quiet docs/$output_file; then - echo -e "No new component README files detected.\nProceeding with push" + echo -e "No new component README files detected.\nNo new commit will be created." exit 0 else commit_msg="chore(docs): auto-update to component docs" echo "New README files detected, committing updated docs/$output_file file..." - git add docs/$output_file && git commit -m "$commit_msg" --no-verify && echo "Commit finished, proceeding with push" + git add docs/$output_file && git commit -m "$commit_msg" --no-verify && echo "Updated documentation added to the TOC in \`docs/components.md\` and committed." fi diff --git a/src/components/Button/PdapButton.vue b/src/components/Button/PdapButton.vue index 36ccd9c..5c92527 100644 --- a/src/components/Button/PdapButton.vue +++ b/src/components/Button/PdapButton.vue @@ -1,12 +1,14 @@ - - diff --git a/src/components/Footer/README.md b/src/components/Footer/README.md index fd5bb54..1f9b742 100644 --- a/src/components/Footer/README.md +++ b/src/components/Footer/README.md @@ -2,32 +2,49 @@ ## Props -| name | required? | types | description | default | -| --------------------- | --------- | -------- | ---------------------- | ----------------------------------------------------------- | -| `logoImageSrc` | no | `string` | Source of logo image | `'node_modules/pdap-design-system/dist/images/acronym.svg'` | -| `logoImageAnchorPath` | no | `string` | Flex alignment presets | `/` | +| name | required? | types | description | default | +| ----------------- | --------- | ---------------------------------- | ---------------------------- | ------- | +| `fundraisingData` | yes | `{ raised: string; goal: string }` | data from donor box campaign | | ## Notes The `Footer` component provides support for overriding the default social links. The `links` variable is `inject`ed by the component, using the following defaults: -``` +```vue export default { ... inject: { footerLinks: { default: [ { - to: 'https://github.com/orgs/Police-Data-Accessibility-Project', - text: 'Github', + href: 'https://github.com/orgs/Police-Data-Accessibility-Project', + text: 'Github', + icon: FOOTER_LINK_ICONS.GITHUB, + }, + { + href: 'https://discord.gg/wMqex8nKZJ', + text: 'Discord', + icon: FOOTER_LINK_ICONS.DISCORD, + }, + { + href: 'https://www.linkedin.com/company/pdap', + text: 'LinkedIn', + icon: FOOTER_LINK_ICONS.LINKEDIN, }, { - to: 'ttps://discord.gg/wMqex8nKZJ', - text: 'Discord', + href: 'https://pdap.io/jobs', + text: 'Jobs', + icon: FOOTER_LINK_ICONS.JOBS, }, { - to: 'https://www.linkedin.com/company/pdap', - text: 'LinkedIn', + href: 'https://newsletter.pdap.io/', + text: 'Newsletter', + icon: FOOTER_LINK_ICONS.NEWSLETTER, + }, + { + href: 'https://docs.pdap.io/', + text: 'Docs', + icon: FOOTER_LINK_ICONS.DOCS, }, ] } @@ -44,7 +61,7 @@ If we desire different links somewhere that `Footer` is rendered, simply `provid ## Example -``` +```vue @@ -37,84 +42,83 @@ import { } from './types'; import PdapInputText from './Text/InputText.vue'; import PdapInputCheckbox from './Checkbox/InputCheckbox.vue'; +import RecordTypeIcon from '../RecordTypeIcon/RecordTypeIcon.vue'; const props = withDefaults(defineProps(), {}); const errorMessageId = computed(() => `pdap-${props.name}-input-error`); - diff --git a/src/components/Input/__snapshots__/input.spec.ts.snap b/src/components/Input/__snapshots__/input.spec.ts.snap index f0913c8..52e20c3 100644 --- a/src/components/Input/__snapshots__/input.spec.ts.snap +++ b/src/components/Input/__snapshots__/input.spec.ts.snap @@ -4,7 +4,11 @@ exports[`Input component > Renders checkbox input in error state 1`] = `
error message
- +
`; @@ -12,7 +16,11 @@ exports[`Input component > Renders checkbox input in okay state 1`] = `
- +
`; @@ -20,7 +28,11 @@ exports[`Input component > Renders password input in error state 1`] = `
error message
- +
`; @@ -28,7 +40,11 @@ exports[`Input component > Renders password input in okay state 1`] = `
- +
`; @@ -36,7 +52,11 @@ exports[`Input component > Renders text input in error state 1`] = `
error message
- +
`; @@ -44,6 +64,10 @@ exports[`Input component > Renders text input in okay state 1`] = `
- +
`; diff --git a/src/components/InputCheckbox/PdapInputCheckbox.vue b/src/components/InputCheckbox/PdapInputCheckbox.vue new file mode 100644 index 0000000..c47c6a9 --- /dev/null +++ b/src/components/InputCheckbox/PdapInputCheckbox.vue @@ -0,0 +1,49 @@ + + + diff --git a/src/components/InputCheckbox/index.ts b/src/components/InputCheckbox/index.ts new file mode 100644 index 0000000..ebdb38b --- /dev/null +++ b/src/components/InputCheckbox/index.ts @@ -0,0 +1 @@ +export { default as InputCheckbox } from './PdapInputCheckbox.vue'; diff --git a/src/components/InputCheckbox/types.ts b/src/components/InputCheckbox/types.ts new file mode 100644 index 0000000..691e4ab --- /dev/null +++ b/src/components/InputCheckbox/types.ts @@ -0,0 +1,6 @@ +export interface PdapInputCheckboxProps { + id: string; + label?: string; + name: string; + defaultChecked?: boolean; +} diff --git a/src/components/InputDatePicker/PdapInputDatePicker.vue b/src/components/InputDatePicker/PdapInputDatePicker.vue new file mode 100644 index 0000000..dafd56b --- /dev/null +++ b/src/components/InputDatePicker/PdapInputDatePicker.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/src/components/InputDatePicker/README.md b/src/components/InputDatePicker/README.md new file mode 100644 index 0000000..37e4792 --- /dev/null +++ b/src/components/InputDatePicker/README.md @@ -0,0 +1,53 @@ +# InputSelect +Date picker component. Uses Vue3 Date Picker library under the hood. + +## Props - required + +| name | required? | types | description | default | +| ------- | ----------------------------- | -------- | ------------- | ------- | +| `id` | yes | `string` | id attr | | +| `label` | yes, if label slot not passed | `string` | label content | | +| `name` | yes | `string` | name attr | | + +## Props - Vue3 Date Picker +The props interface extends the underlying component interface, so [all props available on the Vue 3 Date Picker component](https://vue3datepicker.com/props/modes/) are available to be passed. + +## Slots + +| name | required? | types | description | default | +| ------- | ----------------------------- | --------- | ------------------------------------ | ------- | +| `error` | no* | `Element` | slot content to be rendered as error | | +| `label` | yes, if label prop not passed | `Element` | slot content to be rendered as label | | + +* Note: The error message is determined by Vuelidate via our form validation schema. If the error UI needs to be more complicated than a string that can be passed with the schema, pass an `\#error` slot and it will override the string. + +## Example + +```vue + + + + +... +``` diff --git a/src/components/InputDatePicker/__snapshots__/input-date-picker.spec.ts.snap b/src/components/InputDatePicker/__snapshots__/input-date-picker.spec.ts.snap new file mode 100644 index 0000000..67a04ee --- /dev/null +++ b/src/components/InputDatePicker/__snapshots__/input-date-picker.spec.ts.snap @@ -0,0 +1,50 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`PdapInputDatePicker > Rendering > does not render label when neither prop nor slot is provided 1`] = ` +
+ + + +
+`; + +exports[`PdapInputDatePicker > Rendering > renders complex label slot content 1`] = ` +
+ + + +
+`; + +exports[`PdapInputDatePicker > Rendering > renders error slot when slot is provided 1`] = ` +
+ +
+ Custom Error Message +
+ +
+`; + +exports[`PdapInputDatePicker > Rendering > renders label prop when no slot is provided 1`] = ` +
+ + + +
+`; + +exports[`PdapInputDatePicker > Rendering > renders label slot when provided 1`] = ` +
+ + + +
+`; diff --git a/src/components/InputDatePicker/index.ts b/src/components/InputDatePicker/index.ts new file mode 100644 index 0000000..576327b --- /dev/null +++ b/src/components/InputDatePicker/index.ts @@ -0,0 +1 @@ +export { default as InputDatePicker } from './PdapInputDatePicker.vue'; diff --git a/src/components/InputDatePicker/input-date-picker.spec.ts b/src/components/InputDatePicker/input-date-picker.spec.ts new file mode 100644 index 0000000..04389cb --- /dev/null +++ b/src/components/InputDatePicker/input-date-picker.spec.ts @@ -0,0 +1,349 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import PdapInputDatePicker from './PdapInputDatePicker.vue'; +import { provideKey } from '../FormV2/util'; +import VueDatePicker from '@vuepic/vue-datepicker'; +import { ref } from 'vue'; + +describe('PdapInputDatePicker', () => { + let wrapper: VueWrapper; + const mockSetValues = vi.fn(); + const mockValues = ref({}); + const mockV$ = ref({ + testName: { + $error: false, + $errors: [], + }, + }); + + const defaultProps = { + name: 'testName', + id: 'test-id', + label: 'Test Label', + }; + + const removeEventListener = vi.fn(); + // Mock window.matchMedia + const mockMatchMedia = vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener, + dispatchEvent: vi.fn(), + })); + + beforeEach(() => { + global.MediaQueryListEvent = vi + .fn() + .mockImplementation((type, eventInitDict) => ({ + type, + matches: eventInitDict.matches, + media: '', + target: { + matches: eventInitDict.matches, + }, + })); + + window.matchMedia = mockMatchMedia; + wrapper = mount(PdapInputDatePicker, { + props: defaultProps, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + stubs: { + VueDatePicker: true, + }, + }, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + vi.unstubAllGlobals(); + }); + + describe('Rendering', () => { + it('renders correctly with default props', () => { + expect(wrapper.exists()).toBe(true); + expect(wrapper.find('label').text()).toBe('Test Label'); + expect(wrapper.findComponent(VueDatePicker).exists()).toBe(true); + }); + + it('renders label prop when no slot is provided', () => { + wrapper = mount(PdapInputDatePicker, { + props: { + ...defaultProps, + label: 'Label from prop', + }, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + stubs: { + VueDatePicker: true, + }, + }, + }); + + const label = wrapper.find(`label[for="${defaultProps.id}"]`); + expect(label.exists()).toBe(true); + expect(label.text()).toBe('Label from prop'); + expect(label.attributes('id')).toBe( + `${defaultProps.name}-${defaultProps.id}-label` + ); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('renders label slot when provided', () => { + wrapper = mount(PdapInputDatePicker, { + props: { + ...defaultProps, + label: 'Label from prop', // This should be ignored when slot is present + }, + slots: { + label: 'Custom Label Content', + }, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + stubs: { + VueDatePicker: true, + }, + }, + }); + + const label = wrapper.find(`label[for="${defaultProps.id}"]`); + expect(label.exists()).toBe(true); + expect(label.find('.custom-label').exists()).toBe(true); + expect(label.text()).toBe('Custom Label Content'); + expect(label.text()).not.toBe('Label from prop'); + expect(label.attributes('id')).toBe( + `${defaultProps.name}-${defaultProps.id}-label` + ); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('does not render label when neither prop nor slot is provided', () => { + wrapper = mount(PdapInputDatePicker, { + props: { + name: 'testName', + id: 'test-id', + // label prop intentionally omitted + }, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + stubs: { + VueDatePicker: true, + }, + }, + }); + + expect(wrapper.find('label').exists()).toBe(false); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('renders complex label slot content', () => { + wrapper = mount(PdapInputDatePicker, { + props: { + ...defaultProps, + }, + slots: { + label: ` +
+ Complex Label + * Required field +
+ `, + }, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + stubs: { + VueDatePicker: true, + }, + }, + }); + + const label = wrapper.find(`label[for="${defaultProps.id}"]`); + expect(label.exists()).toBe(true); + expect(label.find('.complex-label').exists()).toBe(true); + expect(label.find('.label-title').text()).toBe('Complex Label'); + expect(label.find('.label-hint').text()).toBe('* Required field'); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('renders error slot when slot is provided', () => { + mockV$.value = { + ...mockV$.value, + testName: { + $error: true, + // @ts-expect-error + $errors: [{ $message: 'Error Message' }], + }, + }; + + wrapper = mount(PdapInputDatePicker, { + props: { + ...defaultProps, + }, + slots: { + error: 'Custom Error Message', + }, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + stubs: { + VueDatePicker: true, + }, + }, + }); + + const errorElement = wrapper.find( + '.pdap-input-error-message .custom-error' + ); + expect(errorElement.exists()).toBe(true); + expect(errorElement.text()).toBe('Custom Error Message'); + expect(wrapper.html()).toMatchSnapshot(); + }); + }); + + describe('Form Integration', () => { + it('calls setValues when date changes', async () => { + const datePicker = wrapper.findComponent(VueDatePicker); + const newDate = new Date('2024-01-01'); + + await datePicker.vm.$emit('update:modelValue', newDate); + + expect(mockSetValues).toHaveBeenCalledWith({ + [defaultProps.name]: newDate, + }); + }); + + it('updates date when form values change externally', async () => { + const newDate = new Date('2024-01-01'); + mockValues.value = { + [defaultProps.name]: newDate, + }; + + await wrapper.vm.$nextTick(); + + // @ts-expect-error + expect(wrapper.vm.date).toEqual(newDate); + }); + + it('clears date when form value is removed', async () => { + mockValues.value = { + [defaultProps.name]: undefined, + }; + + await wrapper.vm.$nextTick(); + + // @ts-expect-error + expect(wrapper.vm.date).toBeUndefined(); + }); + }); + + describe('Dark Mode', () => { + it('initializes with system dark mode preference', () => { + const darkModeMatchMedia = vi.fn().mockImplementation(() => ({ + matches: true, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + })); + window.matchMedia = darkModeMatchMedia; + + wrapper = mount(PdapInputDatePicker, { + props: defaultProps, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + stubs: { + VueDatePicker: true, + }, + }, + }); + + // @ts-expect-error + expect(wrapper.vm.darkModePreference).toBe(true); + }); + + it('updates dark mode preference when system preference changes', async () => { + const mockEvent = { + matches: true, + type: 'change', + } as MediaQueryListEvent; + + // @ts-expect-error + wrapper.vm.updateColorMode(mockEvent); + await wrapper.vm.$nextTick(); + + // @ts-expect-error + expect(wrapper.vm.darkModePreference).toBe(true); + }); + }); + + describe('Validation', () => { + it('shows validation error from v$', async () => { + mockV$.value = { + testName: { + $error: true, + // @ts-expect-error + $errors: [{ $message: 'Validation error' }], + }, + }; + + await wrapper.vm.$nextTick(); + + expect(wrapper.find('.pdap-input-error-message').text()).toBe( + 'Validation error' + ); + }); + }); + + describe('Cleanup', () => { + it('removes event listener on unmount', async () => { + wrapper.unmount(); + + expect(removeEventListener).toHaveBeenCalledWith( + 'change', + expect.any(Function) + ); + }); + }); +}); diff --git a/src/components/InputDatePicker/types.ts b/src/components/InputDatePicker/types.ts new file mode 100644 index 0000000..83bfd57 --- /dev/null +++ b/src/components/InputDatePicker/types.ts @@ -0,0 +1,7 @@ +import { VueDatePickerProps } from '@vuepic/vue-datepicker'; + +export interface PdapDatePickerProps extends VueDatePickerProps { + id: string; + label?: string; + name: string; +} diff --git a/src/components/InputPassword/PdapInputPassword.vue b/src/components/InputPassword/PdapInputPassword.vue new file mode 100644 index 0000000..7662ec6 --- /dev/null +++ b/src/components/InputPassword/PdapInputPassword.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/src/components/InputPassword/index.ts b/src/components/InputPassword/index.ts new file mode 100644 index 0000000..0e15271 --- /dev/null +++ b/src/components/InputPassword/index.ts @@ -0,0 +1 @@ +export { default as InputPassword } from './PdapInputPassword.vue'; diff --git a/src/components/InputRadio/PdapInputRadio.vue b/src/components/InputRadio/PdapInputRadio.vue new file mode 100644 index 0000000..075538c --- /dev/null +++ b/src/components/InputRadio/PdapInputRadio.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/components/InputRadio/README.md b/src/components/InputRadio/README.md new file mode 100644 index 0000000..7c1d9e4 --- /dev/null +++ b/src/components/InputRadio/README.md @@ -0,0 +1,53 @@ +# InputRadio +Radio input. Designed to be wrapped with `RadioGroup` + +## Props - required + +| name | required? | types | description | default | +| ---------------- | ----------------------------- | --------- | --------------------------------------------- | ------- | +| `defaultChecked` | no | `boolean` | radio is checked by default. Only 1 per group | | +| `id` | yes | `string` | id attr | | +| `label` | yes, if label slot not passed | `string` | label content | | +| `name` | yes | `string` | name attr | | + +## Slots + +| name | required? | types | description | default | +| ------- | ----------------------------- | --------- | ------------------------------------ | ------- | +| `label` | yes, if label prop not passed | `Element` | slot content to be rendered as label | | + +## Example + +```vue + + + + +... +``` diff --git a/src/components/InputRadio/index.ts b/src/components/InputRadio/index.ts new file mode 100644 index 0000000..e8e5c4c --- /dev/null +++ b/src/components/InputRadio/index.ts @@ -0,0 +1 @@ +export { default as InputRadio } from './PdapInputRadio.vue'; diff --git a/src/components/InputRadio/input-radio.spec.ts b/src/components/InputRadio/input-radio.spec.ts new file mode 100644 index 0000000..a7d00a2 --- /dev/null +++ b/src/components/InputRadio/input-radio.spec.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import PdapInputRadio from './PdapInputRadio.vue'; +import { provideKey } from '../FormV2/util'; +import { ref } from 'vue'; + +describe('PdapInputRadio', () => { + let wrapper: VueWrapper; + const mockSetValues = vi.fn(); + const mockValues = ref({}); + const mockV$ = ref({ + testName: { + $error: false, + $errors: [], + }, + }); + + const defaultProps = { + name: 'testName', + id: 'test-id', + label: 'Test Label', + value: 'test-value', + }; + + beforeEach(() => { + mockSetValues.mockClear(); + }); + + const createWrapper = (props = {}, provide = {}, slots = {}) => { + return mount(PdapInputRadio, { + props: { + ...defaultProps, + ...props, + }, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + ...provide, + }, + }, + }, + slots: { ...slots }, + }); + }; + + it('renders correctly with default props', () => { + wrapper = createWrapper(); + expect(wrapper.find('input[type="radio"]').exists()).toBe(true); + expect(wrapper.find('label').text()).toBe('Test Label'); + }); + + it('renders with slot label instead of prop label', () => { + wrapper = createWrapper( + {}, + {}, + { + label: 'Slot Label', + } + ); + expect(wrapper.find('label span').text()).toBe('Slot Label'); + }); + + it('throws error when no label passed as slot or prop', async () => { + expect(() => { + wrapper = createWrapper({ label: undefined }, {}, {}); + }).toThrow('All form inputs must have a label, passed as a slot or a prop'); + }); + + it('emits input event and calls setValues when changed', async () => { + wrapper = createWrapper(); + const input = wrapper.find('input'); + await input.setValue(true); + expect(mockSetValues).toHaveBeenCalledWith({ + [defaultProps.name]: defaultProps.value, + }); + }); + + it('renders with defaultChecked prop', () => { + wrapper = createWrapper({ defaultChecked: true }); + expect(wrapper.find('input').element.defaultChecked).toBe(true); + }); +}); diff --git a/src/components/InputRadio/types.ts b/src/components/InputRadio/types.ts new file mode 100644 index 0000000..723264b --- /dev/null +++ b/src/components/InputRadio/types.ts @@ -0,0 +1,7 @@ +export interface PdapInputRadioProps { + id: string; + label?: string; + name: string; + defaultChecked?: boolean; + value: string; +} diff --git a/src/components/InputRadioGroup/PdapInputRadioGroup.vue b/src/components/InputRadioGroup/PdapInputRadioGroup.vue new file mode 100644 index 0000000..6362f0b --- /dev/null +++ b/src/components/InputRadioGroup/PdapInputRadioGroup.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/InputRadioGroup/index.ts b/src/components/InputRadioGroup/index.ts new file mode 100644 index 0000000..0949494 --- /dev/null +++ b/src/components/InputRadioGroup/index.ts @@ -0,0 +1 @@ +export { default as RadioGroup } from './PdapInputRadioGroup.vue'; diff --git a/src/components/InputRadioGroup/input-radio-group.spec.ts b/src/components/InputRadioGroup/input-radio-group.spec.ts new file mode 100644 index 0000000..95c2b4e --- /dev/null +++ b/src/components/InputRadioGroup/input-radio-group.spec.ts @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount, VueWrapper } from '@vue/test-utils'; +import RadioGroup from './PdapInputRadioGroup.vue'; +import { provideKey } from '../FormV2/util'; +import { ref } from 'vue'; + +describe('RadioGroup', () => { + let wrapper: VueWrapper; + const mockSetValues = vi.fn(); + const mockValues = ref({}); + const mockV$ = ref({ + testName: { + $error: false, + $errors: [], + }, + }); + + const defaultProps = { + name: 'testName', + }; + + beforeEach(() => { + mockSetValues.mockClear(); + }); + + const createWrapper = (props = {}, slots = {}) => { + return mount(RadioGroup, { + props: { + ...defaultProps, + ...props, + }, + slots, + global: { + provide: { + [provideKey as symbol]: { + setValues: mockSetValues, + values: mockValues, + v$: mockV$, + }, + }, + }, + }); + }; + + it('renders correctly with default props', () => { + wrapper = createWrapper(); + expect(wrapper.find('.pdap-input-radio-group').exists()).toBe(true); + }); + + it('renders slot content', () => { + wrapper = createWrapper( + {}, + { + default: '
Test Content
', + } + ); + expect(wrapper.find('.test-content').exists()).toBe(true); + }); + + it('shows error message when error exists', async () => { + mockV$.value.testName.$error = true; + // @ts-expect-error + mockV$.value.testName.$errors = [{ $message: 'Test error message' }]; + wrapper = createWrapper(); + expect(wrapper.find('.pdap-input-error').exists()).toBe(true); + }); + + it('shows custom error slot when provided and error exists', () => { + mockV$.value.testName.$error = true; + wrapper = createWrapper( + {}, + { + error: '
Custom Error
', + } + ); + expect(wrapper.find('.custom-error').exists()).toBe(true); + }); +}); diff --git a/src/components/InputSelect/PdapInputSelect.vue b/src/components/InputSelect/PdapInputSelect.vue new file mode 100644 index 0000000..5e3d0fb --- /dev/null +++ b/src/components/InputSelect/PdapInputSelect.vue @@ -0,0 +1,371 @@ + + + + + diff --git a/src/components/InputSelect/README.md b/src/components/InputSelect/README.md new file mode 100644 index 0000000..02956ce --- /dev/null +++ b/src/components/InputSelect/README.md @@ -0,0 +1,84 @@ +# InputSelect +Accessible, flexible custom select component. + +_Note: Only works with `FormV2`. The `FormV1` schema system is not set up to handle this input._ + +## Props + +| name | required? | types | description | default | +| ------------- | ----------------------------- | --------------------------------------- | ---------------- | ------------------ | +| `id` | yes | `string` | id attr | | +| `label` | yes, if label slot not passed | `string` | label content | | +| `name` | yes | `string` | name attr | | +| `placeholder` | no | `string` | placeholder attr | "Select an option" | +| `options` | yes | `Array<{value: string; label: string}>` | options | | + +## Slots + +| name | required? | types | description | default | +| ------- | ----------------------------- | --------- | ------------------------------------ | ------- | +| `error` | no* | `Element` | slot content to be rendered as error | | +| `label` | yes, if label prop not passed | `Element` | slot content to be rendered as label | | + +* Note: The error message is determined by Vuelidate via our form validation schema. If the error UI needs to be more complicated than a string that can be passed with the schema, pass an `\#error` slot and it will override the string. + +## Example + +```vue + + + + +... +``` diff --git a/src/components/InputSelect/index.ts b/src/components/InputSelect/index.ts new file mode 100644 index 0000000..8472aa3 --- /dev/null +++ b/src/components/InputSelect/index.ts @@ -0,0 +1 @@ +export { default as InputSelect } from './PdapInputSelect.vue'; diff --git a/src/components/InputSelect/input-select.spec.ts b/src/components/InputSelect/input-select.spec.ts new file mode 100644 index 0000000..2ad40af --- /dev/null +++ b/src/components/InputSelect/input-select.spec.ts @@ -0,0 +1,275 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import PdapInputSelect from './PdapInputSelect.vue'; +import { nextTick } from 'vue'; +import { provideKey } from '../FormV2/util'; + +const options = [ + { value: 'option1', label: 'Option 1' }, + { value: 'option2', label: 'Option 2' }, + { value: 'option3', label: 'Option 3' }, +]; + +const defaultProps = { + name: 'testSelect', + options, + id: 'testId', + label: 'Test Label', +}; + +const mockFormProvide = { + setValues: vi.fn(), + values: {}, + v$: { value: {} }, +}; + +const BASE_DEFAULT = { + props: defaultProps, + global: { + provide: { + [provideKey as symbol]: mockFormProvide, + }, + }, +}; + +describe('PdapInputSelect', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders correctly with default props', () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + expect(wrapper.find('label').text()).toBe('Test Label'); + expect(wrapper.find('.selected-value').text()).toBe('Select an option'); + expect(wrapper.findAll('.pdap-custom-select-option').length).toBe(3); + }); + + it('opens options when clicked', async () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + + await wrapper.find('.pdap-custom-select').trigger('click'); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + expect(wrapper.find('.pdap-custom-select-options').isVisible()).toBe(true); + }); + + it('selects an option when clicked', async () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + + await wrapper.find('.pdap-custom-select').trigger('click'); + await wrapper.findAll('.pdap-custom-select-option')[1].trigger('click'); + + expect(wrapper.find('.selected-value').text()).toBe('Option 2'); + expect(mockFormProvide.setValues).toHaveBeenCalledWith({ + testSelect: 'option2', + }); + }); + + it('handles keyboard navigation', async () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(0); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(1); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.selected-value').text()).toBe('Option 2'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Escape' }); + expect(wrapper.find('.pdap-custom-select').classes()).not.toContain('open'); + }); + + it('displays error message when provided', async () => { + const wrapper = mount(PdapInputSelect, { + ...BASE_DEFAULT, + global: { + ...BASE_DEFAULT.global, + provide: { + ...BASE_DEFAULT.global.provide, + [provideKey as symbol]: { + ...mockFormProvide, + v$: { + value: { + testSelect: { + $error: true, + $errors: [{ $message: 'Error message' }], + }, + }, + }, + }, + }, + }, + }); + + await nextTick(); + expect(wrapper.find('.pdap-input-error-message').exists()).toBe(true); + expect(wrapper.find('.pdap-input-error-message').text()).toBe( + 'Error message' + ); + }); + + // it('updates when form values change', async () => { + // const wrapper = mount(PdapInputSelect, { + // ...BASE_DEFAULT, + // props: defaultProps, + // global: { + // ...BASE_DEFAULT.global, + // provide: { + // [provideKey as symbol]: { + // ...mockFormProvide, + // values: { testSelect: 'option3' }, + // }, + // }, + // }, + // }); + + // await wrapper.vm.$forceUpdate(); + // expect(wrapper.find('.selected-value').text()).toBe('Option 3'); + // }); + + it('handles Tab key navigation', async () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + + // const options = wrapper.findAll('.pdap-custom-select-option'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Tab' }); + expect(wrapper.find('.pdap-custom-select').classes()).not.toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + + // TODO: figure out why this test isn't working + // await wrapper.find('.pdap-custom-select').trigger('keydown', { key: 'Tab' }); + // expect(options[0].classes()).toContain('selected'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Tab' }); + expect(wrapper.emitted('keydown')).toBeTruthy(); + }); + + it('handles ArrowDown key navigation', async () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(0); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(1); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(2); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(0); + }); + + it('handles ArrowUp key navigation', async () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowUp' }); + expect(wrapper.find('.pdap-custom-select').classes()).not.toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(0); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowUp' }); + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(-1); + }); + + it('handles Enter key navigation', async () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'ArrowDown' }); + // @ts-expect-error vm doesn't play well with TS + expect(wrapper.vm.focusedOptionIndex).toBe(0); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.selected-value').text()).toBe('Option 1'); + expect(wrapper.find('.pdap-custom-select').classes()).not.toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + }); + + it('handles Escape key navigation', async () => { + const wrapper = mount(PdapInputSelect, BASE_DEFAULT); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Enter' }); + expect(wrapper.find('.pdap-custom-select').classes()).toContain('open'); + + await wrapper + .find('.pdap-custom-select') + .trigger('keydown', { key: 'Escape' }); + expect(wrapper.find('.pdap-custom-select').classes()).not.toContain('open'); + }); +}); diff --git a/src/components/InputSelect/types.ts b/src/components/InputSelect/types.ts new file mode 100644 index 0000000..1928495 --- /dev/null +++ b/src/components/InputSelect/types.ts @@ -0,0 +1,17 @@ +export interface PdapSelectOption { + value: string; + label: string; +} + +export interface PdapInputSelectProps { + id: string; + label?: string; + name: string; + placeholder?: string; + options: PdapSelectOption[]; + combobox?: boolean; + filter?: ( + searchText: string, + options: PdapSelectOption[] + ) => PdapSelectOption[]; +} diff --git a/src/components/InputText/PdapInputText.vue b/src/components/InputText/PdapInputText.vue new file mode 100644 index 0000000..d8952d9 --- /dev/null +++ b/src/components/InputText/PdapInputText.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/components/InputText/index.ts b/src/components/InputText/index.ts new file mode 100644 index 0000000..9cfa813 --- /dev/null +++ b/src/components/InputText/index.ts @@ -0,0 +1 @@ +export { default as InputText } from './PdapInputText.vue'; diff --git a/src/components/InputText/types.ts b/src/components/InputText/types.ts new file mode 100644 index 0000000..99ba44b --- /dev/null +++ b/src/components/InputText/types.ts @@ -0,0 +1,6 @@ +export interface PdapInputTextProps { + id: string; + label?: string; + name: string; + placeholder?: string; +} diff --git a/src/components/InputTextArea/PdapInputTextArea.vue b/src/components/InputTextArea/PdapInputTextArea.vue new file mode 100644 index 0000000..3867186 --- /dev/null +++ b/src/components/InputTextArea/PdapInputTextArea.vue @@ -0,0 +1,48 @@ +