Skip to content

Commit

Permalink
feat(sfint-3272): Add ToggleActionButton component (#76)
Browse files Browse the repository at this point in the history
* feat(SFINT-3272): Add ToggleActionButton component.

* feat(SFINT-3272): Add ToggleActionButton tests

* feat(SFINT-3272): Apply review comments

* feat(SFINT-3272): Improve option names
  • Loading branch information
lbergeron committed Jun 19, 2020
1 parent 03a1a47 commit 09fce17
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 17 deletions.
41 changes: 41 additions & 0 deletions pages/toggle_action_button.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, height=device-height" />
<title>Development - Toggle Action Button</title>
<link rel="stylesheet" href="../css/CoveoFullSearch.css" />
<link rel="stylesheet" href="../css/CoveoJsSearchExtensions.css" />
<script src="../js/CoveoJsSearch.Lazy.js"></script>
<script src="../commonjs/CoveoJsSearchExtensions.js"></script>
<script>
let attachedIds = [];
document.addEventListener('DOMContentLoaded', function() {
Coveo.SearchEndpoint.configureSampleEndpointV2();
Coveo.init(document.body, {});
});
</script>
</head>

<body id="search" class="CoveoSearchInterface" data-enable-history="false" style="padding: 1em;">
<span class="CoveoAnalytics"></span>
<div class="coveo-tab-section">
<a class="CoveoTab" data-id="All" data-caption="All Content"></a>
</div>
<div class="coveo-search-section">
<div class="CoveoSearchbox" data-enable-omnibox="true"></div>
</div>

<div>
<h2>Toggle Button</h2>
<br />
<button
class="CoveoToggleActionButton"
data-deactivated-icon='&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 78.997 78.997" height="298.571" width="298.571"&gt;&lt;circle r="39.499" cy="120.385" cx="84.1" transform="translate(-44.601 -80.887)"/&gt;&lt;/svg&gt;'
data-deactivated-tooltip="Normal tooltip"
data-activated-tooltip="Activated tooltip"
data-activated-icon='&lt;svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 78.997 78.997" height="298.571" width="298.571"&gt;&lt;path d="M39.434 0A39.499 39.499 0 000 39.498a39.499 39.499 0 0039.498 39.5 39.499 39.499 0 0039.5-39.5A39.499 39.499 0 0039.497 0a39.499 39.499 0 00-.064 0zm.59 7.273a31.948 31.948 0 0131.948 31.949A31.948 31.948 0 0140.023 71.17 31.948 31.948 0 018.075 39.222 31.948 31.948 0 0140.023 7.273z"/&gt;&lt;/svg&gt;'
></button>
</div>
</body>
</html>
1 change: 1 addition & 0 deletions src/Index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
7 changes: 2 additions & 5 deletions src/components/ActionButton/ActionButton.scss
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
}
}
23 changes: 22 additions & 1 deletion src/components/ActionButton/ActionButton.ts
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down Expand Up @@ -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();

Expand Down
15 changes: 15 additions & 0 deletions src/components/ActionButton/ToggleActionButton.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
182 changes: 182 additions & 0 deletions src/components/ActionButton/ToggleActionButton.ts
Original file line number Diff line number Diff line change
@@ -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
* <svg width="1em" height="1em">...</svg>
* ```
*
* The attribute would be set like this:
*
* ```html
* <button class='CoveoToggleActionButton' data-activated-icon='&lt;svg width=&quot;1em&quot; height=&quot;1em&quot;&gt;...&lt;/svg&gt;'></button>
* ```
*/
activatedIcon: ComponentOptions.buildStringOption(),

/**
* Specifies the button tooltip when the button is activated.
*
* Default is the empty string.
*
* ```html
* <button class='CoveoToggleActionButton' data-activated-tooltip='My activated button tooltip'></button>
* ```
*/
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
* <svg width="1em" height="1em">...</svg>
* ```
*
* The attribute would be set like this:
*
* ```html
* <button class='CoveoToggleActionButton' data-deactivated-icon='&lt;svg width=&quot;1em&quot; height=&quot;1em&quot;&gt;...&lt;/svg&gt;'></button>
* ```
*/
deactivatedIcon: ComponentOptions.buildStringOption(),

/**
* Specifies the button tooltip text when the button is deactivated.
*
* Default is the empty string.
*
* ```html
* <button class='CoveoToggleActionButton' data-deactivated-tooltip='My button tooltip'></button>
* ```
*/
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);
1 change: 1 addition & 0 deletions src/sass/Index.scss
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
50 changes: 39 additions & 11 deletions tests/components/ActionButton/ActionButton.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ describe('ActionButton', () => {
sandbox.restore();
});

function createActionButton(options: IActionButtonOptions) {
const element = document.createElement('button');
const componentSetup = Mock.advancedComponentSetup<ActionButton>(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();
});
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -134,15 +173,4 @@ describe('ActionButton', () => {
});
});
});

const createActionButton = (options: IActionButtonOptions) => {
const element = document.createElement('button');
const componentSetup = Mock.advancedComponentSetup<ActionButton>(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;
};
});
Loading

0 comments on commit 09fce17

Please sign in to comment.