Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): enable segmented button #12808

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 44 additions & 73 deletions libs/core/segmented-button/segmented-button.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { Component, ElementRef, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { ButtonComponent, ButtonModule } from '@fundamental-ngx/core/button';
import { runValueAccessorTests } from 'ngx-cva-test-suite';

import { RtlService } from '@fundamental-ngx/cdk/utils';
import { SegmentedButtonComponent } from './segmented-button.component';
import { SegmentedButtonModule } from './segmented-button.module';
import { SimpleChange } from '@angular/core';

const isSelectedClass = 'fd-button--toggled';

Expand All @@ -21,17 +19,10 @@ const isSelectedClass = 'fd-button--toggled';
`
})
export class HostComponent {
@ViewChild('first', { read: ElementRef })
firstButton: ElementRef;

@ViewChild('second', { read: ButtonComponent })
secondButton: ButtonComponent;

@ViewChild('third', { read: ElementRef })
thirdButton: ElementRef;

@ViewChild(SegmentedButtonComponent)
segmentedButton: SegmentedButtonComponent;
@ViewChild('first', { read: ElementRef }) firstButton: ElementRef;
@ViewChild('second', { read: ButtonComponent }) secondButton: ButtonComponent;
@ViewChild('third', { read: ElementRef }) thirdButton: ElementRef;
@ViewChild(SegmentedButtonComponent) segmentedButton: SegmentedButtonComponent;

toggle = false;
}
Expand All @@ -43,7 +34,6 @@ describe('SegmentedButtonComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [HostComponent],
providers: [RtlService],
imports: [SegmentedButtonModule, ButtonModule]
}).compileComponents();
}));
Expand All @@ -59,103 +49,87 @@ describe('SegmentedButtonComponent', () => {
expect(component).toBeTruthy();
});

it('should select button, when value is changed', () => {
component.segmentedButton.writeValue('first');
// Default Example
it('should correctly select and deselect single value in non-toggle mode', () => {
component.segmentedButton.writeValue('second');
fixture.detectChanges();
expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(true);

component.segmentedButton.writeValue('first');
fixture.detectChanges();
expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(false);
});

it('should select all buttons button, when value is changed', () => {
// Toggle Example
it('should correctly handle multiple selections in toggle mode', () => {
component.toggle = true;
component.segmentedButton.writeValue(['first']);
fixture.detectChanges();

component.segmentedButton.writeValue(['first', 'second', 'third']);
fixture.detectChanges();

expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.thirdButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
});

it('should select button, when trigger event is performed', () => {
component.firstButton.nativeElement.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();

expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
});

it('should select button and deselect other button, when trigger event is performed on non-toggle mode', () => {
component.firstButton.nativeElement.dispatchEvent(new MouseEvent('click'));
component.secondButton.elementRef.nativeElement.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(true);

expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.segmentedButton['_currentValue']).toEqual('first');

component.secondButton.elementRef.nativeElement.dispatchEvent(new MouseEvent('click'));
component.thirdButton.nativeElement.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(component.thirdButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);

// Deselect
component.firstButton.nativeElement.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(false);
expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.segmentedButton['_currentValue']).toEqual('second');
});

it('should select buttons, when trigger event is performed on toggle mode', () => {
component.segmentedButton.toggle = true;
component.firstButton.nativeElement.dispatchEvent(new MouseEvent('click'));
// Form Example
it('should update form value correctly', () => {
component.segmentedButton.writeValue('first');
fixture.detectChanges();

expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.segmentedButton['_currentValue']).toEqual(['first']);

component.secondButton.elementRef.nativeElement.dispatchEvent(new MouseEvent('click'));

component.segmentedButton.writeValue('second');
fixture.detectChanges();

expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.segmentedButton['_currentValue']).toEqual(['first', 'second']);
});

it('should ignore trigger event on disabled', () => {
component.firstButton.nativeElement.dispatchEvent(new MouseEvent('click'));
// Disabled State Check
it('should detect disabled state', () => {
component.segmentedButton.setDisabledState(true);
fixture.detectChanges();

expect(component.firstButton.nativeElement.hasAttribute('disabled')).toBe(true);
expect(component.secondButton.elementRef.nativeElement.hasAttribute('disabled')).toBe(true);
expect(component.thirdButton.nativeElement.hasAttribute('disabled')).toBe(true);

expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.segmentedButton['_currentValue']).toEqual('first');

component.secondButton.disabled = true;
fixture.detectChanges();
component.secondButton.elementRef.nativeElement.dispatchEvent(new MouseEvent('click'));
component.segmentedButton.setDisabledState(false);
fixture.detectChanges();

expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(false);
expect(component.segmentedButton['_currentValue']).toEqual('first');
expect(component.firstButton.nativeElement.hasAttribute('disabled')).toBe(false);
expect(component.secondButton.elementRef.nativeElement.hasAttribute('disabled')).toBe(false);
expect(component.thirdButton.nativeElement.hasAttribute('disabled')).toBe(false);
});

// Event Handling Test
it('should handle all trigger events', () => {
component.segmentedButton.toggle = true;
component.firstButton.nativeElement.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();

component.firstButton.nativeElement.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.segmentedButton['_currentValue']).toEqual(['first']);

component.secondButton.elementRef.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' }));
fixture.detectChanges();

expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.segmentedButton['_currentValue']).toEqual(['first', 'second']);

component.thirdButton.nativeElement.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' }));
fixture.detectChanges();

expect(component.firstButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.secondButton.elementRef.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.thirdButton.nativeElement.classList.contains(isSelectedClass)).toBe(true);
expect(component.segmentedButton['_currentValue']).toEqual(['first', 'second', 'third']);
});

});

describe('Segmented button component CVA', () => {
Expand All @@ -169,18 +143,15 @@ describe('Segmented button component CVA', () => {
},
testModuleMetadata: {
declarations: [HostComponent],
providers: [RtlService],
imports: [SegmentedButtonModule, ButtonModule] // <= importing the module for app-select
imports: [SegmentedButtonModule, ButtonModule]
},
hostTemplate: {
// specify that "AppSelectComponent" should not be tested directly
hostComponent: HostComponent,
// specify the way to access "AppSelectComponent" from the host template
getTestingComponent: (fixture) => fixture.componentInstance.segmentedButton
},
supportsOnBlur: false,
internalValueChangeSetter: null,
getComponentValue: (fixture) => (fixture.componentInstance.segmentedButton as any)._currentValue,
getValues: () => [1, 2, 3] // <= setting the same values as select options in host template
getComponentValue: (fixture) => fixture.componentInstance.segmentedButton['_currentValue'],
getValues: () => ['first', 'second', 'third']
});
});
37 changes: 24 additions & 13 deletions libs/core/segmented-button/segmented-button.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@ import {
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FocusableListDirective, KeyUtil, Nullable, RtlService, destroyObservable } from '@fundamental-ngx/cdk/utils';
import { FocusableItemDirective, FocusableListDirective, KeyUtil, Nullable, RtlService, destroyObservable } from '@fundamental-ngx/cdk/utils';
import { ButtonComponent, FD_BUTTON_COMPONENT } from '@fundamental-ngx/core/button';
import { Subject, asyncScheduler, fromEvent, merge } from 'rxjs';
import { filter, observeOn, startWith, takeUntil, tap } from 'rxjs/operators';

export const isDisabledClass = 'is-disabled';

export type SegmentedButtonValue = string | (string | null)[] | null;

/**
Expand Down Expand Up @@ -79,6 +77,10 @@ export class SegmentedButtonComponent implements AfterViewInit, ControlValueAcce
@ContentChildren(FD_BUTTON_COMPONENT)
_buttons: QueryList<ButtonComponent>;

/** @hidden */
@ContentChildren(FocusableItemDirective)
_focusableItems: QueryList<FocusableItemDirective>;

/**
* Value of segmented button can have 2 types:
* - string, when there is no toggle mode and only 1 value can be chosen.
Expand Down Expand Up @@ -166,12 +168,13 @@ export class SegmentedButtonComponent implements AfterViewInit, ControlValueAcce
setDisabledState(isDisabled: boolean): void {
this._isDisabled = isDisabled;
this._toggleDisableButtons(isDisabled);
this._onRefresh$.next();
this._changeDetRef.detectChanges();
}

/** @hidden */
private _listenToButtonChanges(): void {
this._buttons.changes
merge(this._buttons.changes, this._focusableItems.changes)
.pipe(startWith(1), observeOn(asyncScheduler), takeUntilDestroyed(this._destroyRef))
.subscribe(() => {
this._onRefresh$.next();
Expand Down Expand Up @@ -207,14 +210,12 @@ export class SegmentedButtonComponent implements AfterViewInit, ControlValueAcce
/** @hidden */
private _handleTriggerOnButton(buttonComponent: ButtonComponent): void {
if (!this._isButtonDisabled(buttonComponent)) {
if (!this._isButtonSelected(buttonComponent) && !this.toggle) {
if (!this.toggle) {
this._buttons.forEach((button) => this._deselectButton(button));
this._selectButton(buttonComponent);
this._propagateChange();
this._changeDetRef.markForCheck();
}

if (this.toggle) {
} else {
this._toggleButton(buttonComponent);
this._propagateChange();
this._changeDetRef.markForCheck();
Expand All @@ -224,8 +225,9 @@ export class SegmentedButtonComponent implements AfterViewInit, ControlValueAcce

/** @hidden */
private _propagateChange(): void {
this.onChange(this._getValuesBySelected());
this._currentValue = this._getValuesBySelected();
const selectedValue = this._getValuesBySelected();
this.onChange(selectedValue);
this._currentValue = selectedValue;
}

/** @hidden */
Expand Down Expand Up @@ -282,14 +284,23 @@ export class SegmentedButtonComponent implements AfterViewInit, ControlValueAcce

/** @hidden */
private _toggleDisableButtons(disable: boolean): void {
if (!this._buttons) {
if (!this._buttons || !this._focusableItems) {
return;
}

this._buttons.forEach((button) => (button.disabled = disable));
this._buttons.forEach((button) => button.disabled = disable);

this._focusableItems.forEach((focusableItemDirective) => {
focusableItemDirective.setTabbable(!disable);
focusableItemDirective.fdkFocusableItem = !disable;
});
if (disable) {
this._buttons.forEach((button) => button.elementRef.nativeElement.setAttribute('disabled', 'true'));
this._buttons.forEach((button) => {
button.elementRef.nativeElement.role = 'option';
this._listenToTriggerEvents(button);
});
}

this._changeDetRef.markForCheck();
}

Expand Down
Loading
Loading