Skip to content

Commit

Permalink
feat(ui5-input): Add highlighting (#1943)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilhan007 authored Jul 20, 2020
1 parent 104abcc commit 673ed8d
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 27 deletions.
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.
* <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

0 comments on commit 673ed8d

Please sign in to comment.