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(atomic): add atomic-result-html component #2014

Merged
merged 8 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ AtomicFormatCurrency,
AtomicFormatNumber,
AtomicFormatUnit,
AtomicFrequentlyBoughtTogether,
AtomicHtml,
AtomicIcon,
AtomicLayoutSection,
AtomicLoadMoreChildrenResults,
Expand All @@ -41,6 +42,7 @@ AtomicResultChildren,
AtomicResultChildrenTemplate,
AtomicResultDate,
AtomicResultFieldsList,
AtomicResultHtml,
AtomicResultIcon,
AtomicResultImage,
AtomicResultLink,
Expand Down Expand Up @@ -98,6 +100,7 @@ AtomicFormatCurrency,
AtomicFormatNumber,
AtomicFormatUnit,
AtomicFrequentlyBoughtTogether,
AtomicHtml,
AtomicIcon,
AtomicLayoutSection,
AtomicLoadMoreChildrenResults,
Expand All @@ -119,6 +122,7 @@ AtomicResultChildren,
AtomicResultChildrenTemplate,
AtomicResultDate,
AtomicResultFieldsList,
AtomicResultHtml,
AtomicResultIcon,
AtomicResultImage,
AtomicResultLink,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,27 @@ export class AtomicFrequentlyBoughtTogether {
}


export declare interface AtomicHtml extends Components.AtomicHtml {}

@ProxyCmp({
defineCustomElementFn: undefined,
inputs: ['sanitize', 'value']
})
@Component({
selector: 'atomic-html',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['sanitize', 'value']
})
export class AtomicHtml {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}


export declare interface AtomicIcon extends Components.AtomicIcon {}

