Skip to content

Commit 9bd9ce4

Browse files
authored
feat(ui5-segmented-button-item): introduce new component to serve as child of SegmentedButton (#3258)
Introduce new component SegmentedButtonItem (ui5-segmentedbutton-item), meant to be used within the SegmentedButton (ui5-segmented-button) as a child, in order to implement a11y compliant DOM structure and attributes. The SegmentedButtonItem replaces the ToggleButton, previously used in the SegmentedButton. Fixes: #3191 Closes: #3191 BREAKING_CHANGE: `selectedButton` event detail of "selection-change" has been renamed to `selectedItem` BREAKING_CHANGE: SegmentedButton no longer accepts ToggleButton, you have to use the newly created component, called SegmentedButtonItem as follows: <ui5-segmentedbutton> <ui5-segmentedbutton-item pressed>One</ui5-segmentedbutton-item> <ui5-segmentedbutton-item>Two</ui5-segmentedbutton-item> <ui5-segmentedbutton-item>Three</ui5-segmentedbutton-item> </ui5-segmentedbutton>
1 parent cc8acc7 commit 9bd9ce4

11 files changed

+365
-138
lines changed

docs/Public Module Imports.md

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ For API documentation and samples, please check the [UI5 Web Components Playgrou
5959
| Select | `ui5-select` | `import "@ui5/webcomponents/dist/Select.js";` |
6060
| Select Option | `ui5-option` | comes with `ui5-select ` |
6161
| Segmented Button | `ui5-segmented-button` | `import "@ui5/webcomponents/dist/SegmentedButton.js";` |
62+
| Segmented Button Item | `ui5-segmented-button-item`| comes with `ui5-segmented-button ` |
6263
| Suggestion Item | `ui5-suggestion-item` | comes with `InputSuggestions.js` feature - see below |
6364
| Slider | `ui5-slider` | `import "@ui5/webcomponents/dist/Slider.js";` |
6465
| Step Input | `ui5-step-input` | `import "@ui5/webcomponents/dist/StepInput.js";` |

packages/main/bundle.common.js

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import Panel from "./dist/Panel.js";
5858
import RadioButton from "./dist/RadioButton.js";
5959
import ResponsivePopover from "./dist/ResponsivePopover.js";
6060
import SegmentedButton from "./dist/SegmentedButton.js";
61+
import SegmentedButtonItem from "./dist/SegmentedButtonItem.js";
6162
import Select from "./dist/Select.js";
6263
import Slider from "./dist/Slider.js";
6364
import StepInput from "./dist/StepInput.js";

packages/main/src/SegmentedButton.hbs

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
<div
1+
<ul
22
@click="{{_onclick}}"
3+
@keydown="{{_onkeydown}}"
4+
@keyup="{{_onkeyup}}"
35
@focusin="{{_onfocusin}}"
46
class="ui5-segmented-button-root"
5-
role="group"
7+
role="listbox"
68
dir="{{effectiveDir}}"
7-
aria-roledescription="{{ariaDescription}}"
9+
aria-multiselectable="true"
10+
aria-describedby="{{_id}}-invisibleText"
11+
aria-roledescription={{ariaDescription}}
812
>
913
<slot></slot>
10-
</div>
14+
15+
<span id="{{_id}}-invisibleText" class="ui5-hidden-text">{{ariaDescribedBy}}</span>
16+
17+
</ul>

packages/main/src/SegmentedButton.js

+81-50
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { fetchI18nBundle, getI18nBundle } from "@ui5/webcomponents-base/dist/i18
55
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
66
import { renderFinished } from "@ui5/webcomponents-base/dist/Render.js";
77
import { isIE } from "@ui5/webcomponents-base/dist/Device.js";
8-
import { SEGMENTEDBUTTON_ARIA_DESCRIPTION } from "./generated/i18n/i18n-defaults.js";
9-
import ToggleButton from "./ToggleButton.js";
8+
import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js";
9+
import { SEGMENTEDBUTTON_ARIA_DESCRIPTION, SEGMENTEDBUTTON_ARIA_DESCRIBEDBY } from "./generated/i18n/i18n-defaults.js";
10+
import SegmentedButtonItem from "./SegmentedButtonItem.js";
1011

1112
// Template
1213
import SegmentedButtonTemplate from "./generated/templates/SegmentedButtonTemplate.lit.js";
@@ -26,32 +27,32 @@ const metadata = {
2627
slots: /** @lends sap.ui.webcomponents.main.SegmentedButton.prototype */ {
2728

2829
/**
29-
* Defines the buttons of component.
30+
* Defines the items of <code>ui5-segmented-button</code>.
3031
* <br><br>
31-
* <b>Note:</b> Multiple buttons are allowed.
32+
* <b>Note:</b> Multiple items are allowed.
3233
* <br><br>
33-
* <b>Note:</b> Use the <code>ui5-toggle-button</code> for the intended design.
34+
* <b>Note:</b> Use the <code>ui5-segmented-button-item</code> for the intended design.
3435
* @type {sap.ui.webcomponents.main.IButton[]}
35-
* @slot buttons
36+
* @slot items
3637
* @public
3738
*/
3839
"default": {
39-
propertyName: "buttons",
40+
propertyName: "items",
4041
type: HTMLElement,
4142
},
4243
},
4344
events: /** @lends sap.ui.webcomponents.main.SegmentedButton.prototype */ {
4445

4546
/**
46-
* Fired when the selected button changes.
47+
* Fired when the selected item changes.
4748
*
4849
* @event sap.ui.webcomponents.main.SegmentedButton#selection-change
49-
* @param {HTMLElement} selectedButton the pressed button.
50+
* @param {HTMLElement} selectedItem the pressed item.
5051
* @public
5152
*/
5253
"selection-change": {
5354
detail: {
54-
selectedButton: { type: HTMLElement },
55+
selectedItem: { type: HTMLElement },
5556
},
5657
},
5758
},
@@ -62,11 +63,11 @@ const metadata = {
6263
*
6364
* <h3 class="comment-api-title">Overview</h3>
6465
*
65-
* The <code>ui5-segmented-button</code> shows a group of buttons. When the user clicks or taps
66-
* one of the buttons, it stays in a pressed state. It automatically resizes the buttons
66+
* The <code>ui5-segmented-button</code> shows a group of items. When the user clicks or taps
67+
* one of the items, it stays in a pressed state. It automatically resizes the items
6768
* to fit proportionally within the component. When no width is set, the component uses the available width.
6869
* <br><br>
69-
* <b>Note:</b> There can be just one selected <code>button</code> at a time.
70+
* <b>Note:</b> There can be just one selected <code>item</code> at a time.
7071
*
7172
* <h3>ES6 Module Import</h3>
7273
*
@@ -78,6 +79,7 @@ const metadata = {
7879
* @extends sap.ui.webcomponents.base.UI5Element
7980
* @tagname ui5-segmented-button
8081
* @since 1.0.0-rc.6
82+
* @appenddocs SegmentedButtonItem
8183
* @public
8284
*/
8385
class SegmentedButton extends UI5Element {
@@ -98,7 +100,7 @@ class SegmentedButton extends UI5Element {
98100
}
99101

100102
static get dependencies() {
101-
return [ToggleButton];
103+
return [SegmentedButtonItem];
102104
}
103105

104106
static async onDefine() {
@@ -109,7 +111,7 @@ class SegmentedButton extends UI5Element {
109111
super();
110112

111113
this._itemNavigation = new ItemNavigation(this, {
112-
getItemsCallback: () => this.getSlottedNodes("buttons"),
114+
getItemsCallback: () => this.getSlottedNodes("items"),
113115
});
114116

115117
this.absoluteWidthSet = false; // set to true whenever we set absolute width to the component
@@ -129,31 +131,38 @@ class SegmentedButton extends UI5Element {
129131
}
130132

131133
onBeforeRendering() {
134+
const items = this.getSlottedNodes("items");
135+
136+
items.forEach((item, index, arr) => {
137+
item.posInSet = index + 1;
138+
item.sizeOfSet = arr.length;
139+
});
140+
132141
this.normalizeSelection();
133142
}
134143

135144
async onAfterRendering() {
136145
await this._doLayout();
137146
}
138147

139-
prepareToMeasureButtons() {
148+
prepareToMeasureItems() {
140149
this.style.width = "";
141-
this.buttons.forEach(button => {
142-
button.style.width = "";
150+
this.items.forEach(item => {
151+
item.style.width = "";
143152
});
144153
}
145154

146-
async measureButtonsWidth() {
155+
async measureItemsWidth() {
147156
await renderFinished();
148-
this.prepareToMeasureButtons();
157+
this.prepareToMeasureItems();
149158

150-
this.widths = this.buttons.map(button => {
159+
this.widths = this.items.map(item => {
151160
// +1 is added because for width 100.44px the offsetWidth property returns 100px and not 101px
152-
let width = button.offsetWidth + 1;
161+
let width = item.offsetWidth + 1;
153162

154163
if (isIE()) {
155-
// in IE we are adding 1 one px beacause the width of the border on a button in the middle is not calculated and if the
156-
// longest button is in the middle, it truncates
164+
// in IE we are adding 1 one px beacause the width of the border on an item in the middle is not calculated and if the
165+
// longest item is in the middle, it truncates
157166
width += 1;
158167
}
159168

@@ -162,37 +171,55 @@ class SegmentedButton extends UI5Element {
162171
}
163172

164173
normalizeSelection() {
165-
this._selectedButton = this.buttons.filter(button => button.pressed).pop();
174+
this._selectedItem = this.items.filter(item => item.pressed).pop();
166175

167-
if (this._selectedButton) {
168-
this.buttons.forEach(button => {
169-
button.pressed = false;
176+
if (this._selectedItem) {
177+
this.items.forEach(item => {
178+
item.pressed = false;
170179
});
171-
this._selectedButton.pressed = true;
180+
this._selectedItem.pressed = true;
172181
}
173182
}
174183

175-
_onclick(event) {
184+
_selectItem(event) {
176185
if (event.target.disabled || event.target === this.getDomRef()) {
177186
return;
178187
}
179188

180-
if (event.target !== this._selectedButton) {
181-
if (this._selectedButton) {
182-
this._selectedButton.pressed = false;
189+
if (event.target !== this._selectedItem) {
190+
if (this._selectedItem) {
191+
this._selectedItem.pressed = false;
183192
}
184-
this._selectedButton = event.target;
193+
this._selectedItem = event.target;
185194
this.fireEvent("selection-change", {
186-
selectedButton: this._selectedButton,
195+
selectedItem: this._selectedItem,
187196
});
188197
}
189198

190-
this._selectedButton.pressed = true;
191-
this._itemNavigation.setCurrentItem(this._selectedButton);
199+
this._selectedItem.pressed = true;
200+
this._itemNavigation.setCurrentItem(this._selectedItem);
192201

193202
return this;
194203
}
195204

205+
_onclick(event) {
206+
this._selectItem(event);
207+
}
208+
209+
_onkeydown(event) {
210+
if (isEnter(event)) {
211+
this._selectItem(event);
212+
} else if (isSpace(event)) {
213+
event.preventDefault();
214+
}
215+
}
216+
217+
_onkeyup(event) {
218+
if (isSpace(event)) {
219+
this._selectItem(event);
220+
}
221+
}
222+
196223
_onfocusin(event) {
197224
// If the component was previously focused,
198225
// update the ItemNavigation to sync butons` tabindex values
@@ -203,28 +230,28 @@ class SegmentedButton extends UI5Element {
203230

204231
// If the component is focused for the first time
205232
// focus the selected item if such present
206-
if (this.selectedButton) {
207-
this.selectedButton.focus();
208-
this._itemNavigation.setCurrentItem(this._selectedButton);
233+
if (this.selectedItem) {
234+
this.selectedItem.focus();
235+
this._itemNavigation.setCurrentItem(this._selectedItem);
209236
this.hasPreviouslyFocusedItem = true;
210237
}
211238
}
212239

213240
async _doLayout() {
214-
const buttonsHaveWidth = this.widths && this.widths.some(button => button.offsetWidth > 2); // 2 are the pixel's added for rounding & IE
215-
if (!buttonsHaveWidth) {
216-
await this.measureButtonsWidth();
241+
const itemsHaveWidth = this.widths && this.widths.some(item => item.offsetWidth > 2); // 2 are the pixel's added for rounding & IE
242+
if (!itemsHaveWidth) {
243+
await this.measureItemsWidth();
217244
}
218245

219246
const parentWidth = this.parentNode.offsetWidth;
220247

221248
if (!this.style.width || this.percentageWidthSet) {
222-
this.style.width = `${Math.max(...this.widths) * this.buttons.length}px`;
249+
this.style.width = `${Math.max(...this.widths) * this.items.length}px`;
223250
this.absoluteWidthSet = true;
224251
}
225252

226-
this.buttons.forEach(button => {
227-
button.style.width = "100%";
253+
this.items.forEach(item => {
254+
item.style.width = "100%";
228255
});
229256

230257
if (parentWidth <= this.offsetWidth && this.absoluteWidthSet) {
@@ -234,14 +261,18 @@ class SegmentedButton extends UI5Element {
234261
}
235262

236263
/**
237-
* Currently selected button.
264+
* Currently selected item.
238265
*
239266
* @readonly
240-
* @type { ui5-toggle-button }
267+
* @type { ui5-segmented-button-item }
241268
* @public
242269
*/
243-
get selectedButton() {
244-
return this._selectedButton;
270+
get selectedItem() {
271+
return this._selectedItem;
272+
}
273+
274+
get ariaDescribedBy() {
275+
return this.i18nBundle.getText(SEGMENTEDBUTTON_ARIA_DESCRIBEDBY);
245276
}
246277

247278
get ariaDescription() {
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<li
2+
role="option"
3+
aria-roledescription="{{ariaDescription}}"
4+
aria-posinset="{{posInSet}}"
5+
aria-setsize="{{sizeOfSet}}"
6+
aria-selected="{{pressed}}"
7+
class="ui5-button-root"
8+
aria-disabled="{{disabled}}"
9+
data-sap-focus-ref
10+
{{> ariaPressed}}
11+
dir="{{effectiveDir}}"
12+
@focusout={{_onfocusout}}
13+
@focusin={{_onfocusin}}
14+
@click={{_onclick}}
15+
@mousedown={{_onmousedown}}
16+
@mouseup={{_onmouseup}}
17+
@keydown={{_onkeydown}}
18+
@keyup={{_onkeyup}}
19+
@touchstart="{{_ontouchstart}}"
20+
@touchend="{{_ontouchend}}"
21+
tabindex={{tabIndexValue}}
22+
aria-label="{{ariaLabelText}}"
23+
title="{{accInfo.title}}"
24+
>
25+
{{#if icon}}
26+
<ui5-icon
27+
class="ui5-button-icon"
28+
name="{{icon}}"
29+
part="icon"
30+
?show-tooltip={{showIconTooltip}}
31+
></ui5-icon>
32+
{{/if}}
33+
34+
<span id="{{_id}}-content" class="ui5-button-text">
35+
<bdi>
36+
<slot></slot>
37+
</bdi>
38+
</span>
39+
40+
</li>
41+
42+
{{#*inline "ariaPressed"}}{{/inline}}

0 commit comments

Comments
 (0)