├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE.md ├── README.md ├── angular.json ├── e2e ├── app.po.ts └── app.spec.ts ├── eslint.config.mjs ├── package.json ├── playwright.config.ts ├── projects └── fab-speed-dial │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── fab-speed-dial.scss │ │ ├── fab-speed-dial.spec.ts │ │ └── fab-speed-dial.ts │ └── public-api.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── public ├── favicon.ico └── github-circle-transparent.svg ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ └── app.config.ts ├── index.html ├── main.ts └── styles.css ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | ij_typescript_use_double_quotes = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: '*' 10 | update-types: ['version-update:semver-patch', 'version-update:semver-minor'] 11 | - dependency-name: '@types/*' 12 | update-types: ['version-update:semver-patch', 'version-update:semver-minor', 'version-update:semver-major'] 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | with: 10 | fetch-depth: 0 # Need all tags to get a version number in-between tags 11 | - uses: actions/setup-node@v4 12 | with: 13 | cache: 'yarn' 14 | - run: yarn --frozen-lockfile 15 | - run: yarn build-demo 16 | - uses: actions/upload-artifact@v4 17 | with: 18 | name: dist 19 | path: dist/ 20 | 21 | test: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | cache: 'yarn' 28 | - run: yarn --frozen-lockfile 29 | - run: ./node_modules/.bin/ng test fab-speed-dial --progress false --watch=false --browsers ChromeHeadless 30 | - run: ./node_modules/.bin/ng test demo --progress false --watch=false --browsers ChromeHeadless 31 | - run: yarn e2e 32 | 33 | lint: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: actions/setup-node@v4 38 | with: 39 | cache: 'yarn' 40 | - run: yarn --frozen-lockfile 41 | - run: yarn lint 42 | 43 | prettier: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-node@v4 48 | with: 49 | cache: 'yarn' 50 | - run: yarn --frozen-lockfile 51 | - run: ./node_modules/.bin/prettier --check . 52 | 53 | publish-demo: 54 | runs-on: ubuntu-latest 55 | needs: 56 | - build 57 | - test 58 | steps: 59 | - uses: actions/download-artifact@v4 60 | with: 61 | name: dist 62 | path: dist/ 63 | - uses: crazy-max/ghaction-github-pages@v4 64 | with: 65 | build_dir: dist/demo/browser 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | 69 | release: 70 | runs-on: ubuntu-latest 71 | permissions: 72 | contents: write 73 | id-token: write 74 | packages: write 75 | if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') 76 | needs: 77 | - build 78 | - test 79 | steps: 80 | - uses: actions/checkout@v4 81 | with: 82 | ref: ${{ github.ref }} # Otherwise our annotated tag is not fetched and we cannot get correct version 83 | - uses: actions/download-artifact@v4 84 | with: 85 | name: dist 86 | path: dist/ 87 | - run: rm .gitignore 88 | 89 | # Publish to npm 90 | - uses: actions/setup-node@v4 91 | with: 92 | registry-url: 'https://registry.npmjs.org' 93 | - run: npm publish --provenance --access public dist/fab-speed-dial/ 94 | env: 95 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 96 | 97 | # Publish to GitHub Packages 98 | - uses: actions/setup-node@v4 99 | with: 100 | registry-url: 'https://npm.pkg.github.com' 101 | - run: npm publish --provenance --access public dist/fab-speed-dial/ 102 | env: 103 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | 105 | # Create release 106 | - name: Get release info 107 | run: git tag --format '%(contents:body)' --points-at > release-body.txt 108 | - uses: ncipollo/release-action@v1 109 | env: 110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 111 | with: 112 | bodyFile: release-body.txt 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | # Playwright 45 | /test-results/ 46 | /playwright-report/ 47 | /playwright/.cache/ 48 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /.angular/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/prettierrc", 3 | "tabWidth": 4, 4 | "singleQuote": true, 5 | "trailingComma": "all", 6 | "htmlWhitespaceSensitivity": "strict", 7 | "printWidth": 120, 8 | "arrowParens": "avoid", 9 | "bracketSpacing": false, 10 | "overrides": [ 11 | { 12 | "files": ["*.md"], 13 | "options": { 14 | "tabWidth": 2 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ecodev 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Material FAB speed dial 2 | 3 | [![Build Status](https://github.com/Ecodev/fab-speed-dial/workflows/main/badge.svg)](https://github.com/Ecodev/fab-speed-dial/actions) 4 | [![Total Downloads](https://img.shields.io/npm/dt/@ecodev/fab-speed-dial.svg)](https://www.npmjs.com/package/@ecodev/fab-speed-dial) 5 | [![Latest Stable Version](https://img.shields.io/npm/v/@ecodev/fab-speed-dial.svg)](https://www.npmjs.com/package/@ecodev/fab-speed-dial) 6 | [![License](https://img.shields.io/npm/l/@ecodev/fab-speed-dial.svg)](https://www.npmjs.com/package/@ecodev/fab-speed-dial) 7 | [![Join the chat at https://gitter.im/Ecodev/fab-speed-dial](https://badges.gitter.im/Ecodev/fab-speed-dial.svg)](https://gitter.im/Ecodev/fab-speed-dial) 8 | 9 | This is a FAB speed dial component for Angular Material. 10 | 11 | See the component in action on [the demo page](https://ecodev.github.io/fab-speed-dial). 12 | 13 | ## Install 14 | 15 | 1. Install the library: 16 | ```bash 17 | yarn add @ecodev/fab-speed-dial 18 | ``` 19 | 2. In your standalone components add the following to the `imports` array: 20 | - `EcoFabSpeedDialComponent` 21 | - `EcoFabSpeedDialTriggerComponent` 22 | - `EcoFabSpeedDialActionsComponent` 23 | 24 | ## Usage 25 | 26 | The following is an example of a minimal template. Either implement a `doAction()`, 27 | or adapt the bindings to your needs: 28 | 29 | ```html 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ``` 42 | 43 | ## Properties 44 | 45 | ### eco-fab-speed-dial 46 | 47 | | Property | Type | Default | Description | 48 | | ----------- | ------------------------------- | ------- | ------------------------------------------ | 49 | | `open` | `boolean` | `false` | Indicates if this FAB Speed Dial is opened | 50 | | `direction` | `up`, `down`, `left` or `right` | `up` | The direction to open the action buttons | 51 | 52 | ### eco-fab-speed-dial-trigger 53 | 54 | | Property | Type | Default | Description | 55 | | -------- | --------- | ------- | ------------------------------------------------------------------------- | 56 | | `spin` | `boolean` | `false` | Enables the rotation of the trigger action when the speed dial is opening | 57 | 58 | Additionally to spin property, add class "spin180" or "spin360" on html content inside of `eco-fab-speed-dial-trigger` tag to activate rotation on a specific element. 59 | 60 | In case of buttons, the icon should rotate not the whole button (box-shadow would rotate too). 61 | 62 | ## Development 63 | 64 | The most useful commands for development are: 65 | 66 | - `yarn dev` to start a development server 67 | - `yarn build-demo` to build the demo locally (it will be published automatically by GitHub Actions) 68 | - `git tag -a 1.2.3 && git push` to publish the lib to npm (via GitHub Actions `release` job) 69 | 70 | ## Prior work 71 | 72 | This lib was originally based on [angular-smd](https://github.com/jefersonestevo/angular-smd), 73 | and its various forks, itself based on 74 | [AngularJS FAB Speed Dial](https://material.angularjs.org/latest/demo/fabSpeedDial). 75 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:application", 15 | "options": { 16 | "outputPath": "dist/demo", 17 | "index": "src/index.html", 18 | "browser": "src/main.ts", 19 | "polyfills": ["zone.js"], 20 | "tsConfig": "tsconfig.app.json", 21 | "assets": [ 22 | { 23 | "glob": "**/*", 24 | "input": "public" 25 | } 26 | ], 27 | "styles": ["@angular/material/prebuilt-themes/azure-blue.css", "src/styles.css"], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "budgets": [ 33 | { 34 | "type": "initial", 35 | "maximumWarning": "500kB", 36 | "maximumError": "1MB" 37 | }, 38 | { 39 | "type": "anyComponentStyle", 40 | "maximumWarning": "4kB", 41 | "maximumError": "8kB" 42 | } 43 | ], 44 | "outputHashing": "all" 45 | }, 46 | "development": { 47 | "optimization": false, 48 | "extractLicenses": false, 49 | "sourceMap": true 50 | } 51 | }, 52 | "defaultConfiguration": "production" 53 | }, 54 | "serve": { 55 | "builder": "@angular-devkit/build-angular:dev-server", 56 | "options": { 57 | "port": 4212 58 | }, 59 | "configurations": { 60 | "production": { 61 | "buildTarget": "demo:build:production" 62 | }, 63 | "development": { 64 | "buildTarget": "demo:build:development" 65 | } 66 | }, 67 | "defaultConfiguration": "development" 68 | }, 69 | "extract-i18n": { 70 | "builder": "@angular-devkit/build-angular:extract-i18n" 71 | }, 72 | "test": { 73 | "builder": "@angular-devkit/build-angular:karma", 74 | "options": { 75 | "polyfills": ["zone.js", "zone.js/testing"], 76 | "tsConfig": "tsconfig.spec.json", 77 | "assets": [ 78 | { 79 | "glob": "**/*", 80 | "input": "public" 81 | } 82 | ], 83 | "styles": ["@angular/material/prebuilt-themes/azure-blue.css", "src/styles.css"], 84 | "scripts": [] 85 | } 86 | }, 87 | "lint": { 88 | "builder": "@angular-eslint/builder:lint", 89 | "options": { 90 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 91 | } 92 | }, 93 | "e2e": { 94 | "builder": "playwright-ng-schematics:playwright", 95 | "options": { 96 | "devServerTarget": "demo:serve" 97 | }, 98 | "configurations": { 99 | "production": { 100 | "devServerTarget": "demo:serve:production" 101 | } 102 | } 103 | } 104 | } 105 | }, 106 | "fab-speed-dial": { 107 | "projectType": "library", 108 | "root": "projects/fab-speed-dial", 109 | "sourceRoot": "projects/fab-speed-dial/src", 110 | "prefix": "lib", 111 | "architect": { 112 | "build": { 113 | "builder": "@angular-devkit/build-angular:ng-packagr", 114 | "options": { 115 | "project": "projects/fab-speed-dial/ng-package.json" 116 | }, 117 | "configurations": { 118 | "production": { 119 | "tsConfig": "projects/fab-speed-dial/tsconfig.lib.prod.json" 120 | }, 121 | "development": { 122 | "tsConfig": "projects/fab-speed-dial/tsconfig.lib.json" 123 | } 124 | }, 125 | "defaultConfiguration": "production" 126 | }, 127 | "test": { 128 | "builder": "@angular-devkit/build-angular:karma", 129 | "options": { 130 | "tsConfig": "projects/fab-speed-dial/tsconfig.spec.json", 131 | "polyfills": ["zone.js", "zone.js/testing"] 132 | } 133 | }, 134 | "lint": { 135 | "builder": "@angular-eslint/builder:lint", 136 | "options": { 137 | "lintFilePatterns": ["projects/fab-speed-dial/**/*.ts", "projects/fab-speed-dial/**/*.html"] 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import {Page} from '@playwright/test'; 2 | 3 | export class AppPage { 4 | public constructor(private readonly page: Page) {} 5 | 6 | public getParagraphText(): Promise { 7 | return this.page.innerText('mat-toolbar-row'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/app.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect, test} from '@playwright/test'; 2 | import {AppPage} from './app.po'; 3 | 4 | test.describe('workspace-project App', () => { 5 | let app: AppPage; 6 | 7 | test.beforeEach(({page}) => { 8 | app = new AppPage(page); 9 | }); 10 | 11 | test('should display title', async ({page}) => { 12 | await page.goto('/'); 13 | expect(await app.getParagraphText()).toContain('FAB Speed Dial'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import eslint from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | import angular from 'angular-eslint'; 5 | 6 | function tsFiles(files, extraRules = {}) { 7 | return { 8 | files: [files], 9 | extends: [ 10 | eslint.configs.recommended, 11 | ...tseslint.configs.strictTypeChecked, 12 | ...tseslint.configs.stylisticTypeChecked, 13 | ...angular.configs.tsAll, 14 | ], 15 | processor: angular.processInlineTemplates, 16 | languageOptions: { 17 | parserOptions: { 18 | projectService: true, 19 | tsconfigRootDir: import.meta.dirname, 20 | }, 21 | }, 22 | rules: { 23 | '@angular-eslint/component-max-inline-declarations': 'off', // We use that mostly for testing, so it's fine 24 | '@angular-eslint/no-forward-ref': 'off', // We sometimes need it 25 | '@angular-eslint/prefer-on-push-component-change-detection': 'off', 26 | '@angular-eslint/use-component-selector': 'off', // Some components are not template-able and thus do not need selector 27 | '@typescript-eslint/consistent-type-definitions': ['error', 'type'], 28 | '@typescript-eslint/explicit-member-accessibility': 'error', 29 | '@typescript-eslint/no-confusing-void-expression': 'off', // We prefer code tersity 30 | '@typescript-eslint/no-dynamic-delete': 'off', 31 | '@typescript-eslint/no-extraneous-class': 'off', // We have component without any logic in TS 32 | '@typescript-eslint/no-floating-promises': 'off', 33 | '@typescript-eslint/no-non-null-assertion': 'off', 34 | '@typescript-eslint/no-unnecessary-condition': 'off', // This is very unfortunate, but there are too many dangerous false-positive, see https://github.com/typescript-eslint/typescript-eslint/issues/1798 35 | '@typescript-eslint/no-unsafe-argument': 'off', 36 | '@typescript-eslint/no-unsafe-assignment': 'off', 37 | '@typescript-eslint/no-unsafe-call': 'off', 38 | '@typescript-eslint/no-unsafe-member-access': 'off', 39 | '@typescript-eslint/no-unsafe-return': 'off', 40 | '@typescript-eslint/prefer-nullish-coalescing': 'off', // Usually a good idea, but sometimes dangerous false-positive 41 | '@typescript-eslint/unbound-method': 'off', 42 | '@angular-eslint/directive-selector': [ 43 | 'error', 44 | { 45 | type: 'attribute', 46 | prefix: 'app', 47 | style: 'camelCase', 48 | }, 49 | ], 50 | '@angular-eslint/component-selector': [ 51 | 'error', 52 | { 53 | type: 'element', 54 | prefix: 'app', 55 | style: 'kebab-case', 56 | }, 57 | ], 58 | '@typescript-eslint/explicit-function-return-type': [ 59 | 'error', 60 | { 61 | allowExpressions: true, 62 | }, 63 | ], 64 | '@typescript-eslint/explicit-module-boundary-types': [ 65 | 'error', 66 | { 67 | allowArgumentsExplicitlyTypedAsAny: true, 68 | }, 69 | ], 70 | '@typescript-eslint/no-misused-promises': [ 71 | 'error', 72 | { 73 | checksVoidReturn: { 74 | // We want to use promise in Rxjs subscribes without caring about the promise result 75 | arguments: false, 76 | properties: false, 77 | }, 78 | }, 79 | ], 80 | '@typescript-eslint/restrict-plus-operands': [ 81 | 'error', 82 | { 83 | // Allow some flexibility 84 | allowAny: true, 85 | allowBoolean: true, 86 | allowNullish: true, 87 | allowNumberAndString: true, 88 | }, 89 | ], 90 | '@typescript-eslint/restrict-template-expressions': [ 91 | 'error', 92 | { 93 | // Allow some flexibility 94 | allowAny: true, 95 | allowBoolean: true, 96 | allowNullish: true, 97 | allowNumber: true, 98 | }, 99 | ], 100 | '@typescript-eslint/no-unused-expressions': [ 101 | 'error', 102 | { 103 | allowTernary: true, 104 | }, 105 | ], 106 | '@typescript-eslint/no-unused-vars': [ 107 | 'error', 108 | { 109 | caughtErrors: 'none', 110 | }, 111 | ], 112 | 'no-restricted-globals': [ 113 | 'error', 114 | 'atob', 115 | 'bota', 116 | 'document', 117 | 'event', 118 | 'history', 119 | 'length', 120 | 'localStorage', 121 | 'location', 122 | 'name', 123 | 'navigator', 124 | 'sessionStorage', 125 | 'window', 126 | ], 127 | ...extraRules, 128 | }, 129 | }; 130 | } 131 | 132 | export default tseslint.config( 133 | tsFiles('**/*.ts'), 134 | tsFiles('projects/fab-speed-dial/src/**/*.ts', { 135 | '@angular-eslint/directive-selector': [ 136 | 'error', 137 | { 138 | type: 'attribute', 139 | prefix: 'eco', 140 | style: 'camelCase', 141 | }, 142 | ], 143 | '@angular-eslint/component-selector': [ 144 | 'error', 145 | { 146 | type: 'element', 147 | prefix: 'eco', 148 | style: 'kebab-case', 149 | }, 150 | ], 151 | }), 152 | { 153 | files: ['**/*.html'], 154 | extends: [...angular.configs.templateAll], 155 | rules: { 156 | '@angular-eslint/template/alt-text': 'off', // We don't care as much as we should about a11y 157 | '@angular-eslint/template/button-has-type': 'off', 158 | '@angular-eslint/template/click-events-have-key-events': 'off', // We don't care as much as we should about a11y 159 | '@angular-eslint/template/i18n': 'off', 160 | '@angular-eslint/template/interactive-supports-focus': 'off', // We don't care as much as we should about a11y 161 | '@angular-eslint/template/label-has-associated-control': 'off', // We don't care as much as we should about a11y 162 | '@angular-eslint/template/no-autofocus': 'off', 163 | '@angular-eslint/template/no-call-expression': 'off', 164 | '@angular-eslint/template/no-inline-styles': 'off', // We sometimes use short inline styles 165 | '@angular-eslint/template/prefer-ngsrc': 'off', // TODO: experiment with ngsrc and see if we need to use it or not 166 | '@angular-eslint/template/eqeqeq': [ 167 | 'error', 168 | { 169 | allowNullOrUndefined: true, 170 | }, 171 | ], 172 | }, 173 | }, 174 | ); 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "ng": "ng", 8 | "postinstall": "playwright install chromium", 9 | "dev": "ng serve", 10 | "test-lib": "ng test fab-speed-dial", 11 | "test-demo": "ng test demo", 12 | "lint": "ng lint --max-warnings 0", 13 | "e2e": "ng e2e", 14 | "build-lib": "ng build fab-speed-dial && cp -v README.md dist/fab-speed-dial/ && cd dist/fab-speed-dial && yarn version --no-git-tag-version --new-version `git describe --tags --always`", 15 | "build-demo": "yarn build-lib && ng build demo --aot --base-href /fab-speed-dial/ && cp dist/demo/browser/index.html dist/demo/browser/404.html", 16 | "serve-demo": "echo '💡 open http://localhost:8000/fab-speed-dial/' && mkdir -p dist/server && ln -fs ../demo/browser dist/server/fab-speed-dial && php -S localhost:8000 -t dist/server/" 17 | }, 18 | "dependencies": { 19 | "@angular/animations": "^19.2.13", 20 | "@angular/cdk": "19.2.17", 21 | "@angular/common": "^19.2.13", 22 | "@angular/compiler": "^19.2.13", 23 | "@angular/core": "^19.2.13", 24 | "@angular/forms": "^19.2.13", 25 | "@angular/material": "19.2.17", 26 | "@angular/platform-browser": "^19.2.13", 27 | "@angular/platform-browser-dynamic": "^19.2.13", 28 | "rxjs": "~7.8.2", 29 | "tslib": "^2.8.1", 30 | "zone.js": "~0.15.0" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "^19.2.13", 34 | "@angular/cli": "^19.2.13", 35 | "@angular/compiler-cli": "^19.2.13", 36 | "@playwright/test": "^1.51.1", 37 | "@types/jasmine": "~5.1.7", 38 | "angular-eslint": "^19.3.0", 39 | "eslint": "^9.24.0", 40 | "jasmine-core": "~5.6.0", 41 | "karma": "~6.4.0", 42 | "karma-chrome-launcher": "~3.2.0", 43 | "karma-coverage": "~2.2.0", 44 | "karma-jasmine": "~5.1.0", 45 | "karma-jasmine-html-reporter": "~2.1.0", 46 | "ng-packagr": "^19.2.2", 47 | "playwright-ng-schematics": "2.0.2", 48 | "prettier": "^3.5.3", 49 | "typescript": "~5.8.3", 50 | "typescript-eslint": "^8.29.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, devices} from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: './e2e', 14 | /* Run tests in files in parallel */ 15 | fullyParallel: true, 16 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 17 | forbidOnly: !!process.env['CI'], 18 | /* Retry on CI only */ 19 | retries: process.env['CI'] ? 2 : 0, 20 | /* Opt out of parallel tests on CI. */ 21 | workers: process.env['CI'] ? 1 : undefined, 22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 23 | reporter: 'html', 24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 25 | use: { 26 | /* Base URL to use in actions like `await page.goto('/')`. */ 27 | baseURL: process.env['PLAYWRIGHT_TEST_BASE_URL'] ?? 'http://localhost:4212', 28 | 29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 30 | trace: 'on-first-retry', 31 | }, 32 | 33 | /* Configure projects for major browsers */ 34 | projects: [ 35 | { 36 | name: 'chromium', 37 | use: {...devices['Desktop Chrome']}, 38 | }, 39 | ], 40 | }); 41 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/fab-speed-dial", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ecodev/fab-speed-dial", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "repository": "github:Ecodev/fab-speed-dial", 6 | "sideEffects": false, 7 | "dependencies": { 8 | "tslib": "^2.8.1" 9 | }, 10 | "peerDependencies": { 11 | "@angular/common": "^19.1.0", 12 | "@angular/core": "^19.1.0", 13 | "@angular/material": "^19.1", 14 | "rxjs": "^7.8.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/src/lib/fab-speed-dial.scss: -------------------------------------------------------------------------------- 1 | @mixin eco-fab-speed-dial-actions($flex-direction, $order, $action-item-margin-direction) { 2 | flex-direction: $flex-direction; 3 | order: $order; 4 | 5 | & .eco-fab-action-item { 6 | margin-#{$action-item-margin-direction}: 10px; 7 | } 8 | } 9 | 10 | eco-fab-speed-dial { 11 | display: inline-block; 12 | 13 | &.eco-opened { 14 | .eco-fab-speed-dial-container { 15 | eco-fab-speed-dial-trigger.eco-spin { 16 | .spin180 { 17 | transform: rotate(180deg); 18 | } 19 | 20 | .spin360 { 21 | transform: rotate(360deg); 22 | } 23 | } 24 | } 25 | } 26 | 27 | .eco-fab-speed-dial-container { 28 | position: relative; 29 | display: flex; 30 | align-items: center; 31 | z-index: 20; 32 | 33 | eco-fab-speed-dial-trigger { 34 | pointer-events: auto; 35 | z-index: 24; 36 | 37 | &.eco-spin { 38 | .spin180, 39 | .spin360 { 40 | transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); 41 | } 42 | } 43 | } 44 | 45 | eco-fab-speed-dial-actions { 46 | display: flex; 47 | position: absolute; 48 | height: 0; 49 | width: 0; 50 | 51 | & .eco-fab-action-item { 52 | transform: scale(0); 53 | transition: all 0.3s cubic-bezier(0.55, 0, 0.55, 0.2); 54 | transition-duration: 0.14286s; 55 | } 56 | } 57 | } 58 | 59 | &.eco-down { 60 | eco-fab-speed-dial-actions { 61 | bottom: 2px; 62 | left: 7px; 63 | } 64 | 65 | .eco-fab-speed-dial-container { 66 | flex-direction: column; 67 | 68 | & eco-fab-speed-dial-trigger { 69 | order: 1; 70 | } 71 | 72 | & eco-fab-speed-dial-actions { 73 | @include eco-fab-speed-dial-actions(column, 2, top); 74 | } 75 | } 76 | } 77 | 78 | &.eco-up { 79 | eco-fab-speed-dial-actions { 80 | top: 2px; 81 | left: 7px; 82 | } 83 | 84 | .eco-fab-speed-dial-container { 85 | flex-direction: column; 86 | 87 | & eco-fab-speed-dial-trigger { 88 | order: 2; 89 | } 90 | 91 | & eco-fab-speed-dial-actions { 92 | @include eco-fab-speed-dial-actions(column-reverse, 1, bottom); 93 | } 94 | } 95 | } 96 | 97 | &.eco-left { 98 | eco-fab-speed-dial-actions { 99 | top: 7px; 100 | left: 2px; 101 | } 102 | 103 | .eco-fab-speed-dial-container { 104 | flex-direction: row; 105 | 106 | & eco-fab-speed-dial-trigger { 107 | order: 2; 108 | } 109 | 110 | & eco-fab-speed-dial-actions { 111 | @include eco-fab-speed-dial-actions(row-reverse, 1, right); 112 | } 113 | } 114 | } 115 | 116 | &.eco-right { 117 | eco-fab-speed-dial-actions { 118 | top: 7px; 119 | right: 2px; 120 | } 121 | 122 | .eco-fab-speed-dial-container { 123 | flex-direction: row; 124 | 125 | & eco-fab-speed-dial-trigger { 126 | order: 1; 127 | } 128 | 129 | & eco-fab-speed-dial-actions { 130 | @include eco-fab-speed-dial-actions(row, 2, left); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/src/lib/fab-speed-dial.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | import { 3 | Direction, 4 | EcoFabSpeedDialActionsComponent, 5 | EcoFabSpeedDialComponent, 6 | EcoFabSpeedDialTriggerComponent, 7 | } from './fab-speed-dial'; 8 | import {By} from '@angular/platform-browser'; 9 | import {Component, viewChild} from '@angular/core'; 10 | 11 | describe('FabSpeedDial', () => { 12 | it('should apply direction class based on direction', () => { 13 | const fixture = TestBed.createComponent(TestAppComponent); 14 | 15 | const testComponent = fixture.debugElement.componentInstance; 16 | const speedDialDebugElement = fixture.debugElement.query(By.css('eco-fab-speed-dial')); 17 | 18 | fixture.detectChanges(); 19 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-up')).toBeTruthy(); 20 | 21 | testComponent.direction = 'down'; 22 | fixture.detectChanges(); 23 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-down')).toBeTruthy(); 24 | 25 | testComponent.direction = 'right'; 26 | fixture.detectChanges(); 27 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-right')).toBeTruthy(); 28 | 29 | testComponent.direction = 'left'; 30 | fixture.detectChanges(); 31 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-left')).toBeTruthy(); 32 | // also check if the other class from before is removed 33 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-right')).toBeFalsy(); 34 | }); 35 | 36 | it('should apply opened class trigger button clicked', () => { 37 | const fixture = TestBed.createComponent(TestAppComponent); 38 | const speedDialDebugElement = fixture.debugElement.query(By.css('eco-fab-speed-dial')); 39 | const triggerButtonDebugElement = fixture.debugElement.query(By.css('eco-fab-speed-dial-trigger button')); 40 | fixture.detectChanges(); 41 | 42 | triggerButtonDebugElement.nativeElement.click(); 43 | fixture.detectChanges(); 44 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-opened')).toBeTruthy(); 45 | triggerButtonDebugElement.nativeElement.click(); 46 | 47 | fixture.detectChanges(); 48 | // check if the class is removed afterwards 49 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-opened')).toBeFalsy(); 50 | }); 51 | 52 | it('should apply opened class when property open is set', () => { 53 | const fixture = TestBed.createComponent(TestAppComponent); 54 | 55 | const testComponent = fixture.debugElement.componentInstance; 56 | const speedDialDebugElement = fixture.debugElement.query(By.css('eco-fab-speed-dial')); 57 | 58 | testComponent.open = true; 59 | fixture.detectChanges(); 60 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-opened')).toBeTruthy(); 61 | testComponent.open = false; 62 | fixture.detectChanges(); 63 | // check if the class is removed afterwards 64 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-opened')).toBeFalsy(); 65 | }); 66 | 67 | it('should close when action button is clicked', () => { 68 | const fixture = TestBed.createComponent(TestAppComponent); 69 | 70 | const testComponent = fixture.debugElement.componentInstance; 71 | const speedDialDebugElement = fixture.debugElement.query(By.css('eco-fab-speed-dial')); 72 | 73 | testComponent.open = true; 74 | fixture.detectChanges(); 75 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-opened')).toBeTruthy(); 76 | 77 | const actionButton = fixture.debugElement.query(By.css('eco-fab-speed-dial-actions button:first-child')); 78 | actionButton.nativeElement.click(); 79 | fixture.detectChanges(); 80 | // check if the class is removed after click 81 | expect(speedDialDebugElement.nativeElement.classList.contains('eco-opened')).toBeFalsy(); 82 | }); 83 | 84 | it('should call "show" method of all fabActions', () => { 85 | const fixture = TestBed.createComponent(TestAppComponent); 86 | const testComponent = fixture.debugElement.componentInstance; 87 | fixture.detectChanges(); 88 | 89 | spyOn(fixture.componentInstance.fabSpeedDial(), 'setActionsVisibility').and.callThrough(); 90 | spyOn(fixture.componentInstance.fabActions(), 'show').and.callThrough(); 91 | 92 | testComponent.open = true; 93 | fixture.detectChanges(); 94 | 95 | expect(fixture.componentInstance.fabSpeedDial().setActionsVisibility).toHaveBeenCalled(); 96 | expect(fixture.componentInstance.fabActions().show).toHaveBeenCalled(); 97 | }); 98 | 99 | it('should click on document testElement to hide all fabActions', () => { 100 | const fixture = TestBed.createComponent(TestAppComponent); 101 | const testComponent = fixture.debugElement.componentInstance; 102 | fixture.detectChanges(); 103 | 104 | const actionsSpy = spyOn(fixture.componentInstance.fabSpeedDial(), 'setActionsVisibility').and.callThrough(); 105 | spyOn(fixture.componentInstance.fabActions(), 'show').and.callThrough(); 106 | spyOn(fixture.componentInstance.fabActions(), 'hide').and.callThrough(); 107 | 108 | testComponent.open = true; 109 | fixture.detectChanges(); 110 | 111 | expect(fixture.componentInstance.fabSpeedDial().setActionsVisibility).toHaveBeenCalled(); 112 | expect(fixture.componentInstance.fabActions().show).toHaveBeenCalled(); 113 | actionsSpy.calls.reset(); 114 | 115 | const actionButton = fixture.debugElement.query(By.css('.testElement')); 116 | actionButton.nativeElement.click(); 117 | fixture.detectChanges(); 118 | 119 | expect(fixture.componentInstance.fabSpeedDial().setActionsVisibility).toHaveBeenCalled(); 120 | expect(fixture.componentInstance.fabActions().hide).toHaveBeenCalled(); 121 | }); 122 | }); 123 | 124 | /** Test component that contains an fab speed dial buttons */ 125 | @Component({ 126 | template: ` 127 |
128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 |
Test element
140 |
141 | `, 142 | imports: [EcoFabSpeedDialActionsComponent, EcoFabSpeedDialTriggerComponent, EcoFabSpeedDialComponent], 143 | }) 144 | class TestAppComponent { 145 | public readonly fabActions = viewChild.required(EcoFabSpeedDialActionsComponent); 146 | public readonly fabSpeedDial = viewChild.required(EcoFabSpeedDialComponent); 147 | public direction: Direction = 'up'; 148 | public open = false; 149 | } 150 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/src/lib/fab-speed-dial.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | computed, 4 | contentChild, 5 | contentChildren, 6 | effect, 7 | ElementRef, 8 | inject, 9 | input, 10 | model, 11 | OnDestroy, 12 | output, 13 | Renderer2, 14 | ViewEncapsulation, 15 | } from '@angular/core'; 16 | import {MatMiniFabAnchor, MatMiniFabButton} from '@angular/material/button'; 17 | import {DOCUMENT} from '@angular/common'; 18 | import {forkJoin, fromEvent, Subscription} from 'rxjs'; 19 | import {take} from 'rxjs/operators'; 20 | 21 | const Z_INDEX_ITEM = 23; 22 | 23 | export type Direction = 'up' | 'down' | 'left' | 'right'; 24 | 25 | type MiniFab = MatMiniFabButton | MatMiniFabAnchor; 26 | 27 | function getHostElement(miniFab: MiniFab): HTMLButtonElement | HTMLAnchorElement { 28 | return miniFab._elementRef.nativeElement; 29 | } 30 | 31 | @Component({ 32 | selector: 'eco-fab-speed-dial-actions', 33 | template: `@if (miniFabVisible) { 34 | 35 | }`, 36 | }) 37 | export class EcoFabSpeedDialActionsComponent { 38 | private readonly renderer = inject(Renderer2); 39 | 40 | private readonly parent = inject(EcoFabSpeedDialComponent); 41 | 42 | private readonly buttons = contentChildren(MatMiniFabButton); 43 | private readonly anchors = contentChildren(MatMiniFabAnchor); 44 | private readonly miniFabs = computed(() => [...this.buttons(), ...this.anchors()]); 45 | 46 | private readonly initMiniFabStates = effect(() => { 47 | this.miniFabs().forEach((matMini, i) => { 48 | const hostElement = getHostElement(matMini); 49 | this.renderer.addClass(hostElement, 'eco-fab-action-item'); 50 | this.changeElementStyle(hostElement, 'z-index', '' + (Z_INDEX_ITEM - i).toString()); 51 | }); 52 | 53 | this.parent.setActionsVisibility(); 54 | }); 55 | 56 | /** 57 | * Whether the mini-fab exist in DOM 58 | */ 59 | protected miniFabVisible = false; 60 | 61 | /** 62 | * The timeout ID for the callback to show the mini-fabs 63 | */ 64 | private showMiniFabAnimation: ReturnType | undefined; 65 | 66 | /** 67 | * When we will remove mini-fab from DOM, after the animation is complete 68 | */ 69 | private hideMiniFab: Subscription | null = null; 70 | 71 | public show(): void { 72 | this.resetAnimationState(); 73 | this.miniFabVisible = true; 74 | 75 | this.showMiniFabAnimation = setTimeout(() => { 76 | this.miniFabs().forEach((miniFab, i) => { 77 | const hostElement = getHostElement(miniFab); 78 | 79 | this.changeElementStyle(hostElement, 'transition-delay', this.transitionDelay(i)); 80 | this.changeElementStyle(hostElement, 'transform', 'scale(1)'); 81 | }); 82 | }, 50); // Be sure that @if can show elements before trying to animate them 83 | } 84 | 85 | private resetAnimationState(): void { 86 | clearTimeout(this.showMiniFabAnimation); 87 | if (this.hideMiniFab) { 88 | this.hideMiniFab.unsubscribe(); 89 | this.hideMiniFab = null; 90 | } 91 | } 92 | 93 | public hide(): void { 94 | this.resetAnimationState(); 95 | 96 | const miniFabs = this.miniFabs(); 97 | if (!miniFabs.length) { 98 | this.miniFabVisible = false; 99 | return; 100 | } 101 | 102 | const obs = [...miniFabs].reverse().map((miniFab, i) => { 103 | const hostElement = getHostElement(miniFab); 104 | 105 | this.changeElementStyle(hostElement, 'transition-delay', this.transitionDelay(i)); 106 | this.changeElementStyle(hostElement, 'transform', 'scale(0)'); 107 | 108 | return fromEvent(hostElement, 'transitionend').pipe(take(1)); 109 | }); 110 | 111 | // Wait for all animations to finish, then destroy their elements 112 | this.hideMiniFab = forkJoin(obs).subscribe(() => (this.miniFabVisible = false)); 113 | } 114 | 115 | private transitionDelay(i: number): string { 116 | const total = 100; // Maximum of 100 ms seconds for all cumulated delays 117 | 118 | const length = this.miniFabs().length; 119 | const transitionDelayOne = length ? total / length : 0; 120 | const transitionDelay = transitionDelayOne * i; 121 | 122 | return transitionDelay.toString() + 'ms'; 123 | } 124 | 125 | private changeElementStyle(elem: HTMLElement, style: string, value: string): void { 126 | this.renderer.setStyle(elem, style, value); 127 | } 128 | } 129 | 130 | /** @dynamic @see https://github.com/angular/angular/issues/20351#issuecomment-344009887 */ 131 | @Component({ 132 | selector: 'eco-fab-speed-dial', 133 | template: ` 134 |
135 | 136 | 137 |
138 | `, 139 | styleUrl: './fab-speed-dial.scss', 140 | // eslint-disable-next-line @angular-eslint/use-component-view-encapsulation 141 | encapsulation: ViewEncapsulation.None, 142 | host: { 143 | '[class.eco-opened]': 'open()', 144 | '(click)': 'onClick()', 145 | }, 146 | }) 147 | export class EcoFabSpeedDialComponent implements OnDestroy { 148 | private readonly elementRef = inject>(ElementRef); 149 | private readonly renderer = inject(Renderer2); 150 | private readonly document = inject(DOCUMENT); 151 | 152 | private documentClickUnlistener: (() => void) | null = null; 153 | 154 | /** 155 | * Whether this speed dial is opened 156 | */ 157 | public readonly open = model(false); 158 | private readonly processOpen = effect(() => { 159 | this.openChange.emit(this.open()); 160 | this.setActionsVisibility(); 161 | }); 162 | 163 | /** 164 | * The direction of the speed dial. Can be 'up', 'down', 'left' or 'right' 165 | */ 166 | public readonly direction = input('up'); 167 | private previousDirection: Direction = this.direction(); 168 | private readonly processDirection = effect(() => { 169 | this.setElementClass(this.previousDirection, false); 170 | this.setElementClass(this.direction(), true); 171 | this.previousDirection = this.direction(); 172 | 173 | this.setActionsVisibility(); 174 | }); 175 | 176 | public readonly openChange = output(); 177 | 178 | private readonly childActions = contentChild.required(EcoFabSpeedDialActionsComponent); 179 | 180 | public ngOnDestroy(): void { 181 | this.unsetDocumentClickListener(); 182 | } 183 | 184 | /** 185 | * Toggle the open state of this speed dial 186 | */ 187 | public toggle(): void { 188 | this.open.update(open => !open); 189 | } 190 | 191 | protected onClick(): void { 192 | if (this.open()) { 193 | this.open.set(false); 194 | } 195 | } 196 | 197 | public setActionsVisibility(): void { 198 | if (this.open()) { 199 | this.childActions().show(); 200 | } else { 201 | this.childActions().hide(); 202 | } 203 | this.processOutsideClickState(); 204 | } 205 | 206 | private setElementClass(elemClass: string, isAdd: boolean): void { 207 | const finalClass = `eco-${elemClass}`; 208 | if (isAdd) { 209 | this.renderer.addClass(this.elementRef.nativeElement, finalClass); 210 | } else { 211 | this.renderer.removeClass(this.elementRef.nativeElement, finalClass); 212 | } 213 | } 214 | 215 | private processOutsideClickState(): void { 216 | if (this.open()) { 217 | this.setDocumentClickListener(); 218 | } else { 219 | this.unsetDocumentClickListener(); 220 | } 221 | } 222 | 223 | private setDocumentClickListener(): void { 224 | if (!this.documentClickUnlistener) { 225 | this.documentClickUnlistener = this.renderer.listen(this.document, 'click', () => { 226 | this.open.set(false); 227 | }); 228 | } 229 | } 230 | 231 | private unsetDocumentClickListener(): void { 232 | if (this.documentClickUnlistener) { 233 | this.documentClickUnlistener(); 234 | this.documentClickUnlistener = null; 235 | } 236 | } 237 | } 238 | 239 | @Component({ 240 | selector: 'eco-fab-speed-dial-trigger', 241 | template: ` `, 242 | host: { 243 | '(click)': 'onClick($event)', 244 | '[class.eco-spin]': 'spin()', 245 | }, 246 | }) 247 | export class EcoFabSpeedDialTriggerComponent { 248 | private readonly parent = inject(EcoFabSpeedDialComponent); 249 | 250 | /** 251 | * Whether this trigger should spin (360dg) while opening the speed dial 252 | */ 253 | public readonly spin = input(false); 254 | 255 | protected onClick(event: Event): void { 256 | this.parent.toggle(); 257 | event.stopPropagation(); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of fab-speed-dial 3 | */ 4 | export { 5 | EcoFabSpeedDialActionsComponent, 6 | EcoFabSpeedDialComponent, 7 | EcoFabSpeedDialTriggerComponent, 8 | } from './lib/fab-speed-dial'; 9 | 10 | export type {Direction} from './lib/fab-speed-dial'; 11 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/lib", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "inlineSources": true, 10 | "types": [] 11 | }, 12 | "exclude": ["**/*.spec.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.lib.json", 5 | "compilerOptions": { 6 | "declarationMap": false 7 | }, 8 | "angularCompilerOptions": { 9 | "compilationMode": "partial" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/fab-speed-dial/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "../../tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "../../out-tsc/spec", 7 | "types": ["jasmine"] 8 | }, 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ecodev/fab-speed-dial/7775f92464d53e5608ce7ad1863e6996a73b9615/public/favicon.ico -------------------------------------------------------------------------------- /public/github-circle-transparent.svg: -------------------------------------------------------------------------------- 1 | github-circle-white-transparent 2 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | FAB Speed Dial 4 | 5 | GitHub 8 | 9 | 10 | 11 | 12 | 13 | Options 14 | 15 | 16 | 17 |

18 | Direction: 19 | 20 | Up 21 | Down 22 | Left 23 | Right 24 | 25 |

26 |

27 | Enable Spinning 28 |

29 |
30 |
31 | 32 |
33 | 34 | 35 | Click me 36 | 37 | 38 | 39 | Open 40 | 41 | 42 | 45 | 46 | 47 | 48 | 51 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 70 | Hover me 71 | 72 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | 85 | 88 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | 100 | 101 | 102 | 105 | 108 | 111 | 114 | 117 | 118 | 119 | 120 | 121 | 122 | 125 | 126 | 127 | 128 | 131 | 134 | 137 | 140 | 143 | 146 | 149 | 152 | 155 | 158 | 161 | 164 | 167 | 170 | 173 | 174 | 175 | 176 | 177 |
178 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | mat-card { 2 | margin: 15px; 3 | } 4 | 5 | .container-fab-demo { 6 | width: 100%; 7 | display: flex; 8 | flex-direction: row; 9 | flex-wrap: wrap; 10 | justify-content: flex-start; 11 | align-content: flex-start; 12 | align-items: flex-start; 13 | 14 | & mat-card { 15 | height: 300px; 16 | padding: 20px; 17 | flex-grow: 1; 18 | flex-shrink: 0; 19 | flex-basis: 300px; 20 | } 21 | } 22 | 23 | .example-spacer { 24 | flex: 1 1 auto; 25 | } 26 | 27 | .github-logo { 28 | height: 26px; 29 | margin: 0 4px 3px 0; 30 | vertical-align: middle; 31 | } 32 | 33 | eco-fab-speed-dial { 34 | margin: 1em; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {TestBed} from '@angular/core/testing'; 2 | import {AppComponent} from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent], 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import { 3 | Direction, 4 | EcoFabSpeedDialActionsComponent, 5 | EcoFabSpeedDialComponent, 6 | EcoFabSpeedDialTriggerComponent, 7 | } from '@ecodev/fab-speed-dial'; 8 | import {MatIconModule} from '@angular/material/icon'; 9 | import {MatSlideToggleModule} from '@angular/material/slide-toggle'; 10 | import {FormsModule} from '@angular/forms'; 11 | import {MatRadioModule} from '@angular/material/radio'; 12 | import {MatCardModule} from '@angular/material/card'; 13 | import {MatButtonModule} from '@angular/material/button'; 14 | import {MatToolbarModule} from '@angular/material/toolbar'; 15 | 16 | @Component({ 17 | selector: 'app-root', 18 | templateUrl: './app.component.html', 19 | styleUrl: './app.component.scss', 20 | imports: [ 21 | MatToolbarModule, 22 | MatButtonModule, 23 | MatCardModule, 24 | MatRadioModule, 25 | FormsModule, 26 | MatSlideToggleModule, 27 | MatIconModule, 28 | EcoFabSpeedDialComponent, 29 | EcoFabSpeedDialTriggerComponent, 30 | EcoFabSpeedDialActionsComponent, 31 | ], 32 | }) 33 | export class AppComponent { 34 | public open = false; 35 | public spin = false; 36 | public direction: Direction = 'up'; 37 | 38 | public stopPropagation(event: Event): void { 39 | // Prevent the click to propagate to document and trigger 40 | // the FAB to be closed automatically right before we toggle it ourselves 41 | event.stopPropagation(); 42 | } 43 | 44 | public doAction(event: string): void { 45 | console.log(event); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core'; 2 | 3 | export const appConfig: ApplicationConfig = { 4 | providers: [provideZoneChangeDetection({eventCoalescing: true})], 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FAB Speed Dial - Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import {bootstrapApplication} from '@angular/platform-browser'; 2 | import {appConfig} from './app/app.config'; 3 | import {AppComponent} from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err: unknown) => console.error(err)); 6 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | background-color: #fafafa; 7 | } 8 | body { 9 | margin: 0; 10 | font-family: Roboto, 'Helvetica Neue', sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": ["src/main.ts"], 10 | "include": ["src/**/*.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "paths": { 9 | "@ecodev/fab-speed-dial": ["./projects/fab-speed-dial/src/public-api"] 10 | }, 11 | "noImplicitOverride": true, 12 | "noPropertyAccessFromIndexSignature": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "skipLibCheck": true, 16 | "isolatedModules": true, 17 | "experimentalDecorators": true, 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "preserve" 21 | }, 22 | "angularCompilerOptions": { 23 | "strictStandalone": true, 24 | "enableI18nLegacyMessageIdFormat": false, 25 | "strictInjectionParameters": true, 26 | "strictInputAccessModifiers": true, 27 | "strictTemplates": true, 28 | "extendedDiagnostics": { 29 | "defaultCategory": "error" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": ["jasmine"] 8 | }, 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | --------------------------------------------------------------------------------