diff --git a/commitlint.config.js b/commitlint.config.js index d7ee19a1f..08a18fb83 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -9,6 +9,7 @@ module.exports = { 'badge', 'build', 'button', + 'button-toggle', 'ci', 'cdk', 'card', diff --git a/package.json b/package.json index 578dde3f0..612de3d34 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "server-dev:alert": "npm run server-dev -- --env.component alert", "server-dev:badge": "npm run server-dev -- --env.component badge", "server-dev:button": "npm run server-dev -- --env.component button", + "server-dev:button-toggle": "npm run server-dev -- --env.component button-toggle", "server-dev:card": "npm run server-dev -- --env.component card", "server-dev:cdk-vscroll-custom-strategy": "npm run server-dev -- --env.component cdk-virtual-scroll-custom-strategy", "server-dev:cdk-vscroll-data-source": "npm run server-dev -- --env.component cdk-virtual-scroll-data-source", diff --git a/src/dev-app/system-config.ts b/src/dev-app/system-config.ts index eeb5fbbcc..cb9429636 100644 --- a/src/dev-app/system-config.ts +++ b/src/dev-app/system-config.ts @@ -61,6 +61,7 @@ System.config({ '@ptsecurity/mosaic': 'dist/packages/mosaic/index.js', '@ptsecurity/mosaic/button': 'dist/packages/mosaic/button/index.js', + '@ptsecurity/mosaic/button-toggle': 'dist/packages/mosaic/button-toggle/index.js', '@ptsecurity/mosaic/core': 'dist/packages/mosaic/core/index.js', '@ptsecurity/mosaic/card': 'dist/packages/mosaic/card/index.js', '@ptsecurity/mosaic/datepicker': 'dist/packages/mosaic/datepicker/index.js', diff --git a/src/lib-dev/all/module.ts b/src/lib-dev/all/module.ts index 9a43a49c9..92cb2c90a 100644 --- a/src/lib-dev/all/module.ts +++ b/src/lib-dev/all/module.ts @@ -8,6 +8,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { CdkTreeModule, FlatTreeControl, NestedTreeControl } from '@ptsecurity/cdk/tree'; import { McButtonModule } from '@ptsecurity/mosaic/button'; +import { McButtonToggleModule } from '@ptsecurity/mosaic/button-toggle'; import { McCardModule } from '@ptsecurity/mosaic/card'; import { McCheckboxModule } from '@ptsecurity/mosaic/checkbox'; @@ -54,6 +55,8 @@ export class DemoComponent { disabled: boolean = false; labelPosition = 'after'; + buttonToggleModelResult: string; + typesOfShoes = ['Boots', 'Clogs', 'Loafers', 'Moccasins', 'Sneakers']; folders = [ @@ -194,6 +197,7 @@ export class DemoComponent { ReactiveFormsModule, McIconModule, McButtonModule, + McButtonToggleModule, McLinkModule, McCardModule, McCheckboxModule, diff --git a/src/lib-dev/all/template.html b/src/lib-dev/all/template.html index 66727d643..a8f3b1a73 100644 --- a/src/lib-dev/all/template.html +++ b/src/lib-dev/all/template.html @@ -101,6 +101,133 @@

Button

  +
+

Button-Toggle

+ + + + + default 1 + + + default 2 + + + default 3 + + + default 4 + + + + + + default 6 + + + + + +
Selected value: {{group1.value}}
+ +
+ + + +
+ + + +
+
+ + + + default 1 + + + default 2 + + + default 3 + + + + + +
Selected value: {{disabledGroup.value}}
+ +
+ + + + + default 1 + + + default 2 + + + default + + + + + +
Selected value: {{multipleGroup.value}}
+ +
+ + + +
+ + + default + +
Selected value: {{standAloneToggle.checked}}
+ +
+ + + + + + default 1 + + + default 2 + + + default 3 + + + + + +
Selected value: {{group3.value}}
+ +
+ + + + + + default 1 + + + default 2 + + + default 3 + + + + + +
Model result: {{buttonToggleModelResult}}
+
+

Card

diff --git a/src/lib-dev/button-toggle/module.ts b/src/lib-dev/button-toggle/module.ts new file mode 100644 index 000000000..ae70747e9 --- /dev/null +++ b/src/lib-dev/button-toggle/module.ts @@ -0,0 +1,44 @@ +import { Component, NgModule, ViewEncapsulation } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { McIconModule } from '@ptsecurity/mosaic/icon'; + +import { McButtonModule } from '../../lib/button'; +import { McButtonToggleModule } from '../../lib/button-toggle'; + + +@Component({ + selector: 'app', + template: require('./template.html'), + styleUrls: ['./styles.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ButtonToggleDemoComponent { + modelResult: any; + disabled: boolean; +} + + +@NgModule({ + declarations: [ + ButtonToggleDemoComponent + ], + imports: [ + BrowserModule, + McButtonModule, + McButtonToggleModule, + McIconModule, + FormsModule + ], + bootstrap: [ + ButtonToggleDemoComponent + ] +}) +export class ButtonToggleDemoModule {} + +platformBrowserDynamic() + .bootstrapModule(ButtonToggleDemoModule) + // tslint:disable-next-line:no-console + .catch((error) => console.error(error)); + diff --git a/src/lib-dev/button-toggle/styles.scss b/src/lib-dev/button-toggle/styles.scss new file mode 100644 index 000000000..885247b56 --- /dev/null +++ b/src/lib-dev/button-toggle/styles.scss @@ -0,0 +1,4 @@ +@import '~@ptsecurity/mosaic-icons/dist/styles/mc-icons'; + +// @import '../../lib/core/theming/prebuilt/default-theme'; +@import '../../lib/core/theming/prebuilt/dark-theme'; diff --git a/src/lib-dev/button-toggle/template.html b/src/lib-dev/button-toggle/template.html new file mode 100644 index 000000000..3ed69694e --- /dev/null +++ b/src/lib-dev/button-toggle/template.html @@ -0,0 +1,124 @@ +
+ + + + default 1 + + + default 2 + + + default 3 + + + default 4 + + + + + + default 6 + + + + + +
Selected value: {{group1.value}}
+ +
+ + + +
+ + + +
+
+ + + + default 1 + + + default 2 + + + default 3 + + + + + +
Selected value: {{disabledGroup.value}}
+ +
+ + + + + default 1 + + + default 2 + + + default 3 + + + + + +
Selected value: {{multipleGroup.value}}
+ +
+ + + +
+ + + default + +
Selected value: {{standAloneToggle.checked}}
+ +
+ + + + + + default 1 + + + default 2 + + + default 3 + + + + + +
Selected value: {{group3.value}}
+ +
+ + + + + + default 1 + + + default 2 + + + default 3 + + + + + +
Model result: {{modelResult}}
+
diff --git a/src/lib/button-toggle/README.md b/src/lib/button-toggle/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/lib/button-toggle/_button-toggle-theme.scss b/src/lib/button-toggle/_button-toggle-theme.scss new file mode 100644 index 000000000..5a70aaa08 --- /dev/null +++ b/src/lib/button-toggle/_button-toggle-theme.scss @@ -0,0 +1,48 @@ +@import '../core/theming/palette'; +@import '../core/theming/theming'; +@import '../core/styles/typography/typography-utils'; +@import '../button/button-theme'; + +@mixin mc-button-toggle-theme($theme) { + $primary: map-get($theme, primary); + $second: map-get($theme, second); + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + $divider-color: mc-color($foreground, divider); + + .mc-button-toggle-standalone { + box-shadow: none; + } + + .mc-button-toggle-vertical { + .mc-button-toggle + .mc-button-toggle { + border-left: none; + border-right: none; + } + } + + .mc-button-toggle[disabled] { + outline: 0; + } + + .mc-button-toggle-checked:not([disabled]) { + .mc-button, + .mc-icon-button { + background: darken(map-get($background, button-bg), 5); + } + + &:not(.cdk-keyboard-focused) { + .mc-button, + .mc-icon-button { + border-color: darken(mc-color($background, button-border), 5); + box-shadow: map-get($background, in-shadow); + } + } + } +} + +@mixin mc-button-toggle-typography($config) { + .mc-button-toggle { + font-family: mc-font-family($config); + } +} diff --git a/src/lib/button-toggle/button-toggle.component.spec.ts b/src/lib/button-toggle/button-toggle.component.spec.ts new file mode 100644 index 000000000..3e5eb7b75 --- /dev/null +++ b/src/lib/button-toggle/button-toggle.component.spec.ts @@ -0,0 +1,781 @@ +import { Component, DebugElement, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; +import { FormControl, FormsModule, NgModel, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { McButtonModule } from '@ptsecurity/mosaic/button'; + +import { McButtonToggle, McButtonToggleChange, McButtonToggleGroup, McButtonToggleModule } from './index'; + + +describe('McButtonToggle with forms', () => { + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [McButtonModule, McButtonToggleModule, FormsModule, ReactiveFormsModule], + declarations: [ + ButtonToggleGroupWithNgModel, + ButtonToggleGroupWithFormControl + ] + }); + + TestBed.compileComponents(); + })); + + describe('using FormControl', () => { + let fixture: ComponentFixture; + let groupDebugElement: DebugElement; + let groupInstance: McButtonToggleGroup; + let testComponent: ButtonToggleGroupWithFormControl; + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(ButtonToggleGroupWithFormControl); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + + groupDebugElement = fixture.debugElement.query(By.directive(McButtonToggleGroup)); + groupInstance = groupDebugElement.injector.get(McButtonToggleGroup); + })); + + it('should toggle the disabled state', () => { + testComponent.control.disable(); + + expect(groupInstance.disabled).toBe(true); + + testComponent.control.enable(); + + expect(groupInstance.disabled).toBe(false); + }); + + it('should set the value', () => { + testComponent.control.setValue('green'); + + expect(groupInstance.value).toBe('green'); + + testComponent.control.setValue('red'); + + expect(groupInstance.value).toBe('red'); + }); + + it('should register the on change callback', () => { + const spy = jasmine.createSpy('onChange callback'); + + testComponent.control.registerOnChange(spy); + testComponent.control.setValue('blue'); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('button toggle group with ngModel and change event', () => { + let fixture: ComponentFixture; + let groupDebugElement: DebugElement; + let buttonToggleDebugElements: DebugElement[]; + let groupInstance: McButtonToggleGroup; + let buttonToggleInstances: McButtonToggle[]; + let testComponent: ButtonToggleGroupWithNgModel; + let groupNgModel: NgModel; + let innerButtons: HTMLElement[]; + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(ButtonToggleGroupWithNgModel); + fixture.detectChanges(); + testComponent = fixture.debugElement.componentInstance; + + groupDebugElement = fixture.debugElement.query(By.directive(McButtonToggleGroup)); + groupInstance = groupDebugElement.injector.get(McButtonToggleGroup); + groupNgModel = groupDebugElement.injector.get(NgModel); + + buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(McButtonToggle)); + buttonToggleInstances = buttonToggleDebugElements.map((debugEl) => debugEl.componentInstance); + innerButtons = buttonToggleDebugElements.map( + (debugEl) => debugEl.query(By.css('button')).nativeElement); + + fixture.detectChanges(); + })); + + it('should update the model before firing change event', fakeAsync(() => { + expect(testComponent.modelValue).toBeUndefined(); + expect(testComponent.lastEvent).toBeUndefined(); + + innerButtons[0].click(); + fixture.detectChanges(); + + tick(); + expect(testComponent.modelValue).toBe('red'); + expect(testComponent.lastEvent.value).toBe('red'); + })); + + it('should check the corresponding button toggle on a group value change', () => { + expect(groupInstance.value).toBeFalsy(); + + for (const buttonToggle of buttonToggleInstances) { + expect(buttonToggle.checked).toBeFalsy(); + } + + groupInstance.value = 'red'; + + for (const buttonToggle of buttonToggleInstances) { + expect(buttonToggle.checked).toBe(groupInstance.value === buttonToggle.value); + } + + const selected = groupInstance.selected as McButtonToggle; + + expect(selected.value).toBe(groupInstance.value); + }); + + it('should have the correct FormControl state initially and after interaction', + fakeAsync(() => { + expect(groupNgModel.valid).toBe(true); + expect(groupNgModel.pristine).toBe(true); + expect(groupNgModel.touched).toBe(false); + + buttonToggleInstances[1].checked = true; + fixture.detectChanges(); + tick(); + + expect(groupNgModel.valid).toBe(true); + expect(groupNgModel.pristine).toBe(true); + expect(groupNgModel.touched).toBe(false); + + // tslint:disable-next-line:no-magic-numbers + innerButtons[2].click(); + fixture.detectChanges(); + tick(); + + expect(groupNgModel.valid).toBe(true); + expect(groupNgModel.pristine).toBe(false); + expect(groupNgModel.touched).toBe(true); + })); + + it('should update the ngModel value when selecting a button toggle', fakeAsync(() => { + innerButtons[1].click(); + fixture.detectChanges(); + + tick(); + + expect(testComponent.modelValue).toBe('green'); + })); + }); +}); + +describe('McButtonToggle without forms', () => { + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [McButtonModule, McButtonToggleModule], + declarations: [ + ButtonTogglesInsideButtonToggleGroup, + ButtonTogglesInsideButtonToggleGroupMultiple, + FalsyButtonTogglesInsideButtonToggleGroupMultiple, + ButtonToggleGroupWithInitialValue, + StandaloneButtonToggle, + RepeatedButtonTogglesWithPreselectedValue + ] + }); + + TestBed.compileComponents(); + })); + + describe('inside of an exclusive selection group', () => { + + let fixture: ComponentFixture; + let groupDebugElement: DebugElement; + let groupNativeElement: HTMLElement; + let buttonToggleDebugElements: DebugElement[]; + let buttonToggleNativeElements: HTMLElement[]; + let buttonToggleLabelElements: HTMLLabelElement[]; + let groupInstance: McButtonToggleGroup; + let buttonToggleInstances: McButtonToggle[]; + let testComponent: ButtonTogglesInsideButtonToggleGroup; + + beforeEach(() => { + fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroup); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + + groupDebugElement = fixture.debugElement.query(By.directive(McButtonToggleGroup)); + groupNativeElement = groupDebugElement.nativeElement; + groupInstance = groupDebugElement.injector.get(McButtonToggleGroup); + + buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(McButtonToggle)); + + buttonToggleNativeElements = buttonToggleDebugElements + .map((debugEl) => debugEl.nativeElement); + + buttonToggleLabelElements = fixture.debugElement.queryAll(By.css('button')) + .map((debugEl) => debugEl.nativeElement); + + buttonToggleInstances = buttonToggleDebugElements.map((debugEl) => debugEl.componentInstance); + }); + + it('should disable click interactions when the group is disabled', () => { + testComponent.isGroupDisabled = true; + fixture.detectChanges(); + + buttonToggleNativeElements[0].click(); + + expect(buttonToggleInstances[0].checked).toBe(false); + testComponent.isGroupDisabled = false; + + fixture.detectChanges(); + + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + + expect(buttonToggleInstances[0].checked).toBe(true); + }); + + it('should disable the underlying button when the group is disabled', () => { + const buttons = buttonToggleNativeElements.map((toggle) => toggle.querySelector('button')!); + + expect(buttons.every((input) => input.disabled)).toBe(false); + + testComponent.isGroupDisabled = true; + fixture.detectChanges(); + + expect(buttons.every((input) => input.disabled)).toBe(true); + }); + + it('should update the group value when one of the toggles changes', () => { + expect(groupInstance.value).toBeFalsy(); + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + + expect(groupInstance.value).toBe('test1'); + expect(groupInstance.selected).toBe(buttonToggleInstances[0]); + }); + + it('should propagate the value change back up via a two-way binding', () => { + expect(groupInstance.value).toBeFalsy(); + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + + expect(groupInstance.value).toBe('test1'); + expect(testComponent.groupValue).toBe('test1'); + }); + + it('should update the group and toggles when one of the button toggles is clicked', () => { + expect(groupInstance.value).toBeFalsy(); + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + + expect(groupInstance.value).toBe('test1'); + expect(groupInstance.selected).toBe(buttonToggleInstances[0]); + expect(buttonToggleInstances[0].checked).toBe(true); + expect(buttonToggleInstances[1].checked).toBe(false); + + buttonToggleLabelElements[1].click(); + fixture.detectChanges(); + + expect(groupInstance.value).toBe('test2'); + expect(groupInstance.selected).toBe(buttonToggleInstances[1]); + expect(buttonToggleInstances[0].checked).toBe(false); + expect(buttonToggleInstances[1].checked).toBe(true); + }); + + it('should check a button toggle upon interaction with underlying native radio button', () => { + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + + expect(buttonToggleInstances[0].checked).toBe(true); + expect(groupInstance.value); + }); + + it('should change the vertical state', () => { + expect(groupNativeElement.classList).not.toContain('mc-button-toggle-vertical'); + + groupInstance.vertical = true; + fixture.detectChanges(); + + expect(groupNativeElement.classList).toContain('mc-button-toggle-vertical'); + }); + + it('should emit a change event from button toggles', fakeAsync(() => { + expect(buttonToggleInstances[0].checked).toBe(false); + + const changeSpy = jasmine.createSpy('button-toggle change listener'); + buttonToggleInstances[0].change.subscribe(changeSpy); + + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + tick(); + expect(changeSpy).toHaveBeenCalledTimes(1); + + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + tick(); + + // Always emit change event when button toggle is clicked + // tslint:disable-next-line:no-magic-numbers + expect(changeSpy).toHaveBeenCalledTimes(2); + })); + + it('should emit a change event from the button toggle group', fakeAsync(() => { + expect(groupInstance.value).toBeFalsy(); + + const changeSpy = jasmine.createSpy('button-toggle-group change listener'); + groupInstance.change.subscribe(changeSpy); + + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + tick(); + expect(changeSpy).toHaveBeenCalled(); + + buttonToggleLabelElements[1].click(); + fixture.detectChanges(); + tick(); + // tslint:disable-next-line:no-magic-numbers + expect(changeSpy).toHaveBeenCalledTimes(2); + })); + + it('should update the group and button toggles when updating the group value', () => { + expect(groupInstance.value).toBeFalsy(); + + testComponent.groupValue = 'test1'; + fixture.detectChanges(); + + expect(groupInstance.value).toBe('test1'); + expect(groupInstance.selected).toBe(buttonToggleInstances[0]); + expect(buttonToggleInstances[0].checked).toBe(true); + expect(buttonToggleInstances[1].checked).toBe(false); + + testComponent.groupValue = 'test2'; + fixture.detectChanges(); + + expect(groupInstance.value).toBe('test2'); + expect(groupInstance.selected).toBe(buttonToggleInstances[1]); + expect(buttonToggleInstances[0].checked).toBe(false); + expect(buttonToggleInstances[1].checked).toBe(true); + }); + + it('should deselect all of the checkboxes when the group value is cleared', () => { + buttonToggleInstances[0].checked = true; + + expect(groupInstance.value).toBeTruthy(); + + groupInstance.value = null; + + expect(buttonToggleInstances.every((toggle) => !toggle.checked)).toBe(true); + }); + + it('should update the model if a selected toggle is removed', fakeAsync(() => { + expect(groupInstance.value).toBeFalsy(); + buttonToggleLabelElements[0].click(); + fixture.detectChanges(); + + expect(groupInstance.value).toBe('test1'); + expect(groupInstance.selected).toBe(buttonToggleInstances[0]); + + testComponent.renderFirstToggle = false; + fixture.detectChanges(); + tick(); + + expect(groupInstance.value).toBeFalsy(); + expect(groupInstance.selected).toBeFalsy(); + })); + + }); + + describe('with initial value and change event', () => { + + it('should not fire an initial change event', () => { + const fixture = TestBed.createComponent(ButtonToggleGroupWithInitialValue); + const testComponent = fixture.debugElement.componentInstance; + const groupDebugElement = fixture.debugElement.query(By.directive(McButtonToggleGroup)); + const groupInstance: McButtonToggleGroup = groupDebugElement.injector + .get(McButtonToggleGroup); + + fixture.detectChanges(); + + // Note that we cast to a boolean, because the event has some circular references + // which will crash the runner when Jasmine attempts to stringify them. + expect(!!testComponent.lastEvent).toBe(false); + expect(groupInstance.value).toBe('red'); + + groupInstance.value = 'green'; + fixture.detectChanges(); + + expect(!!testComponent.lastEvent).toBe(false); + expect(groupInstance.value).toBe('green'); + }); + + }); + + describe('inside of a multiple selection group', () => { + let fixture: ComponentFixture; + let groupDebugElement: DebugElement; + let groupNativeElement: HTMLElement; + let buttonToggleDebugElements: DebugElement[]; + let buttonToggleNativeElements: HTMLElement[]; + let buttonToggleButtonElements: HTMLLabelElement[]; + let groupInstance: McButtonToggleGroup; + let buttonToggleInstances: McButtonToggle[]; + let testComponent: ButtonTogglesInsideButtonToggleGroupMultiple; + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple); + fixture.detectChanges(); + + testComponent = fixture.debugElement.componentInstance; + + groupDebugElement = fixture.debugElement.query(By.directive(McButtonToggleGroup)); + groupNativeElement = groupDebugElement.nativeElement; + groupInstance = groupDebugElement.injector.get(McButtonToggleGroup); + + buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(McButtonToggle)); + buttonToggleNativeElements = buttonToggleDebugElements + .map((debugEl) => debugEl.nativeElement); + buttonToggleButtonElements = fixture.debugElement.queryAll(By.css('button')) + .map((debugEl) => debugEl.nativeElement); + buttonToggleInstances = buttonToggleDebugElements.map((debugEl) => debugEl.componentInstance); + })); + + it('should disable click interactions when the group is disabled', () => { + testComponent.isGroupDisabled = true; + fixture.detectChanges(); + + buttonToggleNativeElements[0].click(); + expect(buttonToggleInstances[0].checked).toBe(false); + }); + + it('should check a button toggle when clicked', () => { + expect(buttonToggleInstances.every((buttonToggle) => !buttonToggle.checked)).toBe(true); + + const nativeCheckboxLabel = buttonToggleDebugElements[0].query(By.css('button')).nativeElement; + + nativeCheckboxLabel.click(); + + expect(groupInstance.value).toEqual(['eggs']); + expect(buttonToggleInstances[0].checked).toBe(true); + }); + + it('should allow for multiple toggles to be selected', () => { + buttonToggleInstances[0].checked = true; + fixture.detectChanges(); + + expect(groupInstance.value).toEqual(['eggs']); + expect(buttonToggleInstances[0].checked).toBe(true); + + buttonToggleInstances[1].checked = true; + fixture.detectChanges(); + + expect(groupInstance.value).toEqual(['eggs', 'flour']); + expect(buttonToggleInstances[1].checked).toBe(true); + expect(buttonToggleInstances[0].checked).toBe(true); + }); + + it('should check a button toggle upon interaction with underlying native checkbox', () => { + const nativeCheckboxButton = buttonToggleDebugElements[0].query(By.css('button')).nativeElement; + + nativeCheckboxButton.click(); + fixture.detectChanges(); + + expect(groupInstance.value).toEqual(['eggs']); + expect(buttonToggleInstances[0].checked).toBe(true); + }); + + it('should change the vertical state', () => { + expect(groupNativeElement.classList).not.toContain('mc-button-toggle-vertical'); + + groupInstance.vertical = true; + fixture.detectChanges(); + + expect(groupNativeElement.classList).toContain('mc-button-toggle-vertical'); + }); + + it('should deselect a button toggle when selected twice', fakeAsync(() => { + buttonToggleButtonElements[0].click(); + fixture.detectChanges(); + tick(); + + expect(buttonToggleInstances[0].checked).toBe(true); + expect(groupInstance.value).toEqual(['eggs']); + + buttonToggleButtonElements[0].click(); + fixture.detectChanges(); + tick(); + + expect(groupInstance.value).toEqual([]); + expect(buttonToggleInstances[0].checked).toBe(false); + })); + + it('should emit a change event for state changes', fakeAsync(() => { + expect(buttonToggleInstances[0].checked).toBe(false); + + const changeSpy = jasmine.createSpy('button-toggle change listener'); + buttonToggleInstances[0].change.subscribe(changeSpy); + + buttonToggleButtonElements[0].click(); + fixture.detectChanges(); + tick(); + expect(changeSpy).toHaveBeenCalled(); + expect(groupInstance.value).toEqual(['eggs']); + + buttonToggleButtonElements[0].click(); + fixture.detectChanges(); + tick(); + expect(groupInstance.value).toEqual([]); + + // The default browser behavior is to emit an event, when the value was set + // to false. That's because the current input type is set to `checkbox` when + // using the multiple mode. + // tslint:disable-next-line:no-magic-numbers + expect(changeSpy).toHaveBeenCalledTimes(2); + })); + + it('should throw when attempting to assign a non-array value', () => { + expect(() => { + groupInstance.value = 'not-an-array'; + }).toThrowError(/Value must be an array/); + }); + }); + + describe('as standalone', () => { + let fixture: ComponentFixture; + let buttonToggleDebugElement: DebugElement; + let buttonToggleButtonElement: HTMLLabelElement; + let buttonToggleInstance: McButtonToggle; + + beforeEach(fakeAsync(() => { + fixture = TestBed.createComponent(StandaloneButtonToggle); + fixture.detectChanges(); + + buttonToggleDebugElement = fixture.debugElement.query(By.directive(McButtonToggle)); + buttonToggleButtonElement = fixture.debugElement.query(By.css('button')).nativeElement; + + buttonToggleInstance = buttonToggleDebugElement.componentInstance; + })); + + it('should toggle when clicked', fakeAsync(() => { + buttonToggleButtonElement.click(); + fixture.detectChanges(); + flush(); + + expect(buttonToggleInstance.checked).toBe(true); + + buttonToggleButtonElement.click(); + fixture.detectChanges(); + flush(); + + expect(buttonToggleInstance.checked).toBe(false); + })); + + it('should emit a change event for state changes', fakeAsync(() => { + + expect(buttonToggleInstance.checked).toBe(false); + + const changeSpy = jasmine.createSpy('button-toggle change listener'); + buttonToggleInstance.change.subscribe(changeSpy); + + buttonToggleButtonElement.click(); + fixture.detectChanges(); + tick(); + expect(changeSpy).toHaveBeenCalled(); + + buttonToggleButtonElement.click(); + fixture.detectChanges(); + tick(); + + // The default browser behavior is to emit an event, when the value was set + // to false. That's because the current input type is set to `checkbox`. + // tslint:disable-next-line:no-magic-numbers + expect(changeSpy).toHaveBeenCalledTimes(2); + })); + }); + + it('should not throw on init when toggles are repeated and there is an initial value', () => { + const fixture = TestBed.createComponent(RepeatedButtonTogglesWithPreselectedValue); + + expect(() => fixture.detectChanges()).not.toThrow(); + expect(fixture.componentInstance.toggleGroup.value).toBe('Two'); + expect(fixture.componentInstance.toggles.toArray()[1].checked).toBe(true); + }); + + it('should maintain the selected state when the value and toggles are swapped out at ' + + 'the same time', () => { + const fixture = TestBed.createComponent(RepeatedButtonTogglesWithPreselectedValue); + fixture.detectChanges(); + + expect(fixture.componentInstance.toggleGroup.value).toBe('Two'); + expect(fixture.componentInstance.toggles.toArray()[1].checked).toBe(true); + + fixture.componentInstance.possibleValues = ['Five', 'Six', 'Seven']; + fixture.componentInstance.value = 'Seven'; + fixture.detectChanges(); + + expect(fixture.componentInstance.toggleGroup.value).toBe('Seven'); + // tslint:disable-next-line:no-magic-numbers + expect(fixture.componentInstance.toggles.toArray()[2].checked).toBe(true); + }); + + it('should select falsy button toggle value in multiple selection', () => { + const fixture = TestBed.createComponent(FalsyButtonTogglesInsideButtonToggleGroupMultiple); + fixture.detectChanges(); + + expect(fixture.componentInstance.toggles.toArray()[0].checked).toBe(true); + expect(fixture.componentInstance.toggles.toArray()[1].checked).toBe(false); + // tslint:disable-next-line:no-magic-numbers + expect(fixture.componentInstance.toggles.toArray()[2].checked).toBe(false); + + fixture.componentInstance.value = [0, false]; + fixture.detectChanges(); + + expect(fixture.componentInstance.toggles.toArray()[0].checked).toBe(true); + expect(fixture.componentInstance.toggles.toArray()[1].checked).toBe(false); + // tslint:disable-next-line:no-magic-numbers + expect(fixture.componentInstance.toggles.toArray()[2].checked).toBe(true); + }); +}); + +@Component({ + template: ` + + + Test1 + + + Test2 + + + Test3 + + + ` +}) +class ButtonTogglesInsideButtonToggleGroup { + isGroupDisabled: boolean = false; + isVertical: boolean = false; + groupValue: string; + renderFirstToggle = true; +} + +@Component({ + template: ` + + + {{option.label}} + + + ` +}) +class ButtonToggleGroupWithNgModel { + groupName = 'group-name'; + modelValue: string; + options = [ + {label: 'Red', value: 'red'}, + {label: 'Green', value: 'green'}, + {label: 'Blue', value: 'blue'} + ]; + lastEvent: McButtonToggleChange; +} + +@Component({ + template: ` + + + Eggs + + + Flour + + + Sugar + + + ` +}) +class ButtonTogglesInsideButtonToggleGroupMultiple { + isGroupDisabled: boolean = false; + isVertical: boolean = false; +} + +@Component({ + template: ` + + + Eggs + + + Flour + + + Sugar + + Sugar + + ` +}) +class FalsyButtonTogglesInsideButtonToggleGroupMultiple { + value: ('' | number | null | undefined | boolean)[] = [0]; + @ViewChildren(McButtonToggle) toggles: QueryList; +} + +@Component({ + template: ` + + Yes + + ` +}) +class StandaloneButtonToggle { +} + +@Component({ + template: ` + + + Value Red + + + Value Green + + + ` +}) +class ButtonToggleGroupWithInitialValue { + lastEvent: McButtonToggleChange; +} + +@Component({ + template: ` + + + Value Red + + + Value Green + + + Value Blue + + + ` +}) +class ButtonToggleGroupWithFormControl { + control = new FormControl(); +} + +@Component({ + template: ` + + + {{toggle}} + + + ` +}) +class RepeatedButtonTogglesWithPreselectedValue { + @ViewChild(McButtonToggleGroup) toggleGroup: McButtonToggleGroup; + @ViewChildren(McButtonToggle) toggles: QueryList; + + possibleValues = ['One', 'Two', 'Three']; + value = 'Two'; +} + diff --git a/src/lib/button-toggle/button-toggle.component.ts b/src/lib/button-toggle/button-toggle.component.ts new file mode 100644 index 000000000..6aef5470e --- /dev/null +++ b/src/lib/button-toggle/button-toggle.component.ts @@ -0,0 +1,430 @@ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + Directive, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnDestroy, + OnInit, + Optional, + Output, + QueryList, + ViewEncapsulation, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { FocusMonitor } from '@ptsecurity/cdk/a11y'; +import { coerceBooleanProperty } from '@ptsecurity/cdk/coercion'; +import { SelectionModel } from '@ptsecurity/cdk/collections'; +import { McButton } from '@ptsecurity/mosaic/button'; + + +/** Acceptable types for a button toggle. */ +export type ToggleType = 'checkbox' | 'radio'; + +/** + * Provider Expression that allows mc-button-toggle-group to register as a ControlValueAccessor. + * This allows it to support [(ngModel)]. + * @docs-private + */ +export const MC_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => McButtonToggleGroup), + multi: true +}; + +/** Change event object emitted by MсButtonToggle. */ +export class McButtonToggleChange { + constructor( + /** The MсButtonToggle that emits the event. */ + public source: McButtonToggle, + /** The value assigned to the MсButtonToggle. */ + public value: any) { + } +} + +/** Exclusive selection button toggle group that behaves like a radio-button group. */ +@Directive({ + selector: 'mc-button-toggle-group', + providers: [MC_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR], + host: { + role: 'group', + class: 'mc-button-toggle-group', + '[class.mc-button-toggle-vertical]': 'vertical' + }, + exportAs: 'mcButtonToggleGroup' +}) +export class McButtonToggleGroup implements ControlValueAccessor, OnInit, AfterContentInit { + + /** Whether the toggle group is vertical. */ + @Input() + get vertical(): boolean { + return this._vertical; + } + + set vertical(value: boolean) { + this._vertical = coerceBooleanProperty(value); + } + + /** Value of the toggle group. */ + @Input() + get value(): any { + const selected = this.selectionModel ? this.selectionModel.selected : []; + + if (this.multiple) { + return selected.map((toggle) => toggle.value); + } + + return selected[0] ? selected[0].value : undefined; + } + + set value(newValue: any) { + this.setSelectionByValue(newValue); + this.valueChange.emit(this.value); + } + + /** Selected button toggles in the group. */ + get selected(): any { + const selected = this.selectionModel.selected; + + return this.multiple ? selected : (selected[0] || null); + } + + /** Whether multiple button toggles can be selected. */ + @Input() + get multiple(): boolean { + return this._multiple; + } + + set multiple(value: boolean) { + this._multiple = coerceBooleanProperty(value); + } + + /** Child button toggle buttons. */ + @ContentChildren(forwardRef(() => McButtonToggle)) buttonToggles: QueryList; + + /** Whether multiple button toggle group is disabled. */ + @Input() + get disabled(): boolean { + return this._disabled; + } + + set disabled(value: boolean) { + this._disabled = coerceBooleanProperty(value); + + if (!this.buttonToggles) { + return; + } + + this.buttonToggles.forEach((toggle) => toggle.markForCheck()); + } + + /** + * Event that emits whenever the value of the group changes. + * Used to facilitate two-way data binding. + * @docs-private + */ + @Output() readonly valueChange = new EventEmitter(); + + /** Event emitted when the group's value changes. */ + @Output() readonly change: EventEmitter = + new EventEmitter(); + private _vertical = false; + private _multiple = false; + private _disabled = false; + private selectionModel: SelectionModel; + + /** + * Reference to the raw value that the consumer tried to assign. The real + * value will exclude any values from this one that don't correspond to a + * toggle. Useful for the cases where the value is assigned before the toggles + * have been initialized or at the same that they're being swapped out. + */ + private rawValue: any; + + constructor(private _changeDetector: ChangeDetectorRef) {} + + /** + * The method to be called in order to update ngModel. + * Now `ngModel` binding is not supported in multiple selection mode. + */ + // tslint:disable-next-line:no-empty + controlValueAccessorChangeFn: (value: any) => void = () => {}; + + /** onTouch function registered via registerOnTouch (ControlValueAccessor). */ + // tslint:disable-next-line:no-empty + onTouched: () => any = () => {}; + + ngOnInit() { + this.selectionModel = new SelectionModel(this.multiple, undefined, false); + } + + ngAfterContentInit() { + this.selectionModel.select(...this.buttonToggles.filter((toggle) => toggle.checked)); + this.disabled = this._disabled; + } + + /** + * Sets the model value. Implemented as part of ControlValueAccessor. + * @param value Value to be set to the model. + */ + writeValue(value: any) { + this.value = value; + this._changeDetector.markForCheck(); + } + + // Implemented as part of ControlValueAccessor. + registerOnChange(fn: (value: any) => void) { + this.controlValueAccessorChangeFn = fn; + } + + // Implemented as part of ControlValueAccessor. + registerOnTouched(fn: any) { + this.onTouched = fn; + } + + // Implemented as part of ControlValueAccessor. + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + /** Dispatch change event with current selection and group value. */ + emitChangeEvent(): void { + const selected = this.selected; + const source = Array.isArray(selected) ? selected[selected.length - 1] : selected; + const event = new McButtonToggleChange(source, this.value); + this.controlValueAccessorChangeFn(event.value); + this.change.emit(event); + } + + /** + * Syncs a button toggle's selected state with the model value. + * @param toggle Toggle to be synced. + * @param select Whether the toggle should be selected. + * @param isUserInput Whether the change was a result of a user interaction. + */ + syncButtonToggle(toggle: McButtonToggle, select: boolean, isUserInput = false) { + // Deselect the currently-selected toggle, if we're in single-selection + // mode and the button being toggled isn't selected at the moment. + if (!this.multiple && this.selected && !toggle.checked) { + (this.selected as McButtonToggle).checked = false; + } + + if (select) { + this.selectionModel.select(toggle); + } else { + this.selectionModel.deselect(toggle); + } + + // Only emit the change event for user input. + if (isUserInput) { + this.emitChangeEvent(); + } + + // Note: we emit this one no matter whether it was a user interaction, because + // it is used by Angular to sync up the two-way data binding. + this.valueChange.emit(this.value); + } + + /** Checks whether a button toggle is selected. */ + isSelected(toggle: McButtonToggle) { + return this.selectionModel.isSelected(toggle); + } + + /** Determines whether a button toggle should be checked on init. */ + isPrechecked(toggle: McButtonToggle) { + if (this.rawValue === undefined) { + return false; + } + + if (this.multiple && Array.isArray(this.rawValue)) { + return this.rawValue.some((value) => toggle.value != null && value === toggle.value); + } + + return toggle.value === this.rawValue; + } + + /** Updates the selection state of the toggles in the group based on a value. */ + private setSelectionByValue(value: any | any[]) { + this.rawValue = value; + + if (!this.buttonToggles) { + return; + } + + if (this.multiple && value) { + if (!Array.isArray(value)) { + throw Error('Value must be an array in multiple-selection mode.'); + } + + this.clearSelection(); + value.forEach((currentValue: any) => this.selectValue(currentValue)); + } else { + this.clearSelection(); + this.selectValue(value); + } + } + + /** Clears the selected toggles. */ + private clearSelection() { + this.selectionModel.clear(); + this.buttonToggles.forEach((toggle) => toggle.checked = false); + } + + /** Selects a value if there's a toggle that corresponds to it. */ + private selectValue(value: any) { + const correspondingOption = this.buttonToggles.find((toggle) => { + return toggle.value != null && toggle.value === value; + }); + + if (correspondingOption) { + correspondingOption.checked = true; + this.selectionModel.select(correspondingOption); + } + } +} + +/** Single button inside of a toggle group. */ +@Component({ + selector: 'mc-button-toggle', + template: ` + + `, + styleUrls: ['button-toggle.css'], + encapsulation: ViewEncapsulation.None, + exportAs: 'mcButtonToggle', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[class.mc-button-toggle-standalone]': '!buttonToggleGroup', + '[class.mc-button-toggle-checked]': 'checked', + class: 'mc-button-toggle', + // Always reset the tabindex to -1 so it doesn't conflict with the one on the `button`, + // but can still receive focus from things like cdkFocusInitial. + '[attr.tabindex]': '-1', + '[attr.disabled]': 'disabled || null', + '(focus)': 'focus()' + } +}) +export class McButtonToggle implements OnInit, OnDestroy { + + /** Whether the button is checked. */ + @Input() + get checked(): boolean { + return this.buttonToggleGroup ? this.buttonToggleGroup.isSelected(this) : this._checked; + } + + set checked(value: boolean) { + const newValue = coerceBooleanProperty(value); + + if (newValue !== this._checked) { + this._checked = newValue; + + if (this.buttonToggleGroup) { + this.buttonToggleGroup.syncButtonToggle(this, this._checked); + } + + this.changeDetectorRef.markForCheck(); + } + } + + // tslint:disable-next-line:no-reserved-keywords + type: ToggleType; + + @ViewChild(McButton) mcButton: McButton; + + /** McButtonToggleGroup reads this to assign its own value. */ + @Input() value: any; + + /** Tabindex for the toggle. */ + @Input() tabIndex: number | null; + + @Input() + get disabled(): boolean { + return this._disabled || (this.buttonToggleGroup && this.buttonToggleGroup.disabled); + } + set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } + + /** Event emitted when the group value changes. */ + @Output() readonly change: EventEmitter = + new EventEmitter(); + + private isSingleSelector = false; + private _checked = false; + private _disabled: boolean = false; + + constructor( + @Optional() public buttonToggleGroup: McButtonToggleGroup, + private changeDetectorRef: ChangeDetectorRef, + private focusMonitor: FocusMonitor, + private element: ElementRef + ) {} + + ngOnInit() { + this.isSingleSelector = this.buttonToggleGroup && !this.buttonToggleGroup.multiple; + this.type = this.isSingleSelector ? 'radio' : 'checkbox'; + + if (this.buttonToggleGroup && this.buttonToggleGroup.isPrechecked(this)) { + this.checked = true; + } + + this.focusMonitor.monitor(this.element.nativeElement, true); + } + + ngOnDestroy() { + const group = this.buttonToggleGroup; + + this.focusMonitor.stopMonitoring(this.element.nativeElement); + + // Remove the toggle from the selection once it's destroyed. Needs to happen + // on the next tick in order to avoid "changed after checked" errors. + if (group && group.isSelected(this)) { + Promise.resolve().then(() => group.syncButtonToggle(this, false)); + } + } + + /** Focuses the button. */ + focus(): void { + this.element.nativeElement.focus(); + } + + /** Checks the button toggle due to an interaction with the underlying native button. */ + onToggleClick() { + if (this.disabled) { + return; + } + + const newChecked = this.isSingleSelector ? true : !this._checked; + + if (newChecked !== this._checked) { + this._checked = newChecked; + if (this.buttonToggleGroup) { + this.buttonToggleGroup.syncButtonToggle(this, this._checked, true); + this.buttonToggleGroup.onTouched(); + } + } + // Emit a change event when it's the single selector + this.change.emit(new McButtonToggleChange(this, this.value)); + } + + /** + * Marks the button toggle as needing checking for change detection. + * This method is exposed because the parent button toggle group will directly + * update bound properties of the radio button. + */ + markForCheck() { + // When the group value changes, the button will not be notified. + // Use `markForCheck` to explicit update button toggle's status. + this.changeDetectorRef.markForCheck(); + } +} diff --git a/src/lib/button-toggle/button-toggle.module.ts b/src/lib/button-toggle/button-toggle.module.ts new file mode 100644 index 000000000..e9b29c6f9 --- /dev/null +++ b/src/lib/button-toggle/button-toggle.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { McButtonModule } from '@ptsecurity/mosaic/button'; +import { McCommonModule } from '@ptsecurity/mosaic/core'; + +import { McButtonToggle, McButtonToggleGroup } from './button-toggle.component'; + + +@NgModule({ + imports: [McCommonModule, McButtonModule], + exports: [McCommonModule, McButtonToggleGroup, McButtonToggle], + declarations: [McButtonToggleGroup, McButtonToggle] +}) +export class McButtonToggleModule {} diff --git a/src/lib/button-toggle/button-toggle.scss b/src/lib/button-toggle/button-toggle.scss new file mode 100644 index 000000000..371c3c604 --- /dev/null +++ b/src/lib/button-toggle/button-toggle.scss @@ -0,0 +1,93 @@ +@import '../core/styles/common/vendor-prefixes'; +@import '../core/styles/common/layout'; +@import '../../cdk/a11y/a11y'; +@import '../core/styles/common/button'; + +$mc-button-toggle-standard-padding: 0 12px !default; +$mc-button-toggle-standard-height: 20px !default; +$mc-button-toggle-standard-border-radius: 2px !default; + +$mc-button-toggle-border-size: 1px; +$mc-button-toggle-border-radius: 3px; + +$mc-button-toggle-padding: 5px 7px; + +.mc-button-toggle-group { + display: flex; + flex-direction: row; + + &:not(.mc-button-toggle-vertical) { + .mc-button-toggle:not([disabled]) + .mc-button-toggle:not([disabled]) { + margin-left: -$mc-button-toggle-border-size; + } + } + + .mc-button-toggle { + + > .mc-button { + padding: $mc-button-toggle-padding; + } + + &:first-of-type:not(:last-of-type) { + > .mc-button, + > .mc-icon-button { + @include border-right-radius(0); + } + } + + &:last-of-type:not(:first-of-type) { + > .mc-button, + > .mc-icon-button { + @include border-left-radius(0); + } + } + + &:not(:first-of-type):not(:last-of-type) { + > .mc-button, + > .mc-icon-button { + border-radius: 0; + } + } + + } +} + +.mc-button-toggle-vertical { + flex-direction: column; + + .mc-button-toggle:not([disabled]) + .mc-button-toggle:not([disabled]) { + margin-top: -$mc-button-toggle-border-size; + } + + .mc-button-toggle { + .mc-button, + .mc-icon-button { + width: 100%; + } + + &:first-child:not(:last-child) { + > .mc-button, + > .mc-icon-button { + @include border-bottom-radius(0); + + border-top-right-radius: $mc-button-toggle-border-radius; + } + } + + &:last-child:not(:first-child) { + > .mc-button, + > .mc-icon-button { + @include border-top-radius(0); + + border-bottom-left-radius: $mc-button-toggle-border-radius; + } + } + + &:not(:first-child):not(:last-child) { + > .mc-button, + > .mc-icon-button { + border-radius: 0; + } + } + } +} diff --git a/src/lib/button-toggle/index.ts b/src/lib/button-toggle/index.ts new file mode 100644 index 000000000..7e1a213e3 --- /dev/null +++ b/src/lib/button-toggle/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/src/lib/button-toggle/public-api.ts b/src/lib/button-toggle/public-api.ts new file mode 100644 index 000000000..bf0faccd5 --- /dev/null +++ b/src/lib/button-toggle/public-api.ts @@ -0,0 +1,2 @@ +export * from './button-toggle.module'; +export * from './button-toggle.component'; diff --git a/src/lib/button-toggle/tsconfig.build.json b/src/lib/button-toggle/tsconfig.build.json new file mode 100644 index 000000000..dad2ec50d --- /dev/null +++ b/src/lib/button-toggle/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.build", + "files": [ + "public-api.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "strictMetadataEmit": true, + "flatModuleOutFile": "index.js", + "flatModuleId": "@ptsecurity/mosaic/button-toggle", + "skipTemplateCodegen": true, + "fullTemplateTypeCheck": true + } +} diff --git a/src/lib/core/styles/typography/_all-typography.scss b/src/lib/core/styles/typography/_all-typography.scss index 696b95694..637f7d759 100644 --- a/src/lib/core/styles/typography/_all-typography.scss +++ b/src/lib/core/styles/typography/_all-typography.scss @@ -2,6 +2,7 @@ @import '../alerts'; @import '../badges'; @import '../../../button/button-theme'; +@import '../../../button-toggle/button-toggle-theme'; @import '../../../checkbox/checkbox-theme'; @import '../../../datepicker/datepicker-theme'; @import '../../../dropdown/dropdown-theme'; @@ -33,6 +34,7 @@ @include mc-alert-typography($config); @include mc-badge-typography($config); @include mc-button-typography($config); + @include mc-button-toggle-typography($config); @include mc-checkbox-typography($config); @include mc-datepicker-typography($config); @include mc-dropdown-typography($config); diff --git a/src/lib/core/theming/_all-theme.scss b/src/lib/core/theming/_all-theme.scss index b5a46b098..169906846 100644 --- a/src/lib/core/theming/_all-theme.scss +++ b/src/lib/core/theming/_all-theme.scss @@ -3,6 +3,7 @@ @import '../styles/badges'; @import '../styles/alerts'; @import '../../button/button-theme'; +@import '../../button-toggle/button-toggle-theme'; @import '../../card/card-theme'; @import '../../checkbox/checkbox-theme'; @import '../../datepicker/datepicker-theme'; @@ -39,6 +40,7 @@ @include mc-alert-theme($theme); @include mc-badge-theme($theme); @include mc-button-theme($theme); + @include mc-button-toggle-theme($theme); @include mc-card-theme($theme); @include mc-datepicker-theme($theme); @include mc-dropdown-theme($theme); diff --git a/src/lib/public-api.ts b/src/lib/public-api.ts index 3769d92c2..508bc10d8 100644 --- a/src/lib/public-api.ts +++ b/src/lib/public-api.ts @@ -3,6 +3,7 @@ export * from './version'; export * from '@ptsecurity/mosaic/core'; export * from '@ptsecurity/mosaic/button'; +export * from '@ptsecurity/mosaic/button-toggle'; export * from '@ptsecurity/mosaic/card'; export * from '@ptsecurity/mosaic/checkbox'; export * from '@ptsecurity/mosaic/datepicker'; diff --git a/tests/karma-system-config.js b/tests/karma-system-config.js index bf05dca7c..0cba8d0c9 100644 --- a/tests/karma-system-config.js +++ b/tests/karma-system-config.js @@ -57,6 +57,7 @@ System.config({ '@ptsecurity/mosaic-moment-adapter/adapter': 'dist/packages/mosaic-moment-adapter/adapter/index.js', '@ptsecurity/mosaic/button': 'dist/packages/mosaic/button/index.js', + '@ptsecurity/mosaic/button-toggle': 'dist/packages/mosaic/button-toggle/index.js', '@ptsecurity/mosaic/core': 'dist/packages/mosaic/core/index.js', '@ptsecurity/mosaic/divider': 'dist/packages/mosaic/divider/index.js', '@ptsecurity/mosaic/dropdown': 'dist/packages/mosaic/dropdown/index.js',