diff --git a/pages/toggle_action_button.html b/pages/toggle_action_button.html index 3d236641..f12c0408 100644 --- a/pages/toggle_action_button.html +++ b/pages/toggle_action_button.html @@ -26,6 +26,27 @@
+
+

Disabled Disableable Toggle Button

+
+ + +
+

Toggle Button


diff --git a/src/Index.ts b/src/Index.ts index 1b6a0df0..0a5e1d55 100644 --- a/src/Index.ts +++ b/src/Index.ts @@ -2,6 +2,7 @@ export { ActionButton } from './components/ActionButton/ActionButton'; export { ToggleActionButton } from './components/ActionButton/ToggleActionButton'; +export { DisableableToggleActionButton } from './components/ActionButton/DisableableToggleActionButton'; export { AttachResult } from './components/AttachResult/AttachResult'; export { UserActivity } from './components/UserActions/UserActivity'; export { UserActions } from './components/UserActions/UserActions'; diff --git a/src/components/ActionButton/DisableableActionButton.scss b/src/components/ActionButton/DisableableActionButton.scss new file mode 100644 index 00000000..28daf4cb --- /dev/null +++ b/src/components/ActionButton/DisableableActionButton.scss @@ -0,0 +1,15 @@ +@import './ActionButton.scss'; +@import '../../sass/Variables.scss'; + +$disable-color: $primary-color-light; + +button.CoveoActionButton.coveo-actionbutton.coveo-actionbutton-disabled { + background-color: $primary-color-lightest; + border-color: $disable-color; + + .coveo-actionbutton_icon { + svg { + fill: $disable-color; + } + } +} diff --git a/src/components/ActionButton/DisableableButton.ts b/src/components/ActionButton/DisableableButton.ts new file mode 100644 index 00000000..556e1716 --- /dev/null +++ b/src/components/ActionButton/DisableableButton.ts @@ -0,0 +1,34 @@ +import { StatefulActionButton, IStatefulActionButtonOptionsWithIcon } from './StatefulActionButton'; + +export interface IDisableableButtonOptions { + disabledIcon: string; + disabledTooltip: string; +} + +export interface IDisableableButton { + options: IDisableableButtonOptions; +} + +export class DisabledState implements IStatefulActionButtonOptionsWithIcon { + static DISABLED_CLASS_NAME = 'coveo-actionbutton-disabled'; + public readonly onStateEntry: (this: StatefulActionButton) => void; + public readonly onStateExit: (this: StatefulActionButton) => void; + public readonly click: () => void; + public readonly icon: string; + public readonly tooltip: string; + public readonly name = 'DisabledState'; + + constructor(disabledButton: IDisableableButton) { + this.onStateEntry = function () { + this.element.classList.add(DisabledState.DISABLED_CLASS_NAME); + this.element.setAttribute('disabled', ''); + }; + this.onStateExit = function () { + this.element.classList.remove(DisabledState.DISABLED_CLASS_NAME); + this.element.removeAttribute('disabled'); + }; + this.click = () => {}; + this.icon = disabledButton.options.disabledIcon; + this.tooltip = disabledButton.options.disabledTooltip; + } +} diff --git a/src/components/ActionButton/DisableableToggleActionButton.ts b/src/components/ActionButton/DisableableToggleActionButton.ts new file mode 100644 index 00000000..5f8a6d5f --- /dev/null +++ b/src/components/ActionButton/DisableableToggleActionButton.ts @@ -0,0 +1,120 @@ +import { ComponentOptions, IResultsComponentBindings, Component, Initialization } from 'coveo-search-ui'; +import { StatefulActionButton } from './StatefulActionButton'; +import { + ToggleActivatedState as ActivatedState, + ToggleDeactivatedState as DeactivatedState, + IToggleableButton, + IToggleableButtonOptions, +} from './ToggleableButton'; +import { IDisableableButton, IDisableableButtonOptions, DisabledState } from './DisableableButton'; +import { ToggleActionButton } from './ToggleActionButton'; + +export interface IDisableableToggleActionButtonOptions extends IToggleableButtonOptions, IDisableableButtonOptions {} + +export class DisableableToggleActionButton extends Component implements IToggleableButton, IDisableableButton { + static ID = 'DisableableToggleActionButton'; + static ACTIVATED_CLASS_NAME = 'coveo-toggleactionbutton-activated'; + + private innerStatefulActionButton: StatefulActionButton; + private activatedState: ActivatedState; + private deactivatedState: DeactivatedState; + private disabledState: DisabledState; + + static options: IDisableableToggleActionButtonOptions = { + ...ToggleActionButton.options, + disabledTooltip: ComponentOptions.buildStringOption(), + disabledIcon: ComponentOptions.buildStringOption(), + }; + + constructor(public element: HTMLElement, public options: IDisableableToggleActionButtonOptions, public bindings?: IResultsComponentBindings) { + super(element, DisableableToggleActionButton.ID, bindings); + this.options = ComponentOptions.initComponentOptions(element, DisableableToggleActionButton, options); + + this.createInnerButton(bindings); + } + + /** + * Indicates whether the toggle button is in the activated state. + */ + public isActivated(): boolean { + return this.innerStatefulActionButton.getCurrentState() === this.activatedState; + } + + /** + * Indicates whether the disableable toggle button is in the disable state. + */ + public isDisabled(): boolean { + return this.innerStatefulActionButton.getCurrentState() === this.disabledState; + } + + /** + * Sets the toggle button to the specified state. + * @param activated Whether the button is activated. + */ + public setActivated(activated: boolean): void { + if (this.isDisabled() && !activated) { + this.innerStatefulActionButton.switchTo(this.deactivatedState); + } + if (!this.isDisabled() && activated !== this.isActivated()) { + this.innerStatefulActionButton.switchTo(activated ? this.activatedState : this.deactivatedState); + } + } + + public setEnabled(enabled: boolean): void { + if (enabled) { + this.enable(); + } else { + this.disable(); + } + } + + public disable(): void { + if (this.isDisabled()) { + return; + } + if (this.isActivated()) { + this.innerStatefulActionButton.switchTo(this.deactivatedState); + } + this.innerStatefulActionButton.switchTo(this.disabledState); + } + + public enable(): void { + if (this.isDisabled()) { + this.innerStatefulActionButton.switchTo(this.deactivatedState); + } + } + + public onClick(): void { + if (this.isDisabled()) { + return; + } + this.setActivated(!this.isActivated()); + + if (this.options.click) { + this.options.click(); + } + } + + private createInnerButton(bindings?: IResultsComponentBindings): void { + this.deactivatedState = new DeactivatedState(this); + this.disabledState = new DisabledState(this); + this.activatedState = new ActivatedState(this); + + this.innerStatefulActionButton = new StatefulActionButton( + this.element, + { + initialState: this.deactivatedState, + states: [this.deactivatedState, this.activatedState, this.disabledState], + allowedTransitions: [ + { from: this.deactivatedState, to: this.disabledState }, + { from: this.disabledState, to: this.deactivatedState }, + { from: this.deactivatedState, to: this.activatedState }, + { from: this.activatedState, to: this.deactivatedState }, + ], + }, + bindings + ); + } +} + +Initialization.registerAutoCreateComponent(DisableableToggleActionButton); diff --git a/src/components/ActionButton/StatefulActionButton.ts b/src/components/ActionButton/StatefulActionButton.ts index c2fda486..27cd141a 100644 --- a/src/components/ActionButton/StatefulActionButton.ts +++ b/src/components/ActionButton/StatefulActionButton.ts @@ -1,10 +1,12 @@ import { IResultsComponentBindings } from 'coveo-search-ui'; -import { ActionButton, ActionButtonOptions } from './ActionButton'; +import { ActionButton, ActionButtonOptions, IActionButtonOptionsWithTitle, IActionButtonOptionsWithIcon } from './ActionButton'; /** * Represent a state that can be used by a StatefulActionButton. */ export type StatefulActionButtonState = ActionButtonOptions & IStateOptions; +export interface IStatefulActionButtonOptionsWithTitle extends IActionButtonOptionsWithTitle, IStateOptions {} +export interface IStatefulActionButtonOptionsWithIcon extends IActionButtonOptionsWithIcon, IStateOptions {} export interface IStateOptions { /** diff --git a/src/components/ActionButton/ToggleActionButton.ts b/src/components/ActionButton/ToggleActionButton.ts index 90c3e297..64176506 100644 --- a/src/components/ActionButton/ToggleActionButton.ts +++ b/src/components/ActionButton/ToggleActionButton.ts @@ -1,57 +1,12 @@ import { ComponentOptions, IResultsComponentBindings, Component, Initialization } from 'coveo-search-ui'; +import { ToggleActivatedState, ToggleDeactivatedState, IToggleableButton, IToggleableButtonOptions } from './ToggleableButton'; import { StatefulActionButtonState, StatefulActionButton } from './StatefulActionButton'; -export interface IToggleActionButtonOptions { - activateIcon: string; - activateTooltip: string; - deactivateIcon: string; - deactivateTooltip: string; - click?: () => void; - activate?: () => void; - deactivate?: () => void; -} - -export class ToggleActionButton extends Component { - /** - * Create the deactivated state for a given ToggleActionButton - * @param button {ToggleActionButton} - */ - static generateDeactivatedStateInstance(button: ToggleActionButton): StatefulActionButtonState { - return { - name: 'DeactivatedState', - icon: button.options.activateIcon, - tooltip: button.options.activateTooltip, - click: () => button.onClick(), - }; - } - - /** - * Create the activated state for a given ToggleActionButton - * @param button {ToggleActionButton} - */ - static generateActivatedStateInstance(button: ToggleActionButton): StatefulActionButtonState { - return { - onStateEntry: function () { - this.element.classList.add(ToggleActionButton.ACTIVATED_CLASS_NAME); - this.element.setAttribute('aria-pressed', 'true'); - button.options.activate?.apply(button); - }, - onStateExit: function () { - this.element.classList.remove(ToggleActionButton.ACTIVATED_CLASS_NAME); - this.element.setAttribute('aria-pressed', 'false'); - button.options.deactivate?.apply(button); - }, - name: 'ActivatedState', - click: () => button.onClick(), - icon: button.options.deactivateIcon, - tooltip: button.options.deactivateTooltip, - }; - } - +export class ToggleActionButton extends Component implements IToggleableButton { static ID = 'ToggleActionButton'; static ACTIVATED_CLASS_NAME = 'coveo-toggleactionbutton-activated'; - static options: IToggleActionButtonOptions = { + static options: IToggleableButtonOptions = { /** * Specifies the button SVG icon displayed to activate the button. * Note: The SVG markup has to be HTML encoded when set using the HTML attributes. @@ -145,7 +100,7 @@ export class ToggleActionButton extends Component { private activatedState: StatefulActionButtonState; private deactivatedState: StatefulActionButtonState; - constructor(public element: HTMLElement, public options: IToggleActionButtonOptions, public bindings?: IResultsComponentBindings) { + constructor(public element: HTMLElement, public options: IToggleableButtonOptions, public bindings?: IResultsComponentBindings) { super(element, ToggleActionButton.ID, bindings); this.options = ComponentOptions.initComponentOptions(element, ToggleActionButton, options); @@ -169,7 +124,7 @@ export class ToggleActionButton extends Component { } } - protected onClick(): void { + public onClick(): void { this.setActivated(!this.isActivated()); if (this.options.click) { @@ -178,8 +133,8 @@ export class ToggleActionButton extends Component { } private createInnerButton(bindings?: IResultsComponentBindings): void { - this.activatedState = ToggleActionButton.generateActivatedStateInstance(this); - this.deactivatedState = ToggleActionButton.generateDeactivatedStateInstance(this); + this.deactivatedState = new ToggleDeactivatedState(this); + this.activatedState = new ToggleActivatedState(this); this.innerStatefulActionButton = new StatefulActionButton( this.element, diff --git a/src/components/ActionButton/ToggleableButton.ts b/src/components/ActionButton/ToggleableButton.ts new file mode 100644 index 00000000..cb52c5d7 --- /dev/null +++ b/src/components/ActionButton/ToggleableButton.ts @@ -0,0 +1,53 @@ +import { StatefulActionButton, IStatefulActionButtonOptionsWithIcon } from './StatefulActionButton'; + +export interface IToggleableButtonOptions { + activateIcon: string; + activateTooltip: string; + deactivateIcon: string; + deactivateTooltip: string; + click?: () => void; + activate?: () => void; + deactivate?: () => void; +} + +export interface IToggleableButton { + options: IToggleableButtonOptions; + onClick: () => void; +} + +export class ToggleDeactivatedState implements IStatefulActionButtonOptionsWithIcon { + public readonly name = 'ToggleDeactivatedState'; + public readonly icon: string; + public readonly tooltip: string; + public readonly click: { (): void; (): void; (): void }; + constructor(toggleableButton: IToggleableButton) { + this.icon = toggleableButton.options.activateIcon; + this.tooltip = toggleableButton.options.activateTooltip; + this.click = () => toggleableButton.onClick(); + } +} + +export class ToggleActivatedState implements IStatefulActionButtonOptionsWithIcon { + static ACTIVATED_CLASS_NAME = 'coveo-toggleactionbutton-activated'; + public readonly name = 'ToggleActivatedState'; + public readonly onStateEntry: (this: StatefulActionButton) => void; + public readonly onStateExit: (this: StatefulActionButton) => void; + public readonly click: () => void; + public readonly icon: string; + public readonly tooltip: string; + constructor(toggleableButton: IToggleableButton) { + this.onStateEntry = function () { + this.element.classList.add(ToggleActivatedState.ACTIVATED_CLASS_NAME); + this.element.setAttribute('aria-pressed', 'true'); + toggleableButton.options.activate?.apply(toggleableButton); + }; + this.onStateExit = function () { + this.element.classList.remove(ToggleActivatedState.ACTIVATED_CLASS_NAME); + this.element.setAttribute('aria-pressed', 'false'); + toggleableButton.options.deactivate?.apply(toggleableButton); + }; + this.click = () => toggleableButton.onClick(); + this.icon = toggleableButton.options.deactivateIcon; + this.tooltip = toggleableButton.options.deactivateTooltip; + } +} diff --git a/src/sass/Index.scss b/src/sass/Index.scss index 999bc5d5..43b27f0b 100644 --- a/src/sass/Index.scss +++ b/src/sass/Index.scss @@ -1,5 +1,6 @@ @import '../components/ActionButton/ActionButton.scss'; @import '../components/ActionButton/ToggleActionButton.scss'; +@import '../components/ActionButton/DisableableActionButton.scss'; @import '../components/AttachResult/AttachResult.scss'; @import '../components/UserActions/UserActions.scss'; @import '../components/ViewedByCustomer/ViewedByCustomer.scss'; diff --git a/tests/components/ActionButton/DisableableButton.spec.ts b/tests/components/ActionButton/DisableableButton.spec.ts new file mode 100644 index 00000000..3e13fe89 --- /dev/null +++ b/tests/components/ActionButton/DisableableButton.spec.ts @@ -0,0 +1,50 @@ +import { DisabledState } from '../../../src/components/ActionButton/DisableableButton'; + +describe('DisabledState', () => { + let testElement: HTMLElement; + let testSubject: DisabledState; + let fakeStatefulActionButton: { element: HTMLElement }; + + beforeEach(() => { + testElement = document.createElement('div'); + fakeStatefulActionButton = { element: testElement }; + testSubject = new DisabledState({ options: { disabledIcon: 'someSvgIcon', disabledTooltip: 'someTooltip' } }); + }); + + describe('constructor', () => { + it('should use the icon and tooltip from the option of the disabledButton', () => { + expect(testSubject.icon).toBe('someSvgIcon'); + expect(testSubject.tooltip).toBe('someTooltip'); + }); + }); + + describe('onStateEntry', () => { + beforeEach(() => { + testSubject.onStateEntry.apply(fakeStatefulActionButton); + }); + + it('should add coveo-actionbutton-disabled to the classlist on this.element of the caller', () => { + expect(testElement.classList.value).toBe('coveo-actionbutton-disabled'); + }); + + it('should add the attribute disabled to this.element of the caller', () => { + expect(testElement.hasAttribute('disabled')).toBeTrue(); + }); + }); + + describe('onStateExit', () => { + beforeEach(() => { + testElement.classList.value = 'coveo-actionbutton-disabled'; + testElement.setAttribute('disabled', ''); + testSubject.onStateExit.apply(fakeStatefulActionButton); + }); + + it('should remove coveo-actionbutton-disabled to the classlist on this.element of the caller', () => { + expect(testElement.classList.value).toBe(''); + }); + + it('should remove the attribute disabled to this.element of the caller', () => { + expect(testElement.hasAttribute('disabled')).toBeFalse(); + }); + }); +}); diff --git a/tests/components/ActionButton/DisableableToggleActionButton.spec.ts b/tests/components/ActionButton/DisableableToggleActionButton.spec.ts new file mode 100644 index 00000000..ff3a9ed1 --- /dev/null +++ b/tests/components/ActionButton/DisableableToggleActionButton.spec.ts @@ -0,0 +1,311 @@ +import { SinonSandbox, SinonSpy, createSandbox } from 'sinon'; +import { + IDisableableToggleActionButtonOptions, + DisableableToggleActionButton, +} from '../../../src/components/ActionButton/DisableableToggleActionButton'; +import * as icons from '../../../src/utils/icons'; + +import { Mock } from 'coveo-search-ui-tests'; +import { StatefulActionButton } from '../../../src/components/ActionButton/StatefulActionButton'; +import { ToggleDeactivatedState, ToggleActivatedState } from '../../../src/components/ActionButton/ToggleableButton'; +import { DisabledState } from '../../../src/components/ActionButton/DisableableButton'; + +describe('DisableableToggleActionButton', () => { + let sandbox: SinonSandbox; + let options: IDisableableToggleActionButtonOptions; + let testSubject: DisableableToggleActionButton; + + let clickSpy: SinonSpy; + let activateSpy: SinonSpy; + let deactivateSpy: SinonSpy; + let switchToSpy: SinonSpy; + + beforeAll(() => { + sandbox = createSandbox(); + + clickSpy = sandbox.spy(); + activateSpy = sandbox.spy(); + deactivateSpy = sandbox.spy(); + switchToSpy = sandbox.spy(StatefulActionButton.prototype, 'switchTo'); + }); + + beforeEach(() => { + options = { + activateIcon: icons.copy, + activateTooltip: 'Activate feature', + deactivateIcon: icons.duplicate, + deactivateTooltip: 'Deactivate feature', + click: clickSpy, + activate: activateSpy, + deactivate: deactivateSpy, + disabledIcon: icons.dot, + disabledTooltip: 'Feature disabled', + }; + + testSubject = createToggleButton(options); + }); + + afterEach(() => { + sandbox.reset(); + }); + + function createToggleButton(options: IDisableableToggleActionButtonOptions) { + const element = document.createElement('button'); + const componentSetup = Mock.advancedComponentSetup( + DisableableToggleActionButton, + new Mock.AdvancedComponentSetupOptions(element, options) + ); + return componentSetup.cmp; + } + + describe('when disabled', () => { + beforeEach(() => { + testSubject.disable(); + switchToSpy.resetHistory(); + }); + + it('clicking it should do nothing', () => { + Coveo.$$(testSubject.element).trigger('click'); + expect(clickSpy.called).toBeFalse(); + }); + + it('isActivated should return false', () => { + expect(testSubject.isActivated()).toBeFalse(); + }); + + it('isDisabled should return true', () => { + expect(testSubject.isDisabled()).toBeTrue(); + }); + + describe('setEnabled', () => { + let spyEnable: SinonSpy; + let spyDisable: SinonSpy; + + beforeEach(() => { + spyEnable = sandbox.spy(testSubject, 'enable'); + spyDisable = sandbox.spy(testSubject, 'disable'); + }); + + it('should do call disabled if called with false', () => { + testSubject.setEnabled(false); + + expect(spyEnable.called).toBeFalse(); + expect(spyDisable.calledOnce).toBeTrue(); + }); + + it('should call enable if called with true', () => { + testSubject.setEnabled(true); + + expect(spyEnable.calledOnce).toBeTrue(); + expect(spyDisable.called).toBeFalse(); + }); + }); + + describe('enable', () => { + beforeEach(() => { + testSubject.enable(); + }); + + it('should call switchTo with the deactivatedState', () => { + expect(switchToSpy.calledOnce).toBeTrue(); + expect(switchToSpy.firstCall.args[0] instanceof ToggleDeactivatedState).toBeTrue(); + }); + }); + + describe('disable', () => { + beforeEach(() => { + testSubject.disable(); + }); + + it('should not call switchTo at all', () => { + expect(switchToSpy.called).toBeFalse(); + }); + }); + + describe('setActivated', () => { + it('should switch to deactivated if called with false', () => { + testSubject.setActivated(false); + + expect(switchToSpy.calledOnce).toBeTrue(); + expect(switchToSpy.firstCall.args[0] instanceof ToggleDeactivatedState).toBeTrue(); + }); + + it('should do nothing if called with true', () => { + testSubject.setActivated(true); + + expect(switchToSpy.called).toBeFalse(); + }); + }); + }); + + describe('when Deactivated', () => { + it('isActivated should return false', () => { + expect(testSubject.isActivated()).toBeFalse(); + }); + it('isDisabled should return false', () => { + expect(testSubject.isDisabled()).toBeFalse(); + }); + + describe('when clicked on', () => { + let setActivatedSpy: SinonSpy; + beforeEach(() => { + setActivatedSpy = sandbox.spy(testSubject, 'setActivated'); + Coveo.$$(testSubject.element).trigger('click'); + }); + + it('should call the click handler and setActivated with true', () => { + expect(clickSpy.called).toBeTrue(); + expect(setActivatedSpy.calledOnceWithExactly(true)).toBeTrue(); + }); + }); + + describe('setEnabled', () => { + let spyEnable: SinonSpy; + let spyDisable: SinonSpy; + + beforeEach(() => { + spyEnable = sandbox.spy(testSubject, 'enable'); + spyDisable = sandbox.spy(testSubject, 'disable'); + }); + + it('should do call disabled if called with false', () => { + testSubject.setEnabled(false); + + expect(spyEnable.called).toBeFalse(); + expect(spyDisable.calledOnce).toBeTrue(); + }); + + it('should call enable if called with true', () => { + testSubject.setEnabled(true); + + expect(spyEnable.calledOnce).toBeTrue(); + expect(spyDisable.called).toBeFalse(); + }); + }); + + describe('enable', () => { + beforeEach(() => { + testSubject.enable(); + }); + + it('should not call switchTo at all', () => { + expect(switchToSpy.called).toBeFalse(); + }); + }); + + describe('disable', () => { + beforeEach(() => { + testSubject.disable(); + }); + + it('should call switchTo with the disabledState', () => { + expect(switchToSpy.calledOnce).toBeTrue(); + expect(switchToSpy.firstCall.args[0] instanceof DisabledState).toBeTrue(); + }); + }); + + describe('setActivated', () => { + it('should do nothing if called with false', () => { + testSubject.setActivated(false); + + expect(switchToSpy.called).toBeFalse(); + }); + + it('should switch to activated if called with true', () => { + testSubject.setActivated(true); + + expect(switchToSpy.calledOnce).toBeTrue(); + expect(switchToSpy.firstCall.args[0] instanceof ToggleActivatedState).toBeTrue(); + }); + }); + }); + + describe('when activated', () => { + beforeEach(() => { + testSubject.setActivated(true); + switchToSpy.resetHistory(); + }); + + it('isActivated should return false', () => { + expect(testSubject.isActivated()).toBeTrue(); + }); + + it('isDisabled should return false', () => { + expect(testSubject.isDisabled()).toBeFalse(); + }); + + describe('when clicked on', () => { + let setActivatedSpy: SinonSpy; + beforeEach(() => { + setActivatedSpy = sandbox.spy(testSubject, 'setActivated'); + Coveo.$$(testSubject.element).trigger('click'); + }); + + it('should call the click handler and setActivated with false', () => { + expect(clickSpy.called).toBeTrue(); + expect(setActivatedSpy.calledOnceWithExactly(false)).toBeTrue(); + }); + }); + + describe('setEnabled', () => { + let spyEnable: SinonSpy; + let spyDisable: SinonSpy; + + beforeEach(() => { + spyEnable = sandbox.spy(testSubject, 'enable'); + spyDisable = sandbox.spy(testSubject, 'disable'); + }); + + it('should do call disabled if called with false', () => { + testSubject.setEnabled(false); + + expect(spyEnable.called).toBeFalse(); + expect(spyDisable.calledOnce).toBeTrue(); + }); + + it('should call enable if called with true', () => { + testSubject.setEnabled(true); + + expect(spyEnable.calledOnce).toBeTrue(); + expect(spyDisable.called).toBeFalse(); + }); + }); + + describe('enable', () => { + beforeEach(() => { + testSubject.enable(); + }); + + it('should not call switchTo at all', () => { + expect(switchToSpy.called).toBeFalse(); + }); + }); + + describe('disable', () => { + beforeEach(() => { + testSubject.disable(); + }); + + it('should call switchTo with the DeactivatedState and then with the disabledState', () => { + expect(switchToSpy.calledTwice).toBeTrue(); + expect(switchToSpy.firstCall.args[0] instanceof ToggleDeactivatedState).toBeTrue(); + expect(switchToSpy.secondCall.args[0] instanceof DisabledState).toBeTrue(); + }); + }); + + describe('setActivated', () => { + it('should do nothing if called with true', () => { + testSubject.setActivated(true); + + expect(switchToSpy.called).toBeFalse(); + }); + + it('should switch to Deactivated if called with false', () => { + testSubject.setActivated(false); + + expect(switchToSpy.calledOnce).toBeTrue(); + expect(switchToSpy.firstCall.args[0] instanceof ToggleDeactivatedState).toBeTrue(); + }); + }); + }); +}); diff --git a/tests/components/ActionButton/ToggleActionButton.spec.ts b/tests/components/ActionButton/ToggleActionButton.spec.ts index 4c6ddef6..ad01b74b 100644 --- a/tests/components/ActionButton/ToggleActionButton.spec.ts +++ b/tests/components/ActionButton/ToggleActionButton.spec.ts @@ -1,13 +1,14 @@ import { SinonSandbox, createSandbox, SinonSpy } from 'sinon'; import { Mock } from 'coveo-search-ui-tests'; -import { IToggleActionButtonOptions, ToggleActionButton } from '../../../src/components/ActionButton/ToggleActionButton'; +import { ToggleActionButton } from '../../../src/components/ActionButton/ToggleActionButton'; import * as icons from '../../../src/utils/icons'; import { ActionButton } from '../../../src/components/ActionButton/ActionButton'; import { IComponentOptions } from 'coveo-search-ui'; +import { IToggleableButtonOptions } from '../../../src/components/ActionButton/ToggleableButton'; describe('ToggleActionButton', () => { let sandbox: SinonSandbox; - let options: IToggleActionButtonOptions; + let options: IToggleableButtonOptions; let testSubject: ToggleActionButton; let clickSpy: SinonSpy; @@ -44,7 +45,7 @@ describe('ToggleActionButton', () => { sandbox.reset(); }); - function createToggleButton(options: IToggleActionButtonOptions) { + function createToggleButton(options: IToggleableButtonOptions) { const element = document.createElement('button'); const componentSetup = Mock.advancedComponentSetup( ToggleActionButton, diff --git a/tests/components/ActionButton/ToggleableButton.spec.ts b/tests/components/ActionButton/ToggleableButton.spec.ts new file mode 100644 index 00000000..d1f8a5c8 --- /dev/null +++ b/tests/components/ActionButton/ToggleableButton.spec.ts @@ -0,0 +1,143 @@ +import { + ToggleDeactivatedState, + IToggleableButtonOptions, + ToggleActivatedState, + IToggleableButton, +} from '../../../src/components/ActionButton/ToggleableButton'; +import { createSandbox, SinonSandbox, SinonSpy } from 'sinon'; + +describe('ToggleStates', () => { + let sandbox: SinonSandbox; + + let testElement: HTMLElement; + let fakeStatefulActionButton: { element: HTMLElement }; + let onClickSpy: SinonSpy; + const toggleStateOptions: IToggleableButtonOptions = { + activateIcon: 'someActivateIcon', + activateTooltip: 'someActivateTooltip', + deactivateIcon: 'someDeactivateIcon', + deactivateTooltip: 'someDeactivatedTooltip', + }; + + beforeAll(() => { + sandbox = createSandbox(); + }); + + beforeEach(() => { + testElement = document.createElement('div'); + fakeStatefulActionButton = { element: testElement }; + onClickSpy = sandbox.spy(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('ToggleDeactivatedState', () => { + let testSubject: ToggleDeactivatedState; + beforeEach(() => { + testSubject = new ToggleDeactivatedState({ + options: toggleStateOptions, + onClick: onClickSpy, + }); + }); + + describe('constructor', () => { + it('should use the deactivateIcon, deactivateTooltip from the option of the ToggleableButton', () => { + expect(testSubject.icon).toBe('someActivateIcon'); + expect(testSubject.tooltip).toBe('someActivateTooltip'); + }); + + it('should use the onclick of the ToggleableButton', () => { + testSubject.click(); + expect(onClickSpy.calledOnce).toBeTrue(); + }); + }); + }); + + describe('ToggleActivatedState', () => { + let testSubject: ToggleActivatedState; + beforeEach(() => { + testSubject = new ToggleActivatedState({ + options: toggleStateOptions, + onClick: onClickSpy, + }); + }); + + describe('constructor', () => { + it('should use the deactivateIcon, deactivateTooltip from the option of the ToggleableButton', () => { + expect(testSubject.icon).toBe('someDeactivateIcon'); + expect(testSubject.tooltip).toBe('someDeactivatedTooltip'); + }); + + it('should use the onclick of the ToggleableButton', () => { + testSubject.click(); + expect(onClickSpy.calledOnce).toBeTrue(); + }); + }); + + describe('onStateEntry', () => { + beforeEach(() => { + testSubject.onStateEntry.apply(fakeStatefulActionButton); + }); + it('should add coveo-toggleactionbutton-activated to the classlist on this.element of the caller', () => { + expect(testElement.classList.value).toBe('coveo-toggleactionbutton-activated'); + }); + it('should set the attribute aria-pressed to true on this.element of the caller', () => { + expect(testElement.getAttribute('aria-pressed')).toBe('true'); + }); + describe('if the toggleableButton options include activate', () => { + let activateSpy: SinonSpy; + let toggleableButton: IToggleableButton; + + beforeEach(() => { + activateSpy = sandbox.spy(); + toggleableButton = { + options: { ...toggleStateOptions, activate: activateSpy }, + onClick: onClickSpy, + }; + testSubject = new ToggleActivatedState(toggleableButton); + testSubject.onStateEntry.apply(fakeStatefulActionButton); + }); + + it('should call call it with the toggleableButton for context', () => { + expect(activateSpy.calledOnce).toBeTrue(); + expect(activateSpy.firstCall.thisValue).toBe(toggleableButton); + }); + }); + }); + + describe('onStateExit', () => { + beforeEach(() => { + testElement.classList.value = 'coveo-toggleactionbutton-activated'; + testElement.setAttribute('aria-pressed', 'true'); + testSubject.onStateExit.apply(fakeStatefulActionButton); + }); + + it('should remove coveo-actionbutton-disabled to the classlist on this.element of the caller', () => { + expect(testElement.classList.value).toBe(''); + }); + it('should set the attribute aria-pressed to false on to this.element of the caller', () => { + expect(testElement.getAttribute('aria-pressed')).toBe('false'); + }); + describe('if the toggleableButton options include deactivate', () => { + let deactivateSpy: SinonSpy; + let toggleableButton: IToggleableButton; + beforeEach(() => { + deactivateSpy = sandbox.spy(); + toggleableButton = { + options: { ...toggleStateOptions, deactivate: deactivateSpy }, + onClick: onClickSpy, + }; + testSubject = new ToggleActivatedState(toggleableButton); + testSubject.onStateExit.apply(fakeStatefulActionButton); + }); + + it('should call call it with the toggleableButton for context', () => { + expect(deactivateSpy.calledOnce).toBeTrue(); + expect(deactivateSpy.firstCall.thisValue).toBe(toggleableButton); + }); + }); + }); + }); +});