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(ui5-input): Add highlighting #1943

Merged
merged 18 commits into from
Jul 20, 2020
Merged
27 changes: 23 additions & 4 deletions packages/main/src/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,21 @@ const metadata = {
type: Boolean,
},

/**
* Defines if characters within the suggestions are to be highlighted
* in case the input value matches parts of the suggestions text.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add a comment that it only works with suggestions enabled.

* <br><br>
* <b>Note:</b> takes effect when <code>showSuggestions</code> is set to <code>true</code>
*
* @type {boolean}
* @defaultvalue false
* @public
* @sicne 1.0.0-rc.8
*/
highlight: {
type: Boolean,
},

/**
* Defines a short hint intended to aid the user with data entry when the
* <code>ui5-input</code> has no value.
Expand Down Expand Up @@ -486,6 +501,9 @@ class Input extends UI5Element {
// Indicates, if the component is rendering for first time.
this.firstRendering = true;

// The value that should be highlited.
this.highlightValue = "";

// all sementic events
this.EVENT_SUBMIT = "submit";
this.EVENT_CHANGE = "change";
Expand Down Expand Up @@ -515,7 +533,7 @@ class Input extends UI5Element {
onBeforeRendering() {
if (this.showSuggestions) {
this.enableSuggestions();
this.suggestionsTexts = this.Suggestions.defaultSlotProperties();
this.suggestionsTexts = this.Suggestions.defaultSlotProperties(this.highlightValue);
}

const FormSupport = getFeature("FormSupport");
Expand Down Expand Up @@ -741,12 +759,13 @@ class Input extends UI5Element {

enableSuggestions() {
if (this.Suggestions) {
this.Suggestions.highlight = this.highlight;
return;
}

const Suggestions = getFeature("InputSuggestions");
if (Suggestions) {
this.Suggestions = new Suggestions(this, "suggestionItems");
this.Suggestions = new Suggestions(this, "suggestionItems", this.highlight);
} else {
throw new Error(`You have to import "@ui5/webcomponents/dist/features/InputSuggestions.js" module to use ui5-input suggestions`);
}
Expand Down Expand Up @@ -781,9 +800,8 @@ class Input extends UI5Element {

previewSuggestion(item) {
const emptyValue = item.type === "Inactive" || item.group;

this.valueBeforeItemSelection = this.value;
this.updateValueOnPreview(emptyValue ? "" : item.textContent);
this.updateValueOnPreview(emptyValue ? "" : item.effectiveTitle);
this.announceSelectedItem();
this._previewItem = item;
}
Expand Down Expand Up @@ -822,6 +840,7 @@ class Input extends UI5Element {
const isUserInput = action === this.ACTION_USER_INPUT;

this.value = inputValue;
this.highlightValue = inputValue;

if (isUserInput) { // input
this.fireEvent(this.EVENT_INPUT);
Expand Down
12 changes: 8 additions & 4 deletions packages/main/src/InputPopover.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,22 @@
<ui5-list separators="{{suggestionSeparators}}">
{{#each suggestionsTexts}}
{{#if group}}
<ui5-li-groupheader data-ui5-key="{{key}}">{{ this.text }}</ui5-li-groupheader>
<ui5-li-groupheader data-ui5-key="{{key}}">{{{ this.text }}}</ui5-li-groupheader>
{{else}}
<ui5-li
<ui5-li-suggestion-item
image="{{this.image}}"
icon="{{this.icon}}"
description="{{this.description}}"
info="{{this.info}}"
type="{{this.type}}"
info-state="{{this.infoState}}"
@ui5-_item-press="{{ fnOnSuggestionItemPress }}"
data-ui5-key="{{key}}"
>{{ this.text }}</ui5-li>
>
{{{ this.text }}}
{{#if this.description}}
<span slot="richDescription">{{{ this.description }}}</span>
{{/if}}
</ui5-li-suggestion-item>
{{/if}}
{{/each}}
</ui5-list>
Expand Down
4 changes: 2 additions & 2 deletions packages/main/src/SuggestionItem.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";

import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
import StandardListItem from "./StandardListItem.js";
import SuggestionListItem from "./SuggestionListItem.js";
import GroupHeaderListItem from "./GroupHeaderListItem.js";
import ListItemType from "./types/ListItemType.js";

Expand Down Expand Up @@ -147,7 +147,7 @@ class SuggestionItem extends UI5Element {

static async onDefine() {
await Promise.all([
StandardListItem.define(),
SuggestionListItem.define(),
GroupHeaderListItem.define(),
]);
}
Expand Down
25 changes: 25 additions & 0 deletions packages/main/src/SuggestionListItem.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{{>include "./StandardListItem.hbs"}}

{{#*inline "listItemContent"}}
<div class="ui5-li-title-wrapper">
{{#if hasTitle}}
<span part="title" class="ui5-li-title"><slot></slot></span>
{{/if}}
{{#if hasDescription}}
<span part="description" class="ui5-li-desc">
{{#if richDescription.length}}
<slot name="richDescription"></slot>
{{else}}
{{description}}
{{/if}}
</span>
{{/if}}
{{#unless typeActive}}
<span class="ui5-hidden-text">{{type}}</span>
{{/unless}}
</div>
{{#if info}}
<span part="info" class="ui5-li-info">{{info}}</span>
{{/if}}
{{/inline}}

64 changes: 64 additions & 0 deletions packages/main/src/SuggestionListItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import StandardListItem from "./StandardListItem.js";
import SuggestionListItemTemplate from "./generated/templates/SuggestionListItemTemplate.lit.js";

/**
* @public
*/
const metadata = {
tag: "ui5-li-suggestion-item",
managedSlots: true,
slots: {
/**
* Defines a description that can contain HTML.
* <b>Note:</b> If not specified, the <code>description</code> property will be used.
* <br>
* @type {HTMLElement}
* @since 1.0.0-rc.8
* @slot
* @public
*/
richDescription: {
type: HTMLElement,
},
"default": {
propertyName: "title",
},
},
};

