From 931fbe0a4aae4922fe895199d5df949c32a14da9 Mon Sep 17 00:00:00 2001 From: Ivaylo Plashkov Date: Mon, 10 Feb 2020 14:53:52 +0200 Subject: [PATCH] feat(ui5-segmentedbutton): initial implementation (#1164) --- packages/main/bundle.esm.js | 1 + packages/main/src/Button.js | 18 +- packages/main/src/SegmentedButton.hbs | 6 + packages/main/src/SegmentedButton.js | 185 ++++++++++++++++++ packages/main/src/Select.js | 4 +- packages/main/src/themes/Button.css | 13 +- packages/main/src/themes/SegmentedButton.css | 34 ++++ packages/main/test/pages/SegmentedButton.html | 92 +++++++++ .../test/samples/SegmentedButton.sample.html | 70 +++++++ .../main/test/specs/SegmentedButton.spec.js | 35 ++++ .../build-scripts/samples-prepare.js | 1 + 11 files changed, 453 insertions(+), 6 deletions(-) create mode 100644 packages/main/src/SegmentedButton.hbs create mode 100644 packages/main/src/SegmentedButton.js create mode 100644 packages/main/src/themes/SegmentedButton.css create mode 100644 packages/main/test/pages/SegmentedButton.html create mode 100644 packages/main/test/samples/SegmentedButton.sample.html create mode 100644 packages/main/test/specs/SegmentedButton.spec.js diff --git a/packages/main/bundle.esm.js b/packages/main/bundle.esm.js index 876f8b7e49ba..9816f5b803dd 100644 --- a/packages/main/bundle.esm.js +++ b/packages/main/bundle.esm.js @@ -31,6 +31,7 @@ import Popover from "./dist/Popover.js"; import Panel from "./dist/Panel.js"; import RadioButton from "./dist/RadioButton.js"; import ResponsivePopover from "./dist/ResponsivePopover.js"; +import SegmentedButton from "./dist/SegmentedButton.js"; import Select from "./dist/Select.js"; import Option from "./dist/Option.js"; import Switch from "./dist/Switch.js"; diff --git a/packages/main/src/Button.js b/packages/main/src/Button.js index daa9b41f84bd..1ea5d88dfbbc 100644 --- a/packages/main/src/Button.js +++ b/packages/main/src/Button.js @@ -136,6 +136,16 @@ const metadata = { _iconSettings: { type: Object, }, + + /** + * Defines the tabIndex of the component. + * @private + */ + _tabIndex: { + type: String, + defaultValue: "0", + noAttribute: true, + }, }, slots: /** @lends sap.ui.webcomponents.main.Button.prototype */ { /** @@ -314,7 +324,13 @@ class Button extends UI5Element { } get tabIndexValue() { - return this.nonFocusable ? "-1" : "0"; + const tabindex = this.getAttribute("tabindex"); + + if (tabindex) { + return tabindex; + } + + return this.nonFocusable ? "-1" : this._tabIndex; } static async define(...params) { diff --git a/packages/main/src/SegmentedButton.hbs b/packages/main/src/SegmentedButton.hbs new file mode 100644 index 000000000000..211dbd9fb890 --- /dev/null +++ b/packages/main/src/SegmentedButton.hbs @@ -0,0 +1,6 @@ +
+ +
diff --git a/packages/main/src/SegmentedButton.js b/packages/main/src/SegmentedButton.js new file mode 100644 index 000000000000..171d4e60d1af --- /dev/null +++ b/packages/main/src/SegmentedButton.js @@ -0,0 +1,185 @@ +import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js"; +import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js"; +import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js"; +import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js"; + +// Template +import SegmentedButtonTemplate from "./generated/templates/SegmentedButtonTemplate.lit.js"; + +// Styles +import SegmentedButtonCss from "./generated/themes/SegmentedButton.css.js"; + +/** + * @public + */ +const metadata = { + tag: "ui5-segmentedbutton", + properties: /** @lends sap.ui.webcomponents.main.SegmentedButton.prototype */ {}, + slots: /** @lends sap.ui.webcomponents.main.SegmentedButton.prototype */ { + + /** + * Defines the buttons of ui5-segmentedbutton. + *

+ * Note: Multiple buttons are allowed. + *

+ * Note: Use the ui5-togglebutton for the intended design. + * @type {HTMLElement[]} + * @slot + * @public + */ + "default": { + propertyName: "buttons", + type: HTMLElement, + }, + }, + events: /** @lends sap.ui.webcomponents.main.SegmentedButton.prototype */ { + + /** + * Fired when the selected button changes. + * + * @event + * @param {HTMLElement} selectedButton the pressed button. + * @public + */ + selectionChange: { + detail: { + selectedButton: { type: HTMLElement }, + }, + }, + }, +}; + +/** + * @class + * + *

Overview

+ * + * The SegmentedButton shows a group of buttons. When the user clicks or taps + * one of the buttons, it stays in a pressed state. It automatically resizes the buttons + * to fit proportionally within the component. When no width is set, the component uses the available width. + *

+ * Note: There can be just one selected button at a time. + * + *

ES6 Module Import

+ * + * import "@ui5/webcomponents/dist/SegmentedButton"; + * + * @constructor + * @author SAP SE + * @alias sap.ui.webcomponents.main.SegmentedButton + * @extends sap.ui.webcomponents.base.UI5Element + * @tagname ui5-segmentedbutton + * @since 1.0.0-rc.6 + * @public + */ +class SegmentedButton extends UI5Element { + static get metadata() { + return metadata; + } + + static get render() { + return litRender; + } + + static get template() { + return SegmentedButtonTemplate; + } + + static get styles() { + return SegmentedButtonCss; + } + + constructor() { + super(); + this.initItemNavigation(); + + this.absoluteWidthSet = false; // set to true whenever we set absolute width to the component + this.percentageWidthSet = false; // set to true whenever we set 100% width to the component + } + + onEnterDOM() { + this._handleResizeBound = this._handleResize.bind(this); + + ResizeHandler.register(document.body, this._handleResizeBound); + } + + onExitDOM() { + ResizeHandler.deregister(document.body, this._handleResizeBound); + } + + onBeforeRendering() { + this.normalizeSelection(); + } + + async onAfterRendering() { + await Promise.all(this.buttons.map(button => button._waitForDomRef)); + this.widths = this.buttons.map(button => button.offsetWidth); + } + + initItemNavigation() { + this._itemNavigation = new ItemNavigation(this); + + this._itemNavigation.getItemsCallback = () => this.getSlottedNodes("buttons"); + } + + normalizeSelection() { + this._selectedButton = this.buttons.filter(button => button.pressed).pop(); + + if (this._selectedButton) { + this.buttons.forEach(button => { + button.pressed = false; + }); + this._selectedButton.pressed = true; + } + } + + _onclick(event) { + if (event.target !== this._selectedButton) { + if (this._selectedButton) { + this._selectedButton.pressed = false; + } + this._selectedButton = event.target; + this.fireEvent("selectionChange", { + selectedButton: this._selectedButton, + }); + } + this._selectedButton.pressed = true; + + this._itemNavigation.update(this._selectedButton); + + return this; + } + + _handleResize() { + const parentWidth = this.parentNode.offsetWidth; + + if (!this.style.width || this.percentageWidthSet) { + this.style.width = `${Math.max(...this.widths) * this.buttons.length}px`; + this.absoluteWidthSet = true; + } + + this.buttons.forEach(button => { + button.style.width = "100%"; + }); + + if (parentWidth <= this.offsetWidth && this.absoluteWidthSet) { + this.style.width = "100%"; + this.percentageWidthSet = true; + } + } + + /** + * Currently selected button. + * + * @readonly + * @type { ui5-togglebutton } + * @public + */ + get selectedButton() { + return this._selectedButton; + } +} + +SegmentedButton.define(); + +export default SegmentedButton; diff --git a/packages/main/src/Select.js b/packages/main/src/Select.js index 6afa6fb444ab..b79dfc67d98c 100644 --- a/packages/main/src/Select.js +++ b/packages/main/src/Select.js @@ -127,10 +127,10 @@ const metadata = { }, events: /** @lends sap.ui.webcomponents.main.Select.prototype */ { /** - * Fired when the selected item changes. + * Fired when the selected option changes. * * @event - * @param {HTMLElement} item the selected item. + * @param {HTMLElement} selectedOption the selected option. * @public */ change: { diff --git a/packages/main/src/themes/Button.css b/packages/main/src/themes/Button.css index b0992e3a6adf..e4e6ebd5985a 100644 --- a/packages/main/src/themes/Button.css +++ b/packages/main/src/themes/Button.css @@ -50,6 +50,9 @@ color: inherit; text-shadow: inherit; font: inherit; + white-space: inherit; + overflow: inherit; + text-overflow: inherit; } :host(:not([active]):hover) { @@ -83,6 +86,9 @@ .ui5-button-text { outline: none; position: relative; + white-space: inherit; + overflow: inherit; + text-overflow: inherit; } :host([has-icon]) .ui5-button-text { @@ -104,9 +110,10 @@ } bdi { - display: flex; - justify-content: flex-start; - align-items: center; + display: block; + white-space: inherit; + overflow: inherit; + text-overflow: inherit; } :host([active]:not([disabled])) { diff --git a/packages/main/src/themes/SegmentedButton.css b/packages/main/src/themes/SegmentedButton.css new file mode 100644 index 000000000000..2b58bf3d117a --- /dev/null +++ b/packages/main/src/themes/SegmentedButton.css @@ -0,0 +1,34 @@ +:host(:not([hidden])) { + display: inline-block; +} + +.ui5-segmentedbutton-root { + display: flex; +} + +::slotted(ui5-togglebutton) { + border-radius: 0; + height: 2.75rem; + min-width: 2.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +::slotted(ui5-togglebutton:nth-child(odd)) { + border: 1px solid var(--sapButton_Selected_BorderColor); + border-right: 0; + border-left: 0; +} + +::slotted(ui5-togglebutton:last-child) { + border-top-right-radius: 0.375rem; + border-bottom-right-radius: 0.375rem; + border-right: 1px solid var(--sapButton_Selected_BorderColor); +} + +::slotted(ui5-togglebutton:first-child) { + border-top-left-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; + border-left: 1px solid var(--sapButton_Selected_BorderColor); +} diff --git a/packages/main/test/pages/SegmentedButton.html b/packages/main/test/pages/SegmentedButton.html new file mode 100644 index 000000000000..9a40bd8a9706 --- /dev/null +++ b/packages/main/test/pages/SegmentedButton.html @@ -0,0 +1,92 @@ + + + + + + + + ui5-segmentedbutton + + + + + + + + + + +
+

ui5-segmentedbutton

+
+ +
+
+

Segmentedbutton example

+
+
+ + + ToggleButton + Button + +
+ +
+

Example with 4 buttons

+ + + Button + Button + Click + Pressed ToggleButton + +
+ +
+

Example with 5 buttons

+ + + Word + Pressed ToggleButton With Bigger Text + Button + Pressed ToggleButton + A + +
+ +
+

Example with Icons and custom width

+ + + + + + +
+ +
+

SegmentedButton with 100% width

+ + + Pressed ToggleButton + ToggleButton + ToggleButton + +
+ +
+

SegmentedButton wrapped in a container with set width

+
+ + + + + +
+
+
+
+
+ + diff --git a/packages/main/test/samples/SegmentedButton.sample.html b/packages/main/test/samples/SegmentedButton.sample.html new file mode 100644 index 000000000000..ac0b0c012318 --- /dev/null +++ b/packages/main/test/samples/SegmentedButton.sample.html @@ -0,0 +1,70 @@ +
+

SegmentedButton

+
+ +
+
+ +
@ui5/webcomponents
+ +
<ui5-segmentedbutton>
+ +
+

Basic SegmentedButton

+
+ + Map + Satellite + Terrain + +
+

+<ui5-segmentedbutton>
+    <ui5-togglebutton>Map</ui5-togglebutton>
+    <ui5-togglebutton pressed>Satellite</ui5-togglebutton>
+    <ui5-togglebutton>Terrain</ui5-togglebutton>
+</ui5-segmentedbutton>
+	
+
+ +
+

SegmentedButton with Icons

+
+ + + + + +
+

+<ui5-segmentedbutton>
+    <ui5-togglebutton icon="employee" pressed></ui5-togglebutton>
+    <ui5-togglebutton icon="menu"></ui5-togglebutton>
+    <ui5-togglebutton icon="factory"></ui5-togglebutton>
+</ui5-segmentedbutton>
+	
+
+ +
+

SegmentedButton with 5 ToggleButtons

+
+ + Button + Pressed ToggleButton With Bigger Text + Button + Pressed ToggleButton + Press me + +
+

+<ui5-segmentedbutton>
+    <ui5-togglebutton>Button</ui5-togglebutton>
+    <ui5-togglebutton pressed>Pressed ToggleButton With Bigger Text</ui5-togglebutton>
+    <ui5-togglebutton>Button</ui5-togglebutton>
+    <ui5-togglebutton>ToggleButton</ui5-togglebutton>
+    <ui5-togglebutton>Press me</ui5-togglebutton>
+</ui5-segmentedbutton>
+	
+
+ + diff --git a/packages/main/test/specs/SegmentedButton.spec.js b/packages/main/test/specs/SegmentedButton.spec.js new file mode 100644 index 000000000000..385f5c992f94 --- /dev/null +++ b/packages/main/test/specs/SegmentedButton.spec.js @@ -0,0 +1,35 @@ +const assert = require("chai").assert; + +describe("SegmentedButton general interaction", () => { + browser.url("http://localhost:8080/test-resources/pages/SegmentedButton.html"); + + it("tests if pressed attribute is applied", () => { + const toggleButton = browser.$("#segButton1 > ui5-togglebutton:first-child"); + + assert.ok(toggleButton.getProperty("pressed"), "ToggleButton has property pressed"); + }); + + it("tests if pressed attribute is switched to the newly pressed button", () => { + const firstToggleButton = browser.$("#segButton1 > ui5-togglebutton:first-child"); + const lastToggleButton = browser.$("#segButton1 > ui5-togglebutton:last-child"); + + lastToggleButton.click(); + + assert.ok(lastToggleButton.getProperty("pressed"), "Last ToggleButton has property pressed"); + assert.ok(!firstToggleButton.getProperty("pressed"), "First ToggleButton should not be pressed anymore"); + }); + + it("tests if pressed attribute is applied only to last child when all buttons are pressed", () => { + const toggleButton1 = browser.$("#segButton2 > ui5-togglebutton:first-child"); + const toggleButton2 = browser.$("#segButton2 > ui5-togglebutton:nth-child(2)"); + const toggleButton3 = browser.$("#segButton2 > ui5-togglebutton:nth-child(3)"); + const toggleButton4 = browser.$("#segButton2 > ui5-togglebutton:last-child"); + + // only last button should be pressed + assert.ok(!toggleButton1.getProperty("pressed"), "ToggleButton should not be pressed"); + assert.ok(!toggleButton2.getProperty("pressed"), "ToggleButton should not be pressed"); + assert.ok(!toggleButton3.getProperty("pressed"), "ToggleButton should not be pressed"); + assert.ok(toggleButton4.getProperty("pressed"), "ToggleButton has property pressed"); + + }); +}); diff --git a/packages/playground/build-scripts/samples-prepare.js b/packages/playground/build-scripts/samples-prepare.js index 2988a0e39ef9..e51095b13d2a 100644 --- a/packages/playground/build-scripts/samples-prepare.js +++ b/packages/playground/build-scripts/samples-prepare.js @@ -13,6 +13,7 @@ const components = []; // Add new components here const newComponents = [ "ComboBox", + "SegmentedButton", ]; packages.forEach(package => {