From 419bf97219281cf77e824c167d1bea3bd1a11cc6 Mon Sep 17 00:00:00 2001 From: arturovt Date: Sun, 17 Nov 2024 18:57:08 +0200 Subject: [PATCH] fix: enable strict mode In this commit, we enable strict mode because it is required to generate correct type definition (`.d.ts`) files. These files differ depending on whether strict mode is enabled or disabled. Enabling strict mode allows `null|undefined` values to be provided by the parent component (e.g., `[tp]="null"`, which should be allowed), but the compiler removes `null|undefined` values from the binding. We also had to patch the `@popperjs/core` types due to a mistake in their definitions. This patch was necessary for successful compilation --- package-lock.json | 265 +++++++++++++++++- package.json | 2 + patches/tippy.js++@popperjs+core+2.10.2.patch | 13 + .../ngneat/helipopper/src/lib/defaults.ts | 14 +- .../src/lib/intersection-observer.ts | 3 +- .../helipopper/src/lib/tippy.directive.ts | 161 +++++------ .../helipopper/src/lib/tippy.service.ts | 65 +++-- .../ngneat/helipopper/src/lib/tippy.types.ts | 25 +- projects/ngneat/helipopper/src/lib/utils.ts | 5 +- projects/ngneat/helipopper/tsconfig.lib.json | 1 + src/app/playground/playground.component.ts | 20 +- tsconfig.json | 1 + 12 files changed, 426 insertions(+), 149 deletions(-) create mode 100644 patches/tippy.js++@popperjs+core+2.10.2.patch diff --git a/package-lock.json b/package-lock.json index 125d65f..3e832d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "helipopper-playground", "version": "0.0.0", + "hasInstallScript": true, "dependencies": { "@angular/animations": "^18.0.6", "@angular/common": "^18.0.6", @@ -49,6 +50,7 @@ "karma-jasmine-html-reporter": "^1.5.0", "lint-staged": "^9.2.0", "ng-packagr": "^18.0.0", + "patch-package": "8.0.0", "prettier": "2.8.8", "serve": "^11.3.2", "standard-version": "^8.0.2", @@ -58,7 +60,7 @@ "typescript": "~5.4.5" }, "engines": { - "node": "^18.10" + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" } }, "node_modules/@ampproject/remapping": { @@ -11450,6 +11452,15 @@ "node": ">=8" } }, + "node_modules/find-yarn-workspace-root": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz", + "integrity": "sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==", + "dev": true, + "dependencies": { + "micromatch": "^4.0.2" + } + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -13626,6 +13637,30 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/json-stable-stringify": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.1.1.tgz", + "integrity": "sha512-SU/971Kt5qVQfJpyDveVhQ/vya+5hvrjClFOcr8c0Fq5aODJjMwutrOfCU+eCnVD5gpx1Q3fEqkyom77zH1iIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-stable-stringify/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -13659,6 +13694,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", + "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -13995,6 +14039,15 @@ "node": ">=0.10.0" } }, + "node_modules/klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11" + } + }, "node_modules/launch-editor": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.0.tgz", @@ -16492,6 +16545,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -16841,6 +16903,207 @@ "node": ">= 0.8" } }, + "node_modules/patch-package": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", + "integrity": "sha512-da8BVIhzjtgScwDJ2TtKsfT5JFWz1hYoBl9rUQ1f38MC2HwnEIkK8VN3dKMKcP7P7bvvgzNDbfNHtx3MsQb5vA==", + "dev": true, + "dependencies": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^4.1.2", + "ci-info": "^3.7.0", + "cross-spawn": "^7.0.3", + "find-yarn-workspace-root": "^2.0.0", + "fs-extra": "^9.0.0", + "json-stable-stringify": "^1.0.2", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.6", + "open": "^7.4.2", + "rimraf": "^2.6.3", + "semver": "^7.5.3", + "slash": "^2.0.0", + "tmp": "^0.0.33", + "yaml": "^2.2.2" + }, + "bin": { + "patch-package": "index.js" + }, + "engines": { + "node": ">=14", + "npm": ">5" + } + }, + "node_modules/patch-package/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/cross-spawn": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/patch-package/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/patch-package/node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/patch-package/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/patch-package/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/patch-package/node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/patch-package/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/patch-package/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/patch-package/node_modules/yaml": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", diff --git a/package.json b/package.json index b8ae651..9a3c6a2 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "scripts": { + "postinstall": "patch-package", "ng": "ng", "start-test": "start-server-and-test", "deploy": "ng deploy --base-href=https://ngneat.github.io/helipopper/", @@ -75,6 +76,7 @@ "karma-jasmine-html-reporter": "^1.5.0", "lint-staged": "^9.2.0", "ng-packagr": "^18.0.0", + "patch-package": "8.0.0", "prettier": "2.8.8", "serve": "^11.3.2", "standard-version": "^8.0.2", diff --git a/patches/tippy.js++@popperjs+core+2.10.2.patch b/patches/tippy.js++@popperjs+core+2.10.2.patch new file mode 100644 index 0000000..9681f23 --- /dev/null +++ b/patches/tippy.js++@popperjs+core+2.10.2.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/tippy.js/node_modules/@popperjs/core/lib/types.d.ts b/node_modules/tippy.js/node_modules/@popperjs/core/lib/types.d.ts +index 11f9219..f971927 100644 +--- a/node_modules/tippy.js/node_modules/@popperjs/core/lib/types.d.ts ++++ b/node_modules/tippy.js/node_modules/@popperjs/core/lib/types.d.ts +@@ -114,7 +114,7 @@ export declare type ModifierArguments = { + options: Partial; + name: string; + }; +-export declare type Modifier = { ++export declare type Modifier = { + name: Name; + enabled: boolean; + phase: ModifierPhases; diff --git a/projects/ngneat/helipopper/src/lib/defaults.ts b/projects/ngneat/helipopper/src/lib/defaults.ts index 2ccc42e..cc92936 100644 --- a/projects/ngneat/helipopper/src/lib/defaults.ts +++ b/projects/ngneat/helipopper/src/lib/defaults.ts @@ -1,22 +1,22 @@ -import { TippyConfig } from './tippy.types'; +import type { TippyProps } from './tippy.types'; -type Variation = TippyConfig['variations'][0]; +type Variation = Partial; export const tooltipVariation: Variation = { - theme: null, + theme: undefined, arrow: false, animation: 'scale', trigger: 'mouseenter', - offset: [0, 5] + offset: [0, 5], }; export const popperVariation: Variation = { theme: 'light', arrow: true, offset: [0, 10], - animation: null, + animation: undefined, trigger: 'click', - interactive: true + interactive: true, }; export function withContextMenuVariation(baseVariation: Variation): Variation { @@ -25,6 +25,6 @@ export function withContextMenuVariation(baseVariation: Variation): Variation { placement: 'right-start', trigger: 'manual', arrow: false, - offset: [0, 0] + offset: [0, 0], }; } diff --git a/projects/ngneat/helipopper/src/lib/intersection-observer.ts b/projects/ngneat/helipopper/src/lib/intersection-observer.ts index 49073b3..1edd1e0 100644 --- a/projects/ngneat/helipopper/src/lib/intersection-observer.ts +++ b/projects/ngneat/helipopper/src/lib/intersection-observer.ts @@ -6,4 +6,5 @@ // allows us to remove `runOutsideAngular` calls and reduce indentation, // making the code a bit more readable. export const IntersectionObserver: typeof globalThis.IntersectionObserver = - globalThis['__zone_symbol__IntersectionObserver'] || globalThis.IntersectionObserver; + (globalThis as any)['__zone_symbol__IntersectionObserver'] || + globalThis.IntersectionObserver; diff --git a/projects/ngneat/helipopper/src/lib/tippy.directive.ts b/projects/ngneat/helipopper/src/lib/tippy.directive.ts index aaff12f..1917af4 100644 --- a/projects/ngneat/helipopper/src/lib/tippy.directive.ts +++ b/projects/ngneat/helipopper/src/lib/tippy.directive.ts @@ -17,6 +17,7 @@ import { OnInit, Output, PLATFORM_ID, + SimpleChanges, untracked, ViewContainerRef, } from '@angular/core'; @@ -46,7 +47,6 @@ import { overflowChanges, } from './utils'; import { - NgChanges, TIPPY_CONFIG, TIPPY_REF, TippyConfig, @@ -56,6 +56,22 @@ import { import { TippyFactory } from './tippy.factory'; import { coerceBooleanAttribute } from './coercion'; +// These are the default values used by `tippy.js`. +// We are providing them as default input values. +// The `tippy.js` repository has been archived and is unlikely to +// change in the future, so it is safe to use these values as defaults. +const defaultAppendTo: TippyProps['appendTo'] = () => document.body; +const defaultDelay: TippyProps['delay'] = 0; +const defaultDuration: TippyProps['duration'] = [300, 250]; +const defaultInteractiveBorder: TippyProps['interactiveBorder'] = 2; +const defaultMaxWidth: TippyProps['maxWidth'] = 350; +const defaultOffset: TippyProps['offset'] = [0, 10]; +const defaultPlacement: TippyProps['placement'] = 'top'; +const defaultTrigger: TippyProps['trigger'] = 'mouseenter focus'; +const defaultTriggerTarget: TippyProps['triggerTarget'] = null; +const defaultZIndex: TippyProps['zIndex'] = 9999; +const defaultAnimation: TippyProps['animation'] = 'fade'; + @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector selector: '[tp]', @@ -63,64 +79,70 @@ import { coerceBooleanAttribute } from './coercion'; standalone: true, }) export class TippyDirective implements OnChanges, AfterViewInit, OnInit { - // Note that default values are not provided for these bindings because `tippy.js` - // has its own default values and checks whether the provided props are `undefined`. - // We should keep `undefined` as the default value. - - readonly appendTo = input(undefined, { alias: 'tpAppendTo' }); + readonly appendTo = input(defaultAppendTo, { + alias: 'tpAppendTo', + }); - readonly content = input(undefined, { alias: 'tp' }); + readonly content = input('', { alias: 'tp' }); - readonly delay = input(undefined, { alias: 'tpDelay' }); + readonly delay = input(defaultDelay, { + alias: 'tpDelay', + }); - readonly duration = input(undefined, { alias: 'tpDuration' }); + readonly duration = input(defaultDuration, { + alias: 'tpDuration', + }); - readonly hideOnClick = input(undefined, { + readonly hideOnClick = input(true, { alias: 'tpHideOnClick', }); - readonly interactive = input(undefined, { + readonly interactive = input(false, { alias: 'tpInteractive', }); - readonly interactiveBorder = input(undefined, { + readonly interactiveBorder = input(defaultInteractiveBorder, { alias: 'tpInteractiveBorder', }); - readonly maxWidth = input(undefined, { alias: 'tpMaxWidth' }); + readonly maxWidth = input(defaultMaxWidth, { + alias: 'tpMaxWidth', + }); // Note that some of the input signal types are declared explicitly because the compiler // also uses types from `@popperjs/core` and requires a type annotation. - readonly offset: InputSignal = input( - undefined, - { alias: 'tpOffset' } - ); + readonly offset: InputSignal = input(defaultOffset, { + alias: 'tpOffset', + }); - readonly placement: InputSignal = input< - TippyProps['placement'] - >(undefined, { + readonly placement: InputSignal = input(defaultPlacement, { alias: 'tpPlacement', }); - readonly popperOptions: InputSignal = input< - TippyProps['popperOptions'] - >(undefined, { - alias: 'tpPopperOptions', - }); + readonly popperOptions: InputSignal = input( + {}, + { + alias: 'tpPopperOptions', + } + ); - readonly showOnCreate = input(undefined, { + readonly showOnCreate = input(false, { alias: 'tpShowOnCreate', }); - readonly trigger = input(undefined, { alias: 'tpTrigger' }); + readonly trigger = input(defaultTrigger, { + alias: 'tpTrigger', + }); - readonly triggerTarget = input(undefined, { + readonly triggerTarget = input(defaultTriggerTarget, { alias: 'tpTriggerTarget', }); - readonly zIndex = input(undefined, { alias: 'tpZIndex' }); + readonly zIndex = input(defaultZIndex, { + alias: 'tpZIndex', + }); - readonly animation = input(undefined, { + readonly animation = input(defaultAnimation, { alias: 'tpAnimation', }); @@ -134,7 +156,7 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { alias: 'tpIsLazy', }); - readonly variation = input(undefined, { alias: 'tpVariation' }); + readonly variation = input(undefined, { alias: 'tpVariation' }); readonly isEnabled = input(true, { alias: 'tpIsEnabled' }); @@ -164,19 +186,21 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { readonly detectChangesComponent = input(true, { alias: 'tpDetectChangesComponent' }); - readonly popperWidth = input(undefined, { alias: 'tpPopperWidth' }); + readonly popperWidth = input(undefined, { + alias: 'tpPopperWidth', + }); - readonly customHost = input(undefined, { alias: 'tpHost' }); + readonly customHost = input(undefined, { alias: 'tpHost' }); readonly isVisible = model(false, { alias: 'tpIsVisible' }); @Output('tpVisible') visible = new EventEmitter(); - protected instance: TippyInstance; - protected viewRef: ViewRef; - protected props: Partial; + protected instance!: TippyInstance; + protected viewRef: ViewRef | null = null; + protected props!: Partial; protected variationDefined = false; - protected viewOptions$: ViewOptions; + protected viewOptions$: ViewOptions | null = null; /** * We had use `visible` event emitter previously as a `takeUntil` subscriber in multiple places @@ -186,7 +210,7 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { * in the template (``). */ protected visibleInternal = new Subject(); - private visibilityObserverCleanup: () => void | undefined; + private visibilityObserverCleanup: VoidFunction | null = null; private contentChanged = new Subject(); private host = computed( @@ -222,37 +246,18 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { }); } - ngOnChanges(changes: NgChanges) { + ngOnChanges(changes: SimpleChanges) { if (this.isServer) return; - // TODO: we can update it later. - // We need to go over class properties, check whether it's a signal (`isSignal(prop)`) - // and construct a record where `Record`. - // Follow-up changes would be to have a computation and an effect. - let props: Partial = Object.keys(changes).reduce((acc, change) => { - if (change === 'isVisible') return acc; - - acc[change] = changes[change].currentValue; - - return acc; - }, {}); - - let variation: string; - - if (isChanged('variation', changes)) { - variation = changes.variation.currentValue; - this.variationDefined = true; - } else if (!this.variationDefined) { - variation = this.globalConfig.defaultVariation; - this.variationDefined = true; - } - - if (variation) { - props = { - ...this.globalConfig.variations[variation], - ...props, - }; - } + const variation = this.variation() || this.globalConfig.defaultVariation || ''; + const props = Object.keys(changes) + // `isVisible` is not required as a prop since we update it manually + // in an effect-like manner. + .filter((key) => key !== 'isVisible') + .reduce( + (accumulator, key) => ({ ...accumulator, [key]: changes[key].currentValue }), + { ...this.globalConfig.variations?.[variation] } + ); this.updateProps(props); } @@ -424,7 +429,7 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { if (this.useHostWidth()) { this.setInstanceWidth(instance, this.hostWidth); } else if (this.popperWidth()) { - this.setInstanceWidth(instance, this.popperWidth()); + this.setInstanceWidth(instance, this.popperWidth()!); } this.globalConfig.onShow?.(instance); }, @@ -479,7 +484,7 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { } } - this.viewRef = this.viewService.createView(content, { + this.viewRef = this.viewService.createView(content!, { vcr: this.vcr, ...this.viewOptions$, }); @@ -492,7 +497,7 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { let newContent = this.viewRef.getElement(); if (this.useTextContent()) { - newContent = instance.reference.textContent; + newContent = instance.reference.textContent!; } if (isString(newContent) && this.globalConfig.beforeRender) { @@ -503,7 +508,7 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { } protected handleContextMenu() { - fromEvent(this.host(), 'contextmenu') + fromEvent(this.host(), 'contextmenu') .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((event: MouseEvent) => { event.preventDefault(); @@ -525,7 +530,7 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { } protected handleEscapeButton(): void { - fromEvent(document.body, 'keydown') + fromEvent(document.body, 'keydown') .pipe( filter(({ code }: KeyboardEvent) => code === 'Escape'), takeUntil(this.visibleInternal.pipe(filter((v) => !v))), @@ -600,11 +605,6 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { } private setupListeners(): void { - effect(() => { - const appendTo = this.appendTo(); - this.updateProps({ appendTo }); - }); - effect(() => { // Capture signal read to track its changes. this.content(); @@ -619,10 +619,3 @@ export class TippyDirective implements OnChanges, AfterViewInit, OnInit { }); } } - -function isChanged( - key: keyof NgChanges, - changes: NgChanges -) { - return key in changes; -} diff --git a/projects/ngneat/helipopper/src/lib/tippy.service.ts b/projects/ngneat/helipopper/src/lib/tippy.service.ts index 8815c5f..d648c09 100644 --- a/projects/ngneat/helipopper/src/lib/tippy.service.ts +++ b/projects/ngneat/helipopper/src/lib/tippy.service.ts @@ -1,9 +1,21 @@ import { inject, Inject, Injectable, Injector } from '@angular/core'; -import { isComponent, isTemplateRef, ViewService } from '@ngneat/overview'; +import { + isComponent, + isTemplateRef, + ResolveViewRef, + ViewService, +} from '@ngneat/overview'; import { Content } from '@ngneat/overview'; import type { Observable } from 'rxjs'; -import { CreateOptions, ExtendedTippyInstance, TIPPY_CONFIG, TIPPY_REF, TippyConfig } from './tippy.types'; +import { + CreateOptions, + ExtendedTippyInstance, + TIPPY_CONFIG, + TIPPY_REF, + TippyConfig, + TippyInstance, +} from './tippy.types'; import { normalizeClassName, onlyTippyProps } from './utils'; import { TippyFactory } from './tippy.factory'; @@ -22,24 +34,22 @@ export class TippyService { content: T, options: Partial = {} ): Observable> { - const variation = options.variation || this.globalConfig.defaultVariation; + const variation = options.variation || this.globalConfig.defaultVariation || ''; const config = { - onShow: (instance) => { + onShow: (instance: ExtendedTippyInstance) => { host.setAttribute('data-tippy-open', ''); if (!instance.$viewOptions) { - instance.$viewOptions = {}; - - const injector = Injector.create({ - providers: [ - { - provide: TIPPY_REF, - useValue: instance, - }, - ], - parent: options.injector || this.injector, - }); - - instance.$viewOptions.injector = injector; + instance.$viewOptions = { + injector: Injector.create({ + providers: [ + { + provide: TIPPY_REF, + useValue: instance, + }, + ], + parent: options.injector || this.injector, + }), + }; if (isTemplateRef(content)) { instance.$viewOptions.context = { @@ -51,25 +61,28 @@ export class TippyService { instance.data = options.data; } } - if (!instance.view) { - instance.view = this.view.createView(content, { ...options, ...instance.$viewOptions }); - } + + instance.view ||= this.view.createView(content, { + ...options, + ...instance.$viewOptions, + }) as ResolveViewRef; + instance.setContent(instance.view.getElement()); options?.onShow?.(instance); }, - onHidden: (instance) => { + onHidden: (instance: ExtendedTippyInstance) => { host.removeAttribute('data-tippy-open'); if (!options.preserveView) { - instance.view.destroy(); + instance.view?.destroy(); instance.view = null; } options?.onHidden?.(instance); }, ...onlyTippyProps(this.globalConfig), - ...this.globalConfig.variations[variation], + ...this.globalConfig.variations?.[variation], ...onlyTippyProps(options), - onCreate: (instance) => { + onCreate: (instance: TippyInstance) => { instance.popper.classList.add(`tippy-variation-${variation}`); if (options.className) { for (const klass of normalizeClassName(options.className)) { @@ -81,6 +94,8 @@ export class TippyService { }, }; - return this._tippyFactory.create(host, config) as Observable>; + return this._tippyFactory.create(host, config) as Observable< + ExtendedTippyInstance + >; } } diff --git a/projects/ngneat/helipopper/src/lib/tippy.types.ts b/projects/ngneat/helipopper/src/lib/tippy.types.ts index 4b67921..45fdb73 100644 --- a/projects/ngneat/helipopper/src/lib/tippy.types.ts +++ b/projects/ngneat/helipopper/src/lib/tippy.types.ts @@ -1,7 +1,7 @@ import type tippy from 'tippy.js'; import type { Instance, Props } from 'tippy.js'; import { ElementRef, InjectionToken, type InputSignal } from '@angular/core'; -import { ResolveViewRef, ViewOptions } from '@ngneat/overview'; +import type { ResolveViewRef, ViewOptions } from '@ngneat/overview'; export interface CreateOptions extends Partial, ViewOptions { variation: string; @@ -10,27 +10,6 @@ export interface CreateOptions extends Partial, ViewOptions { data: any; } -type InferInputSignalType = T extends InputSignal ? R : T; - -export type NgChanges> = { - [Key in keyof Props]: { - previousValue: InferInputSignalType; - currentValue: InferInputSignalType; - firstChange: boolean; - isFirstChange(): boolean; - }; -}; - -type MarkFunctionPropertyNames = { - [Key in keyof Component]: Component[Key] extends InputSignal - ? Key - : Component[Key] extends Function - ? never - : Key; -}[keyof Component]; - -type ExcludeFunctions = Pick>; - export const TIPPY_REF = new InjectionToken('TIPPY_REF'); export interface TippyInstance extends Instance { @@ -49,7 +28,7 @@ export interface ExtendedTippyProps extends TippyProps { export type TippyElement = ElementRef | Element; export interface ExtendedTippyInstance extends TippyInstance { - view: ResolveViewRef; + view: ResolveViewRef | null; $viewOptions: ViewOptions; context?: ViewOptions['context']; } diff --git a/projects/ngneat/helipopper/src/lib/utils.ts b/projects/ngneat/helipopper/src/lib/utils.ts index 0975cae..df1db85 100644 --- a/projects/ngneat/helipopper/src/lib/utils.ts +++ b/projects/ngneat/helipopper/src/lib/utils.ts @@ -51,7 +51,8 @@ export function isElementOverflow(host: HTMLElement): boolean { const hostOffsetWidth = host.offsetWidth; return ( - hostOffsetWidth > host.parentElement.offsetWidth || hostOffsetWidth < host.scrollWidth + hostOffsetWidth > host.parentElement!.offsetWidth || + hostOffsetWidth < host.scrollWidth ); } @@ -83,7 +84,7 @@ function resizeObserverStrategy(target: HTMLElement): Observable { } export function onlyTippyProps(allProps: any) { - const tippyProps = {}; + const tippyProps: any = {}; const ownProps = [ 'useTextContent', diff --git a/projects/ngneat/helipopper/tsconfig.lib.json b/projects/ngneat/helipopper/tsconfig.lib.json index 622105c..7b3eb72 100644 --- a/projects/ngneat/helipopper/tsconfig.lib.json +++ b/projects/ngneat/helipopper/tsconfig.lib.json @@ -5,6 +5,7 @@ "declarationMap": true, "declaration": true, "inlineSources": true, + "strict": true, "types": [], "lib": ["dom", "es2020"] }, diff --git a/src/app/playground/playground.component.ts b/src/app/playground/playground.component.ts index c3cb855..915a777 100644 --- a/src/app/playground/playground.component.ts +++ b/src/app/playground/playground.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, ElementRef, ViewChild, computed } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + ViewChild, + computed, +} from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { UntypedFormBuilder } from '@angular/forms'; import { ExampleComponent } from '../example/example.component'; @@ -63,14 +69,16 @@ export class PlaygroundComponent { text3 = `Short`; comp = ExampleComponent; - @ViewChild('inputName', { static: true }) inputName: ElementRef; - @ViewChild('inputNameComp', { static: true }) inputNameComp: ElementRef; + @ViewChild('inputName', { static: true }) inputName!: ElementRef; + @ViewChild('inputNameComp', { static: true }) inputNameComp!: ElementRef; constructor(private fb: UntypedFormBuilder, private service: TippyService) {} toggleText(text: string, index: number) { const resolved = - text === `Only shown when text is overflowed ${index}` ? `Short` : `Only shown when text is overflowed`; + text === `Only shown when text is overflowed ${index}` + ? `Short` + : `Only shown when text is overflowed`; return `${resolved} ${index}`; } @@ -87,7 +95,7 @@ export class PlaygroundComponent { console.log('show tooltip', $event); } - instance2: TippyInstance; + instance2!: TippyInstance; useService(host: HTMLButtonElement) { if (!this.instance2) { @@ -97,7 +105,7 @@ export class PlaygroundComponent { } } - instance: TippyInstance; + instance!: TippyInstance; useServiceComponent(host2: HTMLButtonElement) { if (!this.instance) { diff --git a/tsconfig.json b/tsconfig.json index 7e8ac56..9187663 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "baseUrl": "./", "outDir": "./dist/out-tsc", + "strict": true, "sourceMap": true, "declaration": false, "downlevelIteration": true,