Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(sfint-3272): Add ToggleActionButton component #76

Merged
merged 4 commits into from
Jun 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
lbergeron marked this conversation as resolved.
Show resolved Hide resolved
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;
lbergeron marked this conversation as resolved.
Show resolved Hide resolved
}

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');
lbergeron marked this conversation as resolved.
Show resolved Hide resolved

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