Skip to content

Commit 673ed8d

Browse files
authored
feat(ui5-input): Add highlighting (#1943)
1 parent 104abcc commit 673ed8d

File tree

9 files changed

+216
-27
lines changed

9 files changed

+216
-27
lines changed

packages/main/src/Input.js

+23-4
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@ const metadata = {
134134
type: Boolean,
135135
},
136136

137+
/**
138+
* Defines if characters within the suggestions are to be highlighted
139+
* in case the input value matches parts of the suggestions text.
140+
* <br><br>
141+
* <b>Note:</b> takes effect when <code>showSuggestions</code> is set to <code>true</code>
142+
*
143+
* @type {boolean}
144+
* @defaultvalue false
145+
* @public
146+
* @sicne 1.0.0-rc.8
147+
*/
148+
highlight: {
149+
type: Boolean,
150+
},
151+
137152
/**
138153
* Defines a short hint intended to aid the user with data entry when the
139154
* <code>ui5-input</code> has no value.
@@ -486,6 +501,9 @@ class Input extends UI5Element {
486501
// Indicates, if the component is rendering for first time.
487502
this.firstRendering = true;
488503

504+
// The value that should be highlited.
505+
this.highlightValue = "";
506+
489507
// all sementic events
490508
this.EVENT_SUBMIT = "submit";
491509
this.EVENT_CHANGE = "change";
@@ -515,7 +533,7 @@ class Input extends UI5Element {
515533
onBeforeRendering() {
516534
if (this.showSuggestions) {
517535
this.enableSuggestions();
518-
this.suggestionsTexts = this.Suggestions.defaultSlotProperties();
536+
this.suggestionsTexts = this.Suggestions.defaultSlotProperties(this.highlightValue);
519537
}
520538

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

742760
enableSuggestions() {
743761
if (this.Suggestions) {
762+
this.Suggestions.highlight = this.highlight;
744763
return;
745764
}
746765

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

782801
previewSuggestion(item) {
783802
const emptyValue = item.type === "Inactive" || item.group;
784-
785803
this.valueBeforeItemSelection = this.value;
786-
this.updateValueOnPreview(emptyValue ? "" : item.textContent);
804+
this.updateValueOnPreview(emptyValue ? "" : item.effectiveTitle);
787805
this.announceSelectedItem();
788806
this._previewItem = item;
789807
}
@@ -822,6 +840,7 @@ class Input extends UI5Element {
822840
const isUserInput = action === this.ACTION_USER_INPUT;
823841

824842
this.value = inputValue;
843+
this.highlightValue = inputValue;
825844

826845
if (isUserInput) { // input
827846
this.fireEvent(this.EVENT_INPUT);

packages/main/src/InputPopover.hbs

+8-4
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,22 @@
9191
<ui5-list separators="{{suggestionSeparators}}">
9292
{{#each suggestionsTexts}}
9393
{{#if group}}
94-
<ui5-li-groupheader data-ui5-key="{{key}}">{{ this.text }}</ui5-li-groupheader>
94+
<ui5-li-groupheader data-ui5-key="{{key}}">{{{ this.text }}}</ui5-li-groupheader>
9595
{{else}}
96-
<ui5-li
96+
<ui5-li-suggestion-item
9797
image="{{this.image}}"
9898
icon="{{this.icon}}"
99-
description="{{this.description}}"
10099
info="{{this.info}}"
101100
type="{{this.type}}"
102101
info-state="{{this.infoState}}"
103102
@ui5-_item-press="{{ fnOnSuggestionItemPress }}"
104103
data-ui5-key="{{key}}"
105-
>{{ this.text }}</ui5-li>
104+
>
105+
{{{ this.text }}}
106+
{{#if this.description}}
107+
<span slot="richDescription">{{{ this.description }}}</span>
108+
{{/if}}
109+
</ui5-li-suggestion-item>
106110
{{/if}}
107111
{{/each}}
108112
</ui5-list>

packages/main/src/SuggestionItem.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
22

33
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
4-
import StandardListItem from "./StandardListItem.js";
4+
import SuggestionListItem from "./SuggestionListItem.js";
55
import GroupHeaderListItem from "./GroupHeaderListItem.js";
66
import ListItemType from "./types/ListItemType.js";
77

@@ -147,7 +147,7 @@ class SuggestionItem extends UI5Element {
147147

148148
static async onDefine() {
149149
await Promise.all([
150-
StandardListItem.define(),
150+
SuggestionListItem.define(),
151151
GroupHeaderListItem.define(),
152152
]);
153153
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{{>include "./StandardListItem.hbs"}}
2+
3+
{{#*inline "listItemContent"}}
4+
<div class="ui5-li-title-wrapper">
5+
{{#if hasTitle}}
6+
<span part="title" class="ui5-li-title"><slot></slot></span>
7+
{{/if}}
8+
{{#if hasDescription}}
9+
<span part="description" class="ui5-li-desc">
10+
{{#if richDescription.length}}
11+
<slot name="richDescription"></slot>
12+
{{else}}
13+
{{description}}
14+
{{/if}}
15+
</span>
16+
{{/if}}
17+
{{#unless typeActive}}
18+
<span class="ui5-hidden-text">{{type}}</span>
19+
{{/unless}}
20+
</div>
21+
{{#if info}}
22+
<span part="info" class="ui5-li-info">{{info}}</span>
23+
{{/if}}
24+
{{/inline}}
25+
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import StandardListItem from "./StandardListItem.js";
2+
import SuggestionListItemTemplate from "./generated/templates/SuggestionListItemTemplate.lit.js";
3+
4+
/**
5+
* @public
6+
*/
7+
const metadata = {
8+
tag: "ui5-li-suggestion-item",
9+
managedSlots: true,
10+
slots: {
11+
/**
12+
* Defines a description that can contain HTML.
13+
* <b>Note:</b> If not specified, the <code>description</code> property will be used.
14+
* <br>
15+
* @type {HTMLElement}
16+
* @since 1.0.0-rc.8
17+
* @slot
18+
* @public
19+
*/
20+
richDescription: {
21+
type: HTMLElement,
22+
},
23+
"default": {
24+
propertyName: "title",
25+
},
26+
},
27+
};
28+
29+
/**
30+
* @class
31+
* The <code>ui5-li-suggestion-item</code> represents the suggestion item in the <code>ui5-input</code>
32+
* suggestion popover.
33+
*
34+
* @constructor
35+
* @author SAP SE
36+
* @alias sap.ui.webcomponents.main.SuggestionListItem
37+
* @extends UI5Element
38+
*/
39+
class SuggestionListItem extends StandardListItem {
40+
static get metadata() {
41+
return metadata;
42+
}
43+
44+
static get template() {
45+
return SuggestionListItemTemplate;
46+
}
47+
48+
onBeforeRendering(...params) {
49+
super.onBeforeRendering(...params);
50+
this.hasTitle = !!this.title.length;
51+
}
52+
53+
get effectiveTitle() {
54+
return this.title.map(el => el.textContent).join("");
55+
}
56+
57+
get hasDescription() {
58+
return this.richDescription.length || this.description;
59+
}
60+
}
61+
62+
SuggestionListItem.define();
63+
64+
export default SuggestionListItem;

packages/main/src/features/InputSuggestions.js

+51-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
* @author SAP SE
1818
*/
1919
class Suggestions {
20-
constructor(component, slotName, handleFocus) {
20+
constructor(component, slotName, highlight, handleFocus) {
2121
// The component, that the suggestion would plug into.
2222
this.component = component;
2323

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

30+
// Defines, if the suggestions should highlight.
31+
this.highlight = highlight;
32+
3033
// Press and Focus handlers
3134
this.fnOnSuggestionItemPress = this.onItemPress.bind(this);
3235
this.fnOnSuggestionItemFocus = this.onItemFocused.bind(this);
@@ -43,14 +46,18 @@ class Suggestions {
4346
}
4447

4548
/* Public methods */
46-
defaultSlotProperties() {
49+
defaultSlotProperties(hightlightValue) {
4750
const inputSuggestionItems = this._getComponent().suggestionItems;
48-
51+
const highlight = this.highlight && !!hightlightValue;
4952
const suggestions = [];
53+
5054
inputSuggestionItems.map((suggestion, idx) => {
55+
const text = highlight ? this.getHighlightedText(suggestion, hightlightValue) : this.getRowText(suggestion);
56+
const description = highlight ? this.getHighlightedDesc(suggestion, hightlightValue) : this.getRowDesc(suggestion);
57+
5158
return suggestions.push({
52-
text: suggestion.text || suggestion.textContent, // keep textContent for compatibility
53-
description: suggestion.description || undefined,
59+
text,
60+
description,
5461
image: suggestion.image || undefined,
5562
icon: suggestion.icon || undefined,
5663
type: suggestion.type || undefined,
@@ -311,7 +318,7 @@ class Suggestions {
311318
}
312319

313320
_getItems() {
314-
return [].slice.call(this.responsivePopover.querySelectorAll("ui5-li, ui5-li-groupheader"));
321+
return [].slice.call(this.responsivePopover.querySelectorAll("ui5-li-groupheader, ui5-li-suggestion-item"));
315322
}
316323

317324
_getComponent() {
@@ -349,6 +356,44 @@ class Suggestions {
349356

350357
return `${itemPositionText} ${this.accInfo.itemText} ${itemSelectionText}`;
351358
}
359+
360+
getRowText(suggestion) {
361+
return this.sanitizeText(suggestion.text || suggestion.textContent);
362+
}
363+
364+
getRowDesc(suggestion) {
365+
if (suggestion.description) {
366+
return this.sanitizeText(suggestion.description);
367+
}
368+
}
369+
370+
getHighlightedText(suggestion, input) {
371+
let text = suggestion.text || suggestion.textContent;
372+
text = this.sanitizeText(text);
373+
374+
return this.hightlightInput(text, input);
375+
}
376+
377+
getHighlightedDesc(suggestion, input) {
378+
let text = suggestion.description;
379+
text = this.sanitizeText(text);
380+
381+
return this.hightlightInput(text, input);
382+
}
383+
384+
hightlightInput(text, input) {
385+
if (!text) {
386+
return text;
387+
}
388+
389+
const inputEscaped = input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
390+
const regEx = new RegExp(inputEscaped, "ig");
391+
return text.replace(regEx, match => `<b>${match}</b>`);
392+
}
393+
394+
sanitizeText(text) {
395+
return text && text.replace("<", "&lt");
396+
}
352397
}
353398

354399
Suggestions.SCROLL_STEP = 60;

packages/main/src/themes/ListItemBase.css

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
height: var(--_ui5_list_item_base_height);
77
background: var(--ui5-listitem-background-color);
88
box-sizing: border-box;
9+
border-bottom: 1px solid transparent;
910
}
1011

1112
/* selected */

packages/main/test/pages/Input.html

+26-10
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ <h3>Input in Cozy</h3>
2626
<ui5-input id="myInput"
2727
style="width: 500px"
2828
show-suggestions
29-
placeholder="Search for a country ...">
29+
placeholder="Search for a country ..."
30+
highlight>
3031
</ui5-input>
3132
</div>
3233

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

108+
<h3>Input suggestions with highlighing</h3>
109+
<ui5-input id="myInputHighlighted" highlight show-suggestions style="width: 100%">
110+
<ui5-suggestion-item text="Adam D" description="Administrative Support"></ui5-suggestion-item>
111+
<ui5-suggestion-item text="Aanya Sing" description="Administrative Support"></ui5-suggestion-item>
112+
<ui5-suggestion-item text="Allen K" description="Technical Support"></ui5-suggestion-item>
113+
<ui5-suggestion-item text="Alex" description="Technical Support"></ui5-suggestion-item>
114+
</ui5-input>
115+
107116
<h3> Input disabled</h3>
108117
<ui5-input style="width: auto" id="input-disabled" disabled placeholder="Disabled one ...">
109118
<ui5-icon slot="icon" name="appointment-2"></ui5-icon>
@@ -305,6 +314,13 @@ <h3>Test ariaLabel and ariaLabelledBy</h3>
305314
<ui5-label id="enterNameLabel">Enter name: </ui5-label>
306315
<ui5-input aria-labelledby="enterNameLabel"></ui5-input>
307316

317+
<h3>Input suggestions with highlighing and XSS test</h3>
318+
<ui5-input highlight show-suggestions style="width: 100%">
319+
<ui5-suggestion-item text="<script>alert('XSS')</script>" description="Administrative Support"></ui5-suggestion-item>
320+
<ui5-suggestion-item text="Aanya Sing" description="<b onmouseover=alert('XSS')></b>">
321+
</ui5-suggestion-item>
322+
</ui5-input>
323+
308324
<script>
309325
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" }];
310326

@@ -340,15 +356,15 @@ <h3>Test ariaLabel and ariaLabelledBy</h3>
340356
}
341357

342358
suggestionItems.forEach(function(item, idx) {
343-
var li = document.createElement("ui5-suggestion-item");
344-
li.id = item.key;
345-
li.icon = "world";
346-
li.info = "explore";
347-
li.group = item.text.length === 1;
348-
li.infoState = "Success";
349-
li.description = "travel the world";
350-
li.text = item.text;
351-
input.appendChild(li);
359+
var suggestion = document.createElement("ui5-suggestion-item");
360+
suggestion.id = item.key;
361+
suggestion.icon = "world";
362+
suggestion.info = "explore";
363+
suggestion.group = item.text.length === 1;
364+
suggestion.infoState = "Success";
365+
suggestion.description = "travel the world";
366+
suggestion.text = item.text
367+
input.appendChild(suggestion);
352368
});
353369

354370
labelLiveChange.innerHTML = "Event [input] :: " + value;

0 commit comments

Comments
 (0)