@salesforcedevs/dx-components 0.25.1 → 0.25.2-alpha.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "0.25.1",
3
+ "version": "0.25.2-alpha.30",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -28,6 +28,5 @@
28
28
  "@types/classnames": "^2.2.10",
29
29
  "@types/debounce": "^1.2.0",
30
30
  "@types/lodash.get": "^4.4.6"
31
- },
32
- "gitHead": "19fa005af70d80558071eaacf3b0366d99004754"
31
+ }
33
32
  }
package/src/.DS_Store ADDED
Binary file
Binary file
Binary file
@@ -1,7 +1,7 @@
1
1
  import { html } from "lit-html";
2
2
  import mockProps from "./__tests__/mockProps";
3
3
  import featureListMockProps from "../featuresList/__tests__/mockProps";
4
- import { renderStyle } from "../../utils/storybook/storybook";
4
+ import { renderStyle } from "../../utils-internal/storybook/storybook";
5
5
 
6
6
  export default {
7
7
  title: "dx/dx-group-text",
@@ -1,5 +1,8 @@
1
1
  import { html } from "lit-html";
2
- import { coveoXMLDocsConfig, coveoSFDocsConfig } from "../../utils/coveo/coveo";
2
+ import {
3
+ coveoXMLDocsConfig,
4
+ coveoSFDocsConfig
5
+ } from "../../utils-internal/coveo/coveo";
3
6
  import { SAMPLE_TREES, SAMPLE_TREES_WITH_METHODS } from "./__tests__/mockProps";
4
7
 
5
8
  export default {
@@ -78,10 +78,18 @@ export default class Sidebar extends LightningElement {
78
78
  this.expanded = !this.matchMedia.matches;
79
79
  this.onMediaChange(this.matchMedia);
80
80
  this.matchMedia.addEventListener("change", this.onMediaChange);
81
+ window.addEventListener(
82
+ "referencecontentloaded",
83
+ this.dispatchHighlightedTermChange
84
+ );
81
85
  }
82
86
 
83
87
  disconnectedCallback() {
84
88
  this.matchMedia.removeEventListener("change", this.onMediaChange);
89
+ window.removeEventListener(
90
+ "referencecontentloaded",
91
+ this.dispatchHighlightedTermChange
92
+ );
85
93
  }
86
94
 
87
95
  private onMediaChange = (event: MediaQueryListEvent | MediaQueryList) =>
@@ -120,16 +128,23 @@ export default class Sidebar extends LightningElement {
120
128
  window.clearTimeout(this._searchTimeout);
121
129
  this._searchTimeout = window.setTimeout(() => {
122
130
  this.assignFilteredTrees();
123
- this.dispatchEvent(
124
- new CustomEvent("highlightedtermchange", {
125
- detail: this.searchText,
126
- composed: true,
127
- bubbles: true
128
- })
129
- );
131
+ this.dispatchHighlightedTermChange();
130
132
  }, WAIT_TIME);
131
133
  }
132
134
 
135
+ private dispatchHighlightedTermChange = () => {
136
+ this.dispatchEvent(
137
+ new CustomEvent("highlightedtermchange", {
138
+ detail: {
139
+ query: this.searchText,
140
+ termsToHighlight: [this.searchText]
141
+ },
142
+ composed: true,
143
+ bubbles: true
144
+ })
145
+ );
146
+ };
147
+
133
148
  private assignFilteredTrees() {
134
149
  this.filteredTrees = this._trees
135
150
  .map((singleTree) => {
@@ -1,9 +1,5 @@
1
- import { createElement } from "lwc";
2
1
  import SidebarSearch from "dx/sidebarSearch";
3
- import {
4
- createRenderComponent,
5
- renderLightningElement
6
- } from "utils-internal/tests";
2
+ import { createRenderComponent } from "utils-internal/tests";
7
3
  import { coveoXMLDocsConfig } from "utils-internal/coveo";
8
4
  import { mockQueryParameter, unmockQueryParameter } from "utils-internal/jest";
9
5
  import { SESSION_KEY } from "dx/sidebarSearch";
@@ -72,62 +68,6 @@ describe(TAG, () => {
72
68
  return expect(element).toBeAccessible();
73
69
  });
74
70
 
75
- describe("highlighting", () => {
76
- afterEach(unmockQueryParameter);
77
-
78
- it("triggers on initial render when a search value exists", () => {
79
- mockQueryParameter(QUERY);
80
- const element = createElement(TAG, { is: SidebarSearch });
81
- const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
82
- renderLightningElement(element, PROPS);
83
- expect(dispatchEventSpy).toHaveBeenCalledWith(
84
- expect.objectContaining({
85
- type: "highlightedtermchange",
86
- detail: QUERY
87
- })
88
- );
89
- });
90
-
91
- it("does not trigger on initial render when a search value does not exist", () => {
92
- const element = createElement(TAG, { is: SidebarSearch });
93
- const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
94
- renderLightningElement(element, PROPS);
95
- expect(dispatchEventSpy).not.toHaveBeenCalledWith(
96
- expect.objectContaining({ type: "highlightedtermchange" })
97
- );
98
- });
99
-
100
- it("triggers on input change with non-empty value", () => {
101
- const element = render(PROPS);
102
- const dxInput = element.shadowRoot.querySelector("dx-input");
103
- const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
104
-
105
- dxInput.dispatchEvent(new CustomEvent("change", { detail: QUERY }));
106
-
107
- expect(dispatchEventSpy).toHaveBeenCalledWith(
108
- expect.objectContaining({
109
- type: "highlightedtermchange",
110
- detail: QUERY
111
- })
112
- );
113
- });
114
-
115
- it("triggers on input change with empty value", () => {
116
- const element = render(PROPS);
117
- const dxInput = element.shadowRoot.querySelector("dx-input");
118
- const dispatchEventSpy = jest.spyOn(element, "dispatchEvent");
119
-
120
- dxInput.dispatchEvent(new CustomEvent("change", { detail: "" }));
121
-
122
- expect(dispatchEventSpy).toHaveBeenCalledWith(
123
- expect.objectContaining({
124
- type: "highlightedtermchange",
125
- detail: ""
126
- })
127
- );
128
- });
129
- });
130
-
131
71
  it("correctly consumes a valid session storage object", () => {
132
72
  mockQueryParameter(mockPreloadedState.state.query.q);
133
73
 
@@ -94,6 +94,30 @@ export default class SidebarSearch extends LightningElement {
94
94
  private fetchingMoreResults: boolean = false;
95
95
  private didRender = false;
96
96
 
97
+ private get termsToHighlight(): string[] | null {
98
+ const { termsToHighlight, phrasesToHighlight } =
99
+ this.engine?.state?.search?.response;
100
+ if (!termsToHighlight || !phrasesToHighlight) {
101
+ return null;
102
+ }
103
+
104
+ const flattenTerms = (terms: Object): string[] =>
105
+ Object.entries(terms).reduce((acc, [key, value]) => {
106
+ if (Array.isArray(value)) {
107
+ return [...acc, key, ...value];
108
+ }
109
+ const values = flattenTerms(value);
110
+ return [...acc, key, ...values];
111
+ }, []);
112
+
113
+ const terms = flattenTerms({
114
+ ...termsToHighlight,
115
+ ...phrasesToHighlight
116
+ });
117
+
118
+ return Array.from(new Set([this.value, ...terms].flat(2))) as string[];
119
+ }
120
+
97
121
  private get hasCorrectCoveoConfig(): boolean {
98
122
  const coveoConfigurations = [
99
123
  this.coveoOrganizationId,
@@ -147,12 +171,6 @@ export default class SidebarSearch extends LightningElement {
147
171
  );
148
172
  }
149
173
 
150
- // If this is the first render and there is a search value, trigger
151
- // term highlighting
152
- if (!this.didRender && this.value) {
153
- this.dispatchHighlightedTermChange();
154
- }
155
-
156
174
  this.didRender = true;
157
175
  }
158
176
 
@@ -227,10 +245,6 @@ export default class SidebarSearch extends LightningElement {
227
245
  this.onResultListChange
228
246
  );
229
247
 
230
- if (this.value) {
231
- this.dispatchHighlightedTermChange();
232
- }
233
-
234
248
  if (!this.preloadedState) {
235
249
  if (this.value) {
236
250
  this.submitSearch();
@@ -333,6 +347,7 @@ export default class SidebarSearch extends LightningElement {
333
347
 
334
348
  private dispatchOnChangeEvent(results: Result[]) {
335
349
  this.updateSessionStorage();
350
+ this.dispatchHighlightedTermChange();
336
351
  this.dispatchEvent(
337
352
  new CustomEvent("change", {
338
353
  detail: {
@@ -347,15 +362,20 @@ export default class SidebarSearch extends LightningElement {
347
362
  this.dispatchEvent(new CustomEvent("loading", { detail: loading }));
348
363
  }
349
364
 
350
- private dispatchHighlightedTermChange() {
351
- this.dispatchEvent(
352
- new CustomEvent("highlightedtermchange", {
353
- detail: this.value,
354
- composed: true,
355
- bubbles: true
356
- })
357
- );
358
- }
365
+ private dispatchHighlightedTermChange = () => {
366
+ if (this.termsToHighlight) {
367
+ this.dispatchEvent(
368
+ new CustomEvent("highlightedtermchange", {
369
+ detail: {
370
+ query: this.value,
371
+ termsToHighlight: this.termsToHighlight
372
+ },
373
+ composed: true,
374
+ bubbles: true
375
+ })
376
+ );
377
+ }
378
+ };
359
379
 
360
380
  private updateRecentSearches = () =>
361
381
  (this.recentSearches = UserRecentSearches.get().map((item: string) => ({
@@ -410,7 +430,6 @@ export default class SidebarSearch extends LightningElement {
410
430
  this.value = e.detail;
411
431
 
412
432
  this.handleValueChange(false);
413
- this.dispatchHighlightedTermChange();
414
433
  }
415
434
 
416
435
  private onInputFocus() {
@@ -17,6 +17,12 @@ export default {
17
17
  "Why build on Quip? This is an extra long blurb to test the toc wrapping ability",
18
18
  id: "quip",
19
19
  anchor: "#test3"
20
+ },
21
+ {
22
+ label:
23
+ "SomeLongExampleWordWithoutAnyBreaks and Then a Word",
24
+ id: "long-word",
25
+ anchor: "#test4"
20
26
  }
21
27
  ]
22
28
  };
@@ -9,6 +9,7 @@
9
9
  display: flex;
10
10
  flex-direction: row-reverse;
11
11
  width: var(--dx-c-toc-width);
12
+ word-break: break-word;
12
13
  }
13
14
 
14
15
  .toc_collapsed {
@@ -10,99 +10,101 @@ describe("highlight utility", () => {
10
10
 
11
11
  it("highlights text that matches search criteria", () => {
12
12
  container.innerHTML = mockContent;
13
- highlightTerms(
14
- container.querySelectorAll(allowedTags.join(",")),
15
- "apex"
16
- );
13
+ highlightTerms(container.querySelectorAll(allowedTags.join(",")), {
14
+ query: "apex",
15
+ termsToHighlight: ["apex"]
16
+ });
17
17
  expect(container.querySelectorAll("mark")).toHaveLength(14);
18
18
  });
19
19
 
20
20
  it("doesn't highlight elements that contains dx/docs components", () => {
21
- const searchTerm = "customreport";
21
+ const query = "customreport";
22
22
  const codeBlock = createElement("dx-code-block", { is: CodeBlock });
23
23
  Object.assign(codeBlock, {
24
- codeBlock: searchTerm,
24
+ codeBlock: query,
25
25
  language: "text"
26
26
  });
27
27
  container.appendChild(document.createElement("p"));
28
28
  container.firstElementChild?.appendChild(codeBlock);
29
- highlightTerms(
30
- container.querySelectorAll(allowedTags.join(",")),
31
- searchTerm
32
- );
29
+ highlightTerms(container.querySelectorAll(allowedTags.join(",")), {
30
+ query,
31
+ termsToHighlight: [query]
32
+ });
33
33
  expect(container.querySelectorAll("mark")).toHaveLength(0);
34
- expect(codeBlock.shadowRoot.innerHTML).toMatch(searchTerm); // sanity check
34
+ expect(codeBlock.shadowRoot.innerHTML).toMatch(query); // sanity check
35
35
  });
36
36
 
37
37
  it("doesn't highlight terms separated by html tags", () => {
38
+ const query = "loadDatawithin";
38
39
  container.innerHTML = "<p>loadData<span>within</span></p>";
39
- highlightTerms(
40
- container.querySelectorAll(allowedTags.join(",")),
41
- "loadDatawithin"
42
- );
40
+ highlightTerms(container.querySelectorAll(allowedTags.join(",")), {
41
+ query,
42
+ termsToHighlight: [query]
43
+ });
43
44
  expect(container.querySelectorAll("mark")).toHaveLength(0);
44
45
  });
45
46
 
46
47
  it("escapes regex especial characters", () => {
48
+ const query = "apex.+";
47
49
  container.innerHTML = mockContent;
48
- highlightTerms(
49
- container.querySelectorAll(allowedTags.join(",")),
50
- "apex.+"
51
- );
50
+ highlightTerms(container.querySelectorAll(allowedTags.join(",")), {
51
+ query,
52
+ termsToHighlight: [query]
53
+ });
52
54
  expect(container.querySelectorAll("mark")).toHaveLength(0);
53
55
  });
54
56
 
55
57
  it("cleans previous search result before searching for a new term", () => {
56
- const firstSearch = "apex";
57
- const secondSearch = "salesforce";
58
+ const firstQuery = "apex";
59
+ const secondQuery = "salesforce";
58
60
 
59
61
  container.innerHTML = mockContent;
60
62
 
61
- highlightTerms(
62
- container.querySelectorAll(allowedTags.join(",")),
63
- firstSearch
64
- );
63
+ highlightTerms(container.querySelectorAll(allowedTags.join(",")), {
64
+ query: firstQuery,
65
+ termsToHighlight: [firstQuery]
66
+ });
65
67
  const firstMarks = container.querySelectorAll("mark");
66
68
  expect(firstMarks).toHaveLength(14);
67
69
  firstMarks.forEach(({ textContent }) =>
68
- expect(textContent?.toLowerCase()).toBe(firstSearch)
70
+ expect(textContent?.toLowerCase()).toBe(firstQuery)
69
71
  );
70
72
 
71
- highlightTerms(
72
- container.querySelectorAll(allowedTags.join(",")),
73
- secondSearch
74
- );
73
+ highlightTerms(container.querySelectorAll(allowedTags.join(",")), {
74
+ query: secondQuery,
75
+ termsToHighlight: [secondQuery]
76
+ });
75
77
  const secondMarks = container.querySelectorAll("mark");
76
78
  expect(secondMarks).toHaveLength(4);
77
79
  secondMarks.forEach(({ textContent }) =>
78
- expect(textContent?.toLowerCase()).toBe(secondSearch)
80
+ expect(textContent?.toLowerCase()).toBe(secondQuery)
79
81
  );
80
82
  });
81
83
 
82
84
  it("cleans all marks if the search term is empty string", () => {
83
- const firstSearch = "apex";
84
- const secondSearch = "";
85
+ const firstQuery = "apex";
86
+ const secondQuery = "";
85
87
 
86
88
  container.innerHTML = mockContent;
87
89
 
88
- highlightTerms(
89
- container.querySelectorAll(allowedTags.join(",")),
90
- firstSearch
91
- );
90
+ highlightTerms(container.querySelectorAll(allowedTags.join(",")), {
91
+ query: firstQuery,
92
+ termsToHighlight: [firstQuery]
93
+ });
92
94
  const firstMarks = container.querySelectorAll("mark");
93
95
  expect(firstMarks).toHaveLength(14);
94
96
  firstMarks.forEach(({ textContent }) =>
95
- expect(textContent?.toLowerCase()).toBe(firstSearch)
97
+ expect(textContent?.toLowerCase()).toBe(firstQuery)
96
98
  );
97
99
 
98
- highlightTerms(
99
- container.querySelectorAll(allowedTags.join(",")),
100
- secondSearch
101
- );
100
+ highlightTerms(container.querySelectorAll(allowedTags.join(",")), {
101
+ query: secondQuery,
102
+ termsToHighlight: [secondQuery]
103
+ });
102
104
  const secondMarks = container.querySelectorAll("mark");
103
105
  expect(secondMarks).toHaveLength(0);
104
106
  secondMarks.forEach(({ textContent }) =>
105
- expect(textContent?.toLowerCase()).toBe(secondSearch)
107
+ expect(textContent?.toLowerCase()).toBe(secondQuery)
106
108
  );
107
109
  });
108
110
  });
@@ -1,14 +1,16 @@
1
1
  import { createSearchRegExp } from "utils-internal/regexps";
2
2
 
3
- const MARK_TAGS = /<\/?mark>/gi;
3
+ const MARK_TAGS = /<\/?mark[^>]*>/gi;
4
+
5
+ // mark tags dif color on ref page
4
6
 
5
7
  export const highlightTerms = (
6
8
  // eslint-disable-next-line no-undef
7
- elements: NodeListOf<Element>,
8
- searchTerm: string
9
+ elements: NodeListOf<HTMLElement>,
10
+ { query, termsToHighlight }: { query: string; termsToHighlight: string[] }
9
11
  ): void => {
10
- searchTerm = searchTerm.trim();
11
- const searchExp = createSearchRegExp(searchTerm);
12
+ const searchExp = createSearchRegExp(termsToHighlight);
13
+ let scrolled = false;
12
14
  elements.forEach((element) => {
13
15
  if (
14
16
  element?.innerHTML.includes("<dx-") ||
@@ -17,16 +19,27 @@ export const highlightTerms = (
17
19
  return;
18
20
  }
19
21
 
20
- let innerHtml = element.innerHTML.replace(
21
- // Remove previously added mark tags
22
- MARK_TAGS,
23
- ""
24
- );
25
-
26
- if (searchTerm) {
27
- innerHtml = innerHtml.replace(searchExp, "<mark>$&</mark>");
22
+ try {
23
+ if (element.innerHTML.match(MARK_TAGS)) {
24
+ // Remove previously added mark tags
25
+ element.innerHTML = element.innerHTML.replace(MARK_TAGS, "");
26
+ }
27
+ if (element.innerHTML.match(searchExp) && query) {
28
+ element.innerHTML = element.innerHTML.replace(
29
+ searchExp,
30
+ '<mark style="background-color:var(--dx-g-yellow-vibrant-90)">$&</mark>'
31
+ );
32
+ }
33
+ } catch (e) {
34
+ console.error(
35
+ `${element} could not be parsed by the highlight utility: ${e}`
36
+ );
28
37
  }
29
38
 
30
- element.innerHTML = innerHtml;
39
+ const mark = element.querySelector("mark");
40
+ if (mark && !scrolled) {
41
+ scrolled = true;
42
+ mark.scrollIntoView({ block: "nearest" });
43
+ }
31
44
  });
32
45
  };
@@ -1,5 +1,12 @@
1
1
  const escapeRegExp = (text: string) =>
2
2
  text.replace(/[-[\]{}()*+?.,\\^$|]/g, "\\$&");
3
3
 
4
- export const createSearchRegExp = (searchTerm: string): RegExp =>
5
- new RegExp(`(?:${escapeRegExp(searchTerm)})(?![^<>]*>)`, "gi");
4
+ export const createSearchRegExp = (searchTerms: string[]): RegExp => {
5
+ const escaped = searchTerms.map((term) => escapeRegExp(term.trim()));
6
+ return new RegExp(
7
+ `(?:\\b|[^A-Za-z0-9]\\K)(?:${escaped.join(
8
+ "|"
9
+ )})(?![^<>]*>)(?=\\b|[^A-Za-z0-9])`,
10
+ "gi"
11
+ );
12
+ };
package/LICENSE DELETED
@@ -1,12 +0,0 @@
1
- Copyright (c) 2020, Salesforce.com, Inc.
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
-
6
- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
-
8
- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
-
10
- * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
-
12
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.