diff --git a/.github/actions/deploy-storybook.sh b/.github/actions/deploy-storybook.sh deleted file mode 100755 index 89422733f..000000000 --- a/.github/actions/deploy-storybook.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -e - -#branch=$1 -#folder=$(echo "$branch" | tr / -) - -folder=$1 - -FOLDER=$folder npm run aws:deploy-storybook - -FOLDER=$folder npm run cf:invalidate - -exit $lastCode diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml deleted file mode 100644 index 176851697..000000000 --- a/.github/workflows/deploy-storybook.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Storybook Deploy - -on: - workflow_dispatch: - -permissions: - id-token: write - contents: read - -jobs: - Deploy: - runs-on: [self-hosted, x64, linux, eks, us, demand-amd64, dev] - outputs: - branch_name: ${{ steps.branch_name.outputs.branch }} - folder_name: ${{ steps.branch_name.outputs.folder }} - steps: - - uses: actions/checkout@v3 - - name: Setup Node.js Enviroment 🛠️ - uses: actions/setup-node@v3 - with: - node-version: '20' - cache: 'npm' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y vim jq - - - name: Cache node deps - id: cache-deps - uses: actions/cache@v3 - with: - path: ./node_modules - key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }} - - - name: Install Dependencies 📦 - if: steps.cache-deps.outputs.cache-hit != 'true' - run: npm ci - - - name: Extract branch name - shell: bash - run: | - echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT - echo "folder=$(echo ${GITHUB_REF#refs/heads/} | tr / -)" >> $GITHUB_OUTPUT - id: branch_name - - - - name: Test Output - run: | - echo "Folder: ${{ steps.branch_name.outputs.folder }}" - - - name: Build storybook static - run: npm run build-storybook - - - name: Install AWS cli - run: | - if ! [ -x "$(command -v aws)" ]; then - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.0.30.zip" -o "awscliv2.zip" - unzip awscliv2.zip - sudo ./aws/install - fi - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-region: us-east-1 - role-to-assume: arn:aws:iam::032106861074:role/github-runners-demand-team-role - - - name: Deploy to s3 bucket - run: '${GITHUB_WORKSPACE}/.github/actions/deploy-storybook.sh ${{ steps.branch_name.outputs.folder }} ' - - notify-success: - needs: Deploy - if: ${{ always() && needs.Deploy.result == 'success'}} - runs-on: ubuntu-latest - steps: - - - name: 'Deploy success notify' - uses: ironSource/action-slack-notification@v1 - with: - channel: fusion-docs-notifications - username: FusionUI-CI - icon_url: "https://avatars.githubusercontent.com/t/5433436?s=32&v=4" - message: " Storybook on branch *${{ needs.Deploy.outputs.branch_name }}* by *${{github.actor}}* published success.\n You can check it on: https://fusion-storybook.ironsrc.mobi/branch_${{ needs.Deploy.outputs.folder_name }}/" - slack_webhook: ${{ secrets.SLACK_PATH }} diff --git a/.gitignore b/.gitignore index a47fed4b1..465629f7a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ speed-measure-plugin.json # IDEs and editors /.idea +projects/fusion-e2e/.idea .project .classpath .c9/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8624299d6..f18d4cde8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,117 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## [8.4.0](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.11...v8.4.0) (2024-10-21) + +## [8.4.0-rc.11](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.10...v8.4.0-rc.11) (2024-10-15) + +## [8.4.0-rc.10](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.9...v8.4.0-rc.10) (2024-10-15) + + +### CI + +* **runners:** change runners ([a888bf1](https://github.com/ironSource/fusion-ui/commit/a888bf11401f81bae53e589ac93b5d27b7ea4707)) +* **runners:** change runners ([ebbace9](https://github.com/ironSource/fusion-ui/commit/ebbace94be246610c767d6cea5f177035a043c7a)) +* **runners:** change runners ([05b5b75](https://github.com/ironSource/fusion-ui/commit/05b5b7540a26ba9adc052e52c8521340a1b0ff4d)) +* **storybook:** remove storybook deploy related code ([ecb8697](https://github.com/ironSource/fusion-ui/commit/ecb86975d16a807ec92b246fea5437a30019acb9)) + +## [8.4.0-rc.9](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.8...v8.4.0-rc.9) (2024-10-13) + +## [8.4.0-rc.8](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.7...v8.4.0-rc.8) (2024-10-13) + + +### Bug Fixes + +* **isct-428:** update inline-input v1 in table v1 (with error icon) ([ca88a67](https://github.com/ironSource/fusion-ui/commit/ca88a67f5f15e285ad16024de91fac11f5329365)) + +## [8.4.0-rc.7](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.6...v8.4.0-rc.7) (2024-10-10) + + +### Code Refactoring + +* **tooltip-table-v1:** revert tooltip changes, update table header template ([4ef5e92](https://github.com/ironSource/fusion-ui/commit/4ef5e925fd857d9d4b9ea6b33625a6ec825f771e)) + +## [8.4.0-rc.6](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.5...v8.4.0-rc.6) (2024-10-10) + + +### Features + +* **isct-381:** added dropped-list component ([3343eb0](https://github.com/ironSource/fusion-ui/commit/3343eb0f0df90dbe577f2f1c9f5f036e31fd7fc1)) +* **isct-381:** added text-with-dropped-list component ([452c8d2](https://github.com/ironSource/fusion-ui/commit/452c8d20cf5dc964fd5a88126fe8f7d402b90ff0)) +* **isct-381:** added unit tests for text-with-dropped-list component ([6985d43](https://github.com/ironSource/fusion-ui/commit/6985d43db3ae68ce577710bc904e65c62fbfb26a)) +* **table-v1:** add support to using custom component in table v1 header tooltip ([ae719f6](https://github.com/ironSource/fusion-ui/commit/ae719f64049cc450d27ed1e71d1f9bb6102e5844)) +* **tooltip:** add support for custom component using in tooltip v1 (base) ([ad30d35](https://github.com/ironSource/fusion-ui/commit/ad30d35b2d6c46d43024d188162e7d3aa7996a08)) + + +### CI + +* **isct-381:** added dropped-list component unit-tests ([0c03389](https://github.com/ironSource/fusion-ui/commit/0c033897b41b02ad991179958520f148a818d419)) + + +### Docs + +* **table-v1-doc:** add example for using custom component in table header tooltip ([9ec2353](https://github.com/ironSource/fusion-ui/commit/9ec2353541c0472cd09ab1a69697f628d6fd4b08)) + +## [8.4.0-rc.5](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.4...v8.4.0-rc.5) (2024-10-09) + + +### Features + +* **isct-381:** text with dropped list component ([#348](https://github.com/ironSource/fusion-ui/issues/348)) ([8618249](https://github.com/ironSource/fusion-ui/commit/86182499f5c7ade31093cca37d18e1577c17a809)) + + +### Bug Fixes + +* **tooltip:** fixed tooltip on cycle close/open on element mouse out when mouse on element border ([#349](https://github.com/ironSource/fusion-ui/issues/349)) ([f31be22](https://github.com/ironSource/fusion-ui/commit/f31be222d8cf3ad67ea769e602020f52f11268af)) + +## [8.4.0-rc.4](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.3...v8.4.0-rc.4) (2024-10-08) + +## [8.4.0-rc.3](https://github.com/ironSource/fusion-ui/compare/v8.3.0...v8.4.0-rc.3) (2024-10-08) + + +### Features + +* **isct-159:** added base application-trigger v4 component ([c377612](https://github.com/ironSource/fusion-ui/commit/c377612516eeecb124958e366fdf58fa90129f24)) +* **isct-367:** create app header component ([46b05e7](https://github.com/ironSource/fusion-ui/commit/46b05e7b3a688f2d53058428bfe4d1913a6864ba)) +* **ISCT-369:** Inline copy component ([#344](https://github.com/ironSource/fusion-ui/issues/344)) ([eacdb5b](https://github.com/ironSource/fusion-ui/commit/eacdb5b2f5f5bc1b1d6c0df134c9fc87c5f154c1)) +* **isct-374:** added drag-and-drop directive and draggable-items-list component ([1f073cb](https://github.com/ironSource/fusion-ui/commit/1f073cb2aef7dae50ca619e49617f6df0a241e2e)) + +## [8.4.0-rc.2](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.1...v8.4.0-rc.2) (2024-08-25) + +## [8.4.0-rc.1](https://github.com/ironSource/fusion-ui/compare/v8.4.0-rc.0...v8.4.0-rc.1) (2024-08-14) + + +### Features + +* **isct-165:** added datepicker component v4 ([1861561](https://github.com/ironSource/fusion-ui/commit/18615618690c33149516f2d25f3f8430710f3c90)) +* **isct-166:** added daterange component v4 ([6b7197f](https://github.com/ironSource/fusion-ui/commit/6b7197f7920a55a1701a2d3f6057cf6b3a2a90c8)) +* **isct-166:** added calendar component v4 ([285c4d8](https://github.com/ironSource/fusion-ui/commit/285c4d8d35cedcf6c1eb69be36b97b9a993c5cf2)) +* **isct-168:** remove hover from chip except remove icon ([d50ed86](https://github.com/ironSource/fusion-ui/commit/d50ed8667ce19af9b66ff2e82fda80cd38adaf6e)) + + +### Bug Fixes + +* **trigger-button:** up stories for button trigger v4 ([99beca3](https://github.com/ironSource/fusion-ui/commit/99beca30c176b591acb5528a7fc376d1e8500f9c)) +* **trigger-button:** v4 sync with vue ([9f9843c](https://github.com/ironSource/fusion-ui/commit/9f9843ca5ce25e1aa2807fdd1a9c662554fbeb33)) + +## [8.4.0-rc.0](https://github.com/ironSource/fusion-ui/compare/v8.3.0...v8.4.0-rc.0) (2024-08-01) + + +### Features + +* **isct-161:** added empty-state component v4 ([8959503](https://github.com/ironSource/fusion-ui/commit/89595036e08c53cac7eebd98a13cbb0dde67939c)) +* **isct-161:** added empty-state component v4 unit-tests ([a114b1a](https://github.com/ironSource/fusion-ui/commit/a114b1a0c2f767fd05a52d5c76eadb9c865d423c)) +* **isct-161:** added skeleton component v4 unit-tests ([0bf6dd6](https://github.com/ironSource/fusion-ui/commit/0bf6dd6bae01207ee84bd48d69fc659a8727472b)) +* **isct-163:** added skeleton component v4 ([c63aba1](https://github.com/ironSource/fusion-ui/commit/c63aba1a7a139b882cd459182b2a925931cf0494)) +* **isct-163:** start with skeleton component ([83258ff](https://github.com/ironSource/fusion-ui/commit/83258ff2a32c50ca91a0f2405b575f03161c36dc)) +* **isct-168:** added chip component v4 ([3756856](https://github.com/ironSource/fusion-ui/commit/37568562e53950bc6251c1fb1cf8f008fba508a2)) +* **isct-241:** added new link (button) v4 component ([f6cdcdc](https://github.com/ironSource/fusion-ui/commit/f6cdcdc506d0ac12aa99a4fe06f56da52b174f6c)) + + +### Bug Fixes + +* **master:** remove console.log ([2da5f01](https://github.com/ironSource/fusion-ui/commit/2da5f0184686c6f3d122067c4dad63745dba4148)) + ## [8.3.0](https://github.com/ironSource/fusion-ui/compare/v8.3.0-rc.0...v8.3.0) (2024-07-25) ## [8.3.0-rc.0](https://github.com/ironSource/fusion-ui/compare/v8.2.1...v8.3.0-rc.0) (2024-07-25) diff --git a/package-lock.json b/package-lock.json index 51e5179ca..cdd93d863 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fusion-ui", - "version": "8.1.8", + "version": "8.4.0-rc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fusion-ui", - "version": "8.1.8", + "version": "8.4.0-rc.2", "license": "MIT", "dependencies": { "@angular-devkit/architect": "^0.1702.1", diff --git a/package.json b/package.json index b6a327c1d..b01362d47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fusion-ui", - "version": "8.3.0", + "version": "8.4.0", "description": "UI library created by ironSource", "license": "MIT", "repository": { @@ -31,12 +31,7 @@ "release": "commit-and-tag-version", "docs:show": "compodoc -p ./tsconfig.doc.json -s --disablePrivate --disableProtected --disableLifeCycleHooks --disableInternal", "storybook": "ng run fusion-ui:storybook", - "build-storybook": "ng run fusion-ui:build-storybook", - "aws:build-storybook": "npm run build-storybook", - "aws:deploy-storybook": "npm run deploy:static-storybook && npm run deploy:html-storybook", - "deploy:static-storybook": "aws s3 cp ./storybook-static s3://fusion-storybook/$FOLDER --recursive --exclude 'index.html'", - "deploy:html-storybook": "aws s3 cp ./storybook-static/index.html s3://fusion-storybook/$FOLDER/index.html --cache-control='max-age=0, s-maxage=604800'", - "cf:invalidate": "aws cloudfront create-invalidation --distribution-id E3LE7X89G2T5J3 --paths \"/$FOLDER/*\" " + "build-storybook": "ng run fusion-ui:build-storybook" }, "lint-staged": { "**/*{.ts,.json}": "prettier --config ./prettier.config.js --write" diff --git a/projects/fusion-docs/src/app/app.module.ts b/projects/fusion-docs/src/app/app.module.ts index 35927a04c..0b0671988 100644 --- a/projects/fusion-docs/src/app/app.module.ts +++ b/projects/fusion-docs/src/app/app.module.ts @@ -4,7 +4,7 @@ import {AppComponent} from './app.component'; import {AppRoutingModule} from './app.routing'; import {environment} from '../environments/environment'; import {SvgModule} from '@ironsource/fusion-ui/components/svg'; -import {TooltipModule} from '@ironsource/fusion-ui/components/tooltip/v2'; +import {TooltipModule} from '@ironsource/fusion-ui/components/tooltip/v1'; import {ReactiveFormsModule} from '@angular/forms'; import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; import {ModalModule} from '@ironsource/fusion-ui/components/modal'; diff --git a/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.html b/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.html new file mode 100644 index 000000000..489025c12 --- /dev/null +++ b/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.html @@ -0,0 +1,11 @@ +
+ To locate your custom product page ID,
do the following: +
    +
  1. Open your product page in a web browser.
  2. +
  3. Locate the product page ID at the end of the URL.
  4. +
  5. Copy the product page ID.
  6. +
+
+ diff --git a/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.scss b/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.scss new file mode 100644 index 000000000..aad711e13 --- /dev/null +++ b/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.scss @@ -0,0 +1,28 @@ +:host { + width: 328px; + height: 172px; + pointer-events: none; + border-radius: 3px; + background: #12243D; + color: #fff; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.30); + display: flex; + padding: 10px; + flex-direction: column; + justify-content: flex-end; + align-items: center; + gap: 10px; + align-self: stretch; + font-family: "Open Sans"; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; + + .exmpl-link{ + margin-bottom: 26px; + span{ + color: #3091F6; + } + } +} \ No newline at end of file diff --git a/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.spec.ts b/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.spec.ts new file mode 100644 index 000000000..c4510dada --- /dev/null +++ b/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExmlForTooltipComponent } from './exml-for-tooltip.component'; + +describe('ExmlForTooltipComponent', () => { + let component: ExmlForTooltipComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ExmlForTooltipComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ExmlForTooltipComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.ts b/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.ts new file mode 100644 index 000000000..dddc347e3 --- /dev/null +++ b/projects/fusion-docs/src/app/components/exml-for-tooltip/exml-for-tooltip.component.ts @@ -0,0 +1,12 @@ +import {Component, Input} from '@angular/core'; + +@Component({ + selector: 'fusion-exml-for-tooltip', + standalone: true, + imports: [], + templateUrl: './exml-for-tooltip.component.html', + styleUrl: './exml-for-tooltip.component.scss' +}) +export class ExmlForTooltipComponent { + @Input() ppid: string; +} diff --git a/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.component.html b/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.component.html index 83ee23eb9..e7c539666 100644 --- a/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.component.html +++ b/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.component.html @@ -3,13 +3,13 @@
Type
- ' > Table - Example

Table Component example

@@ -184,9 +184,9 @@

Table - Example

> Table - Example { // define data const tableColumns = [ { key: "id", title: "Id", sort: "asc" }, @@ -336,257 +336,199 @@

Table - Example

document.addEventListener("DOMContentLoaded", onLoad, false);' >
- <!–code examples by framework–> + Server Error! Something wrong.... Try again later. + >Server Error! Something wrong.... Try again later. +
+ - - - - - - - - -
Configuration
- need to update this. - - - + + + + + + + +
+ +
+
+ + + - isRemoveIconHiddenForRow?(row: any): boolean; - infoIconForRowOnHover?(row: any): string; -} + + + +
Grouped Table
+ + + -interface TablePaginationOption { - enable: boolean; - loading?: boolean; - handleLoadingFromHost?: boolean; -} + + + <!–UI-Component Examples–> + <!–UI-Component Configuration Doc–> +
Configuration
+ need to update this. + + + - + enum TableColumnTypeEnum { + String, + Component, + Checkbox, + ToggleButton, + InputEdit, + Date, + Currency, + Number, + Percent + } - - - + enum TableRowClassesEnum { + Selected = 'is-row-selected', + Disabled = 'is-row-disabled', + Loading = 'is-row-loading' + }" + >
+ + + + + + -->
diff --git a/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.component.ts b/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.component.ts index 61cb47217..8aebc79e5 100644 --- a/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.component.ts +++ b/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.component.ts @@ -7,7 +7,7 @@ import { TableRowExpandEmitter } from '@ironsource/fusion-ui/components/table/common/entities'; import {InlineInputType} from '@ironsource/fusion-ui/components/input-inline/common/base'; -import {FormControl, Validators} from '@angular/forms'; +import {AbstractControl, FormControl, Validators} from '@angular/forms'; import {BehaviorSubject, Observable, of, Subject} from 'rxjs'; import {delay, take, takeUntil, tap} from 'rxjs/operators'; import {TableCellIconExampleComponent} from '../../../components/table-cell-icon-exmpale'; @@ -16,11 +16,14 @@ import {Router} from '@angular/router'; import {TableCustomNoDataComponent} from '../../../components/table-custom-no-data/table-custom-no-data.component'; import {VersionService} from '../../../services/version/version.service'; import {StyleVersion} from '@ironsource/fusion-ui/components/fusion-base'; +import {ExmlForTooltipComponent} from '../../../components/exml-for-tooltip/exml-for-tooltip.component'; +import {TooltipCustom} from '@ironsource/fusion-ui/components/tooltip/common/base'; const tblOptions: TableOptions = { sortingType: 'local', remove: {active: true, onRemove: new EventEmitter()} }; + const tblColumns: Array = [ {key: 'checkbox', type: TableColumnTypeEnum.Checkbox, width: '35px'}, {key: 'id', title: 'Id', sort: 'asc'}, // 'asc' | 'desc' | '' @@ -31,11 +34,18 @@ const tblColumns: Array = [ key: 'bid', type: TableColumnTypeEnum.InputEdit, inputType: InlineInputType.Currency, + inputErrorIconShow: true, currencyPipeParameters: { digitsInfo: '1.0-3' }, customErrorMapping: { - required: {errorMessageKey: 'required'}, + invalidBid: { + errorMessageKey: 'invalidBid', + errorText: 'Invalid bid value' + }, + required: { + errorMessageKey: 'required' + }, min: { errorMessageKey: 'min', textMapping: [{key: 'minValue', value: '10'}] @@ -43,7 +53,12 @@ const tblColumns: Array = [ }, title: 'Bid', width: '85px', - tooltip: 'Lorem ipsum dolor sit amet', + tooltipCustom: { + content: { + component: ExmlForTooltipComponent as Type, + dataInputs: {ppid: 'a0ad70e0-bc66-414c-901b-35a410cffd50A'} + } + } as TooltipCustom, align: 'right' }, {key: 'email', title: 'Email', sort: ''}, @@ -174,7 +189,7 @@ export class TableDocsComponent implements OnInit, OnDestroy { // for base table will NOT use select column columnsBasic: Array = tblColumns.filter(cel => cel.key !== 'checkbox'); - optionsBasicTotals = { + optionsBasicTotals: TableOptions = { sortingType: 'local', hasTotalsRow: true }; @@ -824,7 +839,7 @@ export class TableDocsComponent implements OnInit, OnDestroy { .subscribe( data => { this.rows = data.map((item, _index) => { - const fcBid = new FormControl(_index + 23, [Validators.required, Validators.min(10)]); + const fcBid = new FormControl(_index + 23, [Validators.required, Validators.min(10), this.customValidator]); return { checkbox: false, id: item.id, @@ -924,4 +939,8 @@ export class TableDocsComponent implements OnInit, OnDestroy { successCallback(); }, failedCallback); } + + customValidator(control: AbstractControl) { + return control.value === '123' ? {invalidBid: 'Bid is invalid'} : null; + } } diff --git a/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.module.ts b/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.module.ts index 60ca228b8..e5668f780 100644 --- a/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.module.ts +++ b/projects/fusion-docs/src/app/pages/components/table-docs/table-docs.module.ts @@ -11,6 +11,7 @@ import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; import {TableModule} from '@ironsource/fusion-ui/components/table/v1'; import {TableCellIconExampleModule} from '../../../components/table-cell-icon-exmpale'; import {TableCustomNoDataModule} from '../../../components/table-custom-no-data/table-custom-no-data.module'; +import {ExmlForTooltipComponent} from '../../../components/exml-for-tooltip/exml-for-tooltip.component'; const routes: Routes = [{path: '', component: TableDocsComponent}]; @@ -27,7 +28,8 @@ const routes: Routes = [{path: '', component: TableDocsComponent}]; IconModule, AlertModule, TableCellIconExampleModule, - TableCustomNoDataModule + TableCustomNoDataModule, + ExmlForTooltipComponent ] }) export class TableDocsModule {} diff --git a/projects/fusion-docs/src/app/pages/components/tooltip-docs/tooltip-docs.component.html b/projects/fusion-docs/src/app/pages/components/tooltip-docs/tooltip-docs.component.html index c76a68d16..43c1aa199 100644 --- a/projects/fusion-docs/src/app/pages/components/tooltip-docs/tooltip-docs.component.html +++ b/projects/fusion-docs/src/app/pages/components/tooltip-docs/tooltip-docs.component.html @@ -324,7 +324,7 @@
-
Tooltip on top
+
Tooltip on top
@@ -352,7 +352,6 @@
-
@@ -392,8 +391,8 @@ class="is-collapsible" code=" fusion-tooltip { - --tooltip-background-color: white; - --tooltip-text-color: grey; + --tooltip-background-color: white; + --tooltip-text-color: grey; } " >
@@ -433,7 +432,7 @@ - <!–UI-Component Configuration Doc–> +
Configuration
---> diff --git a/projects/fusion-docs/src/app/pages/components/tooltip-docs/tooltip-docs.component.ts b/projects/fusion-docs/src/app/pages/components/tooltip-docs/tooltip-docs.component.ts index 8c8f00cea..6246f0a23 100644 --- a/projects/fusion-docs/src/app/pages/components/tooltip-docs/tooltip-docs.component.ts +++ b/projects/fusion-docs/src/app/pages/components/tooltip-docs/tooltip-docs.component.ts @@ -1,4 +1,4 @@ -import {Component, ElementRef, OnInit, Renderer2} from '@angular/core'; +import {Component, ElementRef, OnInit, Renderer2, Type} from '@angular/core'; import {TooltipPosition, TooltipType} from '@ironsource/fusion-ui/components/tooltip/common/base'; import {DocsMenuItem} from '../../../components/docs-menu/docs-menu'; import {DocsLayoutService} from '../../docs/docs-layout.service'; @@ -6,6 +6,7 @@ import {BehaviorSubject} from 'rxjs'; import {delay, tap} from 'rxjs/operators'; import {VersionService} from '../../../services/version/version.service'; import {TableColumnTypeEnum} from '@ironsource/fusion-ui/components/table/common/entities'; +import {ExmlForTooltipComponent} from '../../../components/exml-for-tooltip/exml-for-tooltip.component'; @Component({ selector: 'fusion-tooltip-docs', @@ -15,7 +16,13 @@ import {TableColumnTypeEnum} from '@ironsource/fusion-ui/components/table/common export class TooltipDocsComponent implements OnInit { public tooltipPosition = TooltipPosition; private styleElement: HTMLStyleElement; + tooltipTypeHtml = TooltipType.Html; + + tooltipTypeComponent = TooltipType.Component; + tooltipCustomComponent = ExmlForTooltipComponent as Type; + tooltipCustomData = {ppid: 'a0ad70e0-bc66-414c-901b-35a410cffd50A'}; + rightMenu: DocsMenuItem[] = [ { title: 'Content', diff --git a/projects/fusion-ui/components/app-header/index.ts b/projects/fusion-ui/components/app-header/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/app-header/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/app-header/ng-package.json b/projects/fusion-ui/components/app-header/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/components/app-header/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/app-header/public-api.ts b/projects/fusion-ui/components/app-header/public-api.ts new file mode 100644 index 000000000..a86af1af2 --- /dev/null +++ b/projects/fusion-ui/components/app-header/public-api.ts @@ -0,0 +1 @@ +export * from './v4'; diff --git a/projects/fusion-ui/components/app-header/v4/app-header.component.html b/projects/fusion-ui/components/app-header/v4/app-header.component.html new file mode 100644 index 000000000..23043edd9 --- /dev/null +++ b/projects/fusion-ui/components/app-header/v4/app-header.component.html @@ -0,0 +1,13 @@ +
+ + +
+
{{appName}}
+ +
+ @if(abTest){ +
+ +
+ } +
diff --git a/projects/fusion-ui/components/app-header/v4/app-header.component.scss b/projects/fusion-ui/components/app-header/v4/app-header.component.scss new file mode 100644 index 000000000..81529b719 --- /dev/null +++ b/projects/fusion-ui/components/app-header/v4/app-header.component.scss @@ -0,0 +1,73 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; + + --fu-app-header-height: 48px; + --fu-app-header-gap: 8px; + --fu-app-header-app-image-border-radius: 6px; + --fu-app-header-app-name-color: var(--text-primary, #{$color-v4-text-primary}); + --fu-app-header-platform-icon-size: 24px; + --fu-app-header-platform-icon-color: var(--action-active, #{$color-v4-action-active}); + --fu-app-header-appkey-color: var(--text-secondary, #{$color-v4-text-secondary}); + --fu-app-header-ab-icon-size: 28px; + --fu-app-header-ab-border: solid 1px var(--common-divider, #{$color-v4-common-divider}); + --fu-app-header-ab-gap: 4px; + + + .fu-app-header-wrapper { + display: flex; + align-items: center; + + &.fu-size-small { + --fu-app-header-height: 32px; + --fu-app-header-gap: 4px; + --fu-app-header-platform-icon-size: 20px; + } + + gap: var(--fu-app-header-gap); + height: var(--fu-app-header-height); + + .fu-app-header-image { + border-radius: var(--fu-app-header-app-image-border-radius); + } + + .fu-app-header-platform { + @include size(var(--fu-app-header-platform-icon-size)); + color: var(--fu-app-header-platform-icon-color); + } + + .fu-app-header-content{ + .fu-app-header-name { + @extend %font-v4-heading-2; + color: var(--fu-app-header-app-name-color); + + &.truncate { + @extend %truncate-flex-child; + } + } + } + + .fu-app-header-ab{ + height: 100%; + border-left: var(--fu-app-header-ab-border); + margin-left: var(--fu-app-header-ab-gap); + padding-left: calc(var(--fu-app-header-ab-gap) * 2); + display: flex; + align-items: center; + .fu-app-header-ab-icon{ + @include size(var(--fu-app-header-ab-icon-size)); + } + } + + &.fu-size-small .fu-app-header-content { + display: flex; + align-items: center; + gap: var(--fu-app-header-gap); + .fu-app-header-name{ + @extend %font-v4-subtitle-1; + } + } + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/app-header/v4/app-header.component.spec.ts b/projects/fusion-ui/components/app-header/v4/app-header.component.spec.ts new file mode 100644 index 000000000..ee36875f8 --- /dev/null +++ b/projects/fusion-ui/components/app-header/v4/app-header.component.spec.ts @@ -0,0 +1,62 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgOptimizedImage} from '@angular/common'; +import {AppHeaderComponent} from './app-header.component'; + +const APP_IMAGE_SRC = 'https://fusion.ironsrc.net/assets/images/v4/app_mock/Poly_Dating.png'; +const APP_NAME = 'Poly Dating'; +const APP_PLATFORM = 'android'; +const APP_KEY = '12345678'; + + +describe('AppHeaderComponent', () => { + let component: AppHeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppHeaderComponent, NgOptimizedImage] + }).compileComponents(); + + fixture = TestBed.createComponent(AppHeaderComponent); + component = fixture.componentInstance; + component.appImageSrc = APP_IMAGE_SRC; + component.appName = APP_NAME; + component.platform = APP_PLATFORM; + component.appKey = APP_KEY; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should has wrapper element size medium by default', () => { + const wrapper = fixture.nativeElement.querySelector('.fu-app-header-wrapper'); + expect(wrapper).toBeTruthy(); + expect(wrapper.classList.contains('fu-size-medium')).toBeTruthy(); + }); + + it('should has application image', () => { + const appImage = fixture.nativeElement.querySelector('.fu-app-header-image'); + expect(appImage).toBeTruthy(); + expect(appImage.src).toBe(APP_IMAGE_SRC); + }); + + it('should has platform icon', () => { + const appPlatform = fixture.nativeElement.querySelector('.fu-app-header-platform'); + expect(appPlatform).toBeTruthy(); + expect(appPlatform.classList.contains(APP_PLATFORM)).toBeTruthy(); + }); + + it('should has app name', () => { + const appContent = fixture.nativeElement.querySelector('.fu-app-header-content'); + expect(appContent).toBeTruthy(); + expect(appContent.querySelector('.fu-app-header-name').textContent).toBe(APP_NAME); + }); + + it('should has app key with copy to clipboard icon', () => { + const appKey = fixture.nativeElement.querySelector('fusion-inline-copy'); + expect(appKey.querySelector('.fu-inline-copy-text').textContent).toBe(APP_KEY); + expect(appKey.querySelector('.fu-inline-copy-icon').classList.contains('copy')).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/app-header/v4/app-header.component.stories.ts b/projects/fusion-ui/components/app-header/v4/app-header.component.stories.ts new file mode 100644 index 000000000..53aed1e4d --- /dev/null +++ b/projects/fusion-ui/components/app-header/v4/app-header.component.stories.ts @@ -0,0 +1,46 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {environment} from 'stories/environments/environment'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {AppHeaderComponent} from './app-header.component'; + +export default { + title: 'V4/Components/AppHeader', + component: AppHeaderComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule] + }), + componentWrapperDecorator(story => `
${story}
`) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + appName: 'Words for Winners', + appImageSrc: 'https://fusion.ironsrc.net/assets/images/v4/app_mock/Words_for_Winners.png', + platform: 'android', + appKey: '3214325' + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const Small: Story = {}; +Small.args = { + size: 'small' +}; + +export const AbIcon: Story = {}; +AbIcon.args = { + size: 'small', + abTest: 'ab' +}; diff --git a/projects/fusion-ui/components/app-header/v4/app-header.component.ts b/projects/fusion-ui/components/app-header/v4/app-header.component.ts new file mode 100644 index 000000000..18785d822 --- /dev/null +++ b/projects/fusion-ui/components/app-header/v4/app-header.component.ts @@ -0,0 +1,44 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {NgOptimizedImage} from '@angular/common'; +import {InlineCopyComponent} from '@ironsource/fusion-ui/components/inline-copy'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {AbTestIcons, PlatformType} from '@ironsource/fusion-ui/entities'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; + +@Component({ + selector: 'fusion-app-header', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [InlineCopyComponent, IconModule, NgOptimizedImage, TooltipDirective], + templateUrl: './app-header.component.html', + styleUrl: './app-header.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AppHeaderComponent { + @Input() size: 'small' | 'medium' = 'medium'; + @Input() appName!: string; + @Input() appImageSrc!: string; + @Input() platform!: PlatformType; + @Input() appKey!: string; + @Input() abTest: AbTestIcons; + + get appImageSize(): number { + return this.size === 'small' ? 32 : 48; + } + + get platformIcon(): string { + return `v4/branded/${this.platform}`; + } + + get abTestIcon(): string { + return !!this.abTest ? `v4/ab-test/${this.abTest}` : null; + } + + get appKeyToCopy(): string { + return this.size === 'small' ? null : this.appKey; + } + + get valueToCopy(): string { + return this.appKey; + } +} diff --git a/projects/fusion-ui/components/app-header/v4/index.ts b/projects/fusion-ui/components/app-header/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/app-header/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/app-header/v4/ng-package.json b/projects/fusion-ui/components/app-header/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/app-header/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/app-header/v4/public-api.ts b/projects/fusion-ui/components/app-header/v4/public-api.ts new file mode 100644 index 000000000..4a30204b6 --- /dev/null +++ b/projects/fusion-ui/components/app-header/v4/public-api.ts @@ -0,0 +1 @@ +export {AppHeaderComponent} from './app-header.component'; diff --git a/projects/fusion-ui/components/app-trigger/index.ts b/projects/fusion-ui/components/app-trigger/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/app-trigger/ng-package.json b/projects/fusion-ui/components/app-trigger/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/app-trigger/public-api.ts b/projects/fusion-ui/components/app-trigger/public-api.ts new file mode 100644 index 000000000..a86af1af2 --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/public-api.ts @@ -0,0 +1 @@ +export * from './v4'; diff --git a/projects/fusion-ui/components/app-trigger/v4/app-trigger-story-wrapper/app-trigger-story-wrapper.component.ts b/projects/fusion-ui/components/app-trigger/v4/app-trigger-story-wrapper/app-trigger-story-wrapper.component.ts new file mode 100644 index 000000000..8e33da852 --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/app-trigger-story-wrapper/app-trigger-story-wrapper.component.ts @@ -0,0 +1,63 @@ +import {ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, Type} from '@angular/core'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {DropdownComponent} from '@ironsource/fusion-ui/components/dropdown/v4'; +import {AppTriggerComponent} from '@ironsource/fusion-ui/components/app-trigger'; +import {DynamicComponent} from '@ironsource/fusion-ui/components/dynamic-components/common/entities'; +import {Subject} from 'rxjs'; +import {distinctUntilChanged, takeUntil} from 'rxjs/operators'; +import {Application} from '@ironsource/fusion-ui/entities'; +import {DropdownOption} from '@ironsource/fusion-ui/components/dropdown-option'; +import {MultiDropdownComponent} from '@ironsource/fusion-ui/components/multi-dropdown/v4'; + +@Component({ + selector: 'fusion-app-trigger-story-wrapper', + standalone: true, + imports: [ReactiveFormsModule, DropdownComponent, MultiDropdownComponent], + template: ` + @if (isMultiselect){ + + } @else { + + } + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AppTriggerStoryWrapperComponent implements OnInit, OnDestroy { + @Input() optionsApp: any[]; + @Input() isMultiselect = false; + + dropdownControl = new FormControl(); + + customDynamicComponent: DynamicComponent = { + type: AppTriggerComponent as Type, + data: { + placeholder: 'Select app' + } + }; + + onDestroy$ = new Subject(); + + ngOnInit() { + this.dropdownControl.valueChanges.pipe(takeUntil(this.onDestroy$), distinctUntilChanged()).subscribe((value: DropdownOption[]) => { + if (this.isMultiselect && value.length > 1) { + this.customDynamicComponent.data = { + placeholder: value.length + ' selected' + }; + } else { + this.customDynamicComponent.data = { + application: { + key: value[0].id, + name: value[0].displayText, + imageSrc: value[0].image, + platform: (value[0].icon as string).includes('android') ? 'android' : 'ios' + } as Application + }; + } + }); + } + + ngOnDestroy() { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } +} diff --git a/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.html b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.html new file mode 100644 index 000000000..eb2472625 --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.html @@ -0,0 +1,30 @@ +
+ @if (application) { + + +
{{ application?.name }}
+ } @else { +
{{ placeholderText }}
+ @if (required) { +
*
+ } + } + +
+@if (application?.key) { + +
+
+ +
+ +
+
+
Copy app key
+
{{application.key}}
+
+
+} diff --git a/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.scss b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.scss new file mode 100644 index 000000000..a66d95e14 --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.scss @@ -0,0 +1,108 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; + --fu-app-trigger-padding: 0 12px; + --fu-app-trigger-app-left-padding: 8px; + --fu-app-trigger-gap: #{$spacingV4-50};; + --fu-app-trigger-border-radius: #{$spacingV4-100}; + --fu-app-trigger-background-color: var(--default-main, #{$color-v4-default-main}); + --fu-app-trigger-hover-background-color: var(--action-selected, #{$color-v4-action-selected}); + --fu-app-trigger-color: var(--text-primary, #{$color-v4-text-primary}); + --fu-app-trigger-image-size: 28px; + --fu-app-trigger-image-border-radius: 6px; + --fu-app-platforn-icon-size: 20px; + --fu-app-platforn-icon-color: var(--action-active, #{$color-v4-action-active}); + --fu-app-platforn-icon-hover-color: var(--action-primary, #{$color-v4-action-primary}); + --fu-app-trigger-mandatory-color: var(--error-main, #{$color-v4-error-main}); + --fu-app-trigger-carret-icon-size: 20px; + --fu-app-trigger-carret-icon-color: var(--action-active, #{$color-v4-action-active}); + --fu-app-trigger-border: 1px solid var(--common-divider, #{$color-v4-common-divider}); + + display: flex; + align-items: center; + width: var(--fu-app-trigger-width, 100%); + height: var(--fu-app-trigger-height, 40px); + + .fu-app-trigger-wrapper { + width: 100%; + height: 100%; + padding: var(--fu-app-trigger-padding); + display: flex; + align-items: center; + gap: var(--fu-app-trigger-gap); + border-top-left-radius: var(--fu-app-trigger-border-radius); + border-bottom-left-radius: var(--fu-app-trigger-border-radius); + background: var(--fu-app-trigger-background-color); + color: var(--fu-app-trigger-color); + @extend %font-v4-button; + + .fu-app-trigger-platform { + @include size(var(--fu-app-platforn-icon-size)); + color: var(--fu-app-platforn-icon-color); + } + + .fu-app-trigger-image { + @include size(var(--fu-app-trigger-image-size)); + border-radius: var(--fu-app-trigger-image-border-radius); + } + + .fu-app-trigger-mandatory { + color: var(--fu-app-trigger-mandatory-color); + } + + .fu-app-trigger-caret-icon { + margin-left: auto; + @include size(var(--fu-app-trigger-carret-icon-size)); + color: var(--fu-app-trigger-carret-icon-color); + } + + &:hover { + cursor: pointer; + background-color: var(--fu-app-trigger-hover-background-color); + } + + + &:has(.fu-app-trigger-image) { + padding-left: var(--fu-app-trigger-app-left-padding); + } + } + + .fu-app-trigger-inline-copy-wrapper { + margin-left: auto; + display: flex; + align-items: center; + border-top-right-radius: var(--fu-app-trigger-border-radius); + border-bottom-right-radius: var(--fu-app-trigger-border-radius); + background: var(--fu-app-trigger-background-color); + border-left: var(--fu-app-trigger-border); + padding: 10px; + width: 40px; + height: 100%; + + .fu-inline-copy-icon{ + @include size(20px); + color: var(--fu-app-platforn-icon-color); + } + + &:hover { + cursor: pointer; + background-color: var(--fu-app-trigger-hover-background-color); + .fu-inline-copy-icon{ + color: var(--fu-app-platforn-icon-hover-color); + } + } + } + + &:not(:has(.fu-app-trigger-app-key)) { + .fu-app-trigger-wrapper { + border-top-right-radius: var(--fu-app-trigger-border-radius); + border-bottom-right-radius: var(--fu-app-trigger-border-radius); + } + } + + .truncate { + @extend %truncate; + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.spec.ts b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.spec.ts new file mode 100644 index 000000000..bef29a2fb --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppTriggerComponent } from './app-trigger.component'; + +describe('AppTriggerComponent', () => { + let component: AppTriggerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppTriggerComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AppTriggerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.stories.ts b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.stories.ts new file mode 100644 index 000000000..e87fdbbca --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.stories.ts @@ -0,0 +1,88 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {environment} from '../../../../../stories/environments/environment'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {AppTriggerComponent} from './app-trigger.component'; +import {optionsApp} from './app-trigger.mock'; +import {AppTriggerStoryWrapperComponent} from './app-trigger-story-wrapper/app-trigger-story-wrapper.component'; + +export default { + title: 'V4/Components/Dropdown/Triggers/Applications', + component: AppTriggerComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule, AppTriggerStoryWrapperComponent] + }), + componentWrapperDecorator(story => `
${story}
`) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; +Basic.args = { + placeholder: 'All apps selected (24)' +}; + +export const SelectRequired: Story = {}; +SelectRequired.args = { + placeholder: 'Select app', + required: true +}; + +export const SingleSelected: Story = {}; +SingleSelected.args = { + placeholder: 'Select app', + required: true, + application: { + name: 'Words for Winners', + imageSrc: 'https://fusion.ironsrc.net/assets/images/v4/app_mock/Words_for_Winners.png', + platform: 'android' + } +}; + +export const SingleSelectedWithCopy: Story = {}; +SingleSelectedWithCopy.args = { + placeholder: 'Select app', + required: true, + application: { + name: 'Words for Winners', + imageSrc: 'https://fusion.ironsrc.net/assets/images/v4/app_mock/Words_for_Winners.png', + platform: 'android', + key: '324571' + } +}; + +export const MultipleSelected: Story = {}; +MultipleSelected.args = { + placeholder: 'All apps selected (24)' +}; + +export const WithDropdown: Story = { + render: args => ({ + props: { + optionsApp: optionsApp + }, + template: `` + }), + decorators: [componentWrapperDecorator(story => `
${story}
`)] +}; + +export const WithDropdownMultiSelect: Story = { + render: args => ({ + props: { + optionsApp: optionsApp + }, + template: `` + }), + decorators: [componentWrapperDecorator(story => `
${story}
`)] +}; diff --git a/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.ts b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.ts new file mode 100644 index 000000000..37a96cc20 --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/app-trigger.component.ts @@ -0,0 +1,58 @@ +import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {Application} from '@ironsource/fusion-ui/entities'; +import {SnackbarService} from '@ironsource/fusion-ui/components/snackbar/v4'; +import {TooltipComponent} from '@ironsource/fusion-ui/components/tooltip/v4'; +import {tooltipConfiguration, TooltipPosition} from '@ironsource/fusion-ui/components/tooltip/common/base'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; +import {CopyToClipboardModule} from '@ironsource/fusion-ui/directives/copy-to-clipboard'; + +@Component({ + selector: 'fusion-app-trigger', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [IconModule, TooltipDirective, CopyToClipboardModule, TooltipComponent], + providers: [SnackbarService], + templateUrl: './app-trigger.component.html', + styleUrl: './app-trigger.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AppTriggerComponent { + @Input() application?: Application; + @Input() placeholder? = ''; + @Input() required? = false; + + /** @internal */ + tooltipConfiguration: tooltipConfiguration = { + position: TooltipPosition.Bottom, + suppressPositionArrow: true + }; + /** @internal */ + snackbarService: SnackbarService = inject(SnackbarService); + /** @internal */ + get appImageSrc(): string { + return this.application?.imageSrc; + } + /** @internal */ + get platformIcon(): string { + return `v4/branded/${this.application?.platform}`; + } + /** @internal */ + get placeholderText(): string { + return this.placeholder; + } + /** @internal */ + copyToClipboard() { + return () => this.application.key; + } + + /** @internal */ + textCopied() { + this.snackbarService.show({ + title: 'Copied successfully', + type: 'success', + location: 'top-right', + duration: 1500 + }); + } +} diff --git a/projects/fusion-ui/components/app-trigger/v4/app-trigger.mock.ts b/projects/fusion-ui/components/app-trigger/v4/app-trigger.mock.ts new file mode 100644 index 000000000..54b7ead36 --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/app-trigger.mock.ts @@ -0,0 +1,175 @@ +import {DropdownOption} from '@ironsource/fusion-ui/components/dropdown-option'; + +export const Applications = [ + { + appKey: '4eeaaecd', + icon: 'https://platform.ssacdn.com/partners/assets/images/appDefaultIcon.v3.png', + platform: 'Android', + name: 'Ballbarian' + }, + { + appKey: '569d93dd', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_0bf687cd946f7ddaa874f0783e5b42fe_1e1895271a4afd0c994ae2e5f5f1a6c7.jpeg', + platform: 'iOS', + name: 'Barbaric: The Golden Hero' + }, + { + appKey: '516027d5', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_b9e3471d9ec998bc28f186b3a35b6dd3_c2424035ec5a63847dd179b3df20735d.jpeg', + platform: 'Android', + name: 'Barbaric: The Golden Hero' + }, + { + appKey: 'acb0f1fd', + icon: 'https://platform.ssacdn.com/partners/assets/images/appDefaultIcon.v3.png', + platform: 'Android', + name: 'Black Desert Mobile' + }, + { + appKey: '14fd81229', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_099b4c5fe742acb8b712ea06cd799da8_2c16022d72a171b9107ab32505727dc3.jpeg', + platform: 'Android', + name: 'Dash Draw 3D' + }, + { + appKey: '5d3b275d', + icon: 'https://platform.ssacdn.com/partners/assets/images/appDefaultIcon.v3.png', + platform: 'Android', + name: 'Hopeless 2 (Amazon)' + }, + { + appKey: '41539d85', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_43609a8611f49ebe8c8e5eeef199112b_51b1115a8ee50c907e46d380d88ce4a9.jpeg', + platform: 'Android', + name: 'Hopeless 2: Cave Escape' + }, + { + appKey: '4185bafd', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_19642604bd7b4b4092caf7c2d238bac7_67aca1613594bd23bb1273f3fa5833b5.jpeg', + platform: 'iOS', + name: 'Hopeless 2: Cave Escape' + }, + { + appKey: '5d5166f5', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_522011a25f7de9c0388fa658d8081d96_0ade7d992f3c5ec910a7bfa1315201a5.jpeg', + platform: 'iOS', + name: 'Hopeless 3: Dark Hollow Earth' + }, + { + appKey: '593a9a65', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_29d672b2cbd27dc4407fc1aa70cc6270_4c0ce0ecbf0b7117de2d0dd817334d3f.jpeg', + platform: 'Android', + name: 'Hopeless 3: Dark Hollow Earth (Unreleased)' + }, + { + appKey: '6f9c5b25', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_ff99eec57873e46762130d257f8518be_aea721ec6ed2379d47f1dc7ce7313841.jpeg', + platform: 'Android', + name: 'Hopeless Heroes: Banner Attack' + }, + { + appKey: '6adbe27d', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_fbde30ca4351f424a5f9f2e3bcf4b317_dfc8109d0ee710851713cef681e1574a.jpeg', + platform: 'iOS', + name: 'Hopeless Heroes: Tap Attack' + }, + { + appKey: '680c4b6d', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_ff99eec57873e46762130d257f8518be_aea721ec6ed2379d47f1dc7ce7313841.jpeg', + platform: 'Android', + name: 'Hopeless Heroes: Tap Attack (Unreleased)' + }, + { + appKey: '3be4a3d9', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_7889765ba9d278dc4090566d9e423806_f63157da7c32bd91b7baa87d462007b1.jpeg', + platform: 'iOS', + name: 'Hopeless: Space Shooting' + }, + { + appKey: '3b5cad71', + icon: 'https://platform.ssacdn.com/partners/assets/images/appDefaultIcon.v3.png', + platform: 'Android', + name: 'Hopeless: Space Shooting' + }, + { + appKey: '37ba2d4d', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_0ae91ab03406b4fbe8bc8a73acd5d035_1d2a4bf8068c627e72d2dca1a3c1f6e5.jpeg', + platform: 'Android', + name: 'Hopeless: The Dark Cave' + }, + { + appKey: '3b7c23d1', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_38416932554a737e7964d94c0d3009b9_2c28f9d5f857b4796fecb73e46f616ab.jpeg', + platform: 'iOS', + name: 'Hopeless: The Dark Cave' + }, + { + appKey: '76a92c9d', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_d2fb94fee44dfd8e30aa4748cb5afb3e_a79ee66adb3c88d87d13e30244448abf.jpeg', + platform: 'iOS', + name: 'Hopeless: The Dark Cave new' + }, + { + appKey: '107ca56b1', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_3e7aa1df421da58a6c7f036ba054cce4_b9baeeed329bcde2bffbc61548d4d5a6.jpeg', + platform: 'iOS', + name: 'Multi Balls' + }, + { + appKey: '3ac7ae39', + icon: 'https://platform.ssacdn.com/partners/assets/images/appDefaultIcon.v3.png', + platform: 'Android', + name: "Ruby Run: Eye God's Revenge" + }, + { + appKey: '3c2e5a19', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_a48f2dd9e590448be84443e27ec6d2b4_f1ab4473317988b1e3aa13e41b5542e0.jpeg', + platform: 'iOS', + name: "Ruby Run: Eye God's Revenge" + }, + { + appKey: '479f2a1d', + icon: 'https://platform.ssacdn.com/partners/assets/images/appDefaultIcon.v3.png', + platform: 'iOS', + name: 'Sugar Slide: The Path Home' + }, + { + appKey: '4586f48d', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_585a7fd6512e0ed2eea4b341fed4d163_50d78a4d6b5c8e03a864b6144ac2e1af.jpeg', + platform: 'Android', + name: 'Sugar Slide: The Path Home' + }, + { + appKey: '76a8f315', + icon: 'https://platform.ssacdn.com/partners/assets/images/appDefaultIcon.v3.png', + platform: 'iOS', + name: 'Test' + }, + { + appKey: '6add0225', + icon: 'https://platform.ssacdn.com/partners/assets/images/appDefaultIcon.v3.png', + platform: 'iOS', + name: 'Test' + }, + { + appKey: 'c7e67f15', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_bcb98c07497e479f0d20d03bbd306ec3_41882f3735b8bac8c0d64383022fead3.jpeg', + platform: 'iOS', + name: 'Wool Island' + }, + { + appKey: 'c81a2f45', + icon: 'https://platform.ssacdn.com/demand-creatives/icons/icon_dc57ff8ab46156a97575ca89107f6f72_54795ae59ca33414ce31a305e6911f06.jpeg', + platform: 'Android', + name: 'Wool Island' + } +]; + +export const optionsApp: DropdownOption[] = Applications.map((app: any, idx: number) => { + return { + id: app.appKey + '' + idx++, + displayText: app.name, + image: app.icon, + icon: `v4/branded/${app.platform.toLowerCase()}.svg` + }; +}); diff --git a/projects/fusion-ui/components/app-trigger/v4/index.ts b/projects/fusion-ui/components/app-trigger/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/app-trigger/v4/ng-package.json b/projects/fusion-ui/components/app-trigger/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/app-trigger/v4/public-api.ts b/projects/fusion-ui/components/app-trigger/v4/public-api.ts new file mode 100644 index 000000000..29301b06e --- /dev/null +++ b/projects/fusion-ui/components/app-trigger/v4/public-api.ts @@ -0,0 +1 @@ +export {AppTriggerComponent} from './app-trigger.component'; diff --git a/projects/fusion-ui/components/calendar/v4/calendar-v4.component.html b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.html new file mode 100644 index 000000000..2bc455eb7 --- /dev/null +++ b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.html @@ -0,0 +1,42 @@ +
+ +
+
+ {{ configuration.month | date: 'MMMM' }} {{ configuration.month | date: 'y' }} +
+
+ + +
+ @for (dow of daysOfTheWeek; track dow) { +
{{ dow }}
+ } +
+ + +
+ @for ( week of calendarService.getParsedMonth(configuration.month, configuration.maxDate).weeks; track week; let last = $last) { + @if (!last || (last && week[0].date)) { +
+ @for ( day of week; track day){ + @if (day.date){ +
+ + {{ day.date | date: 'd' }} +
+ } + @else { +
+ } + } +
+ } + } +
+ +
\ No newline at end of file diff --git a/projects/fusion-ui/components/calendar/v4/calendar-v4.component.scss b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.scss new file mode 100644 index 000000000..e9957095e --- /dev/null +++ b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.scss @@ -0,0 +1,206 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +@mixin calendar-circle($color, $border-radius) { + position: absolute; + content: ''; + left: 0; + top: 0; + background-color: $color; + height: 32px; + width: 32px; + border-radius: $border-radius; + display: block; +} + +@mixin calendar-today-dot($color) { + content: ''; + background-color: $color; + width: 4px; + height: 4px; + position: absolute; + bottom: 5px; + border-radius: 100px; + left: calc(50% - 2px); +} + +:host { + @extend %reset; + @include user-select(none); + + --fu-calendar-width: 224px; + --fu-calendar-mian-color: var(--text-primary, #{$color-v4-text-primary}); + --fu-calendar-bg-color: var(--common-white, #{$color-v4-common-white}); + --fu-month-color: var(--text-primary, #{$color-v4-text-primary}); + --fu-month-height: 40px; + --fu-day-in-week-color: var(--text-secondary, #{$color-v4-text-secondary}); + --fu-day-today-dot-color: var(--primary-main, #{$color-v4-primary-main}); + --fu-day-disabled-color: var(--text-disabled, #{$color-v4-text-disabled}); + --fu-day-hover-bg-color: var(--primary-main-50-p, #{$color-v4-primary-main-50-p}); + --fu-day-selected-background: var(--primary-light, #{$color-v4-primary-light}); + --fu-day-selected-hovered-background: var(--primary-main-50-p, #{$color-v4-primary-main-50-p}); + --fu-day-active-background: var(--primary-main, #{$color-v4-primary-main}); + + .fu-calendar { + width: var(--fu-calendar-width); + @extend %font-v4-body-1; + color: var(--fu-calendar-mian-color); + background-color: var(--fu-calendar-bg-color); + display: flex; + flex-direction: column; + gap: 4px; + + .fu-month-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 8px 0px; + height: var(--fu-month-height); + + .fu-month { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; + @extend %font-v4-button; + color: var(--fu-month-color); + } + } + + .fu-weekdays-wrapper { + display: flex; + align-items: center; + justify-content: center; + + .fu-day-in-week { + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; + height: 32px; + padding: 0px 4px; + @extend %font-v4-caption; + color: var(--fu-day-in-week-color); + } + } + + .fu-days-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + + .fu-week { + display: flex; + align-items: center; + justify-content: center; + + .fu-day { + position: relative; + display: flex; + justify-content: center; + align-items: center; + flex-grow: 1; + @include size(32px); + cursor: pointer; + padding: 6px 0; + position: relative; + + span { + position: relative; + } + + + // region regular day hover + &:not(.selected):not(.disabled):not(.hover-current):hover { + &:before { + @include calendar-circle(var(--fu-day-hover-bg-color), 50%); + } + } + + &.selected:not(.active) { + border: none; + + .selected-range { + display: inline; + cursor: pointer; + width: 32px; + height: 32px; + position: absolute; + top: 0; + left: 0; + background-color: var(--fu-day-selected-background); + } + + &:hover { + .selected-range { + background-color: var(--fu-day-selected-hovered-background); + } + } + } + + // endregion + + &.active { + color: $White; + background-color: transparent; + + &.selected.oneDaySelection { + &:before { + @include calendar-circle(var(--fu-day-active-background), 50%); + } + } + } + + // region today dot + &.today { + &.selected, &.active, &.hover-current { + &:after { + @include calendar-today-dot(var(--fu-calendar-bg-color)); + } + } + } + + &.today:not(.active):not(.hover-current):after { + @include calendar-today-dot(var(--fu-day-today-dot-color)); + } + + // endregion + + // region selected start and end day + &.selectedEnd { + background-color: var(--fu-day-selected-background); + border-radius: 0 50% 50% 0; + + &:before { + @include calendar-circle(var(--fu-day-active-background), 50%); + } + } + + &.selectedStart { + background-color: var(--fu-day-selected-background); + border-radius: 50% 0 0 50%; + + &:before, &.active.selected.oneDaySelection.hover-range:before { + @include calendar-circle(var(--fu-day-active-background), 50%); + } + } + + // endregion + + // region disabled + &.disabled { + pointer-events: none; + + &:not(.selected) { + color: var(--fu-day-disabled-color) + } + } + + // endregion + } + } + } + } +} diff --git a/projects/fusion-ui/components/calendar/v4/calendar-v4.component.spec.ts b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.spec.ts new file mode 100644 index 000000000..b5b642362 --- /dev/null +++ b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CalendarV4Component } from './calendar-v4.component'; + +describe('CalendarV4Component', () => { + let component: CalendarV4Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CalendarV4Component] + }) + .compileComponents(); + + fixture = TestBed.createComponent(CalendarV4Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/calendar/v4/calendar-v4.component.stories_._ts b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.stories_._ts new file mode 100644 index 000000000..bbd4ad19a --- /dev/null +++ b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.stories_._ts @@ -0,0 +1,88 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {CalendarComponentConfigurations, CalendarType} from '@ironsource/fusion-ui/components/calendar'; +import {DaterangeSelection} from '@ironsource/fusion-ui/components/daterange'; +import {CalendarV4Component} from './calendar-v4.component'; + +const TODAY: Date = new Date(); +const TOMORROW: Date = new Date(TODAY); +TOMORROW.setDate(TODAY.getDate() + 1); +const PREVIOUS_MONTH: Date = new Date(new Date().setDate(0)); + +const START_DATE = new Date(TODAY.getFullYear(), TODAY.getMonth(), 2); +const END_DATE = new Date(TODAY.getFullYear(), TODAY.getMonth() + 1, -3); + +export default { + title: 'V4/Components/Dates/Calendar', + component: CalendarV4Component, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule] + }) + /*componentWrapperDecorator(story => `
${story}
`)*/ + ], + tags: ['autodocs'], + parameters: { + layout: 'centered', + options: { + showPanel: true, + panelPosition: 'bottom' + } + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; +Basic.args = { + configuration: { + month: TODAY, + allowFutureSelection: true, + calendarType: CalendarType.DATE_PICKER, + selection: {date: null} as DaterangeSelection + } as CalendarComponentConfigurations +}; + +export const SelectedToday: Story = {}; +SelectedToday.args = { + configuration: { + month: TODAY, + allowFutureSelection: true, + calendarType: CalendarType.DATE_PICKER, + selection: {date: TODAY} as DaterangeSelection + } as CalendarComponentConfigurations +}; + +export const FutureSelectionNotAllowed: Story = {}; +FutureSelectionNotAllowed.args = { + configuration: { + month: TODAY, + allowFutureSelection: false, + calendarType: CalendarType.DATE_RANGE, + selection: {date: null} as DaterangeSelection + } as CalendarComponentConfigurations +}; + +export const PreviousMonth: Story = {}; +PreviousMonth.args = { + configuration: { + month: PREVIOUS_MONTH, + allowFutureSelection: true, + calendarType: CalendarType.DATE_PICKER, + selection: {date: null} as DaterangeSelection + } as CalendarComponentConfigurations +}; + +export const SelectedRange: Story = {}; +SelectedRange.args = { + configuration: { + month: TODAY, + allowFutureSelection: true, + calendarType: CalendarType.DATE_PICKER, + selection: {startDate: START_DATE, endDate: END_DATE} as DaterangeSelection + } as CalendarComponentConfigurations +}; diff --git a/projects/fusion-ui/components/calendar/v4/calendar-v4.component.ts b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.ts new file mode 100644 index 000000000..ac6d498a0 --- /dev/null +++ b/projects/fusion-ui/components/calendar/v4/calendar-v4.component.ts @@ -0,0 +1,17 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {CalendarService} from '@ironsource/fusion-ui/components/calendar/common/base'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {CalendarComponent} from '@ironsource/fusion-ui/components/calendar/v3'; + +@Component({ + selector: 'fusion-calendar', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [CommonModule, IconModule], + providers: [CalendarService], + templateUrl: './calendar-v4.component.html', + styleUrl: './calendar-v4.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class CalendarV4Component extends CalendarComponent {} diff --git a/projects/fusion-ui/components/calendar/v4/index.ts b/projects/fusion-ui/components/calendar/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/calendar/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/calendar/v4/ng-package.json b/projects/fusion-ui/components/calendar/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/calendar/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/calendar/v4/public-api.ts b/projects/fusion-ui/components/calendar/v4/public-api.ts new file mode 100644 index 000000000..a3a061acb --- /dev/null +++ b/projects/fusion-ui/components/calendar/v4/public-api.ts @@ -0,0 +1,3 @@ +export {CalendarV4Component as CalendarComponent} from './calendar-v4.component'; +export * from '@ironsource/fusion-ui/components/calendar/entities'; +export {CalendarService, CalendarComponentConfigurations} from '@ironsource/fusion-ui/components/calendar/common/base'; diff --git a/projects/fusion-ui/components/chip-filter/common/base/chip-filter.base.component.ts b/projects/fusion-ui/components/chip-filter/common/base/chip-filter.base.component.ts index 61d575684..aa4076399 100644 --- a/projects/fusion-ui/components/chip-filter/common/base/chip-filter.base.component.ts +++ b/projects/fusion-ui/components/chip-filter/common/base/chip-filter.base.component.ts @@ -38,6 +38,8 @@ export abstract class ChipFilterBaseComponent implements OnInit, AfterViewInit, /** @internal */ @ViewChild('ref', {static: true}) ref: TemplateRef; + @Input() variant: 'outline' | 'text' = 'outline'; + /** @internal */ id: number | string; /** @internal */ diff --git a/projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.scss b/projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.scss index 5f0d76141..7c08d354a 100644 --- a/projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.scss +++ b/projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.scss @@ -31,6 +31,10 @@ $chipSmallPadding: 4px 6px; background-color: var(--action-hover, #{$color-v4-action-hover}); cursor: pointer; } + + &.fu-chip-filter-variant-text{ + border-color: transparent; + } } // selected + opened @@ -47,6 +51,9 @@ $chipSmallPadding: 4px 6px; background-color: var(--action-hover, #{$color-v4-action-hover}); cursor: pointer; } + &.fu-chip-filter-variant-text{ + border-color: transparent; + } } } @@ -61,6 +68,9 @@ $chipSmallPadding: 4px 6px; .fu-icon-close { color: var(--action-active, #{$color-v4-action-active}); } + &.fu-chip-filter-variant-text{ + border-color: transparent; + } } } diff --git a/projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.stories_._ts b/projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.stories.ts similarity index 84% rename from projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.stories_._ts rename to projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.stories.ts index a310909a5..17da47cc7 100644 --- a/projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.stories_._ts +++ b/projects/fusion-ui/components/chip-filter/v4/chip-filter-button/chip-filter-button.component.stories.ts @@ -73,7 +73,7 @@ const baseTemplateMultiselect = ` `; export default { - title: 'V4/Components/Dropdown/Triggers/Button', + title: 'V4/Components/Dropdown/Triggers/ButtonFilter', component: ChipFilterButtonComponent, decorators: [ moduleMetadata({ @@ -94,7 +94,12 @@ export default { parameters: { docs: { description: { - component: dedent`***ChipTriggerComponent***.` + component: dedent` +******. + +By studio request some stories will be not shown. I understand that it look like not ok, but it what it is. +So, you can check it in the code. +` } }, options: { @@ -107,7 +112,8 @@ export default { options: MOCK_OPTIONS, configuration: {id: 1, mode: 'static', close: true}, size: 'medium', - weight: 'light' + weight: 'light', + showCaretIcon: true }, argTypes: { formControl: { @@ -118,7 +124,7 @@ export default { type Story = StoryObj; -export const Default: Story = { +export const Basic: Story = { render: args => ({ props: { ...args, @@ -130,7 +136,48 @@ export const Default: Story = { decorators: [componentWrapperDecorator(story => `
${story}
`)] }; -export const Disabled: Story = { +export const Variant: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl(), + options: MOCK_OPTIONS_TYPE + }, + template: ` +
+ +
+ + +
+
+ +
+ + +
+
+
+ ` + }), + decorators: [componentWrapperDecorator(story => `
${story}
`)] +}; + +/*export const Disabled: Story = { render: args => ({ props: { ...args, @@ -139,9 +186,9 @@ export const Disabled: Story = { }, template: baseTemplate }) -}; +};*/ -export const Multiselect: Story = { +/*export const Multiselect: Story = { render: args => ({ props: { ...args, @@ -150,8 +197,10 @@ export const Multiselect: Story = { template: baseTemplateMultiselect }), decorators: [componentWrapperDecorator(story => `
${story}
`)] -}; +};*/ +/* +// for studio request this stories will not shown export const WithIconLeft: Story = { render: args => ({ props: { @@ -256,7 +305,7 @@ export const Weight: Story = { }), decorators: [componentWrapperDecorator(story => `
${story}
`)] }; - +*/ export const Icon: Story = { render: args => ({ props: { @@ -269,7 +318,7 @@ export const Icon: Story = {
-
- -
+
+ +
- - - + @if (rightIcon) { + + } + @if (showClose) { + + } + @if (showCaretDown) { + + }
diff --git a/projects/fusion-ui/components/chip/index.ts b/projects/fusion-ui/components/chip/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/chip/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/chip/ng-package.json b/projects/fusion-ui/components/chip/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/components/chip/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/chip/public-api.ts b/projects/fusion-ui/components/chip/public-api.ts new file mode 100644 index 000000000..a86af1af2 --- /dev/null +++ b/projects/fusion-ui/components/chip/public-api.ts @@ -0,0 +1 @@ +export * from './v4'; diff --git a/projects/fusion-ui/components/chip/v4/chip.component.html b/projects/fusion-ui/components/chip/v4/chip.component.html new file mode 100644 index 000000000..7f9c2961a --- /dev/null +++ b/projects/fusion-ui/components/chip/v4/chip.component.html @@ -0,0 +1,12 @@ +
+ @if (iconName()) { + + } +
{{ label() }}
+ @if (removable()) { + + } +
\ No newline at end of file diff --git a/projects/fusion-ui/components/chip/v4/chip.component.scss b/projects/fusion-ui/components/chip/v4/chip.component.scss new file mode 100644 index 000000000..23d847872 --- /dev/null +++ b/projects/fusion-ui/components/chip/v4/chip.component.scss @@ -0,0 +1,104 @@ +@import '../../../src/style/scss/v4/colors'; +@import '../../../src/style/scss/v4/spacings'; +@import '../../../src/style/scss/v4/fonts'; + +:host { + margin: 0; + padding: 0; + display: inline-flex; + + .fu-chip-wrapper { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 2px 4px; + border-radius: 6px; + @extend %font-v4-chip-label; + color: var(--action-primary, #{$color-v4-action-primary}); + background-color: var(--default-light, #{$color-v4-default-light}); + + &.fu-chip-outlined { + background-color: var(--common-inverse-white, #{$color-v4-common-inverse-white}); + outline: solid 1px var(--common-divider-elevation-0, #{$color-v4-common-divider-elevation-0}); + } + + &.fu-chip-round { + border-radius: 100px; + } + + &:hover { + cursor: default; + } + .fu-chip-remove-icon:hover { + color: var(--action-primary, #{$color-v4-action-primary}); + } + + &.fu-chip-medium { + padding: 6px; + } + + .fu-chip-remove-icon, + .fu-chip-start-icon { + width: 16px; + height: 16px; + color: var(--action-active, #{$color-v4-action-active}); + cursor: pointer; + } + + .fu-chip-start-icon { + cursor: default; + } + + &.fu-chip-primary { + color: var(--primary-contrast-text, #{$color-v4-primary-contrast-text}); + background-color: var(--primary-light, #{$color-v4-primary-light}); + } + + &.fu-chip-info { + color: var(--info-contrast-text, #{$color-v4-info-contrast-text}); + background-color: var(--info-light, #{$color-v4-info-light}); + } + + &.fu-chip-error { + color: var(--error-contrast-text, #{$color-v4-error-contrast-text}); + background-color: var(--error-light, #{$color-v4-error-light}); + } + + &.fu-chip-warning { + color: var(--warning-contrast-text, #{$color-v4-warning-contrast-text}); + background-color: var(--warning-light, #{$color-v4-warning-light}); + } + + &.fu-chip-success { + color: var(--success-contrast-text, #{$color-v4-success-contrast-text}); + background-color: var(--success-light, #{$color-v4-success-light}); + } + + &.fu-chip-dark { + color: var(--common-inverse-white, #{$color-v4-common-inverse-white}); + background-color: var(--common-inverse-black, #{$color-v4-common-inverse-black}); + } + + &.fu-chip-selected { + color: var(--action-primary, #{$color-v4-action-primary}); + background-color: var(--action-hover, #{$color-v4-action-hover}); + outline: solid 1px var(--action-active, #{$color-v4-action-active}); + + &.fu-chip-disabled { + outline: solid 1px var(--action-outlined-border, #{$color-v4-action-outlined-border}); + } + } + + &.fu-chip-disabled { + color: var(--action-disabled, #{$color-v4-action-disabled}); + background-color: var(--action-disabled-background, #{$color-v4-action-disabled-background}); + + .fu-chip-remove-icon { + color: var(--action-disabled, #{$color-v4-action-disabled}); + pointer-events: none; + cursor: default; + } + } + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/chip/v4/chip.component.spec.ts b/projects/fusion-ui/components/chip/v4/chip.component.spec.ts new file mode 100644 index 000000000..57990b67e --- /dev/null +++ b/projects/fusion-ui/components/chip/v4/chip.component.spec.ts @@ -0,0 +1,147 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ChipComponent } from './chip.component'; +import {input} from "@angular/core"; + +const LABEL = 'Label'; + +describe('ChipComponent', () => { + let component: ChipComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ChipComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should create label', () => { + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-label')).toBeTruthy(); + expect(el.querySelector('.fu-chip-label').textContent).toBe(LABEL); + }); + + it('by default should be small', () => { + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-small')).toBeTruthy(); + }); + it('by default should be filled', () => { + + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-filled')).toBeTruthy(); + }); + + it('can be rounded', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.shape = input('round'); + fixture.detectChanges(); + + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-round')).toBeTruthy(); + }); + + it('can be disabled', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.disabled = input(true); + fixture.detectChanges(); + + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-disabled')).toBeTruthy(); + }); + it('can be selected', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.selected = input(true); + fixture.detectChanges(); + + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-selected')).toBeTruthy(); + }); + it('can be medium size', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.size = input('medium'); + fixture.detectChanges(); + + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-medium')).toBeTruthy(); + }); + + it('can be primary theme', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.theme = input('primary'); + fixture.detectChanges(); + + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-primary')).toBeTruthy(); + }); + + it('can be info theme', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.theme = input('info'); + fixture.detectChanges(); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-info')).toBeTruthy(); + }); + + it('can be error theme', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.theme = input('error'); + fixture.detectChanges(); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-error')).toBeTruthy(); + }); + + it('can be warning theme', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.theme = input('warning'); + fixture.detectChanges(); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-warning')).toBeTruthy(); + }); + + it('can be success theme', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.theme = input('success'); + fixture.detectChanges(); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-success')).toBeTruthy(); + }); + + it('can be dark theme', () => { + fixture = TestBed.createComponent(ChipComponent); + component = fixture.componentInstance; + component.label = input(LABEL); + component.theme = input('dark'); + fixture.detectChanges(); + const el: HTMLElement = fixture.nativeElement; + expect(el.querySelector('.fu-chip-dark')).toBeTruthy(); + }); + +}); diff --git a/projects/fusion-ui/components/chip/v4/chip.component.stories.ts b/projects/fusion-ui/components/chip/v4/chip.component.stories.ts new file mode 100644 index 000000000..6e552f35b --- /dev/null +++ b/projects/fusion-ui/components/chip/v4/chip.component.stories.ts @@ -0,0 +1,115 @@ +import {Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {InputSignal} from '@angular/core'; +import {IconData, IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {ChipComponent} from './chip.component'; + +// todo: as for signal.inputs check this: https://stackoverflow.com/questions/78379300/how-do-i-use-angular-input-signals-with-storybook + +export default { + title: 'V4/Components/DataDisplay/Chip', + component: ChipComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule] + }) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + label: 'Label' as unknown as InputSignal + }, + argTypes: { + label: {control: {type: 'text'}}, + iconName: {control: {type: 'text'}}, + removeIconName: {control: {type: 'text'}}, + removable: {control: {type: 'boolean'}}, + selected: {control: {type: 'boolean'}}, + disabled: {control: {type: 'boolean'}} + // todo: because of this component used input signals, we can't use control types with options + // theme: {control: {type: 'select', options: ['default', 'primary', 'info', 'error', 'success', 'warning', 'dark']}}, + // size: {control: {type: 'radio', options: ['small', 'medium']}}, + // variant: {control: {type: 'radio', options: ['filled', 'outlined']}}, + // shape: {control: {type: 'radio', options: ['square', 'round']}}, + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const Size: Story = { + render: args => ({ + props: args, + template: ` +
+ + +
+ ` + }) +}; + +export const Themes: Story = { + render: args => ({ + props: args, + template: ` +
+ + + + + + + + +
+ ` + }) +}; + +export const Variant: Story = { + render: args => ({ + props: args, + template: ` +
+ + +
+ ` + }) +}; + +export const Style: Story = { + render: args => ({ + props: args, + template: ` +
+ + +
+ ` + }) +}; + +export const WithRemoveAction: Story = {}; +WithRemoveAction.args = { + label: 'Clickable removable chip' as unknown as InputSignal, + shape: 'round' as unknown as InputSignal<'square' | 'round'>, + removable: true as unknown as InputSignal +}; + +export const WithIcon: Story = {}; +WithIcon.args = { + label: 'With icon' as unknown as InputSignal, + shape: 'round' as unknown as InputSignal<'square' | 'round'>, + iconName: 'ph/placeholder' as unknown as InputSignal +}; diff --git a/projects/fusion-ui/components/chip/v4/chip.component.ts b/projects/fusion-ui/components/chip/v4/chip.component.ts new file mode 100644 index 000000000..f86321ce0 --- /dev/null +++ b/projects/fusion-ui/components/chip/v4/chip.component.ts @@ -0,0 +1,27 @@ +import {ChangeDetectionStrategy, Component, EventEmitter, input, Output} from '@angular/core'; +import {IconData, IconModule} from '@ironsource/fusion-ui/components/icon/v1'; + +@Component({ + selector: 'fusion-chip', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [IconModule], + templateUrl: './chip.component.html', + styleUrl: './chip.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ChipComponent { + label = input.required(); + iconName = input(); + removeIconName = input('ph/fill/x-circle'); + removable = input(false); + theme = input<'default' | 'primary' | 'info' | 'error' | 'success' | 'warning' | 'dark'>('default'); + size = input<'small' | 'medium'>('small'); + variant = input<'filled' | 'outlined'>('filled'); + shape = input<'square' | 'round'>('square'); + selected = input(false); + disabled = input(false); + + /** @internal */ + @Output() readonly remove = new EventEmitter(); +} diff --git a/projects/fusion-ui/components/chip/v4/index.ts b/projects/fusion-ui/components/chip/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/chip/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/chip/v4/ng-package.json b/projects/fusion-ui/components/chip/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/chip/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/chip/v4/public-api.ts b/projects/fusion-ui/components/chip/v4/public-api.ts new file mode 100644 index 000000000..51953ee3b --- /dev/null +++ b/projects/fusion-ui/components/chip/v4/public-api.ts @@ -0,0 +1 @@ +export * from './chip.component'; diff --git a/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.html b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.html new file mode 100644 index 000000000..7449e1dc4 --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.html @@ -0,0 +1 @@ +

datepicker-v4 works!

diff --git a/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.scss b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.scss new file mode 100644 index 000000000..e09b7374d --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.scss @@ -0,0 +1,6 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; +} \ No newline at end of file diff --git a/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.spec.ts b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.spec.ts new file mode 100644 index 000000000..1cc58acb7 --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DatepickerV4Component } from './datepicker-v4.component'; + +describe('DatepickerV4Component', () => { + let component: DatepickerV4Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DatepickerV4Component] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DatepickerV4Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.stories.ts b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.stories.ts new file mode 100644 index 000000000..894a2da3c --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.stories.ts @@ -0,0 +1,65 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {dedent} from 'ts-dedent'; +import {CommonModule} from '@angular/common'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {DatepickerV4Component} from './datepicker-v4.component'; +import {DatepickerOptions, DatepickerSelection} from './datepicker-v4.entities'; + +const TODAY = new Date(); + +export default { + title: 'V4/Components/Dates/DatePicker', + component: DatepickerV4Component, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, ReactiveFormsModule, SvgModule.forRoot({assetsPath: environment.assetsPath})] + }), + componentWrapperDecorator(story => `
${story}
`) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + }, + docs: { + description: { + component: dedent`A ***DatePicker*** component is a reusable user interface element designed to simplify the process of selecting dates in web applications. It provides an interactive calendar interface that allows users to easily choose a specific date without manually typing it. +To set and get the selected date, use the ***formControl*** property with interface. + +\`\`\` +interface DatepickerSelection { + date?: Date; +} +\`\`\` + +Also you can set the options for the date picker component using the ***options*** property with interface. + +\`\`\` +interface DaterangeOptions { + format?: string; // for date format in placeholder. default is 'd MMM, y' + placeholder?: string; + allowFutureSelection?: boolean; + overlayAlignPosition?: 'left' | 'right'; // default is 'right' but calculated based on the component position +}\`\`\` +` + } + } + }, + args: { + formControl: new FormControl() as FormControl, + options: {overlayAlignPosition: 'left'} as DatepickerOptions + }, + argTypes: { + formControl: { + control: false + } + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.ts b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.ts new file mode 100644 index 000000000..8ceeef063 --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.component.ts @@ -0,0 +1,90 @@ +import {ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; +import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, ReactiveFormsModule} from '@angular/forms'; +import {DaterangeOptions} from '@ironsource/fusion-ui/components/daterange'; +import {DaterangeComponent} from '@ironsource/fusion-ui/components/daterange/v4'; +import {DatepickerOptions, DatepickerSelection} from './datepicker-v4.entities'; +import {isDate} from '@ironsource/fusion-ui/utils'; + +const DEFAULT_OPTIONS = { + calendarAmount: 1, + placeholder: 'Select date', + presets: [] +}; + +@Component({ + selector: 'fusion-datepicker', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [CommonModule, ReactiveFormsModule, DaterangeComponent], + template: ``, + styleUrl: './datepicker-v4.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatepickerV4Component), + multi: true + } + ] +}) +export class DatepickerV4Component implements OnInit, OnDestroy, ControlValueAccessor { + @Input() set options(value: DatepickerOptions) { + this.daterangeOptions = {...DEFAULT_OPTIONS, ...value}; + } + @Input() set minDate(value: Date) { + this.daterangeMinDate = new Date(value); + } + @Input() set maxDate(value: Date) { + this.daterangeMaxDate = new Date(value); + } + + @Input() testId: string; + + private onDestroy$ = new Subject(); + + /** @internal */ + daterangeOptions: DaterangeOptions = {...DEFAULT_OPTIONS}; + /** @internal */ + daterangeFormControl: FormControl = new FormControl(); + /** @internal */ + daterangeMinDate: Date; + /** @internal */ + daterangeMaxDate: Date; + + /** @internal */ + ngOnInit() { + this.daterangeFormControl.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe(value => { + this.propagateChange(value); + }); + } + /** @internal */ + ngOnDestroy() { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + /** @internal */ + propagateChange = (_: DatepickerSelection) => {}; + /** @internal */ + propagateTouched = () => {}; + /** @internal */ + writeValue(value: DatepickerSelection): void { + this.daterangeFormControl.setValue(value, {emitEvent: false}); + } + /** @internal */ + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + /** @internal */ + registerOnTouched(fn: any): void { + this.propagateTouched = fn; + } +} diff --git a/projects/fusion-ui/components/datepicker/v4/datepicker-v4.entities.ts b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.entities.ts new file mode 100644 index 000000000..08c0ccc44 --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/datepicker-v4.entities.ts @@ -0,0 +1,10 @@ +export interface DatepickerSelection { + date?: Date; +} + +export interface DatepickerOptions { + format?: string; + placeholder?: string; + allowFutureSelection?: boolean; + overlayAlignPosition?: 'left' | 'right'; +} diff --git a/projects/fusion-ui/components/datepicker/v4/index.ts b/projects/fusion-ui/components/datepicker/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/datepicker/v4/ng-package.json b/projects/fusion-ui/components/datepicker/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/datepicker/v4/public-api.ts b/projects/fusion-ui/components/datepicker/v4/public-api.ts new file mode 100644 index 000000000..c73361bef --- /dev/null +++ b/projects/fusion-ui/components/datepicker/v4/public-api.ts @@ -0,0 +1,2 @@ +export {DatepickerV4Component as DatepickerComponent} from './datepicker-v4.component'; +export * from './datepicker-v4.entities'; diff --git a/projects/fusion-ui/components/daterange/common/base/daterange.base.component.ts b/projects/fusion-ui/components/daterange/common/base/daterange.base.component.ts index a14ef17ad..302964ae6 100644 --- a/projects/fusion-ui/components/daterange/common/base/daterange.base.component.ts +++ b/projects/fusion-ui/components/daterange/common/base/daterange.base.component.ts @@ -204,7 +204,6 @@ export abstract class DaterangeBaseComponent extends ApiBase implements OnInit, } /** @internal */ onOutsideClick(target: HTMLElement) { - // if (this.validateClickOutside(target) && !target.closest('fusion-dropdown-option')) { if (this.validateClickOutside(target)) { this.close(); } @@ -213,9 +212,10 @@ export abstract class DaterangeBaseComponent extends ApiBase implements OnInit, apply() { if (this.isOpen$.getValue() && this.isTimeSelectorValid()) { this.isOpen$.next(false); - if (this.selection?.endDate) { - this.selection.endDate; - } + // todo: check it + // if (this.selection?.endDate) { + // this.selection.endDate; + // } this.originalSelection = {...this.selection}; this.setPlaceholder({isOpen: false}); @@ -365,6 +365,12 @@ export abstract class DaterangeBaseComponent extends ApiBase implements OnInit, const componentClientRect = this.elemRef.nativeElement.getBoundingClientRect(); if (overlayClientRect.width > componentClientRect.width) { // need check if has a place on left + console.log('ov', this.overlay.nativeElement); + console.log('ov', overlayClientRect); + console.log('el', this.elemRef.nativeElement); + console.log('el', componentClientRect); + console.log('>>', componentClientRect.x + componentClientRect.width, overlayClientRect.width); + if (!(componentClientRect.x + componentClientRect.width >= overlayClientRect.width)) { this.overlayAlign$.next('left'); } @@ -391,6 +397,12 @@ export abstract class DaterangeBaseComponent extends ApiBase implements OnInit, } else { this.daterangeOptions = this.defaultOptions; } + if (!!this.daterangeOptions?.placeholder) { + this.dropdownSelectConfigurations$.next({ + ...this.dropdownSelectConfigurations$.getValue(), + placeholder: {value: this.daterangeOptions.placeholder} + }); + } if (!isNullOrUndefined(this.daterangeOptions?.overlayAlignPosition)) { this.overlayAlign$.next(this.daterangeOptions.overlayAlignPosition); @@ -434,6 +446,7 @@ export abstract class DaterangeBaseComponent extends ApiBase implements OnInit, return true; } + /** @internal */ setValueToPropagate(value: DaterangeSelection): DaterangeSelection { if (this.fcHasTimeSelector.value) { return {...value, startTime: this.fcStartTime.value, endTime: this.fcEndTime.value}; diff --git a/projects/fusion-ui/components/daterange/v4/daterange-v4.component.html b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.html new file mode 100644 index 000000000..66cd2c8da --- /dev/null +++ b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.html @@ -0,0 +1,84 @@ +
+ @if (templateRef) { +
+ +
+ } @else { + + } + +
+ @if (isPresetsShown) { +
+ + +
    + @for (preset of options.presets; track preset) { +
  • {{ daterangeService.getPresetName(preset) }} +
  • + } +
+
+
+ } +
+
+
+ +
+ + @for (month of currentMonths; track month) { + + } + +
+ +
+
+ @if (options.calendarAmount !== 1 && options?.withTimeSelect) { +
+ + @if(fcHasTimeSelector.value){ +
+
+ Start +
+
+ End +
+
+ } +
+ } + @if (!isSingleDatePicker){ + + } +
+ +
+
+ + diff --git a/projects/fusion-ui/components/daterange/v4/daterange-v4.component.scss b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.scss new file mode 100644 index 000000000..be1da5e6e --- /dev/null +++ b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.scss @@ -0,0 +1,205 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; + @include user-select(none); + position: relative; + display: inline-block; + + // region variables + --daterange-overlay-top: #{$spacingV4-50}; + --daterange-overlay-bg-color: var(--common-white, #{$color-v4-common-white}); + --daterange-overlay-border: 1px solid var(--common-divider-elevation-0, #{$color-v4-common-divider-elevation-0}); + --daterange-overlay-border-radius: #{$borderRadiusV4-lg}; + --daterange-overlay-box-shadow: #{$boxShadowV4-LG}; + + --daterange-preset-wrapper-width: 164px; + --daterange-preset-wrapper-padding: 8px; + --daterange-preset-wrapper-bg-color: var(--background-paper, #{$color-v4-background-paper}); + --daterange-preset-wrapper-gap: 4px; + --daterange-preset-item-height: 32px; + --daterange-preset-item-padding: 6px 8px;; + --daterange-preset-item-border-radius: #{$borderRadiusV4-lg}; + --daterange-preset-color: var(--text-primary, #{$color-v4-text-primary}); + --daterange-preset-item-disabled-color: var(--text-disabled, #{$color-v4-text-disabled}); + --daterange-preset-item-hover-bg-color: var(--action-hover, #{$color-v4-action-hover}); + --daterange-preset-item-selected-bg-color: var(--action-selected, #{$color-v4-action-selected}); + + --daterange-calendars-wrapper-margin: 16px; + --daterange-calendars-wrapper-gap: 24px; + --daterange-calendars-prev-next-size: 24px; + --daterange-calendars-prev-next-button-hover-color: var(--default-contrast-text, #{$color-v4-default-contrast-text}); + --daterange-calendars-prev-next-button-hover-bg-color: var(--action-hover, #{$color-v4-action-hover}); + + --daterange-calendars-prev-next-icon-size: 20px; + --daterange-calendars-prev-next-icon-color: var(--action-active, #{$color-v4-action-active}); + + --daterange-timeselect-gap: 16px; + --daterange-timeinput-gap: 8px; + + --daterange-footer-message-color: var(--text-secondary, #{$color-v4-text-secondary}); + --daterange-footer-gap: 8px; + --daterange-footer-padding: 12px 16px; + --daterange-footer-height: #{$spacingV4-600}; + // endregion + + &.open-to-right .fu-daterange-overlay { + left: 0; + } + + .fu-daterange-overlay { + position: absolute; + @extend %notificationLayer; + right: 0; + margin-top: var(--daterange-overlay-top); + display: none; + visibility: hidden; + border: var(--daterange-overlay-border); + border-radius: var(--daterange-overlay-border-radius); + box-shadow: var(--daterange-overlay-box-shadow); + background-color: var(--daterange-overlay-bg-color); + + // region overlay state and position + &.isOpen { + display: flex; + } + + &.visible { + visibility: initial; + } + + &.left { + left: 0; + right: initial; + } + + // endregion + + .fu-daterange-preset-wrapper { + border-top-left-radius: var(--daterange-overlay-border-radius); + border-bottom-left-radius: var(--daterange-overlay-border-radius); + border-right: var(--daterange-overlay-border); + min-width: var(--daterange-preset-wrapper-width); + padding: var(--daterange-preset-wrapper-padding); + @extend %font-v4-body-2; + background-color: var(--daterange-preset-wrapper-bg-color); + + ul { + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: var(--daterange-preset-wrapper-gap); + li { + color: var(--daterange-preset-color); + height: var(--daterange-preset-item-height); + padding: var(--daterange-preset-item-padding); + display: flex; + align-items: center; + border-radius: var(--daterange-preset-item-border-radius); + + // region presets state + &:not(.selected):not(.disabled):hover { + cursor: pointer; + background-color: var(--daterange-preset-item-hover-bg-color); + } + + &.selected { + background-color: var(--daterange-preset-item-selected-bg-color); + } + + &.disabled { + color: var(--daterange-preset-item-disabled-color); + pointer-events: none; + } + + // endregion + } + } + } + + .fu-daterange-calendars-wrapper { + display: flex; + flex-direction: column; + + .fu-daterange-calendars { + position: relative; + flex-grow: 1; + display: flex; + margin: var(--daterange-calendars-wrapper-margin); + align-items: flex-start; + gap: var(--daterange-calendars-wrapper-gap); + + .fu-daterange-prev, .fu-daterange-next { + position: absolute; + top: 8px; + @include size(var(--daterange-calendars-prev-next-size)); + cursor: pointer; + + display: flex; + justify-content: center; + align-items: center; + + .fu-daterange-prev-icon, + .fu-daterange-next-icon{ + @include size(var(--daterange-calendars-prev-next-icon-size)); + color: var(--daterange-calendars-prev-next-icon-color); + } + + &:hover { + background-color: var(--daterange-calendars-prev-next-button-hover-bg-color); + color: var(--daterange-calendars-prev-next-button-hover-color); + } + } + .fu-daterange-next{ + right: 0; + } + } + + .fu-time-selector{ + display: flex; + align-items: center; + gap: var(--daterange-timeselect-gap); + padding: var(--daterange-footer-padding); + height: var(--daterange-footer-height); + border-top: var(--daterange-overlay-border); + + fusion-checkbox{ + flex-grow: 1; + } + + .fu-time-select-wrapper{ + display: flex; + justify-content: flex-end; + align-items: center; + gap: var(--daterange-timeselect-gap); + + .fu-start-time-wrapper, + .fu-end-time-wrapper { + @extend %font-v4-heading-6; + display: flex; + align-items: center; + gap: var(--daterange-timeinput-gap); + } + } + } + + .fu-daterange-actions-footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: var(--daterange-footer-gap); + padding: var(--daterange-footer-padding); + height: var(--daterange-footer-height); + border-top: var(--daterange-overlay-border); + + .fu-daterange-actions-footer-message { + flex-grow: 1; + @extend %font-v4-body-2; + color: var(--daterange-footer-message-color); + } + } + } + } +} diff --git a/projects/fusion-ui/components/daterange/v4/daterange-v4.component.spec.ts b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.spec.ts new file mode 100644 index 000000000..11f5b2670 --- /dev/null +++ b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DaterangeV4Component } from './daterange-v4.component'; + +describe('DaterangeV4Component', () => { + let component: DaterangeV4Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DaterangeV4Component] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DaterangeV4Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/daterange/v4/daterange-v4.component.stories.ts b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.stories.ts new file mode 100644 index 000000000..2bb563597 --- /dev/null +++ b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.stories.ts @@ -0,0 +1,247 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {environment} from '../../../../../stories/environments/environment'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {DaterangeV4Component} from './daterange-v4.component'; +import {DaterangeOptions, DaterangeSelection} from '@ironsource/fusion-ui/components/daterange'; +import {dedent} from 'ts-dedent'; + +const BASE_TEMPLATE = ``; + +const TODAY = new Date(); +const YESTERDAY = new Date(Date.now() - 1000 * 60 * 60 * 24); +const BEFORE_5_DAYS = new Date(Date.now() - 1000 * 60 * 60 * 24 * 5); +const LAST_13_DAYS = new Date(Date.now() - 1000 * 60 * 60 * 24 * 13); // with today, it will 14 + +export default { + title: 'V4/Components/Dates/DateRange', + component: DaterangeV4Component, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, ReactiveFormsModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule] + }), + componentWrapperDecorator(story => `
${story}
`) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + }, + docs: { + description: { + component: dedent`A ***DateRange*** component is a user interface element that allows users to select a range of dates. +To set and get the selected date range, use the ***formControl*** property with interface. + +\`\`\` +interface DaterangeSelection { + startDate?: Date; + endDate?: Date; + startTime?: string; + endTime?: string; +} +\`\`\` + +Also you can set the options for the date range component using the ***options*** property with interface. + +\`\`\` +interface DaterangeOptions { + format?: string; // for date format in placeholder. default is 'd MMM, y' + presets?: DaterangeCustomPreset[] | DaterangePresets[]; // if you don't want to show the presets, you can set it to empty array + placeholder?: string; + overlayAlignPosition?: 'left' | 'right'; + allowFutureSelection?: boolean; + maxDaysInSelectedRange?: number; + withTimeSelect?: boolean; +}\`\`\` +` + } + } + }, + args: { + formControl: new FormControl() as FormControl, + options: {placeholder: 'Select date range', format: 'MMM dd, y'} as DaterangeOptions + }, + argTypes: { + formControl: { + control: false + } + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; +Basic.parameters = { + docs: { + description: { + story: dedent`***options:*** {placeholder: 'Select date range', format: 'MMM dd, y'}` + } + } +}; + +export const Selected: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl({ + startDate: BEFORE_5_DAYS, + endDate: YESTERDAY + }) as FormControl + }, + template: BASE_TEMPLATE + }) +}; +Selected.parameters = { + docs: { + description: { + story: dedent`***formControl:*** +\`\`\` +new FormControl({ + startDate: BEFORE_5_DAYS, + endDate: YESTERDAY +}) as FormControl +\`\`\`` + } + } +}; + +export const SelectedToday: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl({ + startDate: TODAY, + endDate: TODAY + }) as FormControl + }, + template: BASE_TEMPLATE + }) +}; +SelectedToday.parameters = { + docs: { + description: { + story: dedent`***formControl:*** +\`\`\` +new FormControl({ + startDate: TODAY, + endDate: TODAY +}) as FormControl +\`\`\`` + } + } +}; + +export const SelectedLast14Days: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl({ + startDate: LAST_13_DAYS, + endDate: TODAY + }) as FormControl + }, + template: BASE_TEMPLATE + }) +}; + +export const WithoutPresets: Story = { + render: args => ({ + props: { + ...args, + options: {placeholder: 'Select date range', format: 'MMM dd, y', presets: [], overlayAlignPosition: 'left'} as DaterangeOptions + }, + template: BASE_TEMPLATE + }) +}; +WithoutPresets.parameters = { + docs: { + description: { + story: dedent`***options:*** {placeholder: 'Select date range', format: 'MMM dd, y', presets: []}` + } + } +}; + +export const LimitedRange: Story = { + render: args => ({ + props: { + ...args, + options: { + placeholder: 'Select date range', + format: 'MMM dd, y', + maxDaysInSelectedRange: 7, + presets: [], + overlayAlignPosition: 'left' + } as DaterangeOptions + }, + template: BASE_TEMPLATE + }) +}; +LimitedRange.parameters = { + docs: { + description: { + story: dedent`***options:*** {placeholder: 'Select date range', format: 'MMM dd, y', presets: [], maxDaysInSelectedRange: 7}` + } + } +}; + +export const NotAllowFutureDateSelected: Story = { + render: args => ({ + props: { + ...args, + options: {placeholder: 'Select date range', format: 'MMM dd, y', allowFutureSelection: false} as DaterangeOptions + }, + template: BASE_TEMPLATE + }) +}; +NotAllowFutureDateSelected.parameters = { + docs: { + description: { + story: dedent`***options:*** {placeholder: 'Select date range', format: 'MMM dd, y', allowFutureSelection: false}` + } + } +}; + +export const WithTimeSelect: Story = { + render: args => ({ + props: { + ...args, + options: { + placeholder: 'Select date range', + format: 'MMM dd, y', + presets: [], + withTimeSelect: true, + overlayAlignPosition: 'left' + } as DaterangeOptions, + formControl: new FormControl({ + startDate: BEFORE_5_DAYS, + endDate: YESTERDAY, + startTime: '12:00', + endTime: '20:30' + }) as FormControl + }, + template: BASE_TEMPLATE + }) +}; +WithTimeSelect.parameters = { + docs: { + description: { + story: dedent`***options:*** {placeholder: 'Select date range', format: 'MMM dd, y', presets: [], withTimeSelect: true} + ***formControl:*** + \`\`\`new FormControl({ + startDate: BEFORE_5_DAYS, + endDate: YESTERDAY, + startTime: '12:00', + endTime: '20:30' + }) as FormControl + \`\`\` +` + } + } +}; diff --git a/projects/fusion-ui/components/daterange/v4/daterange-v4.component.ts b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.ts new file mode 100644 index 000000000..5b4c0680f --- /dev/null +++ b/projects/fusion-ui/components/daterange/v4/daterange-v4.component.ts @@ -0,0 +1,78 @@ +import {ChangeDetectionStrategy, Component, forwardRef, inject, Input} from '@angular/core'; +import {NG_VALUE_ACCESSOR, ReactiveFormsModule} from '@angular/forms'; +import {CommonModule} from '@angular/common'; +import {BehaviorSubject} from 'rxjs'; +import {IconData, IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {DaterangeBaseComponent} from '@ironsource/fusion-ui/components/daterange/common/base'; +import {ApiBase} from '@ironsource/fusion-ui/components/api-base'; +import {ClickOutsideModule} from '@ironsource/fusion-ui/directives/click-outside'; +import { + DropdownPlaceholder, + DropdownSelectComponent, + DropdownSelectConfigurations +} from '@ironsource/fusion-ui/components/dropdown-select/v4'; +import {ButtonComponent} from '@ironsource/fusion-ui/components/button/v4'; +import {CalendarComponent} from '@ironsource/fusion-ui/components/calendar/v4'; +import {CheckboxComponent} from '@ironsource/fusion-ui/components/checkbox/v4'; +import {InputComponent} from '@ironsource/fusion-ui/components/input/v4'; +import {TestIdsService} from '@ironsource/fusion-ui/services/test-ids'; +import {DateRangeTestIdModifiers} from '@ironsource/fusion-ui/entities'; +import {GenericPipe} from '@ironsource/fusion-ui/pipes/generic'; + +@Component({ + selector: 'fusion-daterange', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [ + CommonModule, + ReactiveFormsModule, + IconModule, + ClickOutsideModule, + DropdownSelectComponent, + ButtonComponent, + CalendarComponent, + CheckboxComponent, + InputComponent, + GenericPipe + ], + templateUrl: './daterange-v4.component.html', + styleUrl: './daterange-v4.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + {provide: ApiBase, useExisting: DaterangeV4Component}, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DaterangeV4Component), + multi: true + } + ] +}) +export class DaterangeV4Component extends DaterangeBaseComponent { + /** @internal */ + @Input() selectorIcon: IconData = 'ph/calendar-blank'; + @Input() footerMessage: string = 'All dates are in UTC'; + + @Input() testId: string; + /** @internal */ + testIdModifiers: typeof DateRangeTestIdModifiers = DateRangeTestIdModifiers; + /** @internal */ + testIdsService: TestIdsService = inject(TestIdsService); + + /** @internal */ + dropdownSelectConfigurations$ = new BehaviorSubject({ + placeholder: {value: 'Select'} + }); + + /** @internal */ + pevIconName = 'ph/caret-left'; + /** @internal */ + nextIconName = 'ph/caret-right'; + + get isOpen(): boolean { + return this.dropdownSelectConfigurations$.getValue().isOpen; + } + + get placeholder(): DropdownPlaceholder { + return this.dropdownSelectConfigurations$.getValue().placeholder; + } +} diff --git a/projects/fusion-ui/components/daterange/v4/index.ts b/projects/fusion-ui/components/daterange/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/daterange/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/daterange/v4/ng-package.json b/projects/fusion-ui/components/daterange/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/daterange/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/daterange/v4/public-api.ts b/projects/fusion-ui/components/daterange/v4/public-api.ts new file mode 100644 index 000000000..9f55a2df8 --- /dev/null +++ b/projects/fusion-ui/components/daterange/v4/public-api.ts @@ -0,0 +1,8 @@ +export {DaterangeV4Component as DaterangeComponent} from './daterange-v4.component'; +export * from '@ironsource/fusion-ui/components/daterange/entities'; +export { + DaterangeService, + DEFAULT_DATE_FORMAT, + DEFAULT_DATERANGE_PRESET_LIST, + DEFAULT_DATERANGE_PRESET_NAMES +} from '@ironsource/fusion-ui/components/daterange/common/base'; diff --git a/projects/fusion-ui/components/draggable-items-list/index.ts b/projects/fusion-ui/components/draggable-items-list/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/draggable-items-list/ng-package.json b/projects/fusion-ui/components/draggable-items-list/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/draggable-items-list/public-api.ts b/projects/fusion-ui/components/draggable-items-list/public-api.ts new file mode 100644 index 000000000..a86af1af2 --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/public-api.ts @@ -0,0 +1 @@ +export * from './v4'; diff --git a/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.html b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.html new file mode 100644 index 000000000..520a050a9 --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.html @@ -0,0 +1,20 @@ +
    + @for (item of items; track item.label; let i = $index) { +
  • +
    + +
    +
    +
    {{ item.label }}
    + +
    +
  • + } +
diff --git a/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.scss b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.scss new file mode 100644 index 000000000..68bfc7941 --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.scss @@ -0,0 +1,105 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; + + --fu-list-items-gap: #{$spacingV4-75}; + --fu-list-item-padding: 0 8px 0 0; + --fu-list-item-heigth: #{$spacingV4-400}; + --fu-list-item-background-color: var(--background-paper, #{$color-v4-background-paper}); + --fu-list-item-drag-over-background-color: var(--background-paper-elevation-2, #{$color-v4-background-paper-elevation-2}); + --fu-list-item-drag-over-border-color: var(--background-paper-elevation-2, #{$color-v4-background-paper-elevation-2}); + --fu-list-item-border-color: var(--default-outlinedBorder, #{$color-v4-default-outlined-border}); + --fu-list-item-hover-border-color: var(--action-active, #{$color-v4-action-active}); + --fu-list-item-border: 1px solid var(--fu-list-item-border-color); + --fu-list-item-border-radius: #{$spacingV4-50}; + --fu-list-item-drag-icon-size: #{$spacingV4-250}; + --fu-list-item-drag-icon-padding: 0 8px; + --fu-list-item-drag-icon-color: var(--action-active, #{$color-v4-action-active}); + --fu-list-item-hover-drag-icon-color: var(--action-primary, #{$color-v4-action-primary}); + --fu-list-item-drag-label-color: var(--text-primary, #{$color-v4-text-primary}); + --fu-list-item-draging-box-shadow: #{$boxShadowV4-LG}; + --fu-list-item-content-gap: #{$spacingV4-50}; + + .fu-items-wrapper { + display: flex; + flex-direction: column; + gap: var(--fu-list-items-gap); + padding: 0; + margin: 0; + list-style-type: none; + + .fu-list-item { + user-select: none; + padding: var(--fu-list-item-padding); + margin: 0; + width: 100%; + height: var(--fu-list-item-heigth); + display: flex; + align-items: center; + border: var(--fu-list-item-border); + border-radius: var(--fu-list-item-border-radius); + background-color: var(--fu-list-item-background-color); + + .fu-item-drag-icon { + display: flex; + align-items: center; + justify-content: center; + padding: var(--fu-list-item-drag-icon-padding); + + .fu-drag-icon { + @include size(var(--fu-list-item-drag-icon-size)); + color: var(--fu-list-item-drag-icon-color); + } + } + + .fu-item-content { + flex: 1; + display: flex; + align-items: center; + gap: var(--fu-list-item-border); + + .fu-item-label { + @extend %font-v4-body-2; + color: var(--fu-list-item-drag-label-color); + } + + fusion-icon-button { + margin-left: auto; + } + } + + &:hover { + border-color: var(--fu-list-item-hover-border-color); + cursor: grab; + .fu-item-drag-icon .fu-drag-icon{ + color: var(--fu-list-item-hover-drag-icon-color); + } + } + + &.dragging{ + cursor: grab; + &:active{ + cursor: grab; + box-shadow: var(--fu-list-item-draging-box-shadow); + } + } + + &.dragging-transit { + color: transparent; + background-color: var(--fu-list-item-drag-over-background-color); + border-color: var(--fu-list-item-drag-over-border-color); + .fu-item-drag-icon, + .fu-item-content { + visibility: hidden; + } + &:hover { + border-color: var(--fu-list-item-drag-over-border-color); + } + } + + } + } + +} \ No newline at end of file diff --git a/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.spec.ts b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.spec.ts new file mode 100644 index 000000000..3457634b1 --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.spec.ts @@ -0,0 +1,72 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DraggableItemsListComponent } from './draggable-items-list.component'; +import {ItemDragAndDrop} from "./draggable-items-list.entities"; + +const ITEMS: ItemDragAndDrop[] = [ + {id: 1, label: 'Milk shake'}, + {id: 2, label: 'Cocktails'}, + {id: 3, label: 'Fruit salad'}, + {id: 4, label: 'Coffee'}, +] + +describe('DraggableItemsListComponent', () => { + let component: DraggableItemsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DraggableItemsListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DraggableItemsListComponent); + component = fixture.componentInstance; + component.items = ITEMS; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have item-wrapper', () => { + const itemWrapper: HTMLElement = fixture.nativeElement.querySelector('.fu-items-wrapper'); + expect(itemWrapper).toBeTruthy() + expect(itemWrapper.tagName).toBe('UL') + }); + + it('should have items list', () => { + const items: NodeList = fixture.nativeElement.querySelectorAll('.fu-items-wrapper .fu-list-item'); + expect(items).toBeTruthy(); + expect(items.length).toBe(ITEMS.length); + }); + + it('should have item', () => { + const item: HTMLElement = fixture.nativeElement.querySelector('.fu-items-wrapper .fu-list-item'); + expect(item).toBeTruthy(); + expect(item.getAttribute('data-id')).toBe(ITEMS[0].id.toString()); + }); + + it('should have item drag icon', () => { + const item: HTMLElement = fixture.nativeElement.querySelector('.fu-items-wrapper .fu-list-item'); + const itemDragIcon: HTMLElement = item.querySelector('.fu-item-drag-icon .fu-drag-icon'); + expect(itemDragIcon).toBeTruthy(); + expect(itemDragIcon.classList).toContain('dots-six-vertical-bold'); + }); + + it('should have item label', () => { + const item: HTMLElement = fixture.nativeElement.querySelector('.fu-items-wrapper .fu-list-item'); + const itemLabel: HTMLElement = item.querySelector('.fu-item-content .fu-item-label'); + expect(itemLabel).toBeTruthy(); + expect(itemLabel.textContent).toBe(ITEMS[0].label); + }); + + it('should have item remove icon', () => { + const item: HTMLElement = fixture.nativeElement.querySelector('.fu-items-wrapper .fu-list-item'); + const itemRemoveIconButton: HTMLElement = item.querySelector('.fu-item-content fusion-icon-button'); + expect(itemRemoveIconButton).toBeTruthy(); + expect(itemRemoveIconButton.getAttribute('iconname')).toBe('ph/x'); + expect(itemRemoveIconButton.getAttribute('size')).toBe('extraSmall'); + }); + +}); diff --git a/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.stories.ts b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.stories.ts new file mode 100644 index 000000000..87aff1c1a --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.stories.ts @@ -0,0 +1,89 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {action} from '@storybook/addon-actions'; +import {CommonModule} from '@angular/common'; +import {environment} from 'stories/environments/environment'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {DraggableItemsListComponent} from './draggable-items-list.component'; +import {ItemDragAndDrop} from './draggable-items-list.entities'; +import {dedent} from 'ts-dedent'; + +const actionsData = { + orderChanged: action('orderChanged'), + itemRemoved: action('itemRemoved') +}; + +const ITEMS: ItemDragAndDrop[] = [{label: 'Milk shake'}, {label: 'Cocktails'}, {label: 'Fruit salad'}, {label: 'Coffee'}]; + +export default { + title: 'V4/Components/DragAndDrop/Draggable Items List', + component: DraggableItemsListComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule] + }), + componentWrapperDecorator(story => `
${story}
`) + ], + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: dedent` +**DraggableItemsListComponent** is an interactive UI component designed to display a list of items that can be rearranged by dragging and dropping. + +This component used ***DragAndDropDirective*** to handle the drag and drop functionality. You can use this directive to any element list in DOM. + +#####Example directive usage: +\`\`\`html +
    + @for (item of items; track item.label; let i = $index) { +
  • +
    + +
    +
    +
    {{ item.label }}
    + +
    +
  • + } +
+\`\`\` + +here: +- ***fusionDragAndDrop***: directive selector +- ***#draggableItem***: template reference variable to get the list of items +- ***dragElementDrop***: event emitter to handle the drop event it will emit the changes ***DragAndDropListChanges*** of the list + +\`\`\`typescript +interface DragAndDropListChanges { + element: HTMLElement; + fromIndex: number; + toIndex: number; +} + \`\`\` + +` + } + }, + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + items: ITEMS + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.ts b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.ts new file mode 100644 index 000000000..c2ac48305 --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.component.ts @@ -0,0 +1,35 @@ +import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from '@angular/core'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {DragAndDropDirective, DragAndDropListChanges} from '@ironsource/fusion-ui/directives/drag-and-drop'; +import {IconButtonComponent} from '@ironsource/fusion-ui/components/button/v4'; +import {ItemDragAndDrop} from './draggable-items-list.entities'; + +@Component({ + selector: 'fusion-draggable-items-list', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [IconModule, DragAndDropDirective, IconButtonComponent], + templateUrl: './draggable-items-list.component.html', + styleUrl: './draggable-items-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DraggableItemsListComponent { + @Input() set items(value: ItemDragAndDrop[]) { + if (Array.isArray(value)) { + this.#items = [...value]; + } + } + get items(): ItemDragAndDrop[] { + return this.#items; + } + + @Output() orderChanged = new EventEmitter(); + @Output() itemRemoved = new EventEmitter<{removedAtIndex: number; itemList: ItemDragAndDrop[]}>(); + + #items: ItemDragAndDrop[] = []; + + removeItem(index: number) { + this.#items.splice(index, 1); + this.itemRemoved.emit({removedAtIndex: index, itemList: this.items}); + } +} diff --git a/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.entities.ts b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.entities.ts new file mode 100644 index 000000000..df7e1f396 --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/draggable-items-list.entities.ts @@ -0,0 +1,4 @@ +export interface ItemDragAndDrop { + id?: string | number; + label: string; +} diff --git a/projects/fusion-ui/components/draggable-items-list/v4/index.ts b/projects/fusion-ui/components/draggable-items-list/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/draggable-items-list/v4/ng-package.json b/projects/fusion-ui/components/draggable-items-list/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/draggable-items-list/v4/public-api.ts b/projects/fusion-ui/components/draggable-items-list/v4/public-api.ts new file mode 100644 index 000000000..1e27ad6dc --- /dev/null +++ b/projects/fusion-ui/components/draggable-items-list/v4/public-api.ts @@ -0,0 +1 @@ +export {DraggableItemsListComponent} from './draggable-items-list.component'; diff --git a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4-button-icon.component.stories.ts b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4-button-icon.component.stories.ts new file mode 100644 index 000000000..7c668ecf9 --- /dev/null +++ b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4-button-icon.component.stories.ts @@ -0,0 +1,87 @@ +import {Meta, StoryObj, moduleMetadata, componentWrapperDecorator} from '@storybook/angular'; +import {dedent} from 'ts-dedent'; +import {CommonModule} from '@angular/common'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {DropdownSelectV4Component} from './dropdown-select-v4.component'; +import {DropdownOption} from '@ironsource/fusion-ui/components/dropdown-option'; +import {DropdownComponent} from '@ironsource/fusion-ui/components/dropdown/v4'; + +const foodOptionsList: DropdownOption[] = [ + { + id: 'pizza', + displayText: 'Pizza' + }, + { + id: 'hamburger', + displayText: 'Hamburger' + }, + { + id: 'plant', + displayText: 'Vegan' + }, + { + id: 'bowl-food', + displayText: 'Noodles' + }, + { + id: 'coffee', + displayText: 'Coffee' + } +]; + +export default { + title: 'V4/Components/Dropdown/Triggers/IconButton', + component: DropdownSelectV4Component, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), DropdownComponent] + }) + ], + tags: ['autodocs'], + parameters: { + docs: { + description: { + component: dedent`***DropdownSelectComponent v4 IconButton***.` + } + }, + options: { + showPanel: true, + panelPosition: 'bottom' + } + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: args => ({ + props: args, + template: ` +
+ +
+` + }), + decorators: [componentWrapperDecorator(story => `
${story}
`)] +}; + +export const WithDropdown: Story = { + render: args => ({ + props: { + ...args, + optionsFood: foodOptionsList + }, + template: ` +
+ +
+` + }) +}; diff --git a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4-button.component.stories.ts b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4-button.component.stories.ts index 4d0491c0e..09e7da0d1 100644 --- a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4-button.component.stories.ts +++ b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4-button.component.stories.ts @@ -4,6 +4,31 @@ import {CommonModule} from '@angular/common'; import {SvgModule} from '@ironsource/fusion-ui/components/svg'; import {environment} from '../../../../../stories/environments/environment'; import {DropdownSelectV4Component} from './dropdown-select-v4.component'; +import {DropdownOption} from '@ironsource/fusion-ui/components/dropdown-option'; +import {DropdownComponent} from '@ironsource/fusion-ui/components/dropdown/v4'; + +const foodOptionsList: DropdownOption[] = [ + { + id: 'pizza', + displayText: 'Pizza' + }, + { + id: 'hamburger', + displayText: 'Hamburger' + }, + { + id: 'plant', + displayText: 'Vegan' + }, + { + id: 'bowl-food', + displayText: 'Noodles' + }, + { + id: 'coffee', + displayText: 'Coffee' + } +]; export default { title: 'V4/Components/Dropdown/Triggers/Button', @@ -11,7 +36,7 @@ export default { decorators: [ moduleMetadata({ declarations: [], - imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath})] + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), DropdownComponent] }) ], tags: ['autodocs'], @@ -42,62 +67,101 @@ export const Default: Story = { decorators: [componentWrapperDecorator(story => `
${story}
`)] }; -export const Disabled: Story = { +export const Variants: Story = { render: args => ({ - props: { - ...args, - disabled: true - }, + props: args, template: `
- +
+ +
+
+ +
` - }), - decorators: [componentWrapperDecorator(story => `
${story}
`)] + }) +}; + +export const Size: Story = { + render: args => ({ + props: args, + template: ` +
+
+ +
+
+ +
+
+` + }) }; export const Icon: Story = { render: args => ({ props: args, template: ` +
+ + + + + +
+` + }), + decorators: [componentWrapperDecorator(story => `
${story}
`)] +}; + +export const Disabled: Story = { + render: args => ({ + props: { + ...args, + disabled: true + }, + template: `
- - - +
` }), decorators: [componentWrapperDecorator(story => `
${story}
`)] }; -export const Variant: Story = { +export const WithDropdown: Story = { render: args => ({ - props: args, + props: { + ...args, + optionsFood: foodOptionsList + }, template: ` -
-
- -
-
- -
+
+
` }) }; -export const Size: Story = { +export const AddParam: Story = { render: args => ({ - props: args, + props: { + ...args, + optionsFood: foodOptionsList + }, template: ` -
-
- -
-
- -
+
+
` }) diff --git a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.html b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.html index 80b2cd056..02429ea96 100644 --- a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.html +++ b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.html @@ -5,11 +5,23 @@ [class.fu-success]="validationState === 'success'" [class.fu-warning]="validationState === 'warning'" > - - - + @if (imageUrl) { + + } + @if (country) { + + } + @if (icon) { + + }
- + @if (!hideCaretIcon) { + + }
diff --git a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.scss b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.scss index 272633883..a591e9054 100644 --- a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.scss +++ b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.scss @@ -20,36 +20,38 @@ height: 28px; padding: 4px 8px; border-radius: var(--border-radius-md, #{$borderRadiusV4-md}); - border: solid 1px var(--action-outlined-border, #{$color-v4-action-outlined-border}); + outline: solid 1px var(--action-outlined-border, #{$color-v4-action-outlined-border}); background-color: var(--default-main, #{$color-v4-default-main}); - .fu-placeholder-text{ + .fu-placeholder-text { flex-grow: 1; } - .fu-dropdown-select-image{ + .fu-dropdown-select-image { @include size(20px); border-radius: 2px; } .fu-arrow-icon, .fu-dropdown-select-icon { - @include size(16px); + @include size(var(--dropdown-caret-icon, 20px)); color: var(--action-active, #{$color-v4-action-active}); margin-left: auto; flex-grow: 0; } &:hover { - border-color: var(--action-active, #{$color-v4-action-active}); + outline-color: var(--action-active, #{$color-v4-action-active}); background-color: var(--action-hover, #{$color-v4-action-hover}); cursor: pointer; } + &.fu-disabled { color: var(--text-disabled, #{$color-v4-text-disabled}); - border-color: var(--action-outlined-border, #{$color-v4-action-outlined-border}); + outline-color: var(--action-outlined-border, #{$color-v4-action-outlined-border}); background-color: var(--action-disabled-background, #{$color-v4-action-disabled-background}); pointer-events: none; + .fu-arrow-icon, .fu-dropdown-select-icon { color: var(--text-disabled, #{$color-v4-text-disabled}); @@ -60,53 +62,63 @@ &.fu-error, &.fu-success, &.fu-warning { - border-width: 2px; - padding: 3px 7px; + outline-width: 2px; } &.fu-open { - border-color: var(--primary-main, #{$color-v4-primary-main}); + outline-color: var(--primary-main, #{$color-v4-primary-main}); background-color: var(--action-hover, #{$color-v4-action-hover}); } + &.fu-error { - border-color: var(--error-main, #{$color-v4-error-main}); + outline-color: var(--error-main, #{$color-v4-error-main}); } - &.fu-success{ - border-color: var(--success-main, #{$color-v4-success-main}); + + &.fu-success { + outline-color: var(--success-main, #{$color-v4-success-main}); } - &.fu-warning{ - border-color: var(--warning-main, #{$color-v4-warning-main}); + + &.fu-warning { + outline-color: var(--warning-main, #{$color-v4-warning-main}); } &.fu-size-small { height: 24px; padding: 2px 6px; + .fu-dropdown-select-icon { @include size(16px); } - .fu-dropdown-select-image{ + + .fu-dropdown-select-image { @include size(16px); border-radius: 4px; } } + &.fu-size-large { height: 32px; padding: 6px 8px; + .fu-dropdown-select-icon { @include size(20px); } - .fu-dropdown-select-image{ + + .fu-dropdown-select-image { @include size(20px); border-radius: 4px; } } + &.fu-size-xlarge { height: 40px; padding: 10px 8px; + .fu-dropdown-select-icon { @include size(16px); } - .fu-dropdown-select-image{ + + .fu-dropdown-select-image { @include size(24px); border-radius: 4px; } @@ -114,37 +126,93 @@ } &.fu-mode-button, - &.fu-mode-button-text{ - .fu-dropdown-select-wrapper{ + &.fu-mode-button-add, + &.fu-mode-button-icon, + &.fu-mode-button-text { + .fu-dropdown-select-wrapper { background-color: transparent; + gap: var(--spacing-100, #{$spacingV4-100}); @extend %font-v4-button; - .fu-dropdown-select-icon{ + .fu-dropdown-select-icon { color: var(--text-primary, #{$color-v4-text-primary}); } + &:hover { background-color: var(--action-hover, #{$color-v4-action-hover}); } + &.fu-disabled { background-color: transparent; } + &.fu-open { - border-width: 2px; + outline-width: 2px; padding: 3px 7px; - border-color: var(--action-active, #{$color-v4-action-active}); + outline-color: var(--action-active, #{$color-v4-action-active}); background-color: var(--action-selected, #{$color-v4-action-selected}); - &.fu-size-small{ + + &.fu-size-small { padding: 1px 5px; } } } } - &.fu-mode-button-text{ - .fu-dropdown-select-wrapper{ - border-color: transparent; + + &.fu-mode-button-add, + &.fu-mode-button-icon, + &.fu-mode-button-text { + .fu-dropdown-select-wrapper { + outline-color: transparent; + + &.fu-open { + outline-color: transparent; + background-color: var(--default-main, #{$color-v4-default-main}); + } + } + } + + &.fu-mode-button-icon { + .fu-dropdown-select-wrapper { + padding: 4px; + + &.fu-open { + padding: 4px; + outline-width: 1px; + } + + .fu-dropdown-select-icon { + @include size(20px); + } + + .fu-placeholder-text { + display: none; + } } } - &:has(.fu-dropdown-select-wrapper.fu-disabled){ + &:has(.fu-dropdown-select-wrapper.fu-disabled) { pointer-events: none; } +} + +:host-context(fusion-daterange) { + .fu-dropdown-select-wrapper { + gap: 4px; + padding: 6px; + } + + &.fu-mode-button, + &.fu-mode-button-add, + &.fu-mode-button-icon, + &.fu-mode-button-text { + .fu-dropdown-select-wrapper { + @extend %font-v4-chip-label; + letter-spacing: normal; + gap: 4px; + &.fu-open { + padding: 6px 5px; + } + } + + } } \ No newline at end of file diff --git a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.ts b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.ts index c70f5154c..25135c3f5 100644 --- a/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.ts +++ b/projects/fusion-ui/components/dropdown-select/v4/dropdown-select-v4.component.ts @@ -1,4 +1,4 @@ -import {ChangeDetectionStrategy, Component, Injector, Input} from '@angular/core'; +import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; import {CommonModule} from '@angular/common'; import {TooltipModule} from '@ironsource/fusion-ui/components/tooltip'; import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; @@ -28,8 +28,10 @@ export class DropdownSelectV4Component { @Input() iconColor: string; @Input() testId: string; @Input() country: CountryCode | string; - testIdDropdownModifiers: typeof DropdownTestIdModifiers = DropdownTestIdModifiers; - testIdsService: TestIdsService = this.injector.get(TestIdsService); + @Input() hideCaretIcon: boolean = false; - constructor(private injector: Injector) {} + /** @internal */ + testIdDropdownModifiers: typeof DropdownTestIdModifiers = DropdownTestIdModifiers; + /** @internal */ + testIdsService: TestIdsService = inject(TestIdsService); } diff --git a/projects/fusion-ui/components/dropdown/common/base/dropdown.base.component.ts b/projects/fusion-ui/components/dropdown/common/base/dropdown.base.component.ts index a4fd58f20..cea527920 100644 --- a/projects/fusion-ui/components/dropdown/common/base/dropdown.base.component.ts +++ b/projects/fusion-ui/components/dropdown/common/base/dropdown.base.component.ts @@ -32,7 +32,12 @@ import {DropdownSearchComponent} from '@ironsource/fusion-ui/components/dropdown import {DropdownSelectComponent} from '@ironsource/fusion-ui/components/dropdown-select'; import {DropdownSelectConfigurations} from '@ironsource/fusion-ui/components/dropdown-select/entities'; import {DROPDOWN_DEBOUNCE_TIME, DROPDOWN_OPTIONS_WITHOUT_SCROLL} from './dropdown-config'; -import {BackendPagination, ClosedOptions, DropdownPlaceholderConfiguration} from '@ironsource/fusion-ui/components/dropdown/entities'; +import { + BackendPagination, + ClosedOptions, + DropdownPlaceholderConfiguration, + DropdownTriggerMode +} from '@ironsource/fusion-ui/components/dropdown/entities'; import {ApiBase} from '@ironsource/fusion-ui/components/api-base'; import {DropdownTestIdModifiers} from '@ironsource/fusion-ui/entities'; import {TestIdsService} from '@ironsource/fusion-ui/services/test-ids'; @@ -155,7 +160,7 @@ export abstract class DropdownBaseComponent extends ApiBase implements OnInit, O if (typeof value === 'string') { this.placeholderText = value ?? 'Please Select'; } else { - this.placeholderText = value?.placeholderText || 'Please Select'; + this.placeholderText = value?.placeholderText ?? 'Please Select'; this.placeholderIcon = value?.icon; this.forcePlaceholderOnSelection = value?.isForcedPlaceholder ? value?.isForcedPlaceholder : this.forcePlaceholderOnSelection; } @@ -243,6 +248,7 @@ export abstract class DropdownBaseComponent extends ApiBase implements OnInit, O /** @ignore */ chipDefaultContent: string; + protected _triggerMode: DropdownTriggerMode = 'default'; private _optionsTitle: string; protected _error: string; private _isLocatedRight = false; @@ -499,6 +505,9 @@ export abstract class DropdownBaseComponent extends ApiBase implements OnInit, O * @ignore */ setLabel() { + if (this._triggerMode === 'button-add') { + return; + } this.labelImageSrc = undefined; let placeholder = this.initPlaceholder; let placeholderForSearch = this.searchPlaceholder; diff --git a/projects/fusion-ui/components/dropdown/entities/dropdown-placeholder-configuration.ts b/projects/fusion-ui/components/dropdown/entities/dropdown-placeholder-configuration.ts index 7b47b303b..1865eb407 100644 --- a/projects/fusion-ui/components/dropdown/entities/dropdown-placeholder-configuration.ts +++ b/projects/fusion-ui/components/dropdown/entities/dropdown-placeholder-configuration.ts @@ -10,3 +10,5 @@ export interface SelectedItemName { singular: string; plural: string; } + +export type DropdownTriggerMode = 'button' | 'button-text' | 'button-add' | 'button-icon' | 'default'; diff --git a/projects/fusion-ui/components/dropdown/v4/dropdown-v4.component.html b/projects/fusion-ui/components/dropdown/v4/dropdown-v4.component.html index 0898d9193..a916edfdb 100644 --- a/projects/fusion-ui/components/dropdown/v4/dropdown-v4.component.html +++ b/projects/fusion-ui/components/dropdown/v4/dropdown-v4.component.html @@ -7,12 +7,22 @@ @if(labelText){ } -
- -
- - + @if (templateRef){ +
+ +
+ } @else if (dynamicTrigger){ +
+ +
+ } @else { + + } + @for (listItem of list; track listItem){ +
  • + @if (listItem.flag){ + + } + @if (listItem.imageUrl){ + listItem.label + } +
    {{listItem.label}}
    +
  • + } + + diff --git a/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.scss b/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.scss new file mode 100644 index 000000000..39d701d56 --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.scss @@ -0,0 +1,41 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; + + .fu-list-wrapper{ + display: flex; + flex-direction: column; + width: var(--fu-item-list-width, 224px); + padding: var(--fu-item-list-padding, 0 8px); + margin: 0; + list-style-type: none; + border-radius: var(--fu-item-list-border-radius, 8px); + border: var(--fu-item-list-border, 1px solid var(--common-divider, #{$color-v4-common-divider})); + box-shadow: var(--fu-item-list-box-shadow, #{$boxShadowV4-MD}); + background-color: var(--fu-item-list-background-color, var(--background-default, #{$color-v4-background-default})); + max-height: var(--fu-item-list-max-height, 210px); + overflow-x: hidden; + overflow-y: auto; + --fu-custom-scroll-bg-color: transparent; + @extend %customScroll; + + .fu-list-item{ + display: flex; + align-items: center; + gap: 8px; + @extend %font-v4-body-2; + color: var(--fu-item-color, var(--text-primary, #{$color-v4-text-primary})); + padding: var(--fu-item-padding, 6px 8px); + + .fu-list-item-image{ + @include size(20px); + border-radius: 4px; + } + } + } + .truncate{ + @extend %truncate; + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.spec.ts b/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.spec.ts new file mode 100644 index 000000000..d835e8acf --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.spec.ts @@ -0,0 +1,79 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DroppedListComponent } from './dropped-list.component'; +import { + APPLICATION_LIST_OPTIONS, + BASE_LIST_OPTIONS, + COUNTRY_LIST_OPTIONS +} from "@ironsource/fusion-ui/components/dropped-list/v4/dropped-list.mock"; + +describe('DroppedListComponent', () => { + let component: DroppedListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DroppedListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DroppedListComponent); + component = fixture.componentInstance; + component.list = BASE_LIST_OPTIONS; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should has li list elements', () => { + const itemsEl = fixture.nativeElement.querySelectorAll('li'); + expect(itemsEl.length).toBe(BASE_LIST_OPTIONS.length); + }); + + it('should has li item with text', () => { + const itemsEl = fixture.nativeElement.querySelectorAll('li'); + itemsEl.forEach((item, index) => { + expect(item.textContent).toBe(BASE_LIST_OPTIONS[index].label); + }); + }); + + describe('DroppedListComponent with countries', () => { + beforeEach(async () => { + fixture = TestBed.createComponent(DroppedListComponent); + component = fixture.componentInstance; + component.list = COUNTRY_LIST_OPTIONS; + fixture.detectChanges(); + }); + + it('should has li item with Country flag and name', () => { + const itemsEl = fixture.nativeElement.querySelectorAll('li'); + itemsEl.forEach((item, index) => { + const flagEl = item.querySelector('fusion-flag'); + expect(flagEl.getAttribute('ng-reflect-country-code')).toBe(COUNTRY_LIST_OPTIONS[index].flag); + expect(item.textContent).toBe(COUNTRY_LIST_OPTIONS[index].label); + }); + }); + }); + + describe('DroppedListComponent with applications', () => { + beforeEach(async () => { + fixture = TestBed.createComponent(DroppedListComponent); + component = fixture.componentInstance; + component.list = APPLICATION_LIST_OPTIONS; + fixture.detectChanges(); + }); + + it('should has li item with application image and name', () => { + const itemsEl = fixture.nativeElement.querySelectorAll('li'); + itemsEl.forEach((item, index) => { + const appImageEl = item.querySelector('img.fu-list-item-image'); + expect(appImageEl.getAttribute('src')).toBe(APPLICATION_LIST_OPTIONS[index].imageUrl); + expect(item.textContent).toBe(APPLICATION_LIST_OPTIONS[index].label); + }); + }); + }); + + +}); diff --git a/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.stories.ts b/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.stories.ts new file mode 100644 index 000000000..bc062dc19 --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.stories.ts @@ -0,0 +1,40 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {APPLICATION_LIST_OPTIONS, BASE_LIST_OPTIONS, COUNTRY_LIST_OPTIONS} from './dropped-list.mock'; +import {DroppedListComponent} from './dropped-list.component'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {FlagComponent} from '@ironsource/fusion-ui/components/flag/v4'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; + +export default { + title: 'V4/Components/DataDisplay/Text with dropped list/Dropped List', + component: DroppedListComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), FlagComponent, TooltipDirective] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + list: BASE_LIST_OPTIONS + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const WithApplication: Story = {}; +WithApplication.args = {list: APPLICATION_LIST_OPTIONS}; + +export const WithFlag: Story = {}; +WithFlag.args = {list: COUNTRY_LIST_OPTIONS}; diff --git a/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.ts b/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.ts new file mode 100644 index 000000000..915da0a11 --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/dropped-list.component.ts @@ -0,0 +1,17 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {DroppedListOption} from './dropped-list.entities'; +import {FlagComponent} from '@ironsource/fusion-ui/components/flag/v4'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; + +@Component({ + selector: 'fusion-dropped-list', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [FlagComponent, TooltipDirective], + templateUrl: './dropped-list.component.html', + styleUrl: './dropped-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DroppedListComponent { + @Input() list: DroppedListOption[] = []; +} diff --git a/projects/fusion-ui/components/dropped-list/v4/dropped-list.entities.ts b/projects/fusion-ui/components/dropped-list/v4/dropped-list.entities.ts new file mode 100644 index 000000000..c2ef35922 --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/dropped-list.entities.ts @@ -0,0 +1,8 @@ +import {CountryCode} from '@ironsource/fusion-ui/components/flag/v4'; + +export interface DroppedListOption { + id?: string | number; + label: string; + flag?: CountryCode; + imageUrl?: string; +} diff --git a/projects/fusion-ui/components/dropped-list/v4/dropped-list.mock.ts b/projects/fusion-ui/components/dropped-list/v4/dropped-list.mock.ts new file mode 100644 index 000000000..3e42ba5c7 --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/dropped-list.mock.ts @@ -0,0 +1,19 @@ +import { + MOCK_OPTIONS_COUNTRIES, + MOK_APPLICATIONS_ONE_LINE_OPTIONS +} from '@ironsource/fusion-ui/components/dropdown/v3/stories/dropdown.mock'; +import {CountryCode} from '@ironsource/fusion-ui/components/flag/v4'; + +export const BASE_LIST_OPTIONS = new Array(10).fill(null).map((_, index) => ({ + label: `Option ${index + 1}` +})); + +export const COUNTRY_LIST_OPTIONS = MOCK_OPTIONS_COUNTRIES.map(country => ({ + flag: country.flag.toLowerCase() as CountryCode, + label: country.title +})); + +export const APPLICATION_LIST_OPTIONS = MOK_APPLICATIONS_ONE_LINE_OPTIONS.map(country => ({ + label: country.displayText, + imageUrl: country.image +})); diff --git a/projects/fusion-ui/components/dropped-list/v4/index.ts b/projects/fusion-ui/components/dropped-list/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/dropped-list/v4/ng-package.json b/projects/fusion-ui/components/dropped-list/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/dropped-list/v4/public-api.ts b/projects/fusion-ui/components/dropped-list/v4/public-api.ts new file mode 100644 index 000000000..c0fed58f9 --- /dev/null +++ b/projects/fusion-ui/components/dropped-list/v4/public-api.ts @@ -0,0 +1,2 @@ +export * from './dropped-list.component'; +export * from './dropped-list.entities'; diff --git a/projects/fusion-ui/components/dynamic-components/common/entities/dynamic-component.ts b/projects/fusion-ui/components/dynamic-components/common/entities/dynamic-component.ts index ea3b740e8..bdb54aee4 100644 --- a/projects/fusion-ui/components/dynamic-components/common/entities/dynamic-component.ts +++ b/projects/fusion-ui/components/dynamic-components/common/entities/dynamic-component.ts @@ -1,10 +1,12 @@ import {Type, Component, TemplateRef} from '@angular/core'; +export interface DynamicComponent { + type: Type; + data?: any; +} + export interface DynamicComponentConfiguration { - component?: { - type: Type; - data?: any; - }; + component?: DynamicComponent; element?: Node; htmlSnippet?: string; templateRef?: TemplateRef; diff --git a/projects/fusion-ui/components/empty-state/index.ts b/projects/fusion-ui/components/empty-state/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/empty-state/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/empty-state/ng-package.json b/projects/fusion-ui/components/empty-state/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/components/empty-state/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/empty-state/public-api.ts b/projects/fusion-ui/components/empty-state/public-api.ts new file mode 100644 index 000000000..a86af1af2 --- /dev/null +++ b/projects/fusion-ui/components/empty-state/public-api.ts @@ -0,0 +1 @@ +export * from './v4'; diff --git a/projects/fusion-ui/components/empty-state/v4/empty-state.component.html b/projects/fusion-ui/components/empty-state/v4/empty-state.component.html new file mode 100644 index 000000000..0f6d80671 --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/empty-state.component.html @@ -0,0 +1,49 @@ +@switch (type) { + @case ('error') { +
    + +
    + } + @case ('accessDenied') { +
    + +
    + } + @case ('noResult') { +
    + +
    + } + @case ('noData') { +
    + +
    + } + @case ('chart') { +
    + +
    + } + @case ('files') { +
    + +
    + } + @case ('settings') { +
    + +
    + } + @default { + + } +} +@if (!!title) { +
    {{ title }}
    +} +@if (description) { +
    {{ description }}
    +} +
    + +
    \ No newline at end of file diff --git a/projects/fusion-ui/components/empty-state/v4/empty-state.component.scss b/projects/fusion-ui/components/empty-state/v4/empty-state.component.scss new file mode 100644 index 000000000..f771b532a --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/empty-state.component.scss @@ -0,0 +1,39 @@ +@import '../../../src/style/scss/v4/colors'; +@import '../../../src/style/scss/v4/spacings'; +@import '../../../src/style/scss/v4/fonts'; + +:host { + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 4px; + align-items: center; + justify-content: center; + + .fu-empty-state-icon { + width: 48px; + height: 48px; + color: var(--default-outlined-border, #{$color-v4-default-outlined-border}); + } + + .fu-empty-state-image { + width: 112px; + height: 112px; + display: block; + } + + .fu-empty-state-title { + @extend %font-v4-heading-4; + color: var(--text-primary, #{$color-v4-text-primary}); + } + + .fu-empty-state-description { + @extend %font-v4-body-1; + color: var(--text-secondary, #{$color-v4-text-secondary}); + } + + .fu-empty-state-content { + margin-top: 8px; + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/empty-state/v4/empty-state.component.spec.ts b/projects/fusion-ui/components/empty-state/v4/empty-state.component.spec.ts new file mode 100644 index 000000000..7abfa81f9 --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/empty-state.component.spec.ts @@ -0,0 +1,126 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {EmptyStateComponent} from './empty-state.component'; + +const DEFAULT_ICON_SELECTOR: string = 'fusion-icon.fu-empty-state-icon.ghost'; +const CONTENT_SELECTOR: string = 'div.fu-empty-state-content'; +const TITLE_SELECTOR: string = 'div.fu-empty-state-title'; +const TITLE_TEXT: string = 'Empty State Title'; +const DESCRIPTION_SELECTOR: string = 'div.fu-empty-state-description'; +const DESCRIPTION_TEXT: string = 'Empty State Description'; + +describe('EmptyStateComponent', () => { + let component: EmptyStateComponent; + let fixture: ComponentFixture; + let el: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EmptyStateComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + component.title = TITLE_TEXT.trim(); + component.description = DESCRIPTION_TEXT.trim(); + fixture.detectChanges(); + el = fixture.nativeElement; + }); + + it('should create', () => { + expect(component).toBeDefined(); + }); + + it('must have default icon', () => { + expect(el.querySelector(DEFAULT_ICON_SELECTOR)).toBeTruthy(); + }); + + it('by default have no content', () => { + const contentEl = el.querySelector(CONTENT_SELECTOR); + expect(contentEl).toBeTruthy(); + expect(contentEl.textContent).toBe(''); + }); + + it('must render title', () => { + const contentEl = el.querySelector(TITLE_SELECTOR); + expect(contentEl).toBeTruthy(); + expect(contentEl.textContent).toBe(TITLE_TEXT); + }); + + it('must render description', () => { + const contentEl = el.querySelector(DESCRIPTION_SELECTOR); + expect(contentEl).toBeTruthy(); + expect(contentEl.textContent).toBe(DESCRIPTION_TEXT); + }); + + describe('Must render illustrate instead icon', () => { + it('illustrate "error"', () => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + component.type = 'error'; + fixture.detectChanges(); + el = fixture.nativeElement; + expect(el.querySelector('div.fu-empty-state-image')).toBeTruthy(); + expect(el.querySelector('fusion-svg').getAttribute('path')).toBe('assets/images/v4/illustrations/error.svg'); + }); + + it('illustrate "accessDenied"', () => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + component.type = 'accessDenied'; + fixture.detectChanges(); + el = fixture.nativeElement; + expect(el.querySelector('div.fu-empty-state-image')).toBeTruthy(); + expect(el.querySelector('fusion-svg').getAttribute('path')).toBe('assets/images/v4/illustrations/access-denied.svg'); + }); + + it('illustrate "noResult"', () => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + component.type = 'noResult'; + fixture.detectChanges(); + el = fixture.nativeElement; + expect(el.querySelector('div.fu-empty-state-image')).toBeTruthy(); + expect(el.querySelector('fusion-svg').getAttribute('path')).toBe('assets/images/v4/illustrations/no-result.svg'); + }); + + it('illustrate "noData"', () => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + component.type = 'noData'; + fixture.detectChanges(); + el = fixture.nativeElement; + expect(el.querySelector('div.fu-empty-state-image')).toBeTruthy(); + expect(el.querySelector('fusion-svg').getAttribute('path')).toBe('assets/images/v4/illustrations/no-data.svg'); + }); + + it('illustrate "chart"', () => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + component.type = 'chart'; + fixture.detectChanges(); + el = fixture.nativeElement; + expect(el.querySelector('div.fu-empty-state-image')).toBeTruthy(); + expect(el.querySelector('fusion-svg').getAttribute('path')).toBe('assets/images/v4/illustrations/chart.svg'); + }); + + it('illustrate "files"', () => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + component.type = 'files'; + fixture.detectChanges(); + el = fixture.nativeElement; + expect(el.querySelector('div.fu-empty-state-image')).toBeTruthy(); + expect(el.querySelector('fusion-svg').getAttribute('path')).toBe('assets/images/v4/illustrations/files.svg'); + }); + + it('illustrate "settings"', () => { + fixture = TestBed.createComponent(EmptyStateComponent); + component = fixture.componentInstance; + component.type = 'settings'; + fixture.detectChanges(); + el = fixture.nativeElement; + expect(el.querySelector('div.fu-empty-state-image')).toBeTruthy(); + expect(el.querySelector('fusion-svg').getAttribute('path')).toBe('assets/images/v4/illustrations/settings-exclamation.svg'); + }); + }); +}); diff --git a/projects/fusion-ui/components/empty-state/v4/empty-state.component.stories.ts b/projects/fusion-ui/components/empty-state/v4/empty-state.component.stories.ts new file mode 100644 index 000000000..b3fdb007f --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/empty-state.component.stories.ts @@ -0,0 +1,60 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {ButtonComponent} from '@ironsource/fusion-ui/components/button/v4'; +import {EmptyStateComponent} from './empty-state.component'; + +export default { + title: 'V4/Components/DataDisplay/EmptyState', + component: EmptyStateComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), ButtonComponent] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + title: 'Empty State Title', + description: 'Empty State Description' + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const WithButton: Story = { + render: args => ({ + props: args, + template: ` + + Click me + + ` + }) +}; + +export const WithIllustration: Story = { + render: args => ({ + props: { + ...args, + type: 'noResult', + title: 'No ad units to display', + description: 'Add a new ad unit to get started' + }, + template: ` + + Label + + ` + }) +}; diff --git a/projects/fusion-ui/components/empty-state/v4/empty-state.component.ts b/projects/fusion-ui/components/empty-state/v4/empty-state.component.ts new file mode 100644 index 000000000..9398f692c --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/empty-state.component.ts @@ -0,0 +1,19 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {EmptyStateType} from './empty-state.entities'; + +@Component({ + selector: 'fusion-empty-state', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [SvgModule, IconModule], + templateUrl: './empty-state.component.html', + styleUrl: './empty-state.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EmptyStateComponent { + @Input() title: string; + @Input() description: string; + @Input() type: EmptyStateType = 'empty'; +} diff --git a/projects/fusion-ui/components/empty-state/v4/empty-state.entities.ts b/projects/fusion-ui/components/empty-state/v4/empty-state.entities.ts new file mode 100644 index 000000000..90bb9eeba --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/empty-state.entities.ts @@ -0,0 +1 @@ +export type EmptyStateType = 'empty' | 'error' | 'accessDenied' | 'noResult' | 'noData' | 'chart' | 'files' | 'settings'; diff --git a/projects/fusion-ui/components/empty-state/v4/index.ts b/projects/fusion-ui/components/empty-state/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/empty-state/v4/ng-package.json b/projects/fusion-ui/components/empty-state/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/empty-state/v4/public-api.ts b/projects/fusion-ui/components/empty-state/v4/public-api.ts new file mode 100644 index 000000000..18e04af39 --- /dev/null +++ b/projects/fusion-ui/components/empty-state/v4/public-api.ts @@ -0,0 +1,2 @@ +export * from './empty-state.component'; +export * from './empty-state.entities'; diff --git a/projects/fusion-ui/components/form-card/v4/form-card.component.html b/projects/fusion-ui/components/form-card/v4/form-card.component.html new file mode 100644 index 000000000..9af9a1d36 --- /dev/null +++ b/projects/fusion-ui/components/form-card/v4/form-card.component.html @@ -0,0 +1,21 @@ +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + + + diff --git a/projects/fusion-ui/components/form-card/v4/form-card.component.scss b/projects/fusion-ui/components/form-card/v4/form-card.component.scss new file mode 100644 index 000000000..1af76fed1 --- /dev/null +++ b/projects/fusion-ui/components/form-card/v4/form-card.component.scss @@ -0,0 +1,84 @@ +@import '../../../src/style/scss/v4/vars/vars'; + +:host { + --form-card-border-radius: 6px; + --form-card-border-color: var(--common-divider, #{$color-v4-common-divider}); + --form-card-border: solid 1px var(--form-card-border-color); + + --form-card-header-padding: 14px 24px; + --form-card-header-background: var(--background-paper-elevation-0, #{$color-v4-background-paper-elevation-0}); + --form-card-title-color: var(--text-primary, #{$color-v4-text-primary}); + --form-card-subtitle-color: var(--text-secondary, #{$color-v4-text-secondary}); + + --form-card-content-padding: 32px 40px 40px 40px; + --form-card-content-background: var(--background-paper, #{$color-v4-background-paper}); + + --form-card-footer-padding: 14px 0; + --form-card-footer-actions-gap: 8px; + + @extend %reset; + + .fu-form-card { + display: flex; + flex-direction: column; + border-radius: var(--form-card-border-radius); + border: var(--form-card-border); + + @extend %font-v4-body-2; + + + .form-card__header { + display: flex; + align-items: center; + gap: 42px; + min-height: 48px; + padding: var(--form-card-header-padding); + background-color: var(--form-card-header-background); + border-bottom: var(--form-card-border); + border-top-left-radius: var(--form-card-border-radius); + border-top-right-radius: var(--form-card-border-radius); + + .form-card__header__title { + display: flex; + flex-direction: column; + + @extend %font-v4-heading-4; + color: var(--form-card-title-color); + + .form-card__header__subtitle { + @extend %font-v4-body-2; + color: var(--form-card-subtitle-color); + display: none; + &:not(:empty) { + margin-top: 4px; + display: initial; + } + } + } + + .form-card__header__actions { + margin-left: auto; + } + + } + + .form-card__content { + @extend %font-v4-subtitle-1; + padding: var(--form-card-content-padding); + background-color: var(--form-card-content-background); + color: var(--text-secondary, #{$color-v4-text-secondary}); + border-bottom-left-radius: var(--form-card-border-radius); + border-bottom-right-radius: var(--form-card-border-radius); + + } + } + + .form-card__footer { + display: flex; + justify-content: flex-end; + align-items: center; + padding: var(--form-card-footer-padding); + gap: var(--form-card-footer-actions-gap); + } +} + diff --git a/projects/fusion-ui/components/form-card/v4/form-card.component.spec.ts b/projects/fusion-ui/components/form-card/v4/form-card.component.spec.ts new file mode 100644 index 000000000..f1dc65d13 --- /dev/null +++ b/projects/fusion-ui/components/form-card/v4/form-card.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FormCardComponent } from './form-card.component'; + +describe('FormCardComponent', () => { + let component: FormCardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FormCardComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FormCardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/form-card/v4/form-card.component.stories.ts b/projects/fusion-ui/components/form-card/v4/form-card.component.stories.ts new file mode 100644 index 000000000..c7e03479c --- /dev/null +++ b/projects/fusion-ui/components/form-card/v4/form-card.component.stories.ts @@ -0,0 +1,169 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {action} from '@storybook/addon-actions'; +import {CommonModule} from '@angular/common'; +import {FormCardComponent} from './form-card.component'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {ButtonComponent} from '@ironsource/fusion-ui/components/button/v4'; +import {LinkComponent} from '@ironsource/fusion-ui/components/link'; +import {SkeletonComponent} from '@ironsource/fusion-ui/components/skeleton'; +import {InputLabelComponent} from '@ironsource/fusion-ui/components/input-label/v4'; +import {InputComponent} from '@ironsource/fusion-ui/components/input/v4'; +import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms'; + +const actionsData = { + onActionButtonClicked: action('onActionButtonClicked') +}; + +export default { + title: 'V4/Components/Inputs/FormCard', + component: FormCardComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [ + CommonModule, + ReactiveFormsModule, + SvgModule.forRoot({assetsPath: environment.assetsPath}), + ButtonComponent, + LinkComponent, + SkeletonComponent, + InputLabelComponent, + InputComponent + ] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + title: 'Description card title', + content: 'Any content here...', + onActionButtonClicked: actionsData.onActionButtonClicked + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: args => ({ + props: {...args}, + template: ` + + {{ title }} + {{ content }} + + Cancel + Save + + +` + }) +}; + +export const SurfaceFullyLoaded: Story = { + render: args => ({ + props: {...args, subtitle: 'Lorem ipsum dolor sit amet consectetur. Consectetur massa sed in urna.'}, + template: ` + + {{ title }} + {{ subtitle }} + + Link + + + {{ content }} + + + Cancel + Save + + +` + }) +}; + +export const SaveLoading: Story = { + render: args => ({ + props: {...args}, + template: ` + + {{ title }} + {{ content }} + + Cancel + Save + + +` + }) +}; + +export const NoButtons: Story = { + render: args => ({ + props: {...args}, + template: ` + + {{ title }} + {{ content }} + +` + }) +}; + +export const FormRow: Story = { + render: args => ({ + props: {...args, title: 'Ad unit setup', formControl: new FormControl('Native-01', [Validators.required])}, + template: ` + + {{ title }} + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + Cancel + Save + +
    +` + }) +}; + +export const Skeleton: Story = { + render: args => ({ + props: {...args}, + template: ` + + + + + +
    + + + +
    +
    +
    +` + }) +}; diff --git a/projects/fusion-ui/components/form-card/v4/form-card.component.ts b/projects/fusion-ui/components/form-card/v4/form-card.component.ts new file mode 100644 index 000000000..37ee1d27b --- /dev/null +++ b/projects/fusion-ui/components/form-card/v4/form-card.component.ts @@ -0,0 +1,12 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +@Component({ + selector: 'fusion-form-card', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [], + templateUrl: './form-card.component.html', + styleUrl: './form-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormCardComponent {} diff --git a/projects/fusion-ui/components/form-card/v4/index.ts b/projects/fusion-ui/components/form-card/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/form-card/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/form-card/v4/ng-package.json b/projects/fusion-ui/components/form-card/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/form-card/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/form-card/v4/public-api.ts b/projects/fusion-ui/components/form-card/v4/public-api.ts new file mode 100644 index 000000000..1be976d81 --- /dev/null +++ b/projects/fusion-ui/components/form-card/v4/public-api.ts @@ -0,0 +1 @@ +export * from './form-card.component'; diff --git a/projects/fusion-ui/components/inline-copy/index.ts b/projects/fusion-ui/components/inline-copy/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/inline-copy/ng-package.json b/projects/fusion-ui/components/inline-copy/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/inline-copy/public-api.ts b/projects/fusion-ui/components/inline-copy/public-api.ts new file mode 100644 index 000000000..a86af1af2 --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/public-api.ts @@ -0,0 +1 @@ +export * from './v4'; diff --git a/projects/fusion-ui/components/inline-copy/v4/index.ts b/projects/fusion-ui/components/inline-copy/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.html b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.html new file mode 100644 index 000000000..14e865462 --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.html @@ -0,0 +1,12 @@ +
    + @if(!!text){ +
    {{text}}
    + } + +
    diff --git a/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.scss b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.scss new file mode 100644 index 000000000..4205c2fa1 --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.scss @@ -0,0 +1,36 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; + + --fu-inline-copy-gap: #{$spacingV4-100}; + --fu-inline-copy-color: var(--text-secondary, #{$color-v4-text-secondary}); + --fu-inline-copy-icon-hover-color: var(--action-primary, #{$color-v4-action-primary}); + --fu-inline-copy-icon-size: 16px; + + .fu-inline-copy-wrapper { + display: flex; + align-items: center; + gap: var(--fu-inline-copy-gap); + @extend %font-v4-body-1; + color: var(--fu-inline-copy-color); + + &.fu-size-medium { + --fu-inline-copy-icon-size: 20px; + } + + .fu-inline-copy-icon{ + @include size(var(--fu-inline-copy-icon-size)); + &:hover { + color: var(--fu-inline-copy-icon-hover-color); + cursor: pointer; + } + } + + &.fu-icon-position-left { + justify-content: flex-end; + flex-direction: row-reverse; + } + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.spec.ts b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.spec.ts new file mode 100644 index 000000000..1225a560c --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InlineCopyComponent } from './inline-copy.component'; + +describe('InlineCopyComponent', () => { + let component: InlineCopyComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InlineCopyComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(InlineCopyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.stories.ts b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.stories.ts new file mode 100644 index 000000000..03dddbd44 --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.stories.ts @@ -0,0 +1,61 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {environment} from 'stories/environments/environment'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {InlineCopyComponent} from '@ironsource/fusion-ui/components/inline-copy'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; + +export default { + title: 'V4/Components/Inline Copy', + component: InlineCopyComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule, TooltipDirective] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + } +} as Meta; + +type Story = StoryObj; + +export const InlineCopy: Story = {}; +InlineCopy.args = {text: 'Copy me'}; + +export const SizeMedium: Story = {}; +SizeMedium.args = { + text: 'It medium size', + size: 'medium' +}; + +export const IconOnly: Story = {}; +IconOnly.args = { + valueToCopy: 'Value to copy', + size: 'medium' +}; + +export const WithoutTooltip: Story = {}; +WithoutTooltip.args = { + text: 'No tooltip', + suppressTooltip: true +}; + +export const WithoutSnackbar: Story = {}; +WithoutSnackbar.args = { + text: 'No snackbar', + suppressSnackbar: true +}; + +export const IconOnLeft: Story = {}; +IconOnLeft.args = { + text: 'Icon on left', + iconPosition: 'left' +}; diff --git a/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.ts b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.ts new file mode 100644 index 000000000..5fdda0ffd --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/v4/inline-copy.component.ts @@ -0,0 +1,50 @@ +import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; +import {tooltipConfiguration, TooltipPosition} from '@ironsource/fusion-ui/components/tooltip/common/base'; +import {CopyToClipboardModule} from '@ironsource/fusion-ui/directives/copy-to-clipboard'; +import {SnackbarService} from '@ironsource/fusion-ui/components/snackbar/v4'; + +@Component({ + selector: 'fusion-inline-copy', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [IconModule, TooltipDirective, CopyToClipboardModule], + providers: [SnackbarService], + templateUrl: './inline-copy.component.html', + styleUrl: './inline-copy.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class InlineCopyComponent { + @Input() text = ''; + @Input() size: 'small' | 'medium' = 'small'; + @Input() tooltipText = 'Copy to clipboard'; + @Input() tooltipConfiguration: tooltipConfiguration = { + position: TooltipPosition.Bottom, + suppressPositionArrow: true + }; + @Input() iconName = 'ph/copy'; + @Input() iconPosition: 'left' | 'right' = 'right'; + @Input() testId = ''; + @Input() valueToCopy = ''; + @Input() suppressTooltip = false; + @Input() suppressSnackbar = false; + @Input() copiedSnackbarText = 'Copied successfully'; + + snackbarService: SnackbarService = inject(SnackbarService); + + copyToClipboard() { + return () => this.valueToCopy || this.text; + } + + textCopied() { + if (!this.suppressSnackbar) { + this.snackbarService.show({ + title: this.copiedSnackbarText, + type: 'success', + location: 'top-right', + duration: 1500 + }); + } + } +} diff --git a/projects/fusion-ui/components/inline-copy/v4/ng-package.json b/projects/fusion-ui/components/inline-copy/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/inline-copy/v4/public-api.ts b/projects/fusion-ui/components/inline-copy/v4/public-api.ts new file mode 100644 index 000000000..47d7b3e77 --- /dev/null +++ b/projects/fusion-ui/components/inline-copy/v4/public-api.ts @@ -0,0 +1 @@ +export {InlineCopyComponent} from './inline-copy.component'; diff --git a/projects/fusion-ui/components/input-inline/common/base/inline-input-type.enum.ts b/projects/fusion-ui/components/input-inline/common/base/inline-input-type.enum.ts index 33e198724..78f2726d9 100644 --- a/projects/fusion-ui/components/input-inline/common/base/inline-input-type.enum.ts +++ b/projects/fusion-ui/components/input-inline/common/base/inline-input-type.enum.ts @@ -2,5 +2,6 @@ export enum InlineInputType { Text, Number, Currency, - Percent + Percent, + Dropdown } diff --git a/projects/fusion-ui/components/input-inline/common/base/input-inline.base.component.html b/projects/fusion-ui/components/input-inline/common/base/input-inline.base.component.html index 146dde208..06c51e514 100644 --- a/projects/fusion-ui/components/input-inline/common/base/input-inline.base.component.html +++ b/projects/fusion-ui/components/input-inline/common/base/input-inline.base.component.html @@ -17,11 +17,13 @@
    - +
    diff --git a/projects/fusion-ui/components/input-inline/common/base/input-inline.base.component.ts b/projects/fusion-ui/components/input-inline/common/base/input-inline.base.component.ts index 11791cb64..9e5acee38 100644 --- a/projects/fusion-ui/components/input-inline/common/base/input-inline.base.component.ts +++ b/projects/fusion-ui/components/input-inline/common/base/input-inline.base.component.ts @@ -32,6 +32,8 @@ export abstract class InputInlineBaseComponent implements ControlValueAccessor, @Input() loading: boolean; @Input() readOnly: boolean; @Input() error: string; + @Input() errorType = 'error'; + @Input() inputErrorIconShow: boolean; @Input() currencyPipeParameters: CurrencyPipeParameters; @Input() inputOptions: InputOptions; diff --git a/projects/fusion-ui/components/input-inline/v4/error-messages.config.ts b/projects/fusion-ui/components/input-inline/v4/error-messages.config.ts new file mode 100644 index 000000000..fda6bd74e --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/error-messages.config.ts @@ -0,0 +1,7 @@ +export const INPUT_INLINE_ERROR_MESSAGES_MAP = { + required: 'Required', + min: 'Minimum is {min}', + max: 'Maximum is {max}', + minlength: 'Min length is {requiredLength} characters', + maxlength: 'Max length is {requiredLength} characters' +}; diff --git a/projects/fusion-ui/components/input-inline/v4/index.ts b/projects/fusion-ui/components/input-inline/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.html b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.html new file mode 100644 index 000000000..158baf2db --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.html @@ -0,0 +1,62 @@ +@if (!(isEditMode$ | async)) { +
    + @switch (type) { + @case (InlineInputType.Number) { + {{ inputValue | number: pipeOptions }} + } + @case (InlineInputType.Currency) { + {{ inputValue | currency + : currencyPipeParameters?.currencyCode || undefined + : currencyPipeParameters?.display || (currencyPipeParameters ? undefined : '$') + : currencyPipeParameters?.digitsInfo || undefined }} + } + @case (InlineInputType.Percent) { + {{ inputValue | number: pipeOptions }}% + } + @case (InlineInputType.Dropdown) { + {{ dropdownSelectedText }} + } + @default{ + {{ inputValue }} + } + } +
    +} @else { +
    + @if (isDropdown){ + + } @else { + + } + @if (pending){ + + } +
    +} + diff --git a/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.scss b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.scss new file mode 100644 index 000000000..f17f6a339 --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.scss @@ -0,0 +1,90 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; + + --inline-edit-value-height: 28px; + --inline-edit-value-padding: 0 8px; + --inline-edit-value-color: var(--table-text-color, #{$color-v4-text-primary}); + --dropdown-carret-icon-color: var(--action-active, #{$color-v4-action-active}); + --dropdown-carret-icon-size: 20px; + + width: var(--inline-edit-width, 100%); + + .fu-edit-value-wrapper { + display: flex; + align-items: center; + @extend %font-v4-body-1; + height: var(--inline-edit-value-height); + padding: var(--inline-edit-value-padding); + color: var(--inline-edit-value-color); + + &:hover:not(.fu-read-only) { + border-radius: 6px; + outline: 1px solid var(--action-outlinedBorder, #{$color-v4-action-outlined-border}); + background-color: var(--default-main, #{$color-v4-default-main}); + cursor: text; + &.fu-dropdown .fu-edit-value .fu-dropdown-icon{ + visibility: visible; + } + } + + &.fu-number { + justify-content: flex-end; + } + + &.fu-dropdown { + .fu-edit-value { + display: flex; + align-items: center; + width: 100%; + + .fu-dropdown-icon { + margin-left: auto; + color: var(--dropdown-carret-icon-color); + @include size(var(--dropdown-carret-icon-size)); + visibility: hidden; + } + } + } + + &.fu-read-only { + cursor: default; + } + + &.fu-pending { + opacity: var(--table-row-loading-opacity, 0.7); + } + } + + .fu-hidden { + display: none; + } + + .truncate { + @extend %truncate-flex-child; + } + + .fu-edit-input-wrapper { + position: relative; + + fusion-loader { + position: absolute; + right: 5px; + top: 3px; + } + } +} + +:host-context(tr:hover) { + .fu-edit-value-wrapper:not(.fu-read-only) { + border-radius: 6px; + outline: 1px solid var(--action-outlinedBorder, #{$color-v4-action-outlined-border}); + background-color: var(--default-main, #{$color-v4-default-main}); + cursor: text; + &.fu-dropdown .fu-edit-value .fu-dropdown-icon{ + visibility: visible; + } + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.spec.ts b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.spec.ts new file mode 100644 index 000000000..d01c5d9fa --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InputInlineV4Component } from './input-inline-v4.component'; + +describe('InputInlineV4Component', () => { + let component: InputInlineV4Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InputInlineV4Component] + }) + .compileComponents(); + + fixture = TestBed.createComponent(InputInlineV4Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.stories.ts b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.stories.ts new file mode 100644 index 000000000..ffd592016 --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.stories.ts @@ -0,0 +1,171 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {FormControl, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {InlineInputType} from '@ironsource/fusion-ui/components/input-inline'; +import {InputInlineV4Component} from './input-inline-v4.component'; +import {DropdownOption} from '@ironsource/fusion-ui/components/dropdown-option'; +import {DropdownComponent} from '@ironsource/fusion-ui/components/dropdown/v4'; + +const BASE_TEMPLATE = ` +`; + +const SELECT_OPTIONS: DropdownOption[] = [ + { + id: 1, + displayText: 'Option 1' + }, + { + id: 2, + displayText: 'Option 2' + }, + { + id: 3, + displayText: 'Option 3' + } +]; + +export default { + title: 'V4/Components/Inputs/Inline-Edit', + component: InputInlineV4Component, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + SvgModule.forRoot({assetsPath: environment.assetsPath}), + DropdownComponent + ] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl('Abdullah', [Validators.required, Validators.minLength(3)]) + }, + template: BASE_TEMPLATE + }) +}; +Basic.parameters = { + docs: { + description: { + story: `This example has input validations: **Required, Minimum length 3**` + } + } +}; + +export const Numeric: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl(135, [Validators.required, Validators.min(100), Validators.max(200)]), + type: InlineInputType.Number + }, + template: BASE_TEMPLATE + }) +}; +Numeric.parameters = { + docs: { + description: { + story: `This example has input validations: **Required, Min 100, Max 200**` + } + } +}; + +export const Currency: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl(34.99, [Validators.required]), + type: InlineInputType.Currency + }, + template: BASE_TEMPLATE + }) +}; +Currency.parameters = { + docs: { + description: { + story: `This example has input validations: **Required**` + } + } +}; + +export const Percent: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl(5), + type: InlineInputType.Percent + }, + template: BASE_TEMPLATE + }) +}; + +export const Dropdown: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl([SELECT_OPTIONS[1]]), + selectOptions: SELECT_OPTIONS, + type: InlineInputType.Dropdown + }, + template: BASE_TEMPLATE + }) +}; + +export const Readonly: Story = { + render: args => ({ + props: { + ...args, + readOnly: true, + formControl: new FormControl('Abdullah') + }, + template: BASE_TEMPLATE + }) +}; + +export const Disabled: Story = { + render: args => ({ + props: { + ...args, + formControl: new FormControl({value: 'Abdullah', disabled: true}) + }, + template: BASE_TEMPLATE + }) +}; + +export const Pending: Story = { + render: args => ({ + props: { + ...args, + pending: true, + formControl: new FormControl('Abdullah') + }, + template: BASE_TEMPLATE + }) +}; diff --git a/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.ts b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.ts new file mode 100644 index 000000000..9d1e1b0c9 --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/input-inline-v4.component.ts @@ -0,0 +1,282 @@ +import {ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {BehaviorSubject, fromEvent, Subject, Subscription} from 'rxjs'; +import {isNullOrUndefined} from '@ironsource/fusion-ui/utils'; +import {InlineInputType} from '@ironsource/fusion-ui/components/input-inline'; +import {InputType} from '@ironsource/fusion-ui/components/input/v4'; +import {CurrencyPipeParameters} from '@ironsource/fusion-ui/components/table'; +import {InputComponent} from '@ironsource/fusion-ui/components/input/v4'; +import {takeUntil} from 'rxjs/operators'; +import {INPUT_INLINE_ERROR_MESSAGES_MAP} from './error-messages.config'; +import {LoaderComponent} from '@ironsource/fusion-ui/components/loader/v4'; +import {DropdownOption} from '@ironsource/fusion-ui/components/dropdown-option'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {DropdownComponent} from '@ironsource/fusion-ui/components/dropdown/v4'; +import {SkeletonComponent} from '@ironsource/fusion-ui/components/skeleton'; + +@Component({ + selector: 'fusion-input-inline', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + InputComponent, + LoaderComponent, + IconModule, + DropdownComponent, + SkeletonComponent + ], + templateUrl: './input-inline-v4.component.html', + styleUrl: './input-inline-v4.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class InputInlineV4Component implements OnInit, OnDestroy { + /** @internal */ + @ViewChild('inputComponent') inputComponent: InputComponent; + /** @internal */ + @ViewChild('inputWrapper') inputWrapper: ElementRef; + + @Input() set type(value: InlineInputType) { + if (!isNullOrUndefined(value)) { + this._type = value; + if (value === InlineInputType.Text || value === InlineInputType.Dropdown) { + this._inputType = 'text'; + } else { + this._inputType = 'number'; + } + } + } + + get type(): InlineInputType { + return this._type; + } + + get inputType(): InputType { + return this._inputType; + } + + get isNumber(): boolean { + return this._inputType === 'number'; + } + + get isDropdown(): boolean { + return this.type === InlineInputType.Dropdown; + } + + get dropdownSelectedText(): string { + if (this.isDropdown && Array.isArray(this.inputControl?.value) && this.inputControl?.value.length > 0) { + return this.inputControl?.value[0]?.displayText; + } + return ''; + } + + @Input() readOnly: boolean = false; + + @Input() set pending(value: boolean) { + if (value) { + this.inputControl.disable({emitEvent: false}); + this._pending = true; + } else { + if (!this.disabled) { + this.inputControl.enable({emitEvent: false}); + } + this._pending = false; + } + } + + get pending(): boolean { + return this._pending; + } + + @Input() currencyPipeParameters?: CurrencyPipeParameters; + @Input() pipeOptions?: string; + + @Input() set data(value: FormControl) { + this.inputControl = value; + this.inputValue = this.inputControl.value; + this.disabled = this.inputControl.disabled; + } + + get inputPrefix(): string { + if (this.type === InlineInputType.Currency) { + return this.currencyPipeParameters?.display || '$'; + } + return null; + } + get inputSuffix(): string { + if (this.type === InlineInputType.Percent) { + return '%'; + } + return null; + } + + @Input() error: string; + @Input() set errorMapping(value: {[key: string]: string}) { + if (!isNullOrUndefined(value)) { + this._errorMapping = value; + } + } + @Input() hideNumberArrows = true; + + @Input() selectOptions: DropdownOption[] = []; + + // eslint-disable-next-line + @Output() onSave = new EventEmitter(); + // eslint-disable-next-line + @Output() onCancel = new EventEmitter(); + + /** @internal */ + isEditMode$ = new BehaviorSubject(false); + /** @internal */ + setEditMode$ = new Subject(); + /** @internal */ + inputControl = new FormControl(); + /** @internal */ + inputValue = ''; + /** @internal */ + disabled = false; + /** @internal */ + dropdownIcon = 'ph/caret-down'; + + private _type: InlineInputType = InlineInputType.Text; + private _inputType: InputType = 'text'; + private clickOutSideSubscription: Subscription; + private onDestroy$ = new Subject(); + private stayInEditMode = false; + private _pending = false; + private _errorMapping: {[key: string]: string} = INPUT_INLINE_ERROR_MESSAGES_MAP; + + ngOnInit() { + this.setEditMode$.pipe(takeUntil(this.onDestroy$)).subscribe(this.setEditMode.bind(this)); + this.isEditMode$.asObservable().pipe(takeUntil(this.onDestroy$)).subscribe(this.handleClickOutside.bind(this)); + this.inputControl.statusChanges.pipe(takeUntil(this.onDestroy$)).subscribe(status => { + this.error = status === 'INVALID' ? this.getErrorMessage(this.inputControl.errors) : null; + }); + } + + ngOnDestroy() { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + /** @internal */ + save() { + if (this.isEditMode$.getValue() && this.inputControl.valid) { + if (this.inputControl.value.toString() !== this.inputValue.toString()) { + this.onSave.emit({ + currentValue: this.inputValue, + newValue: this.inputControl.value + }); + this.isEditMode$.next(false); + this.inputValue = this.inputControl.value; + } else { + this.cancel(); + } + } + } + + /** @internal */ + cancel() { + if (this.isEditMode$.getValue() && !this.pending) { + if (!this.stayInEditMode) { + if (this.isDropdown) { + this.handleDropdownSelect(); + } else { + this.inputControl.setValue(this.inputValue, {emitEvent: false}); + this.isEditMode$.next(false); + this.onCancel.emit(); + } + } else { + this.stayInEditMode = false; + } + } + } + + /** @internal */ + goToEditMode(withValue?: string | number): void { + if (!this.disabled && !this.readOnly) { + this.inputControl.setValue(!isNullOrUndefined(withValue) ? withValue : this.inputValue, {emitEvent: false}); + this.isEditMode$.next(true); + } + } + + private getErrorMessage(inputError: {[key: string]: any}): string { + if (inputError) { + const errorKey = Object.keys(inputError)[0]; + let errorMessage = `Error: ${errorKey}`; + if (this._errorMapping[errorKey]) { + errorMessage = this._errorMapping[errorKey]; + Object.keys(inputError[errorKey]).forEach((find: string) => { + errorMessage = errorMessage.replace(`{${find}}`, inputError[errorKey][find]); + }); + } + return errorMessage; + } + return null; + } + + private handleDropdownSelect() { + if (this.inputControl.value === this.inputValue) { + this.isEditMode$.next(false); + } else { + this.onSave.emit({ + currentValue: this.inputValue, + newValue: this.inputControl.value + }); + this.isEditMode$.next(false); + this.inputValue = this.inputControl.value; + } + } + + private setEditMode(val: string | number) { + if (!!val) { + this.goToEditMode(val); + this.stayInEditMode = true; + } + } + + private setFocusToInput() { + setTimeout(() => { + if (this.type === InlineInputType.Dropdown) { + const dropdownTrigger = this.inputWrapper.nativeElement.querySelector('fusion-dropdown-select'); + if (!!dropdownTrigger) { + dropdownTrigger.click(); + } + } else { + this.inputComponent.setFocus(); + } + }, 0); + } + + private handleClickOutside(value: boolean) { + if (value) { + this.setFocusToInput(); + this.clickOutSideSubscription = fromEvent(document, 'click').subscribe((event: MouseEvent) => { + const clickedInside = this.isClickInside(event); + if (!clickedInside && !this.stayInEditMode) { + this.cancel(); + } + }); + } else { + this.clickOutSideSubscription?.unsubscribe(); + } + } + + private isClickInside(event: MouseEvent): boolean { + if (event.clientX === 0 && event.clientY === 0) { + return !!(event.target as HTMLElement).closest('fusion-input-inline'); + } + const parentRect = this.inputWrapper.nativeElement.getBoundingClientRect(); + return ( + parentRect.left <= event.clientX && + parentRect.right >= event.clientX && + parentRect.top <= event.clientY && + parentRect.bottom >= event.clientY + ); + } + + protected readonly InlineInputType = InlineInputType; +} diff --git a/projects/fusion-ui/components/input-inline/v4/ng-package.json b/projects/fusion-ui/components/input-inline/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/input-inline/v4/public-api.ts b/projects/fusion-ui/components/input-inline/v4/public-api.ts new file mode 100644 index 000000000..48de721d5 --- /dev/null +++ b/projects/fusion-ui/components/input-inline/v4/public-api.ts @@ -0,0 +1,2 @@ +export {InputInlineV4Component as InputInlineComponent} from './input-inline-v4.component'; +export {InlineInputType} from '@ironsource/fusion-ui/components/input-inline/common/base'; diff --git a/projects/fusion-ui/components/input/v1/input.component.html b/projects/fusion-ui/components/input/v1/input.component.html index 6bf6371c3..f4d8de0a3 100644 --- a/projects/fusion-ui/components/input/v1/input.component.html +++ b/projects/fusion-ui/components/input/v1/input.component.html @@ -79,7 +79,7 @@ @@ -105,7 +105,7 @@
    diff --git a/projects/fusion-ui/components/input/v1/input.component.scss b/projects/fusion-ui/components/input/v1/input.component.scss index 128e0e413..7d3c4192e 100644 --- a/projects/fusion-ui/components/input/v1/input.component.scss +++ b/projects/fusion-ui/components/input/v1/input.component.scss @@ -318,8 +318,8 @@ $smallPadding: 4px 6px; } .fu-validation-icon-holder { position: absolute; - right: 6px; - top: 3px;; + right: var(--fu-validation-error-right, 6px); + top: 3px; ::ng-deep svg { width: 11px; height: 11px; diff --git a/projects/fusion-ui/components/input/v2/input.component.html b/projects/fusion-ui/components/input/v2/input.component.html index 9de59b60c..e49c76fe0 100644 --- a/projects/fusion-ui/components/input/v2/input.component.html +++ b/projects/fusion-ui/components/input/v2/input.component.html @@ -56,7 +56,7 @@ diff --git a/projects/fusion-ui/components/input/v3/input.component.html b/projects/fusion-ui/components/input/v3/input.component.html index 6aa9e5dc2..4e301050e 100644 --- a/projects/fusion-ui/components/input/v3/input.component.html +++ b/projects/fusion-ui/components/input/v3/input.component.html @@ -28,7 +28,7 @@ diff --git a/projects/fusion-ui/components/input/v4/input-v4.component.scss b/projects/fusion-ui/components/input/v4/input-v4.component.scss index 7a5611461..1d113a8be 100644 --- a/projects/fusion-ui/components/input/v4/input-v4.component.scss +++ b/projects/fusion-ui/components/input/v4/input-v4.component.scss @@ -10,6 +10,8 @@ flex-direction: column; gap: 4px; + --input-time-width: 86px; + .fu-input-wrapper { flex-grow: 2; display: flex; @@ -215,6 +217,10 @@ padding-right: 40px; } } + + &:has(input[type="time"]){ + width: var(--input-time-width); + } } // region chars counter / maxlength diff --git a/projects/fusion-ui/components/input/v4/input-v4.component.stories.ts b/projects/fusion-ui/components/input/v4/input-v4.component.stories.ts index dedefe6c1..18ce88248 100644 --- a/projects/fusion-ui/components/input/v4/input-v4.component.stories.ts +++ b/projects/fusion-ui/components/input/v4/input-v4.component.stories.ts @@ -6,6 +6,7 @@ import {environment} from 'stories/environments/environment'; import {SvgModule} from '@ironsource/fusion-ui/components/svg'; import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; import {InputV4Component} from './input-v4.component'; +import {InputType} from '@ironsource/fusion-ui/components/input/v4/input-v4.entities'; const formControl = new FormControl(); const formControlDisabled = new FormControl({value: 'Disabled', disabled: true}); @@ -405,3 +406,20 @@ export const Password: Story = { ` }) }; + +export const Time: Story = { + render: args => ({ + props: { + ...args, + type: 'time' as InputType, + formControl: formControlPassword + }, + template: ` + +` + }) +}; diff --git a/projects/fusion-ui/components/input/v4/input-v4.component.ts b/projects/fusion-ui/components/input/v4/input-v4.component.ts index e31cd3ff6..e4bff6eb8 100644 --- a/projects/fusion-ui/components/input/v4/input-v4.component.ts +++ b/projects/fusion-ui/components/input/v4/input-v4.component.ts @@ -79,6 +79,23 @@ export class InputV4Component implements OnInit, OnDestroy { private _placeholder: string; // endregion + + // region Inputs - pattern + @Input() + set pattern(value: string) { + this._pattern = value; + } + + get pattern() { + if (this.type === 'time') { + return '[0-9]{2}:[0-9]{2}'; + } + return this._pattern; + } + + private _pattern: string; + // endregion + // region Inputs - input type @Input() set type(value: InputType) { diff --git a/projects/fusion-ui/components/input/v4/input-v4.entities.ts b/projects/fusion-ui/components/input/v4/input-v4.entities.ts index 9e3a00651..373eed185 100644 --- a/projects/fusion-ui/components/input/v4/input-v4.entities.ts +++ b/projects/fusion-ui/components/input/v4/input-v4.entities.ts @@ -1,3 +1,3 @@ -export type InputType = 'text' | 'password' | 'number'; +export type InputType = 'text' | 'password' | 'number' | 'time'; export type InputSize = 'medium' | 'large' | 'xlarge'; export type InputVariant = 'default' | 'error' | 'success' | 'warning'; diff --git a/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header-v4.component.stories.ts b/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header-v4.component.stories.ts index ddf7f6bfe..1c75fc85d 100644 --- a/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header-v4.component.stories.ts +++ b/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header-v4.component.stories.ts @@ -86,6 +86,48 @@ export const WithBottomLine = { } }; +export const WithSkeletons = { + render: LayoutHeaderTemplate, + args: { + teleportElements: [ + { + id: 'fuHeaderTeleport', + skeletons: [{width: '130px', height: '28px', shape: 'pill'}] + }, + { + id: 'fuHeaderTeleportRight', + isOnRight: true, + skeletons: [ + {width: '130px', height: '28px', shape: 'pill'}, + {width: '60px', height: '28px', shape: 'pill'} + ], + skeletonsGap: '8px' + } + ], + headerContent: { + ...HEADER_CONTENT_MOCK, + multiline: true, + topRowContent: { + show: true, + skeletons: [{width: '320px', height: '40px', borderRadius: '8px'}] + }, + bottomRowContent: { + show: true, + skeletons: [ + {width: '130px', height: '28px', shape: 'pill'}, + { + width: '130px', + height: '28px', + shape: 'pill' + }, + {width: '130px', height: '28px', shape: 'pill'} + ], + skeletonsGap: '8px' + } + } + } +}; + export const MainDrilldownTeleport = { render: LayoutHeaderTemplate, args: { diff --git a/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.html b/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.html index c5c0b340e..48716aa15 100644 --- a/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.html +++ b/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.html @@ -1,7 +1,31 @@
    -
    -
    +
    + @for (skeleton of teleportTopRowBaseSkeletons; track skeleton) { + + } +
    + @for (teleportItem of teleportTopRowElements; track teleportItem) { +
    + @for (skeleton of teleportItem?.skeletons; track skeleton) { + + } + +
    + }
    + [component]="headerContent.actionComponent" + [componentData]="headerContent.actionData">
    - -
    -
    + @for (teleportItem of teleportElements; track teleportItem){ +
    + @for (skeleton of teleportItem?.skeletons; track skeleton) { + + } +
    + }
    -
    -
    +
    + @for (skeleton of teleportBottomLineSkeletons; track skeleton) { + + } +
    + @for (teleportItem of bottomTopRowElements; track teleportItem) { +
    + @for (skeleton of teleportItem?.skeletons; track skeleton) { + + } +
    + }
    diff --git a/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.scss b/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.scss index 56d76e1ab..958234954 100644 --- a/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.scss +++ b/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.scss @@ -73,7 +73,7 @@ $drilldownHeight: 72px; padding: 24px 24px 8px 24px; flex-direction: row; align-items: center; - gap: 24px; + gap: var(--header-main-row-gap, 24px); .fu-header-back-button, .fu-page-subtitle, .fu-header-delimiter, @@ -84,7 +84,9 @@ $drilldownHeight: 72px; @extend %font-v4-heading-1; color: var(--text-primary, #{$color-v4-text-primary}); } - + .fu-header-teleport-host{ + display: flex; + } &.fu-header-drilldown{ height: $drilldownHeight; max-height: $drilldownHeight; @@ -95,11 +97,6 @@ $drilldownHeight: 72px; margin-left: 0; } } - /* todo: remove this after approving - &.fu-has-top-row{ - padding-top: 28px; - } - */ } .fu-header-top-row{ display: flex; @@ -116,6 +113,10 @@ $drilldownHeight: 72px; display: flex; width: 100%; } + .fu-header-top-teleport-host{ + display: flex; + width: 100%; + } } .fu-header-bottom-row{ display: flex; @@ -147,9 +148,9 @@ $drilldownHeight: 72px; // for header storybook :host-context(.header-only-story){ - .fu-header-teleport-host{ + .fu-header-teleport-host:not(:has(fusion-skeleton)){ display: flex; - &.fu-flex-align-right{ + &.fu-flex-align-right:not(:has(fusion-skeleton)){ &:after{ content: '#fuHeaderTeleportRight'; margin: auto; @@ -161,13 +162,13 @@ $drilldownHeight: 72px; color: #005BE2; } } - #pageHeaderTopTeleportSlot{ + #pageHeaderTopTeleportSlot:not(:has(fusion-skeleton)){ &:after{ content: '#pageHeaderTopTeleportSlot'; color: #005BE2; } } - #pageHeaderBottomTeleportSlot{ + #pageHeaderBottomTeleportSlot:not(:has(fusion-skeleton)){ &:after{ content: '#pageHeaderBottomTeleportSlot'; color: #005BE2; diff --git a/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.ts b/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.ts index 635c33e94..9b3c43fde 100644 --- a/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.ts +++ b/projects/fusion-ui/components/layout/v4/components/layout-header/layout-header.component.ts @@ -1,14 +1,15 @@ import {ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output} from '@angular/core'; import {CommonModule} from '@angular/common'; import {DynamicComponentsModule} from '@ironsource/fusion-ui/components/dynamic-components/v1'; -import {HeaderContent, TeleportWrapperElement} from '../../layout.entities'; +import {HeaderContent, TeleportSkeleton, TeleportWrapperElement} from '../../layout.entities'; import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; import {IconButtonComponent} from '@ironsource/fusion-ui/components/button/v4'; +import {SkeletonComponent} from '@ironsource/fusion-ui/components/skeleton'; @Component({ selector: 'fusion-layout-header', standalone: true, - imports: [CommonModule, DynamicComponentsModule, IconModule, IconButtonComponent], + imports: [CommonModule, DynamicComponentsModule, IconModule, IconButtonComponent, SkeletonComponent], templateUrl: './layout-header.component.html', styleUrls: ['./layout-header.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -39,15 +40,34 @@ export class LayoutHeaderComponent { get hasTopLine(): boolean { return this.isMultiline && !!this._headerContent?.topRowContent?.show; } + + get teleportTopRowBaseSkeletons(): TeleportSkeleton[] { + return this._headerContent?.topRowContent?.skeletons ?? []; + } + + get teleportTopRowBaseSkeletonsGap(): string { + return this._headerContent?.topRowContent?.skeletonsGap; + } + get teleportTopRowElements(): TeleportWrapperElement[] { return this._headerContent?.topRowContent?.teleportElements ?? []; } get hasBottomLine(): boolean { return this.isMultiline && !!this._headerContent?.bottomRowContent?.show; } + get bottomTopRowElements(): TeleportWrapperElement[] { return this._headerContent?.bottomRowContent?.teleportElements ?? []; } + + get teleportBottomLineSkeletons(): TeleportSkeleton[] { + return this.isMultiline && this._headerContent?.bottomRowContent?.skeletons ? this._headerContent.bottomRowContent.skeletons : []; + } + + get teleportBottomLineSkeletonsGap(): string { + return this._headerContent?.bottomRowContent?.skeletonsGap; + } + get isDrilldown(): boolean { return this.isMultiline && !!this._headerContent?.hasBackButton; } diff --git a/projects/fusion-ui/components/layout/v4/layout.entities.ts b/projects/fusion-ui/components/layout/v4/layout.entities.ts index e3b16de43..c607ad3c9 100644 --- a/projects/fusion-ui/components/layout/v4/layout.entities.ts +++ b/projects/fusion-ui/components/layout/v4/layout.entities.ts @@ -1,6 +1,7 @@ import {Type} from '@angular/core'; import {LayoutUser} from '@ironsource/fusion-ui/entities'; import {PrimaryMenuItem, PrimaryMenuMode} from '@ironsource/fusion-ui/components/navigation-menu/v4'; +import {SkeletonShapeType} from '@ironsource/fusion-ui/components/skeleton'; export interface LayoutConfiguration { navigationMenuItems?: PrimaryMenuItem[]; @@ -11,11 +12,22 @@ export interface LayoutConfiguration { export interface TeleportWrapperElement { id: string; isOnRight?: boolean; + skeletons?: TeleportSkeleton[]; + skeletonsGap?: string; } export interface HeaderAdditionalRowContent { show: boolean; teleportElements?: TeleportWrapperElement[]; + skeletons?: TeleportSkeleton[]; + skeletonsGap?: string; +} + +export interface TeleportSkeleton { + width: string; + height: string; + borderRadius?: string; + shape?: SkeletonShapeType; } export interface HeaderContent { diff --git a/projects/fusion-ui/components/link/index.ts b/projects/fusion-ui/components/link/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/link/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/link/ng-package.json b/projects/fusion-ui/components/link/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/components/link/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/link/public-api.ts b/projects/fusion-ui/components/link/public-api.ts new file mode 100644 index 000000000..a86af1af2 --- /dev/null +++ b/projects/fusion-ui/components/link/public-api.ts @@ -0,0 +1 @@ +export * from './v4'; diff --git a/projects/fusion-ui/components/link/v4/index.ts b/projects/fusion-ui/components/link/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/link/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/link/v4/link.component.entities.ts b/projects/fusion-ui/components/link/v4/link.component.entities.ts new file mode 100644 index 000000000..265c8d6f0 --- /dev/null +++ b/projects/fusion-ui/components/link/v4/link.component.entities.ts @@ -0,0 +1,3 @@ +export type LinkTarget = '_blank' | '_self' | '_parent' | '_top'; +export type LinkColor = 'default' | 'primary'; +export type LinkVariant = 'button' | 'subtitle1' | 'subtitle2' | 'body1' | 'body2' | 'caption' | 'chip-label'; diff --git a/projects/fusion-ui/components/link/v4/link.component.html b/projects/fusion-ui/components/link/v4/link.component.html new file mode 100644 index 000000000..857979e5c --- /dev/null +++ b/projects/fusion-ui/components/link/v4/link.component.html @@ -0,0 +1,31 @@ + + @if (!!startIconName) { + + + } + + @if (!!endIconName) { + + + } + @if (isExternal && !!externalIcon) { + + + } + diff --git a/projects/fusion-ui/components/link/v4/link.component.scss b/projects/fusion-ui/components/link/v4/link.component.scss new file mode 100644 index 000000000..ba13fc65b --- /dev/null +++ b/projects/fusion-ui/components/link/v4/link.component.scss @@ -0,0 +1,92 @@ +@import '../../../src/style/scss/v4/colors'; +@import '../../../src/style/scss/v4/spacings'; +@import '../../../src/style/scss/v4/vars/fonts'; + +:host { + margin: 0; + padding: 0; + + a.fu-link { + text-decoration: none; + @extend %font-v4-body-1; + display: flex; + align-items: center; + gap: 4px; + + // region common + &:link, + &:visited, + &:active { + color: var(--text-secondary, #{$color-v4-text-primary}); + } + + &:hover { + color: var(--text-primary, #{$color-v4-text-primary}); + } + + // endregion + + // region variant + &.fu-link-variant-caption { + @extend %font-v4-caption; + } + + &.fu-link-variant-body2 { + @extend %font-v4-body-2; + } + + &.fu-link-variant-subtitle1 { + @extend %font-v4-subtitle-1; + } + + &.fu-link-variant-subtitle2 { + @extend %font-v4-subtitle-2; + } + + &.fu-link-variant-chip-label { + @extend %font-v4-chip-label; + } + + &.fu-link-variant-button { + @extend %font-v4-button; + } + + // endregion + + // region color primary + &.fu-link-primary { + &:link, + &:visited, + &:active { + color: var(--primary-main, #{$color-v4-primary-main}); + } + + &:hover { + color: var(--primary-dark, #{$color-v4-primary-dark}); + } + } + + // endregion + + // region icons + fusion-icon { + width: 16px; + height: 16px; + } + // endregion + + // region disabled and underlined + &.fu-link-disabled, + &.fu-link-primary.fu-link-disabled { + color: var(--text-disabled, #{$color-v4-text-disabled}); + cursor: default; + pointer-events: none; + } + + &.fu-link-underline { + text-decoration: underline; + } + + // endregion + } +} diff --git a/projects/fusion-ui/components/link/v4/link.component.spec.ts b/projects/fusion-ui/components/link/v4/link.component.spec.ts new file mode 100644 index 000000000..58dd7d28f --- /dev/null +++ b/projects/fusion-ui/components/link/v4/link.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LinkComponent } from './link.component'; + +describe('LinkComponent', () => { + let component: LinkComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LinkComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LinkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/link/v4/link.component.stories.ts b/projects/fusion-ui/components/link/v4/link.component.stories.ts new file mode 100644 index 000000000..543ddfb52 --- /dev/null +++ b/projects/fusion-ui/components/link/v4/link.component.stories.ts @@ -0,0 +1,208 @@ +import {Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {environment} from '../../../../../stories/environments/environment'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {LinkComponent} from './link.component'; + +export default { + title: 'V4/Components/Buttons/Link', + component: LinkComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule] + }) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + testId: 'link-test', + content: 'Read more', + color: 'primary', + disabled: false, + target: '_self', + underline: false + }, + argTypes: { + endIconName: { + type: 'string', + options: [null, 'ph/regular/arrow-right'], + control: { + type: 'select', + labels: { + null: 'no icon', + frame: 'arrow-right' + } + } + }, + externalIconName: { + type: 'string', + options: [null, 'ph/regular/arrow-square-up-right'], + control: { + type: 'select', + labels: { + null: 'no icon', + frame: 'arrow-square-up-right' + } + } + } + } +} as Meta; + +type Story = StoryObj; + +const templateCommon: string = `{{content}}`; + +const oneBlockStyle = `display: flex; flex-direction: column; gap:8px;`; +const labelStyle = `font-family: Inter;font-size: 14px;font-style: normal;font-weight: 500;line-height: 20px;letter-spacing: -0.084px;`; + +export const Basic: Story = { + render: args => ({ + props: args, + template: templateCommon + }) +}; + +export const Variants: Story = { + render: args => ({ + props: args, + template: ` +
    +
    +
    body1
    + {{content}} +
    + +
    +
    body2
    + {{content}} +
    + +
    +
    subtitle1
    + {{content}} +
    + +
    +
    subtitle2
    + {{content}} +
    + +
    +
    button
    + {{content}} +
    +
    + ` + }) +}; + +export const Colors: Story = { + render: args => ({ + props: args, + template: ` +
    +
    +
    Primary
    + {{content}} +
    + +
    +
    Default
    + {{content}} +
    +
    + ` + }) +}; + +export const Disabled: Story = { + render: args => ({ + props: args, + template: ` +
    +
    +
    Primary
    + {{content}} +
    + +
    +
    Default
    + {{content}} +
    +
    + ` + }) +}; + +export const Icons: Story = { + render: args => ({ + props: args, + template: ` +
    +
    +
    Open in new tab
    + {{content}} +
    + +
    +
    ArrowRight
    + {{content}} +
    +
    + ` + }) +}; diff --git a/projects/fusion-ui/components/link/v4/link.component.ts b/projects/fusion-ui/components/link/v4/link.component.ts new file mode 100644 index 000000000..2f161baeb --- /dev/null +++ b/projects/fusion-ui/components/link/v4/link.component.ts @@ -0,0 +1,66 @@ +import {ChangeDetectionStrategy, Component, inject, Input} from '@angular/core'; +import {IconData, IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {GenericPipe} from '@ironsource/fusion-ui/pipes/generic'; +import {TestIdsService} from '@ironsource/fusion-ui/services/test-ids'; +import {LinkColor, LinkTarget, LinkVariant} from './link.component.entities'; +import {LinkTestIdModifiers} from '@ironsource/fusion-ui/entities'; + +@Component({ + selector: 'fusion-link', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [GenericPipe, IconModule], + templateUrl: './link.component.html', + styleUrl: './link.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LinkComponent { + @Input() testId: string; + + @Input() href: string = '#'; + @Input() target: LinkTarget = '_self'; + + @Input() set variant(value: LinkVariant) { + this._variant = value ?? 'body1'; + } + private _variant: LinkVariant = 'body1'; + get variant(): LinkVariant { + return this._variant; + } + @Input() set color(value: LinkColor) { + this._color = value ?? 'default'; + } + private _color: LinkColor = 'default'; + get isPrimary(): boolean { + return this._color === 'primary'; + } + + @Input() disabled: boolean = false; + @Input() underline: boolean = false; + + /** @internal */ + @Input() startIconColor: string; + /** @internal */ + @Input() startIconName: IconData; + + @Input() endIconColor: string; + @Input() endIconName: IconData; + + /** @internal */ + @Input() externalIconColor: string; + /** @internal */ + @Input() externalIconName: IconData; + + get isExternal(): boolean { + return this.target === '_blank'; + } + + get externalIcon(): IconData { + return this.externalIconName ?? 'ph/regular/arrow-square-out'; + } + + /** @internal */ + testIdsService: TestIdsService = inject(TestIdsService); + /** @internal */ + testIdLinkModifiers: typeof LinkTestIdModifiers = LinkTestIdModifiers; +} diff --git a/projects/fusion-ui/components/link/v4/ng-package.json b/projects/fusion-ui/components/link/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/link/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/link/v4/public-api.ts b/projects/fusion-ui/components/link/v4/public-api.ts new file mode 100644 index 000000000..5209da919 --- /dev/null +++ b/projects/fusion-ui/components/link/v4/public-api.ts @@ -0,0 +1 @@ +export * from './link.component'; diff --git a/projects/fusion-ui/components/menu-drop/v4/index.ts b/projects/fusion-ui/components/menu-drop/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.html b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.html new file mode 100644 index 000000000..7ee891a71 --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.html @@ -0,0 +1,13 @@ +
    + @for (menuItem of menuItems; track menuItem) { +
    + @if (menuItem.icon) { + + } +
    {{ menuItem.label }}
    +
    + } +
    diff --git a/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.scss b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.scss new file mode 100644 index 000000000..61e6e390c --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.scss @@ -0,0 +1,80 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + --fu-drop-menu-max-width: 220px; + --fu-drop-menu-piosition: fixed; + --fu-drop-menu-bg-color: #{$color-v4-background-default}; + --fu-drop-menu-padding: 8px; + --fu-drop-menu-border-radius: 8px; + --fu-drop-menu-border: 1px solid var(--common-divider, #{$color-v4-common-divider}); + --fu-drop-menu-box-shadow: #{$boxShadowV4-LG}; + --fu-drop-menu-item-gap: 4px; + --fu-drop-menu-max-items-shown: 5; + --fu-drop-menu-item-icon-size: 20px; + --fu-drop-menu-item-icon-color: var(--action-active, #{$color-v4-action-active}); + --fu-drop-menu-item-color: var(--text-primary, #{$color-v4-text-primary}); + --fu-drop-menu-item-hover-bg-color: #{$color-v4-action-hover}; + --fu-drop-menu-item-padding: 6px 8px; + --fu-drop-menu-item-inner-gap: 8px; + --fu-drop-menu-item-max-height: 32px; + --fu-drop-menu-item-border-radius: 8px; + + display: block; + @extend %border-box-normalize; + @extend %list-reset; + + position: var(--fu-drop-menu-piosition); + z-index: getZIndexLayerOffset(application, 1); + + .fu-menu-holder { + max-width: var(--fu-drop-menu-max-width); + width: var(--fu-drop-menu-width); + max-height: calc(32px * var(--fu-drop-menu-max-items-shown) + (var(--fu-drop-menu-padding)*2)); // 5 items + 36px for padding + overflow-x: hidden; + overflow-y: auto; + @extend %customScroll; + padding: var(--fu-drop-menu-padding); + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--fu-drop-menu-item-gap); + background-color: var(--fu-drop-menu-bg-color); + box-shadow: var(--fu-drop-menu-box-shadow); + border-radius: var(--fu-drop-menu-border-radius); + border: var(--fu-drop-menu-border); + + .fu-menu-item{ + display: flex; + align-items: center; + align-self: stretch; + gap: var(--fu-drop-menu-item-inner-gap); + padding: var(--fu-drop-menu-item-padding); + color: var(--fu-drop-menu-item-color); + @extend %font-v4-body-2; + max-height: var(--fu-drop-menu-item-max-height); + border-radius: var(--fu-drop-menu-item-border-radius); + + .fu-menu-item-icon{ + @include size(var(--fu-drop-menu-item-icon-size)); + color: var(--fu-drop-menu-item-icon-color); + } + + &:hover{ + background-color: var(--fu-drop-menu-item-hover-bg-color); + cursor: pointer; + } + &.fu-disabled{ + pointer-events: none; + opacity: .7; + &:hover { + background-color: unset; + cursor: default; + } + .fu-menu-item-icon{ + opacity: .7; + } + } + } + } +} diff --git a/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.spec.ts b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.spec.ts new file mode 100644 index 000000000..b35b8d365 --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MenuDropV4Component } from './menu-drop.component'; + +describe('MenuDropV4Component', () => { + let component: MenuDropV4Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ MenuDropV4Component ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MenuDropV4Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.stories_._ts b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.stories_._ts new file mode 100644 index 000000000..6c60c1779 --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.stories_._ts @@ -0,0 +1,66 @@ +import {CommonModule} from '@angular/common'; +import {Meta, StoryObj, componentWrapperDecorator} from '@storybook/angular'; +import {moduleMetadata} from '@storybook/angular'; +import {dedent} from 'ts-dedent'; +import {environment} from 'stories/environments/environment'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {MenuDropItem} from '@ironsource/fusion-ui/components/menu-drop'; +import {MenuDropV4Component} from '@ironsource/fusion-ui/components/menu-drop/v4/menu-drop.component'; + +const MOCK_MENU_ITEMS: MenuDropItem[] = [ + {icon: 'frame', label: 'List item 1'}, + {icon: 'frame', label: 'List item 2'}, + {icon: 'frame', label: 'List item 3'}, + {icon: 'frame', label: 'List item 4'} +]; + +const MOCK_ROW_ACTIONS = [ + {icon: 'ph/pencil-simple', label: 'Edit'}, + {icon: 'ph/copy', label: 'Duplicate'}, + {icon: 'ph/trash-simple', label: 'Delete'} +]; + +export default { + title: 'V4/Components/Dropped Menu', + component: MenuDropV4Component, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + parameters: { + docs: { + description: { + component: dedent` + **Dropped menu** useful for example for table-rows for multiple actions. + - **buttonIcon**: optional, icon in the button. Default "more-vert" + - **dropdownPosition: Position**: optional, open dropdown position. (see https://floating-ui.com/ type Position)` + } + } + }, + args: { + menuItems: MOCK_MENU_ITEMS + }, + argTypes: { + menuItemClicked: {control: false} + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const DisabledItems: Story = {}; +DisabledItems.args = { + menuItems: MOCK_MENU_ITEMS.map((item, idx) => { + return {...item, disabled: idx >= 2}; + }) +}; + +export const TableRowMenu: Story = {}; +TableRowMenu.args = { + menuItems: MOCK_ROW_ACTIONS +}; diff --git a/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.ts b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.ts new file mode 100644 index 000000000..1c8efea0a --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/menu-drop.component.ts @@ -0,0 +1,15 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {MenuDropComponent} from '@ironsource/fusion-ui/components/menu-drop'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; + +@Component({ + selector: 'fusion-menu-drop', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [CommonModule, IconModule], + templateUrl: './menu-drop.component.html', + styleUrls: ['./menu-drop.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class MenuDropV4Component extends MenuDropComponent {} diff --git a/projects/fusion-ui/components/menu-drop/v4/menu-drop.entities.ts b/projects/fusion-ui/components/menu-drop/v4/menu-drop.entities.ts new file mode 100644 index 000000000..8729b0dde --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/menu-drop.entities.ts @@ -0,0 +1,7 @@ +import {IconData} from '@ironsource/fusion-ui/components/icon/common/entities'; + +export interface MenuDropItem { + label: string; + icon?: IconData; + disabled?: boolean; +} diff --git a/projects/fusion-ui/components/menu-drop/v4/ng-package.json b/projects/fusion-ui/components/menu-drop/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/menu-drop/v4/public-api.ts b/projects/fusion-ui/components/menu-drop/v4/public-api.ts new file mode 100644 index 000000000..a8f5fc765 --- /dev/null +++ b/projects/fusion-ui/components/menu-drop/v4/public-api.ts @@ -0,0 +1,2 @@ +export {MenuDropV4Component as MenuDropComponent} from './menu-drop.component'; +export {MenuDropItem} from './menu-drop.entities'; diff --git a/projects/fusion-ui/components/multi-dropdown/v4/multi-dropdown-v4.component.html b/projects/fusion-ui/components/multi-dropdown/v4/multi-dropdown-v4.component.html index 448cf710f..5a4139b89 100644 --- a/projects/fusion-ui/components/multi-dropdown/v4/multi-dropdown-v4.component.html +++ b/projects/fusion-ui/components/multi-dropdown/v4/multi-dropdown-v4.component.html @@ -9,12 +9,24 @@ } -
    - -
    - - + @if (templateRef){ +
    + +
    + } @else if (dynamicTrigger){ +
    + +
    + } @else { + + + } + { + let component: SkeletonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SkeletonComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SkeletonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + + expect(component).toBeTruthy(); + }); + + it('should have default: shape-rectangle class', () => { + expect(fixture.nativeElement.querySelector('div.fu-shape-rectangle')).toBeTruthy(); + }); + + it('type circle should have: shape-circle class', () => { + fixture = TestBed.createComponent(SkeletonComponent); + component = fixture.componentInstance; + component.shape = 'circle'; + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('div.fu-shape-circle')).toBeTruthy(); + }); + + it('type square should have: shape-square class', () => { + fixture = TestBed.createComponent(SkeletonComponent); + component = fixture.componentInstance; + component.shape = 'square'; + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('div.fu-shape-square')).toBeTruthy(); + }); + + it('type pill should have: shape-pill class', () => { + fixture = TestBed.createComponent(SkeletonComponent); + component = fixture.componentInstance; + component.shape = 'pill'; + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('div.fu-shape-pill')).toBeTruthy(); + }); + +}); diff --git a/projects/fusion-ui/components/skeleton/v4/skeleton.component.stories.ts b/projects/fusion-ui/components/skeleton/v4/skeleton.component.stories.ts new file mode 100644 index 000000000..774f0981b --- /dev/null +++ b/projects/fusion-ui/components/skeleton/v4/skeleton.component.stories.ts @@ -0,0 +1,58 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {SkeletonComponent} from './skeleton.component'; + +const oneBlockStyle = `display: flex; flex-direction: column; gap:8px;`; +const labelStyle = `font-family: Inter;font-size: 14px;font-style: normal;font-weight: 500;line-height: 20px;letter-spacing: -0.084px;`; + +export default { + title: 'V4/Components/Feedback/Skeleton', + component: SkeletonComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const Shapes: Story = { + render: args => ({ + props: args, + template: ` +
    +
    + +
    Circle
    +
    + +
    + +
    Square
    +
    + +
    + +
    Rectangle
    +
    + +
    + +
    Pill
    +
    +
    + ` + }) +}; diff --git a/projects/fusion-ui/components/skeleton/v4/skeleton.component.ts b/projects/fusion-ui/components/skeleton/v4/skeleton.component.ts new file mode 100644 index 000000000..75b75af6d --- /dev/null +++ b/projects/fusion-ui/components/skeleton/v4/skeleton.component.ts @@ -0,0 +1,25 @@ +import {ChangeDetectionStrategy, Component, HostBinding, Input} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {SkeletonShapeType} from './skeleton.component.entities'; + +@Component({ + selector: 'fusion-skeleton', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [CommonModule], + template: `
    `, + styleUrl: './skeleton.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SkeletonComponent { + @Input() shape: SkeletonShapeType = 'rectangle'; + @Input() size: number = 16; + + @HostBinding('style.--fu-skeleton-height') get height(): string { + return `${this.size}px`; + } + + get class(): string { + return `fu-skeleton fu-shape-${this.shape}`; + } +} diff --git a/projects/fusion-ui/components/svg/svg.component.ts b/projects/fusion-ui/components/svg/svg.component.ts index 98a3c841d..4690575fc 100644 --- a/projects/fusion-ui/components/svg/svg.component.ts +++ b/projects/fusion-ui/components/svg/svg.component.ts @@ -54,6 +54,9 @@ export class SvgComponent implements AfterViewInit, OnDestroy { // check for .svg if no, add this.svgPath += '.svg'; } + if (this.svgPath.startsWith('assets/')) { + assetPath = assetPath.replace('assets/icons/', ''); + } return `${assetPath}${this.svgPath}`; } diff --git a/projects/fusion-ui/components/table/v1/components/table-row/column-data.ts b/projects/fusion-ui/components/table/common/entities/column-data.ts similarity index 100% rename from projects/fusion-ui/components/table/v1/components/table-row/column-data.ts rename to projects/fusion-ui/components/table/common/entities/column-data.ts diff --git a/projects/fusion-ui/components/table/common/entities/public-api.ts b/projects/fusion-ui/components/table/common/entities/public-api.ts index a8fd30b60..44fefaa64 100644 --- a/projects/fusion-ui/components/table/common/entities/public-api.ts +++ b/projects/fusion-ui/components/table/common/entities/public-api.ts @@ -11,3 +11,5 @@ export * from './table-cell-position'; export * from './table-column-type.enum'; export * from './table-row-classes.enum'; export * from './table.config'; + +export * from './column-data'; diff --git a/projects/fusion-ui/components/table/common/entities/table-column.ts b/projects/fusion-ui/components/table/common/entities/table-column.ts index 2d8675472..4fd633840 100644 --- a/projects/fusion-ui/components/table/common/entities/table-column.ts +++ b/projects/fusion-ui/components/table/common/entities/table-column.ts @@ -4,6 +4,9 @@ import {DropdownOption} from '@ironsource/fusion-ui/components/dropdown-option/e import {EventEmitter} from '@angular/core'; import {CellPosition} from './table-cell-position'; import {IconData} from '@ironsource/fusion-ui/components/icon/v1'; +import {TooltipCustom} from '@ironsource/fusion-ui/components/tooltip/common/base'; + +export type TableCellAlign = 'left' | 'center' | 'right'; export interface TableColumn { key: string; @@ -11,16 +14,19 @@ export interface TableColumn { groupName?: string; type?: TableColumnTypeEnum; inputType?: InlineInputType; + inputErrorIconShow?: boolean; // show error icon in input inline v1 + inlineDropdownOptions?: DropdownOption[]; // used for inline dropdown in table v4 totalRowTypeAsString?: boolean; // data type represent in total string. default true component?: any; sort?: string; class?: string; width?: string; style?: any; - align?: 'left' | 'center' | 'right'; - headerAlign?: 'left' | 'center' | 'right'; + align?: TableCellAlign; + headerAlign?: TableCellAlign; tooltip?: string; tooltipIcon?: IconData; + tooltipCustom?: TooltipCustom; pipeOptions?: string; dataParser?: (data: any) => any; // used for data parsing (null to Undefined in budget for example) // customErrorMapping example, turn pattern error to decimal error: { pattern: { error: 'decimalMax', values: {'decimalMax': 2}}} @@ -28,6 +34,7 @@ export interface TableColumn { [errorKey: string]: { errorMessageKey: string; textMapping?: {key: string; value: string}[]; + errorText?: string; }; }; filter?: { @@ -37,6 +44,8 @@ export interface TableColumn { }; sticky?: boolean; stickyLeftMargin?: string; + stickyRight?: boolean; // from v4, sticky column on end of table + stickyRightMargin?: string; // from v4, sticky column on end of table but not last stickyRight column dateFormat?: string; ignoreTimeZone?: boolean; colspan?: number; diff --git a/projects/fusion-ui/components/table/common/entities/table-options.ts b/projects/fusion-ui/components/table/common/entities/table-options.ts index 8bea6dd6c..c095d0a49 100644 --- a/projects/fusion-ui/components/table/common/entities/table-options.ts +++ b/projects/fusion-ui/components/table/common/entities/table-options.ts @@ -4,6 +4,7 @@ import {DynamicComponentConfiguration} from '@ironsource/fusion-ui/components/dy import {IconData} from '@ironsource/fusion-ui/components/icon/common/entities'; import {EventEmitter} from '@angular/core'; import {MenuDropItem} from '@ironsource/fusion-ui/components/menu-drop'; +import {EmptyStateType} from '@ironsource/fusion-ui/components/empty-state/v4'; export interface TableLabel { text: string; @@ -33,6 +34,8 @@ export interface TableOptions { noDataMessage?: string; noDataSubMessage?: string; noDataImageBgUrl?: string; // custom image for empty table as background URL (v3) + emptyTableIcon?: string; + emptyTableType?: EmptyStateType; // used for empty table v4 state customNoData?: DynamicComponentConfiguration; // user defined "no data" content isGroupedTable?: boolean; pagination?: TablePaginationOption; @@ -40,7 +43,6 @@ export interface TableOptions { stickyHeader?: boolean; // is sticky header table hideHeaderOnEmpty?: boolean; // is need to hide columns headers if table empty cellBorders?: boolean; - emptyTableIcon?: string; rowStyle?: any; rowHeight?: TableRowHeight; rowTrackingOption?: string; @@ -78,9 +80,12 @@ export interface TableRowMetaData { maxRowspanInColumn?: number; } +export type InnerEntityType = 'innerRows' | 'dynamicComponent'; // used in table v4 default is 'innerRows' + export interface TableRowsExpandableOptions { key: string; columns: TableColumn[]; + innerEntityType?: InnerEntityType; sticky?: boolean; keyToIgnore?: string; expandLevels?: number; diff --git a/projects/fusion-ui/components/table/common/entities/table-row-expand-emitter.ts b/projects/fusion-ui/components/table/common/entities/table-row-expand-emitter.ts index 0972c9ce0..924c3a158 100644 --- a/projects/fusion-ui/components/table/common/entities/table-row-expand-emitter.ts +++ b/projects/fusion-ui/components/table/common/entities/table-row-expand-emitter.ts @@ -1,3 +1,5 @@ +import {InnerEntityType} from './table-options'; + export interface TableRowExpandEmitter { rowIndex: string | number; row: any; @@ -5,4 +7,5 @@ export interface TableRowExpandEmitter { successCallback?: () => void; failedCallback?: () => void; updateMap?: boolean; + innerEntityType?: InnerEntityType; } diff --git a/projects/fusion-ui/components/table/common/entities/table-row-remove-action.ts b/projects/fusion-ui/components/table/common/entities/table-row-remove-action.ts index f815a47f8..b06043be6 100644 --- a/projects/fusion-ui/components/table/common/entities/table-row-remove-action.ts +++ b/projects/fusion-ui/components/table/common/entities/table-row-remove-action.ts @@ -10,5 +10,6 @@ export interface TableRowRemoveAction { } export interface TableMultipleActions { + stickyActionButton?: boolean; actions: MenuDropItem[]; } diff --git a/projects/fusion-ui/components/table/common/services/table.service.ts b/projects/fusion-ui/components/table/common/services/table.service.ts index bad3aa56e..8b71356fc 100644 --- a/projects/fusion-ui/components/table/common/services/table.service.ts +++ b/projects/fusion-ui/components/table/common/services/table.service.ts @@ -3,6 +3,9 @@ import {isNullOrUndefined, isNumber, isUndefined} from '@ironsource/fusion-ui/ut import {DomSanitizer} from '@angular/platform-browser'; import {LogService} from '@ironsource/fusion-ui/services/log'; import { + DEFAULT_EXPANDABLE_LEVEL, + MAXIMUM_EXPANDABLE_LEVEL, + TableCellAlign, TableColumn, TableColumnTypeEnum, TableOptions, @@ -11,14 +14,15 @@ import { TableRowMetaData, TableRowsExpandableOptions } from '@ironsource/fusion-ui/components/table/common/entities'; -import {DEFAULT_EXPANDABLE_LEVEL, MAXIMUM_EXPANDABLE_LEVEL} from '@ironsource/fusion-ui/components/table/common/entities'; import {MenuDropItem} from '@ironsource/fusion-ui/components/menu-drop'; import {UniqueIdService} from '@ironsource/fusion-ui/services/unique-id'; +import {InlineInputType} from '@ironsource/fusion-ui/components/input-inline'; @Injectable() export class TableService { private selectedRows: any[] = []; public selectionChanged = new EventEmitter(); + public tableScrolled = new EventEmitter(); public rowModelChange: EventEmitter = new EventEmitter(); public rowActionClicked = new EventEmitter<{action: MenuDropItem; rowIndex: string | number; row: TableRow}>(); public expandLevels: number; @@ -123,10 +127,12 @@ export class TableService { return this.selectedRows.length && rows.length !== this.selectedRows.length; } - getColumnStyle(col: any): any { + getColumnStyle(col: TableColumn): any { const style = col.style || {}; if (col.stickyLeftMargin) { style.left = col.stickyLeftMargin; + } else if (col.stickyRightMargin) { + style.right = col.stickyRightMargin; } return style; } @@ -148,7 +154,7 @@ export class TableService { return this.isInSelected(row) !== -1; } - isColumnSortable(col: any): boolean { + isColumnSortable(col: TableColumn): boolean { return !isUndefined(col.sort); } @@ -236,6 +242,8 @@ export class TableService { let headerClass = ''; if (col.sticky) { headerClass += ' sticky-left'; + } else if (col.stickyRight) { + headerClass += ' sticky-right'; } if (col.class && col.class.indexOf('display-shadow-on-scroll') !== -1) { headerClass += ' display-shadow-on-scroll'; @@ -296,6 +304,14 @@ export class TableService { return this.rowsMetadata[row['_rowId']]?.maxRowspanInColumn ?? 0; } + getCellAlignByColumnType(column: TableColumn): TableCellAlign | null { + const inputTypeAlignRight = + this.isTypeInputEdit(column) && column.inputType !== InlineInputType.Text && column.inputType !== InlineInputType.Dropdown; + return this.isTypeCurrency(column) || this.isTypeNumber(column) || this.isTypePercent(column) || inputTypeAlignRight + ? 'right' + : null; + } + private getRowspanColumns(row: any, columnsKeys: string[]): {[key: string]: number} { const multiRows = {}; columnsKeys.forEach(cell => { diff --git a/projects/fusion-ui/components/table/v1/components/table-cell/table-cell.component.html b/projects/fusion-ui/components/table/v1/components/table-cell/table-cell.component.html index 1e1b68575..b27abc624 100644 --- a/projects/fusion-ui/components/table/v1/components/table-cell/table-cell.component.html +++ b/projects/fusion-ui/components/table/v1/components/table-cell/table-cell.component.html @@ -49,6 +49,7 @@ [formControl]="data" [loading]="isInRequest$ | async" [error]="inputError$ | async" + [inputErrorIconShow]="column.inputErrorIconShow" [readOnly]="isReadOnly" [currencyPipeParameters]="column?.currencyPipeParameters" > diff --git a/projects/fusion-ui/components/table/v1/components/table-cell/table-cell.component.ts b/projects/fusion-ui/components/table/v1/components/table-cell/table-cell.component.ts index b1f28cab0..ba6a128ed 100644 --- a/projects/fusion-ui/components/table/v1/components/table-cell/table-cell.component.ts +++ b/projects/fusion-ui/components/table/v1/components/table-cell/table-cell.component.ts @@ -259,7 +259,10 @@ export class TableCellComponent implements OnInit, OnChanges { } else { const allErrors = formControl.errors || {}; Object.keys(allErrors).forEach(errorKey => { - this.inputError$.next(this._getMessage(errorKey, this.column.customErrorMapping[errorKey] || {})); + this.inputError$.next( + this.column.customErrorMapping[errorKey]?.errorText ?? + this._getMessage(errorKey, this.column.customErrorMapping[errorKey] || {}) + ); }); } } diff --git a/projects/fusion-ui/components/table/v1/components/table-row/table-row.component.ts b/projects/fusion-ui/components/table/v1/components/table-row/table-row.component.ts index 99ce2b46a..9297037c7 100644 --- a/projects/fusion-ui/components/table/v1/components/table-row/table-row.component.ts +++ b/projects/fusion-ui/components/table/v1/components/table-row/table-row.component.ts @@ -14,8 +14,7 @@ import {TableColumn, TableOptions, TableRowExpandEmitter, TableRowMetaData} from import {TableService} from '@ironsource/fusion-ui/components/table/common/services'; import {isNullOrUndefined} from '@ironsource/fusion-ui/utils'; import {Observable, of} from 'rxjs'; -import {ColumnData} from './column-data'; -import {TableRow} from '@ironsource/fusion-ui/components/table/common/entities'; +import {TableRow, ColumnData} from '@ironsource/fusion-ui/components/table/common/entities'; import {IconData} from '@ironsource/fusion-ui/components/icon/v1'; @Component({ diff --git a/projects/fusion-ui/components/table/v1/table.component.html b/projects/fusion-ui/components/table/v1/table.component.html index b56e2bc27..2fbd68b29 100644 --- a/projects/fusion-ui/components/table/v1/table.component.html +++ b/projects/fusion-ui/components/table/v1/table.component.html @@ -22,12 +22,21 @@ {{ column.title }} - + @if (column.tooltip){ + + } @else if (column.tooltipCustom){ + + } column.type === TableColumnTypeEnum.Checkbox && column.title !== '') : false; diff --git a/projects/fusion-ui/components/table/v2/components/table-row/column-data.ts b/projects/fusion-ui/components/table/v2/components/table-row/column-data.ts deleted file mode 100644 index 1e8e9a8ba..000000000 --- a/projects/fusion-ui/components/table/v2/components/table-row/column-data.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface ColumnData { - classes: string[]; - tooltip: string; - hasTooltip: boolean; - isRemove: boolean; - infoIconOnHoverTooltip: string; - styles: any; - colspan: number; - width: string; -} diff --git a/projects/fusion-ui/components/table/v2/components/table-row/table-row.component.ts b/projects/fusion-ui/components/table/v2/components/table-row/table-row.component.ts index 62848d73f..686041e5d 100644 --- a/projects/fusion-ui/components/table/v2/components/table-row/table-row.component.ts +++ b/projects/fusion-ui/components/table/v2/components/table-row/table-row.component.ts @@ -14,8 +14,7 @@ import {TableColumn, TableOptions, TableRowExpandEmitter, TableRowMetaData} from import {TableService} from '@ironsource/fusion-ui/components/table/common/services'; import {isNullOrUndefined} from '@ironsource/fusion-ui/utils'; import {Observable, of} from 'rxjs'; -import {ColumnData} from './column-data'; -import {TableRow} from '@ironsource/fusion-ui/components/table/common/entities'; +import {TableRow, ColumnData} from '@ironsource/fusion-ui/components/table/common/entities'; import {IconData} from '@ironsource/fusion-ui/components/icon/v1'; @Component({ diff --git a/projects/fusion-ui/components/table/v3/components/table-row/column-data.ts b/projects/fusion-ui/components/table/v3/components/table-row/column-data.ts deleted file mode 100644 index 1e8e9a8ba..000000000 --- a/projects/fusion-ui/components/table/v3/components/table-row/column-data.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface ColumnData { - classes: string[]; - tooltip: string; - hasTooltip: boolean; - isRemove: boolean; - infoIconOnHoverTooltip: string; - styles: any; - colspan: number; - width: string; -} diff --git a/projects/fusion-ui/components/table/v3/components/table-row/table-row.component.ts b/projects/fusion-ui/components/table/v3/components/table-row/table-row.component.ts index 029043a95..d8f72781e 100644 --- a/projects/fusion-ui/components/table/v3/components/table-row/table-row.component.ts +++ b/projects/fusion-ui/components/table/v3/components/table-row/table-row.component.ts @@ -15,8 +15,7 @@ import {TableColumn, TableOptions, TableRowExpandEmitter, TableRowMetaData} from import {TableService} from '@ironsource/fusion-ui/components/table/common/services'; import {isNullOrUndefined} from '@ironsource/fusion-ui/utils'; import {Observable, of} from 'rxjs'; -import {ColumnData} from './column-data'; -import {TableRow} from '@ironsource/fusion-ui/components/table/common/entities'; +import {TableRow, ColumnData} from '@ironsource/fusion-ui/components/table/common/entities'; import {IconData} from '@ironsource/fusion-ui/components/icon/v1'; import {TableTestIdModifiers} from '@ironsource/fusion-ui/entities'; import {TestIdsService} from '@ironsource/fusion-ui/services/test-ids'; diff --git a/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.html b/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.html new file mode 100644 index 000000000..f8975fd5f --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.html @@ -0,0 +1,92 @@ +@for (row of rows; track trackByRowInvoke(); let rowIndex = $index) { + + + + + @if(displayExpandableRows(rowIndex)){ + + } + + + + + + @if(isInnerEntityType('dynamicComponent')){ + + @if(innerEntity.length){ + + +
    + +
    + + + } +
    + } @else { + + + @if(!loadingChildRows[parentIndex] && (hasMore$ | async) && last){ + + +
    + Load more +
    + + + } +
    + } +
    + @if(loadingChildRows[parentIndex]){ + + } + @if (failedChildRows[parentIndex]){ + + +
    + Failed to load data. + + Try again + +
    + + + } +
    + +} diff --git a/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.scss b/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.scss new file mode 100644 index 000000000..5153a7441 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.scss @@ -0,0 +1,41 @@ +@import "../../../../../src/style/scss/v4/vars/vars"; + +$fullCellHeight: 48px; +$failedCellHeight: 36px; + +:host{ + .full-cell{ + background-color: var(--table-header-cell-bg-color); + &.load-more, &.failed{ + td { + height: $failedCellHeight; + padding: var(--table-row-cell-padding) ; + border-bottom: var(--table-border); + .fu-load-more-button-wrapper, + .fu-load-failed { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; + } + .fu-load-failed{ + padding: 16px; + flex-direction: column; + gap: 8px; + } + } + } + } + tr.is-row-in-request{ + pointer-events: none; + } + tr.fu-inner-object{ + border-bottom: var(--table-border); + .fu-inner-object-wrapper { + display: flex; + align-items: center; + justify-content: center; + } + } +} diff --git a/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.spec.ts b/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.spec.ts new file mode 100644 index 000000000..397bb0aeb --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.spec.ts @@ -0,0 +1,55 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TableService} from '@ironsource/fusion-ui/components/table/common/services'; +import {GenericPipe} from '@ironsource/fusion-ui/pipes/generic'; +import {TableBasicComponent} from './table-basic.component'; + +// todo: check this import +// import {LoadMoreModule} from '@ironsource/fusion-ui/directives/load-more'; +// import {NotAvailablePipe} from '@ironsource/fusion-ui/pipes/not-available'; +// todo: check this versions +// import {MultiDropdownModule} from '@ironsource/fusion-ui/components/multi-dropdown/v1'; +// import {InputInlineModule} from '@ironsource/fusion-ui/components/input-inline/v1'; +// import {TooltipModule} from '@ironsource/fusion-ui/components/tooltip/v1'; +// import {ToggleModule} from '@ironsource/fusion-ui/components/toggle/v1'; +// import {InputModule} from '@ironsource/fusion-ui/components/input/v1'; +// import {LoaderModule} from '@ironsource/fusion-ui/components/loader/v1'; +// import {CheckboxModule} from '@ironsource/fusion-ui/components/checkbox/v1'; +// import {LoaderInlineModule} from '@ironsource/fusion-ui/components/loader-inline/v1'; +// import {DynamicComponentsModule} from '@ironsource/fusion-ui/components/dynamic-components/v1'; +// import {DropdownModule} from '@ironsource/fusion-ui/components/dropdown/v1'; + + +// import {TableCellComponent} from '../table-cell/table-cell.component'; +// import {TableEmptyComponent} from '../table-empty/table-empty.component'; +// import {TableGroupedComponent} from '../table-grouped/table-grouped.component'; +// import {TableLoadingComponent} from '../table-loading/table-loading.component'; +import {TableRowComponent} from '../table-row/table-row.component'; +// import {TableRowGroupedComponent} from '../table-row-grouped/table-row-grouped.component'; + + + +describe('TableBasicComponent', () => { + let component: TableBasicComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + GenericPipe, + TableBasicComponent, + TableRowComponent + ], + providers: [TableService] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TableBasicComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.ts b/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.ts new file mode 100644 index 000000000..b928d48f8 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-basic/table-basic.component.ts @@ -0,0 +1,224 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + inject, + Input, + OnDestroy, + OnInit, + Output, + Renderer2 +} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {fromEvent, Subject, from} from 'rxjs'; +import {filter, mergeMap, takeUntil} from 'rxjs/operators'; +import {GenericPipe} from '@ironsource/fusion-ui/pipes/generic'; +import {TestIdsService} from '@ironsource/fusion-ui/services/test-ids'; +import { + TableColumn, + TableOptions, + TableRowClassesEnum, + TableRowExpandEmitter, + ROW_HOVERED_CLASS_NAME, + InnerEntityType +} from '@ironsource/fusion-ui/components/table/common/entities'; +import {TableTestIdModifiers} from '@ironsource/fusion-ui/entities'; +import {TableService} from '@ironsource/fusion-ui/components/table'; +import {TableRowComponent} from '../table-row/table-row.component'; +import {TableLoadingComponent} from '../table-loading/table-loading.component'; +import {LoadMoreModule} from '@ironsource/fusion-ui/directives/load-more'; +import {LinkComponent} from '@ironsource/fusion-ui/components/link'; + +@Component({ + // eslint-disable-next-line + selector: '[fusionTableBasic]', + standalone: true, + imports: [CommonModule, GenericPipe, TableRowComponent, TableLoadingComponent, LoadMoreModule, LinkComponent], + templateUrl: './table-basic.component.html', + styleUrls: ['./table-basic.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableBasicComponent implements OnInit, OnDestroy, AfterViewInit { + @Input() rows: {[key: string]: any}[]; + @Input() columns: TableColumn[]; + /** @internal */ + @Input() expandedRows: {[key: string]: boolean}; + + @Input() set options(value: TableOptions) { + this.tableOptions = value; + this.childRowOptions = {...value, hasTotalsRow: false}; + } + + /** @internal */ + @Input() testId: string; + + @Output() rowSelected = new EventEmitter(); + @Output() expandRow = new EventEmitter(); + + get options() { + return this.tableOptions; + } + + get fullCellColspan(): number { + if (!!this.tableService.expandLevels) { + return this.columns.length + this.tableService.expandLevels; + } + return this.columns.length; + } + + childRowOptions: TableOptions; + loadingChildRows: {[key: number]: boolean} = {}; + failedChildRows: {[key: number]: boolean} = {}; + + rowIsSelected = this.isRowSelected.bind(this); + rowClass = this.getRowClass.bind(this); + rowRowspanIndexes = this.getRowspanIndexes.bind(this); + + /** @internal */ + tableTestIdModifiers: typeof TableTestIdModifiers = TableTestIdModifiers; + /** @internal */ + testIdsService: TestIdsService = inject(TestIdsService); + + tableService: TableService = inject(TableService); + + private tableOptions: TableOptions; + private onDestroy$ = new Subject(); + private cdr: ChangeDetectorRef = inject(ChangeDetectorRef); + private elementRef: ElementRef = inject(ElementRef); + private renderer: Renderer2 = inject(Renderer2); + + ngOnInit(): void { + this.tableService.selectionChanged.pipe(takeUntil(this.onDestroy$)).subscribe(val => { + this.cdr.markForCheck(); + }); + } + + ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + ngAfterViewInit() { + if (this.tableService.hasRowspanRows) { + this.setHoverForRowspan(); + } + } + + trackByRowInvoke() { + return this.trackByRow.bind(this); + } + + trackByRow(index, row) { + const keyOption = this.options && this.options.rowTrackingOption ? this.options.rowTrackingOption : 'id'; + return row && row[keyOption] ? row[keyOption] : row; + } + + isRowDisabled(row: any): boolean { + return row._options && row._options.some(options => 'disabled'); + } + + isRowSelected(row: any): boolean { + return this.tableService.isRowSelected(row); + } + + getRowClass(row, rowIndex) { + const rowClasses = this.options.rowsOptions || {}; + const classes = {}; + classes[TableRowClassesEnum.Selected] = this.tableService.isRowSelected(row); + classes[TableRowClassesEnum.Disabled] = this.isRowDisabled(row); + return [ + ...Object.keys(classes).filter((item: string) => !!classes[item]), + !!rowClasses[rowIndex] && !!rowClasses[rowIndex].class ? rowClasses[rowIndex].class : null + ].filter(Boolean); + } + + onExpandRow({rowIndex, row, isExpanded}, updateMap = true): void { + if (!!row) { + this.loadingChildRows[rowIndex] = isExpanded; + delete this.failedChildRows[rowIndex]; + const successCallback = this.onExpendRowSuccess(rowIndex).bind(this); + const failedCallback = this.onExpendRowFailed(rowIndex).bind(this); + this.expandRow.emit({rowIndex, row, isExpanded, successCallback, failedCallback, updateMap: updateMap}); + } + } + + displayExpandableRows(rowIndex: number | string): boolean { + return !!this.options?.rowsExpandableOptions?.key && this.isExpanded(rowIndex); + } + + isInnerEntityType(innerType: InnerEntityType) { + return this.options.rowsExpandableOptions.innerEntityType === innerType; + } + + isExpanded(rowIndex: number | string): boolean { + if ( + this.expandedRows?.hasOwnProperty(rowIndex) || + this.loadingChildRows.hasOwnProperty(rowIndex) || + this.failedChildRows.hasOwnProperty(rowIndex) + ) { + return this.expandedRows[rowIndex] || this.loadingChildRows[rowIndex] || this.failedChildRows[rowIndex]; + } + return this.expandedRows['default']; + } + + hasAfterSticky(isLast, hasMore, rowIndex): boolean { + return isLast && (hasMore || this.loadingChildRows[rowIndex] || this.failedChildRows[rowIndex]); + } + + getRowspanIndexes(row): number[] { + return [...Array(this.tableService.getMaxRowspanInColumn(row)).keys()].filter(Boolean); + } + + private setHoverForRowspan() { + const rowElements = this.elementRef.nativeElement.querySelectorAll('tr[data-row-idx]'); + const events = ['mouseenter', 'mouseleave']; + from(events) + .pipe( + mergeMap(event => fromEvent(rowElements, event)), + filter((event: MouseEvent) => { + return ( + (event.type === 'mouseenter' && !(event.target as HTMLElement).classList.contains(ROW_HOVERED_CLASS_NAME)) || + (event.type === 'mouseleave' && (event.target as HTMLElement).classList.contains(ROW_HOVERED_CLASS_NAME)) + ); + }), + takeUntil(this.onDestroy$) + ) + .subscribe(this.toggleHoverClassForRowspan.bind(this)); + } + + private toggleHoverClassForRowspan(event: MouseEvent) { + const eventType = event.type; + const rowIndex = (event.target as HTMLElement).dataset.rowIdx; + const sameRowIndexSelector = 'tr[data-row-idx="' + rowIndex + '"]'; + const rows = [...this.elementRef.nativeElement.querySelectorAll(sameRowIndexSelector)]; + switch (eventType) { + case 'mouseenter': + rows.forEach(row => { + this.renderer.addClass(row, ROW_HOVERED_CLASS_NAME); + }); + break; + case 'mouseleave': + rows.forEach(row => { + this.renderer.removeClass(row, ROW_HOVERED_CLASS_NAME); + }); + break; + } + } + + private onExpendRowSuccess(rowIndex: number): () => void { + return () => { + delete this.loadingChildRows[rowIndex]; + }; + } + + private onExpendRowFailed(rowIndex: number): () => void { + return () => { + delete this.loadingChildRows[rowIndex]; + this.failedChildRows[rowIndex] = true; + this.cdr.detectChanges(); + }; + } +} diff --git a/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.html b/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.html new file mode 100644 index 000000000..d3de19f1e --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.html @@ -0,0 +1,134 @@ +@if ((isRowTotal && tableService.isInTotalTypeString(column)) || tableService.isTypeString(column)) { + + @if(cellStringData){ +
    + {{ cellStringData | notAvailable: notAvailableText }} +
    + } +} @else { + @if (tableService.isTypeCheckbox(column) && isBoolean(data)) { +
    + +
    + } @else if (tableService.isTypeToggleButton(column) && isBoolean(data)) { +
    + +
    + } @else if (tableService.isTypeInputEdit(column) && data) { + @if (!isRowTotal) { +
    + +
    + } + } @else if (tableService.isTypeComponent(column)) { + + + } @else if (tableService.isTypeCurrency(column)) { +
    + {{ + !isNull(data) + ? ($any(data) + | currency + : column?.currencyPipeParameters?.currencyCode || undefined + : column?.currencyPipeParameters?.display || (column?.currencyPipeParameters ? undefined : '$') + : column?.currencyPipeParameters?.digitsInfo || undefined) + : (data | notAvailable: notAvailableText) + }} +
    + } @else if (tableService.isTypeNumber(column)) { +
    + {{ !isNull(data) ? ($any(data) | number: column.pipeOptions) : (data | notAvailable: notAvailableText) }} +
    + } @else if (tableService.isTypePercent(column)) { +
    + {{ !isNull(data) ? ($any(data) | number: column.pipeOptions) : (data | notAvailable: notAvailableText) }} + {{ !isNullOrUndefined(data) ? '%' : null }} +
    + } @else if (tableService.isTypeDate(column)) { +
    + {{ + data + ? isAsDate(data) + ? ($any(data) | date: getDateFormat(column.dateFormat):getDateUTCFormat(column.ignoreTimeZone)) + : data + : !isRowTotal + ? 'No ' + column.title + : '' + }} +
    + } @else { +
     {{ column.type }}
    + } +} + + +@if (!isRowTotal && isLastColumn) { + + + + + + +} + + +
    + +
    +
    + + +
    + + @if (shownActionsMenu$ | async) { + + } +
    +
    diff --git a/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.scss b/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.scss new file mode 100644 index 000000000..bfe253069 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.scss @@ -0,0 +1,110 @@ +@import '../../../../../src/style/scss/v4/vars/vars'; +@import '../../../../../src/style/scss/v4/mixins/mixins'; + +:host { + position: relative; + @extend %font-v4-body-1; + height: var(--table-row-height); + padding: var(--table-row-cell-padding) ; + border-bottom: var(--table-border); + background-color: var(--table-odd-row-background-color); + + &.is-checkbox-holder{ + width: var(--table-checkbox-cell-width); + } + + & > div { + display: flex; + align-items: center; + height: 100%; + width: 100%; + max-width: 350px; + word-break: break-all; + &.right { + justify-content: flex-end; + } + &.center { + justify-content: center; + } + &.checkbox-cell { + margin: 0 auto; + justify-content: center; + } + + &.fu-input-cell{ + position: relative; + left: calc(#{$spacingV4-100} * -1); + &.fu-type-number{ + left: initial; + right: calc(#{$spacingV4-100} * -1); + } + } + } + + .truncate{ + @extend %truncate-flex-child; + } + + .fu-button-holder{ + display: block; + position: absolute; + margin: auto 0; + top: 0; + bottom: 0; + right: 12px; + @include size(28px); + } + + &.fu-sticky-actions{ + position: sticky; + right: 0; + .fu-button-holder{ + border-left: var(--table-border); + right: 0; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + } + } + + &.sticky-left { + position: sticky; + left: 0; + z-index: 2; + &:after{ + content: ''; + position: absolute; + top: 0; + right: -1px; + height: 100%; + width: 1px; + border-right: var(--table-border); + } + } + &.sticky-right { + position: sticky; + right: 0; + z-index: 2; + &:nth-child(1 of .sticky-right){ + &:before{ + content: ''; + position: absolute; + top: 0; + left: -1px; + height: 100%; + width: 1px; + border-left: var(--table-border); + } + } + } +} + +:host-context(.is-row-in-request) { + opacity: var(--table-row-loading-opacity, 0.7); +} + +:host-context(tr:hover) { + background-color: var(--table-row-hover-background-color); +} diff --git a/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.spec.ts b/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.spec.ts new file mode 100644 index 000000000..07da9cb25 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.spec.ts @@ -0,0 +1,39 @@ +import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {ReactiveFormsModule} from '@angular/forms'; +import {ClickOutsideModule} from '@ironsource/fusion-ui/directives/click-outside'; +import {TableCellComponent} from './table-cell.component'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {TableService} from "@ironsource/fusion-ui/components/table/common/services"; +import {NotAvailablePipe} from "@ironsource/fusion-ui/pipes/not-available"; + + + +describe('TableCellComponent', () => { + let component: TableCellComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + TableCellComponent, + IconModule, + ClickOutsideModule, + ReactiveFormsModule, + NotAvailablePipe + ], + providers: [TableService] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TableCellComponent); + component = fixture.componentInstance; + component.options = {}; + component.column = {key: 'a'}; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.ts b/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.ts new file mode 100644 index 000000000..cfa72b4f7 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-cell/table-cell.component.ts @@ -0,0 +1,401 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + HostBinding, + Inject, + Input, + OnChanges, + OnInit, + Optional, + Output, + SimpleChanges, + Type, + ViewChild +} from '@angular/core'; +import {BehaviorSubject, Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {isBoolean, isNull, isNullOrUndefined} from '@ironsource/fusion-ui/utils'; +import {TableService} from '@ironsource/fusion-ui/components/table/common/services'; +import {InputInlineComponent} from '@ironsource/fusion-ui/components/input-inline/v4'; +import {AdvancedInputInline} from '@ironsource/fusion-ui/components/input-inline/common/base'; +import { + DEFAULT_REMOVE_ICON_V3, + DEFAULT_REMOVE_TOOLTIP_TEXT, + TABLE_OPTIONS_TOKEN, + TableModuleOptions, + CellPosition, + TableColumn, + TableOptions, + TableRowHeight, + TableMultipleActions +} from '@ironsource/fusion-ui/components/table/common/entities'; +import {ERROR_MESSAGES} from '@ironsource/fusion-ui/components/error-message'; +import {LogService} from '@ironsource/fusion-ui/services/log'; +import {DynamicComponentConfiguration} from '@ironsource/fusion-ui/components/dynamic-components/common/entities'; +import {IconData} from '@ironsource/fusion-ui/components/icon/common/entities'; +import {MenuDropComponent, MenuDropItem} from '@ironsource/fusion-ui/components/menu-drop/v4'; +import {TooltipPosition} from '@ironsource/fusion-ui/components/tooltip/common/base'; +import {CommonModule} from '@angular/common'; +import {NotAvailablePipe} from '@ironsource/fusion-ui/pipes/not-available'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; +import {CheckboxComponent} from '@ironsource/fusion-ui/components/checkbox/v4'; +import {DynamicComponentsModule} from '@ironsource/fusion-ui/components/dynamic-components/v1'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {IconButtonComponent} from '@ironsource/fusion-ui/components/button/v4'; +import {ClickOutsideModule} from '@ironsource/fusion-ui/directives/click-outside'; +import {TeleportingModule} from '@ironsource/fusion-ui/directives/teleporting'; +import {RepositionDirective} from '@ironsource/fusion-ui/directives/reposition'; +import {ToggleComponent} from '@ironsource/fusion-ui/components/toggle/v4'; + +type CellDataType = Type | FormControl | string | boolean | undefined | null; + +@Component({ + // eslint-disable-next-line + selector: '[fusionTableCell]', + standalone: true, + imports: [ + CommonModule, + ReactiveFormsModule, + NotAvailablePipe, + TooltipDirective, + CheckboxComponent, + InputInlineComponent, + DynamicComponentsModule, + IconModule, + IconButtonComponent, + MenuDropComponent, + ClickOutsideModule, + TeleportingModule, + RepositionDirective, + ToggleComponent + ], + templateUrl: './table-cell.component.html', + styleUrls: ['./table-cell.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableCellComponent implements OnInit, OnChanges { + @Input() set data(value: CellDataType) { + this._data = value; + } + + @Input() column: TableColumn; + @Input() row: {[key: string]: any}; + @Input() rowIndex: string | number; + @Input() rowSpanIndex: number; + @Input() options: TableOptions = null; + @Input() position: CellPosition; + + @Input() infoIconTooltip: string; + @Input() isRemove: boolean; + @Input() floatingActionsDisabled: boolean; + @Input() isRowSelected: boolean; + @Input() isLastColumn: boolean; + @Input() customClass: {[columnKey: string]: string} = {}; + @Input() isReadOnly: boolean; + + @Output() selectedChange = new EventEmitter(); + @Output() dataChange = new EventEmitter(); + @Output() remove = new EventEmitter(); + + @ViewChild('inlineInput') inlineInputEdit: InputInlineComponent; + + @HostBinding('class.is-inline-removable') isInlineRemovable = false; + + @HostBinding('class.sticky-left') get sticky(): boolean { + return this.column.sticky; + } + + @HostBinding('style.left') get stickyLeft(): string { + return this.column.stickyLeftMargin; + } + + isInRequest$: BehaviorSubject = new BehaviorSubject(false); + toggleInRequest$: BehaviorSubject = new BehaviorSubject(false); + innerElementWidth = ''; + isInEditMode = false; + initInputData: string | boolean | undefined | null | AdvancedInputInline; + inputError$ = new BehaviorSubject(''); + notAvailableText: string; + isNull: (object: any) => boolean = isNull; + isNullOrUndefined: (object: any) => boolean = isNullOrUndefined; + customCellData: DynamicComponentConfiguration; + floatingMenuPosition = TooltipPosition.BottomRight; + + shownActionsMenu$: BehaviorSubject = new BehaviorSubject(false); + + get actionsMenuButtonId(): string { + return this.options.tableId + '_' + this.rowIndex; + } + + get data(): CellDataType { + let data = this._data; + if (Array.isArray(data)) { + data = data[this.rowSpanIndex ?? 0]; + } + if (!isNull(data) && this.tableService.isTypeComponent(this.column) && typeof data === 'object') { + data['cellPosition'] = this.position; + } + return data; + } + + get cellStringData(): string { + if (typeof this.data === 'string' || typeof this.data === 'number') { + return this.data; + } else if (this.isRowTotal && !isNullOrUndefined(this.data)) { + this.logService.error( + new Error( + `Expected data type String for cell in total row with type "totalRowTypeAsString" for column key:${this.column.key}` + ) + ); + return ' '; + } else if (this.isRowTotal && isNullOrUndefined(this.data)) { + return ' '; // for total row cell as string if data not arrive + } + return isNull(this.data) ? null : undefined; + } + + get isRowTotal(): boolean { + return !isNullOrUndefined(this.options.hasTotalsRow) && this.options.hasTotalsRow && this.position.x === 0; + } + + get isSmallActionButton(): boolean { + return !!this.options && !!this.options.rowHeight && this.options.rowHeight === TableRowHeight.Small; + } + + get nativeElement(): Node { + return this.column && typeof this.column.renderNativeElement === 'function' + ? this.column.renderNativeElement(this.data, this.position, this.row) + : null; + } + + get cellRemoveActionIcon(): IconData { + return this.options?.remove && this.options.remove?.icon ? this.options.remove.icon : DEFAULT_REMOVE_ICON_V3; + } + + get multipleActions(): TableMultipleActions { + const actionsMenu = this.options?.rowActionsMenu; + if (this.options?.rowActionsMenu && Array.isArray(this.options?.rowActionsMenu.actions)) { + actionsMenu.actions = this.options?.rowActionsMenu?.actions.map(this.setDisableStateForFloatingAction.bind(this)); + } + return actionsMenu; + } + + private _data: CellDataType; + private inlineInputViewOnlyText = ''; + private onActionMenuClose$ = new Subject(); + + constructor( + public tableService: TableService, + @Optional() + @Inject(TABLE_OPTIONS_TOKEN) + private tableModuleOptions: TableModuleOptions, + private logService: LogService, + public elementRef: ElementRef + ) {} + + ngOnInit() { + const {paddingLeft, paddingRight} = this.getSellLefRightPadding(); + this.innerElementWidth = this.column.width ? `calc(${this.column.width} - ${paddingLeft} - ${paddingRight})` : null; + this.notAvailableText = this.options ? this.options.notAvailableText : null; + } + + ngOnChanges(changes: SimpleChanges) { + this.isInlineRemovable = this.isRemove; + if (this.tableService.isTypeInputEdit(this.column) && changes?.data?.currentValue && !changes.data.firstChange) { + this._setInputData(); + } + + if (this.tableService.isTypeComponent(this.column) && (changes?.data?.currentValue || changes?.column?.currentValue)) { + this.renderCustomElement(); + } + + if (changes?.data?.firstChange && changes?.data?.currentValue?.value?.viewOnlyText) { + this.inlineInputViewOnlyText = changes.data.currentValue.value.viewOnlyText; + } + } + + _setInputData() { + this.initInputData = !isNullOrUndefined(this.column.dataParser) + ? this.column.dataParser((this.data as FormControl).value) + : (this.data as FormControl).value; + } + + getRemoveIconTooltipText(): string { + return this.options && this.options.remove && this.options.remove.tooltip && this.options.remove.tooltip.text + ? this.options.remove.tooltip.text + : DEFAULT_REMOVE_TOOLTIP_TEXT; + } + + isBoolean(variable): boolean { + return isBoolean(variable); + } + + isRowChecked(): boolean { + return this.options.isGroupedTable ? (this.data as boolean) : this.isRowSelected; + } + + isAsDate(date: any): boolean { + return !isNaN(Date.parse(date.toString())); + } + + onToggleChanged(newValue: boolean) { + this.toggleInRequest$.next(true); + this.dataChange.emit({ + newValue, + onCellRequestDone: (isSuccess: boolean, error: {message: string; status: number}, stayInEditMode = false) => { + if (isSuccess) { + this.data = newValue; + } else { + this.data = !newValue; + } + this.toggleInRequest$.next(false); + } + }); + } + + onEndEdit(valuesOptions) { + const formControl = this.data as FormControl; + if (formControl.valid) { + this.isInEditMode = false; + const newValue = !isNullOrUndefined(this.column.dataParser) ? this.column.dataParser(formControl.value) : formControl.value; + const prevValue = valuesOptions.currentValue; + const inlineInputComponent = this.inlineInputEdit; + if (newValue !== this.initInputData) { + this.initInputData = newValue; + // set waiter for cell + this.isInRequest$.next(true); + this.dataChange.emit({ + newValue, + prevValue, + onCellRequestDone: (isSuccess: boolean, error: {message: string; status: number}, stayInEditMode = false) => { + if (!isSuccess) { + if (stayInEditMode) { + inlineInputComponent.setEditMode$.next(newValue); + } else { + this.inputError$.next(error?.message); + } + this.initInputData = prevValue; + } else if (this.initInputData === '') { + this.inputError$.next(''); + this.initInputData = formControl.value; + } else { + this.inputError$.next(''); + const newInputValue = this.inlineInputViewOnlyText + ? { + value: newValue, + viewOnlyText: this.inlineInputViewOnlyText + } + : newValue; + formControl.setValue(newInputValue, { + emitEvent: false + }); + } + this.isInRequest$.next(false); + } + }); + } + } else { + const allErrors = formControl.errors || {}; + Object.keys(allErrors).forEach(errorKey => { + this.inputError$.next( + this._getMessage( + errorKey, + !isNullOrUndefined(this.column.customErrorMapping) ? this.column.customErrorMapping[errorKey] ?? {} : {}, + allErrors[errorKey] + ) + ); + }); + } + } + + onCancel() { + this.inputError$.next(''); + this.isInEditMode = false; + } + + onRowRemoveClicked($event: MouseEvent) { + if ($event) { + $event.preventDefault(); + $event.stopPropagation(); + } + this.remove.emit(); + } + + getDateFormat(dateFormat: string): string { + return dateFormat || 'd MMM yyyy'; + } + + getDateUTCFormat(ignoreTimeZone: boolean): string { + return ignoreTimeZone ? null : 'UTC'; + } + + renderCustomElement() { + if (this.column) { + this.customCellData = { + component: { + type: this.column.component, + data: this.data + }, + element: this.nativeElement + }; + } + } + + menuItemClicked(action: MenuDropItem) { + this.closeActionsMenu(); + this.tableService.rowActionClicked.emit({action: action, rowIndex: this.rowIndex, row: this.row}); + } + + onActionButtonClicked() { + this.shownActionsMenu$.next(true); + this.tableService.tableScrolled.pipe(takeUntil(this.onActionMenuClose$)).subscribe($event => { + this.closeActionsMenu(); + }); + } + + onActionMenuClickOutSide(target) { + if (!target.closest('#' + this.actionsMenuButtonId)) { + this.closeActionsMenu(); + } + } + + private closeActionsMenu() { + this.shownActionsMenu$.next(false); + this.onActionMenuClose$.next(); + this.onActionMenuClose$.complete(); + } + + private _getMessage(errorKey, {errorMessageKey = '', textMapping = []}, errorDefaults?: any): string { + const tableModuleOptions = !isNullOrUndefined(this.tableModuleOptions) ? this.tableModuleOptions : {errorMessages: ERROR_MESSAGES}; + if (!tableModuleOptions.errorMessages) { + tableModuleOptions.errorMessages = ERROR_MESSAGES; + } + let errorMessage = tableModuleOptions.errorMessages[errorMessageKey] || tableModuleOptions.errorMessages[errorKey]; + if (errorMessage) { + errorMessage = errorMessage.replace('{NAME}', this.column.title); + errorMessage = errorMessage.replace('{INNER-NAME}', this.column.title); + if (textMapping && textMapping.length > 0) { + textMapping.forEach(mappObj => { + errorMessage = errorMessage.replace(`{${mappObj.key}}`, mappObj.value); + }); + } + } + return errorMessage; + } + + private getSellLefRightPadding(): {paddingLeft: string; paddingRight: string} { + const computedStyle = getComputedStyle(this.elementRef.nativeElement); + const paddingLeft = computedStyle.paddingLeft; + const paddingRight = computedStyle.paddingRight; + return {paddingLeft, paddingRight}; + } + + setDisableStateForFloatingAction(menuItem: MenuDropItem): MenuDropItem { + return this.options?.isFloatingActionDisabled && typeof this.options?.isFloatingActionDisabled === 'function' + ? {...menuItem, disabled: this.options.isFloatingActionDisabled(this.row, menuItem)} + : menuItem; + } +} diff --git a/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.html b/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.html new file mode 100644 index 000000000..fb51ef40d --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.html @@ -0,0 +1,12 @@ + + + @if (!!customContent?.component?.type) { + + } @else { + + } + + diff --git a/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.scss b/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.scss new file mode 100644 index 000000000..7e3b0b35a --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.scss @@ -0,0 +1,5 @@ +:host { + tr td { + padding: var(--table-empty-state-padding, 48px); + } +} diff --git a/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.spec.ts b/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.spec.ts new file mode 100644 index 000000000..7fd881e15 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.spec.ts @@ -0,0 +1,43 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, DebugElement} from '@angular/core'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {EmptyStateComponent} from '@ironsource/fusion-ui/components/empty-state'; +import {DynamicComponentsModule} from '@ironsource/fusion-ui/components/dynamic-components/v1'; +import {TableEmptyComponent} from './table-empty.component'; + +// do dummy component - holder +@Component({ + template: ` + + +
    + ` +}) +class TestTableRowEmptyComponent { + public colsSpan = 1; + public noDataMessage = ''; + public noDataSubMessage = ''; +} + +describe('TableEmptyComponent', () => { + let component: TableEmptyComponent; + let fixture: ComponentFixture; + let debugEl: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TableEmptyComponent, EmptyStateComponent, IconModule, DynamicComponentsModule] + }); + + fixture = TestBed.createComponent(TableEmptyComponent); + + component = fixture.componentInstance; + + debugEl = fixture.debugElement; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.ts b/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.ts new file mode 100644 index 000000000..faf690a81 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-empty/table-empty.component.ts @@ -0,0 +1,32 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {isNullOrUndefined} from '@ironsource/fusion-ui/utils'; +import {EmptyStateComponent, EmptyStateType} from '@ironsource/fusion-ui/components/empty-state/v4'; +import {DynamicComponentConfiguration} from '@ironsource/fusion-ui/components/dynamic-components/common/entities'; +import {DynamicComponentsModule} from '@ironsource/fusion-ui/components/dynamic-components/v1'; + +@Component({ + // eslint-disable-next-line + selector: '[fusionTableEmpty]', + standalone: true, + imports: [CommonModule, EmptyStateComponent, DynamicComponentsModule], + templateUrl: './table-empty.component.html', + styleUrls: ['./table-empty.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableEmptyComponent { + @Input() fusionTableEmpty: number; + @Input() customContent: DynamicComponentConfiguration; + @Input() header: string; + @Input() subHeader: string; + @Input() set type(value: EmptyStateType) { + if (!isNullOrUndefined(value)) { + this._type = value; + } + } + get type(): EmptyStateType { + return this._type; + } + + private _type: EmptyStateType = 'noData'; +} diff --git a/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.html b/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.html new file mode 100644 index 000000000..f8150274f --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.html @@ -0,0 +1,17 @@ +@if(fusionTableLoadingExpanding){ + @for(c of colsToShow; track c; let first = $first){ + + @if (!first){ + + } + + } +} @else { + @for (r of rowsToShow; track r){ + + @for(c of colsToShow; track c){ + + } + + } +} diff --git a/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.scss b/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.scss new file mode 100644 index 000000000..31f747792 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.scss @@ -0,0 +1,13 @@ +:host { + .fu-table-cell-loading { + height: var(--table-row-height, 48px); + padding: var(--table-row-cell-padding, 0 16px); + border-bottom: var(--table-border); + + &.fu-expanding-loader:first-of-type { + div { + display: none; + } + } + } +} diff --git a/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.spec.ts b/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.spec.ts new file mode 100644 index 000000000..911d727d5 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.spec.ts @@ -0,0 +1,40 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement} from '@angular/core'; +import {TableLoadingComponent} from './table-loading.component'; + +// do dummy component - holder +@Component({ + standalone: true, + imports: [TableLoadingComponent], + template: ` + + +
    + ` +}) +class TestTableLoadingComponent {} + +describe('TestTableEmptyComponent', () => { + let component: TestTableLoadingComponent; + let fixture: ComponentFixture; + let debugEl: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [TestTableLoadingComponent, TableLoadingComponent] + }); + + fixture = TestBed.createComponent(TestTableLoadingComponent); + + component = fixture.componentInstance; + + debugEl = fixture.debugElement; + + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.ts b/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.ts new file mode 100644 index 000000000..52c64fd3d --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-loading/table-loading.component.ts @@ -0,0 +1,24 @@ +import {Component, Input} from '@angular/core'; +import {SkeletonComponent} from '@ironsource/fusion-ui/components/skeleton'; + +@Component({ + // eslint-disable-next-line + selector: '[fusionTableLoading]', + imports: [SkeletonComponent], + templateUrl: './table-loading.component.html', + styleUrls: ['./table-loading.component.scss'], + standalone: true +}) +export class TableLoadingComponent { + @Input() fusionTableLoading: number; + @Input() fusionTableLoadingExpanding = false; + @Input() fusionTableLoadingRows = 3; + + get rowsToShow(): number[] { + return [...Array(this.fusionTableLoadingRows).keys()]; + } + + get colsToShow(): number[] { + return [...Array(this.fusionTableLoading ?? 1).keys()]; + } +} diff --git a/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.html b/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.html new file mode 100644 index 000000000..82b77891d --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.html @@ -0,0 +1,58 @@ + +@for (expandCell of (expandCellCount | async); track expandCell; let isLast = $last, idx = $index){ + @if(cellShown | generic:'cell-expand'){ + + @if(isLast && showExpandIcon()){ +
    + } + + } +} + +@for (column of columns; track column; let columnIndex = $index, isLast = $last){ + @if (cellShown | generic:column.key){ + + } +} + diff --git a/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.scss b/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.scss new file mode 100644 index 000000000..b808b2f7c --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.scss @@ -0,0 +1,36 @@ +@import '../../../../../src/style/scss/v4/vars/vars'; +@import '../../../../../src/style/scss/v4/mixins/mixins'; + +:host { + // region zebra + &:nth-child(odd) { + background-color: var(--table-odd-row-background-color); + } + + &:nth-child(even) { + background-color: var(--table-even-row-background-color); + } + + // endregion + + // region Expand cell in row + td.expand-cell { + width: var(--table-expand-cell-width); + border-bottom: var(--table-border); + vertical-align: middle; + + & > div { + padding-right: 4px; + display: flex; + align-items: center; + justify-content: flex-end; + width: var(--table-expand-cell-width); + } + } + // endregion + + &:last-of-type td, + &:last-of-type td.expand-cell{ + border-bottom: none; + } +} diff --git a/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.spec.ts b/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.spec.ts new file mode 100644 index 000000000..94deab9a1 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.spec.ts @@ -0,0 +1,50 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {TableRowComponent} from './table-row.component'; +import {TableCellComponent} from '../table-cell/table-cell.component'; +import {TooltipModule} from '@ironsource/fusion-ui/components/tooltip/v1'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {TableService} from '@ironsource/fusion-ui/components/table/common/services'; +import {LogService} from '@ironsource/fusion-ui/services/log'; +import {NotAvailablePipe} from '@ironsource/fusion-ui/pipes/not-available'; +import {GenericPipe} from '@ironsource/fusion-ui/pipes/generic'; + +// do dummy component - holder +@Component({ + standalone: true, + template: ` + + +
    + ` +}) +class TestTableRowComponent { + public rowIndex = 1; + public row = []; + public columns = []; + public isRowSelected = false; + public isRowDisabled = false; + public options = {hasTotalsRow: false}; +} + +describe('TableRowComponent', () => { + let component: TestTableRowComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [TestTableRowComponent, IconModule, TooltipModule, NotAvailablePipe, GenericPipe, TableRowComponent, TableCellComponent], + providers: [TableService, LogService] + }); + + fixture = TestBed.createComponent(TestTableRowComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.ts b/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.ts new file mode 100644 index 000000000..5451b4180 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/components/table-row/table-row.component.ts @@ -0,0 +1,229 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + HostBinding, + Injector, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; +import {TableColumn, TableOptions, TableRowExpandEmitter, TableRowMetaData} from '@ironsource/fusion-ui/components/table/common/entities'; +import {TableService} from '@ironsource/fusion-ui/components/table/common/services'; +import {isNullOrUndefined} from '@ironsource/fusion-ui/utils'; +import {Observable, of} from 'rxjs'; +import {CommonModule} from '@angular/common'; +import {GenericPipe} from '@ironsource/fusion-ui/pipes/generic'; +import {TableRow, ColumnData} from '@ironsource/fusion-ui/components/table/common/entities'; +import {IconData, IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {TableTestIdModifiers} from '@ironsource/fusion-ui/entities'; +import {TestIdsService} from '@ironsource/fusion-ui/services/test-ids'; +import {TableCellComponent} from '../table-cell/table-cell.component'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; +import {IconButtonComponent} from '@ironsource/fusion-ui/components/button/v4'; + +@Component({ + // eslint-disable-next-line + selector: '[fusionTableRow]', + standalone: true, + imports: [CommonModule, GenericPipe, IconModule, TooltipDirective, TableCellComponent, IconButtonComponent], + templateUrl: './table-row.component.html', + styleUrls: ['./table-row.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableRowComponent implements OnInit, OnChanges { + @Input() rowIndex: string | number; + @Input() rowSpanIndex: number; + @Input() row: TableRow; + @Input() options: TableOptions; + @Input() columns: TableColumn[]; + @Input() isRemovableOnHover: boolean; + @Input() isRowSelected: boolean; + @Input() isExpanded: boolean; + @Input() isInnerRow: boolean; + @Input() hasAfterSticky: boolean; + + /** @internal */ + @Input() testId: string; + + @Output() rowRemoved = new EventEmitter(); + @Output() selectedChange = new EventEmitter(); + @Output() expandRow = new EventEmitter(); + + @HostBinding('attr.data-row-idx') dataRowIndex: string | number; + + @HostBinding('class.is-row-expanded') get isRowExpanded(): boolean { + return this.isExpanded; + } + + @HostBinding('class.is-inner-row-expandable') get isInnerRowExpandable(): boolean { + return ( + this.options && + this.options.rowsExpandableOptions && + this.tableService.expandLevels && + this.tableService.getExpandLevelByRowIndex(this.rowIndex) <= this.tableService.expandLevels + ); + } + + @HostBinding('class.is-with-totals') get isRowTotal(): boolean { + return !isNullOrUndefined(this.options.hasTotalsRow) && this.options.hasTotalsRow && this.rowIndex === 0; + } + + @HostBinding('class.is-row-readonly') get isRowReadOnly(): boolean { + return this.tableService.isRowReadOnly(this.row); + } + + expandArrowIconName: IconData; + collapseArrowIconName: IconData; + columnsData: ColumnData[] = []; + + cellShown = this.showCell.bind(this); + + /** @internal */ + tableTestIdModifiers: typeof TableTestIdModifiers = TableTestIdModifiers; + /** @internal */ + testIdsService: TestIdsService = this.injector.get(TestIdsService); + + get expandCellCount(): Observable { + if (!!this.options && !!this.options.rowsExpandableOptions && !!this.tableService.expandLevels) { + const expandLevelsByIndex = this.tableService.getExpandLevelByRowIndex(this.rowIndex); + const expandLevels = + expandLevelsByIndex <= this.tableService.expandLevels ? expandLevelsByIndex : this.tableService.expandLevels; + + return of([...Array(expandLevels).keys()]); + } + return of([]); + } + + hasTooltip(key: string, row: TableRow): boolean { + return !isNullOrUndefined(this.getCellTooltip(key, row)); + } + + getCellTooltip(key: string, row): string { + // eslint-disable-next-line + const rowMetaData: TableRowMetaData = row['rowMetaData']; + return !!rowMetaData && !!rowMetaData.cellToolTip ? rowMetaData.cellToolTip[key] : null; + } + + infoIconOnHooverToolTip(options: TableOptions, row: TableRow): string { + return options?.infoIconForRowOnHover ? options?.infoIconForRowOnHover(row) : ''; + } + + rowRemoveIconOptions(options: TableOptions, row: TableRow): {hideRemoveIcon: boolean} { + return { + hideRemoveIcon: options?.remove && options?.isRemoveIconHiddenForRow && options?.isRemoveIconHiddenForRow(row) + }; + } + + constructor(public tableService: TableService, private cdRef: ChangeDetectorRef, private injector: Injector) {} + + ngOnInit(): void { + this.dataRowIndex = this.rowIndex; + this.expandArrowIconName = 'ph/caret-right'; + this.collapseArrowIconName = 'ph/caret-down'; + if (this.isRowTotal) { + Object.assign(this.row, {isRowTotal: true}); + } + } + + ngOnChanges({options, columns, row}: SimpleChanges) { + const activeOptions = options?.currentValue || this.options; + const activeColumns = columns?.currentValue || this.columns; + const activeRow = row?.currentValue || this.row; + if (options?.currentValue || columns?.currentValue || row?.currentValue) { + this.columnsData = this.getColumnsData(activeColumns, activeOptions, activeRow); + } + } + + onDataChange(options: any, rowKey): void { + this.tableService.toggleRowInRequest(this.row, true); + this.tableService.rowModelChange.emit({ + rowIndex: this.rowIndex, + rowSpanIndex: this.rowSpanIndex ?? 0, + rowModel: this.row, + keyChanged: rowKey, + newValue: options.newValue, + prevValue: options.prevValue, + onRequestDone: (state: boolean, error: {message: string; status: number}, stayInEditOnCancel = false) => { + this.tableService.toggleRowInRequest(this.row, false); + if (options.onCellRequestDone) { + options.onCellRequestDone(state, error, stayInEditOnCancel); + } + this.cdRef.markForCheck(); + } + }); + } + + trackByFn(index, item) { + if (!item) { + return null; + } + return item.key ? item.key : index; + } + + showExpandIcon(): boolean { + const hasKeyToIgnore = this.options && this.options.rowsExpandableOptions && this.options.rowsExpandableOptions.keyToIgnore; + if (!hasKeyToIgnore) { + return this.isInnerRow ? this.isInnerRowExpandable : !this.isRowTotal; + } + return !this.isRowTotal && !this.row[this.options.rowsExpandableOptions.keyToIgnore] && this.isInnerRowExpandable; + } + + getCellColspan(isFirstDataCell: boolean, cellColspan?: number, expandLevel?: number): number | undefined { + if (isFirstDataCell && expandLevel) { + if (this.isInnerRow) { + const colspan = !isNullOrUndefined(cellColspan) ? cellColspan : 0; + return colspan + (expandLevel + 1 - this.tableService.getExpandLevelByRowIndex(this.rowIndex)); + } + return expandLevel + 1 - this.tableService.getExpandLevelByRowIndex(this.rowIndex); + } + return cellColspan; + } + + getAttrRowspan(columnKey: string): number { + let rowSpan = 0; + const maxRowspan = this.tableService.getMaxRowspanInColumn(this.row); + if (columnKey === 'cell-expand') { + rowSpan = maxRowspan; + } else { + const multiRowsKeys = this.tableService.getRowspanColumnsData(this.row); + if (!isNullOrUndefined(multiRowsKeys) && isNullOrUndefined(this.rowSpanIndex)) { + rowSpan = maxRowspan - multiRowsKeys[columnKey]; + } + } + return rowSpan > 0 ? rowSpan : null; + } + + /** + * Show regular cell "isNullOrUndefined(this.rowSpanIndex)" + * or if cell has rowspan index "!isNullOrUndefined(this.rowSpanIndex)" and key for multirow + * @internal + */ + showCell(columnKey: string): boolean { + if (columnKey.startsWith('cell-expand')) { + return isNullOrUndefined(this.rowSpanIndex); + } + const multiRowsKeys = this.tableService.getRowspanColumnsData(this.row); + return isNullOrUndefined(this.rowSpanIndex) || (!isNullOrUndefined(this.rowSpanIndex) && multiRowsKeys[columnKey] !== 0); + } + + private getColumnsData(columns: TableColumn[], options: TableOptions, row: TableRow): ColumnData[] { + return columns.map((column, index) => { + const isLast = index === columns.length - 1; + const isFirst = index === 0; + return { + classes: this.tableService.getColumnClasses(column), + tooltip: this.getCellTooltip(column.key, row), + hasTooltip: this.hasTooltip(column.key, row), + isRemove: this.tableService.isRemove(isLast, options, this.rowRemoveIconOptions(options, row)), + infoIconOnHoverTooltip: isLast ? this.infoIconOnHooverToolTip(options, row) : '', + styles: this.tableService.getColumnStyle(column), + colspan: this.getCellColspan(isFirst, column.colspan, this.tableService.expandLevels), + width: this.tableService.setWidth(isLast, column.width) + }; + }); + } +} diff --git a/projects/fusion-ui/components/table/v4/index.ts b/projects/fusion-ui/components/table/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/table/v4/ng-package.json b/projects/fusion-ui/components/table/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/table/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/table/v4/public-api.ts b/projects/fusion-ui/components/table/v4/public-api.ts new file mode 100644 index 000000000..4327b2802 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/public-api.ts @@ -0,0 +1,3 @@ +export {TableV4Component as TableComponent} from './table-v4.component'; +export * from '@ironsource/fusion-ui/components/table/common/services'; +export * from '@ironsource/fusion-ui/components/table/common/entities'; diff --git a/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.html b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.html new file mode 100644 index 000000000..37e9d2474 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.html @@ -0,0 +1,13 @@ +
    + {{title}} +
    +
    + {{subtitle}} +
    +
    +
      + @for (item of benefits; track item){ +
    • {{item}}
    • + } +
    +
    \ No newline at end of file diff --git a/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.scss b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.scss new file mode 100644 index 000000000..7f8ecf90c --- /dev/null +++ b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.scss @@ -0,0 +1,19 @@ +@import '../../../../../src/style/scss/v4/vars/vars'; + +:host { + display: block; + width: 100%; + padding: 24px 32px; + color: var(--text-primary, #{$color-v4-text-primary}); + .fu-title{ + @extend %font-v4-heading-3; + margin-bottom: 8px; + } + .fu-subtitle{ + @extend %font-v4-body-1; + } + .fu-benefits{ + margin-top: 24px; + @extend %font-v4-body-1; + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.spec.ts b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.spec.ts new file mode 100644 index 000000000..450eb91de --- /dev/null +++ b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RowExpandInnerComponent } from './row-expand-inner.component'; + +describe('RowExpandInnerComponent', () => { + let component: RowExpandInnerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RowExpandInnerComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RowExpandInnerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.stories_._ts b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.stories_._ts new file mode 100644 index 000000000..6c388e5dd --- /dev/null +++ b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.stories_._ts @@ -0,0 +1,31 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {RowExpandInnerComponent} from './row-expand-inner.component'; + +export default { + title: 'V4/Components/DataDisplay/DataGrid (Table)/innerComponents', + component: RowExpandInnerComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + title: 'Soybean Oil', + subtitle: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dolor magna eget est lorem ipsum dolor sit amet.', + benefits: ['Odio pellentesque diam volutpat commodo', 'Egestas sed tempus urna et pharetra pharetra', 'Viverra tellus in hac habitasse platea dictumst vestibulum rhoncus'] + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; diff --git a/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.ts b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.ts new file mode 100644 index 000000000..dece6ac21 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/stories/row-expand-inner/row-expand-inner.component.ts @@ -0,0 +1,15 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; + +@Component({ + selector: 'fusion-row-expand-inner', + standalone: true, + imports: [], + templateUrl: './row-expand-inner.component.html', + styleUrl: './row-expand-inner.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RowExpandInnerComponent { + @Input() title: string; + @Input() subtitle: string; + @Input() benefits: string[]; +} diff --git a/projects/fusion-ui/components/table/v4/stories/table-v4.component.stories.ts b/projects/fusion-ui/components/table/v4/stories/table-v4.component.stories.ts new file mode 100644 index 000000000..5bff5b92e --- /dev/null +++ b/projects/fusion-ui/components/table/v4/stories/table-v4.component.stories.ts @@ -0,0 +1,387 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {EventEmitter} from '@angular/core'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../../stories/environments/environment'; +import {TableV4Component} from '../table-v4.component'; +import { + EXPAND_COLUMNS_CONFIG, + EXPAND_ROWS_DEFAULT_DATA, + ROWS_DEFAULT_DATA, + ROWS_DEFAULT_DATA_WITH_ID, + ROWS_EDITABLE_DATA, + ROWS_HORIZONTAL_DATA_WITH, + ROWS_NUMBERS_DATA, + ROWS_SELECTABLE_DATA, + TABLE_DEFAULT_COLUMNS_CONFIG, + TABLE_EDITABLE_COLUMNS_CONFIG, + TABLE_HORIZONTAL_COLUMNS_CONFIG, + TABLE_NUMBERS_COLUMNS_CONFIG, + TABLE_NUMBERS_SORTABLE_COLUMNS_CONFIG, + TABLE_SELECTABLE_COLUMNS_CONFIG, + TABLE_TOOLTIP_COLUMNS_CONFIG, + MOCK_ROW_ACTIONS, + TABLE_TOGGLEABLE_COLUMNS_CONFIG, + ROWS_TOGGLEABLE_DATA, + ROWS_SELECTABLE_STICKY_DATA, + TABLE_SELECTABLE_STICKY_COLUMNS_CONFIG, + TABLE_STICKY_COLUMNS_CONFIG, + TABLE_DROPDOWN_COLUMNS_CONFIG, + ROWS_DROPDOWN_DATA +} from './table.mock-data'; +import {TableV4StoryHolderComponent} from './table.story-holder.component/table.story-holder.component.component'; +import {action} from '@storybook/addon-actions'; +import {TableColumn, TableOptions} from '@ironsource/fusion-ui/components/table'; + +const TEMPLATE_TABLE_HOLDER = ``; + +const actionsData = { + selectionChanged: action('selectionChanged'), + rowModelChange: action('rowModelChange'), + expandRow: action('expandRow'), + rowActionClicked: action('rowActionClicked') +}; + +export default { + title: 'V4/Components/DataDisplay/DataGrid (Table)', + component: TableV4Component, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), TableV4StoryHolderComponent] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + columns: TABLE_DEFAULT_COLUMNS_CONFIG, + rows: ROWS_DEFAULT_DATA + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const Numbers: Story = {}; +Numbers.args = { + columns: TABLE_NUMBERS_COLUMNS_CONFIG, + rows: ROWS_NUMBERS_DATA +}; + +export const HeaderAndFooter: Story = { + render: args => ({ + props: { + ...args, + options: { + tableLabel: {text: 'Users', tooltip: 'Users table'}, + searchOptions: { + placeholder: 'Search', + onSearch: new EventEmitter() + } + }, + hasCustomFooter: true + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; + +export const ActionsHeader: Story = { + render: args => ({ + props: { + ...args, + hasCustomHeader: true + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; + +export const InlineEditing: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_EDITABLE_COLUMNS_CONFIG, + rows: ROWS_EDITABLE_DATA, + rowModelChange: actionsData.rowModelChange + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; + +export const InlineDropdown: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_DROPDOWN_COLUMNS_CONFIG, + rows: ROWS_DROPDOWN_DATA, + rowModelChange: actionsData.rowModelChange + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; + +export const ToggleInRows: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_TOGGLEABLE_COLUMNS_CONFIG, + rows: ROWS_TOGGLEABLE_DATA, + selectionChanged: actionsData.selectionChanged + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; + +export const SelectableRows: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_SELECTABLE_COLUMNS_CONFIG, + rows: ROWS_SELECTABLE_DATA, + selectionChanged: actionsData.selectionChanged + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; + +export const SelectableStickyRows: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_SELECTABLE_STICKY_COLUMNS_CONFIG as TableColumn[], + rows: ROWS_SELECTABLE_STICKY_DATA, + selectionChanged: actionsData.selectionChanged, + options: { + stickyHeader: true + } + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; +SelectableStickyRows.decorators = [componentWrapperDecorator(story => `
    ${story}
    `)]; + +export const StickyColumns: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_STICKY_COLUMNS_CONFIG as TableColumn[], + rows: ROWS_HORIZONTAL_DATA_WITH, + options: { + stickyHeader: true + } + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; +StickyColumns.decorators = [componentWrapperDecorator(story => `
    ${story}
    `)]; + +export const MenuActions: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_DEFAULT_COLUMNS_CONFIG, + rows: ROWS_DEFAULT_DATA_WITH_ID, + rowModelChange: actionsData.rowModelChange, + options: { + stickyHeader: true, + rowActionsMenu: { + actions: MOCK_ROW_ACTIONS + } as TableOptions + } + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; +MenuActions.decorators = [componentWrapperDecorator(story => `
    ${story}
    `)]; + +export const StickyActions: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_HORIZONTAL_COLUMNS_CONFIG, + rows: ROWS_HORIZONTAL_DATA_WITH, + rowModelChange: actionsData.rowModelChange, + options: { + stickyHeader: true, + rowActionsMenu: { + stickyActionButton: true, + actions: MOCK_ROW_ACTIONS + } + } + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; +StickyActions.decorators = [componentWrapperDecorator(story => `
    ${story}
    `)]; + +export const DeleteRow: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_DEFAULT_COLUMNS_CONFIG, + rows: ROWS_DEFAULT_DATA_WITH_ID, + rowModelChange: actionsData.rowModelChange, + options: { + stickyHeader: true, + remove: { + active: true, + icon: 'ph/trash', + tooltip: {text: 'Remove this row'} + } + } + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; +DeleteRow.decorators = [componentWrapperDecorator(story => `
    ${story}
    `)]; + +export const SortableColumns: Story = {}; +SortableColumns.args = { + columns: TABLE_NUMBERS_SORTABLE_COLUMNS_CONFIG, + rows: ROWS_NUMBERS_DATA +}; + +export const ColumnTooltips: Story = {}; +ColumnTooltips.args = { + columns: TABLE_TOOLTIP_COLUMNS_CONFIG +}; + +export const InfiniteScrolling: Story = { + render: args => ({ + props: { + ...args, + rows: ROWS_DEFAULT_DATA_WITH_ID, + options: { + stickyHeader: true, + pagination: { + enable: true + } + } + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; +InfiniteScrolling.decorators = [componentWrapperDecorator(story => `
    ${story}
    `)]; + +export const StickyHeader: Story = { + render: args => ({ + props: { + ...args, + rows: ROWS_DEFAULT_DATA_WITH_ID, + options: { + stickyHeader: true, + tableLabel: {text: 'Users', tooltip: 'Users table'}, + searchOptions: { + placeholder: 'Search', + onSearch: new EventEmitter() + } + }, + hasCustomFooter: true + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; +StickyHeader.decorators = [componentWrapperDecorator(story => `
    ${story}
    `)]; + +export const VerticalAndHorizontalScroll: Story = { + render: args => ({ + props: { + ...args, + columns: TABLE_HORIZONTAL_COLUMNS_CONFIG, + rows: ROWS_HORIZONTAL_DATA_WITH, + options: { + stickyHeader: true, + tableLabel: {text: 'Users', tooltip: 'Users table'}, + searchOptions: { + placeholder: 'Search', + onSearch: new EventEmitter() + } + }, + hasCustomFooter: true + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; +VerticalAndHorizontalScroll.decorators = [componentWrapperDecorator(story => `
    ${story}
    `)]; + +export const ExpandRows: Story = { + render: args => ({ + props: { + ...args, + columns: EXPAND_COLUMNS_CONFIG, + rows: EXPAND_ROWS_DEFAULT_DATA.slice(0, 5), + options: { + stickyHeader: true, + hasRowSpan: true, + rowsExpandableOptions: { + key: 'children', + columns: EXPAND_COLUMNS_CONFIG + } + }, + expandRow: actionsData.expandRow + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; + +export const ExpandWithDynamicObject: Story = { + render: args => ({ + props: { + ...args, + columns: EXPAND_COLUMNS_CONFIG, + rows: EXPAND_ROWS_DEFAULT_DATA.slice(0, 5), + options: { + stickyHeader: true, + hasRowSpan: true, + rowsExpandableOptions: { + key: 'children', + columns: EXPAND_COLUMNS_CONFIG, + innerEntityType: 'dynamicComponent' + } + }, + expandRow: actionsData.expandRow + }, + template: TEMPLATE_TABLE_HOLDER + }) +}; + +export const SkeletonLoading: Story = {}; +SkeletonLoading.args = { + loading: true, + rows: [] +}; + +export const NoData: Story = {}; +NoData.args = { + rows: [], + options: { + noDataMessage: 'No data to display', + noDataSubMessage: 'Lorem ipsum' + } +}; + +export const NoSearchResult: Story = {}; +NoSearchResult.args = { + rows: [], + options: { + emptyTableType: 'noResult', + noDataMessage: 'No data to display', + noDataSubMessage: 'Search again with different filters' + } +}; diff --git a/projects/fusion-ui/components/table/v4/stories/table.mock-data.ts b/projects/fusion-ui/components/table/v4/stories/table.mock-data.ts new file mode 100644 index 000000000..de5f037df --- /dev/null +++ b/projects/fusion-ui/components/table/v4/stories/table.mock-data.ts @@ -0,0 +1,883 @@ +import {TableColumn, TableColumnTypeEnum} from '@ironsource/fusion-ui/components/table'; +import {InlineInputType} from '@ironsource/fusion-ui/components/input-inline'; +import {FormControl, Validators} from '@angular/forms'; + +// region default data +export const TABLE_DEFAULT_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'firstName', title: 'First name'}, + {key: 'lastName', title: 'Last name', width: '150px'}, + {key: 'address', title: 'Address'}, + {key: 'state', title: 'State'} +]; + +export const TABLE_TOOLTIP_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'firstName', title: 'First name', tooltip: 'First name tooltip'}, + {key: 'lastName', title: 'Last name', tooltip: 'Last name tooltip'}, + {key: 'address', title: 'Address'}, + {key: 'state', title: 'State', tooltip: 'State tooltip'} +]; + +export const TABLE_HORIZONTAL_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'firstName', title: 'First name', width: '150px'}, + {key: 'lastName', title: 'Last name', width: '150px'}, + {key: 'address', title: 'Address', width: '200px'}, + {key: 'state', title: 'State'}, + {key: 'phone', title: 'Phone number', width: '150px'}, + {key: 'status', title: 'Status', width: '100px'} +]; + +export const ROWS_DEFAULT_DATA = [ + { + id: 1, + firstName: 'Abdullah', + lastName: 'Williamson', + address: '2785 Karlie Run', + state: 'Florida' + }, + { + id: 2, + firstName: 'Ada', + lastName: 'McLaughlin lorem ipsum dolor sit amet consectetur adipiscing elit.', + address: '841 Chanelle Canyon', + state: 'Arkansas' + }, + { + id: 3, + firstName: 'Adell', + lastName: 'Bergstrom', + address: '3844 Cormier Island', + state: 'Georgia' + } +]; + +export const ROWS_DEFAULT_EDITABLE_DATA = ROWS_DEFAULT_DATA.map((row, idx) => { + const fcLastName = new FormControl(row.lastName, [Validators.required, Validators.minLength(3)]); + return {...row, lastName: fcLastName}; +}); + +export const ROWS_DEFAULT_DATA_WITH_ID = [ + { + id: 1, + firstName: 'Abdullah', + lastName: 'Williamson', + address: '2785 Karlie Run', + state: 'Florida' + }, + {id: 2, firstName: 'Sophia', lastName: 'Martinez', address: '4921 Eliza Crescent', state: 'California'}, + { + id: 3, + firstName: 'Liam', + lastName: 'Johnson', + address: '7856 Oliver Street', + state: 'Texas' + }, + {id: 4, firstName: 'Emma', lastName: 'Brown', address: '3214 Noah Avenue', state: 'New York'}, + { + id: 5, + firstName: 'Noah', + lastName: 'Garcia', + address: '9632 Ava Lane', + state: 'Illinois' + }, + {id: 6, firstName: 'Olivia', lastName: 'Miller', address: '1478 Ethan Road', state: 'Pennsylvania'}, + { + id: 7, + firstName: 'Ethan', + lastName: 'Davis', + address: '5236 Mia Circle', + state: 'Ohio' + }, + {id: 8, firstName: 'Ava', lastName: 'Rodriguez', address: '8741 William Boulevard', state: 'Georgia'}, + { + id: 9, + firstName: 'Mason', + lastName: 'Wilson', + address: '3698 Charlotte Drive', + state: 'North Carolina' + }, + {id: 10, firstName: 'Sophia', lastName: 'Anderson', address: '1597 James Court', state: 'Michigan'}, + { + id: 11, + firstName: 'William', + lastName: 'Taylor', + address: '7412 Amelia Way', + state: 'New Jersey' + }, + {id: 12, firstName: 'Isabella', lastName: 'Thomas', address: '9630 Benjamin Street', state: 'Virginia'}, + { + id: 13, + firstName: 'James', + lastName: 'Hernandez', + address: '2581 Evelyn Avenue', + state: 'Washington' + }, + {id: 14, firstName: 'Charlotte', lastName: 'Moore', address: '4723 Daniel Lane', state: 'Arizona'}, + { + id: 15, + firstName: 'Benjamin', + lastName: 'Martin', + address: '8159 Scarlett Road', + state: 'Massachusetts' + }, + {id: 16, firstName: 'Amelia', lastName: 'Jackson', address: '3647 Henry Circle', state: 'Indiana'}, + { + id: 17, + firstName: 'Lucas', + lastName: 'Thompson', + address: '6392 Alexander Court', + state: 'Tennessee' + }, + {id: 18, firstName: 'Mia', lastName: 'White', address: '1875 Sebastian Drive', state: 'Missouri'}, + { + id: 19, + firstName: 'Henry', + lastName: 'Lopez', + address: '5914 Violet Way', + state: 'Maryland' + }, + {id: 20, firstName: 'Evelyn', lastName: 'Lee', address: '2369 Jack Boulevard', state: 'Wisconsin'}, + { + id: 21, + firstName: 'Alexander', + lastName: 'Gonzalez', + address: '7896 Chloe Street', + state: 'Minnesota' + }, + {id: 22, firstName: 'Harper', lastName: 'Harris', address: '4125 Owen Road', state: 'Colorado'}, + { + id: 23, + firstName: 'Daniel', + lastName: 'Clark', + address: '9587 Zoey Avenue', + state: 'Alabama' + }, + {id: 24, firstName: 'Abigail', lastName: 'Lewis', address: '3214 Levi Lane', state: 'South Carolina'}, + { + id: 25, + firstName: 'Michael', + lastName: 'Robinson', + address: '6547 Aria Circle', + state: 'Louisiana' + }, + {id: 26, firstName: 'Emily', lastName: 'Walker', address: '1932 Grayson Court', state: 'Kentucky'}, + { + id: 27, + firstName: 'David', + lastName: 'Perez', + address: '8763 Layla Drive', + state: 'Oregon' + }, + {id: 28, firstName: 'Elizabeth', lastName: 'Hall', address: '5698 Asher Way', state: 'Oklahoma'}, + { + id: 29, + firstName: 'Joseph', + lastName: 'Young', + address: '2147 Ellie Road', + state: 'Connecticut' + }, + {id: 30, firstName: 'Sofia', lastName: 'Allen', address: '7536 Julian Avenue', state: 'Utah'}, + { + id: 31, + firstName: 'John', + lastName: 'Sanchez', + address: '4269 Leo Street', + state: 'Iowa' + }, + {id: 32, firstName: 'Avery', lastName: 'Wright', address: '9874 Nora Boulevard', state: 'Arkansas'}, + { + id: 33, + firstName: 'Samuel', + lastName: 'King', + address: '3652 Eli Circle', + state: 'Mississippi' + }, + {id: 34, firstName: 'Scarlett', lastName: 'Scott', address: '1478 Hannah Lane', state: 'Kansas'}, + { + id: 35, + firstName: 'Christopher', + lastName: 'Green', + address: '7896 Isaac Court', + state: 'Nevada' + }, + {id: 36, firstName: 'Victoria', lastName: 'Baker', address: '5214 Lily Road', state: 'New Mexico'}, + { + id: 37, + firstName: 'Andrew', + lastName: 'Adams', + address: '9632 Wyatt Drive', + state: 'West Virginia' + }, + {id: 38, firstName: 'Chloe', lastName: 'Nelson', address: '3698 Grace Way', state: 'Nebraska'}, + { + id: 39, + firstName: 'Jack', + lastName: 'Hill', + address: '1597 Luca Avenue', + state: 'Idaho' + }, + {id: 40, firstName: 'Grace', lastName: 'Ramirez', address: '7412 Aubrey Street', state: 'Hawaii'}, + { + id: 41, + firstName: 'Luke', + lastName: 'Campbell', + address: '2581 Hazel Circle', + state: 'New Hampshire' + }, + {id: 42, firstName: 'Zoe', lastName: 'Mitchell', address: '4723 Ezra Boulevard', state: 'Maine'}, + { + id: 43, + firstName: 'Isaac', + lastName: 'Roberts', + address: '8159 Aurora Lane', + state: 'Montana' + }, + {id: 44, firstName: 'Hannah', lastName: 'Carter', address: '3647 Hudson Road', state: 'Delaware'}, + { + id: 45, + firstName: 'Owen', + lastName: 'Phillips', + address: '6392 Stella Court', + state: 'South Dakota' + }, + {id: 46, firstName: 'Lily', lastName: 'Evans', address: '1875 Sawyer Drive', state: 'North Dakota'}, + { + id: 47, + firstName: 'Wyatt', + lastName: 'Turner', + address: '5914 Lincoln Way', + state: 'Alaska' + }, + {id: 48, firstName: 'Addison', lastName: 'Torres', address: '2369 Bella Avenue', state: 'Vermont'}, + { + id: 49, + firstName: 'Eli', + lastName: 'Parker', + address: '7896 Maverick Street', + state: 'Wyoming' + }, + {id: 50, firstName: 'Aubrey', lastName: 'Collins', address: '4125 Paisley Road', state: 'Rhode Island'} +]; + +export const ROWS_HORIZONTAL_DATA_WITH = [ + { + id: 1, + firstName: 'Abdullah', + lastName: 'Williamson', + address: '2785 Karlie Run', + state: 'Florida', + phone: '(212) 95-212-32', + status: 'Active' + }, + { + id: 2, + firstName: 'Sophia', + lastName: 'Martinez', + address: '4721 Oak Street', + state: 'California', + phone: '(555) 123-4567', + status: 'Inactive' + }, + { + id: 3, + firstName: 'Liam', + lastName: 'Johnson', + address: '789 Pine Avenue', + state: 'New York', + phone: '(333) 987-6543', + status: 'Active' + }, + { + id: 4, + firstName: 'Emma', + lastName: 'Garcia', + address: '1010 Maple Lane', + state: 'Texas', + phone: '(444) 567-8901', + status: 'Active' + }, + { + id: 5, + firstName: 'Noah', + lastName: 'Brown', + address: '2468 Elm Street', + state: 'Illinois', + phone: '(777) 234-5678', + status: 'Inactive' + }, + { + id: 6, + firstName: 'Olivia', + lastName: 'Davis', + address: '3690 Cedar Road', + state: 'Pennsylvania', + phone: '(888) 345-6789', + status: 'Active' + }, + { + id: 7, + firstName: 'Ethan', + lastName: 'Wilson', + address: '1357 Birch Boulevard', + state: 'Ohio', + phone: '(999) 876-5432', + status: 'Active' + }, + { + id: 8, + firstName: 'Ava', + lastName: 'Anderson', + address: '2468 Spruce Street', + state: 'Michigan', + phone: '(111) 222-3333', + status: 'Inactive' + }, + { + id: 9, + firstName: 'Mason', + lastName: 'Taylor', + address: '9876 Willow Way', + state: 'Georgia', + phone: '(222) 333-4444', + status: 'Active' + }, + { + id: 10, + firstName: 'Isabella', + lastName: 'Thomas', + address: '5432 Aspen Avenue', + state: 'Washington', + phone: '(333) 444-5555', + status: 'Active' + }, + { + id: 11, + firstName: 'William', + lastName: 'Jackson', + address: '7890 Sycamore Street', + state: 'Arizona', + phone: '(444) 555-6666', + status: 'Inactive' + }, + { + id: 12, + firstName: 'Charlotte', + lastName: 'White', + address: '1234 Magnolia Drive', + state: 'Massachusetts', + phone: '(555) 666-7777', + status: 'Active' + }, + { + id: 13, + firstName: 'James', + lastName: 'Harris', + address: '5678 Juniper Lane', + state: 'Virginia', + phone: '(666) 777-8888', + status: 'Active' + }, + { + id: 14, + firstName: 'Amelia', + lastName: 'Martin', + address: '9012 Poplar Place', + state: 'New Jersey', + phone: '(777) 888-9999', + status: 'Inactive' + }, + { + id: 15, + firstName: 'Benjamin', + lastName: 'Thompson', + address: '3456 Chestnut Court', + state: 'North Carolina', + phone: '(888) 999-0000', + status: 'Active' + }, + { + id: 16, + firstName: 'Mia', + lastName: 'Garcia', + address: '7890 Walnut Way', + state: 'Colorado', + phone: '(999) 000-1111', + status: 'Active' + }, + { + id: 17, + firstName: 'Elijah', + lastName: 'Martinez', + address: '2345 Hickory Hill', + state: 'Oregon', + phone: '(000) 111-2222', + status: 'Inactive' + }, + { + id: 18, + firstName: 'Evelyn', + lastName: 'Robinson', + address: '6789 Beech Boulevard', + state: 'Indiana', + phone: '(111) 222-3333', + status: 'Active' + }, + { + id: 19, + firstName: 'Daniel', + lastName: 'Clark', + address: '1357 Cypress Circle', + state: 'Minnesota', + phone: '(222) 333-4444', + status: 'Active' + }, + { + id: 20, + firstName: 'Harper', + lastName: 'Rodriguez', + address: '2468 Fir Forest', + state: 'Wisconsin', + phone: '(333) 444-5555', + status: 'Inactive' + } +]; + +// endregion + +// region selectable rows data +export const TABLE_SELECTABLE_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'checkbox', type: TableColumnTypeEnum.Checkbox}, + ...TABLE_DEFAULT_COLUMNS_CONFIG +]; + +export const TABLE_STICKY_COLUMNS_CONFIG: TableColumn[] = TABLE_HORIZONTAL_COLUMNS_CONFIG.map((column: TableColumn, idx: number) => { + let colData: TableColumn; + const length = TABLE_HORIZONTAL_COLUMNS_CONFIG.length; + if (idx === length - 1 || idx === length - 2) { + colData = {...column, stickyRight: true}; + if (idx === length - 2) { + colData = {...colData, stickyRightMargin: '100px'}; + } + } else { + colData = {...column, sticky: idx === 0}; + } + return colData; +}); + +export const TABLE_SELECTABLE_STICKY_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'checkbox', type: TableColumnTypeEnum.Checkbox, sticky: true}, + ...TABLE_HORIZONTAL_COLUMNS_CONFIG +]; + +export const ROWS_SELECTABLE_STICKY_DATA = ROWS_HORIZONTAL_DATA_WITH.map((row, idx) => { + return {checkbox: idx == 3, ...row}; +}); + +export const ROWS_SELECTABLE_DATA = ROWS_DEFAULT_DATA.map((row, idx) => { + return {checkbox: idx == 3, ...row}; +}); +// endregion + +// region toggle rows data +export const TABLE_TOGGLEABLE_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'toggle', type: TableColumnTypeEnum.ToggleButton, width: '50px'}, + ...TABLE_DEFAULT_COLUMNS_CONFIG +]; +export const ROWS_TOGGLEABLE_DATA = ROWS_DEFAULT_DATA.map((row, idx) => { + return {toggle: true, ...row}; +}); +// endregion + +// region numbers data +export const TABLE_NUMBERS_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'planName', title: 'Plan name'}, + {key: 'lastUpdate', title: 'Last updated', type: TableColumnTypeEnum.Date}, + {key: 'price', title: 'Price', type: TableColumnTypeEnum.Currency}, + {key: 'amount', title: 'Amount', type: TableColumnTypeEnum.Number}, + {key: 'discount', title: 'Discount', type: TableColumnTypeEnum.Percent} +]; + +export const TABLE_NUMBERS_SORTABLE_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'planName', title: 'Plan name', sort: 'asc'}, + {key: 'lastUpdate', title: 'Last updated', type: TableColumnTypeEnum.Date, sort: ''}, + {key: 'price', title: 'Price', type: TableColumnTypeEnum.Currency, sort: ''}, + {key: 'amount', title: 'Amount', type: TableColumnTypeEnum.Number, sort: ''}, + {key: 'discount', title: 'Discount', type: TableColumnTypeEnum.Percent, sort: '', tooltip: 'Discount tooltip'} +]; + +export const ROWS_NUMBERS_DATA = [ + { + planName: 'Starter', + lastUpdate: new Date('12 Oct 2023'), + price: 10.9, + amount: 46, + discount: 1.3 + }, + { + planName: 'Pro', + lastUpdate: new Date('8 Oct 2023'), + price: 35.9, + amount: 22, + discount: 2.4 + }, + { + planName: 'Business', + lastUpdate: new Date('11 Oct 2023'), + price: 89.9, + amount: 15, + discount: 5 + } +]; + +// endregion + +// region inline input data +export const TABLE_EDITABLE_COLUMNS_CONFIG: TableColumn[] = [ + { + key: 'planName', + title: 'Plan name', + type: TableColumnTypeEnum.InputEdit, + inputType: InlineInputType.Text, + customErrorMapping: { + required: {errorMessageKey: 'required'} + } + }, + {key: 'lastUpdate', title: 'Last updated', type: TableColumnTypeEnum.Date}, + { + key: 'price', + title: 'Price', + type: TableColumnTypeEnum.InputEdit, + inputType: InlineInputType.Currency, + customErrorMapping: { + required: {errorMessageKey: 'required'} + } + }, + { + key: 'amount', + title: 'Amount', + type: TableColumnTypeEnum.InputEdit, + inputType: InlineInputType.Number, + customErrorMapping: { + required: {errorMessageKey: 'required'} + } + }, + { + key: 'discount', + title: 'Discount', + type: TableColumnTypeEnum.InputEdit, + inputType: InlineInputType.Percent, + customErrorMapping: { + required: {errorMessageKey: 'required'} + } + } +]; + +export const ROWS_EDITABLE_DATA = ROWS_NUMBERS_DATA.map((row, idx) => { + const data = { + planName: new FormControl(row.planName, [Validators.required]), + lastUpdate: row.lastUpdate, + price: new FormControl(row.price, [Validators.required]), + amount: new FormControl(row.amount, [Validators.required]), + discount: new FormControl(row.discount, [Validators.required]) + }; + return data; +}); +// endregion + +// region inline dropdown data +export const TABLE_DROPDOWN_COLUMNS_CONFIG: TableColumn[] = [ + { + key: 'planName', + title: 'Plan name', + type: TableColumnTypeEnum.InputEdit, + inputType: InlineInputType.Dropdown, + inlineDropdownOptions: ROWS_NUMBERS_DATA.map((row, idx) => ({id: idx++, displayText: row.planName})) + }, + {key: 'lastUpdate', title: 'Last updated', type: TableColumnTypeEnum.Date}, + { + key: 'price', + title: 'Price', + type: TableColumnTypeEnum.InputEdit, + inputType: InlineInputType.Currency, + customErrorMapping: { + required: {errorMessageKey: 'required'} + } + }, + { + key: 'amount', + title: 'Amount', + type: TableColumnTypeEnum.InputEdit, + inputType: InlineInputType.Number, + customErrorMapping: { + required: {errorMessageKey: 'required'} + } + }, + { + key: 'discount', + title: 'Discount', + type: TableColumnTypeEnum.InputEdit, + inputType: InlineInputType.Percent, + customErrorMapping: { + required: {errorMessageKey: 'required'} + } + } +]; + +export const ROWS_DROPDOWN_DATA = ROWS_NUMBERS_DATA.map((row, idx) => { + const data = { + planName: new FormControl(TABLE_DROPDOWN_COLUMNS_CONFIG[0].inlineDropdownOptions.filter(item => item.displayText === row.planName)), + lastUpdate: row.lastUpdate, + price: new FormControl(row.price, [Validators.required]), + amount: new FormControl(row.amount, [Validators.required]), + discount: new FormControl(row.discount, [Validators.required]) + }; + return data; +}); +// endregion + +// region row expand data +export const EXPAND_COLUMNS_CONFIG: TableColumn[] = [ + {key: 'id', title: 'Id', width: '50px'}, + {key: 'name', title: 'Name'}, + {key: 'username', title: 'Username'}, + {key: 'email', title: 'Email', width: '250px'}, + {key: 'website', title: 'Website'} +]; + +export const EXPAND_ROWS_DEFAULT_DATA = [ + { + id: 1, + name: 'Leanne Graham', + username: 'Bret', + email: 'Sincere@april.biz', + website: 'hildegard.org' + }, + { + id: 2, + name: 'Ervin Howell', + username: 'Antonette', + email: 'Shanna@melissa.tv', + website: 'anastasia.net' + }, + { + id: 3, + name: 'Clementine Bauch', + username: 'Samantha', + email: 'Nathan@yesenia.net', + website: 'ramiro.info' + }, + { + id: 4, + name: 'Patricia Lebsack', + username: 'Karianne', + email: 'Julianne.OConner@kory.org', + website: 'kale.biz' + }, + { + id: 5, + name: 'Chelsey Dietrich', + username: 'Kamren', + email: 'Lucio_Hettinger@annie.ca', + website: 'demarco.info' + }, + { + id: 6, + name: 'Mrs. Dennis Schulist', + username: 'Leopoldo_Corkery', + email: 'Karley_Dach@jasper.info', + website: 'ola.org' + }, + { + id: 7, + name: 'Kurtis Weissnat', + username: 'Elwyn.Skiles', + email: 'Telly.Hoeger@billy.biz', + website: 'elvis.io' + }, + { + id: 8, + name: 'Nicholas Runolfsdottir V', + username: 'Maxime_Nienow', + email: 'Sherwood@rosamond.me', + website: 'jacynthe.com' + }, + { + id: 9, + name: 'Glenna Reichert', + username: 'Delphine', + email: 'Chaim_McDermott@dana.io', + website: 'conrad.com' + }, + { + id: 10, + name: 'Clementina DuBuque', + username: 'Moriah.Stanton', + email: 'Rey.Padberg@karina.biz', + website: 'ambrose.net' + }, + { + id: 11, + name: 'Evelyn Prescott', + username: 'Eve.Prescott', + email: 'EPrescott@lumina.com', + website: 'prescottdesigns.net' + }, + {id: 12, name: "Liam O'Connor", username: 'LiamOC', email: 'Liam.OConnor@techwave.io', website: 'oconnortech.com'}, + { + id: 13, + name: 'Sofia Rodriguez', + username: 'SofiaR', + email: 'Sofia.Rodriguez@globalnet.org', + website: 'rodriguezarts.net' + }, + {id: 14, name: 'Ethan Zhao', username: 'E_Zhao', email: 'EthanZ@innovative.biz', website: 'zhaoengineering.com'}, + { + id: 15, + name: 'Isabella Moretti', + username: 'Bella.Moretti', + email: 'I.Moretti@fashionista.it', + website: 'morettistyle.net' + }, + { + id: 16, + name: 'Noah Campbell', + username: 'N_Campbell', + email: 'Noah.Campbell@ecofriendly.org', + website: 'campbellgreen.com' + }, + {id: 17, name: 'Mia Tanaka', username: 'MiaTanaka', email: 'Mia.T@tokyotech.jp', website: 'tanakadesigns.net'}, + { + id: 18, + name: 'Oliver Singh', + username: 'O_Singh', + email: 'Oliver.Singh@globalfinance.com', + website: 'singhconsulting.net' + }, + {id: 19, name: 'Ava Kowalski', username: 'AvaK', email: 'A.Kowalski@polisharts.pl', website: 'kowalskigallery.com'}, + { + id: 20, + name: 'Lucas Ferreira', + username: 'L_Ferreira', + email: 'Lucas.F@braziltech.br', + website: 'ferreirasoft.net' + }, + { + id: 21, + name: 'Emma Larsson', + username: 'EmmaL', + email: 'E.Larsson@nordicdesign.se', + website: 'larssoninteriors.com' + }, + { + id: 22, + name: 'Alexander Volkov', + username: 'A_Volkov', + email: 'Alex.Volkov@russiancoder.ru', + website: 'volkovtech.net' + }, + {id: 23, name: 'Charlotte Wu', username: 'CharlotteW', email: 'C.Wu@asianmarket.cn', website: 'wuenterprises.com'}, + {id: 24, name: 'William Nkosi', username: 'Will_Nkosi', email: 'W.Nkosi@africanart.za', website: 'nkosicraft.net'}, + { + id: 25, + name: 'Sophia Müller', + username: 'S_Mueller', + email: 'Sophia.Mueller@deutschebank.de', + website: 'muellerfinance.com' + }, + {id: 26, name: "James O'Brien", username: 'JOBrien', email: 'J.OBrien@irishpub.ie', website: 'obrientavern.net'}, + { + id: 27, + name: 'Amelia Dubois', + username: 'A_Dubois', + email: 'Amelia.D@frenchcuisine.fr', + website: 'duboiscooking.com' + }, + {id: 28, name: 'Benjamin Cohen', username: 'BenC', email: 'B.Cohen@isratech.il', website: 'cohentechnology.net'}, + { + id: 29, + name: 'Harper Nguyen', + username: 'H_Nguyen', + email: 'Harper.Nguyen@vietsoft.vn', + website: 'nguyencode.com' + }, + { + id: 30, + name: 'Elijah Sanchez', + username: 'E_Sanchez', + email: 'Elijah.S@latinoart.mx', + website: 'sanchezgallery.net' + }, + { + id: 31, + name: 'Aria Rossi', + username: 'AriaR', + email: 'A.Rossi@italiandesign.it', + website: 'rossiarchitecture.com' + }, + {id: 32, name: 'Leo Kim', username: 'LeoK', email: 'Leo.Kim@kpopstar.kr', website: 'kimenterprises.net'}, + { + id: 33, + name: 'Zoe Andersen', + username: 'ZoeA', + email: 'Z.Andersen@danishdesign.dk', + website: 'anderseninteriors.com' + }, + { + id: 34, + name: 'Gabriel Santos', + username: 'G_Santos', + email: 'Gabriel.S@brazilsoccer.br', + website: 'santossports.net' + }, + { + id: 35, + name: 'Chloe Dupont', + username: 'ChloeDupont', + email: 'C.Dupont@parismode.fr', + website: 'dupontfashion.com' + }, + { + id: 36, + name: 'Daniel Yamamoto', + username: 'D_Yamamoto', + email: 'Daniel.Y@tokyotech.jp', + website: 'yamamotorobotics.net' + }, + { + id: 37, + name: 'Victoria Ivanova', + username: 'V_Ivanova', + email: 'Victoria.I@russianballet.ru', + website: 'ivanovadance.com' + }, + { + id: 38, + name: 'Henry Okafor', + username: 'HenryO', + email: 'H.Okafor@africantech.ng', + website: 'okafortechnology.net' + }, + { + id: 39, + name: 'Scarlett Chen', + username: 'S_Chen', + email: 'Scarlett.Chen@chinesemed.cn', + website: 'chenholistic.com' + }, + { + id: 40, + name: 'Sebastian Gomez', + username: 'SebGomez', + email: 'S.Gomez@latinmusic.co', + website: 'gomezrecords.net' + } +]; + +// endregion + +// region row actions +export const MOCK_ROW_ACTIONS = [ + {icon: 'ph/pencil-simple', label: 'Edit'}, + {icon: 'ph/copy-simple', label: 'Duplicate'}, + {icon: 'ph/trash-simple', label: 'Delete'} +]; +// endregion diff --git a/projects/fusion-ui/components/table/v4/stories/table.story-holder.component/table.story-holder.component.component.ts b/projects/fusion-ui/components/table/v4/stories/table.story-holder.component/table.story-holder.component.component.ts new file mode 100644 index 000000000..4c255d31b --- /dev/null +++ b/projects/fusion-ui/components/table/v4/stories/table.story-holder.component/table.story-holder.component.component.ts @@ -0,0 +1,319 @@ +import {Component, EventEmitter, Input, OnDestroy, OnInit, Output, Type} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms'; +import {delay, finalize, map, take, takeUntil, tap} from 'rxjs/operators'; +import {BehaviorSubject, Observable, of, Subject} from 'rxjs'; +import {isNullOrUndefined, isNumber} from '@ironsource/fusion-ui/utils'; +import {ButtonComponent} from '@ironsource/fusion-ui/components/button/v4'; +import {GenericPipe, TableTestIdModifiers} from '@ironsource/fusion-ui'; +import {SearchV4Component} from '@ironsource/fusion-ui/components/search/v4/search-v4.component'; +import {DynamicComponent, DynamicComponentConfiguration} from '@ironsource/fusion-ui/components/dynamic-components/common/entities'; +import {InnerEntityType, TableColumn, TableOptions, TableRowExpandEmitter} from '@ironsource/fusion-ui/components/table'; +import {EXPAND_ROWS_DEFAULT_DATA} from '../table.mock-data'; +import {TableV4Component} from '../../table-v4.component'; +import {RowExpandInnerComponent} from '../row-expand-inner/row-expand-inner.component'; + +@Component({ + selector: 'fusion-table-story-holder', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, TableV4Component, ButtonComponent, GenericPipe, SearchV4Component], + template: ` +
    +
    + Displaying {{ shownRows }} out of + {{ totalRows }} total rows +
    +
    + + + Secondary + Primary +
    +
    + + +
    `, + styles: [ + ` + ::ng-deep tbody tr td.fu-badge:not(.inner-row) div { + width: unset !important; + height: 20px; + display: flex; + align-items: center; + padding: 2px 4px; + border-radius: 4px; + background-color: #edeff0; + } + ` + ] +}) +export class TableV4StoryHolderComponent implements OnInit, OnDestroy { + /** + * Table columns configuration + * columns: TableColumn[] + */ + @Input() columns: TableColumn[] = []; + /** + * Table Options (configuration) + * @param value: TableOptions + */ + @Input() options: TableOptions = {}; + + /** + * Table rows data + * rows: {[key: string]: any}[] + */ + @Input() set rows(value: {[key: string]: any}[]) { + if (Array.isArray(value)) { + this._rows = value; + this.tableRows = this._rows; + } + } + + @Input() hasCustomHeader: boolean = false; + @Input() hasCustomFooter: boolean = false; + + @Output() rowModelChange = new EventEmitter(); + @Output() selectionChanged = new EventEmitter(); + @Output() expandRow = new EventEmitter(); + @Output() rowActionClicked = new EventEmitter(); + + /** @ignore */ + @Input() set loading(value: boolean) { + this.tableLoading$.next(value); + } + + /** @ignore */ + tableRows = []; + /** @ignore */ + tableLoading$ = new BehaviorSubject(false); + /** @ignore */ + expandedRows: {[key: string]: boolean} = {}; // maf expanded rows - {1: true} mean that row with index 1 - expanded + + totalRows: number; + shownRows: number; + + searchFormControl = new FormControl(); + + private onDestroy$ = new Subject(); + private _rows = []; + + private onRowDataChanged$ = new EventEmitter(); + + ngOnInit() { + this.totalRows = this._rows.length; + this.shownRows = this.totalRows; + + if (this.hasCustomHeader) { + this.searchFormControl.valueChanges.pipe(takeUntil(this.onDestroy$)).subscribe(this.doSearch.bind(this)); + } + + if (!isNullOrUndefined(this.options?.searchOptions?.onSearch)) { + this.options.searchOptions.onSearch.pipe(takeUntil(this.onDestroy$)).subscribe(this.doSearch.bind(this)); + } + + this.onRowDataChanged$.pipe(takeUntil(this.onDestroy$)).subscribe(this.onRowModelChange.bind(this)); + } + + ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + onRowModelChange($event) { + console.log('onRowModelChange >>', $event); + this.rowModelChange.emit($event); + setTimeout(() => { + if ($event.keyChanged === 'live') { + $event.rowModel[$event.keyChanged] = $event.newValue; + } + $event.onRequestDone(true); + }, 2000); + } + + selectedChanged($event) { + console.log('selectedChanged >>', $event); + this.tableRows = this.tableRows.map(item => { + item.checkbox = $event.some(row => row.id === item.id); + return item; + }); + this.selectionChanged.emit($event); + } + + onscrollDown() { + const shownLength = this._rows.length; + const newRows = Array.from({length: 20}, (_, i) => { + const id = i + shownLength + 1; + return { + id: id, + firstName: id + ' first name', + lastName: id + ' last Name', + address: id + ' address', + state: id + ' state' + }; + }); + + setTimeout(() => { + this._rows = [...this._rows, ...newRows]; + this.tableRows = this._rows; + this.options = {...this.options, pagination: {enable: true, loading: false}}; + }, 700); + } + + onSortChanged(sortByKey) { + let sortDirection: 'asc' | 'desc'; + this.columns = this.columns.map(column => { + if (column.key === sortByKey) { + sortDirection = column.sort === 'asc' ? 'desc' : 'asc'; + column.sort = sortDirection; + } else { + column.sort = ''; + } + return column; + }); + + console.log('onSortChanged: ', sortByKey, sortDirection); + + this.tableLoading$.next(true); + of(this._rows) + .pipe( + takeUntil(this.onDestroy$), + map(rows => { + return this.doRowsSort(rows, sortByKey, sortDirection); + }), + delay(1000), + finalize(() => { + this.tableLoading$.next(false); + }) + ) + .subscribe(rows => { + this.tableRows = [...rows]; + }); + } + + onExpandRow({rowIndex, row, isExpanded, successCallback, failedCallback, updateMap, innerEntityType}: TableRowExpandEmitter): void { + this.expandRow.emit({rowIndex, row, isExpanded, successCallback, failedCallback, updateMap, innerEntityType}); + // updateMap - in case external expand call it must be false because map will be already updated. + const tableRows = this.tableRows; + // get child rows that can be already existed + const childExisted: any[] = tableRows[rowIndex].children; + (isExpanded ? (!isNullOrUndefined(childExisted) ? of(childExisted) : this.getExpandedData(rowIndex, innerEntityType)) : of(null)) + .pipe( + take(1), + tap(() => { + // set what row expanded, or update to collapsed state if was expanded + // @ts-ignore + // throw new Exception(); + return (this.expandedRows = updateMap + ? { + ...this.expandedRows, + [rowIndex]: isExpanded + } + : this.expandedRows); + }) + ) + .subscribe(data => { + if (isNullOrUndefined(childExisted)) { + // if was no children, set arrived data as children + const children = !!data ? data : []; + // update row by index with children + tableRows.splice(parseInt(rowIndex as string, 10), 1, {...row, children}); + // update table rows + this.tableRows = [...tableRows]; + } + // all Ok - call success + successCallback(); + }, failedCallback); + } + + onRowActionClicked($event) { + console.log('onRowActionClicked >>', $event); + this.rowActionClicked.emit($event); + } + + private doSearch(value: string) { + console.log('onSearch >>', value); + this.tableRows = [ + ...this._rows.filter(item => { + return Object.keys(item).some(key => { + return item[key].toString().toLowerCase().includes(value.toLowerCase()); + }); + }) + ]; + this.shownRows = this.tableRows.length; + if (this.tableRows.length === 0) { + this.options = { + ...this.options, + noDataMessage: 'No data to display', + noDataSubMessage: 'Search again with different filters', + emptyTableType: 'noResult' + }; + } + } + + /** + * Just get from main data mock - portion for child rows + */ + private getExpandedData(rowIndex, innerEntityType: InnerEntityType): Observable { + if (isNumber(rowIndex)) { + if (innerEntityType === 'dynamicComponent') { + // @ts-ignore + return of([ + { + component: { + type: RowExpandInnerComponent as Type, + data: { + title: this.tableRows[rowIndex]?.name ?? 'NoName', + subtitle: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Dolor magna eget est lorem ipsum dolor sit amet', + benefits: ['Odio pellentesque diam volutpat commodo', 'Egestas sed tempus urna et pharetra pharetra'] + } + } as DynamicComponent + } + ] as DynamicComponentConfiguration[]).pipe(delay(1000)); + } else { + // @ts-ignore + return of( + EXPAND_ROWS_DEFAULT_DATA.slice(5, 30).map(item => { + if (!this.options.hasRowSpan) { + return item; + } else { + return { + ...item + }; + } + }) + ).pipe(delay(1000)); + } + } + return of([]).pipe(delay(1000)); + } + + private doRowsSort(rows: any[], sortKey: string, sortDirection: 'asc' | 'desc'): any[] { + return rows.sort((a, b) => { + if (isNumber(a[sortKey])) { + return sortDirection === 'asc' ? a[sortKey] - b[sortKey] : b[sortKey] - a[sortKey]; + } + return sortDirection === 'asc' ? a[sortKey].localeCompare(b[sortKey]) : b[sortKey].localeCompare(a[sortKey]); + }); + } + + protected readonly tableTestIdModifiers = TableTestIdModifiers; +} diff --git a/projects/fusion-ui/components/table/v4/table-v4.component.html b/projects/fusion-ui/components/table/v4/table-v4.component.html new file mode 100644 index 000000000..b7c1d523e --- /dev/null +++ b/projects/fusion-ui/components/table/v4/table-v4.component.html @@ -0,0 +1,195 @@ +@if (hasCustomHeader || options?.tableLabel) { +
    + @if (hasCustomHeader) { + + } @else { + @if (options?.tableLabel) { +
    +
    + {{ options?.tableLabel?.text }} +
    + @if (options?.tableLabel?.tooltip) { + + } +
    + } + @if (options?.searchOptions) { + + } + } +
    +} +
    +
    + @if (!tableMainError) { + + + @if (columns && options) { + @if (columns.length) { + + + @if (subHeader.length) { + + @if (!!options?.rowsExpandableOptions) { + + } + @for (subheader of subHeader; track subHeader; let isFirst = $first) { + + } + + } + + + + @if (!!options?.rowsExpandableOptions) { + + } + @for (column of columns; track trackByFunc; let isLast = $last; let isFirst = $first; let idx = $index) { + + } + + + + } + } + + + @if (!options.isGroupedTable && (!loading || isLoadingOverlay) && !isEmpty) { + + } + + + + + @if (!loading && isEmpty) { + + } + + + @if ((loading && !isLoadingOverlay) || + (options?.pagination && options.pagination.loading) || + (isEmpty && loading)) { + + } + + +
    + {{ subheader.name }} +
    +
    + @if (tableService.isTypeCheckbox(column) && isAllRowsSelectable && !isEmpty && isCheckboxTitleShown) { + + } @else if (column.filter && column.filter.options) { + + + } @else { +
    {{ column.title }} +
    + @if (column.tooltip) { + + } + @if (tableService.isColumnSortable(column)) { +
    + @if (column.sort === 'asc') { +
    + +
    + } @else if (column.sort === 'desc') { +
    + +
    + } +
    + } + } +
    +
    + } +
    +@if (hasCustomFooter) { + +} + + + + diff --git a/projects/fusion-ui/components/table/v4/table-v4.component.scss b/projects/fusion-ui/components/table/v4/table-v4.component.scss new file mode 100644 index 000000000..29e939388 --- /dev/null +++ b/projects/fusion-ui/components/table/v4/table-v4.component.scss @@ -0,0 +1,246 @@ +@import "../../../src/style/scss/v4/vars/vars"; +@import "../../../src/style/scss/v4/mixins/mixins"; + +:host { + // region css variables + --fu-scroll-width: 6px; + --fu-scroll-border: solid 1px var(--common-divider, #{$color-v4-common-divider}); + --fu-scroll-button-bg-color: var(--default-light, #{$color-v4-default-light}); + --table-border-width: 1px; + --table-border-type: solid; + --table-border-radius: 4px; + --table-border-color: var(--common-divider, #{$color-v4-common-divider}); + --table-border: var(--table-border-width) var(--table-border-type) var(--table-border-color); + --table-text-color: var(--text-primary, #{$color-v4-text-primary}); + + --table-header-bg-color: var(--background-paper-elevation-0, #{$color-v4-background-paper-elevation-0}); + --table-header-search-width: 220px; + --table-header-cell-height: 40px; + --table-header-cell-padding: 0 16px; + --table-header-cell-bg-color: var(--background-paper-elevation-0, #{$color-v4-background-paper-elevation-0}); + --table-header-cell-gap: 4px; + --table-header-text-color: var(--table-text-color); + --table-header-sort-icon-color: var(--action-active, #{$color-v4-action-active}); + --table-header-sort-icon-size: 20px; + --table-header-tooltip-icon-size: 16px; + --table-header-tooltip-icon-color: var(--action-active, #{$color-v4-action-active}); + + --table-footer-height: 48px; + --table-footer-padding: 14px 16px; + --table-footer-gap: 8px; + --table-footer-text-color: var(--table-text-color); + --table-footer-bg-color: var(--background-paper-elevation-0, #{$color-v4-background-paper-elevation-0}); + + --table-empty-state-padding: 48px; + --table-checkbox-cell-width: 32px; + + --table-body-border: var(--table-border); + --table-row-height: 48px; + --table-row-cell-lr-padding: 16px; + --table-row-cell-padding: 0 var(--table-row-cell-lr-padding); + + --table-expand-icon-color: var(--action-active, #{$color-v4-action-active}); + --table-expand-icon-size: 20px; + --table-expand-cell-width: 46px; + + --table-row-hover-background-color: var(--background-paper-elevation-1, #{$color-v4-background-paper-elevation-1}); + --table-odd-row-background-color: #{$color-v4-common-white}; + --table-even-row-background-color: #{$color-v4-common-white}; + --table-row-loading-opacity: 0.7; + + // endregion + + @extend %reset; + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + + border: var(--table-border); + border-radius: var(--table-border-radius); + + .fu-table-header-wrapper { + display: flex; + height: var(--spacing-800, #{$spacingV4-800}); + padding: 0px var(--spacing-300, #{$spacingV4-300}); + align-items: center; + gap: 8px; + + border-top-left-radius: var(--table-border-radius); + border-top-right-radius: var(--table-border-radius); + + border-bottom: var(--table-border); + background-color: var(--table-header-bg-color); + + .fu-table-label { + display: flex; + align-items: center; + gap: 4px; + flex: 1; + + .fu-table-label-text { + @extend %font-v4-heading-3; + color: var(--table-header-text-color); + } + + .fu-header-tooltip-icon { + @include size(var(--table-header-tooltip-icon-size)); + color: var(--table-header-tooltip-icon-color); + } + } + + fusion-search { + width: var(--table-header-search-width); + } + } + + .fu-table-footer-wrapper { + display: flex; + align-items: center; + height: var(--table-footer-height); + padding: var(--table-footer-padding); + gap: var(--table-footer-gap); + background-color: var(--table-footer-bg-color); + border-top: var(--table-border); + border-bottom-left-radius: var(--table-border-radius); + border-bottom-right-radius: var(--table-border-radius); + @extend %font-v4-body-1; + color: var(--table-footer-text-color); + } + + .tableWrap { + flex-grow: 1; + display: block; + + table { + border: none; + margin: 0; + border-collapse: collapse; + border-spacing: 0; + width: 100%; + + thead { + tr { + td { + @extend %font-v4-table-label; + height: var(--table-header-cell-height); + padding: var(--table-header-cell-padding); + background-color: var(--table-header-cell-bg-color); + border-bottom: var(--table-border); + + .fu-header-cell-content { + display: flex; + align-items: center; + gap: var(--table-header-cell-gap); + &.right{ + justify-content: flex-end; + } + &.center{ + justify-content: center; + } + } + + // region sort + .fu-sort-wrapper { + display: none; + @include size(var(--table-header-sort-icon-size)); + &:hover { + cursor: pointer; + } + fusion-icon { + @include size(var(--table-header-sort-icon-size)); + color: var(--table-header-sort-icon-color); + } + } + + &.is-sort { + &.asc, + &.desc { + .fu-sort-wrapper { + display: inherit; + } + } + + & > div.fu-header-cell-content .fu-header-text:hover { + cursor: pointer; + } + } + + // endregion + + fusion-icon.fu-header-tooltip-icon { + @include size(var(--table-header-tooltip-icon-size)); + color: var(--table-header-tooltip-icon-color); + } + + &.is-checkbox-holder { + width: var(--table-checkbox-cell-width); + } + } + } + } + } + + &.fu-table-sticky-header { + height: 100%; + overflow: auto; + @extend %customNavBarScroll; + + table { + thead { + position: sticky; + top: 0; + z-index: getZIndexLayerOffset(normal, 3); + outline: var(--table-border); + td.sticky-left { + position: sticky; + left: 0; + z-index: 2; + &:after{ + content: ''; + position: absolute; + top: 0; + right: -1px; + height: 100%; + width: 1px; + border-right: var(--table-border); + } + } + td.sticky-right { + position: sticky; + right: 0; + z-index: 2; + &:nth-child(1 of .sticky-right){ + &:before{ + content: ''; + position: absolute; + top: 0; + left: -1px; + height: 100%; + width: 1px; + border-left: var(--table-border); + } + } + } + + } + } + } + } + + &:not(:has(.fu-table-header-wrapper)) { + .tableWrap { + border-top-left-radius: var(--table-border-radius); + border-top-right-radius: var(--table-border-radius); + } + } + + &:not(:has(.fu-table-footer-wrapper)) { + .tableWrap { + border-bottom-left-radius: var(--table-border-radius); + border-bottom-right-radius: var(--table-border-radius); + } + } + +} \ No newline at end of file diff --git a/projects/fusion-ui/components/table/v4/table-v4.component.spec.ts b/projects/fusion-ui/components/table/v4/table-v4.component.spec.ts new file mode 100644 index 000000000..dea0c336e --- /dev/null +++ b/projects/fusion-ui/components/table/v4/table-v4.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TableV4Component } from './table-v4.component'; + +describe('TableV4Component', () => { + let component: TableV4Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TableV4Component] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TableV4Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/table/v4/table-v4.component.ts b/projects/fusion-ui/components/table/v4/table-v4.component.ts new file mode 100644 index 000000000..28d3e1a1d --- /dev/null +++ b/projects/fusion-ui/components/table/v4/table-v4.component.ts @@ -0,0 +1,616 @@ +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + HostBinding, + inject, + Input, + OnDestroy, + OnInit, + Output, + ViewChild +} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {BehaviorSubject, defer, fromEvent, Subject} from 'rxjs'; +import {debounceTime, takeUntil, tap} from 'rxjs/operators'; +import {isNullOrUndefined, isUndefined} from '@ironsource/fusion-ui/utils'; +import {GenericPipe} from '@ironsource/fusion-ui/pipes/generic'; +import {CheckboxComponent} from '@ironsource/fusion-ui/components/checkbox/v4'; +import {MenuDropItem} from '@ironsource/fusion-ui/components/menu-drop'; +import {TooltipComponent, TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {TableService} from '@ironsource/fusion-ui/components/table/common/services'; +import { + CONFIG_TABLE_BY_UI_STYLE, + ROW_CLICK_SUPPRESS_FOR_PARENT_SELECTORS, + TableColumn, + TableColumnTypeEnum, + TableIconsConfigByStyle, + TableOptions, + TableRow, + TableRowExpandEmitter, + TableRowsGrouped +} from '@ironsource/fusion-ui/components/table/common/entities'; +import {UniqueIdService} from '@ironsource/fusion-ui/services/unique-id'; +import {TestIdsService} from '@ironsource/fusion-ui/services/test-ids'; +import {TableTestIdModifiers} from '@ironsource/fusion-ui/entities'; +import {TableEmptyComponent} from './components/table-empty/table-empty.component'; +import {TableBasicComponent} from './components/table-basic/table-basic.component'; +import {TableLoadingComponent} from './components/table-loading/table-loading.component'; +import {SearchComponent} from '@ironsource/fusion-ui/components/search/v4'; + +@Component({ + selector: 'fusion-table', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [ + CommonModule, + GenericPipe, + ReactiveFormsModule, + IconModule, + CheckboxComponent, + SearchComponent, + TooltipDirective, + TooltipComponent, + TableEmptyComponent, + TableLoadingComponent, + TableBasicComponent + ], + providers: [TableService], + templateUrl: './table-v4.component.html', + styleUrl: './table-v4.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TableV4Component implements OnInit, OnDestroy, AfterViewInit { + // region public props + /** @internal */ + tableService: TableService = inject(TableService); + /** @internal */ + isRowsInit = false; + /** @internal */ + noDataMessage: string; + /** @internal */ + noDataSubMessage: string; + /** @internal */ + hideHeaderOnEmpty: boolean; + /** @internal */ + isAllRowsSelectable: boolean; + /** @internal */ + configIconNames: TableIconsConfigByStyle; + /** @internal */ + wrapperClasses: string[]; + /** @internal */ + tableMainError = false; + /** @internal */ + shownGoTopButton$ = new BehaviorSubject(false); + /** @internal */ + subHeader: {name: string; colspan: number}[] = []; + /** @internal */ + searchFormControl: FormControl; + /** @internal */ + iconArrowUp = 'ph/arrow-up'; + /** @internal */ + iconArrowDown = 'ph/arrow-down'; + /** @internal */ + iconTooltip = 'ph/question'; + // endregion + + // region E2E test id + /** @internal */ + testIdsService: TestIdsService = inject(TestIdsService); + /** @internal */ + tableTestIdModifiers: typeof TableTestIdModifiers = TableTestIdModifiers; + /** @internal */ + @Input() testId: string; + // endregion + + // region inputs + // region table element id + private uniqueService: UniqueIdService = inject(UniqueIdService); + @Input() id: string = `fuDataGrid_${this.uniqueService.getUniqueId()}`; + // endregion + + // region options + @Input() set options(value: TableOptions) { + if (!isNullOrUndefined(value)) { + this._options = value; + this.tableService.hasRowspanRows = value.hasRowSpan ?? false; + this.tableService.rowsExpandableKey = value.rowsExpandableOptions?.key; + } + } + + get options(): TableOptions { + return this._options; + } + + private _options: TableOptions = {}; + // endregion + + // region columns + @Input() set columns(value: TableColumn[]) { + if (Array.isArray(value)) { + this._columns = value; + this.subHeader = this.getSubHeaders(this._columns); + } + } + + get columns(): TableColumn[] { + return this._columns; + } + + private _columns: TableColumn[] = []; + // endregion + + // region rows + @Input() set rows(value: any[] | TableRowsGrouped) { + if (Array.isArray(value)) { + this._rows = this.tableService.setRowsMetadata([...value]); + this.initRows(); + } + } + + get rows(): any[] | TableRowsGrouped { + return this._rows; + } + + private _rows: any[] | TableRowsGrouped = []; + // endregion + + // region expandedRows + /** @internal */ + @Input() set expandedRows(value: {[key: string]: boolean}) { + this.onExternalExpandRowChanged(value); + this._expandedRows = value; + } + + get expandedRows(): {[key: string]: boolean} { + return this._expandedRows; + } + + private _expandedRows: {[key: string]: boolean} = {}; + // endregion + + @Input() loading: boolean; + @Input() sortTableOnDataChanges = false; + @Input() hasCustomHeader = false; + @Input() hasCustomFooter = false; + // endregion + + // region outputs + @Output() sortChanged: EventEmitter = new EventEmitter(); + @Output() selectionChanged = this.tableService.selectionChanged; + @Output() rowModelChange = this.tableService.rowModelChange; + @Output() rowClicked = new EventEmitter<{$event: MouseEvent; rowIndex: string; rowEl: Element; rowData: any}>(); + @Output() scrollDown: EventEmitter = new EventEmitter(); + @Output() rowActionClicked: EventEmitter<{action: MenuDropItem; rowIndex: string | number; row: TableRow}> = + this.tableService.rowActionClicked; + @Output() expandRow: EventEmitter = new EventEmitter(); + @Output() expandedRowsChange = new EventEmitter<{[key: string]: boolean}>(); + // endregion + + // region ViewChild + /** @internal */ + @ViewChild('stringCell') stringCell; + /** @internal */ + @ViewChild('checkboxCell') checkboxCell; + /** @internal */ + @ViewChild('templateCell') templateCell; + /** @internal */ + @ViewChild('table') tableElement: ElementRef; + /** @internal */ + @ViewChild('tableWrapper', {static: true}) tableWrapperElement: ElementRef; + /** @internal */ + @ViewChild('tableBody') tableBodyComponent: TableBasicComponent; + // endregion + + // region HostBindings + @HostBinding('class.fixed-table') get isFixedHeader(): boolean { + return !isNullOrUndefined(this.options) && !isNullOrUndefined(this.options.stickyHeader) && this.options.stickyHeader; + } + + @HostBinding('class.fu-no-table-frame') get noTableFrame(): boolean { + return !(!!this.options?.tableLabel || !!this.options?.searchOptions); + } + + @HostBinding('class.fu-no-table-footer') get noTableFooter(): boolean { + return !this.noTableFrame && this.options?.noTableFooter; + } + + @HostBinding('class.is-empty') get isEmpty(): boolean { + return this.tableService.isTableEmpty(this.rows, this.options.isGroupedTable, this.options.hasTotalsRow); + } + + @HostBinding('class.is-loading') get isLoading(): boolean { + return this.loading; + } + + @HostBinding('class.on-scroll-right') isScrollRight: boolean; + + @HostBinding('class.on-vertical-scroll') get onVerticalScroll(): boolean { + if (this.tableWrapperElement) { + return this.tableWrapperElement.nativeElement.scrollTop > 0; + } + } + + // endregion + + // region getters + get isCheckboxTitleShown(): boolean { + return this.columns ? this.columns.some(column => column.type === TableColumnTypeEnum.Checkbox && column.title !== '') : false; + } + + get isLoadingOverlay(): boolean { + return ( + isNullOrUndefined(this.options) || // default - true + isNullOrUndefined(this.options.isLoadingOverlayMode) || // default - true + this.options.isLoadingOverlayMode // get from options + ); + } + + get tableRows(): any[] { + return this.rows as any[]; + } + + get groupedTableRows(): TableRowsGrouped { + return this.rows as TableRowsGrouped; + } + + get colsCount(): number { + let columnsCount = Array.isArray(this.columns) ? this.columns.length : 1; + if (!!this.options && this.options.rowsExpandableOptions) { + columnsCount += !!this.options.rowsExpandableOptions.expandLevels ? this.options.rowsExpandableOptions.expandLevels : 1; + } + return columnsCount; + } + + get scrollElement(): HTMLElement { + const scrollElement = this.tableWrapperElement.nativeElement; + if (this.options.scrollElementSelector) { + return document.querySelector(this.options.scrollElementSelector) || scrollElement; + } + return scrollElement; + } + + // endregion + + // region private props + private lastScrollLeftValue: number; + private currentExpandedMap: {[key: string]: boolean} = {}; + private ignoredParentSelectorsRowClickEvent: string[]; + private onDestroy$ = new Subject(); + + // endregion + + ngOnInit(): void { + this.tableService.clearSelectedRows(); + this.searchFormControl = new FormControl(this.options?.searchOptions?.initalValue || ''); + if (!!this.options.rowsExpandableOptions) { + try { + this.tableService.setExpandLevelByExpandOptions(this.options.rowsExpandableOptions); + } catch (e) { + this.tableMainError = true; + throw new Error(e); + } + } + this.options.tableId = this.id; + this.noDataMessage = isNullOrUndefined(this.options.noDataMessage) ? 'No Data to Display' : this.options.noDataMessage; + this.noDataSubMessage = this.options.noDataSubMessage ?? ''; + this.hideHeaderOnEmpty = !isNullOrUndefined(this.options.hideHeaderOnEmpty) ? this.options.hideHeaderOnEmpty : false; + this.isAllRowsSelectable = typeof this.options.isAllRowsSelectable === 'undefined' ? true : this.options.isAllRowsSelectable; + this.scrollListeners(); + this.configIconNames = CONFIG_TABLE_BY_UI_STYLE[`style_v2`]; + this.wrapperClasses = this.getWrapperClasses(); + this.initColumns(); + + this.ignoredParentSelectorsRowClickEvent = ROW_CLICK_SUPPRESS_FOR_PARENT_SELECTORS.concat( + this.options.rowsOptions?.ignoredParentSelectorsRowClickEvent ?? [] + ); + + if (this.sortTableOnDataChanges && this.columns.find(col => !!col.sort)) { + this.doLocalSorting(); + } + + this.searchFormControl.valueChanges.pipe(takeUntil(this.onDestroy$), debounceTime(500)).subscribe(value => { + this.options?.searchOptions.onSearch.emit(value); + }); + } + + ngOnDestroy(): void { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + ngAfterViewInit() { + const columns = this.tableElement.nativeElement.querySelectorAll('thead tr td'); + columns.forEach((column: HTMLElement, index: number) => { + if (!column.style.width && column?.dataset?.editable === 'true') { + column.style.width = `${column.clientWidth}px`; + } + }); + } + + /** @internal */ + onHeaderClicked(col: any): void { + if (!this.tableService.isColumnSortable(col)) { + return; + } + const sortKey: string = col.key; + if (!(this.options && this.options.sortingType && this.options.sortingType === 'external')) { + this.localSorting(sortKey); + } + this.sortChanged.emit(sortKey); + } + + /** @internal */ + filterColumn(column, filterIn) { + if (column.filter.changed && column.filter.options) { + const isAllFiltered = column.filter.options.length === filterIn.length || filterIn.length === 0; + column.filter.changed.emit(isAllFiltered ? [] : filterIn); + } + } + + /** @internal */ + replaceSelectedRows({selectedTableRows, iditicationFunc}: {selectedTableRows: any[]; iditicationFunc: (row: any) => number}): void { + this.tableService.replaceSelectedRows({selectedTableRows, iditicationFunc}); + } + + /** @internal */ + doExpandRow({rowIndex, row, isExpanded, successCallback, failedCallback, updateMap}: TableRowExpandEmitter) { + if (!!this.expandRow.observers.length) { + // has expandRow event subscription in host + if (Array.isArray(this.rows)) { + const innerEntityType = this.options.rowsExpandableOptions?.innerEntityType ?? 'innerRows'; + this.expandRow.emit({rowIndex, row, isExpanded, successCallback, failedCallback, updateMap, innerEntityType}); + this.currentExpandedMap = {...this.currentExpandedMap, [rowIndex]: isExpanded}; + } + } else { + successCallback(); + this.updateExpandedRowsMap(rowIndex, isExpanded); + } + } + + /** @internal */ + trackByFunc(index, column) { + return column && column.key ? column.key : index; + } + + /** @internal */ + getTableClientWidth(): number { + if (this.tableWrapperElement) { + return this.tableWrapperElement.nativeElement.clientWidth; + } + } + + /** @internal */ + onTableBodyClicked($event: MouseEvent) { + if (!this.isElementChildOfSuppressed($event.target as Element)) { + const rowEl = ($event.target as Element).closest('tr'); + if (!isNullOrUndefined(rowEl)) { + const rowIndex = rowEl.dataset.rowIdx; + const rowData = this.rows[rowIndex]; + this.rowClicked.emit({$event, rowIndex, rowEl, rowData}); + } + } + } + + /** @internal */ + onClickReturnTop() { + const viewPortElement = this.scrollElement || document.documentElement; + const currentScroll = viewPortElement.scrollTop || document.body.scrollTop; + if (currentScroll > 0) { + (function smoothScroll() { + let currentScroll = viewPortElement.scrollTop || document.body.scrollTop; + if (currentScroll > 0) { + window.requestAnimationFrame(smoothScroll); + viewPortElement.scrollTo(0, currentScroll - currentScroll / 8); + } + })(); + } + } + + private initColumns() { + if (this.options.rowActionsMenu?.stickyActionButton) { + this._columns = [...this.columns, {key: 'row_actions_column', title: '', width: '52px'}]; + } + } + + private initRows() { + if (!this.options?.isGroupedTable && (this.rows as any[])?.length) { + this.tableService.initSelectedRows(this.rows as any[]); + } + this.doLocalSorting(); + + // check for rowspan columns + this.tableService.setRowspanColumnsData( + this.rows as [], + this._columns.map(col => col.key) + ); + } + + private getSubHeaders(columns: TableColumn[]): {name: string; colspan: number}[] { + if (columns.some(item => !!item.groupName)) { + return columns.reduce((groups, column, idx, columns) => { + if (column.groupName) { + groups.push({name: column.groupName ?? ' ', colspan: 1}); + } else { + if (groups[groups.length - 1] && groups[groups.length - 1].name) { + groups[groups.length - 1].colspan++; + } else { + groups.push({name: ' ', colspan: 1}); + } + } + return groups; + }, []); + } else { + return []; + } + } + + private doLocalSorting() { + if (Array.isArray(this.rows) && this.columns && this.sortTableOnDataChanges) { + const sortedColumn = this.columns.find(col => !!col.sort); + if (sortedColumn) { + sortedColumn.sort = sortedColumn.sort === 'asc' ? 'desc' : 'asc'; + this.localSorting(sortedColumn.key); + } + } + } + + private isElementChildOfSuppressed(element: Element): boolean { + return this.ignoredParentSelectorsRowClickEvent.some((selector: string) => { + return element.closest(selector); + }); + } + + private updateExpandedRowsMap(rowIndex: string | number, isExpanded: boolean): void { + this._expandedRows = {...this._expandedRows, [rowIndex]: isExpanded}; + this.expandedRowsChange.emit(this._expandedRows); + } + + private onExternalExpandRowChanged(newValue: {[key: string]: boolean}) { + const diffMap = this.getRowsToExpandToggle(this.currentExpandedMap, newValue); + this.currentExpandedMap = newValue; + + if (diffMap.includes('default')) { + const rowsInTable = (this.rows as any[]).length; + [...Array(rowsInTable).keys()].forEach(rowIndex => { + this.callOnExpandRow({ + rowIndex: rowIndex, + row: this.rows[rowIndex], + isExpanded: newValue['default'] + }); + }); + } else { + diffMap.forEach(rowIndex => { + this.callOnExpandRow({ + rowIndex: parseInt(rowIndex, 10), + row: this.rows[rowIndex], + isExpanded: newValue[rowIndex] + }); + }); + } + } + + private callOnExpandRow({rowIndex, row, isExpanded}) { + this.tableBodyComponent.onExpandRow({rowIndex, row, isExpanded}, false); + } + + private getRowsToExpandToggle( + curValue: {[key: string]: boolean}, + newValue: { + [key: string]: boolean; + } + ): string[] { + return Object.keys(newValue) + .map(key => { + if (newValue[key] !== curValue[key]) { + return key; + } + return null; + }) + .filter(Boolean); + } + + private getWrapperClasses(): string[] { + const classes: string[] = []; + if (!!this.options && !!this.options.rowHeight) { + classes.push(`is-row-height-${this.options.rowHeight}`); + } + if (this.options?.stickyHeader && this.options?.scrollElementSelector) { + classes.push(`fu-stocky-to-external`); + } + if (this.options.stickyHeader || this.options?.pagination?.enable) { + classes.push(`fu-table-sticky-header`); + } + return classes; + } + + private scrollListeners(): void { + defer(() => + fromEvent(this.scrollElement, 'scroll').pipe( + takeUntil(this.onDestroy$), + tap(_ => { + const scrollLeft = this.scrollElement.scrollLeft; + if (this.lastScrollLeftValue !== scrollLeft) { + this.isScrollRight = scrollLeft > 0; + this.lastScrollLeftValue = scrollLeft; + } + }), + debounceTime(10) + ) + ).subscribe(this.onScroll.bind(this)); + } + + private localSorting(sortKey: string): void { + let isAscSort: boolean; + const tableRows = [...(this.rows as any[])]; + // reset header sort options + this.columns.forEach(col => { + if (col.key !== sortKey) { + if (!isUndefined(col.sort)) { + col.sort = ''; + } + } else { + col.sort = col.sort === '' ? 'asc' : col.sort === 'asc' ? 'desc' : 'asc'; + isAscSort = col.sort === 'asc'; + } + }); + + let totalRow = []; + let otherRows = []; + if (!isNullOrUndefined(this.options.hasTotalsRow) && this.options.hasTotalsRow) { + totalRow = tableRows.slice(0, 1); + otherRows = tableRows.slice(1); + } else { + otherRows = tableRows; + } + otherRows.sort((a: any, b: any): number => { + if (isNullOrUndefined(a[sortKey]) || isNullOrUndefined(b[sortKey])) { + return 0; + } + + // if data type - numeric + if (!Array.isArray(a[sortKey]) && !isNaN(a[sortKey]) && !isNaN(b[sortKey]) && a[sortKey] - parseFloat(a[sortKey]) + 1 >= 0) { + return isAscSort ? a[sortKey] - b[sortKey] : (a[sortKey] - b[sortKey]) * -1; + } + + // if it string; + const strA: string = a[sortKey].toString().toUpperCase(); + const strB: string = b[sortKey].toString().toUpperCase(); + + if (strA < strB) { + return isAscSort ? -1 : 1; + } + if (strA > strB) { + return isAscSort ? 1 : -1; + } + return 0; + }); + + this._rows = [...totalRow, ...otherRows].filter(Boolean); + } + + private onScroll($event) { + this.tableService.tableScrolled.emit($event); + + if (this.options.hasReturnToTopButton) { + this.shownGoTopButton$.next(this.scrollElement.scrollTop > this.tableElement.nativeElement.offsetTop); + } + + const target = $event.target || $event; + if (!this.options.pagination || this.options.pagination.loading || !this.options.pagination.enable) { + return; + } + + const top = this.scrollElement.scrollTop; + if (top >= this.tableElement.nativeElement.offsetHeight - this.scrollElement.offsetHeight - 100) { + if (!this.options.pagination.handleLoadingFromHost) { + this.options.pagination.loading = true; + } + this.scrollDown.emit(target); + } + } +} diff --git a/projects/fusion-ui/components/text-with-dropped-list/index.ts b/projects/fusion-ui/components/text-with-dropped-list/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/text-with-dropped-list/ng-package.json b/projects/fusion-ui/components/text-with-dropped-list/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/text-with-dropped-list/public-api.ts b/projects/fusion-ui/components/text-with-dropped-list/public-api.ts new file mode 100644 index 000000000..a86af1af2 --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/public-api.ts @@ -0,0 +1 @@ +export * from './v4'; diff --git a/projects/fusion-ui/components/text-with-dropped-list/v4/index.ts b/projects/fusion-ui/components/text-with-dropped-list/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/text-with-dropped-list/v4/ng-package.json b/projects/fusion-ui/components/text-with-dropped-list/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/text-with-dropped-list/v4/public-api.ts b/projects/fusion-ui/components/text-with-dropped-list/v4/public-api.ts new file mode 100644 index 000000000..a21157442 --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/v4/public-api.ts @@ -0,0 +1 @@ +export * from './text-with-dropped-list.component'; diff --git a/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.html b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.html new file mode 100644 index 000000000..cc6a5dcd5 --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.html @@ -0,0 +1,7 @@ +
    {{text}}
    +@if (showedList$ | async){ + +} diff --git a/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.scss b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.scss new file mode 100644 index 000000000..ca437627d --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.scss @@ -0,0 +1,46 @@ +@import '../../../src/style/scss/v4/vars/vars'; +@import '../../../src/style/scss/v4/mixins/mixins'; + +:host { + @extend %reset; + + --fu-text-color: var(--text-primary, #{$color-v4-text-primary}); + --fu-text-disabled-color: var(--text-disabled, #{$color-v4-text-disabled}); + --fu-text-border-width: 1px; + --fu-text-border-type: dashed; + --fu-text-border-color: var(--text-secondary, #{$color-v4-text-secondary}); + --fu-text-disabled-border-color: var(--text-disabled, #{$color-v4-text-disabled}); + + display: flex; + width: fit-content; + align-items: center; + position: relative; + + .fu-text{ + @extend %font-v4-body-1; + color: var(--fu-text-color); + border-bottom: var(--fu-text-border-width) var(--fu-text-border-type) var(--fu-text-border-color); + cursor: default; + } + + &.fu-small{ + .fu-text{ + @extend %font-v4-body-2; + } + } + &.fu-disabled{ + pointer-events: none; + .fu-text{ + pointer-events: none; + color: var(--fu-text-disabled-color); + border-bottom-color: var(--fu-text-disabled-border-color); + } + } + + .fu-dropped-list{ + position: absolute; + top: calc(100% + 4px); + right: var(--fu-dropped-list-right); + z-index: getZIndexLayerOffset(notification, 3); + } +} \ No newline at end of file diff --git a/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.spec.ts b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.spec.ts new file mode 100644 index 000000000..62caca660 --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.spec.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TextWithDroppedListComponent } from './text-with-dropped-list.component'; +import {BASE_LIST_OPTIONS} from "@ironsource/fusion-ui/components/dropped-list/v4/dropped-list.mock"; + +const TEXT = 'Test text'; + +describe('TextWithDroppedListComponent', () => { + let component: TextWithDroppedListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TextWithDroppedListComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TextWithDroppedListComponent); + component = fixture.componentInstance; + component.text = TEXT; + component.list = BASE_LIST_OPTIONS + component.size = 'small'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have text', () => { + const textEl = fixture.nativeElement.querySelector('.fu-text'); + expect(textEl).toBeTruthy(); + expect(textEl.textContent).toBe(TEXT); + }); + + it('should have class small', () => { + expect(fixture.nativeElement.classList).toContain('fu-small') + }); + + it('should have list on mouseenter', () => { + fixture.nativeElement.dispatchEvent(new Event('mouseenter')); + fixture.detectChanges(); + const droppedListEl = fixture.nativeElement.querySelector('.fu-dropped-list'); + expect(droppedListEl).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.stories.ts b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.stories.ts new file mode 100644 index 000000000..b4c1d34f5 --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.stories.ts @@ -0,0 +1,65 @@ +import {componentWrapperDecorator, Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {TextWithDroppedListComponent} from './text-with-dropped-list.component'; +import { + APPLICATION_LIST_OPTIONS, + BASE_LIST_OPTIONS, + COUNTRY_LIST_OPTIONS +} from '@ironsource/fusion-ui/components/dropped-list/v4/dropped-list.mock'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; + +export default { + title: 'V4/Components/DataDisplay/Text with dropped list', + component: TextWithDroppedListComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, SvgModule.forRoot({assetsPath: environment.assetsPath})] + }), + componentWrapperDecorator(story => `
    ${story}
    `) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + text: 'Text with help border', + list: BASE_LIST_OPTIONS + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const Size: Story = { + render: args => ({ + props: { + ...args, + appList: APPLICATION_LIST_OPTIONS, + countryList: COUNTRY_LIST_OPTIONS + }, + template: ` +
    + + +
    +` + }) +}; + +export const Disabled: Story = { + render: args => ({ + props: {...args}, + template: ` +
    + + +
    +` + }) +}; diff --git a/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.ts b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.ts new file mode 100644 index 000000000..e040c18c3 --- /dev/null +++ b/projects/fusion-ui/components/text-with-dropped-list/v4/text-with-dropped-list.component.ts @@ -0,0 +1,71 @@ +import {ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {BehaviorSubject, from, Subject} from 'rxjs'; +import {debounceTime, filter, switchMap, takeUntil, tap} from 'rxjs/operators'; +import {computePosition, flip, shift} from '@floating-ui/dom'; +import {RepositionDirective} from '@ironsource/fusion-ui/directives/reposition'; +import {TeleportingModule} from '@ironsource/fusion-ui/directives/teleporting'; +import {DroppedListComponent, DroppedListOption} from '@ironsource/fusion-ui/components/dropped-list/v4'; + +@Component({ + selector: 'fusion-text-with-dropped-list', + standalone: true, + host: { + class: 'fusion-v4', + '[class.fu-disabled]': 'disabled', + '[class.fu-small]': 'size === "small"', + '(mouseenter)': 'showedList$.next(true)', + '(mouseleave)': 'showedList$.next(false)' + }, + imports: [CommonModule, TeleportingModule, RepositionDirective, DroppedListComponent], + templateUrl: './text-with-dropped-list.component.html', + styleUrl: './text-with-dropped-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class TextWithDroppedListComponent implements OnInit, OnDestroy { + @Input() size: 'small' | 'medium' = 'medium'; + @Input() text: string; + @Input() disabled = false; + @Input() list: DroppedListOption[] = []; + + /** @ignore */ + showedList$ = new BehaviorSubject(false); + + #onDestroy$ = new Subject(); + #hostElement: HTMLElement = this.hostElementRef.nativeElement; + #textLabel: HTMLElement; + #droppedList: HTMLElement; + + constructor(private hostElementRef: ElementRef) {} + + ngOnInit() { + this.showedList$ + .asObservable() + .pipe( + takeUntil(this.#onDestroy$), + filter(isShow => isShow && !this.list.length), + debounceTime(0), + tap(() => { + this.#textLabel = this.#hostElement.querySelector(`.fu-text`); + this.#droppedList = this.#hostElement.querySelector(`fusion-dropped-list`); + }), + switchMap(() => + from( + computePosition(this.#textLabel, this.#droppedList, { + placement: 'bottom', + middleware: [flip(), shift({padding: 5})] + }) + ) + ) + ) + .subscribe(({x, y}) => { + this.#droppedList.style.left = x + 'px'; + this.#droppedList.style.top = y + 'px'; + }); + } + + ngOnDestroy(): void { + this.#onDestroy$.next(); + this.#onDestroy$.complete(); + } +} diff --git a/projects/fusion-ui/components/toggle/v4/index.ts b/projects/fusion-ui/components/toggle/v4/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/components/toggle/v4/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/components/toggle/v4/ng-package.json b/projects/fusion-ui/components/toggle/v4/ng-package.json new file mode 100644 index 000000000..d9b2030ce --- /dev/null +++ b/projects/fusion-ui/components/toggle/v4/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/components/toggle/v4/public-api.ts b/projects/fusion-ui/components/toggle/v4/public-api.ts new file mode 100644 index 000000000..938b6daac --- /dev/null +++ b/projects/fusion-ui/components/toggle/v4/public-api.ts @@ -0,0 +1 @@ +export {ToggleV4Component as ToggleComponent} from './toggle-v4.component'; diff --git a/projects/fusion-ui/components/toggle/v4/toggle-v4.component.html b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.html new file mode 100644 index 000000000..fe48e619d --- /dev/null +++ b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.html @@ -0,0 +1,52 @@ +
    + +
    + @if (startIcon) { + + } + @if (labelText) { + {{ labelText }} + @if (endIcon) { + + } + @if (labelHelpIcon) { + + } + } +
    +
    +@if (helperText) { + +} + diff --git a/projects/fusion-ui/components/toggle/v4/toggle-v4.component.scss b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.scss new file mode 100644 index 000000000..3b7ac32ba --- /dev/null +++ b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.scss @@ -0,0 +1,260 @@ +@import "../../../src/style/scss/v4/vars/vars"; +@import "../../../src/style/scss/v4/mixins/mixins"; + +:host { + @extend %reset; + display: inline-flex; + flex-direction: column; + gap: 12px; + + // region Variables + --slider-color: var(--action-hover, #{$color-v4-action-hover}); + --slider-borer-color: var(--action-outlined-border, #{$color-v4-action-outlined-border}); + --slider-dot-color: var(--action-active, #{$color-v4-action-active}); + + --slider-hover-color: var(--action-hover, #{$color-v4-action-hover}); + --slider-hover-borer-color: var(--action-active, #{$color-v4-action-active}); + + --slider-disabled-color: var(--action-disabled-background, #{$color-v4-action-disabled-background}); + --slider-disabled-borer-color: var(--action-outlined-border, #{$color-v4-action-outlined-border}); + --slider-disabled-dot-color: var(--action-disabled, #{$color-v4-action-disabled}); + + --slider-checked-color: var(--primary-main, #{$color-v4-primary-main}); + --slider-checked-border-color: var(--primary-dark, #{$color-v4-primary-dark}); + --slider-checked-dot-color: var(--common-white, #{$color-v4-common-white}); + + --slider-hover-checked-color: var(--primary-main-50-p, #{$color-v4-primary-main-50-p}); + --slider-hover-checked-border-color: var(--primary-main-50-p, #{$color-v4-primary-main-50-p}); + + --slider-disabled-checked-color: var(--primary-main-8-p, #{$color-v4-primary-main-8-p}); + --slider-disabled-checked-border-color: var(--primary-main-50-p, #{$color-v4-primary-main-50-p}); + --slider-disabled-checked-dot-color: var(--primary-main-50-p, #{$color-v4-primary-main-50-p}); + + --slider-test-color: var(--warning-main, #{$color-v4-warning-main}); + --slider-test-border-color: var(--warning-dark, #{$color-v4-warning-dark}); + + --slider-hover-test-color: var(--warning-main-50-p, #{$color-v4-warning-main-50-p}); + --slider-hover-test-border-color: var(--warning-dark, #{$color-v4-warning-dark}); + + --slider-disabled-test-color: var(--warning-main-8-p, #{$color-v4-warning-main-8-p}); + --slider-disabled-test-border-color: var(--warning-main-50-p, #{$color-v4-warning-main-50-p}); + --slider-disabled-test-dot-color: var(--warning-main-50-p, #{$color-v4-warning-main-50-p}); + + --slider-loader-color: var(--action-active, #{$color-v4-action-active}); + --slider-loader-checked-color: var(--primary-dartker, #{$color-v4-primary-darker}); + --slider-loader-checked-test-color: var(--warning-darker, #{$color-v4-warning-darker}); + + --slider-width: 28px; + --slider-height: 16px; + --slider-medium-width: 36px; + --slider-medium-height: 20px; + + --slioder-dot-size: 12px; + --slioder-loader-size: 8px; + --slioder-medium-dot-size: 16px; + --slioder-medium-loader-size: 12px; + + // endregion + + @keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + + .fu-toggle-wrapper { + display: flex; + gap: var(--toggle-inner-gap, #{$spacingV4-150}); + align-items: center; + + label { + .fu-toggle-slider { + background-color: var(--slider-color); + width: var(--slider-width); + height: var(--slider-height); + border-radius: 100px; + border: 1px solid var(--slider-borer-color); + position: relative; + @include trn(background-color); + + .fu-toggle-slider-dot { + display: flex; + align-items: center; + justify-content: center; + @include size(var(--slioder-dot-size)); + border-radius: var(--slioder-dot-size); + background-color: var(--slider-dot-color); + position: absolute; + top: 1px; + left: 1px; + @include trn(left); + } + + &:hover { + cursor: pointer; + background-color: var(--slider-hover-color); + border: 1px solid var(--slider-hover-borer-color); + } + } + + input[type="checkbox"] { + display: none; + } + + input[type="checkbox"]:checked { + & + .fu-toggle-slider { + background-color: var(--slider-checked-color); + border-color: var(--slider-checked-border-color); + + .fu-toggle-slider-dot { + left: 13px; + background-color: var(--slider-checked-dot-color); + + .fu-toggle-loader { + color: var(--slider-loader-checked-color); + } + } + + &:hover { + background-color: var(--slider-hover-checked-color); + border-color: var(--slider-hover-checked-border-color); + } + } + } + + input[type="checkbox"]:disabled { + & ~ .fu-toggle-slider { + background-color: var(--slider-disabled-color); + border-color: var(--slider-disabled-borer-color); + pointer-events: unset; + + &:hover { + cursor: default; + } + + .fu-toggle-slider-dot { + background-color: var(--slider-disabled-dot-color); + } + } + } + + input[type="checkbox"]:disabled:checked { + & ~ .fu-toggle-slider { + background-color: var(--slider-disabled-checked-color); + border-color: var(--slider-disabled-checked-border-color); + + .fu-toggle-slider-dot { + background-color: var(--slider-disabled-checked-dot-color); + } + } + } + + // region color test + &.fu-toggle-color-test { + input[type="checkbox"]:checked { + & + .fu-toggle-slider { + background-color: var(--slider-test-color); + border-color: var(--slider-test-border-color); + + .fu-toggle-slider-dot { + left: 13px; + background-color: var(--slider-checked-dot-color); + + .fu-toggle-loader { + color: var(--slider-loader-checked-test-color); + } + } + + &:hover { + background-color: var(--slider-hover-test-color); + border-color: var(--slider-hover-test-border-color); + } + } + } + + input[type="checkbox"]:disabled:checked { + & ~ .fu-toggle-slider { + background-color: var(--slider-disabled-test-color); + border-color: var(--slider-disabled-test-border-color); + + .fu-toggle-slider-dot { + background-color: var(--slider-disabled-test-dot-color); + } + } + } + } + + // endregion + + // region size medium + &.fu-toggle-size-medium { + .fu-toggle-slider { + width: var(--slider-medium-width); + height: var(--slider-medium-height); + + .fu-toggle-slider-dot { + @include size(var(--slioder-medium-dot-size)); + + .fu-toggle-loader { + @include size(var(--slioder-medium-loader-size)); + } + } + } + + input[type="checkbox"]:checked { + & + .fu-toggle-slider { + .fu-toggle-slider-dot { + left: 17px; + } + } + } + } + + // endregion + } + + // region Loader + .fu-toggle-loader { + display: flex; + align-items: center; + justify-content: center; + @include size(var(--slioder-loader-size)); + animation: rotation 1s linear infinite; + color: var(--slider-loader-color); + opacity: 0; + } + + &.fu-toggle-loading .fu-toggle-loader { + opacity: 1; + } + + // endregion + + // region label + .fu-toggle-start-icon, + .fu-toggle-end-icon, + .fu-toggle-label-icon { + display: flex; + align-items: center; + justify-content: center; + @include size(20px); + color: var(--action-active, #{$color-v4-action-active}); + } + + .fu-toggle-label { + display: flex; + align-items: center; + gap: var(--toggle-label-gap, #{$spacingV4-50}); + + .fu-toggle-label-text { + @extend %font-v4-body-1; + color: var(--text-primary, #{$color-v4-text-primary}); + } + } + + // endregion + } +} diff --git a/projects/fusion-ui/components/toggle/v4/toggle-v4.component.spec.ts b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.spec.ts new file mode 100644 index 000000000..0a4c70095 --- /dev/null +++ b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.spec.ts @@ -0,0 +1,197 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Injectable} from "@angular/core"; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {ToggleV4Component} from './toggle-v4.component'; +import {UniqueIdService} from '@ironsource/fusion-ui'; + +@Injectable() +class MockUniqueIdService extends UniqueIdService { + getUniqueId() { + return 987654321; + } +} + +const HELPER_TEXT = 'Helper text'; +const LABEL_TEXT = 'Label text'; +const TOOLTIP_TEXT = 'Tooltip text'; +const ICON_NAME = 'icon-name'; + +describe('ToggleV4Component', () => { + let component: ToggleV4Component; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ToggleV4Component, FormsModule, ReactiveFormsModule], + providers: [{provide: UniqueIdService, useClass: MockUniqueIdService}] + }).compileComponents(); + + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have label and input type:checkbox', () => { + expect(fixture.nativeElement.querySelector('label')).toBeTruthy(); + expect(fixture.nativeElement.querySelector('label input[type="checkbox"]')).toBeTruthy(); + }); + + it('by default label should have class "size-small"', () => { + const inputEl = fixture.nativeElement.querySelector('label.fu-toggle-size-small'); + expect(inputEl).toBeTruthy(); + }); + + it('by default label should have class "color-primary"', () => { + const inputEl = fixture.nativeElement.querySelector('label.fu-toggle-color-primary'); + expect(inputEl).toBeTruthy(); + }); + + it('input should have id', () => { + const inputEl = fixture.nativeElement.querySelector('label input[type="checkbox"]'); + expect(inputEl).toBeTruthy(); + expect(inputEl.id).toBe('fuToggle_987654321'); + }); + + it('should not be "checked" ', () => { + const inputEl = fixture.nativeElement.querySelector('label input[type="checkbox"]'); + expect(inputEl).toBeTruthy(); + expect(inputEl.checked).toBeFalse(); + }); + + it('should be "checked" if formControl value is true', () => { + const formControl = new FormControl(true) + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.writeValue(formControl.value); + fixture.detectChanges(); + const inputEl = fixture.nativeElement.querySelector('label input[type="checkbox"]'); + expect(inputEl).toBeTruthy(); + expect(inputEl.checked).toBeTruthy(); + }); + + it('should be "disabled" if formControl disabled', () => { + const formControl = new FormControl({value: true, disabled: true}) + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.writeValue(formControl.value); + component.setDisabledState(formControl.disabled); + fixture.detectChanges(); + const inputEl = fixture.nativeElement.querySelector('label input[type="checkbox"]'); + expect(inputEl).toBeTruthy(); + expect(inputEl.checked).toBeTruthy(); + expect(inputEl.disabled).toBeTruthy(); + }); + + it('should be "disabled" if input disabled set true', () => { + const formControl = new FormControl(true) + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.writeValue(formControl.value); + component.disabled = true; + fixture.detectChanges(); + const inputEl = fixture.nativeElement.querySelector('label input[type="checkbox"]'); + expect(inputEl).toBeTruthy(); + expect(inputEl.checked).toBeTruthy(); + expect(inputEl.disabled).toBeTruthy(); + }); + + + it('should have loading class if set loading', () => { + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.loading = true; + fixture.detectChanges(); + const wrapperEl = fixture.nativeElement.querySelector('div.fu-toggle-wrapper.fu-toggle-loading'); + expect(wrapperEl).toBeTruthy(); + }); + + it('should have "color-test" class if set color "test"', () => { + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.color = 'test'; + fixture.detectChanges(); + + const wrapperEl = fixture.nativeElement.querySelector('label.fu-toggle-holder.fu-toggle-color-test'); + expect(wrapperEl).toBeTruthy(); + }); + + it('should have "size-medium" class if set size "medium"', () => { + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.size = 'medium'; + fixture.detectChanges(); + + const wrapperEl = fixture.nativeElement.querySelector('label.fu-toggle-holder.fu-toggle-size-medium'); + expect(wrapperEl).toBeTruthy(); + }); + + + it('should have helper component if set helper', () => { + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.helperText = HELPER_TEXT; + fixture.detectChanges(); + + const helperEl = fixture.nativeElement.querySelector('fusion-input-helper'); + expect(helperEl).toBeTruthy(); + expect(helperEl.textContent).toBe(HELPER_TEXT); + }); + + + it('should have label text if set label text', () => { + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.labelText = LABEL_TEXT; + fixture.detectChanges(); + + const labelEl = fixture.nativeElement.querySelector('.fu-toggle-label .fu-toggle-label-text'); + expect(labelEl).toBeTruthy(); + expect(labelEl.textContent).toBe(LABEL_TEXT); + }); + + it('should have start icon if it set', () => { + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.startIcon = ICON_NAME; + fixture.detectChanges(); + + const iconEl = fixture.nativeElement.querySelector('fusion-icon.fu-toggle-start-icon'); + expect(iconEl).toBeTruthy(); + expect(iconEl.getAttribute('ng-reflect-name')).toBe(ICON_NAME); + expect(iconEl.classList).toContain(ICON_NAME); + + }); + + it('should have end icon if it set - labelText must be set', () => { + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.labelText = LABEL_TEXT + component.endIcon = ICON_NAME; + fixture.detectChanges(); + + const iconEl = fixture.nativeElement.querySelector('fusion-icon.fu-toggle-end-icon'); + expect(iconEl).toBeTruthy(); + expect(iconEl.getAttribute('ng-reflect-name')).toBe(ICON_NAME); + expect(iconEl.classList).toContain(ICON_NAME); + }); + + it('should have help icon with tooltip if it set - labelText must be set', () => { + fixture = TestBed.createComponent(ToggleV4Component); + component = fixture.componentInstance; + component.labelText = LABEL_TEXT + component.labelHelpIcon = ICON_NAME; + component.labelTooltipText = TOOLTIP_TEXT; + fixture.detectChanges(); + + const iconEl = fixture.nativeElement.querySelector('fusion-icon.fu-toggle-label-icon'); + + expect(iconEl).toBeTruthy(); + expect(iconEl.getAttribute('ng-reflect-fusion-tooltip')).toBe(TOOLTIP_TEXT); + }); + + +}); diff --git a/projects/fusion-ui/components/toggle/v4/toggle-v4.component.stories.ts b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.stories.ts new file mode 100644 index 000000000..1091f3c9d --- /dev/null +++ b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.stories.ts @@ -0,0 +1,132 @@ +import {Meta, moduleMetadata, StoryObj} from '@storybook/angular'; +import {CommonModule} from '@angular/common'; +import {ToggleV4Component as ToggleComponent} from './toggle-v4.component'; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {SvgModule} from '@ironsource/fusion-ui/components/svg'; +import {environment} from '../../../../../stories/environments/environment'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; + +const formControlChecked = new FormControl(true); +const formControlCheckedDisabled = new FormControl({value: true, disabled: true}); +const formControlUnchecked = new FormControl(false); + +export default { + title: 'V4/Components/Inputs/Switch (toggle)', + component: ToggleComponent, + decorators: [ + moduleMetadata({ + declarations: [], + imports: [CommonModule, FormsModule, ReactiveFormsModule, SvgModule.forRoot({assetsPath: environment.assetsPath}), IconModule] + }) + ], + tags: ['autodocs'], + parameters: { + options: { + showPanel: true, + panelPosition: 'bottom' + } + }, + args: { + formControl: formControlUnchecked, + loading: false, + disabled: false, + color: 'primary', + size: 'small' + }, + argTypes: { + formControl: { + control: false + } + } +} as Meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: args => ({ + props: {...args}, + template: ` +` + }) +}; + +export const Color: Story = { + render: args => ({ + props: { + ...args, + formControlChecked: formControlChecked, + formControlCheckedDisabled: formControlCheckedDisabled + }, + template: `
    + + + +
    ` + }) +}; + +export const Loading: Story = { + render: args => ({ + props: {...args, formControlChecked: formControlChecked}, + template: `` + }) +}; + +export const Size: Story = { + render: args => ({ + props: { + ...args, + formControlChecked: formControlChecked, + formControlCheckedDisabled: formControlCheckedDisabled + }, + template: `
    + + +
    ` + }) +}; + +export const FullyLoaded: Story = { + render: args => ({ + props: { + ...args, + formControlChecked: formControlChecked, + labelText: 'Item name', + startIcon: 'ph/placeholder', + endIcon: 'ph/placeholder', + labelHelpIcon: 'ph/fill/question', + labelTooltipText: 'Tooltip text', + helperText: 'Helper text' + }, + template: ` +` + }) +}; diff --git a/projects/fusion-ui/components/toggle/v4/toggle-v4.component.ts b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.ts new file mode 100644 index 000000000..f77b6fefc --- /dev/null +++ b/projects/fusion-ui/components/toggle/v4/toggle-v4.component.ts @@ -0,0 +1,149 @@ +import {ChangeDetectionStrategy, Component, EventEmitter, forwardRef, inject, Input, Output} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule} from '@angular/forms'; +import {BehaviorSubject} from 'rxjs'; +import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; +import {IconData} from '@ironsource/fusion-ui/components/icon/common/entities'; +import {tooltipConfiguration} from '@ironsource/fusion-ui/components/tooltip'; +import {TooltipDirective} from '@ironsource/fusion-ui/components/tooltip/v4'; +import {TestIdsService} from '@ironsource/fusion-ui/services/test-ids'; +import {UniqueIdService} from '@ironsource/fusion-ui/services/unique-id'; +import {ToggleTestIdModifiers} from '@ironsource/fusion-ui/entities'; +import {InputHelperComponent} from '@ironsource/fusion-ui/components/input-helper/v4'; +import {InputVariant} from '@ironsource/fusion-ui/components/input/v4'; +import {GenericPipe} from '@ironsource/fusion-ui/pipes/generic'; + +@Component({ + selector: 'fusion-toggle', + standalone: true, + host: {class: 'fusion-v4'}, + imports: [CommonModule, FormsModule, ReactiveFormsModule, IconModule, TooltipDirective, InputHelperComponent, GenericPipe], + templateUrl: './toggle-v4.component.html', + styleUrl: './toggle-v4.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ToggleV4Component), + multi: true + } + ] +}) +export class ToggleV4Component { + private uniqueIdService: UniqueIdService = inject(UniqueIdService); + + @Input() id: string = `fuToggle_${this.uniqueIdService.getUniqueId()}`; + + // region label + @Input() labelText?: string; + @Input() labelHelpIcon?: IconData; + @Input() labelTooltipText?: string; + @Input() labelTooltipConfiguration?: tooltipConfiguration; + // endregion + + // region Helper + @Input() helperText: string; + @Input() helperIcon: string; + @Input() helperVariant: InputVariant = 'default'; + // endregion + + // region icons + @Input() startIcon?: IconData; + @Input() endIcon?: IconData; + // endregion + + // region variants and state + @Input() color: 'primary' | 'test' = 'primary'; + @Input() size: 'small' | 'medium' = 'small'; + @Input() set loading(value: boolean) { + this.loading$.next(value); + } + @Input() set disabled(value: boolean) { + this.disabled$.next(value); + } + // endregion + + // region model in case work with component as model, not as form control + @Input() set model(value: boolean) { + this.#model = value ?? false; + } + get model(): boolean { + return this.#model; + } + #model = false; + @Output() modelChange = new EventEmitter(); + // region model + + // region testId + @Input() testId?: string; + /** @internal */ + testIdToggleModifiers: typeof ToggleTestIdModifiers = ToggleTestIdModifiers; + /** @internal */ + testIdsService: TestIdsService = inject(TestIdsService); + // endregion + + // region common states + /** @internal */ + loading$: BehaviorSubject = new BehaviorSubject(false); + /** @internal */ + checked$: BehaviorSubject = new BehaviorSubject(false); + /** @internal */ + disabled$: BehaviorSubject = new BehaviorSubject(false); + // endregion + + /** @ignore */ + change($event: Event): void { + this.propagateTouched(); + this.model = ($event.target as HTMLInputElement).checked; + this.checked$.next(this.model); + this.propagateChange(this.model); + this.modelChange.emit(this.model); + } + + // Implement ControlValueAccessor methods + /** + * Method to call when value has changes. + * @ignore + */ + propagateChange = (_: boolean) => {}; + + /** + * Method to call when the component is touched (when it was is clicked). + * @ignore + */ + propagateTouched = () => {}; + + /** + * update value from model to the component + * @ignore + */ + writeValue(value: boolean): void { + this.checked$.next(!!value); + } + + /** + * Informs the outside world about changes. + * see method propagateChange call - this.propagateChange(this.model); + * @ignore + */ + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + /** + * on click + * @ignore + */ + registerOnTouched(fn: any): void { + this.propagateTouched = fn; + } + + /** + * on set form controll enabled / disabled + * also do UI Component enabled / disabled + * @ignore + */ + setDisabledState?(disabled: boolean): void { + this.disabled$.next(disabled); + } +} diff --git a/projects/fusion-ui/components/tooltip/v1/tooltip.component.html b/projects/fusion-ui/components/tooltip/v1/tooltip.component.html index 6a448190e..586d760ec 100644 --- a/projects/fusion-ui/components/tooltip/v1/tooltip.component.html +++ b/projects/fusion-ui/components/tooltip/v1/tooltip.component.html @@ -1,7 +1,9 @@ - - -
    -
    - - - +@if (isHtml){ + +
    +} @else { + + +} diff --git a/projects/fusion-ui/components/tooltip/v1/tooltip.module.ts b/projects/fusion-ui/components/tooltip/v1/tooltip.module.ts index cb2ef7856..66bae48d2 100644 --- a/projects/fusion-ui/components/tooltip/v1/tooltip.module.ts +++ b/projects/fusion-ui/components/tooltip/v1/tooltip.module.ts @@ -1,13 +1,12 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {TooltipDirective} from './tooltip.directive'; -import {DynamicComponentsModule} from '@ironsource/fusion-ui/components/dynamic-components/v1'; import {IconModule} from '@ironsource/fusion-ui/components/icon/v1'; import {TooltipComponent} from './tooltip.component'; @NgModule({ declarations: [TooltipDirective, TooltipComponent], exports: [TooltipDirective, TooltipComponent], - imports: [CommonModule, DynamicComponentsModule, IconModule] + imports: [CommonModule, IconModule] }) export class TooltipModule {} diff --git a/projects/fusion-ui/components/tooltip/v4/tooltip-content-v4.component.scss b/projects/fusion-ui/components/tooltip/v4/tooltip-content-v4.component.scss index 5f865874f..713b10b12 100644 --- a/projects/fusion-ui/components/tooltip/v4/tooltip-content-v4.component.scss +++ b/projects/fusion-ui/components/tooltip/v4/tooltip-content-v4.component.scss @@ -35,6 +35,7 @@ $arrowWidth: 16px; flex-shrink: 0; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='9' fill='none' viewBox='0 0 16 9'%3E%3Cpath fill='%23fff' stroke='%23E4E4E4' d='M14.793 1.044H1.236l6.779 6.778 6.778-6.778Z'/%3E%3C/svg%3E"); &:after{ + pointer-events: none; content: ''; position: absolute; margin-left: -7px; diff --git a/projects/fusion-ui/components/tooltip/v4/tooltip-v4.implementation.stories.ts b/projects/fusion-ui/components/tooltip/v4/tooltip-v4.implementation.stories.ts index 006c02510..84534fa01 100644 --- a/projects/fusion-ui/components/tooltip/v4/tooltip-v4.implementation.stories.ts +++ b/projects/fusion-ui/components/tooltip/v4/tooltip-v4.implementation.stories.ts @@ -44,7 +44,7 @@ export const Basic: Story = { render: args => ({ props: { ...args, - testId: 'tooltip-default--tt-trigger' + testId: null //'tooltip-default--tt-trigger' }, template: `Hover me` }) @@ -60,7 +60,7 @@ export const WithoutArrow: Story = { tooltipConfiguration: { suppressPositionArrow: true }, - testId: 'tooltip-default--tt-trigger' + testId: null //'tooltip-default--tt-trigger' } }; diff --git a/projects/fusion-ui/directives/copy-to-clipboard/copy-to-clipboard.directive.ts b/projects/fusion-ui/directives/copy-to-clipboard/copy-to-clipboard.directive.ts index f6e082079..c9d71853b 100644 --- a/projects/fusion-ui/directives/copy-to-clipboard/copy-to-clipboard.directive.ts +++ b/projects/fusion-ui/directives/copy-to-clipboard/copy-to-clipboard.directive.ts @@ -1,4 +1,4 @@ -import {Directive, ElementRef, HostListener, Inject, Input, Renderer2} from '@angular/core'; +import {Directive, ElementRef, EventEmitter, HostListener, Inject, Input, Output, Renderer2} from '@angular/core'; import {DOCUMENT} from '@angular/common'; import {isFunction} from '@ironsource/fusion-ui/utils'; @@ -7,7 +7,7 @@ import {isFunction} from '@ironsource/fusion-ui/utils'; }) export class CopyToClipboardDirective { @Input() fusionCopyToClipboard?: (elRef?: ElementRef) => string; - + @Output() copied = new EventEmitter(); private _document?: Document; constructor( @@ -26,6 +26,8 @@ export class CopyToClipboardDirective { ? this.fusionCopyToClipboard(this.elementRef) : this.elementRef.nativeElement.innerHTML; this.copyText(textToCopy); + + this.copied.emit(); } private copyText(textToCopy: string): void { diff --git a/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.directive.spec.ts b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.directive.spec.ts new file mode 100644 index 000000000..6c18ebf64 --- /dev/null +++ b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.directive.spec.ts @@ -0,0 +1,8 @@ +import { DragAndDropDirective } from './drag-and-drop.directive'; + +describe('DragAndDropDirective', () => { + it('should create an instance', () => { + const directive = new DragAndDropDirective(null, null); + expect(directive).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.directive.ts b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.directive.ts new file mode 100644 index 000000000..45ddb0e33 --- /dev/null +++ b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.directive.ts @@ -0,0 +1,91 @@ +import {ContentChildren, Directive, ElementRef, EventEmitter, HostListener, Output, QueryList} from '@angular/core'; +import {DragAndDropService} from './drag-and-drop.service'; +import {DragAndDropListChanges} from './drag-and-drop.entities'; + +@Directive({ + selector: '[fusionDragAndDrop]', + standalone: true, + providers: [DragAndDropService] +}) +export class DragAndDropDirective { + @ContentChildren('draggableItem') set draggableListItems(value: QueryList) { + this.setDraggableAttributeToItems(value); + } + + @Output() dragElementDrop = new EventEmitter(); + @Output() dragElementEnd = new EventEmitter(); + @Output() dragElementStart = new EventEmitter(); + + private dragElement: HTMLElement; + + private dragChanges: DragAndDropListChanges = {element: null, fromIndex: null, toIndex: null}; + + constructor(private hostElement: ElementRef, private readonly dragAndDropStableService: DragAndDropService) {} + + @HostListener('dragstart', ['$event']) + onDragStart($event: any) { + const draggableElement = $event?.target?.closest('[draggable]'); + $event.dataTransfer.effectAllowed = 'move'; + if (draggableElement) { + this.dragChanges.fromIndex = this.getElementIndex(draggableElement); + this.dragElement = draggableElement; + this.dragElement.classList.add('dragging'); + this.dragElementStart.emit(this.dragElement); + } + } + + @HostListener('dragend', ['$event']) + onDragEnd($event: any) { + const draggableElement = $event?.target?.closest('[draggable]'); + if (draggableElement) { + this.dragElement.classList.remove('dragging'); + this.dragElement.classList.remove('dragging-transit'); + this.dragElementEnd.emit(); + } + } + + @HostListener('dragover', ['$event']) + onDragOver($event: any) { + $event.preventDefault(); + this.dragElement.classList.add('dragging-transit'); + const afterElement = this.dragAndDropStableService.getDragAfterElement($event.clientY, this.hostElement); + if (!afterElement) { + this.hostElement.nativeElement.appendChild(this.dragElement); + } else { + this.hostElement.nativeElement.insertBefore(this.dragElement, afterElement); + } + this.dragAndDropStableService.onDragToEdgeOfScrollableContainer({ + dragElement: this.dragElement, + containerElement: this.hostElement.nativeElement + }); + } + + @HostListener('drop', ['$event']) + onDrop($event: DragEvent) { + const draggableElement: HTMLElement = ($event?.target as HTMLElement)?.closest('[draggable]'); + if (draggableElement) { + this.dragChanges.toIndex = this.getElementIndex(draggableElement); + this.dragChanges.element = draggableElement; + this.dragElement.classList.remove('dragging'); + this.dragElement.classList.remove('dragging-transit'); + + if (this.dragChanges.toIndex !== this.dragChanges.fromIndex) { + this.dragElementDrop.emit(this.dragChanges); + } + } + this.dragChanges = {element: null, fromIndex: null, toIndex: null}; + } + + private getElementIndex(element: HTMLElement) { + return Array.from(this.hostElement.nativeElement.children).findIndex(child => child === element); + } + + private setDraggableAttributeToItems(draggableItems: QueryList) { + draggableItems.forEach(itemElement => { + const isDraggable = itemElement.nativeElement.getAttribute('draggable'); + if (!isDraggable) { + itemElement.nativeElement.setAttribute('draggable', true); + } + }); + } +} diff --git a/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.entities.ts b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.entities.ts new file mode 100644 index 000000000..3b04da175 --- /dev/null +++ b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.entities.ts @@ -0,0 +1,5 @@ +export interface DragAndDropListChanges { + element: HTMLElement; + fromIndex: number; + toIndex: number; +} diff --git a/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.service.spec.ts b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.service.spec.ts new file mode 100644 index 000000000..5b68ac257 --- /dev/null +++ b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { DragAndDropService } from './drag-and-drop.service'; + +describe('DragAndDropService', () => { + let service: DragAndDropService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(DragAndDropService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.service.ts b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.service.ts new file mode 100644 index 000000000..40b23ca54 --- /dev/null +++ b/projects/fusion-ui/directives/drag-and-drop/drag-and-drop.service.ts @@ -0,0 +1,41 @@ +import {ElementRef, Injectable} from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class DragAndDropService { + getDragAfterElement(y: number, hostElement: ElementRef): HTMLElement { + const draggableElements = [...hostElement.nativeElement.children].filter(child => !child.classList.contains('dragging')); + return draggableElements.reduce( + (closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + return offset < 0 && offset > closest.offset ? {offset, element: child} : closest; + }, + {offset: Number.NEGATIVE_INFINITY} + ).element; + } + + onDragToEdgeOfScrollableContainer({dragElement, containerElement}: {dragElement: HTMLElement; containerElement: HTMLElement}) { + let pageY = 0; + let distanceFromTopOfContainer = 0; + if (dragElement) { + pageY = dragElement.getBoundingClientRect().top; + distanceFromTopOfContainer = pageY - containerElement.getBoundingClientRect().top; + + if (distanceFromTopOfContainer < 30) { + this.scrollEntities({containerElement, action: 'scrollUp'}); + } else if (distanceFromTopOfContainer > containerElement.clientHeight - 30) { + this.scrollEntities({containerElement, action: 'scrollDown'}); + } + } + } + + private scrollEntities({containerElement, action}: {containerElement: HTMLElement; action: string}) { + if (action === 'scrollDown') { + containerElement.scrollTop += 10; + } else if (action === 'scrollUp') { + containerElement.scrollTop -= 10; + } + } +} diff --git a/projects/fusion-ui/directives/drag-and-drop/index.ts b/projects/fusion-ui/directives/drag-and-drop/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/projects/fusion-ui/directives/drag-and-drop/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/projects/fusion-ui/directives/drag-and-drop/ng-package.json b/projects/fusion-ui/directives/drag-and-drop/ng-package.json new file mode 100644 index 000000000..c154cd4ee --- /dev/null +++ b/projects/fusion-ui/directives/drag-and-drop/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + }, + "allowedNonPeerDependencies": ["."] +} diff --git a/projects/fusion-ui/directives/drag-and-drop/public-api.ts b/projects/fusion-ui/directives/drag-and-drop/public-api.ts new file mode 100644 index 000000000..cb563fcfc --- /dev/null +++ b/projects/fusion-ui/directives/drag-and-drop/public-api.ts @@ -0,0 +1,2 @@ +export * from './drag-and-drop.directive'; +export * from './drag-and-drop.entities'; diff --git a/projects/fusion-ui/entities/ab-test-icons.ts b/projects/fusion-ui/entities/ab-test-icons.ts new file mode 100644 index 000000000..5babfd43d --- /dev/null +++ b/projects/fusion-ui/entities/ab-test-icons.ts @@ -0,0 +1 @@ +export type AbTestIcons = 'ab' | '2a' | '2b' | 'ab-gray'; diff --git a/projects/fusion-ui/entities/application.ts b/projects/fusion-ui/entities/application.ts new file mode 100644 index 000000000..55510ba0a --- /dev/null +++ b/projects/fusion-ui/entities/application.ts @@ -0,0 +1,8 @@ +import {PlatformType} from './platform-type'; + +export interface Application { + name: string; + imageSrc: string; + platform: PlatformType; + key?: string; +} diff --git a/projects/fusion-ui/entities/platform-type.ts b/projects/fusion-ui/entities/platform-type.ts new file mode 100644 index 000000000..beea5acee --- /dev/null +++ b/projects/fusion-ui/entities/platform-type.ts @@ -0,0 +1 @@ +export type PlatformType = 'android' | 'ios'; diff --git a/projects/fusion-ui/entities/public-api.ts b/projects/fusion-ui/entities/public-api.ts index 8616d0a6e..ad82118fd 100644 --- a/projects/fusion-ui/entities/public-api.ts +++ b/projects/fusion-ui/entities/public-api.ts @@ -1,2 +1,5 @@ export * from './layout-user'; export * from './test-ids-modifiers'; +export * from './ab-test-icons'; +export * from './platform-type'; +export * from './application'; diff --git a/projects/fusion-ui/entities/test-ids-modifiers.ts b/projects/fusion-ui/entities/test-ids-modifiers.ts index de6f33b6e..76784f477 100644 --- a/projects/fusion-ui/entities/test-ids-modifiers.ts +++ b/projects/fusion-ui/entities/test-ids-modifiers.ts @@ -1,6 +1,12 @@ export enum ChipFilterTestIdModifiers { CHIP_FILTER = 'chf', - RESET_BUTTON = 'chf-reset-button' + RESET_BUTTON = 'chf-reset-button', + CONTAINER = 'chf-container', + WRAPPER = 'chf-wrapper', + LEFT_ICON = 'chf-left-icon', + RIGHT_ICON = 'chf-right-icon', + CLOSE_ICON = 'chf-close-icon', + CARET_ICON = 'chf-caret-icon' } export enum ButtonTestIdModifiers { @@ -10,10 +16,22 @@ export enum ButtonTestIdModifiers { CONTENT = 'button-content' } +export enum LinkTestIdModifiers { + LINK = 'link', + CONTENT = 'link-content', + START_ICON = 'link-start-icon', + END_ICON = 'link-end-icon', + EXTERNAL_ICON = 'link-external-icon' +} + export enum DropdownTestIdModifiers { TRIGGER = 'dd-trigger', WRAPPER = 'dd-wrapper', BUTTON = 'dd-button', + TRIGGER_IMAGE = 'dd-trigger-image', + TRIGGER_ICON = 'dd-trigger-icon', + TRIGGER_CARET_ICON = 'dd-trigger-caret', + TRIGGER_COUNTRY_FLAG = 'dd-trigger-flag', LOADING = 'dd-loading', BUTTON_WRAPPER = 'dd-button-wrapper', BUTTON_CONTENT = 'dd-button-content', @@ -110,11 +128,11 @@ export enum InputTestIdModifiers { export enum ToggleTestIdModifiers { WRAPPER = 'toggle-wrapper', - BODY = 'toggle-body', - FIELD = 'toggle-field', - TEXT = 'toggle-text', - HELPER_TEXT = 'toggle-helper-text', - ERROR_TEXT = 'toggle-error-text' + START_ICON = 'toggle-start-icon', + END_ICON = 'toggle-end-icon', + LABEL = 'toggle-label', + LABEL_ICON = 'toggle-label-icon', + HELPER = 'toggle-helper' } export enum IncludeExcludeTestIdModifiers { @@ -205,12 +223,34 @@ export enum SearchTestIdModifiers { export enum TableTestIdModifiers { LABEL = 'table-label', + SEARCH = 'table-search', + HEADER_TOOLTIP = 'table-header-tooltip', COLUMN_HEADER = 'table-header-c', COLUMN_TITLE = 'table-column-title-c', + COLUMN_TOOLTIP = 'table-column-tooltip-c', COLUMN_SORT_UP = 'table-column-sort-up-c', COLUMN_SORT_DOWN = 'table-column-sort-down-c', COLUMN_HEADER_SELECT_ALL = 'table-header-select-all', CELL = 'table-cell', + EXPAND_ICON_BUTTON = 'table-expand-icon-button', BUTTON_GO_TOP = 'table-button-go-top' } + +export enum DateRangeTestIdModifiers { + TRIGGER = 'trigger', + TRIGGER_CUSTOM = 'trigger-custom', + OVERLAY = 'overlay', + PRESETS_WRAPPER = 'presets-wrapper', + PREV_MONTH_BUTTON = 'prev-month-button', + NEXT_MONTH_BUTTON = 'next-month-button', + CALENDAR = 'calendar', + TIME_SELECTOR = 'time-selector', + TIME_CHECKBOX = 'time-checkbox', + TIME_START = 'time-start', + TIME_END = 'time-END', + ACTION_FOOTER = 'action-footer', + ACTION_FOOTER_MESSAGE = 'action-footer-message', + ACTION_CANCEL_BUTTON = 'action-cancel-button', + ACTION_APPLY_BUTTON = 'action-apply-button' +} diff --git a/projects/fusion-ui/package.json b/projects/fusion-ui/package.json index 7df90c7a1..520e86d7b 100644 --- a/projects/fusion-ui/package.json +++ b/projects/fusion-ui/package.json @@ -1,6 +1,6 @@ { "name": "@ironsource/fusion-ui", - "version": "8.3.0", + "version": "8.4.0", "dependencies": { "chart.js": "4.4.2", "@floating-ui/dom": "^1.0.9", diff --git a/projects/fusion-ui/src/style/scss/v4/vars/_fonts.scss b/projects/fusion-ui/src/style/scss/v4/vars/_fonts.scss index aaf39aca6..cee7cc621 100644 --- a/projects/fusion-ui/src/style/scss/v4/vars/_fonts.scss +++ b/projects/fusion-ui/src/style/scss/v4/vars/_fonts.scss @@ -164,6 +164,7 @@ $inter: 'Inter', sans-serif; %font-v4-chip-label { font-size: 0.75rem; line-height: 1rem; + letter-spacing: normal; @extend %font-v4-semibold; } // endregion diff --git a/projects/fusion-ui/src/style/scss/v4/vars/_vars.scss b/projects/fusion-ui/src/style/scss/v4/vars/_vars.scss index 2ee287cd8..0eddd7be1 100644 --- a/projects/fusion-ui/src/style/scss/v4/vars/_vars.scss +++ b/projects/fusion-ui/src/style/scss/v4/vars/_vars.scss @@ -49,13 +49,13 @@ $scrollBarColor: rgba(83, 87, 91, 0.3); %customScroll{ /* total width */ &::-webkit-scrollbar { - background-color: #fff; + background-color: var(--fu-custom-scroll-bg-color, #fff); width: 12px; } /* background of the scrollbar except button or resizer */ &::-webkit-scrollbar-track { - background-color: #fff; + background-color: var(--fu-custom-scroll-bg-color, #fff); } /* scrollbar itself */ @@ -75,18 +75,21 @@ $scrollBarColor: rgba(83, 87, 91, 0.3); /* total width */ &::-webkit-scrollbar { background-color: transparent; - width: 4px; + border-left: var(--fu-scroll-border, solid 0px transparent); + border-top: var(--fu-scroll-border, solid 0px transparent); + width: var(--fu-scroll-width, 4px); + height: var(--fu-scroll-width, 4px); } /* background of the scrollbar except button or resizer */ &::-webkit-scrollbar-track { - background-color: transparent; + background-color: var(--fu-scroll-bg-color, transparent); } /* scrollbar itself */ &::-webkit-scrollbar-thumb { - background-color: $scrollBarColor; - border-radius: 4px; + background-color: var(--fu-scroll-button-bg-color, $scrollBarColor); + border-radius: var(--fu-scroll-button-border-radius, 4px); } /* set button(top and bottom of the scrollbar) */ &::-webkit-scrollbar-button {