├── packages ├── .gitkeep ├── use-double-tap │ ├── src │ │ ├── index.ts │ │ ├── tests │ │ │ ├── setup-tests.ts │ │ │ ├── use-double-tap.test.utils.ts │ │ │ ├── TestComponent.tsx │ │ │ └── use-double-tap.test.tsx │ │ └── lib │ │ │ ├── index.ts │ │ │ ├── use-double-tap.types.ts │ │ │ └── use-double-tap.ts │ ├── images │ │ └── react-double-tap-hook.webp │ ├── .eslintrc.json │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── package.json │ ├── LICENSE.md │ ├── README.md │ ├── project.json │ ├── vite.config.ts │ └── CHANGELOG.md ├── use-long-press │ ├── src │ │ ├── index.ts │ │ ├── tests │ │ │ ├── setup-tests.ts │ │ │ ├── use-long-press.test.types.ts │ │ │ ├── TestComponent.tsx │ │ │ ├── use-long-press.test-d.ts │ │ │ ├── use-long-press.test.functions.ts │ │ │ ├── use-long-press.test.consts.ts │ │ │ └── use-long-press.test.utils.ts │ │ └── lib │ │ │ ├── index.ts │ │ │ ├── use-long-press.utils.ts │ │ │ ├── use-long-press.types.ts │ │ │ └── use-long-press.ts │ ├── images │ │ ├── logo-horizontal.png │ │ └── react-long-press-hook.webp │ ├── .eslintrc.json │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── package.json │ ├── LICENSE.md │ ├── project.json │ ├── README.md │ └── vite.config.ts └── react-interval-hook │ ├── src │ ├── index.ts │ ├── lib │ │ ├── index.ts │ │ ├── react-interval-hook.types.ts │ │ └── react-interval-hook.ts │ └── tests │ │ └── react-interval-hook.test.tsx │ ├── images │ └── react-interval-hook.webp │ ├── .eslintrc.json │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ ├── package.json │ ├── LICENSE.md │ ├── project.json │ ├── README.md │ ├── vite.config.ts │ └── CHANGELOG.md ├── .eslintignore ├── .yarnrc.yml ├── babel.config.json ├── libs ├── shared │ ├── util-tests │ │ ├── src │ │ │ ├── index.ts │ │ │ └── lib │ │ │ │ ├── index.ts │ │ │ │ ├── util.functions.ts │ │ │ │ └── events.functions.ts │ │ ├── README.md │ │ ├── .eslintrc.json │ │ ├── tsconfig.spec.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.json │ │ ├── project.json │ │ └── vite.config.ts │ └── util-polyfills │ │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── index.ts │ │ │ └── pointer-event.polyfill.ts │ │ ├── README.md │ │ ├── .eslintrc.json │ │ ├── tsconfig.spec.json │ │ ├── tsconfig.lib.json │ │ ├── tsconfig.json │ │ ├── project.json │ │ └── vite.config.ts └── storybook-host │ ├── .storybook │ ├── addons │ │ └── vercel-analytics │ │ │ ├── index.ts │ │ │ ├── vercel-analytics.consts.ts │ │ │ └── VercelAnalyticsAddon.tsx │ ├── decorators │ │ ├── SnackbarDecorator.tsx │ │ ├── Theme.decorator.tsx │ │ └── Content.decorator.tsx │ ├── manager.ts │ ├── preview.ts │ └── main.ts │ ├── vite.config.ts │ ├── README.md │ ├── .babelrc │ ├── tsconfig.json │ ├── .eslintrc.json │ ├── tsconfig.lib.json │ ├── src │ └── lib │ │ ├── theme.ts │ │ └── long-press │ │ ├── LongPressButton.stories.tsx │ │ └── LongPressButton.tsx │ ├── tsconfig.storybook.json │ └── project.json ├── .github ├── CODEOWNERS ├── workflows │ ├── stale.yml │ ├── post-release.yml │ ├── verify.yml │ └── release.yml └── FUNDING.yml ├── .husky ├── pre-commit └── commit-msg ├── tools ├── scripts │ ├── release │ │ ├── release.types.ts │ │ ├── cli.types.ts │ │ ├── changelog.types.ts │ │ ├── release.consts.ts │ │ ├── version.types.ts │ │ ├── publish.ts │ │ ├── changelog.ts │ │ ├── cli.ts │ │ ├── version.ts │ │ ├── git.ts │ │ └── release.ts │ └── utils │ │ └── output.ts └── tsconfig.json ├── images └── react-libraries-banner.webp ├── jest.preset.js ├── .prettierrc ├── codecov.yml ├── .prettierignore ├── jest.config.ts ├── commitlint.config.js ├── .vscode └── extensions.json ├── .aiignore ├── .editorconfig ├── .gitignore ├── tsconfig.base.json ├── migrations.json ├── .eslintrc.json ├── LICENSE.md ├── README.md ├── package.json └── nx.json /packages/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "babelrcRoots": ["*"] 3 | } 4 | -------------------------------------------------------------------------------- /libs/shared/util-tests/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /packages/use-double-tap/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /packages/use-long-press/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners for all files 2 | * @minwork 3 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /packages/react-interval-hook/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn check 5 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pointer-event.polyfill'; 2 | -------------------------------------------------------------------------------- /packages/use-double-tap/src/tests/setup-tests.ts: -------------------------------------------------------------------------------- 1 | import '@react/shared/util-polyfills'; 2 | -------------------------------------------------------------------------------- /packages/use-long-press/src/tests/setup-tests.ts: -------------------------------------------------------------------------------- 1 | import '@react/shared/util-polyfills'; 2 | -------------------------------------------------------------------------------- /tools/scripts/release/release.types.ts: -------------------------------------------------------------------------------- 1 | export type ReleasePreidValue = string | undefined; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /images/react-libraries-banner.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minwork/react/HEAD/images/react-libraries-banner.webp -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/shared/util-tests/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './events.functions'; 2 | export * from './util.functions'; 3 | -------------------------------------------------------------------------------- /packages/use-double-tap/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-double-tap'; 2 | export * from './use-double-tap.types'; 3 | -------------------------------------------------------------------------------- /packages/use-long-press/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-long-press'; 2 | export * from './use-long-press.types'; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 100% 6 | threshold: 1% 7 | -------------------------------------------------------------------------------- /packages/react-interval-hook/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './react-interval-hook'; 2 | export * from './react-interval-hook.types'; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | 6 | /.nx/cache 7 | /.nx/workspace-data -------------------------------------------------------------------------------- /libs/shared/util-tests/src/lib/util.functions.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-function 2 | export const noop = () => {}; 3 | -------------------------------------------------------------------------------- /packages/use-long-press/images/logo-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minwork/react/HEAD/packages/use-long-press/images/logo-horizontal.png -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/addons/vercel-analytics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './VercelAnalyticsAddon'; 2 | export * from './vercel-analytics.consts'; 3 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjectsAsync } from '@nx/jest'; 2 | 3 | export default async () => ({ 4 | projects: await getJestProjectsAsync(), 5 | }); 6 | -------------------------------------------------------------------------------- /packages/use-double-tap/images/react-double-tap-hook.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minwork/react/HEAD/packages/use-double-tap/images/react-double-tap-hook.webp -------------------------------------------------------------------------------- /packages/use-long-press/images/react-long-press-hook.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minwork/react/HEAD/packages/use-long-press/images/react-long-press-hook.webp -------------------------------------------------------------------------------- /packages/react-interval-hook/images/react-interval-hook.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minwork/react/HEAD/packages/react-interval-hook/images/react-interval-hook.webp -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'subject-case': [2, 'always', 'sentence-case'], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /libs/storybook-host/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | }); 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /libs/storybook-host/README.md: -------------------------------------------------------------------------------- 1 | # storybook-host 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test storybook-host` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/storybook-host/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/react/babel", 5 | { 6 | "runtime": "automatic", 7 | "useBuiltIns": "usage" 8 | } 9 | ] 10 | ], 11 | "plugins": [] 12 | } 13 | -------------------------------------------------------------------------------- /libs/shared/util-tests/README.md: -------------------------------------------------------------------------------- 1 | # shared-util-tests 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test shared-util-tests` to execute the unit tests via [Vitest](https://vitest.dev/). 8 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/README.md: -------------------------------------------------------------------------------- 1 | # shared-util-polyfills 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test shared-util-polyfills` to execute the unit tests via [Vitest](https://vitest.dev/). 8 | -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/addons/vercel-analytics/vercel-analytics.consts.ts: -------------------------------------------------------------------------------- 1 | export const VERCEL_ANALYTICS_ADDON_ID = 'storybook/vercel-analytics'; 2 | export const VERCEL_ANALYTICS_TOOL_ID = `${VERCEL_ANALYTICS_ADDON_ID}/listener`; 3 | export const VERCEL_ANALYTICS_ADDON_NAME = 'Vercel Analytics'; 4 | -------------------------------------------------------------------------------- /tools/scripts/release/cli.types.ts: -------------------------------------------------------------------------------- 1 | import { ReleaseChannel } from './release.consts'; 2 | 3 | export interface ParsedOptions { 4 | isPrerelease: boolean; 5 | isCI: boolean; 6 | preid: string | undefined; 7 | tag: ReleaseChannel; 8 | dryRun: boolean; 9 | verbose: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /.aiignore: -------------------------------------------------------------------------------- 1 | # An .aiignore file follows the same syntax as a .gitignore file. 2 | # .gitignore documentation: https://git-scm.com/docs/gitignore 3 | 4 | # you can ignore files 5 | .DS_Store 6 | *.log 7 | *.tmp 8 | 9 | # or folders 10 | dist/ 11 | build/ 12 | out/ 13 | 14 | # Environment 15 | .env* 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /tools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false, 10 | "strict": true 11 | }, 12 | "include": ["**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /tools/scripts/release/changelog.types.ts: -------------------------------------------------------------------------------- 1 | import { ChangelogOptions as NxChangelogOptions } from 'nx/src/command-line/release/command-object'; 2 | 3 | export interface HandleChangelogOptions { 4 | projectName: string; 5 | isPrerelease: boolean; 6 | options: Omit & Required>; 7 | } 8 | -------------------------------------------------------------------------------- /tools/scripts/release/release.consts.ts: -------------------------------------------------------------------------------- 1 | import { ReleasePreidValue } from './release.types'; 2 | 3 | export enum ReleaseChannel { 4 | Preview = 'preview', 5 | Latest = 'latest', 6 | } 7 | 8 | export const releaseChannelPreid = { 9 | [ReleaseChannel.Preview]: 'preview', 10 | [ReleaseChannel.Latest]: undefined, 11 | } satisfies Record; 12 | -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/decorators/SnackbarDecorator.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from 'react'; 2 | import { SnackbarProvider } from 'notistack'; 3 | 4 | export const SnackbarDecorator = (Story: ComponentType, context: object) => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/decorators/Theme.decorator.tsx: -------------------------------------------------------------------------------- 1 | import { CssBaseline, ThemeProvider } from '@mui/material'; 2 | import type { ComponentType } from 'react'; 3 | import { theme } from '../../src/lib/theme'; 4 | 5 | export const ThemeDecorator = (Story: ComponentType, context: object) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/addons/vercel-analytics/VercelAnalyticsAddon.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from 'react'; 2 | import { useStorybookState } from '@storybook/manager-api'; 3 | import { pageview } from '@vercel/analytics'; 4 | 5 | export const VercelAnalyticsAddon: FC = () => { 6 | const { path } = useStorybookState(); 7 | 8 | useEffect(() => { 9 | pageview({ 10 | path, 11 | }); 12 | }, [path]); 13 | 14 | return null; 15 | }; 16 | -------------------------------------------------------------------------------- /tools/scripts/release/version.types.ts: -------------------------------------------------------------------------------- 1 | import { VersionData } from 'nx/src/command-line/release/utils/shared'; 2 | 3 | export interface HandleVersionOptions { 4 | projectName: string; 5 | suggestedVersionData: VersionData[keyof VersionData]; 6 | isPrerelease: boolean; 7 | options: VersionOptions; 8 | } 9 | 10 | export interface VersionOptions { 11 | // specifier?: ReleaseType | string; 12 | preid?: string; 13 | dryRun?: boolean; 14 | verbose?: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /libs/storybook-host/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowJs": false, 5 | "esModuleInterop": false, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true 8 | }, 9 | "files": [], 10 | "include": [], 11 | "references": [ 12 | { 13 | "path": "./tsconfig.lib.json" 14 | }, 15 | { 16 | "path": "./tsconfig.storybook.json" 17 | } 18 | ], 19 | "extends": "../../tsconfig.base.json" 20 | } 21 | -------------------------------------------------------------------------------- /libs/shared/util-tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/use-double-tap/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/use-long-press/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/react-interval-hook/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/storybook-host/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*", "storybook-static", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/use-double-tap/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowJs": false, 5 | "esModuleInterop": false, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true, 8 | "types": ["vite/client", "vitest"] 9 | }, 10 | "files": [], 11 | "include": [], 12 | "references": [ 13 | { 14 | "path": "./tsconfig.lib.json" 15 | }, 16 | { 17 | "path": "./tsconfig.spec.json" 18 | } 19 | ], 20 | "extends": "../../tsconfig.base.json" 21 | } 22 | -------------------------------------------------------------------------------- /packages/use-long-press/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowJs": false, 5 | "esModuleInterop": false, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true, 8 | "types": ["vite/client", "vitest"] 9 | }, 10 | "files": [], 11 | "include": [], 12 | "references": [ 13 | { 14 | "path": "./tsconfig.lib.json" 15 | }, 16 | { 17 | "path": "./tsconfig.spec.json" 18 | } 19 | ], 20 | "extends": "../../tsconfig.base.json" 21 | } 22 | -------------------------------------------------------------------------------- /packages/react-interval-hook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowJs": false, 5 | "esModuleInterop": false, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true, 8 | "types": ["vite/client", "vitest"] 9 | }, 10 | "files": [], 11 | "include": [], 12 | "references": [ 13 | { 14 | "path": "./tsconfig.lib.json" 15 | }, 16 | { 17 | "path": "./tsconfig.spec.json" 18 | } 19 | ], 20 | "extends": "../../tsconfig.base.json" 21 | } 22 | -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/decorators/Content.decorator.tsx: -------------------------------------------------------------------------------- 1 | import type { Decorator } from '@storybook/react'; 2 | import { Stack } from '@mui/material'; 3 | 4 | export const ContentDecorator: Decorator = (Story, context) => { 5 | return ( 6 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/use-long-press/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node", "vite/client"] 6 | }, 7 | "files": ["../../node_modules/@nx/react/typings/cssmodule.d.ts", "../../node_modules/@nx/react/typings/image.d.ts"], 8 | "exclude": [ 9 | "**/*.spec.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.tsx", 12 | "**/*.test.tsx", 13 | "**/*.spec.js", 14 | "**/*.test.js", 15 | "**/*.spec.jsx", 16 | "**/*.test.jsx" 17 | ], 18 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import { addons, types } from '@storybook/manager-api'; 2 | import { inject } from '@vercel/analytics'; 3 | import { 4 | VERCEL_ANALYTICS_ADDON_ID, 5 | VERCEL_ANALYTICS_ADDON_NAME, 6 | VERCEL_ANALYTICS_TOOL_ID, 7 | VercelAnalyticsAddon, 8 | } from './addons/vercel-analytics'; 9 | 10 | addons.register(VERCEL_ANALYTICS_ADDON_ID, () => { 11 | inject({ 12 | disableAutoTrack: true, 13 | }); 14 | addons.add(VERCEL_ANALYTICS_TOOL_ID, { 15 | type: types.TOOLEXTRA, 16 | render: VercelAnalyticsAddon, 17 | title: VERCEL_ANALYTICS_ADDON_NAME, 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/use-long-press/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] 6 | }, 7 | "include": [ 8 | "vite.config.ts", 9 | "src/**/*.test.ts", 10 | "src/**/*.test-d.ts", 11 | "src/**/*.spec.ts", 12 | "src/**/*.test.tsx", 13 | "src/**/*.test-d.tsx", 14 | "src/**/*.spec.tsx", 15 | "src/**/*.test.js", 16 | "src/**/*.test-d.js", 17 | "src/**/*.spec.js", 18 | "src/**/*.test.jsx", 19 | "src/**/*.test-d.jsx", 20 | "src/**/*.spec.jsx", 21 | "src/**/*.d.ts" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /packages/use-double-tap/src/lib/use-double-tap.types.ts: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler } from 'react'; 2 | 3 | type EmptyCallback = () => void; 4 | 5 | export type CallbackFunction = MouseEventHandler | EmptyCallback; 6 | export type DoubleTapCallback = CallbackFunction | null; 7 | 8 | export interface DoubleTapOptions { 9 | onSingleTap?: CallbackFunction; 10 | } 11 | 12 | export type DoubleTapResult = Callback extends CallbackFunction 13 | ? { 14 | onClick: CallbackFunction; 15 | } 16 | : Callback extends null 17 | ? Record 18 | : never; 19 | -------------------------------------------------------------------------------- /packages/use-double-tap/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node", "vite/client", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] 6 | }, 7 | "files": ["../../node_modules/@nx/react/typings/cssmodule.d.ts", "../../node_modules/@nx/react/typings/image.d.ts"], 8 | "exclude": [ 9 | "**/*.spec.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.tsx", 12 | "**/*.test.tsx", 13 | "**/*.spec.js", 14 | "**/*.test.js", 15 | "**/*.spec.jsx", 16 | "**/*.test.jsx" 17 | ], 18 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /packages/react-interval-hook/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node", "vite/client", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] 6 | }, 7 | "files": ["../../node_modules/@nx/react/typings/cssmodule.d.ts", "../../node_modules/@nx/react/typings/image.d.ts"], 8 | "exclude": [ 9 | "**/*.spec.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.tsx", 12 | "**/*.test.tsx", 13 | "**/*.spec.js", 14 | "**/*.test.js", 15 | "**/*.spec.jsx", 16 | "**/*.test.jsx" 17 | ], 18 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /libs/shared/util-tests/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [ 6 | "vitest/globals", 7 | "vitest/importMeta", 8 | "vite/client", 9 | "node", 10 | "@nx/react/typings/cssmodule.d.ts", 11 | "@nx/react/typings/image.d.ts" 12 | ] 13 | }, 14 | "include": [ 15 | "vite.config.ts", 16 | "src/**/*.test.ts", 17 | "src/**/*.spec.ts", 18 | "src/**/*.test.tsx", 19 | "src/**/*.spec.tsx", 20 | "src/**/*.test.js", 21 | "src/**/*.spec.js", 22 | "src/**/*.test.jsx", 23 | "src/**/*.spec.jsx", 24 | "src/**/*.d.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/use-double-tap/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [ 6 | "vitest/globals", 7 | "vitest/importMeta", 8 | "vite/client", 9 | "node", 10 | "@nx/react/typings/cssmodule.d.ts", 11 | "@nx/react/typings/image.d.ts" 12 | ] 13 | }, 14 | "include": [ 15 | "vite.config.ts", 16 | "src/**/*.test.ts", 17 | "src/**/*.spec.ts", 18 | "src/**/*.test.tsx", 19 | "src/**/*.spec.tsx", 20 | "src/**/*.test.js", 21 | "src/**/*.spec.js", 22 | "src/**/*.test.jsx", 23 | "src/**/*.spec.jsx", 24 | "src/**/*.d.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [ 6 | "vitest/globals", 7 | "vitest/importMeta", 8 | "vite/client", 9 | "node", 10 | "@nx/react/typings/cssmodule.d.ts", 11 | "@nx/react/typings/image.d.ts" 12 | ] 13 | }, 14 | "include": [ 15 | "vite.config.ts", 16 | "src/**/*.test.ts", 17 | "src/**/*.spec.ts", 18 | "src/**/*.test.tsx", 19 | "src/**/*.spec.tsx", 20 | "src/**/*.test.js", 21 | "src/**/*.spec.js", 22 | "src/**/*.test.jsx", 23 | "src/**/*.spec.jsx", 24 | "src/**/*.d.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/react-interval-hook/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [ 6 | "vitest/globals", 7 | "vitest/importMeta", 8 | "vite/client", 9 | "node", 10 | "@nx/react/typings/cssmodule.d.ts", 11 | "@nx/react/typings/image.d.ts" 12 | ] 13 | }, 14 | "include": [ 15 | "vite.config.ts", 16 | "src/**/*.test.ts", 17 | "src/**/*.spec.ts", 18 | "src/**/*.test.tsx", 19 | "src/**/*.spec.tsx", 20 | "src/**/*.test.js", 21 | "src/**/*.spec.js", 22 | "src/**/*.test.jsx", 23 | "src/**/*.spec.jsx", 24 | "src/**/*.d.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] 6 | }, 7 | "files": [ 8 | "../../../node_modules/@nx/react/typings/cssmodule.d.ts", 9 | "../../../node_modules/@nx/react/typings/image.d.ts" 10 | ], 11 | "exclude": [ 12 | "**/*.spec.ts", 13 | "**/*.test.ts", 14 | "**/*.spec.tsx", 15 | "**/*.test.tsx", 16 | "**/*.spec.js", 17 | "**/*.test.js", 18 | "**/*.spec.jsx", 19 | "**/*.test.jsx" 20 | ], 21 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 22 | } 23 | -------------------------------------------------------------------------------- /libs/shared/util-tests/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/out-tsc", 5 | "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] 6 | }, 7 | "files": [ 8 | "../../../node_modules/@nx/react/typings/cssmodule.d.ts", 9 | "../../../node_modules/@nx/react/typings/image.d.ts" 10 | ], 11 | "exclude": [ 12 | "**/*.spec.ts", 13 | "**/*.test.ts", 14 | "**/*.spec.tsx", 15 | "**/*.test.tsx", 16 | "**/*.spec.js", 17 | "**/*.test.js", 18 | "**/*.spec.jsx", 19 | "**/*.test.jsx" 20 | ], 21 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 22 | } 23 | -------------------------------------------------------------------------------- /libs/storybook-host/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node", "@nx/react/typings/cssmodule.d.ts", "@nx/react/typings/image.d.ts"] 6 | }, 7 | "exclude": [ 8 | "jest.config.ts", 9 | "src/**/*.spec.ts", 10 | "src/**/*.test.ts", 11 | "src/**/*.spec.tsx", 12 | "src/**/*.test.tsx", 13 | "src/**/*.spec.js", 14 | "src/**/*.test.js", 15 | "src/**/*.spec.jsx", 16 | "src/**/*.test.jsx", 17 | "**/*.stories.ts", 18 | "**/*.stories.js", 19 | "**/*.stories.jsx", 20 | "**/*.stories.tsx" 21 | ], 22 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/react-interval-hook/src/lib/react-interval-hook.types.ts: -------------------------------------------------------------------------------- 1 | type EmptyCallback = () => void; 2 | export type IntervalHookCallback = (ticks?: number) => void; 3 | export type IntervalHookFinishCallback = () => void; 4 | export type IntervalHookStartMethod = EmptyCallback; 5 | export type IntervalHookStopMethod = (triggerFinishCallback?: boolean) => void; 6 | export type IntervalHookIsActiveMethod = () => boolean; 7 | 8 | export interface IntervalHookOptions { 9 | onFinish?: IntervalHookFinishCallback; 10 | autoStart?: boolean; 11 | immediate?: boolean; 12 | selfCorrecting?: boolean; 13 | } 14 | 15 | export type IntervalHookResult = { 16 | start: IntervalHookStartMethod; 17 | stop: IntervalHookStopMethod; 18 | isActive: IntervalHookIsActiveMethod; 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: '45 0 * * *' 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | permissions: 12 | issues: write 13 | pull-requests: write 14 | 15 | steps: 16 | - uses: actions/stale@v3 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | stale-issue-message: > 20 | This issue has been automatically marked as stale because it has not had 21 | recent activity. It will be closed if no further activity occurs. Thank you 22 | for your contributions. 23 | stale-issue-label: 'stale' 24 | days-before-issue-stale: 14 25 | days-before-pr-stale: -1 26 | exempt-issue-labels: 'pinned,security,next' 27 | -------------------------------------------------------------------------------- /libs/storybook-host/src/lib/theme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material'; 2 | 3 | const fallbackFontFamily = [ 4 | '-apple-system', 5 | 'BlinkMacSystemFont', 6 | '"Segoe UI"', 7 | 'Roboto', 8 | '"Helvetica Neue"', 9 | 'Arial', 10 | 'sans-serif', 11 | '"Apple Color Emoji"', 12 | '"Segoe UI Emoji"', 13 | '"Segoe UI Symbol"', 14 | ]; 15 | 16 | export const fontFamily = ['Montserrat', ...fallbackFontFamily].join(', '); 17 | 18 | export const theme = createTheme({ 19 | palette: { 20 | mode: 'light', 21 | background: { 22 | default: '#f6f6f6', 23 | paper: '#FFFFFF', 24 | }, 25 | text: { 26 | primary: '#212121', 27 | secondary: '#636363', 28 | }, 29 | primary: { 30 | main: '#FFCD00', 31 | }, 32 | }, 33 | typography: { 34 | fontFamily: fontFamily, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: minwork # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /libs/shared/util-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "strict": true, 7 | "moduleResolution": "Node", 8 | "resolveJsonModule": true, 9 | "isolatedModules": true, 10 | "types": ["vite/client"], 11 | "noEmit": true, 12 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 13 | "allowJs": false, 14 | "esModuleInterop": false, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "files": [], 21 | "include": ["src"], 22 | "references": [ 23 | { 24 | "path": "./tsconfig.lib.json" 25 | }, 26 | { 27 | "path": "./tsconfig.spec.json" 28 | } 29 | ], 30 | "extends": "../../../tsconfig.base.json" 31 | } 32 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "strict": true, 7 | "moduleResolution": "Node", 8 | "resolveJsonModule": true, 9 | "isolatedModules": true, 10 | "types": ["vite/client"], 11 | "noEmit": true, 12 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 13 | "allowJs": false, 14 | "esModuleInterop": false, 15 | "skipLibCheck": true, 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "jsx": "react-jsx" 19 | }, 20 | "files": [], 21 | "include": ["src"], 22 | "references": [ 23 | { 24 | "path": "./tsconfig.lib.json" 25 | }, 26 | { 27 | "path": "./tsconfig.spec.json" 28 | } 29 | ], 30 | "extends": "../../../tsconfig.base.json" 31 | } 32 | -------------------------------------------------------------------------------- /libs/storybook-host/tsconfig.storybook.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "emitDecoratorMetadata": true, 5 | "outDir": "" 6 | }, 7 | "files": [ 8 | "../../node_modules/@nx/react/typings/styled-jsx.d.ts", 9 | "../../node_modules/@nx/react/typings/cssmodule.d.ts", 10 | "../../node_modules/@nx/react/typings/image.d.ts" 11 | ], 12 | "exclude": [ 13 | "src/**/*.spec.ts", 14 | "src/**/*.test.ts", 15 | "src/**/*.spec.js", 16 | "src/**/*.test.js", 17 | "src/**/*.spec.tsx", 18 | "src/**/*.test.tsx", 19 | "src/**/*.spec.jsx", 20 | "src/**/*.test.js" 21 | ], 22 | "include": [ 23 | "src/**/*.stories.ts", 24 | "src/**/*.stories.js", 25 | "src/**/*.stories.jsx", 26 | "src/**/*.stories.tsx", 27 | "src/**/*.stories.mdx", 28 | ".storybook/*.js", 29 | ".storybook/*.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/use-double-tap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-double-tap", 3 | "version": "1.3.7", 4 | "description": "React hook for handling double tap on mobile devices", 5 | "author": "minwork", 6 | "license": "MIT", 7 | "keywords": [ 8 | "double tap", 9 | "react", 10 | "hook" 11 | ], 12 | "repository": "https://github.com/minwork/react", 13 | "readme": "https://github.com/minwork/react/blob/main/packages/use-double-tap/README.md", 14 | "homepage": "https://minwork.gitbook.io/double-tap-hook/", 15 | "main": "./index.js", 16 | "types": "./index.d.ts", 17 | "exports": { 18 | ".": { 19 | "import": "./index.mjs", 20 | "require": "./index.js", 21 | "types": "./index.d.ts" 22 | } 23 | }, 24 | "files": [ 25 | "**/*.{js,ts,mjs}", 26 | "LICENSE.md" 27 | ], 28 | "peerDependencies": { 29 | "react": ">=16.8.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | .nx/ 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | **/coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | # Environment 43 | .env*.local 44 | 45 | # Yarn 46 | .pnp.* 47 | .yarn/* 48 | !.yarn/patches 49 | !.yarn/plugins 50 | !.yarn/releases 51 | !.yarn/sdks 52 | !.yarn/versions 53 | 54 | vite.config.*.timestamp* 55 | vitest.config.*.timestamp* -------------------------------------------------------------------------------- /packages/use-long-press/src/tests/use-long-press.test.types.ts: -------------------------------------------------------------------------------- 1 | import { UIEvent } from 'react'; 2 | import { LongPressDomEvents, LongPressReactEvents } from '../lib'; 3 | 4 | export type LongPressTestHandlerType = 'start' | 'move' | 'stop'; 5 | export type LongPressTestHandler = (event: LongPressReactEvents | LongPressDomEvents) => void; 6 | export type LongPressTestHandlersMap = { 7 | start: LongPressTestHandler; 8 | move: LongPressTestHandler; 9 | stop: LongPressTestHandler; 10 | leave?: LongPressTestHandler; 11 | }; 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | export type LongPressTestEventCreator = (options?: object) => UIEvent; 15 | export type LongPressTestPositionedEventCreator = (x: number, y: number) => E; 16 | 17 | export type LongPressTestPositionedEventFactory = Record< 18 | LongPressTestHandlerType, 19 | LongPressTestPositionedEventCreator 20 | >; 21 | -------------------------------------------------------------------------------- /packages/react-interval-hook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-interval-hook", 3 | "version": "1.1.5", 4 | "description": "React hook for using self-correcting setInterval, augmented by management methods (start, stop, isActive)", 5 | "author": "minwork", 6 | "license": "MIT", 7 | "keywords": [ 8 | "interval", 9 | "hook", 10 | "react", 11 | "setInterval", 12 | "self-correcting" 13 | ], 14 | "repository": "https://github.com/minwork/react", 15 | "readme": "https://github.com/minwork/react/blob/main/packages/react-interval-hook/README.md", 16 | "homepage": "https://minwork.gitbook.io/react-interval-hook/", 17 | "main": "./index.js", 18 | "types": "./index.d.ts", 19 | "exports": { 20 | ".": { 21 | "import": "./index.mjs", 22 | "require": "./index.js", 23 | "types": "./index.d.ts" 24 | } 25 | }, 26 | "files": [ 27 | "**/*.{js,ts,mjs}", 28 | "LICENSE.md" 29 | ], 30 | "peerDependencies": { 31 | "react": ">=16.8.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@react/shared/util-polyfills": ["libs/shared/util-polyfills/src/index.ts"], 19 | "@react/shared/util-tests": ["libs/shared/util-tests/src/index.ts"], 20 | "react-interval-hook": ["packages/react-interval-hook/src/index.ts"], 21 | "storybook-host": ["libs/storybook-host/src/index.ts"], 22 | "use-double-tap": ["packages/use-double-tap/src/index.ts"], 23 | "use-long-press": ["packages/use-long-press/src/index.ts"] 24 | } 25 | }, 26 | "exclude": ["node_modules", "tmp"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/use-long-press/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-long-press", 3 | "version": "3.3.0", 4 | "description": "React hook for detecting click, tap or point and hold event. Easy to use, highly customizable options, thoroughly tested.", 5 | "author": "minwork", 6 | "license": "MIT", 7 | "keywords": [ 8 | "long press", 9 | "hook", 10 | "react", 11 | "click and hold", 12 | "tap and hold", 13 | "point and hold" 14 | ], 15 | "repository": "https://github.com/minwork/react", 16 | "readme": "https://github.com/minwork/react/blob/main/packages/use-long-press/README.md", 17 | "homepage": "https://minwork.gitbook.io/long-press-hook/", 18 | "main": "./index.js", 19 | "types": "./index.d.ts", 20 | "exports": { 21 | ".": { 22 | "import": "./index.mjs", 23 | "require": "./index.js", 24 | "types": "./index.d.ts" 25 | } 26 | }, 27 | "files": [ 28 | "**/*.{js,ts,mjs}", 29 | "LICENSE.md" 30 | ], 31 | "peerDependencies": { 32 | "react": ">=16.8.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "version": "20.5.0-beta.2", 5 | "description": "Install jiti as a devDependency to allow vite to parse TS postcss files.", 6 | "implementation": "./src/migrations/update-20-5-0/install-jiti", 7 | "package": "@nx/vite", 8 | "name": "update-20-5-0-install-jiti" 9 | }, 10 | { 11 | "version": "20.5.0-beta.3", 12 | "description": "Update resolve.conditions to include defaults that are no longer provided by Vite.", 13 | "implementation": "./src/migrations/update-20-5-0/update-resolve-conditions", 14 | "package": "@nx/vite", 15 | "name": "update-20-5-0-update-resolve-conditions" 16 | }, 17 | { 18 | "version": "20.5.0-beta.3", 19 | "description": "Add vite config temporary files to the ESLint configuration ignore patterns if ESLint is used.", 20 | "implementation": "./src/migrations/update-20-5-0/eslint-ignore-vite-temp-files", 21 | "package": "@nx/vite", 22 | "name": "eslint-ignore-vite-temp-files" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/use-double-tap/src/tests/use-double-tap.test.utils.ts: -------------------------------------------------------------------------------- 1 | import { CallbackFunction, DoubleTapOptions, useDoubleTap } from '../lib'; 2 | import { renderHook } from '@testing-library/react'; 3 | import { expect } from 'vitest'; 4 | 5 | export function renderUseDoubleTap( 6 | callback: CallbackFunction | null, 7 | threshold = 300, 8 | options: DoubleTapOptions = {} 9 | ) { 10 | return renderHook(({ callback, threshold, options }) => useDoubleTap(callback, threshold, options), { 11 | initialProps: { 12 | callback, 13 | threshold, 14 | options, 15 | }, 16 | }); 17 | } 18 | 19 | export const expectMouseEvent = expect.objectContaining({ nativeEvent: expect.any(MouseEvent) }); 20 | export const expectTouchEvent = expect.objectContaining({ nativeEvent: expect.any(TouchEvent) }); 21 | export const expectPointerEvent = expect.objectContaining({ nativeEvent: expect.any(PointerEvent) }); 22 | export const expectSpecificEvent = (event: Event) => 23 | expect.objectContaining({ 24 | nativeEvent: event, 25 | }); 26 | -------------------------------------------------------------------------------- /tools/scripts/release/publish.ts: -------------------------------------------------------------------------------- 1 | import { ProjectGraph } from 'nx/src/config/project-graph'; 2 | import chalk from 'chalk'; 3 | import { colorProjectName, printHeader } from '../utils/output'; 4 | import { $ } from 'execa'; 5 | import * as path from 'node:path'; 6 | 7 | export function syncPackageJson(projectsList: string[], graph: ProjectGraph) { 8 | console.log(printHeader('sync', 'cyan'), `Copy "package.json" files to projects output to sync package version\n`); 9 | const projects = projectsList.map((projectName) => graph.nodes[projectName]); 10 | 11 | const file = 'package.json'; 12 | projects.forEach((project) => { 13 | const projectRoot = `${project.data.root}`; 14 | const outputRoot = project.data.targets?.['build']?.options?.outputPath ?? `dist/${project.data.root}`; 15 | console.log( 16 | colorProjectName(project.name), 17 | '📄', 18 | `Copy ${chalk.yellow('package.json')} from ${chalk.grey(projectRoot)} to ${chalk.green(outputRoot)}` 19 | ); 20 | $`cp ${path.join(projectRoot, file)} ${path.join(outputRoot, file)}`; 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": { 28 | "@typescript-eslint/no-extra-semi": "error", 29 | "no-extra-semi": "off" 30 | } 31 | }, 32 | { 33 | "files": ["*.js", "*.jsx"], 34 | "extends": ["plugin:@nx/javascript"], 35 | "rules": { 36 | "@typescript-eslint/no-extra-semi": "error", 37 | "no-extra-semi": "off" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/src/lib/pointer-event.polyfill.ts: -------------------------------------------------------------------------------- 1 | export class PointerEvent extends MouseEvent { 2 | public height?: number; 3 | public isPrimary?: boolean; 4 | public pointerId?: number; 5 | public pointerType?: string; 6 | public pressure?: number; 7 | public tangentialPressure?: number; 8 | public tiltX?: number; 9 | public tiltY?: number; 10 | public twist?: number; 11 | public width?: number; 12 | 13 | constructor(type: string, params: PointerEventInit = {}) { 14 | super(type, params); 15 | this.pointerId = params.pointerId; 16 | this.width = params.width; 17 | this.height = params.height; 18 | this.pressure = params.pressure; 19 | this.tangentialPressure = params.tangentialPressure; 20 | this.tiltX = params.tiltX; 21 | this.tiltY = params.tiltY; 22 | this.pointerType = params.pointerType; 23 | this.isPrimary = params.isPrimary; 24 | } 25 | /* c8 ignore next 1 */ 26 | } 27 | 28 | if (!global.PointerEvent) { 29 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 30 | global.PointerEvent = PointerEvent as any; 31 | } 32 | -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | import { ContentDecorator } from './decorators/Content.decorator'; 3 | import { ThemeDecorator } from './decorators/Theme.decorator'; 4 | import { SnackbarDecorator } from './decorators/SnackbarDecorator'; 5 | 6 | const preview: Preview = { 7 | decorators: [ThemeDecorator, ContentDecorator, SnackbarDecorator], 8 | 9 | parameters: { 10 | layout: 'fullscreen', 11 | controls: { 12 | expanded: true, 13 | sort: 'requiredFirst', 14 | matchers: { 15 | color: /(background|color)$/i, 16 | date: /Date$/, 17 | }, 18 | }, 19 | docs: { 20 | toc: true, 21 | subtitle: 'Example usage of useLongPress hook', 22 | description: { 23 | component: 24 | 'All `LongPressButton` props are just the **useLongPress** hook options that are forwarded in order to configure its behavior.', 25 | }, 26 | }, 27 | }, 28 | 29 | tags: ['autodocs'], 30 | 31 | initialGlobals: { 32 | backgrounds: '#333', 33 | }, 34 | }; 35 | 36 | export default preview; 37 | -------------------------------------------------------------------------------- /libs/shared/util-tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-util-tests", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/shared/util-tests/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/eslint:lint", 10 | "outputs": ["{options.outputFile}"] 11 | }, 12 | "test": { 13 | "executor": "@nx/vite:test", 14 | "outputs": ["{workspaceRoot}/coverage/libs/shared/util-tests"], 15 | "options": { 16 | "passWithNoTests": true, 17 | "reportsDirectory": "../../../coverage/libs/shared/util-tests" 18 | } 19 | }, 20 | "build": { 21 | "executor": "@nx/vite:build", 22 | "outputs": ["{options.outputPath}"], 23 | "defaultConfiguration": "production", 24 | "options": { 25 | "outputPath": "dist/libs/shared/util-tests" 26 | }, 27 | "configurations": { 28 | "development": { 29 | "mode": "development" 30 | }, 31 | "production": { 32 | "mode": "production" 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/storybook-host/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | 3 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 4 | import { mergeConfig } from 'vite'; 5 | 6 | const config: StorybookConfig = { 7 | stories: ['../src/lib/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'], 8 | addons: ['@storybook/addon-essentials', '@storybook/addon-interactions', '@chromatic-com/storybook'], 9 | 10 | framework: { 11 | name: '@storybook/react-vite', 12 | options: {}, 13 | }, 14 | 15 | core: { 16 | disableTelemetry: true, 17 | }, 18 | 19 | viteFinal: async (config) => 20 | mergeConfig(config, { 21 | plugins: [nxViteTsPaths()], 22 | }), 23 | 24 | docs: { 25 | defaultName: 'Simplified Documentation', 26 | }, 27 | 28 | typescript: { 29 | reactDocgen: 'react-docgen-typescript', 30 | }, 31 | }; 32 | 33 | export default config; 34 | 35 | // To customize your Vite configuration you can use the viteFinal field. 36 | // Check https://storybook.js.org/docs/react/builders/vite#configuration 37 | // and https://nx.dev/recipes/storybook/custom-builder-configs 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Krzysztof Kalkhoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared-util-polyfills", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/shared/util-polyfills/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/eslint:lint", 10 | "outputs": ["{options.outputFile}"] 11 | }, 12 | "test": { 13 | "executor": "@nx/vite:test", 14 | "outputs": ["{workspaceRoot}/coverage/libs/shared/util-polyfills"], 15 | "options": { 16 | "passWithNoTests": true, 17 | "reportsDirectory": "../../../coverage/libs/shared/util-polyfills" 18 | } 19 | }, 20 | "build": { 21 | "executor": "@nx/vite:build", 22 | "outputs": ["{options.outputPath}"], 23 | "defaultConfiguration": "production", 24 | "options": { 25 | "outputPath": "dist/libs/shared/util-polyfills" 26 | }, 27 | "configurations": { 28 | "development": { 29 | "mode": "development" 30 | }, 31 | "production": { 32 | "mode": "production" 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/use-double-tap/src/tests/TestComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DoubleTapCallback, DoubleTapOptions, useDoubleTap } from '../lib'; 3 | import { render, RenderResult } from '@testing-library/react'; 4 | 5 | import { noop } from '@react/shared/util-tests'; 6 | 7 | export interface TestComponentProps { 8 | callback?: DoubleTapCallback; 9 | threshold?: number; 10 | options?: DoubleTapOptions; 11 | } 12 | 13 | const TestComponent: React.FC = ({ callback = noop, threshold, options }) => { 14 | const bind = useDoubleTap(callback, threshold, options); 15 | 16 | return ; 17 | }; 18 | 19 | export function createTestElement(props: TestComponentProps) { 20 | const component = createTestComponent(props); 21 | 22 | return getComponentElement(component); 23 | } 24 | 25 | export function createTestComponent(props: TestComponentProps) { 26 | return render(); 27 | } 28 | 29 | export function getComponentElement(component: RenderResult): HTMLButtonElement { 30 | return component.container.firstChild as HTMLButtonElement; 31 | } 32 | 33 | export default TestComponent; 34 | -------------------------------------------------------------------------------- /packages/use-double-tap/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Krzysztof Kalkhoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/use-long-press/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Krzysztof Kalkhoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/react-interval-hook/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Krzysztof Kalkhoff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/storybook-host/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storybook-host", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/storybook-host/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/eslint:lint" 10 | }, 11 | "storybook": { 12 | "executor": "@nx/storybook:storybook", 13 | "options": { 14 | "port": 4400, 15 | "configDir": "libs/storybook-host/.storybook" 16 | }, 17 | "configurations": { 18 | "ci": { 19 | "quiet": true 20 | } 21 | } 22 | }, 23 | "build-storybook": { 24 | "executor": "@nx/storybook:build", 25 | "outputs": ["{options.outputDir}"], 26 | "options": { 27 | "outputDir": "dist/storybook/storybook-host", 28 | "configDir": "libs/storybook-host/.storybook" 29 | }, 30 | "configurations": { 31 | "ci": { 32 | "quiet": true 33 | } 34 | } 35 | }, 36 | "test-storybook": { 37 | "executor": "nx:run-commands", 38 | "options": { 39 | "command": "test-storybook -c libs/storybook-host/.storybook --url=http://localhost:4400" 40 | } 41 | }, 42 | "static-storybook": { 43 | "executor": "@nx/web:file-server", 44 | "options": { 45 | "buildTarget": "storybook-host:build-storybook", 46 | "staticFilePath": "dist/storybook/storybook-host", 47 | "spa": true 48 | }, 49 | "configurations": { 50 | "ci": { 51 | "buildTarget": "storybook-host:build-storybook:ci" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/use-double-tap/README.md: -------------------------------------------------------------------------------- 1 | # React Double Tap Hook 2 | 3 | 4 | [![codecov](https://codecov.io/gh/minwork/react/branch/main/graph/badge.svg?token=2KPMMSLDOM)](https://codecov.io/gh/minwork/react) 5 | ![npm type definitions](https://img.shields.io/npm/types/use-double-tap) 6 | ![NPM Downloads](https://img.shields.io/npm/dm/use-double-tap) 7 | ![npm](https://img.shields.io/npm/v/use-double-tap) 8 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/use-double-tap) 9 | 10 | ![React Double Tap Hook](https://raw.githubusercontent.com/minwork/react/main/packages/use-double-tap/images/react-double-tap-hook.webp) 11 | 12 | > React hook for handling double tap on mobile devices 13 | 14 | # Main features 15 | - Detect double tap on mobile devices 16 | - Adjustable detection threshold 17 | - Callback for single tap 18 | 19 | # Installation 20 | 21 | ```bash 22 | yarn add use-double-tap 23 | ``` 24 | or 25 | ```bash 26 | npm install --save use-double-tap 27 | ``` 28 | 29 | # Basic usage 30 | 31 | ```tsx 32 | import React from 'react'; // No longer necessary in newer React versions 33 | import { useDoubleTap } from 'use-double-tap'; 34 | 35 | export const Example = () => { 36 | const bind = useDoubleTap((event) => { 37 | // Your action here 38 | console.log('Double tapped'); 39 | }); 40 | 41 | return ; 42 | } 43 | ``` 44 | 45 | # Documentation 46 | 47 | Full documentation can be found [here](https://minwork.gitbook.io/double-tap-hook/) 48 | 49 | # Support 50 | 51 | If you like my work, consider making a [donation](https://github.com/sponsors/minwork) through Github Sponsors. 52 | 53 | # License 54 | 55 | MIT © [minwork](https://github.com/minwork) 56 | -------------------------------------------------------------------------------- /packages/use-double-tap/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-double-tap", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/use-double-tap/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/eslint:lint", 10 | "outputs": ["{options.outputFile}"] 11 | }, 12 | "build": { 13 | "executor": "@nx/vite:build", 14 | "outputs": ["{options.outputPath}"], 15 | "defaultConfiguration": "production", 16 | "options": { 17 | "outputPath": "dist/packages/use-double-tap" 18 | }, 19 | "configurations": { 20 | "development": { 21 | "mode": "development" 22 | }, 23 | "production": { 24 | "mode": "production" 25 | } 26 | } 27 | }, 28 | "test": { 29 | "executor": "@nx/vite:test", 30 | "outputs": ["{workspaceRoot}/coverage/packages/use-double-tap"], 31 | "options": { 32 | "passWithNoTests": true, 33 | "reportsDirectory": "../../coverage/packages/use-double-tap" 34 | } 35 | }, 36 | "release": { 37 | "executor": "nx:run-commands", 38 | "options": { 39 | "parallel": false, 40 | "commands": [ 41 | { 42 | "command": "nx run use-double-tap:test", 43 | "forwardAllArgs": false 44 | }, 45 | { 46 | "command": "nx run use-double-tap:build", 47 | "forwardAllArgs": false 48 | }, 49 | { 50 | "command": "nx run use-double-tap:semantic-release", 51 | "forwardAllArgs": true 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/use-long-press/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "use-long-press", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/use-long-press/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/eslint:lint", 10 | "outputs": ["{options.outputFile}"] 11 | }, 12 | "build": { 13 | "executor": "@nx/vite:build", 14 | "outputs": ["{options.outputPath}"], 15 | "defaultConfiguration": "production", 16 | "options": { 17 | "outputPath": "dist/packages/use-long-press" 18 | }, 19 | "configurations": { 20 | "development": { 21 | "mode": "development" 22 | }, 23 | "production": { 24 | "mode": "production" 25 | } 26 | } 27 | }, 28 | "test": { 29 | "executor": "@nx/vite:test", 30 | "outputs": ["{workspaceRoot}/coverage/packages/use-long-press"], 31 | "options": { 32 | "passWithNoTests": true, 33 | "reportsDirectory": "../../coverage/packages/use-long-press" 34 | } 35 | }, 36 | "release": { 37 | "executor": "nx:run-commands", 38 | "options": { 39 | "parallel": false, 40 | "commands": [ 41 | { 42 | "command": "nx run use-long-press:test", 43 | "forwardAllArgs": false 44 | }, 45 | { 46 | "command": "nx run use-long-press:build", 47 | "forwardAllArgs": false 48 | }, 49 | { 50 | "command": "nx run use-long-press:semantic-release", 51 | "forwardAllArgs": true 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/react-interval-hook/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-interval-hook", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/react-interval-hook/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/eslint:lint", 10 | "outputs": ["{options.outputFile}"] 11 | }, 12 | "build": { 13 | "executor": "@nx/vite:build", 14 | "outputs": ["{options.outputPath}"], 15 | "defaultConfiguration": "production", 16 | "options": { 17 | "outputPath": "dist/packages/react-interval-hook" 18 | }, 19 | "configurations": { 20 | "development": { 21 | "mode": "development" 22 | }, 23 | "production": { 24 | "mode": "production" 25 | } 26 | } 27 | }, 28 | "test": { 29 | "executor": "@nx/vite:test", 30 | "outputs": ["{workspaceRoot}/coverage/packages/react-interval-hook"], 31 | "options": { 32 | "passWithNoTests": true, 33 | "reportsDirectory": "../../coverage/packages/react-interval-hook" 34 | } 35 | }, 36 | "release": { 37 | "executor": "nx:run-commands", 38 | "options": { 39 | "parallel": false, 40 | "commands": [ 41 | { 42 | "command": "nx run react-interval-hook:test", 43 | "forwardAllArgs": false 44 | }, 45 | { 46 | "command": "nx run react-interval-hook:build", 47 | "forwardAllArgs": false 48 | }, 49 | { 50 | "command": "nx run react-interval-hook:semantic-release", 51 | "forwardAllArgs": true 52 | } 53 | ] 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/use-double-tap/src/lib/use-double-tap.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent, useCallback, useRef } from 'react'; 2 | import { CallbackFunction, DoubleTapCallback, DoubleTapOptions, DoubleTapResult } from './use-double-tap.types'; 3 | 4 | /** 5 | * @param callback - The function to be called on a double tap event. 6 | * @param threshold - The time in milliseconds that defines the interval between single taps for them to be considered a double tap. Default is 300 ms. 7 | * @param options - An object containing optional callbacks for single tap and other configurations. 8 | * @return An object with an onClick handler if a callback is provided, otherwise an empty object. 9 | */ 10 | export function useDoubleTap = DoubleTapCallback>( 11 | callback: Callback, 12 | threshold = 300, 13 | options: DoubleTapOptions = {} 14 | ): DoubleTapResult { 15 | const timer = useRef(null); 16 | 17 | const handler = useCallback>( 18 | (event: MouseEvent) => { 19 | if (!timer.current) { 20 | timer.current = setTimeout(() => { 21 | if (options.onSingleTap) { 22 | options.onSingleTap(event); 23 | } 24 | timer.current = null; 25 | }, threshold); 26 | } else { 27 | clearTimeout(timer.current); 28 | timer.current = null; 29 | callback && callback(event); 30 | } 31 | }, 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | [callback, threshold, options.onSingleTap] 34 | ); 35 | 36 | return ( 37 | callback 38 | ? { 39 | onClick: handler, 40 | } 41 | : {} 42 | ) as DoubleTapResult; 43 | } 44 | -------------------------------------------------------------------------------- /packages/react-interval-hook/README.md: -------------------------------------------------------------------------------- 1 | # React Interval Hook 2 | 3 | [![codecov](https://codecov.io/gh/minwork/react/branch/main/graph/badge.svg?token=2KPMMSLDOM)](https://codecov.io/gh/minwork/react) 4 | ![npm type definitions](https://img.shields.io/npm/types/react-interval-hook) 5 | ![NPM Downloads](https://img.shields.io/npm/dm/react-interval-hook) 6 | [![npm](https://img.shields.io/npm/v/react-interval-hook)](https://www.npmjs.com/package/react-interval-hook) 7 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-interval-hook) 8 | 9 | ![React Interval Hook](https://raw.githubusercontent.com/minwork/react/main/packages/react-interval-hook/images/react-interval-hook.webp) 10 | 11 | > React _self-correcting_ interval hook for precision timing, augmented by management methods 12 | 13 | ## Main features 14 | 15 | - Self-correcting ([explanation](https://stackoverflow.com/a/29972322/10322539)) 16 | - Manageable (start, stop, isActive) 17 | - Thoroughly tested 18 | 19 | ## Installation 20 | 21 | ```bash 22 | yarn add react-interval-hook 23 | ``` 24 | 25 | or 26 | 27 | ```bash 28 | npm install --save react-interval-hook 29 | ``` 30 | 31 | # Basic usage 32 | 33 | ```tsx 34 | import React from 'react'; // No longer necessary in newer React versions 35 | import { useInterval } from 'react-interval-hook'; 36 | 37 | export const Example = () => { 38 | useInterval(() => { 39 | console.log('I am called every second'); 40 | }); 41 | 42 | return null; 43 | }; 44 | ``` 45 | 46 | # Documentation 47 | 48 | Full documentation can be found [here](https://minwork.gitbook.io/react-interval-hook/) 49 | 50 | # Support 51 | 52 | If you like my work, consider making a [donation](https://github.com/sponsors/minwork) through Github Sponsors. 53 | 54 | # License 55 | 56 | MIT © [minwork](https://github.com/minwork) 57 | -------------------------------------------------------------------------------- /tools/scripts/utils/output.ts: -------------------------------------------------------------------------------- 1 | import chalk, { BackgroundColorName, ColorName, ForegroundColorName } from 'chalk'; 2 | 3 | const colors = [ 4 | { instance: chalk.green, spinnerColor: 'green' }, 5 | { instance: chalk.greenBright, spinnerColor: 'green' }, 6 | { instance: chalk.red, spinnerColor: 'red' }, 7 | { instance: chalk.redBright, spinnerColor: 'red' }, 8 | { instance: chalk.cyan, spinnerColor: 'cyan' }, 9 | { instance: chalk.cyanBright, spinnerColor: 'cyan' }, 10 | { instance: chalk.yellow, spinnerColor: 'yellow' }, 11 | { instance: chalk.yellowBright, spinnerColor: 'yellow' }, 12 | { instance: chalk.magenta, spinnerColor: 'magenta' }, 13 | { instance: chalk.magentaBright, spinnerColor: 'magenta' }, 14 | ] as const; 15 | 16 | function getColor(projectName: string) { 17 | let code = 0; 18 | for (let i = 0; i < projectName.length; ++i) { 19 | code += projectName.charCodeAt(i); 20 | } 21 | const colorIndex = code % colors.length; 22 | 23 | return colors[colorIndex]; 24 | } 25 | 26 | export function colorProjectName(name: string): string { 27 | return getColor(name).instance.bold(name); 28 | } 29 | 30 | export async function suppressOutput(fn: () => T): Promise { 31 | const originalProcessStdoutWrite = process.stdout.write; 32 | const originalProcessStderrWrite = process.stderr.write; 33 | 34 | process.stdout.write = () => { 35 | return false; 36 | }; 37 | process.stderr.write = () => { 38 | return false; 39 | }; 40 | 41 | const result = await fn(); 42 | 43 | process.stdout.write = originalProcessStdoutWrite; 44 | process.stderr.write = originalProcessStderrWrite; 45 | 46 | return result; 47 | } 48 | 49 | export function printHeader(title: string, color: ForegroundColorName): string { 50 | const bgColor = `bg${color.slice(0, 1).toUpperCase() + color.slice(1)}` as BackgroundColorName; 51 | return chalk.reset[bgColor].black.bold(` ${title.toUpperCase()} `); 52 | } 53 | -------------------------------------------------------------------------------- /packages/use-long-press/README.md: -------------------------------------------------------------------------------- 1 | # React Long Press Hook 2 | 3 | [![codecov](https://codecov.io/gh/minwork/react/branch/main/graph/badge.svg?token=2KPMMSLDOM)](https://codecov.io/gh/minwork/react) 4 | ![npm type definitions](https://img.shields.io/npm/types/use-long-press) 5 | ![NPM Downloads](https://img.shields.io/npm/dm/use-long-press) 6 | [![npm](https://img.shields.io/npm/v/use-long-press)](https://www.npmjs.com/package/use-long-press) 7 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/use-long-press) 8 | 9 | ![React Long Press Hook](https://raw.githubusercontent.com/minwork/react/main/packages/use-long-press/images/react-long-press-hook.webp) 10 | 11 | > React hook for detecting _click_ / _tap_ / _point_ and _hold_ event 12 | 13 | # Main features 14 | - Mouse, Touch and Pointer events support 15 | - Pass custom context and access it in callback 16 | - Cancel long press if moved too far from the target 17 | - Flexible callbacks: `onStart`, `onMove`, `onFinish`, `onCancel` 18 | - Disable hook when necessary 19 | - Filter undesired events (like mouse right clicks) 20 | 21 | # Installation 22 | ```shell 23 | yarn add use-long-press 24 | ``` 25 | or 26 | ```shell 27 | npm install --save use-long-press 28 | ``` 29 | 30 | # Basic usage 31 | Basic hook usage example to get started immediately 32 | ```tsx 33 | import React from 'react'; // No longer necessary in newer React versions 34 | import { useLongPress } from 'use-long-press'; 35 | 36 | const Example = () => { 37 | const handlers = useLongPress(() => { 38 | // Your action here 39 | console.log('Long pressed!'); 40 | }); 41 | return ; 42 | }; 43 | ``` 44 | 45 | # Documentation 46 | 47 | - [Interactive documentation](https://react-libraries-storybook.vercel.app/) (Storybook) 48 | - [Static documentation](https://minwork.gitbook.io/long-press-hook/) (Gitbook) 49 | 50 | # Support 51 | 52 | If you like my work, consider making a [donation](https://github.com/sponsors/minwork) through Github Sponsors. 53 | 54 | # License 55 | 56 | MIT © [minwork](https://github.com/minwork) 57 | -------------------------------------------------------------------------------- /libs/storybook-host/src/lib/long-press/LongPressButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { LongPressButton } from './LongPressButton'; 3 | import { LongPressEventType } from 'use-long-press'; 4 | 5 | const meta: Meta = { component: LongPressButton }; 6 | export default meta; 7 | 8 | type Story = StoryObj; 9 | // type StoryComponent = StoryFn; 10 | 11 | export const Default: Story = { 12 | argTypes: { 13 | filterEvents: { 14 | control: false, 15 | table: { 16 | disable: true, 17 | }, 18 | }, 19 | context: { 20 | control: 'object', 21 | }, 22 | cancelOnMovement: { 23 | control: 'boolean', 24 | }, 25 | detect: { 26 | options: [LongPressEventType.Mouse, LongPressEventType.Touch, LongPressEventType.Pointer], 27 | control: 'radio', 28 | }, 29 | }, 30 | args: { 31 | threshold: 1000, 32 | }, 33 | }; 34 | 35 | export const CancelOnMovement: Story = { 36 | ...Default, 37 | argTypes: { 38 | ...Default.argTypes, 39 | cancelOnMovement: { 40 | control: { 41 | type: 'number', 42 | }, 43 | }, 44 | }, 45 | args: { 46 | ...Default.args, 47 | cancelOnMovement: 25, 48 | }, 49 | }; 50 | 51 | export const NoCancelOutsideElement: Story = { 52 | ...Default, 53 | args: { 54 | ...Default.args, 55 | cancelOutsideElement: false, 56 | }, 57 | }; 58 | 59 | export const FilterRightClick: Story = { 60 | ...Default, 61 | argTypes: { 62 | ...Default.argTypes, 63 | filterEvents: { 64 | control: false, 65 | }, 66 | }, 67 | args: { 68 | ...Default.args, 69 | filterEvents(event) { 70 | // Filter right clicks 71 | return !( 72 | event.nativeEvent instanceof MouseEvent && 73 | (event.nativeEvent.which === 3 || event.nativeEvent.button === 2 || event.ctrlKey || event.metaKey) 74 | ); 75 | }, 76 | }, 77 | }; 78 | 79 | export const WithContext: Story = { 80 | ...Default, 81 | args: { 82 | ...Default.args, 83 | context: 'Example context', 84 | }, 85 | }; 86 | -------------------------------------------------------------------------------- /libs/shared/util-tests/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import dts from 'vite-plugin-dts'; 3 | import { joinPathFragments } from '@nx/devkit'; 4 | import { defineConfig } from 'vite'; 5 | import react from '@vitejs/plugin-react'; 6 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 7 | 8 | export default defineConfig({ 9 | root: __dirname, 10 | // Configuration for building your library. 11 | // See: https://vitejs.dev/guide/build.html#library-mode 12 | build: { 13 | emptyOutDir: true, 14 | outDir: '../../../dist/packages/shared/util-tests', 15 | reportCompressedSize: true, 16 | commonjsOptions: { transformMixedEsModules: true }, 17 | lib: { 18 | // Could also be a dictionary or array of multiple entry points. 19 | entry: 'src/index.ts', 20 | name: 'shared-util-tests', 21 | fileName: 'index', 22 | // Change this to the formats you want to support. 23 | // Don't forgot to update your package.json as well. 24 | formats: ['es', 'cjs'], 25 | }, 26 | rollupOptions: { 27 | // External packages that should not be bundled into your library. 28 | external: ['react', 'react-dom', 'react/jsx-runtime'], 29 | }, 30 | }, 31 | cacheDir: '../../../node_modules/.vite/shared-util-tests', 32 | 33 | plugins: [ 34 | ...[react(), nxViteTsPaths()], 35 | dts({ 36 | entryRoot: 'src', 37 | tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'), 38 | skipDiagnostics: true, 39 | }), 40 | ], 41 | 42 | // Uncomment this if you are using workers. 43 | // worker: { 44 | // plugins: [ 45 | // viteTsConfigPaths({ 46 | // root: '../../../', 47 | // }), 48 | // ], 49 | // }, 50 | 51 | test: { 52 | reporters: ['default'], 53 | globals: true, 54 | environment: 'jsdom', 55 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 56 | coverage: { 57 | enabled: false, 58 | reportsDirectory: '../../../coverage/packages/shared/util-tests', 59 | provider: 'v8', 60 | // Disable coverage for this library 61 | include: [], 62 | }, 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /.github/workflows/post-release.yml: -------------------------------------------------------------------------------- 1 | name: Post Release 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | tags: 9 | - "*@*.*.*" 10 | 11 | jobs: 12 | extract-project: 13 | name: Extract project name from git tag 14 | runs-on: ubuntu-latest 15 | outputs: 16 | project: ${{ steps.extract-project-name.outputs.project }} 17 | steps: 18 | - name: Extract Project Name 19 | id: extract-project-name 20 | shell: bash 21 | run: | 22 | TAG_NAME=${{ github.ref_name }} 23 | if [[ "$TAG_NAME" =~ ^(.+)@[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then 24 | PROJECT_NAME=${BASH_REMATCH[1]} 25 | else 26 | PROJECT_NAME="" 27 | fi 28 | echo "project=$PROJECT_NAME" >> "$GITHUB_OUTPUT" 29 | echo "Extracted project '$PROJECT_NAME' from '$TAG_NAME'" 30 | 31 | upload-test-coverage: 32 | name: Upload tests coverage for project 33 | runs-on: ubuntu-latest 34 | needs: extract-project 35 | if: ${{ needs.extract-project.outputs.project != '' }} 36 | env: 37 | PROJECT: ${{ needs.extract-project.outputs.project }} 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | with: 42 | fetch-tags: 'true' 43 | fetch-depth: '0' 44 | 45 | - name: Enable Corepack 46 | run: corepack enable 47 | 48 | - name: Install Node.js 49 | uses: actions/setup-node@v4 50 | with: 51 | node-version: 20 52 | registry-url: 'https://registry.npmjs.org/' 53 | cache: 'yarn' 54 | cache-dependency-path: 'yarn.lock' 55 | 56 | - name: Install dependencies 57 | run: yarn install --immutable 58 | 59 | - name: Run tests with coverage 60 | run: npx nx run ${{ env.PROJECT }}:test --coverage 61 | 62 | - name: Upload ${{ env.PROJECT }} coverage reports to Codecov 63 | uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 64 | with: 65 | token: ${{ secrets.CODECOV_TOKEN }} 66 | directory: ./coverage/packages/${{ env.PROJECT }} 67 | flags: ${{ env.PROJECT }} 68 | -------------------------------------------------------------------------------- /libs/shared/util-polyfills/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | import dts from 'vite-plugin-dts'; 6 | import { joinPathFragments } from '@nx/devkit'; 7 | 8 | export default defineConfig({ 9 | root: __dirname, 10 | cacheDir: '../../../node_modules/.vite/shared-util-polyfills', 11 | 12 | plugins: [ 13 | dts({ 14 | entryRoot: 'src', 15 | tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'), 16 | skipDiagnostics: true, 17 | }), 18 | react(), 19 | nxViteTsPaths(), 20 | ], 21 | 22 | // Uncomment this if you are using workers. 23 | // worker: { 24 | // plugins: [ 25 | // viteTsConfigPaths({ 26 | // root: '../../../', 27 | // }), 28 | // ], 29 | // }, 30 | 31 | // Configuration for building your library. 32 | // See: https://vitejs.dev/guide/build.html#library-mode 33 | build: { 34 | emptyOutDir: true, 35 | outDir: '../../../dist/packages/shared/util-polyfills', 36 | reportCompressedSize: true, 37 | commonjsOptions: { transformMixedEsModules: true }, 38 | lib: { 39 | // Could also be a dictionary or array of multiple entry points. 40 | entry: 'src/index.ts', 41 | name: 'shared-util-polyfills', 42 | fileName: 'index', 43 | // Change this to the formats you want to support. 44 | // Don't forgot to update your package.json as well. 45 | formats: ['es', 'cjs'], 46 | }, 47 | rollupOptions: { 48 | // External packages that should not be bundled into your library. 49 | external: ['react', 'react-dom', 'react/jsx-runtime'], 50 | }, 51 | }, 52 | 53 | test: { 54 | reporters: ['default'], 55 | globals: true, 56 | environment: 'jsdom', 57 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 58 | coverage: { 59 | enabled: false, 60 | provider: 'v8', 61 | reportsDirectory: '../../../coverage/packages/shared/util-polyfills', 62 | // Disable coverage for this library 63 | include: [], 64 | }, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /packages/use-long-press/src/tests/TestComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { LongPressCallback, LongPressOptions, useLongPress } from '../lib'; 3 | import { render, RenderResult } from '@testing-library/react'; 4 | 5 | export interface TestComponentProps extends LongPressOptions { 6 | callback: LongPressCallback | null; 7 | context?: unknown; 8 | } 9 | 10 | let i = 1; 11 | 12 | export const TestComponent: React.FC = ({ callback, context, ...options }) => { 13 | const bind = useLongPress(callback, options); 14 | const key = useRef(i++); 15 | const handlers = context === undefined ? bind() : bind(context); 16 | 17 | return ( 18 | 51 | ); 52 | }; 53 | 54 | export function createTestElement(props: TestComponentProps) { 55 | const component = createTestComponent(props); 56 | 57 | return getComponentElement(component); 58 | } 59 | 60 | export function createTestComponent(props: TestComponentProps) { 61 | return render(); 62 | } 63 | 64 | export function getComponentElement(component: RenderResult): HTMLButtonElement { 65 | return component.container.firstChild as HTMLButtonElement; 66 | } 67 | -------------------------------------------------------------------------------- /tools/scripts/release/changelog.ts: -------------------------------------------------------------------------------- 1 | import { colorProjectName, printHeader } from '../utils/output'; 2 | import { releaseChangelog } from 'nx/release'; 3 | import { getGitTail, getLatestGitTagVersionsForProject } from './git'; 4 | import { isPrereleaseVersion } from './version'; 5 | import chalk from 'chalk'; 6 | import { HandleChangelogOptions } from './changelog.types'; 7 | 8 | export async function handleChangelog({ projectName, isPrerelease, options }: HandleChangelogOptions): Promise { 9 | // Option to explicitly specify scope of the changelog 10 | let from: string | undefined; 11 | const currentVersion = options.versionData[projectName].currentVersion; 12 | const newVersion = options.versionData[projectName].newVersion; 13 | 14 | // If generating changelog for regular release but current version is detected as prerelease, then explicitly specify 'from' option to generate changelog from last regular release 15 | if (!isPrerelease && isPrereleaseVersion(currentVersion)) { 16 | console.log( 17 | printHeader('changelog', 'cyan'), 18 | `Correcting changelog generation for ${colorProjectName(projectName)}\n` 19 | ); 20 | 21 | // Get latest regular release version that is not a new version 22 | const { releaseVersionTag, releaseVersion } = await getLatestGitTagVersionsForProject(projectName, newVersion); 23 | 24 | if (releaseVersion && releaseVersionTag) { 25 | console.log( 26 | colorProjectName(projectName), 27 | 'Detected changelog generation from', 28 | chalk.red(currentVersion), 29 | 'to', 30 | chalk.redBright(newVersion), 31 | 'but instead will generate from', 32 | chalk.yellow(releaseVersion), 33 | 'to', 34 | chalk.green(newVersion) 35 | ); 36 | 37 | from = releaseVersionTag; 38 | } else { 39 | console.log( 40 | colorProjectName(projectName), 41 | 'Detected changelog generation from', 42 | chalk.red(currentVersion), 43 | 'to', 44 | chalk.redBright(newVersion), 45 | 'but instead will generate from', 46 | chalk.yellow('TAIL'), 47 | 'to', 48 | chalk.green(newVersion) 49 | ); 50 | 51 | from = await getGitTail(); 52 | } 53 | } 54 | 55 | await releaseChangelog({ 56 | ...options, 57 | projects: [projectName], 58 | from, 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /packages/react-interval-hook/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | import dts from 'vite-plugin-dts'; 6 | import { joinPathFragments } from '@nx/devkit'; 7 | import { viteStaticCopy } from 'vite-plugin-static-copy'; 8 | 9 | export default defineConfig({ 10 | root: __dirname, 11 | cacheDir: '../../node_modules/.vite/react-interval-hook', 12 | 13 | plugins: [ 14 | dts({ 15 | entryRoot: 'src', 16 | tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'), 17 | skipDiagnostics: true, 18 | }), 19 | react(), 20 | nxViteTsPaths(), 21 | viteStaticCopy({ 22 | targets: [ 23 | { 24 | src: '*.md', 25 | dest: '.', 26 | }, 27 | ], 28 | }), 29 | ], 30 | 31 | // Uncomment this if you are using workers. 32 | // worker: { 33 | // plugins: [ 34 | // viteTsConfigPaths({ 35 | // root: '../../', 36 | // }), 37 | // ], 38 | // }, 39 | 40 | // Configuration for building your library. 41 | // See: https://vitejs.dev/guide/build.html#library-mode 42 | build: { 43 | emptyOutDir: true, 44 | outDir: '../../dist/packages/react-interval-hook', 45 | reportCompressedSize: true, 46 | commonjsOptions: { transformMixedEsModules: true }, 47 | lib: { 48 | // Could also be a dictionary or array of multiple entry points. 49 | entry: 'src/index.ts', 50 | name: 'react-interval-hook', 51 | fileName: 'index', 52 | // Change this to the formats you want to support. 53 | // Don't forgot to update your package.json as well. 54 | formats: ['es', 'cjs'], 55 | }, 56 | rollupOptions: { 57 | // External packages that should not be bundled into your library. 58 | external: ['react', 'react-dom', 'react/jsx-runtime'], 59 | }, 60 | }, 61 | 62 | test: { 63 | reporters: ['default'], 64 | globals: true, 65 | environment: 'jsdom', 66 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 67 | coverage: { 68 | reportsDirectory: '../../coverage/packages/react-interval-hook', 69 | // Cover only lib files 70 | include: ['src/lib/**/*'], 71 | // you can include other reporters, but 'json-summary' is required, json is recommended 72 | reporter: ['text', 'json-summary', 'json'], 73 | // If you want a coverage reports even if your tests are failing, include the reportOnFailure option 74 | reportOnFailure: true, 75 | thresholds: { 76 | 100: true, 77 | }, 78 | }, 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /packages/use-double-tap/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 5 | import dts from 'vite-plugin-dts'; 6 | import { joinPathFragments } from '@nx/devkit'; 7 | import { viteStaticCopy } from 'vite-plugin-static-copy'; 8 | 9 | export default defineConfig({ 10 | root: __dirname, 11 | cacheDir: '../../node_modules/.vite/use-double-tap', 12 | 13 | plugins: [ 14 | dts({ 15 | entryRoot: 'src', 16 | tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'), 17 | skipDiagnostics: true, 18 | }), 19 | react(), 20 | nxViteTsPaths(), 21 | viteStaticCopy({ 22 | targets: [ 23 | { 24 | src: '*.md', 25 | dest: '.', 26 | }, 27 | ], 28 | }), 29 | ], 30 | 31 | // Uncomment this if you are using workers. 32 | // worker: { 33 | // plugins: [ 34 | // viteTsConfigPaths({ 35 | // root: '../../', 36 | // }), 37 | // ], 38 | // }, 39 | 40 | // Configuration for building your library. 41 | // See: https://vitejs.dev/guide/build.html#library-mode 42 | build: { 43 | emptyOutDir: true, 44 | outDir: '../../dist/packages/use-double-tap', 45 | reportCompressedSize: true, 46 | commonjsOptions: { transformMixedEsModules: true }, 47 | lib: { 48 | // Could also be a dictionary or array of multiple entry points. 49 | entry: 'src/index.ts', 50 | name: 'use-double-tap', 51 | fileName: 'index', 52 | // Change this to the formats you want to support. 53 | // Don't forgot to update your package.json as well. 54 | formats: ['es', 'cjs'], 55 | }, 56 | rollupOptions: { 57 | // External packages that should not be bundled into your library. 58 | external: ['react', 'react-dom', 'react/jsx-runtime'], 59 | }, 60 | }, 61 | 62 | test: { 63 | reporters: ['default'], 64 | globals: true, 65 | setupFiles: 'src/tests/setup-tests.ts', 66 | environment: 'jsdom', 67 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 68 | coverage: { 69 | reportsDirectory: '../../coverage/packages/use-double-tap', 70 | // Cover only lib files 71 | include: ['src/lib/**/*'], 72 | // you can include other reporters, but 'json-summary' is required, json is recommended 73 | reporter: ['text', 'json-summary', 'json'], 74 | // If you want a coverage reports even if your tests are failing, include the reportOnFailure option 75 | reportOnFailure: true, 76 | thresholds: { 77 | 100: true, 78 | }, 79 | }, 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /packages/use-long-press/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | import dts from 'vite-plugin-dts'; 5 | import { viteStaticCopy } from 'vite-plugin-static-copy'; 6 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 7 | import { joinPathFragments } from 'nx/src/utils/path'; 8 | 9 | export default defineConfig({ 10 | root: __dirname, 11 | cacheDir: '../../node_modules/.vite/use-long-press', 12 | 13 | plugins: [ 14 | dts({ 15 | entryRoot: 'src', 16 | tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'), 17 | skipDiagnostics: true, 18 | }), 19 | react(), 20 | nxViteTsPaths(), 21 | viteStaticCopy({ 22 | targets: [ 23 | { 24 | src: '*.md', 25 | dest: '.', 26 | }, 27 | ], 28 | }), 29 | ], 30 | 31 | // Uncomment this if you are using workers. 32 | // worker: { 33 | // plugins: [ 34 | // viteTsConfigPaths({ 35 | // root: '../../', 36 | // }), 37 | // ], 38 | // }, 39 | 40 | // Configuration for building your library. 41 | // See: https://vitejs.dev/guide/build.html#library-mode 42 | build: { 43 | emptyOutDir: true, 44 | outDir: '../../dist/packages/use-long-press', 45 | reportCompressedSize: true, 46 | commonjsOptions: { transformMixedEsModules: true }, 47 | lib: { 48 | // Could also be a dictionary or array of multiple entry points. 49 | entry: 'src/index.ts', 50 | name: 'use-long-press', 51 | fileName: 'index', 52 | // Change this to the formats you want to support. 53 | // Don't forgot to update your package.json as well. 54 | formats: ['es', 'cjs'], 55 | }, 56 | rollupOptions: { 57 | // External packages that should not be bundled into your library. 58 | external: ['react', 'react-dom', 'react/jsx-runtime'], 59 | }, 60 | }, 61 | 62 | test: { 63 | reporters: ['default'], 64 | globals: true, 65 | setupFiles: 'src/tests/setup-tests.ts', 66 | environment: 'jsdom', 67 | include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 68 | coverage: { 69 | reportsDirectory: '../../coverage/packages/use-long-press', 70 | // Cover only lib files 71 | include: ['src/lib/**/*'], 72 | // you can include other reporters, but 'json-summary' is required, json is recommended 73 | reporter: ['text', 'json-summary', 'json'], 74 | // If you want a coverage reports even if your tests are failing, include the reportOnFailure option 75 | reportOnFailure: true, 76 | thresholds: { 77 | 100: true, 78 | }, 79 | }, 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minwork - React Libraries 2 | 3 | ![Minwork React Libraries](https://raw.githubusercontent.com/minwork/react/main/images/react-libraries-banner.webp) 4 | 5 | # What is Minwork? 6 | Minwork is the personal brand of Krzysztof Kalkhoff. You can find more information about it on my [website](https://minwork.it). 7 | 8 | # Libraries 9 | 10 | ## Use Long Press 11 | 12 | ![NPM Downloads](https://img.shields.io/npm/dm/use-long-press) 13 | [![npm](https://img.shields.io/npm/v/use-long-press)](https://www.npmjs.com/package/use-long-press) 14 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/use-long-press) 15 | 16 | ![React Long Press Hook](https://raw.githubusercontent.com/minwork/react/main/packages/use-long-press/images/react-long-press-hook.webp) 17 | 18 | > React hook for detecting _click_, _tap_ or _point_ and _hold_ event. 19 | 20 | - [Readme](./packages/use-long-press/README.md) 21 | - [Interactive documentation](https://react-libraries-storybook.vercel.app/) (Storybook) 22 | - [Static documentation](https://minwork.gitbook.io/long-press-hook/) (Gitbook) 23 | 24 | ## Use Double Tap 25 | 26 | ![NPM Downloads](https://img.shields.io/npm/dm/use-double-tap) 27 | ![npm](https://img.shields.io/npm/v/use-double-tap) 28 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/use-double-tap) 29 | 30 | 31 | ![React Double Tap Hook](https://raw.githubusercontent.com/minwork/react/main/packages/use-double-tap/images/react-double-tap-hook.webp) 32 | 33 | > React hook for handling _double tap_ on mobile devices 34 | 35 | - [Readme](./packages/use-double-tap/README.md) 36 | - [Documentation](https://minwork.gitbook.io/double-tap-hook) 37 | 38 | ## React Interval Hook 39 | 40 | ![NPM Downloads](https://img.shields.io/npm/dm/react-interval-hook) 41 | [![npm](https://img.shields.io/npm/v/react-interval-hook)](https://www.npmjs.com/package/react-interval-hook) 42 | ![npm bundle size](https://img.shields.io/bundlephobia/minzip/react-interval-hook) 43 | 44 | ![React Interval Hook](https://raw.githubusercontent.com/minwork/react/main/packages/react-interval-hook/images/react-interval-hook.webp) 45 | 46 | > React _self-correcting_ interval hook for precision timing, augmented by management methods 47 | 48 | - [Readme](./packages/react-interval-hook/README.md) 49 | - [Documentation](https://minwork.gitbook.io/react-interval-hook/) 50 | 51 | ## What's new 52 | If you’re curious about the direction these libraries are heading, check out the [issues list](https://github.com/minwork/react/issues) to see what [new features](https://github.com/minwork/react/issues?q=is%3Aissue+is%3Aopen+label%3Afeature) are coming soon™️. 53 | 54 | # Support 55 | If you like to support my work consider making a [donation](https://github.com/sponsors/minwork). 56 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Verify PR changes 5 | 6 | permissions: 7 | contents: read 8 | pull-requests: write 9 | 10 | on: 11 | pull_request: 12 | branches: ['main'] 13 | 14 | jobs: 15 | verify-pr: 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | node-version: [20.x, 22.x] 22 | package: [use-long-press, use-double-tap, react-interval-hook] 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-tags: 'true' 29 | fetch-depth: '0' 30 | 31 | - name: Enable Corepack 32 | run: corepack enable 33 | 34 | - name: Install Node.js v${{ matrix.node-version }} 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version: ${{ matrix.node-version }} 38 | registry-url: 'https://registry.npmjs.org/' 39 | cache: 'yarn' 40 | cache-dependency-path: 'yarn.lock' 41 | 42 | - name: Install dependencies 43 | run: yarn install --immutable 44 | 45 | - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies 46 | run: npm audit signatures 47 | 48 | - name: Lint all packages 49 | run: yarn lint 50 | 51 | - name: Type check all packages 52 | run: yarn typecheck 53 | 54 | - name: Test all packages with coverage 55 | run: yarn test:coverage 56 | 57 | - name: Report coverage for ${{ matrix.package }} 58 | uses: davelosert/vitest-coverage-report-action@4921c44721dd660c957e91843f00e1f837ab4375 59 | with: 60 | name: ${{ matrix.package }} coverage report 61 | working-directory: packages/${{ matrix.package }} 62 | json-summary-path: ../../coverage/packages/${{ matrix.package }}/coverage-summary.json 63 | json-final-path: ../../coverage/packages/${{ matrix.package }}/coverage-final.json 64 | 65 | report-status: 66 | name: Report PR verification status 67 | runs-on: ubuntu-latest 68 | needs: verify-pr 69 | if: always() # Ensures it runs even if a matrix job fails 70 | steps: 71 | - name: Determine overall status 72 | run: | 73 | if [[ "${{ needs.verify-pr.result }}" == "success" ]]; then 74 | echo "All matrix jobs passed ✅" 75 | exit 0 76 | else 77 | echo "Some matrix jobs failed ❌" 78 | exit 1 79 | fi 80 | outputs: 81 | status: ${{ job.status }} 82 | -------------------------------------------------------------------------------- /packages/use-long-press/src/tests/use-long-press.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, test } from 'vitest'; 2 | import { renderHook } from '@testing-library/react-hooks'; 3 | import { 4 | LongPressCallback, 5 | LongPressEmptyHandlers, 6 | LongPressEventType, 7 | LongPressHandlers, 8 | LongPressMouseHandlers, 9 | LongPressOptions, 10 | LongPressPointerHandlers, 11 | LongPressResult, 12 | LongPressTouchHandlers, 13 | useLongPress, 14 | } from '../lib'; 15 | import { noop } from './use-long-press.test.consts'; 16 | 17 | describe('useLongPress typings', () => { 18 | test('General hook function typings', () => { 19 | const { result } = renderHook(() => useLongPress(noop)); 20 | const bind = result.current; 21 | 22 | expectTypeOf(useLongPress).toBeFunction(); 23 | expectTypeOf(useLongPress).parameter(0).toMatchTypeOf(); 24 | expectTypeOf(useLongPress).parameter(1).toMatchTypeOf(); 25 | expectTypeOf(useLongPress).returns.toBeFunction(); 26 | 27 | expectTypeOf(bind).toMatchTypeOf>(); 28 | expectTypeOf(bind).toBeFunction(); 29 | expectTypeOf(bind).parameter(0).toBeUnknown(); 30 | expectTypeOf(bind).returns.toBeObject(); 31 | expectTypeOf(bind).returns.toMatchTypeOf(); 32 | 33 | const handlers = bind(); 34 | 35 | expectTypeOf(handlers).toBeObject(); 36 | expectTypeOf(handlers).toMatchTypeOf(); 37 | }); 38 | 39 | test('Hook with null callback do not return handlers', () => { 40 | const { result } = renderHook(() => useLongPress(null)); 41 | expectTypeOf(result.current).returns.toMatchTypeOf(); 42 | }); 43 | 44 | test('Hook with default detect to return pointer handlers', () => { 45 | const { result } = renderHook(() => useLongPress(noop)); 46 | expectTypeOf(result.current).returns.toMatchTypeOf>(); 47 | }); 48 | 49 | test('Hook with "mouse" detect to return mouse handlers', () => { 50 | const { result } = renderHook(() => useLongPress(noop, { detect: LongPressEventType.Mouse })); 51 | expectTypeOf(result.current).returns.toMatchTypeOf>(); 52 | }); 53 | 54 | test('Hook with "touch" detect to return touch handlers', () => { 55 | const { result } = renderHook(() => useLongPress(noop, { detect: LongPressEventType.Touch })); 56 | expectTypeOf(result.current).returns.toMatchTypeOf>(); 57 | }); 58 | 59 | test('Hook with "pointer" detect to return pointer handlers', () => { 60 | const { result } = renderHook(() => useLongPress(noop, { detect: LongPressEventType.Pointer })); 61 | expectTypeOf(result.current).returns.toMatchTypeOf>(); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /packages/use-long-press/src/lib/use-long-press.utils.ts: -------------------------------------------------------------------------------- 1 | import { LongPressDomEvents, LongPressReactEvents } from './use-long-press.types'; 2 | import { 3 | MouseEvent as ReactMouseEvent, 4 | PointerEvent as ReactPointerEvent, 5 | SyntheticEvent, 6 | TouchEvent as ReactTouchEvent, 7 | } from 'react'; 8 | 9 | const recognisedMouseEvents: string[] = [ 10 | 'mousedown', 11 | 'mousemove', 12 | 'mouseup', 13 | 'mouseleave', 14 | 'mouseout', 15 | ] satisfies (keyof WindowEventMap)[]; 16 | 17 | const recognisedTouchEvents: string[] = [ 18 | 'touchstart', 19 | 'touchmove', 20 | 'touchend', 21 | 'touchcancel', 22 | ] satisfies (keyof WindowEventMap)[]; 23 | 24 | const recognisedPointerEvents: string[] = [ 25 | 'pointerdown', 26 | 'pointermove', 27 | 'pointerup', 28 | 'pointerleave', 29 | 'pointerout', 30 | ] satisfies (keyof WindowEventMap)[]; 31 | 32 | function hasPageCoordinates(obj: unknown): boolean { 33 | return ( 34 | typeof obj === 'object' && 35 | obj !== null && 36 | 'pageX' in obj && 37 | typeof obj.pageX === 'number' && 38 | 'pageY' in obj && 39 | typeof obj.pageY === 'number' 40 | ); 41 | } 42 | 43 | export function isMouseEvent(event: SyntheticEvent): event is ReactMouseEvent { 44 | return recognisedMouseEvents.includes(event?.nativeEvent?.type); 45 | } 46 | 47 | export function isTouchEvent(event: SyntheticEvent): event is ReactTouchEvent { 48 | return recognisedTouchEvents.includes(event?.nativeEvent?.type) || 'touches' in event; 49 | } 50 | 51 | export function isPointerEvent( 52 | event: SyntheticEvent 53 | ): event is ReactPointerEvent { 54 | const { nativeEvent } = event; 55 | if (!nativeEvent) return false; 56 | 57 | return recognisedPointerEvents.includes(nativeEvent?.type) || 'pointerId' in nativeEvent; 58 | } 59 | 60 | export function isRecognisableEvent( 61 | event: SyntheticEvent 62 | ): event is LongPressReactEvents { 63 | return isMouseEvent(event) || isTouchEvent(event) || isPointerEvent(event); 64 | } 65 | 66 | export function getCurrentPosition( 67 | event: LongPressReactEvents 68 | ): { 69 | x: number; 70 | y: number; 71 | } | null { 72 | const positionHolder = isTouchEvent(event) ? event?.touches?.[0] : event; 73 | 74 | if (hasPageCoordinates(positionHolder)) { 75 | return { 76 | x: positionHolder.pageX, 77 | y: positionHolder.pageY, 78 | }; 79 | } 80 | return null; 81 | } 82 | 83 | export function createArtificialReactEvent( 84 | event: LongPressDomEvents 85 | ): LongPressReactEvents { 86 | return { 87 | target: event.target, 88 | currentTarget: event.currentTarget, 89 | nativeEvent: event, 90 | // eslint-disable-next-line @typescript-eslint/no-empty-function 91 | persist: () => {}, 92 | } as LongPressReactEvents; 93 | } 94 | -------------------------------------------------------------------------------- /libs/shared/util-tests/src/lib/events.functions.ts: -------------------------------------------------------------------------------- 1 | import '@react/shared/util-polyfills'; 2 | import { createEvent } from '@testing-library/react'; 3 | import { EventType } from '@testing-library/dom/types/events'; 4 | import { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, TouchEvent as ReactTouchEvent } from 'react'; 5 | /* 6 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 7 | ⎹ Mocked events 8 | ⌞____________________________________________________________________________________________________ 9 | */ 10 | 11 | export function createMockedTouchEvent( 12 | options?: Partial> & { nativeEvent?: TouchEvent } 13 | ): ReactTouchEvent { 14 | return { 15 | nativeEvent: new TouchEvent('touch'), 16 | touches: [{ pageX: 0, pageY: 0 }], 17 | ...options, 18 | } as ReactTouchEvent; 19 | } 20 | 21 | export function createMockedMouseEvent( 22 | options?: Partial> & { nativeEvent?: MouseEvent } 23 | ): ReactMouseEvent { 24 | return { 25 | nativeEvent: new MouseEvent('mouse'), 26 | ...options, 27 | } as ReactMouseEvent; 28 | } 29 | 30 | export function createMockedPointerEvent( 31 | options?: Partial> & { nativeEvent?: PointerEvent } 32 | ): ReactPointerEvent { 33 | return { 34 | nativeEvent: new PointerEvent('pointer'), 35 | ...options, 36 | } as ReactPointerEvent; 37 | } 38 | 39 | /* 40 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 41 | ⎹ Mocked positioned events (with 'x' and 'y' coordinates) 42 | ⌞____________________________________________________________________________________________________ 43 | */ 44 | export function createPositionedMouseEvent( 45 | element: Document | Element | Window | Node, 46 | eventType: EventType, 47 | x: number, 48 | y: number 49 | ): MouseEvent { 50 | const event = createEvent[eventType](element) as unknown as MouseEvent; 51 | Object.assign(event, { 52 | pageX: x, 53 | pageY: y, 54 | }); 55 | 56 | return event; 57 | } 58 | 59 | export function createPositionedPointerEvent( 60 | element: Document | Element | Window | Node, 61 | eventType: EventType, 62 | x: number, 63 | y: number 64 | ): PointerEvent { 65 | const event = createEvent[eventType]({ 66 | ...element, 67 | // Remove this after jsdom add support for pointer events 68 | ownerDocument: { ...document, defaultView: window }, 69 | }) as PointerEvent; 70 | Object.assign(event, { 71 | pageX: x, 72 | pageY: y, 73 | }); 74 | 75 | return event; 76 | } 77 | 78 | export function createPositionedTouchEvent( 79 | element: Document | Element | Window | Node, 80 | eventType: EventType, 81 | x: number, 82 | y: number 83 | ): TouchEvent { 84 | return createEvent[eventType](element, { 85 | touches: [{ pageX: x, pageY: y } as Touch], 86 | }) as TouchEvent; 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | channel: 6 | description: 'NPM Release channel' 7 | type: choice 8 | required: false 9 | options: 10 | - preview 11 | - latest 12 | dry-run: 13 | description: Should only preview operations instead of actually executing them 14 | type: boolean 15 | required: false 16 | default: true 17 | publish-only: 18 | description: Should run only publish command 19 | type: boolean 20 | required: false 21 | default: false 22 | skip-publish: 23 | description: Should run only version and changelog and skip publishing 24 | type: boolean 25 | required: false 26 | default: false 27 | github-release: 28 | description: Should create Github release (doing so will require pushing changes) 29 | type: boolean 30 | required: false 31 | default: true 32 | 33 | env: 34 | TERM: xterm-256color 35 | FORCE_COLOR: 'true' 36 | 37 | jobs: 38 | release: 39 | runs-on: ubuntu-latest 40 | permissions: 41 | contents: write # to be able to publish a GitHub release 42 | issues: write # to be able to comment on released issues 43 | pull-requests: write # to be able to comment on released pull requests 44 | id-token: write # to enable use of OIDC for npm provenance 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 49 | with: 50 | fetch-tags: 'true' 51 | fetch-depth: '0' 52 | 53 | - name: Enable Corepack 54 | run: corepack enable 55 | 56 | - name: Install Node.js 57 | uses: actions/setup-node@v4 58 | with: 59 | node-version: 20 60 | registry-url: 'https://registry.npmjs.org/' 61 | cache: 'yarn' 62 | cache-dependency-path: 'yarn.lock' 63 | 64 | - name: Install dependencies 65 | run: yarn install --immutable 66 | 67 | - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies 68 | run: npm audit signatures 69 | 70 | - name: Lint all packages 71 | run: yarn lint 72 | 73 | - name: Test all packages 74 | run: yarn test 75 | 76 | - name: Build all packages 77 | run: yarn build 78 | 79 | - name: Setup Git user 80 | run: | 81 | git config user.name "${{ github.actor }}" 82 | git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" 83 | 84 | - name: Release packages on '${{ inputs.channel }}' channel ${{ inputs.dry-run && '(dry-run)' || ''}} 85 | run: yarn release -c ${{ inputs.channel }} --verbose ${{ inputs.publish-only && '--publishOnly' || '' }} ${{ inputs.skip-publish && '--skipPublish' || '' }} ${{ inputs.github-release && '--githubRelease' || '' }} ${{ inputs.dry-run && '--dryRun' || '' }} 86 | env: 87 | RELEASE_ENABLED: ${{ vars.RELEASE_ENABLED }} 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 90 | NPM_CONFIG_PROVENANCE: true 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "lint": "nx run-many -t lint", 7 | "test": "nx run-many -t test", 8 | "test:coverage": "nx run-many -t test --coverage", 9 | "build": "nx run-many -t build", 10 | "typecheck": "nx run-many -t build --noEmit", 11 | "check": "nx run-many -t lint test build", 12 | "release": "tsx tools/scripts/release/release.ts" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@emotion/react": "^11.13.3", 17 | "@emotion/styled": "^11.13.0", 18 | "@mui/material": "^5.16.7", 19 | "@vercel/analytics": "^1.5.0", 20 | "notistack": "^3.0.1", 21 | "react": "18.3.1", 22 | "react-dom": "18.3.1", 23 | "storybook": "^8.6.15", 24 | "tslib": "^2.3.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.14.5", 28 | "@babel/preset-react": "^7.14.5", 29 | "@chromatic-com/storybook": "^3", 30 | "@commitlint/cli": "^17.4.4", 31 | "@commitlint/config-conventional": "^17.4.4", 32 | "@nx/eslint": "20.5.0-beta.4", 33 | "@nx/eslint-plugin": "20.5.0-beta.4", 34 | "@nx/jest": "20.5.0-beta.4", 35 | "@nx/js": "20.5.0-beta.4", 36 | "@nx/node": "20.5.0-beta.4", 37 | "@nx/react": "20.5.0-beta.4", 38 | "@nx/storybook": "20.5.0-beta.4", 39 | "@nx/vite": "20.5.0-beta.4", 40 | "@nx/web": "20.5.0-beta.4", 41 | "@nx/workspace": "20.5.0-beta.4", 42 | "@storybook/addon-essentials": "8.5.8", 43 | "@storybook/addon-interactions": "8.5.8", 44 | "@storybook/core-server": "8.5.8", 45 | "@storybook/react-vite": "8.5.8", 46 | "@storybook/test": "^8.2.8", 47 | "@storybook/test-runner": "0.19.1", 48 | "@swc-node/register": "~1.9.1", 49 | "@swc/core": "~1.5.7", 50 | "@swc/helpers": "~0.5.11", 51 | "@testing-library/dom": "^10.4.0", 52 | "@testing-library/react": "16.1.0", 53 | "@testing-library/react-hooks": "^7.0.2", 54 | "@types/jest": "29.5.14", 55 | "@types/node": "18.16.9", 56 | "@types/react": "18.3.1", 57 | "@types/react-dom": "18.3.0", 58 | "@types/yargs": "^17.0.24", 59 | "@typescript-eslint/eslint-plugin": "7.18.0", 60 | "@typescript-eslint/parser": "7.18.0", 61 | "@vitejs/plugin-react": "4.3.4", 62 | "@vitest/coverage-v8": "3.0.6", 63 | "@vitest/ui": "1.6.0", 64 | "chalk": "^5.3.0", 65 | "eslint": "8.57.0", 66 | "eslint-config-prettier": "9.1.0", 67 | "eslint-plugin-import": "2.31.0", 68 | "eslint-plugin-jsx-a11y": "6.10.1", 69 | "eslint-plugin-react": "7.32.2", 70 | "eslint-plugin-react-hooks": "5.0.0", 71 | "execa": "^7.1.1", 72 | "husky": "^8.0.3", 73 | "jest": "29.7.0", 74 | "jest-environment-node": "^29.4.1", 75 | "jiti": "2.4.2", 76 | "jsdom": "22.1.0", 77 | "nx": "20.5.0-beta.4", 78 | "prettier": "^2.6.2", 79 | "semver": "^7.6.3", 80 | "ts-jest": "29.1.0", 81 | "ts-node": "10.9.1", 82 | "tsx": "^4.19.3", 83 | "typescript": "5.7.3", 84 | "vite": "6.1.1", 85 | "vite-plugin-dts": "2.3.0", 86 | "vite-plugin-eslint": "^1.8.1", 87 | "vite-plugin-static-copy": "^0.14.0", 88 | "vite-tsconfig-paths": "^4.0.2", 89 | "vitest": "3.0.6", 90 | "yargs": "^17.7.1" 91 | }, 92 | "resolutions": { 93 | "esbuild": "~0.25.0", 94 | "koa": "~2.15.4" 95 | }, 96 | "packageManager": "yarn@4.6.0" 97 | } 98 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "targetDefaults": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "inputs": ["production", "^production"], 7 | "cache": true 8 | }, 9 | "test": { 10 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 11 | "cache": true 12 | }, 13 | "@nx/vite:test": { 14 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 15 | "cache": true 16 | }, 17 | "@nx/eslint:lint": { 18 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.eslintignore"], 19 | "cache": true 20 | }, 21 | "build-storybook": { 22 | "cache": true, 23 | "inputs": ["default", "^production", "{projectRoot}/.storybook/**/*", "{projectRoot}/tsconfig.storybook.json"] 24 | }, 25 | "nx-release-publish": { 26 | "options": { 27 | "packageRoot": "dist/packages/{projectName}" 28 | } 29 | } 30 | }, 31 | "namedInputs": { 32 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 33 | "production": [ 34 | "default", 35 | "!{projectRoot}/.eslintrc.json", 36 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 37 | "!{projectRoot}/tsconfig.spec.json", 38 | "!{projectRoot}/jest.config.[jt]s", 39 | "!{projectRoot}/src/test-setup.[jt]s", 40 | "!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)", 41 | "!{projectRoot}/.storybook/**/*", 42 | "!{projectRoot}/tsconfig.storybook.json" 43 | ], 44 | "sharedGlobals": ["{workspaceRoot}/babel.config.json"] 45 | }, 46 | "workspaceLayout": { 47 | "appsDir": "packages", 48 | "libsDir": "packages" 49 | }, 50 | "generators": { 51 | "@nx/react": { 52 | "application": { 53 | "babel": true 54 | }, 55 | "library": { 56 | "unitTestRunner": "vitest" 57 | } 58 | } 59 | }, 60 | "cli": { 61 | "packageManager": "yarn" 62 | }, 63 | "useInferencePlugins": false, 64 | "defaultBase": "master", 65 | "release": { 66 | "projectsRelationship": "independent", 67 | "projects": ["packages/*", "!libs/*"], 68 | "conventionalCommits": { 69 | "types": { 70 | "chore": { 71 | "changelog": false 72 | }, 73 | "docs": { 74 | "semverBump": "patch" 75 | }, 76 | "refactor": { 77 | "semverBump": "patch" 78 | }, 79 | "style": { 80 | "semverBump": "patch", 81 | "changelog": false 82 | }, 83 | "perf": { 84 | "semverBump": "patch", 85 | "changelog": false 86 | }, 87 | "build": { 88 | "semverBump": "patch" 89 | }, 90 | "test": { 91 | "changelog": false 92 | } 93 | } 94 | }, 95 | "version": { 96 | "conventionalCommits": true, 97 | "generatorOptions": { 98 | "fallbackCurrentVersionResolver": "disk" 99 | }, 100 | "preVersionCommand": "yarn nx run-many -t build" 101 | }, 102 | "changelog": { 103 | "projectChangelogs": true, 104 | "workspaceChangelog": false, 105 | "git": { 106 | "commitMessage": "chore(release): Release {projectName} {version} [skip ci]" 107 | } 108 | } 109 | }, 110 | "useLegacyCache": true 111 | } 112 | -------------------------------------------------------------------------------- /packages/use-long-press/src/tests/use-long-press.test.functions.ts: -------------------------------------------------------------------------------- 1 | import { createEvent } from '@testing-library/react'; 2 | import { EventType } from '@testing-library/dom/types/events'; 3 | import { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, TouchEvent as ReactTouchEvent } from 'react'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-empty-function 6 | export const noop = () => {}; 7 | 8 | export function convertHandlerNameToEventName(handlerName: string): string { 9 | const str = handlerName.substring(2); 10 | return str.charAt(0).toLowerCase() + str.substring(1); 11 | } 12 | 13 | export function appendPositionToEvent(event: object, x: number, y: number): void { 14 | Object.defineProperties(event, { 15 | pageX: { 16 | value: x, 17 | writable: false, 18 | enumerable: true, 19 | }, 20 | pageY: { 21 | value: y, 22 | writable: false, 23 | enumerable: true, 24 | }, 25 | }); 26 | } 27 | /* 28 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 29 | ⎹ Mocked events 30 | ⌞____________________________________________________________________________________________________ 31 | */ 32 | 33 | export function createMockedTouchEvent( 34 | options?: Partial> & { nativeEvent?: TouchEvent } 35 | ): ReactTouchEvent { 36 | return { 37 | nativeEvent: new TouchEvent('touchstart'), 38 | touches: [{ pageX: 0, pageY: 0 }], 39 | ...options, 40 | } as ReactTouchEvent; 41 | } 42 | 43 | export function createMockedMouseEvent( 44 | options?: Partial> & { nativeEvent?: MouseEvent } 45 | ): ReactMouseEvent { 46 | return { 47 | nativeEvent: new MouseEvent('mousedown'), 48 | ...options, 49 | } as ReactMouseEvent; 50 | } 51 | 52 | export function createMockedPointerEvent( 53 | options?: Partial> & { nativeEvent?: PointerEvent } 54 | ): ReactPointerEvent { 55 | return { 56 | nativeEvent: new PointerEvent('pointerdown'), 57 | // pointerId: 1, 58 | ...options, 59 | } as ReactPointerEvent; 60 | } 61 | 62 | /* 63 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 64 | ⎹ Mocked positioned events (with 'x' and 'y' coordinates) 65 | ⌞____________________________________________________________________________________________________ 66 | */ 67 | export function createPositionedMouseEvent( 68 | element: Document | Element | Window | Node, 69 | eventType: EventType, 70 | x: number, 71 | y: number 72 | ): MouseEvent { 73 | const event = createEvent[eventType](element) as unknown as MouseEvent; 74 | appendPositionToEvent(event, x, y); 75 | 76 | return event; 77 | } 78 | 79 | export function createPositionedPointerEvent( 80 | element: Document | Element | Window | Node, 81 | eventType: EventType, 82 | x: number, 83 | y: number 84 | ): PointerEvent { 85 | const event = createEvent[eventType]({ 86 | ...element, 87 | // Remove this after jsdom add support for pointer events 88 | ownerDocument: { ...document, defaultView: window }, 89 | }) as PointerEvent; 90 | appendPositionToEvent(event, x, y); 91 | 92 | return event; 93 | } 94 | 95 | export function createPositionedTouchEvent( 96 | element: Document | Element | Window | Node, 97 | eventType: EventType, 98 | x: number, 99 | y: number 100 | ): TouchEvent { 101 | return createEvent[eventType](element, { 102 | touches: [{ pageX: x, pageY: y } as Touch], 103 | }) as TouchEvent; 104 | } 105 | -------------------------------------------------------------------------------- /packages/use-long-press/src/tests/use-long-press.test.consts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LongPressCallbackMeta, 3 | LongPressEventType, 4 | LongPressMouseHandlers, 5 | LongPressPointerHandlers, 6 | LongPressTouchHandlers, 7 | } from '../lib'; 8 | import { EventType } from '@testing-library/dom/types/events'; 9 | import { LongPressTestEventCreator, LongPressTestHandlerType } from './use-long-press.test.types'; 10 | import { 11 | createMockedMouseEvent, 12 | createMockedPointerEvent, 13 | createMockedTouchEvent, 14 | createPositionedMouseEvent, 15 | createPositionedPointerEvent, 16 | createPositionedTouchEvent, 17 | } from './use-long-press.test.functions'; 18 | import { expect } from 'vitest'; 19 | 20 | /* 21 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 22 | ⎹ Utility constants 23 | ⌞____________________________________________________________________________________________________ 24 | */ 25 | export const emptyContext: LongPressCallbackMeta = { context: undefined }; 26 | // eslint-disable-next-line @typescript-eslint/no-empty-function 27 | export const noop = () => {}; 28 | 29 | /* 30 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 31 | ⎹ Test constants 32 | ⌞____________________________________________________________________________________________________ 33 | */ 34 | export const expectMouseEvent = expect.objectContaining({ nativeEvent: expect.any(MouseEvent) }); 35 | export const expectTouchEvent = expect.objectContaining({ nativeEvent: expect.any(TouchEvent) }); 36 | export const expectPointerEvent = expect.objectContaining({ nativeEvent: expect.any(PointerEvent) }); 37 | export const expectSpecificEvent = (event: Event) => 38 | expect.objectContaining({ 39 | nativeEvent: event, 40 | }); 41 | 42 | /* 43 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 44 | ⎹ Utility maps 45 | ⌞____________________________________________________________________________________________________ 46 | */ 47 | 48 | export const longPressPositionedEventCreatorMap = { 49 | [LongPressEventType.Mouse]: createPositionedMouseEvent, 50 | [LongPressEventType.Touch]: createPositionedTouchEvent, 51 | [LongPressEventType.Pointer]: createPositionedPointerEvent, 52 | } satisfies Record Event>; 53 | 54 | export const longPressTestHandlerNamesMap = { 55 | [LongPressEventType.Mouse]: { 56 | start: 'onMouseDown', 57 | move: 'onMouseMove', 58 | stop: 'onMouseUp', 59 | leave: 'onMouseLeave', 60 | }, 61 | [LongPressEventType.Touch]: { 62 | start: 'onTouchStart', 63 | move: 'onTouchMove', 64 | stop: 'onTouchEnd', 65 | leave: undefined as unknown as keyof LongPressTouchHandlers, 66 | }, 67 | [LongPressEventType.Pointer]: { 68 | start: 'onPointerDown', 69 | move: 'onPointerMove', 70 | stop: 'onPointerUp', 71 | leave: 'onPointerLeave', 72 | }, 73 | } satisfies Record< 74 | LongPressEventType, 75 | Record< 76 | LongPressTestHandlerType | 'leave', 77 | keyof LongPressMouseHandlers | keyof LongPressTouchHandlers | keyof LongPressPointerHandlers 78 | > 79 | >; 80 | 81 | export const longPressMockedEventCreatorMap = { 82 | [LongPressEventType.Mouse]: createMockedMouseEvent, 83 | [LongPressEventType.Touch]: createMockedTouchEvent, 84 | [LongPressEventType.Pointer]: createMockedPointerEvent, 85 | } satisfies Record; 86 | 87 | export const longPressExpectedEventMap = { 88 | [LongPressEventType.Mouse]: expectMouseEvent, 89 | [LongPressEventType.Touch]: expectTouchEvent, 90 | [LongPressEventType.Pointer]: expectPointerEvent, 91 | } satisfies Record; 92 | -------------------------------------------------------------------------------- /libs/storybook-host/src/lib/long-press/LongPressButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useRef } from 'react'; 2 | import { LongPressEventType, LongPressReactEvents, useLongPress } from 'use-long-press'; 3 | import { Button } from '@mui/material'; 4 | import { useSnackbar } from 'notistack'; 5 | 6 | export interface LongPressButtonProps { 7 | /** 8 | * Button text 9 | */ 10 | children?: string; 11 | /** 12 | * Any value passed to result of `useLongPress` returned in callbacks inside `meta` param 13 | */ 14 | context?: unknown; 15 | /** 16 | * Period of time that must elapse after detecting click or tap in order to trigger _callback_ 17 | * @default 400 18 | */ 19 | threshold?: number; 20 | /** 21 | * If `event.persist()` should be called on react event 22 | * @default false 23 | */ 24 | captureEvent?: boolean; 25 | /** 26 | * Which type of events should be detected ('mouse' | 'touch' | 'pointer'). For TS use *LongPressEventType* enum. 27 | * @see LongPressEventType 28 | * @default LongPressEventType.Pointer 29 | */ 30 | detect?: LongPressEventType; 31 | /** 32 | * Function to filter incoming events. Function should return `false` for events that will be ignored (e.g. right mouse clicks) 33 | * @param {Object} event React event coming from handlers 34 | * @see LongPressReactEvents 35 | */ 36 | filterEvents?: (event: LongPressReactEvents) => boolean; 37 | /** 38 | * If long press should be canceled on mouse / touch / pointer move. Possible values: 39 | * - `false`: [default] Disable cancelling on movement 40 | * - `true`: Enable cancelling on movement and use default 25px threshold 41 | * - `number`: Set a specific tolerance value in pixels (square side size inside which movement won't cancel long press) 42 | * @default false 43 | */ 44 | cancelOnMovement?: boolean | number; 45 | /** 46 | * If long press should be canceled when moving mouse / touch / pointer outside the element to which it was bound. 47 | * 48 | * Works for mouse and pointer events, touch events will be supported in the future. 49 | * @default true 50 | */ 51 | cancelOutsideElement?: boolean; 52 | } 53 | 54 | export const LongPressButton: FC = ({ 55 | context, 56 | children, 57 | cancelOnMovement, 58 | threshold, 59 | ...options 60 | }) => { 61 | const { enqueueSnackbar } = useSnackbar(); 62 | const timer = useRef(Date.now()); 63 | const moveThrottle = useRef(Date.now()); 64 | const handlers = useLongPress( 65 | () => { 66 | enqueueSnackbar(`Long pressed after ${threshold} ms`, { variant: 'success' }); 67 | }, 68 | { 69 | ...options, 70 | threshold, 71 | cancelOnMovement, 72 | onStart: (event, meta) => { 73 | timer.current = Date.now(); 74 | enqueueSnackbar(`'onStart' callback called with: ${JSON.stringify(meta)}`, { variant: 'info' }); 75 | }, 76 | onMove: cancelOnMovement 77 | ? (event, meta) => { 78 | if (Date.now() - moveThrottle.current >= 700) { 79 | enqueueSnackbar(`'onMove' callback called with: ${JSON.stringify(meta)}`); 80 | moveThrottle.current = Date.now(); 81 | } 82 | } 83 | : undefined, 84 | onCancel: (event, meta) => { 85 | enqueueSnackbar( 86 | `'onCancel' callback called after ${Date.now() - timer.current}ms with: ${JSON.stringify(meta)}`, 87 | { variant: 'error' } 88 | ); 89 | }, 90 | onFinish: (event, meta) => { 91 | enqueueSnackbar( 92 | `'onFinish' callback called after ${Date.now() - timer.current}ms with: ${JSON.stringify(meta)}`, 93 | { variant: 'info' } 94 | ); 95 | }, 96 | } 97 | ); 98 | 99 | return ( 100 | 103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /packages/react-interval-hook/src/lib/react-interval-hook.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import { IntervalHookCallback, IntervalHookOptions, IntervalHookResult } from './react-interval-hook.types'; 3 | 4 | export function useInterval( 5 | callback: IntervalHookCallback, 6 | interval = 1000, 7 | { 8 | // eslint-disable-next-line @typescript-eslint/no-empty-function 9 | onFinish = (): void => {}, 10 | autoStart = true, 11 | immediate = false, 12 | selfCorrecting = true, 13 | }: IntervalHookOptions = {} 14 | ): IntervalHookResult { 15 | const timer = useRef(); 16 | const active = useRef(false); 17 | const expected = useRef(null); 18 | const savedCallback = useRef(callback); 19 | 20 | const tick = useCallback(() => { 21 | /* istanbul ignore next */ 22 | const expectedTimestamp = expected.current || 0; 23 | 24 | if (selfCorrecting) { 25 | // If timer has more delay than it's interval 26 | const delay = Date.now() - expectedTimestamp; 27 | // How many intervals passed since the last call 28 | const ticks = 1 + (delay > 0 ? Math.floor(delay / interval) : 0); 29 | // Set new timeout 30 | expected.current = expectedTimestamp + interval * ticks; 31 | // Save timeout id 32 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 33 | set(Math.max(interval - delay, 1)); 34 | // Call callback function with amount of ticks passed 35 | savedCallback.current(ticks); 36 | } else { 37 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 38 | set(interval); 39 | // Without self correction ticks are undefined (or equivalently equal to 1) 40 | savedCallback.current(); 41 | } 42 | 43 | // eslint-disable-next-line react-hooks/exhaustive-deps 44 | }, [interval]); 45 | 46 | const set = useCallback( 47 | (ms: number) => { 48 | if (timer.current !== undefined) { 49 | clearTimeout(timer.current); 50 | } 51 | // Failsafe: Set new timeout only if timer is active 52 | 53 | if (active.current) { 54 | timer.current = setTimeout(tick, ms); 55 | /* c8 ignore start */ 56 | } else { 57 | // eslint-disable-next-line no-console 58 | console.debug( 59 | 'Trying to set interval timeout on inactive timer, this is no-op and probably indicates bug in your code.' 60 | ); 61 | } 62 | /* c8 ignore stop */ 63 | }, 64 | [tick, active] 65 | ); 66 | 67 | const start = useCallback(() => { 68 | // Save current active value 69 | const isActive = active.current; 70 | // Switch to active 71 | active.current = true; 72 | 73 | if (expected.current === null) { 74 | expected.current = Date.now() + interval; 75 | } 76 | 77 | if (immediate && !isActive) { 78 | expected.current -= interval; 79 | tick(); 80 | } 81 | 82 | set(interval); 83 | }, [tick, interval, immediate, set]); 84 | 85 | const stop = useCallback( 86 | (triggerFinish = true) => { 87 | // Save current active value 88 | const isActive = active.current; 89 | 90 | if (timer.current !== undefined) { 91 | clearTimeout(timer.current); 92 | } 93 | 94 | active.current = false; 95 | timer.current = undefined; 96 | expected.current = null; 97 | if (isActive && triggerFinish) { 98 | onFinish(); 99 | } 100 | }, 101 | [onFinish] 102 | ); 103 | 104 | const isActive = useCallback(() => active.current, []); 105 | 106 | useEffect(() => { 107 | savedCallback.current = callback; 108 | }, [callback]); 109 | 110 | useEffect(() => { 111 | autoStart && start(); 112 | 113 | return stop; 114 | // eslint-disable-next-line react-hooks/exhaustive-deps 115 | }, []); 116 | 117 | return { start, stop, isActive }; 118 | } 119 | -------------------------------------------------------------------------------- /tools/scripts/release/cli.ts: -------------------------------------------------------------------------------- 1 | import * as yargs from 'yargs'; 2 | import { parseCSV } from 'nx/src/command-line/yargs-utils/shared-options'; 3 | import chalk from 'chalk'; 4 | import { printHeader } from '../utils/output'; 5 | import { execaSync } from 'execa'; 6 | import { ReleaseChannel, releaseChannelPreid } from './release.consts'; 7 | import { ReleasePreidValue } from './release.types'; 8 | import { ParsedOptions } from './cli.types'; 9 | import { isCI as isNxCI } from 'nx/src/utils/is-ci'; 10 | 11 | export function parseReleaseCliOptions() { 12 | return yargs 13 | .version(false) // don't use the default meaning of version in yargs 14 | .option('channel', { 15 | alias: 'c', 16 | description: 'Explicit channel specifier to use when not deploying based on branch', 17 | type: 'string', 18 | choices: Object.values(ReleaseChannel), 19 | }) 20 | .option('dryRun', { 21 | alias: 'd', 22 | description: 'Whether or not to perform a dry-run of the release process, defaults to true', 23 | type: 'boolean', 24 | default: false, 25 | }) 26 | .option('verbose', { 27 | description: 'Whether or not to enable verbose logging, defaults to false', 28 | type: 'boolean', 29 | default: false, 30 | }) 31 | .options('publishOnly', { 32 | description: 'Whether or not to only execute publishing step', 33 | type: 'boolean', 34 | default: false, 35 | }) 36 | .options('skipPublish', { 37 | description: 'Whether or not to skip publishing step', 38 | type: 'boolean', 39 | default: false, 40 | }) 41 | .option('otp', { 42 | description: 'One time password for publishing', 43 | type: 'number', 44 | }) 45 | .option('projects', { 46 | type: 'string', 47 | alias: 'p', 48 | coerce: parseCSV, 49 | describe: 'Projects to run. (comma/space delimited project names and/or patterns)', 50 | }) 51 | .option('ci', { 52 | type: 'boolean', 53 | describe: 'Whether or not to run in CI context.', 54 | demandOption: false, 55 | }) 56 | .option('githubRelease', { 57 | type: 'boolean', 58 | alias: 'ghr', 59 | default: false, 60 | describe: 'Whether or not to create Github release (doing so will require pushing changes)', 61 | }) 62 | .parseAsync(); 63 | } 64 | 65 | export function parseReleaseOptions({ 66 | channel, 67 | dryRun, 68 | ci, 69 | verbose, 70 | }: Awaited>): ParsedOptions { 71 | let isPrerelease: boolean; 72 | let preid: ReleasePreidValue; 73 | let tag: ReleaseChannel; 74 | 75 | let selectedChannel: ReleaseChannel; 76 | 77 | console.log(printHeader('channel', 'blueBright'), `Detecting release channel...\n`); 78 | 79 | if (!channel) { 80 | // Auto determine based on branch 81 | const { stdout: branch } = execaSync('git', ['branch', '--show-current']); 82 | console.log(`🚫 No channel specified, using current git branch ${chalk.blueBright(branch)}`); 83 | selectedChannel = branch === 'main' ? ReleaseChannel.Latest : ReleaseChannel.Preview; 84 | } else { 85 | selectedChannel = channel; 86 | } 87 | 88 | console.log(`🔀 ${chalk.yellow(selectedChannel)} channel selected for release\n`); 89 | 90 | switch (selectedChannel) { 91 | case ReleaseChannel.Latest: 92 | isPrerelease = false; 93 | break; 94 | case ReleaseChannel.Preview: 95 | default: 96 | isPrerelease = true; 97 | break; 98 | } 99 | 100 | preid = releaseChannelPreid[selectedChannel]; 101 | tag = selectedChannel; 102 | 103 | const releaseEnabled = process.env.RELEASE_ENABLED === '1' || process.env.RELEASE_ENABLED === 'true'; 104 | 105 | console.info( 106 | printHeader('release', 'yellow'), 107 | `Live release enabled? ${releaseEnabled ? chalk.green('Yes') : chalk.red('No')} (RELEASE_ENABLED=${ 108 | process.env.RELEASE_ENABLED 109 | })\n` 110 | ); 111 | 112 | const isCI = ci ?? isNxCI(); 113 | 114 | return { 115 | isPrerelease, 116 | preid, 117 | tag, 118 | dryRun: dryRun || !releaseEnabled, 119 | verbose: isCI || verbose, 120 | isCI, 121 | }; 122 | } 123 | -------------------------------------------------------------------------------- /packages/use-long-press/src/tests/use-long-press.test.utils.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, TouchEvent as ReactTouchEvent } from 'react'; 2 | import { createEvent, fireEvent, FireObject } from '@testing-library/react'; 3 | import { EventType } from '@testing-library/dom/types/events'; 4 | import { LongPressDomEvents, LongPressEventType, LongPressHandlers } from '../lib'; 5 | import { 6 | LongPressTestHandler, 7 | LongPressTestHandlersMap, 8 | LongPressTestHandlerType, 9 | LongPressTestPositionedEventCreator, 10 | LongPressTestPositionedEventFactory, 11 | } from './use-long-press.test.types'; 12 | import { convertHandlerNameToEventName } from './use-long-press.test.functions'; 13 | import { longPressPositionedEventCreatorMap, longPressTestHandlerNamesMap, noop } from './use-long-press.test.consts'; 14 | 15 | export function createMockedDomEventFactory( 16 | eventType: LongPressEventType 17 | ): Record LongPressDomEvents> { 18 | return (['start', 'move', 'stop'] as const).reduce((result, handlerName) => { 19 | const eventName = convertHandlerNameToEventName(longPressTestHandlerNamesMap[eventType][handlerName]) as EventType; 20 | result[handlerName] = (options?: EventInit) => createEvent[eventName](window, options) as LongPressDomEvents; 21 | return result; 22 | }, {} as Record LongPressDomEvents>); 23 | } 24 | 25 | export function createPositionedDomEventFactory( 26 | eventType: LongPressEventType, 27 | element: Element 28 | ): LongPressTestPositionedEventFactory { 29 | return (['start', 'move', 'stop'] as LongPressTestHandlerType[]).reduce((result, handlerType) => { 30 | const handlerName = longPressTestHandlerNamesMap[eventType][handlerType]; 31 | const eventName = convertHandlerNameToEventName(handlerName) as EventType; 32 | const creator = longPressPositionedEventCreatorMap[eventType]; 33 | 34 | result[handlerType] = ((x: number, y: number) => 35 | creator(element, eventName, x, y)) as LongPressTestPositionedEventCreator; 36 | 37 | return result; 38 | }, {} as LongPressTestPositionedEventFactory); 39 | } 40 | /* 41 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 42 | ⎹ Test handlers 43 | ⌞____________________________________________________________________________________________________ 44 | */ 45 | export function getTestHandlersMap( 46 | eventType: LongPressEventType, 47 | handlers: LongPressHandlers 48 | ): LongPressTestHandlersMap { 49 | const handlerNames = longPressTestHandlerNamesMap[eventType]; 50 | 51 | return Object.fromEntries( 52 | Object.keys(handlerNames).map((type) => { 53 | const handlerName = handlerNames[type as LongPressTestHandlerType]; 54 | const handler = handlers[handlerName as keyof LongPressHandlers] as 55 | | ((event: ReactMouseEvent | ReactTouchEvent | ReactPointerEvent) => void) 56 | | undefined; 57 | 58 | return [type, handler ?? noop]; 59 | }) 60 | ) as Record; 61 | } 62 | 63 | export function getDOMTestHandlersMap( 64 | eventType: LongPressEventType, 65 | element: Window | Element | Node | Document 66 | ): LongPressTestHandlersMap { 67 | function eventHandler(eventName: keyof FireObject) { 68 | return (options?: object) => fireEvent[eventName](element, options); 69 | } 70 | 71 | switch (eventType) { 72 | case LongPressEventType.Mouse: 73 | return { 74 | start: eventHandler('mouseDown'), 75 | move: eventHandler('mouseMove'), 76 | stop: eventHandler('mouseUp'), 77 | leave: eventHandler('mouseLeave'), 78 | }; 79 | case LongPressEventType.Touch: 80 | return { 81 | start: eventHandler('touchStart'), 82 | move: eventHandler('touchMove'), 83 | stop: eventHandler('touchEnd'), 84 | }; 85 | case LongPressEventType.Pointer: { 86 | return { 87 | start: eventHandler('pointerDown'), 88 | move: eventHandler('pointerMove'), 89 | stop: eventHandler('pointerUp'), 90 | leave: eventHandler('pointerLeave'), 91 | }; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tools/scripts/release/version.ts: -------------------------------------------------------------------------------- 1 | import { VersionData } from 'nx/src/command-line/release/utils/shared'; 2 | import { colorProjectName, printHeader } from '../utils/output'; 3 | import { diff, parse, prerelease, ReleaseType } from 'semver'; 4 | import chalk from 'chalk'; 5 | import { releaseVersion } from 'nx/release'; 6 | import { VersionOptions as NxVersionOptions } from 'nx/src/command-line/release/command-object'; 7 | import { HandleVersionOptions } from './version.types'; 8 | 9 | export async function handleVersion({ 10 | options, 11 | projectName, 12 | suggestedVersionData, 13 | isPrerelease, 14 | }: HandleVersionOptions): Promise { 15 | // If suggested version was modified 16 | let modified = false; 17 | let { newVersion, currentVersion } = suggestedVersionData; 18 | 19 | console.log(printHeader('version', 'cyan'), 'Detecting version changes\n'); 20 | 21 | // If no change, skip 22 | if (newVersion === currentVersion) { 23 | console.log(colorProjectName(projectName), 'Current version same as the new version, skipping'); 24 | return null; 25 | } 26 | 27 | // If new version was detected as prerelease, but it shouldn't correct it to proper regular version 28 | if (newVersion !== null && isPrereleaseVersion(newVersion) && !isPrerelease) { 29 | const regularVersion = getRegularVersionFromPrerelease(newVersion); 30 | 31 | console.log( 32 | colorProjectName(projectName), 33 | 'Detected version change from', 34 | chalk.red(currentVersion), 35 | 'to', 36 | chalk.yellow(newVersion), 37 | 'but instead correcting to', 38 | chalk.green(regularVersion) 39 | ); 40 | 41 | newVersion = regularVersion; 42 | modified = true; 43 | } 44 | 45 | // If no new version was detected but current is prerelease and want to release regular then promote it 46 | if (newVersion === null && !isPrerelease && isPrereleaseVersion(currentVersion)) { 47 | const regularVersion = getRegularVersionFromPrerelease(currentVersion); 48 | 49 | console.log( 50 | colorProjectName(projectName), 51 | 'Promoting prerelease version', 52 | chalk.yellow(currentVersion), 53 | 'to', 54 | chalk.green(regularVersion) 55 | ); 56 | 57 | newVersion = regularVersion; 58 | modified = true; 59 | } 60 | 61 | // If no modifications were made just log what will be released 62 | if (!modified) { 63 | console.log( 64 | colorProjectName(projectName), 65 | 'Detected version change from', 66 | chalk.red(currentVersion), 67 | 'to', 68 | chalk.green(newVersion) 69 | ); 70 | } 71 | 72 | // Calculate precise specifier if modified 73 | const specifier: ReleaseType | undefined = 74 | modified && newVersion ? diff(newVersion, currentVersion) ?? undefined : undefined; 75 | 76 | // Version using NX version script 77 | const { projectsVersionData } = await releaseVersion({ 78 | ...options, 79 | specifier, 80 | projects: [projectName], 81 | }); 82 | 83 | return projectsVersionData; 84 | } 85 | 86 | export async function getSuggestedProjectsVersionData(options: NxVersionOptions): Promise { 87 | const { projectsVersionData } = await releaseVersion({ 88 | ...options, 89 | gitCommit: false, 90 | gitTag: false, 91 | dryRun: true, 92 | verbose: false, 93 | stageChanges: false, 94 | }); 95 | 96 | return projectsVersionData; 97 | } 98 | 99 | export function isPrereleaseVersion(version: string): boolean { 100 | const prereleaseComponents = prerelease(version); 101 | return (prereleaseComponents?.length ?? 0) > 0; 102 | } 103 | 104 | export function getRegularVersionFromPrerelease(prereleaseVersion: string): string { 105 | if (!isPrereleaseVersion(prereleaseVersion)) { 106 | throw new Error( 107 | `Trying to get regular version from prerelease version, but provided version is not prerelease: ${chalk.red( 108 | prereleaseVersion 109 | )}` 110 | ); 111 | } 112 | 113 | const data = parse(prereleaseVersion); 114 | 115 | if (data === null) { 116 | throw new Error(`Cannot parse prerelease version ${prereleaseVersion}`); 117 | } 118 | 119 | return `${data.major}.${data.minor}.${data.patch}`; 120 | } 121 | -------------------------------------------------------------------------------- /tools/scripts/release/git.ts: -------------------------------------------------------------------------------- 1 | import { execCommand } from 'nx/src/command-line/release/utils/exec-command'; 2 | import { interpolate } from 'nx/src/tasks-runner/utils'; 3 | import { readNxJson } from 'nx/src/config/nx-json'; 4 | import { isPrereleaseVersion } from './version'; 5 | 6 | function escapeRegExp(str: string): string { 7 | return str.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); 8 | } 9 | 10 | // https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string 11 | const SEMVER_REGEX = 12 | /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g; 13 | 14 | export async function getLatestGitTagVersionsForProject( 15 | projectName: string, 16 | excludeVersion?: string | null | string[] 17 | ): Promise<{ 18 | releaseVersion: string | null; 19 | releaseVersionTag: string | null; 20 | prereleaseVersion: string | null; 21 | prereleaseVersionTag: string | null; 22 | }> { 23 | const nxJson = readNxJson(); 24 | const releaseTagPattern = nxJson?.release?.releaseTagPattern ?? '{projectName}@{version}'; 25 | const excludedVersions: string[] = 26 | typeof excludeVersion === 'string' ? [excludeVersion] : Array.isArray(excludeVersion) ? excludeVersion : []; 27 | 28 | try { 29 | let tags: string[]; 30 | tags = await execCommand('git', ['tag', '--sort', '-v:refname', '--merged']).then((r) => 31 | r 32 | .trim() 33 | .split('\n') 34 | .map((t) => t.trim()) 35 | .filter(Boolean) 36 | ); 37 | if (!tags.length) { 38 | // try again, but include all tags on the repo instead of just --merged ones 39 | tags = await execCommand('git', ['tag', '--sort', '-v:refname']).then((r) => 40 | r 41 | .trim() 42 | .split('\n') 43 | .map((t) => t.trim()) 44 | .filter(Boolean) 45 | ); 46 | } 47 | 48 | // If no tags matched both version will be null 49 | if (!tags.length) { 50 | return { releaseVersion: null, releaseVersionTag: null, prereleaseVersion: null, prereleaseVersionTag: null }; 51 | } 52 | 53 | const interpolatedTagPattern = interpolate(releaseTagPattern, { 54 | version: '%v%', 55 | projectName, 56 | }); 57 | 58 | const tagRegexp = `^${escapeRegExp(interpolatedTagPattern).replace('%v%', '(.+)').replace('%p%', '(.+)')}`; 59 | 60 | const matchingSemverTags = tags.filter( 61 | (tag) => 62 | // Do the match against SEMVER_REGEX to ensure that we skip tags that aren't valid semver versions 63 | !!tag.match(tagRegexp) && tag.match(tagRegexp)?.some((r) => r.match(SEMVER_REGEX)) 64 | ); 65 | 66 | // If no tags matched both version will be null 67 | if (!matchingSemverTags.length) { 68 | return { releaseVersion: null, releaseVersionTag: null, prereleaseVersion: null, prereleaseVersionTag: null }; 69 | } 70 | 71 | let releaseVersion: string | null = null; 72 | let releaseVersionTag: string | null = null; 73 | let prereleaseVersion: string | null = null; 74 | let prereleaseVersionTag: string | null = null; 75 | 76 | for (const semverTag of matchingSemverTags) { 77 | const match = semverTag.match(tagRegexp); 78 | if (!match) { 79 | continue; 80 | } 81 | const [tag, ...rest] = match; 82 | const version = rest.filter((r) => { 83 | return r.match(SEMVER_REGEX); 84 | })[0]; 85 | 86 | // If this version is excluded then skip to the next one 87 | if (excludedVersions.includes(version)) { 88 | continue; 89 | } 90 | 91 | if (isPrereleaseVersion(version)) { 92 | if (prereleaseVersion === null) { 93 | prereleaseVersion = version; 94 | prereleaseVersionTag = tag; 95 | } 96 | } else { 97 | if (releaseVersion === null) { 98 | releaseVersion = version; 99 | releaseVersionTag = tag; 100 | } 101 | } 102 | if (prereleaseVersion !== null && releaseVersion !== null) { 103 | break; 104 | } 105 | } 106 | 107 | return { 108 | releaseVersion: releaseVersion, 109 | releaseVersionTag: releaseVersionTag, 110 | prereleaseVersion: prereleaseVersion, 111 | prereleaseVersionTag: prereleaseVersionTag, 112 | }; 113 | } catch { 114 | return { releaseVersion: null, releaseVersionTag: null, prereleaseVersion: null, prereleaseVersionTag: null }; 115 | } 116 | } 117 | 118 | export async function hasGitChanges(): Promise { 119 | const changesAmount = await execCommand('git', ['status', '--porcelain']).then((result) => { 120 | return result.split('\n').filter((line) => line.trim().length > 0).length; 121 | }); 122 | 123 | return changesAmount > 0; 124 | } 125 | 126 | export async function getGitTail(): Promise { 127 | return await execCommand('git', ['rev-list', '--max-parents=0', 'HEAD']); 128 | } 129 | -------------------------------------------------------------------------------- /packages/react-interval-hook/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.5 (2025-03-16) 2 | 3 | ### 🚀 Features 4 | 5 | - **storybook-host:** Add Vercel Analytics in form of Storybook addon ([1103f48](https://github.com/minwork/react/commit/1103f48)) 6 | - **storybook-host:** Create storybook lib and add stories for Long Press Hook ([b206ad4](https://github.com/minwork/react/commit/b206ad4)) 7 | 8 | ### 🩹 Fixes 9 | 10 | - **project:** Fix running test coverage tasks ([394fdde](https://github.com/minwork/react/commit/394fdde)) 11 | 12 | ### 📖 Documentation 13 | 14 | - **project:** Update links for Long press hook section ([f59e8b8](https://github.com/minwork/react/commit/f59e8b8)) 15 | - **react-interval-hook:** Add basic usage example to README ([21cdd85](https://github.com/minwork/react/commit/21cdd85)) 16 | - **README:** Restore bundle size badge for all libraries ([cc40996](https://github.com/minwork/react/commit/cc40996)) 17 | - **README:** Refactor the README for clearer formatting ([59c185a](https://github.com/minwork/react/commit/59c185a)) 18 | - **README:** Refactor main README file ([7a5e045](https://github.com/minwork/react/commit/7a5e045)) 19 | - **react-interval-hook:** Replace package README image and unify badges ([2c2af61](https://github.com/minwork/react/commit/2c2af61)) 20 | - **README:** Add what's new section ([826bd51](https://github.com/minwork/react/commit/826bd51)) 21 | - **README:** Update react-interval-hook description ([893cd05](https://github.com/minwork/react/commit/893cd05)) 22 | 23 | ### 📦 Build 24 | 25 | - **project:** Resolve packages resolutions validation errors ([ae96d2d](https://github.com/minwork/react/commit/ae96d2d)) 26 | - **deps:** bump rollup from 2.79.1 to 2.79.2 ([#45](https://github.com/minwork/react/pull/45)) 27 | - **pr:** Update node versions matrix ([a3e3dc7](https://github.com/minwork/react/commit/a3e3dc7)) 28 | - **pr:** Enable Corepack to use the newest Yarn version ([9f31d44](https://github.com/minwork/react/commit/9f31d44)) 29 | - **pr:** Update PR check node and action versions ([233ef73](https://github.com/minwork/react/commit/233ef73)) 30 | - **deps:** bump cross-spawn from 7.0.3 to 7.0.6 ([3b7ce9b](https://github.com/minwork/react/commit/3b7ce9b)) 31 | 32 | ### ❤️ Thank You 33 | 34 | - minwork @minwork 35 | 36 | ## 1.1.5-preview.1 (2025-03-01) 37 | 38 | ### 🚀 Features 39 | 40 | - **storybook-host:** Add Vercel Analytics in form of Storybook addon 41 | 42 | ### 📖 Documentation 43 | 44 | - **project:** Update links for Long press hook section 45 | - **react-interval-hook:** Add basic usage example to README 46 | 47 | ### 📦 Build 48 | 49 | - **project:** Resolve packages resolutions validation errors 50 | - **deps:** bump rollup from 2.79.1 to 2.79.2 51 | - **pr:** Update node versions matrix 52 | - **pr:** Enable Corepack to use the newest Yarn version 53 | - **pr:** Update PR check node and action versions 54 | - **deps:** bump cross-spawn from 7.0.3 to 7.0.6 55 | 56 | ### ❤️ Thank You 57 | 58 | - minwork 59 | 60 | ## 1.1.5-preview.0 (2024-09-06) 61 | 62 | 63 | ### 🚀 Features 64 | 65 | - **storybook-host:** Create storybook lib and add stories for Long Press Hook 66 | 67 | 68 | ### 🩹 Fixes 69 | 70 | - **project:** Fix running test coverage tasks 71 | 72 | 73 | ### 📖 Documentation 74 | 75 | - **README:** Update react-interval-hook description 76 | 77 | - **README:** Add what's new section 78 | 79 | - **react-interval-hook:** Replace package README image and unify badges 80 | 81 | - **README:** Refactor main README file 82 | 83 | - **README:** Refactor the README for clearer formatting 84 | 85 | - **README:** Restore bundle size badge for all libraries 86 | 87 | 88 | ### ❤️ Thank You 89 | 90 | - minwork 91 | 92 | ## [1.1.4](https://github.com/minwork/react/compare/react-interval-hook-v1.1.3...react-interval-hook-v1.1.4) (2023-08-30) 93 | 94 | 95 | ### Documentation 96 | 97 | * **react-interval-hook:** Move documentation to GitBook ([30e693c](https://github.com/minwork/react/commit/30e693c11c5d5b0dd3e59bdba612193e68129572)) 98 | 99 | 100 | ### Refactors 101 | 102 | * **react-interval-hook:** Update links and keywords in package.json ([410bc83](https://github.com/minwork/react/commit/410bc83cbdad0e2e6f2a7fe84f273157fa065a2b)) 103 | 104 | 105 | ### Build config 106 | 107 | * **react-interval-hook:** Copy *.md files to build output ([30a6a77](https://github.com/minwork/react/commit/30a6a77698b972ef84bbace87c6d223e62f2b759)) 108 | 109 | ## [1.1.4-alpha.2](https://github.com/minwork/react/compare/react-interval-hook-v1.1.4-alpha.1...react-interval-hook-v1.1.4-alpha.2) (2023-08-30) 110 | 111 | 112 | ### Build config 113 | 114 | * **react-interval-hook:** Copy *.md files to build output ([30a6a77](https://github.com/minwork/react/commit/30a6a77698b972ef84bbace87c6d223e62f2b759)) 115 | 116 | ## [1.1.4-alpha.1](https://github.com/minwork/react/compare/react-interval-hook-v1.1.3...react-interval-hook-v1.1.4-alpha.1) (2023-08-29) 117 | 118 | 119 | ### Documentation 120 | 121 | * **react-interval-hook:** Move documentation to GitBook ([30e693c](https://github.com/minwork/react/commit/30e693c11c5d5b0dd3e59bdba612193e68129572)) 122 | 123 | 124 | ### Refactors 125 | 126 | * **react-interval-hook:** Update links and keywords in package.json ([410bc83](https://github.com/minwork/react/commit/410bc83cbdad0e2e6f2a7fe84f273157fa065a2b)) 127 | -------------------------------------------------------------------------------- /tools/scripts/release/release.ts: -------------------------------------------------------------------------------- 1 | import { releasePublish } from 'nx/release'; 2 | import * as process from 'node:process'; 3 | import chalk from 'chalk'; 4 | import { createProjectGraphAsync } from 'nx/src/project-graph/project-graph'; 5 | import { colorProjectName, printHeader, suppressOutput } from '../utils/output'; 6 | import { syncPackageJson } from './publish'; 7 | import { parseReleaseCliOptions, parseReleaseOptions } from './cli'; 8 | import { getSuggestedProjectsVersionData, handleVersion } from './version'; 9 | import { hasGitChanges } from './git'; 10 | import { VersionOptions } from 'nx/src/command-line/release/command-object'; 11 | import { handleChangelog } from './changelog'; 12 | 13 | (async () => { 14 | const options = await parseReleaseCliOptions(); 15 | const graph = await createProjectGraphAsync({ exitOnError: true }); 16 | const { publishOnly, projects, otp, skipPublish, githubRelease } = options; 17 | const { tag, preid, isPrerelease, dryRun, isCI, verbose } = parseReleaseOptions(options); 18 | 19 | const projectsList = new Set(projects ?? []); 20 | 21 | // If using verbose option, dump calculated options as pseudo-JSON 22 | if (verbose) { 23 | console.group(printHeader('Options', 'gray'), 'Provided and calculated options'); 24 | console.log('{'); 25 | Object.entries({ projects, verbose, publishOnly, skipPublish, tag, preid, isPrerelease, dryRun }).forEach( 26 | ([key, value]) => { 27 | console.log(` ${chalk.whiteBright(key)}: ${chalk.cyan(value)}`); 28 | } 29 | ); 30 | console.log('}'); 31 | console.groupEnd(); 32 | console.log('\n'); 33 | } 34 | 35 | if (!dryRun && (await hasGitChanges())) { 36 | console.warn(printHeader('git', 'redBright'), '🚨 Detected uncommitted git changes, aborting! 🚨'); 37 | console.log(chalk.grey('Commit your changes first, then run release as it will create new commits')); 38 | process.exit(1); 39 | } 40 | 41 | if (publishOnly) { 42 | // Create new version and update changelog if not only publishing 43 | console.log(printHeader('mode', 'cyan'), `Publish only, skipping version and changelog\n`); 44 | } else { 45 | console.log('Calculating changed projects...\n'); 46 | // Start by obtaining all projects and their suggested release version 47 | const versionOptions: VersionOptions = { 48 | preid, 49 | projects, 50 | dryRun, 51 | verbose, 52 | }; 53 | const suggestedProjectsVersionData = await suppressOutput(() => getSuggestedProjectsVersionData(versionOptions)); 54 | 55 | console.log( 56 | `Finished calculating proposed changes for ${Object.keys(suggestedProjectsVersionData) 57 | .map(colorProjectName) 58 | .join(', ')}.` 59 | ); 60 | console.log('Proceeding with release...\n'); 61 | 62 | // Iterate through changed projects and release them one by one 63 | for (const projectName of Object.keys(suggestedProjectsVersionData)) { 64 | const suggestedVersionData = suggestedProjectsVersionData[projectName]; 65 | 66 | const versionData = await handleVersion({ 67 | projectName, 68 | suggestedVersionData, 69 | isPrerelease, 70 | options: { 71 | preid, 72 | dryRun, 73 | verbose, 74 | }, 75 | }); 76 | 77 | // If version changed 78 | if (versionData) { 79 | await handleChangelog({ 80 | projectName, 81 | isPrerelease, 82 | options: { 83 | versionData, 84 | dryRun, 85 | verbose, 86 | createRelease: githubRelease ? 'github' : undefined, 87 | gitPush: githubRelease || isCI ? true : undefined, 88 | }, 89 | }); 90 | 91 | // Add this project to projects list that will be published 92 | if (versionData[projectName].newVersion !== null) { 93 | projectsList.add(projectName); 94 | } else { 95 | console.log( 96 | printHeader('Publish', 'cyanBright'), 97 | `Skipped publishing ${projectName} as ${chalk.whiteBright('newVersion')} is ${chalk.grey('null')}` 98 | ); 99 | } 100 | } else { 101 | console.log( 102 | printHeader('Changelog', 'yellow'), 103 | `Skipped generating changelog as ${chalk.whiteBright('versionData')} is ${chalk.grey('null')}` 104 | ); 105 | } 106 | } 107 | } 108 | 109 | console.log('\n'); 110 | 111 | if (skipPublish) { 112 | console.log(printHeader('mode', 'cyan'), `Skip publish, version and changelog only\n`); 113 | return process.exit(0); 114 | } else { 115 | if (projectsList.size === 0) { 116 | console.log( 117 | printHeader('Publish', 'cyan'), 118 | `⏭️ Trying to publish but no projects were specified, skipping publish step altogether` 119 | ); 120 | return process.exit(0); 121 | } 122 | 123 | const projectsListArray = Array.from(projectsList.values()); 124 | // Sync package.json files before release 125 | syncPackageJson(projectsListArray, graph); 126 | 127 | // The returned number value from releasePublish will be zero if all projects are published successfully, non-zero if not 128 | const publishProjectsResult = await releasePublish({ 129 | dryRun, 130 | verbose, 131 | projects: projectsListArray, 132 | tag, 133 | registry: 'https://registry.npmjs.org/', 134 | otp, 135 | }); 136 | 137 | const publishStatus = Object.values(publishProjectsResult).reduce((sum, { code }) => sum + code, 0); 138 | 139 | return process.exit(publishStatus); 140 | } 141 | })(); 142 | -------------------------------------------------------------------------------- /packages/use-long-press/src/lib/use-long-press.types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MouseEvent as ReactMouseEvent, 3 | MouseEventHandler, 4 | PointerEvent as ReactPointerEvent, 5 | PointerEventHandler, 6 | TouchEvent as ReactTouchEvent, 7 | TouchEventHandler, 8 | } from 'react'; 9 | 10 | /* 11 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 12 | ⎹ Enums 13 | ⌞____________________________________________________________________________________________________ 14 | */ 15 | 16 | /** 17 | * Which event listeners should be returned from the hook 18 | */ 19 | export enum LongPressEventType { 20 | Mouse = 'mouse', 21 | Touch = 'touch', 22 | Pointer = 'pointer', 23 | } 24 | 25 | /** 26 | * What was the reason behind calling specific callback 27 | * For now it applies only to 'onCancel' which receives cancellation reason 28 | * 29 | * @see LongPressCallbackMeta 30 | */ 31 | export enum LongPressCallbackReason { 32 | /** 33 | * Returned when mouse / touch / pointer was moved outside initial press area when `cancelOnMovement` is active 34 | */ 35 | CancelledByMovement = 'cancelled-by-movement', 36 | /** 37 | * Returned when click / tap / point was released before long press detection time threshold 38 | */ 39 | CancelledByRelease = 'cancelled-by-release', 40 | /** 41 | * Returned when mouse / touch / pointer was moved outside element and _cancelOutsideElement_ option was set to `true` 42 | */ 43 | CancelledOutsideElement = 'cancelled-outside-element', 44 | } 45 | 46 | /* 47 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 48 | ⎹ Long press callback 49 | ⌞____________________________________________________________________________________________________ 50 | */ 51 | 52 | /** 53 | * Function to call when long press event is detected 54 | * 55 | * @callback useLongPress~callback 56 | * @param {Object} event React mouse, touch or pointer event (depends on *detect* param) 57 | * @param {Object} meta Object containing *context* and / or *reason* (if applicable) 58 | */ 59 | export type LongPressCallback = ( 60 | event: LongPressReactEvents, 61 | meta: LongPressCallbackMeta 62 | ) => void; 63 | 64 | export type LongPressDomEvents = MouseEvent | TouchEvent | PointerEvent; 65 | export type LongPressReactEvents = 66 | | ReactMouseEvent 67 | | ReactTouchEvent 68 | | ReactPointerEvent; 69 | export type LongPressCallbackMeta = { context?: Context; reason?: LongPressCallbackReason }; 70 | 71 | /* 72 | ⌜‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ 73 | ⎹ Hook function 74 | ⌞____________________________________________________________________________________________________ 75 | */ 76 | 77 | export interface LongPressOptions< 78 | Target extends Element = Element, 79 | Context = unknown, 80 | EventType extends LongPressEventType = LongPressEventType 81 | > { 82 | /** 83 | * Period of time that must elapse after detecting click or tap in order to trigger _callback_ 84 | * @default 400 85 | */ 86 | threshold?: number; 87 | /** 88 | * If `event.persist()` should be called on react event 89 | * @default false 90 | */ 91 | captureEvent?: boolean; 92 | /** 93 | * Which type of events should be detected ('mouse' | 'touch' | 'pointer'). For TS use *LongPressEventType* enum. 94 | * @see LongPressEventType 95 | * @default LongPressEventType.Pointer 96 | */ 97 | detect?: EventType; 98 | /** 99 | * Function to filter incoming events. Function should return `false` for events that will be ignored (e.g. right mouse clicks) 100 | * @param {Object} event React event coming from handlers 101 | * @see LongPressReactEvents 102 | */ 103 | filterEvents?: (event: LongPressReactEvents) => boolean; 104 | /** 105 | * If long press should be canceled on mouse / touch / pointer move. Possible values: 106 | * - `false`: [default] Disable cancelling on movement 107 | * - `true`: Enable cancelling on movement and use default 25px threshold 108 | * - `number`: Set a specific tolerance value in pixels (square side size inside which movement won't cancel long press) 109 | * @default false 110 | */ 111 | cancelOnMovement?: boolean | number; 112 | /** 113 | * If long press should be canceled when moving mouse / touch / pointer outside the element to which it was bound. 114 | * 115 | * Works for mouse and pointer events, touch events will be supported in the future. 116 | * @default true 117 | */ 118 | cancelOutsideElement?: boolean; 119 | /** 120 | * Called after detecting initial click / tap / point event. Allows to change event position before registering it for the purpose of `cancelOnMovement`. 121 | */ 122 | onStart?: LongPressCallback; 123 | /** 124 | * Called on every move event. Allows to change event position before calculating distance for the purpose of `cancelOnMovement`. 125 | */ 126 | onMove?: LongPressCallback; 127 | /** 128 | * Called when releasing click / tap / point if long press **was** triggered. 129 | */ 130 | onFinish?: LongPressCallback; 131 | /** 132 | * Called when releasing click / tap / point if long press **was not** triggered 133 | */ 134 | onCancel?: LongPressCallback; 135 | } 136 | 137 | export type LongPressResult< 138 | T extends LongPressHandlers | LongPressEmptyHandlers, 139 | Context = unknown, 140 | Target extends Element = Element 141 | > = (context?: Context) => T; 142 | 143 | export type LongPressEmptyHandlers = Record; 144 | 145 | export interface LongPressMouseHandlers { 146 | onMouseDown: MouseEventHandler; 147 | onMouseMove: MouseEventHandler; 148 | onMouseUp: MouseEventHandler; 149 | onMouseLeave?: MouseEventHandler; 150 | } 151 | export interface LongPressTouchHandlers { 152 | onTouchStart: TouchEventHandler; 153 | onTouchMove: TouchEventHandler; 154 | onTouchEnd: TouchEventHandler; 155 | } 156 | 157 | export interface LongPressPointerHandlers { 158 | onPointerDown: PointerEventHandler; 159 | onPointerMove: PointerEventHandler; 160 | onPointerUp: PointerEventHandler; 161 | onPointerLeave?: PointerEventHandler; 162 | } 163 | 164 | export type LongPressHandlers = 165 | | LongPressMouseHandlers 166 | | LongPressTouchHandlers 167 | | LongPressPointerHandlers 168 | | LongPressEmptyHandlers; 169 | 170 | export type LongPressWindowListener = (event: LongPressDomEvents) => void; 171 | -------------------------------------------------------------------------------- /packages/use-double-tap/src/tests/use-double-tap.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expectMouseEvent, renderUseDoubleTap } from './use-double-tap.test.utils'; 3 | import { DoubleTapCallback } from '../lib'; 4 | import { createTestComponent, createTestElement } from './TestComponent'; 5 | import { fireEvent } from '@testing-library/react'; 6 | import { createMockedMouseEvent, noop } from '@react/shared/util-tests'; 7 | 8 | describe('Abstract hook usage', () => { 9 | test('Return expected result', () => { 10 | const { result } = renderUseDoubleTap(noop); 11 | 12 | expect(result.current).toEqual({ 13 | onClick: expect.any(Function), 14 | }); 15 | }); 16 | 17 | test('Accept two arguments', () => { 18 | const { result } = renderUseDoubleTap(noop, 1000); 19 | 20 | expect(result.current).toEqual({ 21 | onClick: expect.any(Function), 22 | }); 23 | }); 24 | 25 | test('Accept three arguments', () => { 26 | const { result } = renderUseDoubleTap(noop, 1000, { 27 | onSingleTap: noop, 28 | }); 29 | 30 | expect(result.current).toEqual({ 31 | onClick: expect.any(Function), 32 | }); 33 | }); 34 | 35 | test('Accept empty options argument', () => { 36 | const { result } = renderUseDoubleTap(noop, 1000, {}); 37 | 38 | expect(result.current).toEqual({ 39 | onClick: expect.any(Function), 40 | }); 41 | }); 42 | 43 | test('Null callback call return empty object', () => { 44 | const { result } = renderUseDoubleTap(null); 45 | 46 | expect(result.current).toEqual({}); 47 | 48 | const { result: result2 } = renderUseDoubleTap(null, 500); 49 | expect(result2.current).toEqual({}); 50 | 51 | const { result: result3 } = renderUseDoubleTap(null, 500, {}); 52 | expect(result3.current).toEqual({}); 53 | }); 54 | }); 55 | 56 | describe('Component usage', () => { 57 | let callback: DoubleTapCallback, mouseEvent: React.MouseEvent; 58 | beforeEach(() => { 59 | vi.useFakeTimers(); 60 | callback = vi.fn(); 61 | mouseEvent = createMockedMouseEvent(); 62 | }); 63 | 64 | afterEach(() => { 65 | vi.clearAllMocks(); 66 | vi.clearAllTimers(); 67 | }); 68 | 69 | test('Render component with initial tap count equal zero', () => { 70 | createTestComponent({ callback }); 71 | expect(callback).toBeCalledTimes(0); 72 | }); 73 | 74 | test('Detect double click and pass proper event to callback', () => { 75 | const element = createTestElement({ callback }); 76 | fireEvent.click(element, mouseEvent); 77 | fireEvent.click(element, mouseEvent); 78 | expect(callback).toBeCalledWith(expectMouseEvent); 79 | 80 | fireEvent.click(element, mouseEvent); 81 | fireEvent.click(element, mouseEvent); 82 | expect(callback).toBeCalledWith(expectMouseEvent); 83 | 84 | fireEvent.click(element, mouseEvent); 85 | fireEvent.click(element, mouseEvent); 86 | expect(callback).toBeCalledWith(expectMouseEvent); 87 | 88 | expect(callback).toBeCalledTimes(3); 89 | }); 90 | 91 | test('Trigger double tap only on clicks within threshold', () => { 92 | const element = createTestElement({ callback, threshold: 400 }); 93 | expect(callback).toBeCalledTimes(0); 94 | 95 | fireEvent.click(element, mouseEvent); 96 | // Wait 500ms 97 | vi.advanceTimersByTime(500); 98 | 99 | fireEvent.click(element, mouseEvent); 100 | expect(callback).toBeCalledTimes(0); 101 | 102 | fireEvent.click(element, mouseEvent); 103 | fireEvent.click(element, mouseEvent); 104 | expect(callback).toBeCalledTimes(1); 105 | }); 106 | 107 | test('Trigger double tap only on clicks within custom threshold', () => { 108 | const element = createTestElement({ callback, threshold: 400 }); 109 | 110 | expect(callback).toBeCalledTimes(0); 111 | 112 | fireEvent.click(element, mouseEvent); 113 | 114 | // Wait 500ms 115 | vi.advanceTimersByTime(500); 116 | 117 | fireEvent.click(element, mouseEvent); 118 | expect(callback).toBeCalledTimes(0); 119 | 120 | fireEvent.click(element, mouseEvent); 121 | fireEvent.click(element, mouseEvent); 122 | expect(callback).toBeCalledTimes(1); 123 | 124 | fireEvent.click(element, mouseEvent); 125 | 126 | // Wait 200ms 127 | vi.advanceTimersByTime(200); 128 | 129 | fireEvent.click(element, mouseEvent); 130 | expect(callback).toBeCalledTimes(2); 131 | }); 132 | 133 | test('Ignore double tap when callback is null', () => { 134 | const element = createTestElement({ callback: null }); 135 | 136 | fireEvent.click(element, mouseEvent); 137 | fireEvent.click(element, mouseEvent); 138 | 139 | fireEvent.click(element, mouseEvent); 140 | fireEvent.click(element, mouseEvent); 141 | 142 | fireEvent.click(element, mouseEvent); 143 | fireEvent.click(element, mouseEvent); 144 | 145 | expect(callback).toBeCalledTimes(0); 146 | }); 147 | 148 | test('Ignore double tap when callback is null and using custom threshold', () => { 149 | const element = createTestElement({ callback: null, threshold: 2000 }); 150 | 151 | fireEvent.click(element, mouseEvent); 152 | fireEvent.click(element, mouseEvent); 153 | 154 | fireEvent.click(element, mouseEvent); 155 | fireEvent.click(element, mouseEvent); 156 | 157 | fireEvent.click(element, mouseEvent); 158 | fireEvent.click(element, mouseEvent); 159 | 160 | expect(callback).toBeCalledTimes(0); 161 | }); 162 | 163 | test('Trigger custom callback', () => { 164 | const element = createTestElement({ callback }); 165 | 166 | expect(callback).toBeCalledTimes(0); 167 | 168 | fireEvent.click(element, mouseEvent); 169 | fireEvent.click(element, mouseEvent); 170 | 171 | expect(callback).toBeCalledTimes(1); 172 | 173 | fireEvent.click(element, mouseEvent); 174 | expect(callback).toBeCalledTimes(1); 175 | fireEvent.click(element, mouseEvent); 176 | expect(callback).toBeCalledTimes(2); 177 | }); 178 | 179 | test('Trigger custom callback when having custom threshold', () => { 180 | const element = createTestElement({ callback, threshold: 1500 }); 181 | 182 | expect(callback).toBeCalledTimes(0); 183 | 184 | fireEvent.click(element, mouseEvent); 185 | 186 | // Wait 100ms 187 | vi.advanceTimersByTime(100); 188 | 189 | fireEvent.click(element, mouseEvent); 190 | 191 | expect(callback).toBeCalledTimes(1); 192 | 193 | fireEvent.click(element, mouseEvent); 194 | 195 | vi.advanceTimersByTime(2000); 196 | 197 | expect(callback).toBeCalledTimes(1); 198 | 199 | fireEvent.click(element, mouseEvent); 200 | fireEvent.click(element, mouseEvent); 201 | expect(callback).toBeCalledTimes(2); 202 | }); 203 | 204 | test('Call single tap or double tap callback depending on clicks interval, supply proper event', () => { 205 | const singleTapCallback = vi.fn(); 206 | 207 | const element = createTestElement({ 208 | callback, 209 | threshold: 300, 210 | options: { onSingleTap: singleTapCallback }, 211 | }); 212 | 213 | expect(callback).toBeCalledTimes(0); 214 | 215 | fireEvent.click(element, mouseEvent); 216 | 217 | // Trigger single tap 218 | vi.runOnlyPendingTimers(); 219 | expect(singleTapCallback).toBeCalledTimes(1); 220 | expect(singleTapCallback).toBeCalledWith(expectMouseEvent); 221 | expect(callback).toBeCalledTimes(0); 222 | 223 | // After double tap, counter should stay the same 224 | fireEvent.click(element, mouseEvent); 225 | fireEvent.click(element, mouseEvent); 226 | expect(singleTapCallback).toBeCalledTimes(1); 227 | expect(callback).toBeCalledTimes(1); 228 | 229 | // After double tap with 200ms delay, counter should stay the same 230 | vi.runOnlyPendingTimers(); 231 | 232 | fireEvent.click(element, mouseEvent); 233 | vi.advanceTimersByTime(200); 234 | fireEvent.click(element, mouseEvent); 235 | expect(singleTapCallback).toBeCalledTimes(1); 236 | expect(callback).toBeCalledTimes(2); 237 | 238 | // Counter should increase when there was a long delay after first tap 239 | fireEvent.click(element, mouseEvent); 240 | 241 | // Wait 5 seconds 242 | vi.advanceTimersByTime(5000); 243 | expect(singleTapCallback).toBeCalledTimes(2); 244 | expect(callback).toBeCalledTimes(2); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /packages/use-double-tap/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.7 (2025-03-16) 2 | 3 | ### 🚀 Features 4 | 5 | - **storybook-host:** Add Vercel Analytics in form of Storybook addon ([1103f48](https://github.com/minwork/react/commit/1103f48)) 6 | - **storybook-host:** Create storybook lib and add stories for Long Press Hook ([b206ad4](https://github.com/minwork/react/commit/b206ad4)) 7 | 8 | ### 🩹 Fixes 9 | 10 | - **project:** Fix running test coverage tasks ([394fdde](https://github.com/minwork/react/commit/394fdde)) 11 | 12 | ### 💅 Refactors 13 | 14 | - **react-interval-hook:** Move hook code to react monorepo ([1ce9766](https://github.com/minwork/react/commit/1ce9766)) 15 | 16 | ### 📖 Documentation 17 | 18 | - **project:** Update links for Long press hook section ([f59e8b8](https://github.com/minwork/react/commit/f59e8b8)) 19 | - **use-double-tap:** Add basic usage example to README ([5b968b3](https://github.com/minwork/react/commit/5b968b3)) 20 | - **README:** Restore bundle size badge for all libraries ([cc40996](https://github.com/minwork/react/commit/cc40996)) 21 | - **README:** Refactor the README for clearer formatting ([59c185a](https://github.com/minwork/react/commit/59c185a)) 22 | - **README:** Refactor main README file ([7a5e045](https://github.com/minwork/react/commit/7a5e045)) 23 | - **use-double-tap:** Replace package README image and unify badges ([9043a81](https://github.com/minwork/react/commit/9043a81)) 24 | - **README:** Add what's new section ([826bd51](https://github.com/minwork/react/commit/826bd51)) 25 | - **README:** Update react-interval-hook description ([893cd05](https://github.com/minwork/react/commit/893cd05)) 26 | - **README:** Stylistic changes to main README ([aadaf67](https://github.com/minwork/react/commit/aadaf67)) 27 | 28 | ### 📦 Build 29 | 30 | - **project:** Resolve packages resolutions validation errors ([ae96d2d](https://github.com/minwork/react/commit/ae96d2d)) 31 | - **deps:** bump rollup from 2.79.1 to 2.79.2 ([#45](https://github.com/minwork/react/pull/45)) 32 | - **pr:** Update node versions matrix ([a3e3dc7](https://github.com/minwork/react/commit/a3e3dc7)) 33 | - **pr:** Enable Corepack to use the newest Yarn version ([9f31d44](https://github.com/minwork/react/commit/9f31d44)) 34 | - **pr:** Update PR check node and action versions ([233ef73](https://github.com/minwork/react/commit/233ef73)) 35 | - **deps:** bump cross-spawn from 7.0.3 to 7.0.6 ([3b7ce9b](https://github.com/minwork/react/commit/3b7ce9b)) 36 | - **workflows:** Add react-interval-hook to test workflow ([1983df2](https://github.com/minwork/react/commit/1983df2)) 37 | - **workflows:** Separate build and test workflow ([14d31f8](https://github.com/minwork/react/commit/14d31f8)) 38 | 39 | ### ❤️ Thank You 40 | 41 | - minwork @minwork 42 | 43 | ## 1.3.7-preview.0 (2025-03-01) 44 | 45 | ### 🚀 Features 46 | 47 | - **storybook-host:** Add Vercel Analytics in form of Storybook addon 48 | - **storybook-host:** Create storybook lib and add stories for Long Press Hook 49 | 50 | ### 🩹 Fixes 51 | 52 | - **project:** Fix running test coverage tasks 53 | 54 | ### 💅 Refactors 55 | 56 | - **react-interval-hook:** Move hook code to react monorepo 57 | 58 | ### 📖 Documentation 59 | 60 | - **project:** Update links for Long press hook section 61 | - **use-double-tap:** Add basic usage example to README 62 | - **README:** Restore bundle size badge for all libraries 63 | - **README:** Refactor the README for clearer formatting 64 | - **README:** Refactor main README file 65 | - **use-double-tap:** Replace package README image and unify badges 66 | - **README:** Add what's new section 67 | - **README:** Update react-interval-hook description 68 | - **README:** Stylistic changes to main README 69 | 70 | ### 📦 Build 71 | 72 | - **project:** Resolve packages resolutions validation errors 73 | - **deps:** bump rollup from 2.79.1 to 2.79.2 74 | - **pr:** Update node versions matrix 75 | - **pr:** Enable Corepack to use the newest Yarn version 76 | - **pr:** Update PR check node and action versions 77 | - **deps:** bump cross-spawn from 7.0.3 to 7.0.6 78 | - **workflows:** Add react-interval-hook to test workflow 79 | - **workflows:** Separate build and test workflow 80 | 81 | ### ❤️ Thank You 82 | 83 | - minwork 84 | 85 | ## [1.3.6](https://github.com/minwork/react/compare/use-double-tap-v1.3.5...use-double-tap-v1.3.6) (2023-08-27) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * **use-double-tap:** Add proper path to TS typings for ESM ([dbfbb03](https://github.com/minwork/react/commit/dbfbb03ea3214dc51950f818acd9fee421c8a4ea)) 91 | 92 | 93 | ### Build config 94 | 95 | * **deps:** bump word-wrap from 1.2.3 to 1.2.5 ([232430b](https://github.com/minwork/react/commit/232430b3adc80e024128b6f49f1d5f5537323bed)) 96 | * **use-double-tap:** Move semantic-release config to project.json ([738531e](https://github.com/minwork/react/commit/738531e18041d464be6f666af4832e08f536350d)) 97 | * **use-double-tap:** Remove empty options object from semantic-release executor config ([c071848](https://github.com/minwork/react/commit/c0718484acbf8d34b22d25b936eff4c1876da63c)) 98 | * **use-long-press:** Migrate semantic release pipeline to nx-semantic-release ([1f8f239](https://github.com/minwork/react/commit/1f8f239b60d22fe3301e50b21306486302fca4f7)) 99 | 100 | 101 | ### Documentation 102 | 103 | * **use-double-tap:** Fix link to documentation ([ea43630](https://github.com/minwork/react/commit/ea43630ec4fb26549dbd45f28638d20396ab5ddd)) 104 | * **use-double-tap:** Move documentation to gitbook ([b67574e](https://github.com/minwork/react/commit/b67574e12a943dc3ac2c596435dba6f0e368fb2f)) 105 | 106 | ## [1.3.6-alpha.4](https://github.com/minwork/react/compare/use-double-tap-v1.3.6-alpha.3...use-double-tap-v1.3.6-alpha.4) (2023-08-27) 107 | 108 | 109 | ### Documentation 110 | 111 | * **use-double-tap:** Fix link to documentation ([ea43630](https://github.com/minwork/react/commit/ea43630ec4fb26549dbd45f28638d20396ab5ddd)) 112 | 113 | ## [1.3.6-alpha.3](https://github.com/minwork/react/compare/use-double-tap-v1.3.6-alpha.2...use-double-tap-v1.3.6-alpha.3) (2023-08-27) 114 | 115 | 116 | ### Documentation 117 | 118 | * **use-double-tap:** Move documentation to gitbook ([b67574e](https://github.com/minwork/react/commit/b67574e12a943dc3ac2c596435dba6f0e368fb2f)) 119 | 120 | ## [1.3.6-alpha.2](https://github.com/minwork/react/compare/use-double-tap-v1.3.6-alpha.1...use-double-tap-v1.3.6-alpha.2) (2023-08-15) 121 | 122 | 123 | ### Build config 124 | 125 | * **use-double-tap:** Move semantic-release config to project.json ([738531e](https://github.com/minwork/react/commit/738531e18041d464be6f666af4832e08f536350d)) 126 | 127 | ## [1.3.6-alpha.1](https://github.com/minwork/react/compare/use-double-tap-v1.3.5...use-double-tap-v1.3.6-alpha.1) (2023-08-15) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * **use-double-tap:** Add proper path to TS typings for ESM ([dbfbb03](https://github.com/minwork/react/commit/dbfbb03ea3214dc51950f818acd9fee421c8a4ea)) 133 | 134 | ## [1.3.5](https://github.com/minwork/react/compare/use-double-tap-v1.3.4...use-double-tap-v1.3.5) (2023-08-01) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * **use-double-tap:** Remove invalid exports ([6bd8dce](https://github.com/minwork/react/commit/6bd8dcedc1fa539b8f6702d372aadd665b57084e)) 140 | 141 | ## [1.3.5-rc.3](https://github.com/minwork/react/compare/use-double-tap-v1.3.5-rc.2...use-double-tap-v1.3.5-rc.3) (2023-07-20) 142 | 143 | ## [1.3.5-alpha.3](https://github.com/minwork/react/compare/use-double-tap-v1.3.5-alpha.2...use-double-tap-v1.3.5-alpha.3) (2023-07-12) 144 | 145 | ## [1.3.5-rc.2](https://github.com/minwork/react/compare/use-double-tap-v1.3.5-rc.1...use-double-tap-v1.3.5-rc.2) (2023-07-12) 146 | 147 | ## [1.3.5-alpha.2](https://github.com/minwork/react/compare/use-double-tap-v1.3.5-alpha.1...use-double-tap-v1.3.5-alpha.2) (2023-07-05) 148 | 149 | ## [1.3.5-rc.1](https://github.com/minwork/react/compare/use-double-tap-v1.3.4...use-double-tap-v1.3.5-rc.1) (2023-07-05) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * **use-double-tap:** Remove invalid exports ([6bd8dce](https://github.com/minwork/react/commit/6bd8dcedc1fa539b8f6702d372aadd665b57084e)) 155 | 156 | ## [1.3.5-alpha.1](https://github.com/minwork/react/compare/use-double-tap-v1.3.4...use-double-tap-v1.3.5-alpha.1) (2023-07-02) 157 | 158 | 159 | ### Bug Fixes 160 | 161 | * **use-double-tap:** Remove invalid exports ([6bd8dce](https://github.com/minwork/react/commit/6bd8dcedc1fa539b8f6702d372aadd665b57084e)) 162 | -------------------------------------------------------------------------------- /packages/react-interval-hook/src/tests/react-interval-hook.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { IntervalHookCallback, IntervalHookFinishCallback, useInterval } from '../lib'; 3 | import { noop } from '@react/shared/util-tests'; 4 | 5 | describe('Check isolated hook calls', () => { 6 | let callback: IntervalHookCallback; 7 | let onFinish: IntervalHookFinishCallback; 8 | beforeEach(() => { 9 | vi.useFakeTimers(); 10 | callback = vi.fn(); 11 | onFinish = vi.fn(); 12 | }); 13 | 14 | afterEach(() => { 15 | vi.clearAllTimers(); 16 | vi.clearAllMocks(); 17 | }); 18 | 19 | it('Always return object with management methods as a result', () => { 20 | const resultTemplate = { 21 | start: expect.any(Function), 22 | stop: expect.any(Function), 23 | isActive: expect.any(Function), 24 | }; 25 | expect(renderHook(() => useInterval(noop)).result.current).toMatchObject(resultTemplate); 26 | expect(renderHook(() => useInterval(noop, 200)).result.current).toMatchObject(resultTemplate); 27 | expect( 28 | renderHook(() => 29 | useInterval(noop, 1000, { 30 | onFinish: noop, 31 | immediate: false, 32 | autoStart: true, 33 | }) 34 | ).result.current 35 | ).toMatchObject(resultTemplate); 36 | }); 37 | 38 | it('Call interval callback regularly after started and before stopped', () => { 39 | const interval = 500; 40 | const manage = renderHook(() => useInterval(callback, interval, { autoStart: false, immediate: false })).result 41 | .current; 42 | 43 | expect(callback).toBeCalledTimes(0); 44 | // Start interval 45 | manage.start(); 46 | vi.runOnlyPendingTimers(); 47 | expect(callback).toBeCalledTimes(1); 48 | vi.runOnlyPendingTimers(); 49 | expect(callback).toBeCalledTimes(2); 50 | vi.runOnlyPendingTimers(); 51 | expect(callback).toBeCalledTimes(3); 52 | manage.stop(); 53 | vi.runOnlyPendingTimers(); 54 | vi.advanceTimersByTime(interval); 55 | expect(callback).toBeCalledTimes(3); 56 | }); 57 | 58 | it('Automatically starts timer when autoStart option is set to true', () => { 59 | renderHook(() => useInterval(callback, 1000, { autoStart: true })); 60 | 61 | vi.runOnlyPendingTimers(); 62 | expect(callback).toBeCalledTimes(1); 63 | }); 64 | 65 | it('Immediately call callback when immediate option and autoStart is set to true', () => { 66 | renderHook(() => useInterval(callback, 1000, { immediate: true, autoStart: true })); 67 | 68 | expect(callback).toBeCalledTimes(1); 69 | vi.runOnlyPendingTimers(); 70 | expect(callback).toBeCalledTimes(2); 71 | }); 72 | 73 | it('Call callback after using start method when immediate option is set to true and autoStart is set to false', () => { 74 | const { start } = renderHook(() => useInterval(callback, 1000, { immediate: true, autoStart: false })).result 75 | .current; 76 | 77 | start(); 78 | expect(callback).toBeCalledTimes(1); 79 | vi.runOnlyPendingTimers(); 80 | expect(callback).toBeCalledTimes(2); 81 | }); 82 | 83 | it('Call onFinish callback after using stop method (accordingly to stop method argument)', () => { 84 | renderHook(() => useInterval(callback, 1000, { immediate: true, autoStart: true, onFinish })).result.current.stop(); 85 | 86 | expect(onFinish).toBeCalledTimes(1); 87 | 88 | renderHook(() => useInterval(callback, 1000, { onFinish })).result.current.stop(); 89 | 90 | expect(onFinish).toBeCalledTimes(2); 91 | 92 | renderHook(() => useInterval(callback, 1000, { onFinish })).result.current.stop(false); 93 | 94 | expect(onFinish).toBeCalledTimes(2); 95 | 96 | // Do not call stop if timer wasn't started 97 | renderHook(() => 98 | useInterval(callback, 1000, { immediate: false, autoStart: false, onFinish }) 99 | ).result.current.stop(); 100 | 101 | expect(onFinish).toBeCalledTimes(2); 102 | 103 | renderHook(() => useInterval(callback, 1000, { immediate: true, autoStart: false, onFinish })).result.current.stop( 104 | false 105 | ); 106 | 107 | expect(onFinish).toBeCalledTimes(2); 108 | }); 109 | 110 | it('Properly return if interval is active using isActive method', () => { 111 | const { start, stop, isActive } = renderHook(() => useInterval(callback, 1000, { autoStart: false, onFinish })) 112 | .result.current; 113 | 114 | expect(isActive()).toBe(false); 115 | 116 | start(); 117 | expect(isActive()).toBe(true); 118 | vi.runOnlyPendingTimers(); 119 | expect(isActive()).toBe(true); 120 | stop(); 121 | 122 | expect(isActive()).toBe(false); 123 | }); 124 | 125 | it('Interval is properly self-correcting and callback is called with correct amount of ticks', () => { 126 | const { start, stop } = renderHook(() => useInterval(callback, 1000, { autoStart: false })).result.current; 127 | 128 | vi.spyOn(Date, 'now') 129 | .mockReturnValue(0) 130 | .mockReturnValueOnce(0) 131 | .mockReturnValueOnce(2000) 132 | .mockReturnValueOnce(5000) 133 | .mockReturnValueOnce(10500) 134 | .mockReturnValueOnce(11000); 135 | 136 | start(); 137 | vi.runOnlyPendingTimers(); 138 | expect(callback).toHaveBeenLastCalledWith(2); 139 | 140 | vi.runOnlyPendingTimers(); 141 | expect(callback).toHaveBeenLastCalledWith(3); 142 | 143 | vi.runOnlyPendingTimers(); 144 | expect(callback).toHaveBeenLastCalledWith(5); 145 | 146 | vi.runOnlyPendingTimers(); 147 | expect(callback).toHaveBeenLastCalledWith(1); 148 | 149 | stop(); 150 | }); 151 | 152 | it('Hook properly ignore duplicated managing method calls', () => { 153 | const { start, stop } = renderHook(() => 154 | useInterval(callback, 1000, { autoStart: false, immediate: false, onFinish }) 155 | ).result.current; 156 | 157 | expect(callback).toBeCalledTimes(0); 158 | start(); 159 | expect(callback).toBeCalledTimes(0); 160 | start(); 161 | vi.runOnlyPendingTimers(); 162 | start(); 163 | expect(callback).toBeCalledTimes(1); 164 | 165 | expect(onFinish).toBeCalledTimes(0); 166 | stop(); 167 | expect(onFinish).toBeCalledTimes(1); 168 | vi.runOnlyPendingTimers(); 169 | stop(); 170 | stop(); 171 | vi.runOnlyPendingTimers(); 172 | expect(onFinish).toBeCalledTimes(1); 173 | 174 | expect(callback).toBeCalledTimes(1); 175 | 176 | start(); 177 | stop(); 178 | start(); 179 | stop(); 180 | start(); 181 | stop(); 182 | 183 | expect(callback).toBeCalledTimes(1); 184 | expect(onFinish).toBeCalledTimes(4); 185 | 186 | start(); 187 | start(); 188 | expect(callback).toBeCalledTimes(1); 189 | start(); 190 | vi.runOnlyPendingTimers(); 191 | expect(callback).toBeCalledTimes(2); 192 | }); 193 | 194 | it('Hook properly ignore duplicated managing method calls when immediate option is set to true', () => { 195 | const { start, stop } = renderHook(() => useInterval(callback, 1000, { autoStart: false, immediate: true })).result 196 | .current; 197 | 198 | expect(callback).toBeCalledTimes(0); 199 | start(); 200 | expect(callback).toBeCalledTimes(1); 201 | start(); 202 | vi.runOnlyPendingTimers(); 203 | start(); 204 | expect(callback).toBeCalledTimes(2); 205 | 206 | stop(); 207 | vi.runOnlyPendingTimers(); 208 | stop(); 209 | stop(); 210 | vi.runOnlyPendingTimers(); 211 | 212 | expect(callback).toBeCalledTimes(2); 213 | 214 | start(); 215 | start(); 216 | expect(callback).toBeCalledTimes(3); 217 | start(); 218 | vi.runOnlyPendingTimers(); 219 | expect(callback).toBeCalledTimes(4); 220 | }); 221 | 222 | it('should not self correct and pass ticks when selfCorrecting flag is false', () => { 223 | const { start, stop } = renderHook(() => useInterval(callback, 1000, { autoStart: false, selfCorrecting: false })) 224 | .result.current; 225 | 226 | start(); 227 | vi.runOnlyPendingTimers(); 228 | expect(callback).toHaveBeenCalledTimes(1); 229 | expect(callback).toHaveBeenCalledWith(); 230 | 231 | vi.runOnlyPendingTimers(); 232 | expect(callback).toHaveBeenCalledTimes(2); 233 | 234 | vi.runOnlyPendingTimers(); 235 | expect(callback).toHaveBeenCalledTimes(3); 236 | 237 | vi.runOnlyPendingTimers(); 238 | expect(callback).toHaveBeenCalledTimes(4); 239 | 240 | stop(); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /packages/use-long-press/src/lib/use-long-press.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MouseEvent, 3 | MouseEventHandler, 4 | PointerEvent, 5 | PointerEventHandler, 6 | TouchEventHandler, 7 | useCallback, 8 | useEffect, 9 | useRef, 10 | } from 'react'; 11 | import { 12 | LongPressCallback, 13 | LongPressCallbackReason, 14 | LongPressDomEvents, 15 | LongPressEmptyHandlers, 16 | LongPressEventType, 17 | LongPressHandlers, 18 | LongPressMouseHandlers, 19 | LongPressOptions, 20 | LongPressPointerHandlers, 21 | LongPressReactEvents, 22 | LongPressResult, 23 | LongPressTouchHandlers, 24 | } from './use-long-press.types'; 25 | import { createArtificialReactEvent, getCurrentPosition, isRecognisableEvent } from './use-long-press.utils'; 26 | 27 | // Disabled callback 28 | export function useLongPress( 29 | callback: null, 30 | options?: LongPressOptions 31 | ): LongPressResult; 32 | // Touch events 33 | export function useLongPress< 34 | Target extends Element = Element, 35 | Context = unknown, 36 | Callback extends LongPressCallback = LongPressCallback 37 | >( 38 | callback: Callback, 39 | options: LongPressOptions 40 | ): LongPressResult, Context>; 41 | // Mouse events 42 | export function useLongPress< 43 | Target extends Element = Element, 44 | Context = unknown, 45 | Callback extends LongPressCallback = LongPressCallback 46 | >( 47 | callback: Callback, 48 | options: LongPressOptions 49 | ): LongPressResult, Context>; 50 | // Pointer events 51 | export function useLongPress< 52 | Target extends Element = Element, 53 | Context = unknown, 54 | Callback extends LongPressCallback = LongPressCallback 55 | >( 56 | callback: Callback, 57 | options: LongPressOptions 58 | ): LongPressResult, Context>; 59 | // Default options 60 | export function useLongPress< 61 | Target extends Element = Element, 62 | Context = unknown, 63 | Callback extends LongPressCallback = LongPressCallback 64 | >(callback: Callback): LongPressResult, Context>; 65 | // General 66 | export function useLongPress< 67 | Target extends Element = Element, 68 | Context = unknown, 69 | Callback extends LongPressCallback = LongPressCallback 70 | >( 71 | callback: Callback | null, 72 | options?: LongPressOptions 73 | ): LongPressResult, Context>; 74 | /** 75 | * Detect click / tap and hold event 76 | * @param {useLongPress~callback|null} callback - Function to call when long press is detected (click or tap lasts for threshold amount of time or longer) 77 | * 78 | * @param {number} threshold - Period of time that must elapse after detecting click or tap in order to trigger _callback_ 79 | * 80 | * @param {boolean} captureEvent - If `event.persist()` should be called on react event 81 | * 82 | * @param {string} detect - Which type of events should be detected (`'mouse'` | `'touch'` | `'pointer'`). For TS use *LongPressEventType* enum. 83 | * 84 | * @param {boolean|number} cancelOnMovement - If long press should be canceled on mouse / touch / pointer move. Possible values:
    85 | *
  • `false` - [default] Disable cancelling on movement
  • 86 | *
  • `true` - Enable cancelling on movement and use default 25px threshold
  • 87 | *
  • `number` - Set a specific tolerance value in pixels (square side size inside which movement won't cancel long press)
  • 88 | *
89 | * 90 | * @param {boolean} cancelOutsideElement If long press should be canceled when moving mouse / touch / pointer outside the element to which it was bound. Works for mouse and pointer events, touch events will be supported in the future. 91 | * 92 | * @param {(event:Object)=>boolean} filterEvents - Function to filter incoming events. Function should return `false` for events that will be ignored (e.g. right mouse clicks) 93 | * 94 | * @param {useLongPress~callback} onStart - Called after detecting initial click / tap / point event. Allows to change event position before registering it for the purpose of `cancelOnMovement`. 95 | * 96 | * @param {useLongPress~callback} onMove - Called on every move event. Allows to change event position before calculating distance for the purpose of `cancelOnMovement`. 97 | * 98 | * @param {useLongPress~callback} onFinish - Called when releasing click / tap / point if long press **was** triggered. 99 | * 100 | * @param {useLongPress~callback} onCancel - Called when releasing click / tap / point if long press **was not** triggered 101 | * 102 | * @see LongPressCallback 103 | * @see LongPressOptions 104 | * @see LongPressResult 105 | */ 106 | export function useLongPress< 107 | Target extends Element = Element, 108 | Context = unknown, 109 | Callback extends LongPressCallback = LongPressCallback 110 | >( 111 | callback: Callback | null, 112 | { 113 | threshold = 400, 114 | captureEvent = false, 115 | detect = LongPressEventType.Pointer, 116 | cancelOnMovement = false, 117 | cancelOutsideElement = true, 118 | filterEvents, 119 | onStart, 120 | onMove, 121 | onFinish, 122 | onCancel, 123 | }: LongPressOptions = {} 124 | ): LongPressResult, Context> { 125 | const isLongPressActive = useRef(false); 126 | const isPressed = useRef(false); 127 | const target = useRef(); 128 | const timer = useRef>(); 129 | const savedCallback = useRef(callback); 130 | const startPosition = useRef<{ 131 | x: number; 132 | y: number; 133 | } | null>(null); 134 | 135 | const start = useCallback( 136 | (context?: Context) => (event: LongPressReactEvents) => { 137 | // Prevent multiple start triggers 138 | if (isPressed.current) { 139 | return; 140 | } 141 | 142 | // Ignore unrecognised events 143 | if (!isRecognisableEvent(event)) { 144 | return; 145 | } 146 | 147 | // If we don't want all events to trigger long press and provided event is filtered out 148 | if (filterEvents !== undefined && !filterEvents(event)) { 149 | return; 150 | } 151 | 152 | if (captureEvent) { 153 | event.persist(); 154 | } 155 | 156 | // When touched trigger onStart and start timer 157 | onStart?.(event, { context }); 158 | 159 | // Calculate position after calling 'onStart' so it can potentially change it 160 | startPosition.current = getCurrentPosition(event); 161 | isPressed.current = true; 162 | target.current = event.currentTarget; 163 | 164 | timer.current = setTimeout(() => { 165 | if (savedCallback.current) { 166 | savedCallback.current(event, { context }); 167 | isLongPressActive.current = true; 168 | } 169 | }, threshold); 170 | }, 171 | [captureEvent, filterEvents, onStart, threshold] 172 | ); 173 | 174 | const cancel = useCallback( 175 | (context?: Context) => (event: LongPressReactEvents, reason?: LongPressCallbackReason) => { 176 | // Ignore unrecognised events 177 | if (!isRecognisableEvent(event)) { 178 | return; 179 | } 180 | 181 | // Ignore when element is not pressed anymore 182 | if (!isPressed.current) { 183 | return; 184 | } 185 | 186 | startPosition.current = null; 187 | 188 | if (captureEvent) { 189 | event.persist(); 190 | } 191 | 192 | // Trigger onFinish callback only if timer was active 193 | if (isLongPressActive.current) { 194 | onFinish?.(event, { context }); 195 | } else if (isPressed.current) { 196 | // If not active but pressed, trigger onCancel 197 | onCancel?.(event, { context, reason: reason ?? LongPressCallbackReason.CancelledByRelease }); 198 | } 199 | 200 | isLongPressActive.current = false; 201 | isPressed.current = false; 202 | timer.current !== undefined && clearTimeout(timer.current); 203 | }, 204 | [captureEvent, onFinish, onCancel] 205 | ); 206 | 207 | const move = useCallback( 208 | (context?: Context) => (event: LongPressReactEvents) => { 209 | // Ignore unrecognised events 210 | if (!isRecognisableEvent(event)) { 211 | return; 212 | } 213 | 214 | // First call callback to allow modifying event position 215 | onMove?.(event, { context }); 216 | 217 | if (cancelOnMovement !== false && startPosition.current) { 218 | const currentPosition = getCurrentPosition(event); 219 | 220 | if (currentPosition) { 221 | const moveThreshold = cancelOnMovement === true ? 25 : cancelOnMovement; 222 | const movedDistance = { 223 | x: Math.abs(currentPosition.x - startPosition.current.x), 224 | y: Math.abs(currentPosition.y - startPosition.current.y), 225 | }; 226 | 227 | // If moved outside move tolerance box then cancel long press 228 | if (movedDistance.x > moveThreshold || movedDistance.y > moveThreshold) { 229 | cancel(context)(event, LongPressCallbackReason.CancelledByMovement); 230 | } 231 | } 232 | } 233 | }, 234 | [cancel, cancelOnMovement, onMove] 235 | ); 236 | 237 | const binder = useCallback, Context>>( 238 | (ctx?: Context) => { 239 | if (callback === null) { 240 | return {}; 241 | } 242 | 243 | switch (detect) { 244 | case LongPressEventType.Mouse: { 245 | const result: LongPressMouseHandlers = { 246 | onMouseDown: start(ctx) as MouseEventHandler, 247 | onMouseMove: move(ctx) as MouseEventHandler, 248 | onMouseUp: cancel(ctx) as MouseEventHandler, 249 | }; 250 | 251 | if (cancelOutsideElement) { 252 | result.onMouseLeave = (event: MouseEvent) => { 253 | cancel(ctx)(event, LongPressCallbackReason.CancelledOutsideElement); 254 | }; 255 | } 256 | 257 | return result; 258 | } 259 | 260 | case LongPressEventType.Touch: 261 | return { 262 | onTouchStart: start(ctx) as TouchEventHandler, 263 | onTouchMove: move(ctx) as TouchEventHandler, 264 | onTouchEnd: cancel(ctx) as TouchEventHandler, 265 | }; 266 | 267 | case LongPressEventType.Pointer: { 268 | const result: LongPressPointerHandlers = { 269 | onPointerDown: start(ctx) as PointerEventHandler, 270 | onPointerMove: move(ctx) as PointerEventHandler, 271 | onPointerUp: cancel(ctx) as PointerEventHandler, 272 | }; 273 | 274 | if (cancelOutsideElement) { 275 | result.onPointerLeave = (event: PointerEvent) => 276 | cancel(ctx)(event, LongPressCallbackReason.CancelledOutsideElement); 277 | } 278 | 279 | return result; 280 | } 281 | } 282 | }, 283 | [callback, cancel, cancelOutsideElement, detect, move, start] 284 | ); 285 | 286 | // Listen to long press stop events on window 287 | useEffect(() => { 288 | function listener(event: LongPressDomEvents) { 289 | const reactEvent = createArtificialReactEvent(event); 290 | cancel()(reactEvent); 291 | } 292 | 293 | window.addEventListener('mouseup', listener); 294 | window.addEventListener('touchend', listener); 295 | window.addEventListener('pointerup', listener); 296 | 297 | // Unregister all listeners on unmount 298 | return () => { 299 | window.removeEventListener('mouseup', listener); 300 | window.removeEventListener('touchend', listener); 301 | window.removeEventListener('pointerup', listener); 302 | }; 303 | }, [cancel]); 304 | 305 | // Clear timer on unmount 306 | useEffect( 307 | () => (): void => { 308 | timer.current !== undefined && clearTimeout(timer.current); 309 | }, 310 | [] 311 | ); 312 | 313 | // Update callback handle when it changes 314 | useEffect(() => { 315 | savedCallback.current = callback; 316 | }, [callback]); 317 | 318 | return binder; 319 | } 320 | --------------------------------------------------------------------------------