@ProxyCmp({
Expand Down Expand Up @@ -757,6 +778,27 @@ export class AtomicResultFieldsList {
}


export declare interface AtomicResultHtml extends Components.AtomicResultHtml {}

@ProxyCmp({
defineCustomElementFn: undefined,
inputs: ['field', 'sanitize']
})
@Component({
selector: 'atomic-result-html',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
inputs: ['field', 'sanitize']
})
export class AtomicResultHtml {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}


export declare interface AtomicResultIcon extends Components.AtomicResultIcon {}

@ProxyCmp({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const AtomicFormatCurrency = /*@__PURE__*/createReactComponent<JSX.Atomic
export const AtomicFormatNumber = /*@__PURE__*/createReactComponent<JSX.AtomicFormatNumber, HTMLAtomicFormatNumberElement>('atomic-format-number');
export const AtomicFormatUnit = /*@__PURE__*/createReactComponent<JSX.AtomicFormatUnit, HTMLAtomicFormatUnitElement>('atomic-format-unit');
export const AtomicFrequentlyBoughtTogether = /*@__PURE__*/createReactComponent<JSX.AtomicFrequentlyBoughtTogether, HTMLAtomicFrequentlyBoughtTogetherElement>('atomic-frequently-bought-together');
export const AtomicHtml = /*@__PURE__*/createReactComponent<JSX.AtomicHtml, HTMLAtomicHtmlElement>('atomic-html');
export const AtomicIcon = /*@__PURE__*/createReactComponent<JSX.AtomicIcon, HTMLAtomicIconElement>('atomic-icon');
export const AtomicLayoutSection = /*@__PURE__*/createReactComponent<JSX.AtomicLayoutSection, HTMLAtomicLayoutSectionElement>('atomic-layout-section');
export const AtomicLoadMoreChildrenResults = /*@__PURE__*/createReactComponent<JSX.AtomicLoadMoreChildrenResults, HTMLAtomicLoadMoreChildrenResultsElement>('atomic-load-more-children-results');
Expand All @@ -43,6 +44,7 @@ export const AtomicResultChildren = /*@__PURE__*/createReactComponent<JSX.Atomic
export const AtomicResultChildrenTemplate = /*@__PURE__*/createReactComponent<JSX.AtomicResultChildrenTemplate, HTMLAtomicResultChildrenTemplateElement>('atomic-result-children-template');
export const AtomicResultDate = /*@__PURE__*/createReactComponent<JSX.AtomicResultDate, HTMLAtomicResultDateElement>('atomic-result-date');
export const AtomicResultFieldsList = /*@__PURE__*/createReactComponent<JSX.AtomicResultFieldsList, HTMLAtomicResultFieldsListElement>('atomic-result-fields-list');
export const AtomicResultHtml = /*@__PURE__*/createReactComponent<JSX.AtomicResultHtml, HTMLAtomicResultHtmlElement>('atomic-result-html');
export const AtomicResultIcon = /*@__PURE__*/createReactComponent<JSX.AtomicResultIcon, HTMLAtomicResultIconElement>('atomic-result-icon');
export const AtomicResultImage = /*@__PURE__*/createReactComponent<JSX.AtomicResultImage, HTMLAtomicResultImageElement>('atomic-result-image');
export const AtomicResultLink = /*@__PURE__*/createReactComponent<JSX.AtomicResultLink, HTMLAtomicResultLinkElement>('atomic-result-link');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {ResultListSelectors} from '../result-list-selectors';

export const resultHtmlComponent = 'atomic-result-html';

export const ResultHtmlSelectors = {
shadow: () => cy.get(resultHtmlComponent),
firstInResult: () =>
ResultListSelectors.firstResult().find(resultHtmlComponent),
atomicHTML: () => ResultHtmlSelectors.firstInResult().find('atomic-html'),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {
generateComponentHTML,
TagProps,
TestFixture,
} from '../../../fixtures/test-fixture';
import * as CommonAssertions from '../../common-assertions';
import {addResultList, buildTemplateWithSections} from '../result-list-actions';
import {
resultHtmlComponent,
ResultHtmlSelectors,
} from './result-html-selectors';

interface ResultHtmlProps {
field?: string;
sanitize?: boolean;
}

const addResultHTMLInResultList = (props: ResultHtmlProps = {}) =>
addResultList(
buildTemplateWithSections({
bottomMetadata: generateComponentHTML(
resultHtmlComponent,
props as TagProps
),
})
);

describe('Result Html Component', () => {
describe('when not used inside a result template', () => {
beforeEach(() => {
new TestFixture()
.withElement(generateComponentHTML(resultHtmlComponent))
.init();
});

CommonAssertions.assertRemovesComponent(ResultHtmlSelectors.shadow);
CommonAssertions.assertConsoleError();
});

describe('when the field does not exist for the result', () => {
beforeEach(() => {
new TestFixture()
.with(addResultHTMLInResultList({field: 'thisfielddoesnotexist'}))
.init();
});

CommonAssertions.assertRemovesComponent(ResultHtmlSelectors.firstInResult);
});

describe('when the field value is not a string', () => {
const field = 'hello';
beforeEach(() => {
new TestFixture()
.with(addResultHTMLInResultList({field}))
.withCustomResponse((response) =>
response.results.forEach((result) => (result.raw[field] = 1337))
)
.init();
});

CommonAssertions.assertRemovesComponent(ResultHtmlSelectors.firstInResult);
CommonAssertions.assertConsoleError(false);
});

describe('when the field value exists & is an HTML string', () => {
const field = 'hello_world';
const rawValue = '<img src="google.com" onerror="console.log()" />';
const sanitizedValue = '<img src="google.com" />';
function setupExistingFieldValue(sanitize: boolean) {
new TestFixture()
.with(
addResultHTMLInResultList({
field: field,
sanitize,
})
)
.withCustomResponse((response) =>
response.results.forEach((result) => {
result.raw[field] = rawValue;
})
)
.init();
}

describe('when the "sanitize" prop is true', () => {
beforeEach(() => {
setupExistingFieldValue(true);
});

it('should render the HTML sanitized', () => {
ResultHtmlSelectors.atomicHTML()
.shadow()
.find('img')
.should('not.have.attr', 'onerror');
});

CommonAssertions.assertAccessibility(ResultHtmlSelectors.firstInResult);
});

describe('when the "sanitize" prop is false', () => {
beforeEach(() => {
setupExistingFieldValue(false);
});

it('should render the HTML as is', () => {
ResultHtmlSelectors.atomicHTML()
.shadow()
.find('img')
.should('have.attr', 'onerror');
});
});
});
});
58 changes: 58 additions & 0 deletions packages/atomic/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,16 @@ export namespace Components {
}
interface AtomicFrequentlyBoughtTogether {
}
interface AtomicHtml {
/**
* Specify if the content should be sanitized, using `DOMPurify` (https://www.npmjs.com/package/dompurify).
*/
"sanitize": boolean;
/**
* The string value containg HTML to display;
*/
"value": string;
}
interface AtomicIcon {
/**
* The SVG icon to display. - Use a value that starts with `http://`, `https://`, `./`, or `../`, to fetch and display an icon from a given location. - Use a value that starts with `assets://`, to display an icon from the Atomic package. - Use a stringified SVG to display it directly.
Expand Down Expand Up @@ -611,6 +621,16 @@ export namespace Components {
}
interface AtomicResultFieldsList {
}
interface AtomicResultHtml {
/**
* The result field which the component should use. This will look in the Result object first, and then in the Result.raw object for the fields. It is important to include the necessary field in the ResultList component.
*/
"field": string;
/**
* Specify if the content should be sanitized, using `DOMPurify` (https://www.npmjs.com/package/dompurify).
*/
"sanitize": boolean;
}
interface AtomicResultIcon {
}
interface AtomicResultImage {
Expand Down Expand Up @@ -1127,6 +1147,12 @@ declare global {
prototype: HTMLAtomicFrequentlyBoughtTogetherElement;
new (): HTMLAtomicFrequentlyBoughtTogetherElement;
};
interface HTMLAtomicHtmlElement extends Components.AtomicHtml, HTMLStencilElement {
}
var HTMLAtomicHtmlElement: {
prototype: HTMLAtomicHtmlElement;
new (): HTMLAtomicHtmlElement;
};
interface HTMLAtomicIconElement extends Components.AtomicIcon, HTMLStencilElement {
}
var HTMLAtomicIconElement: {
Expand Down Expand Up @@ -1259,6 +1285,12 @@ declare global {
prototype: HTMLAtomicResultFieldsListElement;
new (): HTMLAtomicResultFieldsListElement;
};
interface HTMLAtomicResultHtmlElement extends Components.AtomicResultHtml, HTMLStencilElement {
}
var HTMLAtomicResultHtmlElement: {
prototype: HTMLAtomicResultHtmlElement;
new (): HTMLAtomicResultHtmlElement;
};
interface HTMLAtomicResultIconElement extends Components.AtomicResultIcon, HTMLStencilElement {
}
var HTMLAtomicResultIconElement: {
Expand Down Expand Up @@ -1512,6 +1544,7 @@ declare global {
"atomic-format-number": HTMLAtomicFormatNumberElement;
"atomic-format-unit": HTMLAtomicFormatUnitElement;
"atomic-frequently-bought-together": HTMLAtomicFrequentlyBoughtTogetherElement;
"atomic-html": HTMLAtomicHtmlElement;
"atomic-icon": HTMLAtomicIconElement;
"atomic-layout-section": HTMLAtomicLayoutSectionElement;
"atomic-load-more-children-results": HTMLAtomicLoadMoreChildrenResultsElement;
Expand All @@ -1534,6 +1567,7 @@ declare global {
"atomic-result-children-template": HTMLAtomicResultChildrenTemplateElement;
"atomic-result-date": HTMLAtomicResultDateElement;
"atomic-result-fields-list": HTMLAtomicResultFieldsListElement;
"atomic-result-html": HTMLAtomicResultHtmlElement;
"atomic-result-icon": HTMLAtomicResultIconElement;
"atomic-result-image": HTMLAtomicResultImageElement;
"atomic-result-link": HTMLAtomicResultLinkElement;
Expand Down Expand Up @@ -1845,6 +1879,16 @@ declare namespace LocalJSX {
}
interface AtomicFrequentlyBoughtTogether {
}
interface AtomicHtml {
/**
* Specify if the content should be sanitized, using `DOMPurify` (https://www.npmjs.com/package/dompurify).
*/
"sanitize"?: boolean;
/**
* The string value containg HTML to display;
*/
"value": string;
}
interface AtomicIcon {
/**
* The SVG icon to display. - Use a value that starts with `http://`, `https://`, `./`, or `../`, to fetch and display an icon from a given location. - Use a value that starts with `assets://`, to display an icon from the Atomic package. - Use a stringified SVG to display it directly.
Expand Down Expand Up @@ -2165,6 +2209,16 @@ declare namespace LocalJSX {
}
interface AtomicResultFieldsList {
}
interface AtomicResultHtml {
/**
* The result field which the component should use. This will look in the Result object first, and then in the Result.raw object for the fields. It is important to include the necessary field in the ResultList component.
*/
"field": string;
/**
* Specify if the content should be sanitized, using `DOMPurify` (https://www.npmjs.com/package/dompurify).
*/
"sanitize"?: boolean;
}
interface AtomicResultIcon {
}
interface AtomicResultImage {
Expand Down Expand Up @@ -2576,6 +2630,7 @@ declare namespace LocalJSX {
"atomic-format-number": AtomicFormatNumber;
"atomic-format-unit": AtomicFormatUnit;
"atomic-frequently-bought-together": AtomicFrequentlyBoughtTogether;
"atomic-html": AtomicHtml;
"atomic-icon": AtomicIcon;
"atomic-layout-section": AtomicLayoutSection;
"atomic-load-more-children-results": AtomicLoadMoreChildrenResults;
Expand All @@ -2598,6 +2653,7 @@ declare namespace LocalJSX {
"atomic-result-children-template": AtomicResultChildrenTemplate;
"atomic-result-date": AtomicResultDate;
"atomic-result-fields-list": AtomicResultFieldsList;
"atomic-result-html": AtomicResultHtml;
"atomic-result-icon": AtomicResultIcon;
"atomic-result-image": AtomicResultImage;
"atomic-result-link": AtomicResultLink;
Expand Down Expand Up @@ -2661,6 +2717,7 @@ declare module "@stencil/core" {
"atomic-format-number": LocalJSX.AtomicFormatNumber & JSXBase.HTMLAttributes<HTMLAtomicFormatNumberElement>;
"atomic-format-unit": LocalJSX.AtomicFormatUnit & JSXBase.HTMLAttributes<HTMLAtomicFormatUnitElement>;
"atomic-frequently-bought-together": LocalJSX.AtomicFrequentlyBoughtTogether & JSXBase.HTMLAttributes<HTMLAtomicFrequentlyBoughtTogetherElement>;
"atomic-html": LocalJSX.AtomicHtml & JSXBase.HTMLAttributes<HTMLAtomicHtmlElement>;
"atomic-icon": LocalJSX.AtomicIcon & JSXBase.HTMLAttributes<HTMLAtomicIconElement>;
"atomic-layout-section": LocalJSX.AtomicLayoutSection & JSXBase.HTMLAttributes<HTMLAtomicLayoutSectionElement>;
"atomic-load-more-children-results": LocalJSX.AtomicLoadMoreChildrenResults & JSXBase.HTMLAttributes<HTMLAtomicLoadMoreChildrenResultsElement>;
Expand All @@ -2683,6 +2740,7 @@ declare module "@stencil/core" {
"atomic-result-children-template": LocalJSX.AtomicResultChildrenTemplate & JSXBase.HTMLAttributes<HTMLAtomicResultChildrenTemplateElement>;
"atomic-result-date": LocalJSX.AtomicResultDate & JSXBase.HTMLAttributes<HTMLAtomicResultDateElement>;
"atomic-result-fields-list": LocalJSX.AtomicResultFieldsList & JSXBase.HTMLAttributes<HTMLAtomicResultFieldsListElement>;
"atomic-result-html": LocalJSX.AtomicResultHtml & JSXBase.HTMLAttributes<HTMLAtomicResultHtmlElement>;
"atomic-result-icon": LocalJSX.AtomicResultIcon & JSXBase.HTMLAttributes<HTMLAtomicResultIconElement>;
"atomic-result-image": LocalJSX.AtomicResultImage & JSXBase.HTMLAttributes<HTMLAtomicResultImageElement>;
"atomic-result-link": LocalJSX.AtomicResultLink & JSXBase.HTMLAttributes<HTMLAtomicResultLinkElement>;
Expand Down
Loading