From 316dc815c15e10e095837be50156a17b2817a6c8 Mon Sep 17 00:00:00 2001 From: Thinh Trinh Date: Tue, 30 Jan 2024 08:58:41 +0700 Subject: [PATCH 1/8] chore: fix broken link (#56) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 323df4a..4c0796f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ React Awesome Components is a collection of modern, reusable and well-tested Rea ## Getting Started -Visit https://react-awesome-components.vercel.app/introduction to get started with React Awesome Components. +Visit https://react-awesome-components.vercel.app/introduction to get started with React Awesome Components. ## Documentation From 9e81d96192d0a504f23be093dfbf6ab85be50886 Mon Sep 17 00:00:00 2001 From: Thinh Trinh Date: Tue, 30 Jan 2024 09:00:44 +0700 Subject: [PATCH 2/8] chore: fix broken link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4c0796f..d53d467 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ React Awesome Components is a collection of modern, reusable and well-tested Rea ## Getting Started -Visit https://react-awesome-components.vercel.app/introduction to get started with React Awesome Components. +Visit https://react-awesome-components.vercel.app to get started with React Awesome Components. ## Documentation From d54823b884ea4d7e25d45b91e3d66ef402fa9ca2 Mon Sep 17 00:00:00 2001 From: Thinh Trinh Date: Tue, 30 Jan 2024 14:11:03 +0700 Subject: [PATCH 3/8] docs: added use click outside docs (#59) --- apps/docs/package.json | 1 + apps/docs/src/pages/_meta.json | 9 +- apps/docs/src/pages/about.mdx | 25 ++++++ apps/docs/src/pages/docs/_meta.json | 3 + .../docs/src/pages/docs/use-click-outside.mdx | 90 +++++++++++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 apps/docs/src/pages/about.mdx create mode 100644 apps/docs/src/pages/docs/use-click-outside.mdx diff --git a/apps/docs/package.json b/apps/docs/package.json index 1f76716..43e8a04 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@react-awesome/components": "1.0.6", + "classnames": "^2.5.1", "lucide-react": "^0.315.0", "next": "^14.0.4", "nextra": "^2.13.2", diff --git a/apps/docs/src/pages/_meta.json b/apps/docs/src/pages/_meta.json index 77a6f86..1b5352e 100644 --- a/apps/docs/src/pages/_meta.json +++ b/apps/docs/src/pages/_meta.json @@ -14,9 +14,16 @@ "playground": { "type": "page", "title": "Playground", - "href": "https://codesandbox.io/p/devbox/modern-bush-2mzr3w?file=%2Fsrc%2FApp.tsx%3A15%2C1", + "href": "https://codesandbox.io/p/devbox/determined-moon-y7lw4f", "newWindow": true }, + "about": { + "type": "page", + "title": "About", + "theme": { + "typesetting": "article" + } + }, "404": { "type": "page", "theme": { diff --git a/apps/docs/src/pages/about.mdx b/apps/docs/src/pages/about.mdx new file mode 100644 index 0000000..d75a0af --- /dev/null +++ b/apps/docs/src/pages/about.mdx @@ -0,0 +1,25 @@ +--- +title: About +--- + +# About React Awesome Components + +Hi guys! My name is [Thinh](https://github.com/trinhthinh388), I've been working as a Fronend Developer for more than {new Date().getFullYear() - 2020} years. During my career, there are so many +components or UI logic that could be reused in the other projects. Thus, we've seen so many libraries having different names but just do exactly same thing, +it makes me tired of choosing the library to best fit my needs. That's why **React Awesome Components** was born. + +With **React Awesome Components** we don't have to spend time on finding the next package to use in your project. Just install `@react-awesome/components`, one package that solve every problem. + +# Credits + +**React Awesome Components** is powered by these incredible open source projects: + +- https://reactjs.org +- https://nextra.site +- https://tailwindcss.com +- https://www.npmjs.com/package/libphonenumber-js +- https://www.npmjs.com/package/@uidotdev/usehooks + +## License + +The **React Awesome Components** project are licensed under the MIT license. diff --git a/apps/docs/src/pages/docs/_meta.json b/apps/docs/src/pages/docs/_meta.json index 10a54cb..491d573 100644 --- a/apps/docs/src/pages/docs/_meta.json +++ b/apps/docs/src/pages/docs/_meta.json @@ -20,6 +20,9 @@ "use-preserve-input-caret-position": { "title": "usePreserveInputCaretPosition" }, + "use-click-outside": { + "title": "useClickOutside" + }, "-- More": { "type": "separator", "title": "More" diff --git a/apps/docs/src/pages/docs/use-click-outside.mdx b/apps/docs/src/pages/docs/use-click-outside.mdx new file mode 100644 index 0000000..765b669 --- /dev/null +++ b/apps/docs/src/pages/docs/use-click-outside.mdx @@ -0,0 +1,90 @@ +--- +title: useClickOutside +--- + +# useClickOutside + +**useClickOutside** triggers callback when user clicks on the outside area of an element. + +## Install + +To start using **useClickOutside**, you can install the `@react-awesome/use-click-outside` library or you can import it directly from `@react-awesome/components` if you have installed it before. In your project directory, run +the following command to install the dependencies: + +```sh npm2yarn +npm i @react-awesome/use-click-outside +``` + +## Usage + +import { useState } from 'react' +import { Container } from '../../components/Container' +import { useClickOutside } from '@react-awesome/components' + +export const Example = () => { + const [ref, setRef] = useState(null) + const [state, setState] = useState(false) + +useClickOutside(ref, () => setState(false)) + +return ( + +
+
+ + +

Hey! click outside to close me.

+
+
+
+) } + + + + + +```jsx +import { useClickOutside } from '@react-awesome/use-click-outside' + +const Example = () => { + const ref = useRef(null) + const [state, setState] = useState(false) + + useClickOutside(ref.current, () => { + setState(false) + }) + + return ( + <> +
+ + +

Hey! click outside to close me.

+
+
+ + ) +} +``` + +## Parameters + +The `useClickOutside` takes the following parameters: + +#### `inputEl` + +- Type: `HTMLInputElement` + +#### `callback` + +- Type: `(event: MouseEvent | undefined) => any` From 5901d7f1f11dd0e1079e4da0c7572900e7f08a7e Mon Sep 17 00:00:00 2001 From: Thinh Trinh Date: Tue, 30 Jan 2024 16:53:17 +0700 Subject: [PATCH 4/8] chore: update about.mdx --- apps/docs/src/pages/about.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/docs/src/pages/about.mdx b/apps/docs/src/pages/about.mdx index d75a0af..9464281 100644 --- a/apps/docs/src/pages/about.mdx +++ b/apps/docs/src/pages/about.mdx @@ -5,10 +5,12 @@ title: About # About React Awesome Components Hi guys! My name is [Thinh](https://github.com/trinhthinh388), I've been working as a Fronend Developer for more than {new Date().getFullYear() - 2020} years. During my career, there are so many -components or UI logic that could be reused in the other projects. Thus, we've seen so many libraries having different names but just do exactly same thing, +components or UI logic that could be reused in the other projects. Thus, I've seen so many libraries having different names but just do exactly same thing, it makes me tired of choosing the library to best fit my needs. That's why **React Awesome Components** was born. -With **React Awesome Components** we don't have to spend time on finding the next package to use in your project. Just install `@react-awesome/components`, one package that solve every problem. +With **React Awesome Components** you don't have to spend time on finding the next package to use in your project. Just install `@react-awesome/components`. + +One package to solve every problem. # Credits From e31832c15d1b46825a5657b1236aa8972535f002 Mon Sep 17 00:00:00 2001 From: Thinh Trinh Date: Tue, 30 Jan 2024 21:29:34 +0700 Subject: [PATCH 5/8] test: added tests for phone input (#60) * test: added tests for phone input * chore: updated prop desc * chore: updated prop name --- .../docs/phone-input/use-phone-input.mdx | 8 - .../src/PhoneInput/PhoneInput.spec.tsx | 114 ++++++++++ .../src/hooks/usePhoneInput.spec.tsx | 210 ++++++++++++++++++ .../phone-input/src/hooks/usePhoneInput.tsx | 10 +- vite.config.ts | 8 +- 5 files changed, 333 insertions(+), 17 deletions(-) create mode 100644 packages/phone-input/src/PhoneInput/PhoneInput.spec.tsx create mode 100644 packages/phone-input/src/hooks/usePhoneInput.spec.tsx diff --git a/apps/docs/src/pages/docs/phone-input/use-phone-input.mdx b/apps/docs/src/pages/docs/phone-input/use-phone-input.mdx index 125d9bb..009c407 100644 --- a/apps/docs/src/pages/docs/phone-input/use-phone-input.mdx +++ b/apps/docs/src/pages/docs/phone-input/use-phone-input.mdx @@ -116,14 +116,6 @@ The callback's `event` could be `undefined` when the event is triggered when use - Type: `(ev: React.ChangeEvent | undefined, metadata: PhoneInputChangeMetadata) => void` - Default: `() => void 0` -#### `guessOn` (optional) - -Specify event to trigger the guess country function. -When `guessOn` is `true`, the behaviour is equivalent with `change`. - -- Type: `'blur' | 'change' | boolean` -- Default: `change` - #### `smartCaret` (optional) Use smart caret. diff --git a/packages/phone-input/src/PhoneInput/PhoneInput.spec.tsx b/packages/phone-input/src/PhoneInput/PhoneInput.spec.tsx new file mode 100644 index 0000000..230d6f7 --- /dev/null +++ b/packages/phone-input/src/PhoneInput/PhoneInput.spec.tsx @@ -0,0 +1,114 @@ +import { PhoneInput } from './PhoneInput' +import { render } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { act } from 'react-dom/test-utils' +import '@testing-library/jest-dom' + +const user = userEvent.setup({ + delay: 300, +}) + +const Comp = () => { + return ( +
+ +
+ ) +} + +describe('PhoneInput', () => { + it('should render phone input', async () => { + const { container } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + input.focus() + await user.keyboard('{a},{b},{c},{1},{2},{3}') + }) + + expect(input.getAttribute('value')).toBe('+1 23') + }) + + it('should show country select', async () => { + const { container } = render() + + const input = container.querySelector('input') + const countrySelect = container.querySelector('button') + + if (!input || !countrySelect) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + await user.click(countrySelect) + }) + + expect( + container.querySelector('ul[class*=selectList]')?.parentElement, + ).toBeVisible() + }) + + it('should close country select when click outside', async () => { + const { container } = render() + + const input = container.querySelector('input') + const countrySelect = container.querySelector('div[class*=countrySelect]') + + if (!input || !countrySelect) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + await user.click(countrySelect) + }) + + expect( + container.querySelector('ul[class*=selectList]')?.parentElement, + ).toBeVisible() + + await act(async () => { + await user.click(input) + }) + + expect( + container + .querySelector('ul[class*=selectList]') + ?.parentElement?.classList.value.includes('hidden'), + ).toBe(true) + }) + + it('should close the country select when click on country option', async () => { + const { container } = render() + + const input = container.querySelector('input') + const countrySelect = container.querySelector('div[class*=countrySelect]') + const opt = container.querySelector('li') + + if (!input || !countrySelect || !opt) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + await user.click(countrySelect) + }) + + expect( + container.querySelector('ul[class*=selectList]')?.parentElement, + ).toBeVisible() + + await act(async () => { + await user.click(opt) + }) + + expect( + container + .querySelector('ul[class*=selectList]') + ?.parentElement?.classList.value.includes('hidden'), + ).toBe(true) + }) +}) diff --git a/packages/phone-input/src/hooks/usePhoneInput.spec.tsx b/packages/phone-input/src/hooks/usePhoneInput.spec.tsx new file mode 100644 index 0000000..d7e78f3 --- /dev/null +++ b/packages/phone-input/src/hooks/usePhoneInput.spec.tsx @@ -0,0 +1,210 @@ +import { usePhoneInput } from './usePhoneInput' +import { render } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { act } from 'react-dom/test-utils' +import '@testing-library/jest-dom' +import { CountryCode } from 'libphonenumber-js' + +const user = userEvent.setup({ + delay: 300, +}) + +const Comp = ({ + country, + defaultCountry, + smartCaret, + value, +}: { + country?: CountryCode[] + defaultCountry?: CountryCode + smartCaret?: boolean + value?: string +}) => { + const { register, selectedCountry, setSelectedCountry } = usePhoneInput({ + supportedCountries: country, + defaultCountry, + smartCaret, + value, + }) + + return ( +
+ + + +
+ ) +} + +describe('usePhoneInput', () => { + it('Should only allow to enter phone number characters', async () => { + const { container } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + input.focus() + await user.keyboard('{a},{b},{c},{1},{2},{3}') + }) + + expect(input.getAttribute('value')).toBe('+1 23') + }) + + it('Should allow to enter plus sign at the beginning', async () => { + const { container } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + input.focus() + await user.keyboard('+') + }) + + expect(input.getAttribute('value')).toBe('+') + }) + + it('Should be able to format the phone value on typing', async () => { + const { container } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + input.focus() + await user.keyboard('{+},{1},{2},{3}') + }) + + expect(input.getAttribute('value')).toBe('+1 23') + }) + + it('Should only allow supported country to be detected', async () => { + const { container } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + expect(container.querySelector('#VN')).toBeVisible() + + await act(async () => { + input.focus() + await user.keyboard('{+},{1},{2},{3}') + }) + + expect(input.getAttribute('value')).toBe('+1 23') + + expect(container.querySelector('#VN')).toBeVisible() + }) + + it('Should render default country', async () => { + const { container } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + expect(container.querySelector('#CA')).toBeVisible() + }) + + it('Should be able to delete all', async () => { + const { container, rerender } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + input.focus() + await user.keyboard('{+},{1},{2},{3}') + }) + + expect(input.getAttribute('value')).toBe('+1 23') + + await act(async () => { + input.focus() + await user.keyboard('{Control>}a{/Control}') + await user.keyboard('{Backspace}') + }) + + expect(input.getAttribute('value')).toBe('') + }) + + it('Should allow to turn off smart caret', async () => { + const { container } = render( + , + ) + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + expect(container.querySelector('#CA')).toBeVisible() + + await act(async () => { + input.focus() + await user.keyboard('{+},{1},{2},{3},{4},{5}') + }) + + expect(input.getAttribute('value')).toBe('+1 234 5') + + await act(async () => { + input.focus() + await user.keyboard('{ArrowLeft}{ArrowLeft}{Backspace}{6}') // +1 234| 5 + }) + + expect(input.getAttribute('value')).toBe('+1 235 6') // it should be +1 236 5 if used smart caret + }) + + it('Should be able to set value passed as props', async () => { + const { container, rerender } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + expect(input.getAttribute('value')).toBe('') + + await act(async () => { + rerender() + }) + + expect(input.getAttribute('value')).toBe('+1 753 396 4271') + }) + + it('Should be able to set the selected country via API', async () => { + const { container } = render() + + const input = container.querySelector('input') + + if (!input) { + throw new Error('input is not a valid element.') + } + + await act(async () => { + await user.click(container.querySelector('button')!) + }) + + expect(container.querySelector('#FI')).toBeVisible() + }) +}) diff --git a/packages/phone-input/src/hooks/usePhoneInput.tsx b/packages/phone-input/src/hooks/usePhoneInput.tsx index b635c2c..cf51f73 100644 --- a/packages/phone-input/src/hooks/usePhoneInput.tsx +++ b/packages/phone-input/src/hooks/usePhoneInput.tsx @@ -53,9 +53,9 @@ export type UsePhoneInput = { metadata: PhoneInputChangeMetadata, ) => void /** - * @description Specify event to trigger the guess country function. + * @description Turn on/off guessing country on change. */ - guessOn?: 'blur' | 'change' | boolean + guessCountryOnChange?: boolean /** * @description - use smart caret */ @@ -67,7 +67,7 @@ export const usePhoneInput = ({ supportedCountries, defaultCountry, onChange: onPhoneChange = () => {}, - guessOn = 'change', + guessCountryOnChange = true, smartCaret = true, }: UsePhoneInput = {}) => { /** @@ -133,10 +133,10 @@ export const usePhoneInput = ({ */ const guessCountry = React.useCallback( (value: string) => { - if (!guessOn) return + if (!guessCountryOnChange) return return guessCountryByIncompleteNumber(value) }, - [guessOn], + [guessCountryOnChange], ) const openCountrySelect = React.useCallback(() => setSelectOpen(true), []) const closeCountrySelect = React.useCallback(() => setSelectOpen(false), []) diff --git a/vite.config.ts b/vite.config.ts index 86a1900..6c4894a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -83,10 +83,10 @@ export default defineConfig({ * minimum threshold range, should be 100 */ thresholds: { - statements: 10, - functions: 10, - lines: 10, - branches: 10, + statements: 80, + functions: 80, + lines: 80, + branches: 80, }, }, }, From 8f368499d7a4a3c86ec1fdf90e621f7ad7c2a91b Mon Sep 17 00:00:00 2001 From: Thinh Trinh Date: Tue, 30 Jan 2024 21:34:15 +0700 Subject: [PATCH 6/8] chore: update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d53d467..36d84fd 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ React Awesome Components is a collection of modern, reusable and well-tested Rea

version - coverage + coverage license build status

From 56b77f4351440d9e2c8817a2960b98d368e3539c Mon Sep 17 00:00:00 2001 From: Thinh Trinhduc Date: Wed, 31 Jan 2024 13:49:00 +0700 Subject: [PATCH 7/8] fix(usephoneinput): fixed unable to detect country when received value on mount --- apps/docs/package.json | 2 +- packages/components/CHANGELOG.md | 7 +++++++ packages/components/package.json | 4 ++-- packages/phone-input/CHANGELOG.md | 8 ++++++++ packages/phone-input/package.json | 2 +- packages/phone-input/src/hooks/usePhoneInput.tsx | 7 +++++-- 6 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/docs/package.json b/apps/docs/package.json index 43e8a04..13ba551 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -10,7 +10,7 @@ "clean": "rimraf .turbo && rimraf node_modules && rimraf .next" }, "dependencies": { - "@react-awesome/components": "1.0.6", + "@react-awesome/components": "1.0.7", "classnames": "^2.5.1", "lucide-react": "^0.315.0", "next": "^14.0.4", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6541398..a08b85e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,5 +1,12 @@ # @react-awesome/components +## 1.0.7 + +### Patch Changes + +- Updated dependencies + - @react-awesome/phone-input@1.0.6 + ## 1.0.6 ### Patch Changes diff --git a/packages/components/package.json b/packages/components/package.json index 9a2bdff..1b3e1df 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@react-awesome/components", - "version": "1.0.6", + "version": "1.0.7", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@react-awesome/phone-input": "1.0.5", + "@react-awesome/phone-input": "1.0.6", "@react-awesome/use-click-outside": "0.0.3", "@react-awesome/use-preserve-input-caret-position": "0.0.3", "@react-awesome/use-selection-range": "0.0.3", diff --git a/packages/phone-input/CHANGELOG.md b/packages/phone-input/CHANGELOG.md index 0590669..8047bdf 100644 --- a/packages/phone-input/CHANGELOG.md +++ b/packages/phone-input/CHANGELOG.md @@ -1,5 +1,13 @@ # @react-awesome/phone-input +## 1.0.6 + +### Patch Changes + +- Bug fixes: + + - `usePhoneInput` doesn't detect country when received `value` on mount. + ## 1.0.5 ### Patch Changes diff --git a/packages/phone-input/package.json b/packages/phone-input/package.json index 1777542..012e2b0 100644 --- a/packages/phone-input/package.json +++ b/packages/phone-input/package.json @@ -1,6 +1,6 @@ { "name": "@react-awesome/phone-input", - "version": "1.0.5", + "version": "1.0.6", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/phone-input/src/hooks/usePhoneInput.tsx b/packages/phone-input/src/hooks/usePhoneInput.tsx index cf51f73..007d4e0 100644 --- a/packages/phone-input/src/hooks/usePhoneInput.tsx +++ b/packages/phone-input/src/hooks/usePhoneInput.tsx @@ -97,7 +97,7 @@ export const usePhoneInput = ({ } return { - phone: value || '', + phone: '', country: getInitialCountry(), } }) @@ -245,12 +245,15 @@ export const usePhoneInput = ({ React.useEffect(() => { if (!value) return if (value !== innerValue.phone) { + const metadata = generateMetadata(value, innerValue.country) + onPhoneChange(undefined, metadata) setInnerValue((prev) => ({ ...prev, + country: metadata.country, phone: value, })) } - }, [innerValue, onPhoneChange, value]) + }, [generateMetadata, innerValue, onPhoneChange, value]) return { inputEl: inputRef, From f5ad0d056098e433243575da59efa66aef732e92 Mon Sep 17 00:00:00 2001 From: Thinh Trinh Date: Wed, 31 Jan 2024 13:58:13 +0700 Subject: [PATCH 8/8] fix(usephoneinput): fixed country detector doesnt work when received value on mount (#62)