/**
* @class
* The <code>ui5-li-suggestion-item</code> represents the suggestion item in the <code>ui5-input</code>
* suggestion popover.
*
* @constructor
* @author SAP SE
* @alias sap.ui.webcomponents.main.SuggestionListItem
* @extends UI5Element
*/
class SuggestionListItem extends StandardListItem {
static get metadata() {
return metadata;
}

static get template() {
return SuggestionListItemTemplate;
}

onBeforeRendering(...params) {
super.onBeforeRendering(...params);
this.hasTitle = !!this.title.length;
}

get effectiveTitle() {
return this.title.map(el => el.textContent).join("");
}

get hasDescription() {
return this.richDescription.length || this.description;
}
}

SuggestionListItem.define();

export default SuggestionListItem;
57 changes: 51 additions & 6 deletions packages/main/src/features/InputSuggestions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
* @author SAP SE
*/
class Suggestions {
constructor(component, slotName, handleFocus) {
constructor(component, slotName, highlight, handleFocus) {
// The component, that the suggestion would plug into.
this.component = component;

Expand All @@ -27,6 +27,9 @@ class Suggestions {
// Defines, if the focus will be moved via the arrow keys.
this.handleFocus = handleFocus;

// Defines, if the suggestions should highlight.
this.highlight = highlight;

// Press and Focus handlers
this.fnOnSuggestionItemPress = this.onItemPress.bind(this);
this.fnOnSuggestionItemFocus = this.onItemFocused.bind(this);
Expand All @@ -43,14 +46,18 @@ class Suggestions {
}

/* Public methods */
defaultSlotProperties() {
defaultSlotProperties(hightlightValue) {
const inputSuggestionItems = this._getComponent().suggestionItems;

const highlight = this.highlight && !!hightlightValue;
const suggestions = [];

inputSuggestionItems.map((suggestion, idx) => {
const text = highlight ? this.getHighlightedText(suggestion, hightlightValue) : this.getRowText(suggestion);
const description = highlight ? this.getHighlightedDesc(suggestion, hightlightValue) : this.getRowDesc(suggestion);

return suggestions.push({
text: suggestion.text || suggestion.textContent, // keep textContent for compatibility
description: suggestion.description || undefined,
text,
description,
image: suggestion.image || undefined,
icon: suggestion.icon || undefined,
type: suggestion.type || undefined,
Expand Down Expand Up @@ -311,7 +318,7 @@ class Suggestions {
}

_getItems() {
return [].slice.call(this.responsivePopover.querySelectorAll("ui5-li, ui5-li-groupheader"));
return [].slice.call(this.responsivePopover.querySelectorAll("ui5-li-groupheader, ui5-li-suggestion-item"));
}

_getComponent() {
Expand Down Expand Up @@ -349,6 +356,44 @@ class Suggestions {

return `${itemPositionText} ${this.accInfo.itemText} ${itemSelectionText}`;
}

getRowText(suggestion) {
return this.sanitizeText(suggestion.text || suggestion.textContent);
}

getRowDesc(suggestion) {
if (suggestion.description) {
return this.sanitizeText(suggestion.description);
}
}

getHighlightedText(suggestion, input) {
let text = suggestion.text || suggestion.textContent;
text = this.sanitizeText(text);

return this.hightlightInput(text, input);
}

getHighlightedDesc(suggestion, input) {
let text = suggestion.description;
text = this.sanitizeText(text);

return this.hightlightInput(text, input);
}

hightlightInput(text, input) {
if (!text) {
return text;
}

const inputEscaped = input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regEx = new RegExp(inputEscaped, "ig");
return text.replace(regEx, match => `<b>${match}</b>`);
}

sanitizeText(text) {
return text && text.replace("<", "&lt");
}
}

Suggestions.SCROLL_STEP = 60;
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/themes/ListItemBase.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
height: var(--_ui5_list_item_base_height);
background: var(--ui5-listitem-background-color);
box-sizing: border-box;
border-bottom: 1px solid transparent;
}

/* selected */
Expand Down
36 changes: 26 additions & 10 deletions packages/main/test/pages/Input.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ <h3>Input in Cozy</h3>
<ui5-input id="myInput"
style="width: 500px"
show-suggestions
placeholder="Search for a country ...">
placeholder="Search for a country ..."
highlight>
</ui5-input>
</div>

Expand Down Expand Up @@ -104,6 +105,14 @@ <h3>Input suggestions with grouping</h3>
<ui5-suggestion-item type="Inactive" text="Inactive HCB"></ui5-suggestion-item>
</ui5-input>

<h3>Input suggestions with highlighing</h3>
<ui5-input id="myInputHighlighted" highlight show-suggestions style="width: 100%">
<ui5-suggestion-item text="Adam D" description="Administrative Support"></ui5-suggestion-item>
<ui5-suggestion-item text="Aanya Sing" description="Administrative Support"></ui5-suggestion-item>
<ui5-suggestion-item text="Allen K" description="Technical Support"></ui5-suggestion-item>
<ui5-suggestion-item text="Alex" description="Technical Support"></ui5-suggestion-item>
</ui5-input>

<h3> Input disabled</h3>
<ui5-input style="width: auto" id="input-disabled" disabled placeholder="Disabled one ...">
<ui5-icon slot="icon" name="appointment-2"></ui5-icon>
Expand Down Expand Up @@ -305,6 +314,13 @@ <h3>Test ariaLabel and ariaLabelledBy</h3>
<ui5-label id="enterNameLabel">Enter name: </ui5-label>
<ui5-input aria-labelledby="enterNameLabel"></ui5-input>

<h3>Input suggestions with highlighing and XSS test</h3>
<ui5-input highlight show-suggestions style="width: 100%">
<ui5-suggestion-item text="<script>alert('XSS')</script>" description="Administrative Support"></ui5-suggestion-item>
<ui5-suggestion-item text="Aanya Sing" description="<b onmouseover=alert('XSS')></b>">
</ui5-suggestion-item>
</ui5-input>

<script>
var sap_database_entries = [{ key: "A", text: "A" }, { key: "Afg", text: "Afghanistan" }, { key: "Arg", text: "Argentina" }, { key: "Alb", text: "Albania" }, { key: "Arm", text: "Armenia" }, { key: "Alg", text: "Algeria" }, { key: "And", text: "Andorra" }, { key: "Ang", text: "Angola" }, { key: "Ast", text: "Austria" }, { key: "Aus", text: "Australia" }, { key: "Aze", text: "Azerbaijan" }, { key: "Aruba", text: "Aruba" }, { key: "Antigua", text: "Antigua and Barbuda" }, { key: "B", text: "B" }, { key: "Bel", text: "Belarus" }, { key: "Bel", text: "Belgium" }, { key: "Bg", text: "Bulgaria" }, { key: "Bra", text: "Brazil" }, { key: "C", text: "C" }, { key: "Ch", text: "China" }, { key: "Cub", text: "Cuba" }, { key: "Chil", text: "Chili" }, { key: "L", text: "L" }, { key: "Lat", text: "Latvia" }, { key: "Lit", text: "Litva" }, { key: "P", text: "P" }, { key: "Prt", text: "Portugal" }, { key: "S", text: "S" }, { key: "Sen", text: "Senegal" }, { key: "Ser", text: "Serbia" }, { key: "Sey", text: "Seychelles" }, { key: "Sierra", text: "Sierra Leone" }, { key: "Sgp", text: "Singapore" }, { key: "Sint", text: "Sint Maarten" }, { key: "Slv", text: "Slovakia" }, { key: "Slo", text: "Slovenia" }];

Expand Down Expand Up @@ -340,15 +356,15 @@ <h3>Test ariaLabel and ariaLabelledBy</h3>
}

suggestionItems.forEach(function(item, idx) {
var li = document.createElement("ui5-suggestion-item");
li.id = item.key;
li.icon = "world";
li.info = "explore";
li.group = item.text.length === 1;
li.infoState = "Success";
li.description = "travel the world";
li.text = item.text;
input.appendChild(li);
var suggestion = document.createElement("ui5-suggestion-item");
suggestion.id = item.key;
suggestion.icon = "world";
suggestion.info = "explore";
suggestion.group = item.text.length === 1;
suggestion.infoState = "Success";
suggestion.description = "travel the world";
suggestion.text = item.text
input.appendChild(suggestion);
});

labelLiveChange.innerHTML = "Event [input] :: " + value;
Expand Down
Loading