Skip to content

Commit

Permalink
refactor(ui5-icon): add mode property (#8834)
Browse files Browse the repository at this point in the history
The properties `ariaHidden` , `interactive` and `accessibleRole` , previously available in the `ui5-icon` component, have been removed. 
They are replaced by a new property named `mode`.

BREAKING CHANGE: The properties `ariaHidden` , `interactive` and `accessibleRole` , previously available in the `ui5-icon` component, have been removed. They are replaced by a new property named `mode` that specifies the component's mode. 
Alongside this update, a new enumeration `IconMode`, has been introduced to outline the available options for this property:

`Image`: This is the default setting. It configures the component to internally render `role="img"`.
`Interactive`: Configures the component to internally render `role="button"`. This mode also supports focus and press handling to enhance interactivity.
`Decorative`: In this mode, the component internally renders `role="presentation"` and `aria-hidden="true"`, making it purely decorative without semantic content or interactivity.

Now, you can set the mode of the `ui5-icon` as it follows:
```html
<ui5-icon id="imageIcon" mode="Image" name="add-equipment"></ui5-icon>
<ui5-icon id="myInteractiveIcon" mode="Interactive" name="add-equipment"></ui5-icon>
<ui5-icon id="decorativeIcon" mode="Decorative" name="add-equipment"></ui5-icon>
```

Related to: #8461, #7887
  • Loading branch information
yanaminkova committed Apr 30, 2024
1 parent ecb3c61 commit 446483d
Show file tree
Hide file tree
Showing 14 changed files with 114 additions and 85 deletions.
2 changes: 1 addition & 1 deletion packages/main/src/Button.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<ui5-icon
class="ui5-button-icon"
name="{{icon}}"
accessible-role="{{iconRole}}"
mode="{{iconMode}}"
part="icon"
?show-tooltip={{showIconTooltip}}
></ui5-icon>
Expand Down
5 changes: 3 additions & 2 deletions packages/main/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import ButtonAccessibleRole from "./types/ButtonAccessibleRole.js";
import ButtonTemplate from "./generated/templates/ButtonTemplate.lit.js";
import Icon from "./Icon.js";
import HasPopup from "./types/HasPopup.js";
import IconMode from "./types/IconMode.js";

import { BUTTON_ARIA_TYPE_ACCEPT, BUTTON_ARIA_TYPE_REJECT, BUTTON_ARIA_TYPE_EMPHASIZED } from "./generated/i18n/i18n-defaults.js";

Expand Down Expand Up @@ -458,12 +459,12 @@ class Button extends UI5Element implements IFormElement, IButton {
return this.design !== ButtonDesign.Default && this.design !== ButtonDesign.Transparent;
}

get iconRole() {
get iconMode() {
if (!this.icon) {
return "";
}

return "presentation";
return IconMode.Decorative;
}

get isIconOnly() {
Expand Down
5 changes: 3 additions & 2 deletions packages/main/src/DatePicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type { CalendarSelectionChangeEventDetail } from "./Calendar.js";
import CalendarDateComponent from "./CalendarDate.js";
import Input from "./Input.js";
import InputType from "./types/InputType.js";
import IconMode from "./types/IconMode.js";
import DatePickerTemplate from "./generated/templates/DatePickerTemplate.lit.js";

// default calendar for bundling
Expand Down Expand Up @@ -726,8 +727,8 @@ class DatePicker extends DateComponentBase implements IFormElement {
* Defines whether the value help icon is hidden
* @private
*/
get _ariaHidden() {
return isDesktop();
get _iconMode() {
return isDesktop() ? IconMode.Decorative : IconMode.Interactive;
}

_respPopover() {
Expand Down
3 changes: 1 addition & 2 deletions packages/main/src/DatePickerInput.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
name="{{openIconName}}"
tabindex="-1"
accessible-name="{{openIconTitle}}"
accessible-role="button"
aria-hidden="{{_ariaHidden}}"
mode={{_iconMode}}
show-tooltip
@click="{{togglePicker}}"
input-icon
Expand Down
65 changes: 17 additions & 48 deletions packages/main/src/Icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { isSpace, isEnter } from "@ui5/webcomponents-base/dist/Keys.js";
import executeTemplate from "@ui5/webcomponents-base/dist/renderer/executeTemplate.js";
import IconTemplate from "./generated/templates/IconTemplate.lit.js";
import IconDesign from "./types/IconDesign.js";
import IconMode from "./types/IconMode.js";

// Styles
import iconCss from "./generated/themes/Icon.css.js";
Expand All @@ -22,7 +23,6 @@ import iconCss from "./generated/themes/Icon.css.js";
interface IIcon extends HTMLElement { }

const ICON_NOT_FOUND = "ICON_NOT_FOUND";
const PRESENTATION_ROLE = "presentation";

/**
* @class
Expand Down Expand Up @@ -82,7 +82,7 @@ const PRESENTATION_ROLE = "presentation";
*
* ### Keyboard Handling
*
* - [Space] / [Enter] or [Return] - Fires the `click` event if the `interactive` property is set to true.
* - [Space] / [Enter] or [Return] - Fires the `click` event if the `mode` property is set to `Interactive`.
* - [Shift] - If [Space] / [Enter] or [Return] is pressed, pressing [Shift] releases the ui5-icon without triggering the click event.
*
* ### ES6 Module Import
Expand Down Expand Up @@ -120,15 +120,6 @@ class Icon extends UI5Element implements IIcon {
@property({ type: IconDesign, defaultValue: IconDesign.Default })
design!: `${IconDesign}`;

/**
* Defines if the icon is interactive (focusable and pressable)
* @default false
* @public
* @since 1.0.0-rc.8
*/
@property({ type: Boolean })
interactive!: boolean;

/**
* Defines the unique identifier (icon name) of the component.
*
Expand Down Expand Up @@ -180,22 +171,13 @@ class Icon extends UI5Element implements IIcon {
showTooltip!: boolean;

/**
* Defines the accessibility role of the component.
* @default ""
* Defines the mode of the component.
* @default "Image"
* @public
* @since 1.1.0
*/
@property()
accessibleRole!: string;

/**
* Defines the ARIA hidden state of the component.
* Note: If the role is presentation the default value of aria-hidden will be true.
* @private
* @since 1.0.0-rc.15
* @since 2.0.0
*/
@property()
ariaHidden!: string;
@property({ type: IconMode, defaultValue: IconMode.Image })
mode!: `${IconMode}`;

/**
* @private
Expand Down Expand Up @@ -227,7 +209,7 @@ class Icon extends UI5Element implements IIcon {
customSvg?: object;

_onkeydown(e: KeyboardEvent) {
if (!this.interactive) {
if (this.mode !== IconMode.Interactive) {
return;
}

Expand All @@ -241,7 +223,7 @@ class Icon extends UI5Element implements IIcon {
}

_onkeyup(e: KeyboardEvent) {
if (this.interactive && isSpace(e)) {
if (this.mode === IconMode.Interactive && isSpace(e)) {
this.fireEvent("click");
}
}
Expand All @@ -254,35 +236,22 @@ class Icon extends UI5Element implements IIcon {
}

get effectiveAriaHidden() {
if (this.ariaHidden === "") {
if (this.isDecorative) {
return true;
}

return;
}

return this.ariaHidden;
return this.mode === IconMode.Decorative ? "true" : undefined;
}

get _tabIndex() {
return this.interactive ? "0" : undefined;
}

get isDecorative() {
return this.effectiveAccessibleRole === PRESENTATION_ROLE;
return this.mode === IconMode.Interactive ? "0" : undefined;
}

get effectiveAccessibleRole() {
if (this.accessibleRole) {
return this.accessibleRole;
}

if (this.interactive) {
switch (this.mode) {
case IconMode.Interactive:
return "button";
case IconMode.Decorative:
return "presentation";
default:
return "img";
}

return this.effectiveAccessibleName ? "img" : PRESENTATION_ROLE;
}

onEnterDOM() {
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/Select.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
>
{{#if selectedOptionIcon}}
<ui5-icon
aria-hidden="true"
mode="Decorative"
class="ui5-select-option-icon"
name="{{selectedOptionIcon}}"
></ui5-icon>
Expand Down
4 changes: 2 additions & 2 deletions packages/main/src/StandardListItem.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@

{{#*inline "iconBegin"}}
{{#if displayIconBegin}}
<ui5-icon part="icon" name="{{icon}}" class="ui5-li-icon" accessible-role="presentation" aria-hidden="true"></ui5-icon>
<ui5-icon part="icon" name="{{icon}}" class="ui5-li-icon" mode="Decorative"></ui5-icon>
{{/if}}
{{/inline}}

{{#*inline "iconEnd"}}
{{#if displayIconEnd}}
<ui5-icon part="icon" name="{{icon}}" class="ui5-li-icon" accessible-role="presentation" aria-hidden="true"></ui5-icon>
<ui5-icon part="icon" name="{{icon}}" class="ui5-li-icon" mode="Decorative"></ui5-icon>
{{/if}}
{{/inline}}
6 changes: 3 additions & 3 deletions packages/main/src/themes/Icon.css
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@
color: var(--sapPositiveElementColor);
}

:host([interactive][desktop]) .ui5-icon-root:focus-within,
:host([interactive]) .ui5-icon-root:focus-visible {
:host([mode="Interactive"][desktop]) .ui5-icon-root:focus-within,
:host([mode="Interactive"]) .ui5-icon-root:focus-visible {
outline: var(--sapContent_FocusWidth) var(--sapContent_FocusStyle) var(--sapContent_FocusColor);
border-radius: var(--ui5-icon-focus-border-radius);
}
Expand All @@ -65,7 +65,7 @@
}


:host([interactive]) {
:host([mode="Interactive"]) {
cursor: pointer;
}

Expand Down
26 changes: 26 additions & 0 deletions packages/main/src/types/IconMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Different Icon modes.
* @public
* @since 2.0.0
*/
enum IconMode {
/**
* Image mode (by default)
* @public
*/
Image = "Image",

/**
* Decorative mode
* @public
*/
Decorative = "Decorative",

/**
* Interactive mode
* @public
*/
Interactive = "Interactive",
}

export default IconMode;
32 changes: 25 additions & 7 deletions packages/main/test/pages/Icon.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,35 @@ <h3>Icon semantic 'design' variants</h3>
<ui5-icon name="female" class="icon3auto" design="Neutral"></ui5-icon>
<ui5-icon name="female" class="icon3auto" design="Positive"></ui5-icon>

<h3>Icon with aria-hidden="true"</h3>
<ui5-icon id="araHiddenIcon" name="add-employee" aria-hidden="true" class="icon-red icon-small"></ui5-icon>

<h3>Icon with tooltip</h3>
<ui5-icon title="company view" class="icon3auto" show-tooltip accessible-name="This is the tooltip`s text" name="company-view"></ui5-icon>

<h3>Interactive Icon</h3>
<ui5-icon id="accRoleIcon" accessible-role="link" interactive class="samples-margin" accessible-name="Hello World" name="horizon/accept"></ui5-icon>
<h3>API: mode="Interactive"</h3>
<ui5-icon mode="Interactive" class="samples-margin" accessible-name="Hello World" name="horizon/accept"></ui5-icon>
<ui5-icon
id="myInteractiveIcon"
interactive
mode="Interactive"
name="add-equipment"
class="icon3auto">
</ui5-icon>

<br>

<h3>API: mode="Image" (by default)</h3>
<ui5-icon class="samples-margin" accessible-name="Hello World" name="horizon/accept"></ui5-icon>
<ui5-icon
id="imageIcon"
name="add-equipment"
class="icon3auto">
</ui5-icon>

<br>

<h3>API: mode="Decorative"</h3>
<ui5-icon mode="Decorative" class="samples-margin" accessible-name="Hello World" name="horizon/accept"></ui5-icon>
<ui5-icon
id="decorativeIcon"
mode="Decorative"
name="add-equipment"
class="icon3auto">
</ui5-icon>
Expand Down Expand Up @@ -89,7 +107,7 @@ <h3>Interactive Icon</h3>

<ui5-title>Tests "click" and "ui5-click" events</ui5-title>
<br/>
<ui5-icon id="interactive-icon" name="add-equipment" class="icon-blue icon-medium" interactive></ui5-icon>
<ui5-icon id="interactive-icon" name="add-equipment" class="icon-blue icon-medium" mode="Interactive"></ui5-icon>
<ui5-label>"click"</ui5-label><ui5-input id="click-event" value="0"></ui5-input>
<ui5-label>"ui5-click"</ui5-label><ui5-input id="ui5-click-event" value="0"></ui5-input>
<br/>
Expand Down
36 changes: 26 additions & 10 deletions packages/main/test/specs/Icon.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,6 @@ describe("Icon general interaction", () => {
assert.strictEqual(await inpUI5ClickRes2.getAttribute("value"), "0", "The 'ui5-click' event is not fired on mouse click..");
});

it("Tests the accessibility attributes", async () => {
const iconRoot = await browser.$("#myIcon").shadow$(".ui5-icon-root");
const accRoleIconRoot = await browser.$("#accRoleIcon").shadow$(".ui5-icon-root");
const ariaHiddenIconRoot = await browser.$("#araHiddenIcon").shadow$(".ui5-icon-root");

assert.strictEqual(await iconRoot.getAttribute("aria-hidden"), null, "The aria-hidden attribute is not set");
assert.strictEqual(await accRoleIconRoot.getAttribute("role"), "link", "The accessibleRole property works");
assert.strictEqual(await ariaHiddenIconRoot.getAttribute("aria-hidden"), "true", "The ariaHidden property works");
});

it("Tests switch to sap_horizon", async () => {
const V4_PATH_START = "M118";
const V5_PATH_START = "M486";
Expand Down Expand Up @@ -125,4 +115,30 @@ describe("Icon general interaction", () => {
assert.strictEqual(actualAccNames.join(), expectedAccNames.join(),
"getIconAccessibleName returns the correct icon a11y names.");
});

it("Tests mode property", async () => {
let icon = await browser.$("#imageIcon");
let iconSVG = await browser.$("#imageIcon").shadow$(".ui5-icon-root");
let mode = icon.getProperty("mode");
const intercativeMode = "Interactive";
const imageMode = "Image";
const decorativeMode = "Decorative";

assert.equal(await mode, imageMode, "Image mode is correctly set by default.");
assert.equal(await iconSVG.getAttribute("role"), "img", "The SVG for the image icon has the correct role.");

await icon.setProperty("mode", intercativeMode)
mode = await icon.getProperty("mode");

assert.equal(await mode, intercativeMode, "Interactive mode is correctly set.");
assert.equal(await iconSVG.getAttribute("role"), "button", "The SVG for the interactive icon has the correct role.");

await icon.setProperty("mode", decorativeMode)
mode = await icon.getProperty("mode");

assert.equal(await mode, decorativeMode, "Decorative mode is correctly set.");
assert.equal(await iconSVG.getAttribute("role"), "presentation", "The SVG for the decorative icon has the correct role.");
assert.equal(await iconSVG.getAttribute("aria-hidden"), "true", "The SVG for the decorative icon includes aria-hidden=true as expected");

});
});
3 changes: 1 addition & 2 deletions packages/playground/_stories/main/Icon/Icon.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ export default {
const Template: UI5StoryArgs<Icon, StoryArgsSlots> = (args) =>
html`<ui5-icon
design="${ifDefined(args.design)}"
?interactive="${ifDefined(args.interactive)}"
mode="${ifDefined(args.mode)}"
name="${ifDefined(args.name)}"
accessible-name="${ifDefined(args.accessibleName)}"
accessible-role="${ifDefined(args.accessibleRole)}"
?show-tooltip="${ifDefined(args.showTooltip)}"
style="${ifDefined(args.style)}"
></ui5-icon>`;
Expand Down
8 changes: 4 additions & 4 deletions packages/playground/docs/landing-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,14 @@ <h2 class="title">Browser Compatibility</h2>
<!-------------------------------------- TESTIMONIALS -------------------------------------->

<section class="testimonials">

<div class="container">
<h2 class="title">Testimonials</h2>
<div class="slideshow">
<ui5-icon name="navigation-left-arrow" interactive class="arrow left"></ui5-icon>
<ui5-icon name="navigation-left-arrow" mode="Interactive" class="arrow left"></ui5-icon>
<div class="slide active">
<div class="slide-text">"I absolutely loved working with the UI5 web components since they are
very easy to plug into any application. It made our MDK Web runtime development much easier
<div class="slide-text">"I absolutely loved working with the UI5 web components since they are
very easy to plug into any application. It made our MDK Web runtime development much easier
and more productive."
</div>
<div>
Expand Down

0 comments on commit 446483d

Please sign in to comment.