diff --git a/pages/toggle_action_button.html b/pages/toggle_action_button.html
new file mode 100644
index 00000000..3cc7ba42
--- /dev/null
+++ b/pages/toggle_action_button.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Development - Toggle Action Button
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Toggle Button
+
+
+
+
+
diff --git a/src/Index.ts b/src/Index.ts
index 8f0fd7af..1b6a0df0 100644
--- a/src/Index.ts
+++ b/src/Index.ts
@@ -1,6 +1,7 @@
// This entry point defines all the components that are included in the extensions.
export { ActionButton } from './components/ActionButton/ActionButton';
+export { ToggleActionButton } from './components/ActionButton/ToggleActionButton';
export { AttachResult } from './components/AttachResult/AttachResult';
export { UserActivity } from './components/UserActions/UserActivity';
export { UserActions } from './components/UserActions/UserActions';
diff --git a/src/components/ActionButton/ActionButton.scss b/src/components/ActionButton/ActionButton.scss
index 8ab42867..43c49a69 100644
--- a/src/components/ActionButton/ActionButton.scss
+++ b/src/components/ActionButton/ActionButton.scss
@@ -1,10 +1,7 @@
-@import '../../sass/Variables.scss';
-
$primary-color-lightest: #ffffff;
$primary-color-lightest-hover: whitesmoke;
$primary-color-light: #e5e5e5;
$primary-color-dark: #4a4a4a;
-$accent-color: $calypso;
$button-size: 36px;
@@ -52,11 +49,11 @@ button.CoveoActionButton.coveo-actionbutton {
.CoveoActionButton.coveo-actionbutton:hover,
.CoveoActionButton.coveo-actionbutton:active {
& {
- color: $accent-color;
+ color: $primary-color-dark;
background-color: $primary-color-lightest-hover;
}
.coveo-actionbutton_icon svg {
- fill: $accent-color;
+ fill: $primary-color-dark;
}
}
diff --git a/src/components/ActionButton/ActionButton.ts b/src/components/ActionButton/ActionButton.ts
index 944940e8..e0945f17 100644
--- a/src/components/ActionButton/ActionButton.ts
+++ b/src/components/ActionButton/ActionButton.ts
@@ -1,9 +1,9 @@
import { Component, ComponentOptions, IResultsComponentBindings, Initialization } from 'coveo-search-ui';
export interface IActionButtonOptions {
+ icon?: string;
title?: string;
tooltip?: string;
- icon?: string;
click?: () => void;
}
@@ -91,6 +91,27 @@ export class ActionButton extends Component {
}
}
+ /**
+ * Updates the button icon.
+ * @param icon Markup of the SVG icon to set.
+ */
+ public updateIcon(icon: string): void {
+ const iconElement = this.element.querySelector('.coveo-actionbutton_icon');
+ if (iconElement && icon && icon != iconElement.innerHTML) {
+ iconElement.innerHTML = icon;
+ }
+ }
+
+ /**
+ * Updates the button tooltip.
+ * @param tooltip The tooltip to set.
+ */
+ public updateTooltip(tooltip: string): void {
+ if (tooltip && tooltip != this.element.title) {
+ this.element.title = tooltip;
+ }
+ }
+
protected render(): void {
this.applyButtonStyles();
diff --git a/src/components/ActionButton/ToggleActionButton.scss b/src/components/ActionButton/ToggleActionButton.scss
new file mode 100644
index 00000000..a01c2435
--- /dev/null
+++ b/src/components/ActionButton/ToggleActionButton.scss
@@ -0,0 +1,15 @@
+@import './ActionButton.scss';
+@import '../../sass/Variables.scss';
+
+$activated-color: $calypso;
+
+button.CoveoActionButton.coveo-actionbutton.coveo-toggleactionbutton-activated {
+ background-color: $activated-color;
+ border-color: $activated-color;
+
+ .coveo-actionbutton_icon {
+ svg {
+ fill: $primary-color-lightest;
+ }
+ }
+}
diff --git a/src/components/ActionButton/ToggleActionButton.ts b/src/components/ActionButton/ToggleActionButton.ts
new file mode 100644
index 00000000..331e99a2
--- /dev/null
+++ b/src/components/ActionButton/ToggleActionButton.ts
@@ -0,0 +1,182 @@
+import { ComponentOptions, IResultsComponentBindings, Component, Initialization } from 'coveo-search-ui';
+import { ActionButton } from './ActionButton';
+
+export interface IToggleActionButtonOptions {
+ activatedIcon: string;
+ activatedTooltip: string;
+ deactivatedIcon: string;
+ deactivatedTooltip: string;
+ click?: () => void;
+ activate?: () => void;
+ deactivate?: () => void;
+}
+
+export class ToggleActionButton extends Component {
+ static ID = 'ToggleActionButton';
+ static ACTIVATED_CLASS_NAME = 'coveo-toggleactionbutton-activated';
+
+ static options: IToggleActionButtonOptions = {
+ /**
+ * Specifies the button icon when the button is activated.
+ *
+ * Default is the empty string.
+ *
+ * For example, with this SVG markup:
+ *
+ * ```xml
+ *
+ * ```
+ *
+ * The attribute would be set like this:
+ *
+ * ```html
+ *
+ * ```
+ */
+ activatedIcon: ComponentOptions.buildStringOption(),
+
+ /**
+ * Specifies the button tooltip when the button is activated.
+ *
+ * Default is the empty string.
+ *
+ * ```html
+ *
+ * ```
+ */
+ activatedTooltip: ComponentOptions.buildStringOption(),
+
+ /**
+ * Specifies the button SVG icon when the button is deactivated.
+ * Note: The SVG markup has to be HTML encoded when set using the HTML attributes.
+ *
+ * Default is the empty string.
+ *
+ * For example, with this SVG markup:
+ *
+ * ```xml
+ *
+ * ```
+ *
+ * The attribute would be set like this:
+ *
+ * ```html
+ *
+ * ```
+ */
+ deactivatedIcon: ComponentOptions.buildStringOption(),
+
+ /**
+ * Specifies the button tooltip text when the button is deactivated.
+ *
+ * Default is the empty string.
+ *
+ * ```html
+ *
+ * ```
+ */
+ deactivatedTooltip: ComponentOptions.buildStringOption(),
+
+ /**
+ * Specifies the handler called when the button is clicked.
+ *
+ * Default is `null`.
+ *
+ * This option is set in JavaScript when initializing the component.
+ */
+ click: ComponentOptions.buildCustomOption(s => null),
+
+ /**
+ * Specifies the handler called when the button is activated.
+ *
+ * Default is `null`.
+ *
+ * This option is set in JavaScript when initializing the component.
+ */
+ activate: ComponentOptions.buildCustomOption(s => null),
+
+ /**
+ * Specifies the handler called when the button is deactivated.
+ *
+ * Default is `null`.
+ *
+ * This option is set in JavaScript when initializing the component.
+ */
+ deactivate: ComponentOptions.buildCustomOption(s => null)
+ };
+
+ private _isActivated: boolean = false;
+ private innerActionButton: ActionButton;
+
+ constructor(public element: HTMLElement, public options: IToggleActionButtonOptions, public bindings?: IResultsComponentBindings) {
+ super(element, ToggleActionButton.ID, bindings);
+ this.options = ComponentOptions.initComponentOptions(element, ToggleActionButton, options);
+
+ this.createInnerButton(bindings);
+ }
+
+ /**
+ * Indicates whether the toggle button is in the activated state.
+ */
+ public isActivated(): boolean {
+ return this._isActivated;
+ }
+
+ /**
+ * Sets the toggle button to the specified state.
+ * @param activated Whether the button is activated.
+ */
+ public setActivated(activated: boolean): void {
+ if (activated !== this.isActivated()) {
+ this._isActivated = activated;
+ this.updateButton();
+
+ if (this._isActivated && this.options.activate) {
+ this.options.activate();
+ }
+ if (!this._isActivated && this.options.deactivate) {
+ this.options.deactivate();
+ }
+ }
+ }
+
+ protected onClick(): void {
+ this.setActivated(!this.isActivated());
+
+ if (this.options.click) {
+ this.options.click();
+ }
+ }
+
+ private createInnerButton(bindings?: IResultsComponentBindings): void {
+ this.innerActionButton = new ActionButton(
+ this.element,
+ {
+ icon: this.options.deactivatedIcon,
+ tooltip: this.options.deactivatedTooltip,
+ click: () => this.onClick()
+ },
+ bindings
+ );
+
+ this.updateButton();
+ }
+
+ private updateButton() {
+ if (this._isActivated) {
+ this.element.classList.add(ToggleActionButton.ACTIVATED_CLASS_NAME);
+ this.element.setAttribute('aria-pressed', 'true');
+
+ this.innerActionButton.updateIcon(this.options.activatedIcon);
+ this.innerActionButton.updateTooltip(this.options.activatedTooltip);
+ } else {
+ this.element.classList.remove(ToggleActionButton.ACTIVATED_CLASS_NAME);
+ this.element.setAttribute('aria-pressed', 'false');
+
+ this.innerActionButton.updateIcon(this.options.deactivatedIcon);
+ this.innerActionButton.updateTooltip(this.options.deactivatedTooltip);
+ }
+ }
+}
+
+Initialization.registerAutoCreateComponent(ToggleActionButton);
diff --git a/src/sass/Index.scss b/src/sass/Index.scss
index 3fb1269d..999bc5d5 100644
--- a/src/sass/Index.scss
+++ b/src/sass/Index.scss
@@ -1,4 +1,5 @@
@import '../components/ActionButton/ActionButton.scss';
+@import '../components/ActionButton/ToggleActionButton.scss';
@import '../components/AttachResult/AttachResult.scss';
@import '../components/UserActions/UserActions.scss';
@import '../components/ViewedByCustomer/ViewedByCustomer.scss';
diff --git a/tests/components/ActionButton/ActionButton.spec.ts b/tests/components/ActionButton/ActionButton.spec.ts
index 541868bb..41af615e 100644
--- a/tests/components/ActionButton/ActionButton.spec.ts
+++ b/tests/components/ActionButton/ActionButton.spec.ts
@@ -30,6 +30,27 @@ describe('ActionButton', () => {
sandbox.restore();
});
+ function createActionButton(options: IActionButtonOptions) {
+ const element = document.createElement('button');
+ const componentSetup = Mock.advancedComponentSetup(ActionButton, new Mock.AdvancedComponentSetupOptions(element, options));
+ return componentSetup.cmp;
+ }
+
+ function setOption(optionName: string, optionValue: any) {
+ const dictOptions = options as { [key: string]: any };
+ dictOptions[optionName] = optionValue;
+ }
+
+ function assertIconsAreEqual(actualIcon: string, expectedIcon: string) {
+ const actualElement = document.createElement('span');
+ actualElement.innerHTML = actualIcon;
+
+ const expectedElement = document.createElement('span');
+ expectedElement.innerHTML = expectedIcon;
+
+ expect(actualElement.innerHTML).toEqual(expectedElement.innerHTML);
+ }
+
it('should not log warnings in the console', () => {
expect(consoleWarnSpy.called).toBeFalse();
});
@@ -93,6 +114,24 @@ describe('ActionButton', () => {
});
});
+ describe('updateIcon', () => {
+ it('should update the button icon', () => {
+ testSubject.updateIcon(icons.duplicate);
+
+ const iconChild = testSubject.element.querySelector('.coveo-actionbutton_icon');
+ assertIconsAreEqual(iconChild.innerHTML, icons.duplicate);
+ });
+ });
+
+ describe('updateTooltip', () => {
+ it('should update the button tooltip', () => {
+ const newTooltip = 'some new tooltip';
+ testSubject.updateTooltip(newTooltip);
+
+ expect(testSubject.element.title).toEqual(newTooltip);
+ });
+ });
+
[
{
optionName: 'title',
@@ -134,15 +173,4 @@ describe('ActionButton', () => {
});
});
});
-
- const createActionButton = (options: IActionButtonOptions) => {
- const element = document.createElement('button');
- const componentSetup = Mock.advancedComponentSetup(ActionButton, new Mock.AdvancedComponentSetupOptions(element, options));
- return componentSetup.cmp;
- };
-
- const setOption = (optionName: string, optionValue: any) => {
- const dictOptions = options as { [key: string]: any };
- dictOptions[optionName] = optionValue;
- };
});
diff --git a/tests/components/ActionButton/ToggleActionButton.spec.ts b/tests/components/ActionButton/ToggleActionButton.spec.ts
new file mode 100644
index 00000000..f89e09ba
--- /dev/null
+++ b/tests/components/ActionButton/ToggleActionButton.spec.ts
@@ -0,0 +1,132 @@
+import { SinonSandbox, createSandbox, SinonSpy } from 'sinon';
+import { Mock } from 'coveo-search-ui-tests';
+import { IToggleActionButtonOptions, ToggleActionButton } from '../../../src/components/ActionButton/ToggleActionButton';
+import * as icons from '../../../src/utils/icons';
+import { ActionButton } from '../../../src/components/ActionButton/ActionButton';
+
+describe('ToggleActionButton', () => {
+ let sandbox: SinonSandbox;
+ let options: IToggleActionButtonOptions;
+ let testSubject: ToggleActionButton;
+
+ let clickSpy: SinonSpy;
+ let activateSpy: SinonSpy;
+ let deactivateSpy: SinonSpy;
+ let updateIconSpy: SinonSpy;
+ let updateTooltipSpy: SinonSpy;
+
+ beforeAll(() => {
+ sandbox = createSandbox();
+
+ clickSpy = sandbox.spy();
+ activateSpy = sandbox.spy();
+ deactivateSpy = sandbox.spy();
+ updateIconSpy = sandbox.spy(ActionButton.prototype, 'updateIcon');
+ updateTooltipSpy = sandbox.spy(ActionButton.prototype, 'updateTooltip');
+ });
+
+ beforeEach(() => {
+ options = {
+ activatedIcon: icons.duplicate,
+ activatedTooltip: 'activated tooltip',
+ deactivatedIcon: icons.copy,
+ deactivatedTooltip: 'tooltip',
+ click: clickSpy,
+ activate: activateSpy,
+ deactivate: deactivateSpy
+ };
+
+ testSubject = createToggleButton(options);
+ });
+
+ afterEach(() => {
+ sandbox.reset();
+ });
+
+ function createToggleButton(options: IToggleActionButtonOptions) {
+ const element = document.createElement('button');
+ const componentSetup = Mock.advancedComponentSetup(
+ ToggleActionButton,
+ new Mock.AdvancedComponentSetupOptions(element, options)
+ );
+ return componentSetup.cmp;
+ }
+
+ describe('clicking the button', () => {
+ beforeEach(() => {
+ Coveo.$$(testSubject.element).trigger('click');
+ });
+
+ it('should call the click handler', () => {
+ expect(clickSpy.called).toBeTrue();
+ });
+ });
+
+ describe('activating the button', () => {
+ beforeEach(() => {
+ testSubject.setActivated(true);
+ });
+
+ it('should have the isActivated method return true', () => {
+ expect(testSubject.isActivated()).toBeTrue();
+ });
+
+ it('should call the activate handler', () => {
+ expect(activateSpy.called).toBeTrue();
+ });
+
+ it('should add the activated marker CSS class', () => {
+ const hasMarkerClass = testSubject.element.classList.contains(ToggleActionButton.ACTIVATED_CLASS_NAME);
+ expect(hasMarkerClass).toBeTrue();
+ });
+
+ it('should set aria-pressed attribute to true', () => {
+ const attributeValue = testSubject.element.getAttribute('aria-pressed');
+
+ expect(attributeValue).toEqual('true');
+ });
+
+ it('should update button with activated icon', () => {
+ expect(updateIconSpy.calledWith(options.activatedIcon)).toBeTrue();
+ });
+
+ it('should update button with activated tooltip', () => {
+ expect(updateTooltipSpy.calledWith(options.activatedTooltip)).toBeTrue();
+ });
+ });
+
+ describe('deactivating the button', () => {
+ beforeEach(() => {
+ testSubject.setActivated(true);
+ sandbox.reset();
+
+ testSubject.setActivated(false);
+ });
+
+ it('should have the isActivated method return false', () => {
+ expect(testSubject.isActivated()).toBeFalse();
+ });
+
+ it('should call the deactivate handler', () => {
+ expect(deactivateSpy.called).toBeTrue();
+ });
+
+ it('should remove the activated marker CSS class', () => {
+ const hasMarkerClass = testSubject.element.classList.contains(ToggleActionButton.ACTIVATED_CLASS_NAME);
+ expect(hasMarkerClass).toBeFalse();
+ });
+
+ it('should set aria-pressed attribute to false', () => {
+ const attributeValue = testSubject.element.getAttribute('aria-pressed');
+ expect(attributeValue).toEqual('false');
+ });
+
+ it('should update button with deactivated icon', () => {
+ expect(updateIconSpy.calledWith(options.deactivatedIcon)).toBeTrue();
+ });
+
+ it('should update button with deactivated tooltip', () => {
+ expect(updateTooltipSpy.calledWith(options.deactivatedTooltip)).toBeTrue();
+ });
+ });
+});