diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 612358521..3da3a5ec9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,7 @@ jobs: speech, storage, workers, + view-transition, ] name: ${{ matrix.project }} steps: @@ -128,6 +129,11 @@ jobs: directory: ./coverage/workers/ flags: summary,workers name: workers + - uses: codecov/codecov-action@v3.1.3 + with: + directory: ./coverage/view-transition/ + flags: summary,view-transition + name: view-transition concurrency: group: test-${{ github.workflow }}-${{ github.ref }} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..72446f434 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/README.md b/README.md index a98631c35..d5dee845c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ High quality lightweight wrappers for native Web APIs for idiomatic use with Ang - [`@ng-web-apis/storage`](https://github.com/taiga-family/ng-web-apis/blob/main/libs/storage/CHANGELOG.md) - [`@ng-web-apis/universal`](https://github.com/taiga-family/ng-web-apis/blob/main/libs/universal/CHANGELOG.md) - [`@ng-web-apis/workers`](https://github.com/taiga-family/ng-web-apis/blob/main/libs/workers/CHANGELOG.md) +- [`@ng-web-apis/view-transition`](https://github.com/taiga-family/ng-web-apis/blob/main/libs/view-transition/CHANGELOG.md) ## Core team diff --git a/apps/demo/src/app/app.routes.ts b/apps/demo/src/app/app.routes.ts index e7d608c32..72ca75ae9 100644 --- a/apps/demo/src/app/app.routes.ts +++ b/apps/demo/src/app/app.routes.ts @@ -87,6 +87,12 @@ export const appRoutes: Routes = [ loadChildren: async () => (await import(`./pages/workers/workers-page.module`)).WorkersPageModule, }, + { + path: DemoPath.ViewTransitionPage, + loadChildren: async () => + (await import(`./pages/view-transition/view-transition-page.module`)) + .ViewTransitionPageModule, + }, { path: '', redirectTo: DemoPath.HomePage, diff --git a/apps/demo/src/app/constants/demo-path.ts b/apps/demo/src/app/constants/demo-path.ts index 641b45f57..69b22db46 100644 --- a/apps/demo/src/app/constants/demo-path.ts +++ b/apps/demo/src/app/constants/demo-path.ts @@ -14,4 +14,5 @@ export enum DemoPath { PermissionsPage = `permissions`, StoragePage = `storage`, WorkersPage = `workers`, + ViewTransitionPage = `view-transition`, } diff --git a/apps/demo/src/app/pages/home/home-page.component.html b/apps/demo/src/app/pages/home/home-page.component.html index 48ded43cd..bee02b7ce 100644 --- a/apps/demo/src/app/pages/home/home-page.component.html +++ b/apps/demo/src/app/pages/home/home-page.component.html @@ -235,3 +235,18 @@

Workers

class="icon" /> + +
+

View Transition

+ A library for declarative use of + View Transition API + with Angular +
+ View transition API logo +
diff --git a/apps/demo/src/app/pages/view-transition/view-transition-page.component.html b/apps/demo/src/app/pages/view-transition/view-transition-page.component.html new file mode 100644 index 000000000..5f807ad7d --- /dev/null +++ b/apps/demo/src/app/pages/view-transition/view-transition-page.component.html @@ -0,0 +1,33 @@ +

How to use

+

+ Usage is pretty simple: just import service in your component and call + startViewTransition + on it. +

+
+

Basic example

+ +
+
+ + Photo by + {{item.author}} + + +
+
+ +
+ +
+ + + Photos provided by Pexels + diff --git a/apps/demo/src/app/pages/view-transition/view-transition-page.component.less b/apps/demo/src/app/pages/view-transition/view-transition-page.component.less new file mode 100644 index 000000000..6599c4de5 --- /dev/null +++ b/apps/demo/src/app/pages/view-transition/view-transition-page.component.less @@ -0,0 +1,56 @@ +:host { + display: flex; + align-items: center; + flex-direction: column; +} + +.photos { + display: flex; + justify-content: center; + align-items: flex-start; + flex-wrap: wrap; + gap: 1rem; + margin: 1rem 0; +} + +.photo { + width: 12rem; + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + + .author { + margin-bottom: 0.5rem; + + a:hover { + text-decoration: underline; + } + } + + img { + display: block; + width: 100%; + border-radius: 0.5rem; + transition: transform 0.2s; + + &:hover { + transform: scale(1.05); + } + } +} + +.expanded-photo { + margin: 0 auto 1rem; + width: 20rem; + display: flex; + cursor: pointer; + + img { + display: block; + view-transition-name: expandable-image; + border-radius: 1rem; + box-shadow: 0 12px 36px rgba(0, 0, 0, 0.2); + width: 100%; + } +} diff --git a/apps/demo/src/app/pages/view-transition/view-transition-page.component.ts b/apps/demo/src/app/pages/view-transition/view-transition-page.component.ts new file mode 100644 index 000000000..1685ebc90 --- /dev/null +++ b/apps/demo/src/app/pages/view-transition/view-transition-page.component.ts @@ -0,0 +1,91 @@ +import {ChangeDetectionStrategy, ChangeDetectorRef, Component} from '@angular/core'; +import {finalize} from 'rxjs'; +import {ViewTransitionService} from '@ng-web-apis/view-transition'; + +interface Photo { + src: string; + author: string; + url: string; +} + +const PHOTOS = [ + { + src: 'https://images.pexels.com/photos/16316785/pexels-photo-16316785/free-photo-of-fluffy-cat-on-blooming-tree-background.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', + author: 'Peng Louis', + url: 'https://www.pexels.com/photo/fluffy-cat-on-blooming-tree-background-16316785/', + }, + { + src: 'https://images.pexels.com/photos/6001385/pexels-photo-6001385.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', + author: 'Sam Lion', + url: 'https://www.pexels.com/photo/cute-curious-cat-watching-video-on-laptop-sitting-on-couch-6001385/', + }, + { + src: 'https://images.pexels.com/photos/7210265/pexels-photo-7210265.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1', + author: 'Blue Bird', + url: 'https://www.pexels.com/photo/ginger-cat-sleeping-on-ground-in-autumn-7210265/', + }, +]; + +const USAGE_SAMPLE = ` + // 1) Import service throught DI + // In Constructor + constructor(private viewTransitionService: ViewTransitionService) {} + // or with inject (Angular 14+) + private service = inject(ViewTransitionService); + + // 2) Call startViewTransition method and pass callback that would change the DOM + private showMyComponent(): void { + this.viewTransitionService.startViewTransition(() => { + this.showMyComponent = true; + // You might want to call detectChanges to update the DOM inside startViewTransition callback + this.cdr.detectChanges(); + }).subscribe(); + } +`; + +@Component({ + selector: `view-transition-page`, + templateUrl: `./view-transition-page.component.html`, + styleUrls: [`./view-transition-page.component.less`], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ViewTransitionPageComponent { + codeSample = USAGE_SAMPLE; + activeIndex = -1; + showDetails = false; + detailInfo: Photo | undefined = undefined; + data = PHOTOS; + + constructor( + private readonly viewTransitionService: ViewTransitionService, + private readonly cdr: ChangeDetectorRef, + ) {} + + open(index: number): void { + this.activeIndex = index; + this.detailInfo = this.data[index]; + this.cdr.detectChanges(); + + this.viewTransitionService + .startViewTransition(() => { + this.showDetails = true; + this.cdr.detectChanges(); + }) + .subscribe(); + } + + close(): void { + this.viewTransitionService + .startViewTransition(() => { + this.showDetails = false; + this.cdr.detectChanges(); + }) + .pipe( + finalize(() => { + this.activeIndex = -1; + this.detailInfo = undefined; + }), + ) + .subscribe(); + } +} diff --git a/apps/demo/src/app/pages/view-transition/view-transition-page.module.ts b/apps/demo/src/app/pages/view-transition/view-transition-page.module.ts new file mode 100644 index 000000000..1fe5b3542 --- /dev/null +++ b/apps/demo/src/app/pages/view-transition/view-transition-page.module.ts @@ -0,0 +1,17 @@ +import {NgModule} from '@angular/core'; +import {ViewTransitionPageComponent} from './view-transition-page.component'; +import {RouterModule} from '@angular/router'; +import {CommonModule} from '@angular/common'; +import {HighlightModule} from 'ngx-highlightjs'; +import {TuiLinkModule} from '@taiga-ui/core'; + +@NgModule({ + imports: [ + CommonModule, + HighlightModule, + TuiLinkModule, + RouterModule.forChild([{path: '', component: ViewTransitionPageComponent}]), + ], + declarations: [ViewTransitionPageComponent], +}) +export class ViewTransitionPageModule {} diff --git a/apps/demo/src/assets/images/view-transition.svg b/apps/demo/src/assets/images/view-transition.svg new file mode 100644 index 000000000..e9744a2e8 --- /dev/null +++ b/apps/demo/src/assets/images/view-transition.svg @@ -0,0 +1,15 @@ + diff --git a/apps/demo/tsconfig.app.json b/apps/demo/tsconfig.app.json index abcb4fcba..42464cb4a 100644 --- a/apps/demo/tsconfig.app.json +++ b/apps/demo/tsconfig.app.json @@ -2,8 +2,7 @@ "extends": "../../tsconfig.json", "files": ["src/main.ts", "src/polyfills.ts"], "compilerOptions": { - "outDir": "./out-tsc/app", - "types": ["webmidi"] + "outDir": "./out-tsc/app" }, "include": ["**/*.d.ts"], "angularCompilerOptions": { diff --git a/apps/demo/tsconfig.server.json b/apps/demo/tsconfig.server.json index e16c33a28..651c1e5ec 100644 --- a/apps/demo/tsconfig.server.json +++ b/apps/demo/tsconfig.server.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "./dist/out-tsc-server", "target": "es2016", - "types": ["node", "webmidi"] + "types": ["node", "webmidi", "dom-view-transitions"] }, "files": ["src/typings.d.ts", "src/main.server.ts", "server.ts"], "angularCompilerOptions": { diff --git a/libs/view-transition/CHANGELOG.md b/libs/view-transition/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/libs/view-transition/LICENSE b/libs/view-transition/LICENSE new file mode 100644 index 000000000..b84e23a28 --- /dev/null +++ b/libs/view-transition/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2023 Tinkoff Bank + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/libs/view-transition/README.md b/libs/view-transition/README.md new file mode 100644 index 000000000..fb15d512c --- /dev/null +++ b/libs/view-transition/README.md @@ -0,0 +1,65 @@ +# ![ng-web-apis logo](https://raw.githubusercontent.com/taiga-family/ng-web-apis/main/libs/view-transition/logo.svg) View Transition API for Angular + +[![npm version](https://img.shields.io/npm/v/@ng-web-apis/view-transition.svg)](https://npmjs.com/package/@ng-web-apis/view-transition) +![npm bundle size](https://img.shields.io/bundlephobia/minzip/@ng-web-apis/view-transition) +[![Coveralls github](https://img.shields.io/coveralls/github/ng-web-apis/view-transition)](https://coveralls.io/github/ng-web-apis/view-transition?branch=master) + +This service is an abstraction over +[view transition API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for Angular + +## Install + +```bash +npm i @ng-web-apis/view-transition +``` + +## How to use + +1. Import the `ViewTransitionService` into your Angular component or service where you want to use it. + +```ts +import {ViewTransitionService} from '@ng-web-apis/view-transition'; +``` + +2. Inject the `ViewTransitionService` into your component's constructor or with `inject` (Angular 14+). + +```ts +// in constructor +constructor(private viewTransitionService: ViewTransitionService) {} + +// via inject +viewTransitionService = inject(ViewTransitionService); +``` + +3. Use the `startViewTransition` method to initiate a view transition. This method takes a callback function that + returns a `Promise` or `void`. You can perform any necessary DOM changing logic within this callback. + +```ts +startTransition() { + this.viewTransitionService.startViewTransition(() => { + // Your DOM changing logic goes here + return this.animateTransition(); + }).subscribe({ + next: (transition) => { + // Callback is done and transition is about to begin + console.log('View transition is about to begin:', transition); + }, + complete: () => { + console.log('View transition completed'); + }, + error: (error) => { + // Handle any errors that occur during the transition + console.error('View transition error:', error); + }, + }); +} +``` + +## Demo + +You can [try online demo here](https://taiga-family.github.io/ng-web-apis/view-transition) + +## See also + +Other [Web APIs for Angular](https://taiga-family.github.io/ng-web-apis/) by +[@ng-web-apis](https://github.com/taiga-family/ng-web-apis) diff --git a/libs/view-transition/karma.conf.js b/libs/view-transition/karma.conf.js new file mode 100644 index 000000000..d22389b30 --- /dev/null +++ b/libs/view-transition/karma.conf.js @@ -0,0 +1,42 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma'), + ], + client: { + clearContext: false, // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../../coverage/view-transition'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true, + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['ChromeHeadless'], + singleRun: true, + customLaunchers: { + ChromeHeadless: { + base: 'Chrome', + flags: [ + '--no-sandbox', + '--headless', + '--disable-gpu', + '--remote-debugging-port=9222', + ], + }, + }, + }); +}; diff --git a/libs/view-transition/logo.svg b/libs/view-transition/logo.svg new file mode 100644 index 000000000..f806cf775 --- /dev/null +++ b/libs/view-transition/logo.svg @@ -0,0 +1,15 @@ + diff --git a/libs/view-transition/ng-package.json b/libs/view-transition/ng-package.json new file mode 100644 index 000000000..627cafca6 --- /dev/null +++ b/libs/view-transition/ng-package.json @@ -0,0 +1,11 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "assets": [ + "logo.svg", + "README.md" + ], + "dest": "../../dist/view-transition", + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/view-transition/package.json b/libs/view-transition/package.json new file mode 100644 index 000000000..34a1939d8 --- /dev/null +++ b/libs/view-transition/package.json @@ -0,0 +1,30 @@ +{ + "name": "@ng-web-apis/view-transition", + "version": "3.0.2", + "description": "This is a library for declarative use of View Transition API with Angular", + "keywords": [ + "angular", + "ng", + "view transition", + "view transition api" + ], + "homepage": "https://github.com/taiga-family/ng-web-apis/blob/main/libs/view-transition/README.md", + "bugs": "https://github.com/taiga-family/ng-web-apis/issues", + "repository": "https://github.com/taiga-family/ng-web-apis", + "license": "Apache-2.0", + "author": { + "name": "Vsevolod Arutiunov", + "email": "sevaru@inbox.ru" + }, + "contributors": [ + "Alex Inkin ", + "Roman Sedov <79601794011@ya.ru>" + ], + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0" + } +} diff --git a/libs/view-transition/project.json b/libs/view-transition/project.json new file mode 100644 index 000000000..61d39365a --- /dev/null +++ b/libs/view-transition/project.json @@ -0,0 +1,41 @@ +{ + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "name": "view-transition", + "root": "libs/view-transition", + "sourceRoot": "libs/view-transition", + "projectType": "library", + "targets": { + "test": { + "executor": "@angular-devkit/build-angular:karma", + "outputs": ["coverage/view-transition"], + "options": { + "main": "libs/view-transition/test.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "libs/view-transition/karma.conf.js", + "codeCoverage": true, + "browsers": "ChromeHeadless" + } + }, + "build": { + "executor": "@angular-devkit/build-angular:ng-packagr", + "outputs": ["dist/view-transition"], + "options": { + "tsConfig": "tsconfig.build.json", + "project": "libs/view-transition/ng-package.json" + }, + "dependsOn": [ + { + "target": "build", + "projects": "dependencies", + "params": "forward" + } + ] + }, + "publish": { + "executor": "@nrwl/workspace:run-commands", + "options": { + "command": "npm publish ./dist/view-transition --ignore-scripts || echo \"already published\"" + } + } + } +} diff --git a/libs/view-transition/src/index.ts b/libs/view-transition/src/index.ts new file mode 100644 index 000000000..0bbf03e29 --- /dev/null +++ b/libs/view-transition/src/index.ts @@ -0,0 +1,6 @@ +/** + * Public API Surface of view transition api + */ + +// Services +export * from './services/view-transition.service'; diff --git a/libs/view-transition/src/services/view-transition.service.ts b/libs/view-transition/src/services/view-transition.service.ts new file mode 100644 index 000000000..148f0fa2a --- /dev/null +++ b/libs/view-transition/src/services/view-transition.service.ts @@ -0,0 +1,53 @@ +import {DOCUMENT} from '@angular/common'; +import {Inject, Injectable} from '@angular/core'; +import {Observable, throwError} from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class ViewTransitionService { + private readonly isSupported = 'startViewTransition' in this.document; + + constructor(@Inject(DOCUMENT) private readonly document: Document) {} + + startViewTransition( + callback: () => Promise | void, + ): Observable { + if (!this.isSupported) { + return throwError( + () => new Error('startViewTransition is not supported in your browser'), + ); + } + + return new Observable(subscriber => { + const transition: ViewTransition = + this.document.startViewTransition(callback); + + transition.updateCallbackDone.then( + () => { + subscriber.next(transition); + }, + (error: Error) => { + subscriber.error(error); + }, + ); + + transition.ready.catch((error: Error) => { + subscriber.error(error); + }); + + transition.finished.then( + () => { + subscriber.complete(); + }, + (error: Error) => { + subscriber.error(error); + }, + ); + + return () => { + transition.skipTransition(); + }; + }); + } +} diff --git a/libs/view-transition/test.ts b/libs/view-transition/test.ts new file mode 100644 index 000000000..bb10cb1ff --- /dev/null +++ b/libs/view-transition/test.ts @@ -0,0 +1,23 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files +import 'zone.js'; +import 'zone.js/testing'; + +import {getTestBed} from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +); + +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); + +// And load the modules. +context.keys().map(context); diff --git a/libs/view-transition/tests/view-transition.service.spec.ts b/libs/view-transition/tests/view-transition.service.spec.ts new file mode 100644 index 000000000..103a06917 --- /dev/null +++ b/libs/view-transition/tests/view-transition.service.spec.ts @@ -0,0 +1,107 @@ +import {TestBed} from '@angular/core/testing'; +import {ViewTransitionService} from '../src/services/view-transition.service'; +import {DOCUMENT} from '@angular/common'; + +describe('ViewTransitionService', () => { + describe('not supported provider', () => { + it('throw error if startViewTransition is not supported', done => { + TestBed.configureTestingModule({ + providers: [ViewTransitionService, {provide: DOCUMENT, useValue: {}}], + }); + const service = TestBed.inject(ViewTransitionService); + + const observable = service.startViewTransition(() => {}); + + observable.subscribe({ + error: error => { + expect(error.message).toBe( + 'startViewTransition is not supported in your browser', + ); + done(); + }, + }); + }); + }); + + describe('supported provider', () => { + let service: ViewTransitionService; + + const mockDocument = { + startViewTransition: (callback: () => Promise | void) => { + callback(); + return { + updateCallbackDone: Promise.resolve(), + finished: Promise.resolve(), + ready: Promise.resolve(), + skipTransition: () => {}, + }; + }, + } as unknown as Document; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ViewTransitionService, + {provide: DOCUMENT, useValue: mockDocument}, + ], + }); + service = TestBed.inject(ViewTransitionService); + }); + + it('complete the observable when transition finishes', done => { + const observable = service.startViewTransition(() => {}); + + observable.subscribe({ + complete: () => { + done(); + }, + }); + }); + + it('pass ViewTransition object to the observable after callback called', done => { + let callbackCalled = false; + const observable = service.startViewTransition(() => { + callbackCalled = true; + }); + + observable.subscribe(viewTransition => { + expect(viewTransition).toBeTruthy(); + expect(callbackCalled).toBe(true); + done(); + }); + }); + }); + + describe('custom DOCUMENT provider', () => { + it('call skipTransition when observable unsubscribed', () => { + const viewTransitionValue = { + updateCallbackDone: Promise.resolve(), + finished: Promise.resolve(), + ready: Promise.resolve(), + skipTransition: () => {}, + }; + const mockDocument = { + startViewTransition: (callback: () => Promise | void) => { + callback(); + return viewTransitionValue; + }, + }; + TestBed.configureTestingModule({ + providers: [ + ViewTransitionService, + { + provide: DOCUMENT, + useValue: mockDocument, + }, + ], + }); + const service = TestBed.inject(ViewTransitionService); + + const skipSpy = spyOn(viewTransitionValue, 'skipTransition'); + const observable = service.startViewTransition(() => {}); + observable.subscribe().unsubscribe(); + + expect(skipSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 84144afcf..e000804b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "@tinkoff/eslint-config": "^1.22.0", "@tinkoff/eslint-config-angular": "^1.23.0", "@tinkoff/prettier-config": "^1.22.0", + "@types/dom-view-transitions": "1.0.1", "@types/dompurify": "^2.4.0", "@types/estree": "^0.0.51", "@types/express": "^4.17.13", @@ -227,6 +228,18 @@ "rxjs": ">=6.0.0" } }, + "libs/view-transition": { + "name": "@ng-web-apis/view-transition", + "version": "3.0.2", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=6.0.0", + "@angular/core": ">=6.0.0" + } + }, "libs/workers": { "name": "@ng-web-apis/workers", "version": "3.0.2", @@ -6028,6 +6041,10 @@ "resolved": "libs/universal", "link": true }, + "node_modules/@ng-web-apis/view-transition": { + "resolved": "libs/view-transition", + "link": true + }, "node_modules/@ng-web-apis/workers": { "resolved": "libs/workers", "link": true @@ -10803,6 +10820,12 @@ "@types/node": "*" } }, + "node_modules/@types/dom-view-transitions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/dom-view-transitions/-/dom-view-transitions-1.0.1.tgz", + "integrity": "sha512-A9S1ijj/4MX06I1W/6on8lhaYyq1Ir7gaOvfllW1o4RzVWW88HAeqX0pUx9VgOLnNpdiGeUW2CTkg18p5LWIrA==", + "dev": true + }, "node_modules/@types/dompurify": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", diff --git a/package.json b/package.json index b1a70815b..728e4b0b9 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "Dmitryi Khirnyi", "Dmitry Efimenko", "Andrew Grekov ", - "Debmallya Bhattacharya " + "Debmallya Bhattacharya ", + "Vsevolod Arutiunov " ], "workspaces": [ "apps/*", @@ -103,6 +104,7 @@ "@tinkoff/eslint-config": "^1.22.0", "@tinkoff/eslint-config-angular": "^1.23.0", "@tinkoff/prettier-config": "^1.22.0", + "@types/dom-view-transitions": "1.0.1", "@types/dompurify": "^2.4.0", "@types/estree": "^0.0.51", "@types/express": "^4.17.13", diff --git a/tsconfig.build.json b/tsconfig.build.json index db547a771..f8d857b0b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -23,7 +23,8 @@ "@ng-web-apis/intersection-observer": ["./dist/intersection-observer"], "@ng-web-apis/midi": ["./dist/midi"], "@ng-web-apis/workers": ["./dist/workers"], - "@ng-web-apis/resize-observer": ["./dist/resize-observer"] + "@ng-web-apis/resize-observer": ["./dist/resize-observer"], + "@ng-web-apis/view-transition": ["./dist/view-transition"] } } } diff --git a/tsconfig.json b/tsconfig.json index 85d6d4608..ba5de1392 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,7 +30,7 @@ "module": "es2020", "lib": ["es2018", "dom"], "typeRoots": ["node_modules/@types"], - "types": ["node", "webmidi"], + "types": ["node", "webmidi", "dom-view-transitions"], "skipLibCheck": true, "downlevelIteration": true, "skipDefaultLibCheck": true, @@ -75,7 +75,8 @@ "@ng-web-apis/midi": ["./libs/midi/src/index.ts"], "@ng-web-apis/storage": ["./libs/storage/src/index.ts"], "@ng-web-apis/workers": ["./libs/workers/src/index.ts"], - "@ng-web-apis/resize-observer": ["./libs/resize-observer/src/index.ts"] + "@ng-web-apis/resize-observer": ["./libs/resize-observer/src/index.ts"], + "@ng-web-apis/view-transition": ["./libs/view-transition/src/index.ts"] } }, "ts-node": { diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 6e99222aa..a6655f930 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -3,6 +3,6 @@ "exclude": [], "compilerOptions": { "outDir": "out-tsc/spec", - "types": ["jasmine", "node", "webmidi"] + "types": ["jasmine", "node", "webmidi", "dom-view-transitions"] } }