-
-
+
+
+
-
-
-
+ @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){
+
+ }
+
+ }
+ @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) {
+ -
+
+
+
+
+
+ }
+
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) {
+ -
+
+
+
+
+
+ }
+
+\`\`\`
+
+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}}
+
+ }
+
+
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 @@
+ [component]="headerContent.actionComponent"
+ [componentData]="headerContent.actionData">
-
-
-
+ @for (teleportItem of teleportElements; track teleportItem){
+
+ }
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: `
+
+
+
+
+
+
+
subtitle1
+
{{content}}
+
+
+
+
subtitle2
+
{{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 @@
+
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: `
+
+ `
+ })
+};
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) {
+
+
+
+
+
+
+}
+
+
+
+
+
+
+
+
+
+
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: `
+
+
+
+ `,
+ 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 (!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)) {
+
+ }
+
+
+
+ }
+
+@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 {