diff --git a/.changeset/chatty-paws-invent.md b/.changeset/chatty-paws-invent.md new file mode 100644 index 0000000000..07c3a83d8e --- /dev/null +++ b/.changeset/chatty-paws-invent.md @@ -0,0 +1,6 @@ +--- +'@swisspost/design-system-documentation': patch +'@swisspost/design-system-components': patch +--- + +Aligned prop validation across the component library and replaced thrown errors with console errors. diff --git a/packages/components/cypress/e2e/back-to-top.cy.ts b/packages/components/cypress/e2e/back-to-top.cy.ts index 96e527b5b5..1e5b048920 100644 --- a/packages/components/cypress/e2e/back-to-top.cy.ts +++ b/packages/components/cypress/e2e/back-to-top.cy.ts @@ -10,17 +10,19 @@ describe('Back-to-top', () => { cy.get('post-back-to-top').should('exist'); }); - it('should throw an error if the label is missing', () => { - cy.on('uncaught:exception', err => { - expect(err.message).to.include( - 'The label property of the Back to Top component is required for accessibility purposes. Please ensure it is set.', - ); - return false; + it('should log a message if the label is removed', () => { + cy.window().then(win => { + cy.spy(win.console, 'error').as('consoleError'); }); - cy.document().then(doc => { - const element = doc.createElement('post-back-to-top'); - doc.body.appendChild(element); + + cy.get('post-back-to-top').then($el => { + $el[0].removeAttribute('label'); }); + + cy.get('@consoleError').should( + 'be.calledWith', + 'The prop `label` of the `post-back-to-top` component is not defined.', + ); }); it('should hide the label visually', () => { diff --git a/packages/components/cypress/e2e/card-control.cy.ts b/packages/components/cypress/e2e/card-control.cy.ts index bbc699fafb..732fd465ee 100644 --- a/packages/components/cypress/e2e/card-control.cy.ts +++ b/packages/components/cypress/e2e/card-control.cy.ts @@ -58,8 +58,8 @@ describe('Card-Control', () => { cy.get('@consoleError') .invoke('getCalls') .then(calls => { - expect(calls[0].args[0].message).to.eq( - 'The prop `label` of the `post-card-control` component is required.', + expect(calls[0].args[0]).to.eq( + 'The prop `label` of the `post-card-control` component is not defined.', ); }); }); @@ -92,8 +92,8 @@ describe('Card-Control', () => { cy.get('@consoleError') .invoke('getCalls') .then(calls => { - expect(calls[0].args[0].message).to.eq( - 'The prop `type` of the `post-card-control` component must be one of the following values: checkbox, radio.', + expect(calls[0].args[0]).to.eq( + 'The prop `type` of the `post-card-control` component is not defined.', ); }); }); diff --git a/packages/components/cypress/e2e/list.cy.ts b/packages/components/cypress/e2e/list.cy.ts index 6078019acb..4ec09d9f2d 100644 --- a/packages/components/cypress/e2e/list.cy.ts +++ b/packages/components/cypress/e2e/list.cy.ts @@ -22,17 +22,19 @@ describe('PostList Component', { baseUrl: null, includeShadowDom: false }, () => }); }); - it('should throw an error if the title is missing', () => { - // Check for the mandatory title accessibility error if no title is provided - cy.on('uncaught:exception', err => { - expect(err.message).to.include( - 'Please provide a title to the list component. Title is mandatory for accessibility purposes.', - ); - return false; + it('should log an error if the title is missing', () => { + cy.window().then(win => { + cy.spy(win.console, 'error').as('consoleError'); }); + cy.get('post-list').within(() => { - cy.get('[slot="post-list-item"]').first().invoke('remove'); + cy.get(':first-child').invoke('remove'); }); + + cy.get('@consoleError').should( + 'be.calledWith', + 'Please provide a title to the list component. Title is mandatory for accessibility purposes.', + ); }); it('should hide the title when title-hidden is set', () => { diff --git a/packages/components/cypress/fixtures/post-back-to-top.test.html b/packages/components/cypress/fixtures/post-back-to-top.test.html deleted file mode 100644 index 5e572ffe08..0000000000 --- a/packages/components/cypress/fixtures/post-back-to-top.test.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Document - - - - - - diff --git a/packages/components/src/components.d.ts b/packages/components/src/components.d.ts index 8b4c1d4fda..813f812145 100644 --- a/packages/components/src/components.d.ts +++ b/packages/components/src/components.d.ts @@ -85,7 +85,7 @@ export namespace Components { /** * The label to use for the close button of a dismissible banner. */ - "dismissLabel": string; + "dismissLabel"?: string; /** * If `true`, a close button (×) is displayed and the banner can be dismissed by the user. */ @@ -93,7 +93,7 @@ export namespace Components { /** * The icon to display in the banner. By default, the icon depends on the banner type. If `none`, no icon is displayed. */ - "icon": string; + "icon"?: string; /** * The type of the banner. */ @@ -126,7 +126,7 @@ export namespace Components { /** * Defines the description in the control-label. */ - "description": string; + "description"?: string; /** * Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms' data. */ @@ -138,7 +138,7 @@ export namespace Components { /** * Defines the icon `name` inside the card. If not set the icon will not show up. */ - "icon": string; + "icon"?: string; /** * Defines the text in the control-label. */ @@ -146,7 +146,7 @@ export namespace Components { /** * Defines the `name` attribute of the control. This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. */ - "name": string; + "name"?: string; /** * A public method to reset the controls `checked` and `validity` state. The validity state is set to `null`, so it's neither valid nor invalid. */ @@ -158,11 +158,11 @@ export namespace Components { /** * Defines the validation `validity` of the control. To reset validity to an undefined state, simply remove the attribute from the control. */ - "validity": null | 'true' | 'false'; + "validity"?: 'true' | 'false'; /** * Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. */ - "value": string; + "value"?: string; } interface PostClosebutton { } @@ -205,11 +205,11 @@ export namespace Components { /** * The name of the animation. */ - "animation"?: Animation | null; + "animation"?: Animation; /** * The base path, where the icons are located (must be a public url).
Leave this field empty to use the default cdn url. */ - "base"?: string | null; + "base"?: string; /** * When set to `true`, the icon will be flipped horizontally. */ @@ -225,17 +225,17 @@ export namespace Components { /** * The number of degree for the css rotate transformation. */ - "rotate"?: number | null; + "rotate"?: number; /** * The number for the css scale transformation. */ - "scale"?: number | null; + "scale"?: number; } interface PostLanguageOption { /** * If set to `true`, the language option is considered the current language for the page. */ - "active": boolean; + "active"?: boolean; /** * The ISO 639 language code, formatted according to [RFC 5646 (also known as BCP 47)](https://datatracker.ietf.org/doc/html/rfc5646). For example, "de". */ @@ -243,7 +243,7 @@ export namespace Components { /** * The full name of the language. For example, "Deutsch". */ - "name": string; + "name"?: string; /** * Selects the language option programmatically. */ @@ -251,11 +251,11 @@ export namespace Components { /** * The URL used for the href attribute of the internal anchor. This field is optional; if not provided, a button will be used internally instead of an anchor. */ - "url": string; + "url"?: string; /** * To communicate the variant prop from the parent (post-language-switch) component to the child (post-language-option) component. See parent docs for a description about the property itself. */ - "variant"?: SwitchVariant | null; + "variant"?: SwitchVariant; } interface PostLanguageSwitch { /** @@ -289,7 +289,7 @@ export namespace Components { /** * The URL to which the user is redirected upon clicking the logo. */ - "url": string | URL; + "url"?: string | URL; } interface PostMainnavigation { } @@ -444,7 +444,7 @@ export namespace Components { /** * The name of the panel that is initially shown. If not specified, it defaults to the panel associated with the first tab. **Changing this value after initialization has no effect.** */ - "activePanel": HTMLPostTabPanelElement['name']; + "activePanel"?: HTMLPostTabPanelElement['name']; /** * Shows the panel with the given name and selects its associated tab. Any other panel that was previously shown becomes hidden and its associated tab is unselected. */ @@ -454,15 +454,15 @@ export namespace Components { /** * Defines the icon `name` inside of the component. If not set the icon will not show up. To learn which icons are available, please visit our icon library. */ - "icon": null | string; + "icon": string; /** * Defines the size of the component. */ - "size": null | 'sm'; + "size"?: 'sm'; /** * Defines the color variant of the component. */ - "variant": 'white' | 'info' | 'success' | 'error' | 'warning' | 'yellow'; + "variant"?: 'white' | 'info' | 'success' | 'error' | 'warning' | 'yellow'; } interface PostTogglebutton { /** @@ -472,7 +472,7 @@ export namespace Components { } interface PostTooltip { /** - * Wheter or not to display a little pointer arrow + * Whether or not to display a little pointer arrow */ "arrow"?: boolean; /** @@ -985,7 +985,7 @@ declare namespace LocalJSX { /** * The URL for the home breadcrumb item. */ - "homeUrl"?: string; + "homeUrl": string; } interface PostBreadcrumbItem { /** @@ -1036,7 +1036,7 @@ declare namespace LocalJSX { /** * Defines the validation `validity` of the control. To reset validity to an undefined state, simply remove the attribute from the control. */ - "validity"?: null | 'true' | 'false'; + "validity"?: 'true' | 'false'; /** * Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. */ @@ -1058,7 +1058,7 @@ declare namespace LocalJSX { /** * Link the trigger to a post-collapsible with this id */ - "for"?: string; + "for": string; } interface PostFooter { /** @@ -1079,11 +1079,11 @@ declare namespace LocalJSX { /** * The name of the animation. */ - "animation"?: Animation | null; + "animation"?: Animation; /** * The base path, where the icons are located (must be a public url).
Leave this field empty to use the default cdn url. */ - "base"?: string | null; + "base"?: string; /** * When set to `true`, the icon will be flipped horizontally. */ @@ -1099,11 +1099,11 @@ declare namespace LocalJSX { /** * The number of degree for the css rotate transformation. */ - "rotate"?: number | null; + "rotate"?: number; /** * The number for the css scale transformation. */ - "scale"?: number | null; + "scale"?: number; } interface PostLanguageOption { /** @@ -1133,17 +1133,17 @@ declare namespace LocalJSX { /** * To communicate the variant prop from the parent (post-language-switch) component to the child (post-language-option) component. See parent docs for a description about the property itself. */ - "variant"?: SwitchVariant | null; + "variant"?: SwitchVariant; } interface PostLanguageSwitch { /** * A title for the list of language options */ - "caption"?: string; + "caption": string; /** * A descriptive text for the list of language options */ - "description"?: string; + "description": string; /** * Whether the component is rendered as a list or a menu */ @@ -1271,13 +1271,13 @@ declare namespace LocalJSX { /** * The name of the panel controlled by the tab header. */ - "panel"?: HTMLPostTabPanelElement['name']; + "panel": HTMLPostTabPanelElement['name']; } interface PostTabPanel { /** * The name of the panel, used to associate it with a tab header. */ - "name"?: string; + "name": string; } interface PostTabs { /** @@ -1293,11 +1293,11 @@ declare namespace LocalJSX { /** * Defines the icon `name` inside of the component. If not set the icon will not show up. To learn which icons are available, please visit our icon library. */ - "icon"?: null | string; + "icon"?: string; /** * Defines the size of the component. */ - "size"?: null | 'sm'; + "size"?: 'sm'; /** * Defines the color variant of the component. */ @@ -1311,7 +1311,7 @@ declare namespace LocalJSX { } interface PostTooltip { /** - * Wheter or not to display a little pointer arrow + * Whether or not to display a little pointer arrow */ "arrow"?: boolean; /** diff --git a/packages/components/src/components/post-accordion/post-accordion.tsx b/packages/components/src/components/post-accordion/post-accordion.tsx index a57dcbc1e6..c8e467f1b4 100644 --- a/packages/components/src/components/post-accordion/post-accordion.tsx +++ b/packages/components/src/components/post-accordion/post-accordion.tsx @@ -1,7 +1,7 @@ import { Component, Element, h, Host, Listen, Method, Prop, Watch } from '@stencil/core'; import { version } from '@root/package.json'; import { HEADING_LEVELS, HeadingLevel } from '@/types'; -import { checkOneOf } from '@/utils'; +import { checkOneOf, checkNonEmpty } from '@/utils'; import { eventGuard } from '@/utils/event-guard'; // Import eventGuard /** @@ -25,18 +25,19 @@ export class PostAccordion { @Prop() readonly headingLevel!: HeadingLevel; @Watch('headingLevel') - validateHeadingLevel(newValue = this.headingLevel) { - if (!newValue) return; - checkOneOf( - this, - 'headingLevel', - HEADING_LEVELS, - 'The `heading-level` property of the `post-accordion` must be a number between 1 and 6.', - ); - - this.accordionItems.forEach(item => { - item.setAttribute('heading-level', String(newValue)); - }); + validateHeadingLevel() { + if (!checkNonEmpty(this, 'headingLevel')) { + checkOneOf( + this, + 'headingLevel', + HEADING_LEVELS, + 'The `heading-level` property of the `post-accordion` must be a number between 1 and 6.', + ); + + this.accordionItems.forEach(item => { + item.setAttribute('heading-level', String(this.headingLevel)); + }); + } } /** @@ -44,14 +45,13 @@ export class PostAccordion { */ @Prop() readonly multiple: boolean = false; - componentWillLoad() { - this.registerAccordionItems(); + componentDidLoad() { this.validateHeadingLevel(); + this.registerAccordionItems(); } @Listen('postToggle') collapseToggleHandler(event: CustomEvent) { - eventGuard( this.host, event, @@ -74,10 +74,10 @@ export class PostAccordion { .forEach(item => { item.toggle(false); }); - } + }, ); } - + /** * Toggles the `post-accordion-item` with the given id. */ diff --git a/packages/components/src/components/post-avatar/post-avatar.tsx b/packages/components/src/components/post-avatar/post-avatar.tsx index 371e32436f..abda16ae8b 100644 --- a/packages/components/src/components/post-avatar/post-avatar.tsx +++ b/packages/components/src/components/post-avatar/post-avatar.tsx @@ -1,6 +1,6 @@ import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; import { version } from '@root/package.json'; -import { checkNonEmpty } from '@/utils'; +import { checkNonEmpty, checkEmptyOrType, checkEmptyOrPattern, checkType } from '@/utils'; // https://docs.gravatar.com/api/avatars/images/ const GRAVATAR_DEFAULT = '404'; @@ -9,6 +9,8 @@ const GRAVATAR_SIZE = 80; const GRAVATAR_BASE_URL = `https://www.gravatar.com/avatar/{email}?s=${GRAVATAR_SIZE}&d=${GRAVATAR_DEFAULT}&r=${GRAVATAR_RATING}`; +const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + enum AvatarType { Slotted = 'slotted', Image = 'image', @@ -57,7 +59,24 @@ export class PostAvatar { @Watch('firstname') validateFirstname() { - checkNonEmpty(this, 'firstname'); + if (!checkNonEmpty(this, 'firstname')) { + checkType(this, 'firstname', 'string'); + } + } + + @Watch('lastname') + validateLastname() { + checkEmptyOrType(this, 'lastname', 'string'); + } + + @Watch('userid') + validateUserid() { + checkEmptyOrType(this, 'userid', 'string'); + } + + @Watch('email') + validateEmail() { + checkEmptyOrPattern(this, 'email', emailPattern); } private async getAvatar() { @@ -166,6 +185,9 @@ export class PostAvatar { componentDidLoad() { this.validateFirstname(); + this.validateLastname(); + this.validateUserid(); + this.validateEmail(); } render() { diff --git a/packages/components/src/components/post-back-to-top/post-back-to-top.tsx b/packages/components/src/components/post-back-to-top/post-back-to-top.tsx index 7633054a0e..ed7b820c27 100644 --- a/packages/components/src/components/post-back-to-top/post-back-to-top.tsx +++ b/packages/components/src/components/post-back-to-top/post-back-to-top.tsx @@ -29,6 +29,13 @@ export class PostBackToTop { this.belowFold = this.isBelowFold(); }; + @Watch('label') + validateLabel() { + if (!checkNonEmpty(this, 'label')) { + checkType(this, 'label', 'string'); + } + } + /*Watch for changes in belowFold to show/hide the back to top button*/ @Watch('belowFold') watchBelowFold(newValue: boolean) { @@ -87,13 +94,6 @@ export class PostBackToTop { } } - // Validate the label - @Watch('label') - validateLabel() { - checkNonEmpty(this, 'label'); - checkType(this, 'label', 'string'); - } - // Set the initial state componentWillLoad() { this.belowFold = this.isBelowFold(); diff --git a/packages/components/src/components/post-banner/post-banner.tsx b/packages/components/src/components/post-banner/post-banner.tsx index 7c6912018f..ed4d601b53 100644 --- a/packages/components/src/components/post-banner/post-banner.tsx +++ b/packages/components/src/components/post-banner/post-banner.tsx @@ -12,7 +12,7 @@ import { } from '@stencil/core'; import { version } from '@root/package.json'; import { fadeOut } from '@/animations'; -import { checkEmptyOrOneOf, checkEmptyOrPattern, checkNonEmpty, checkType } from '@/utils'; +import { checkEmptyOrOneOf, checkNonEmpty, checkEmptyOrType, checkType } from '@/utils'; import { BANNER_TYPES, BannerType } from './banner-types'; import { nanoid } from 'nanoid'; @@ -43,24 +43,28 @@ export class PostBanner { @Watch('dismissible') validateDismissible() { - checkType(this, 'dismissible', 'boolean'); setTimeout(() => this.validateDismissLabel()); } /** * The label to use for the close button of a dismissible banner. */ - @Prop() readonly dismissLabel: string; + @Prop() readonly dismissLabel?: string; @Watch('dismissLabel') validateDismissLabel() { if (this.dismissible) { - checkNonEmpty( - this, - 'dismissLabel', - 'Dismissible post-banner\'s require a "dismiss-label" prop.', - ); + if ( + !checkNonEmpty( + this, + 'dismissLabel', + 'Dismissible post-banner\'s require a "dismiss-label" prop.', + ) + ) { + checkType(this, 'dismissLabel', 'string'); + } } + checkEmptyOrType(this, 'dismissLabel', 'string'); } /** @@ -68,16 +72,11 @@ export class PostBanner { * * If `none`, no icon is displayed. */ - @Prop() readonly icon: string; + @Prop() readonly icon?: string; @Watch('icon') validateIcon() { - checkEmptyOrPattern( - this, - 'icon', - /\d{4}|none/, - 'The post-banner "icon" prop should be a 4-digit string.', - ); + checkEmptyOrType(this, 'icon', 'string'); } /** diff --git a/packages/components/src/components/post-breadcrumb/post-breadcrumb.tsx b/packages/components/src/components/post-breadcrumb/post-breadcrumb.tsx index 49eb678bcd..75c2bf4dbd 100644 --- a/packages/components/src/components/post-breadcrumb/post-breadcrumb.tsx +++ b/packages/components/src/components/post-breadcrumb/post-breadcrumb.tsx @@ -1,6 +1,6 @@ import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; import { version } from '@root/package.json'; -import { checkUrl, debounce } from '@/utils'; +import { checkEmptyOrType, checkNonEmpty, checkUrl, debounce } from '@/utils'; @Component({ tag: 'post-breadcrumb', @@ -13,7 +13,7 @@ export class PostBreadcrumb { /** * The URL for the home breadcrumb item. */ - @Prop() homeUrl: string; + @Prop() homeUrl!: string; /** * The text label for the home breadcrumb item. @@ -28,8 +28,15 @@ export class PostBreadcrumb { private lastItem: { url: string; text: string }; @Watch('homeUrl') - validateUrl() { - checkUrl(this, 'homeUrl'); + validateHomeUrl() { + if (!checkNonEmpty(this, 'homeUrl')) { + checkUrl(this, 'homeUrl'); + } + } + + @Watch('homeText') + validateHomeText() { + checkEmptyOrType(this, 'homeUrl', 'string'); } componentWillLoad() { @@ -37,6 +44,8 @@ export class PostBreadcrumb { } componentDidLoad() { + this.validateHomeUrl(); + this.validateHomeText(); window.addEventListener('resize', this.handleResize); this.waitForBreadcrumbRef(); } diff --git a/packages/components/src/components/post-breadcrumb/readme.md b/packages/components/src/components/post-breadcrumb/readme.md index 916ff74906..ba548dda6e 100644 --- a/packages/components/src/components/post-breadcrumb/readme.md +++ b/packages/components/src/components/post-breadcrumb/readme.md @@ -7,10 +7,10 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ---------- | ----------- | -------------------------------------------- | -------- | ----------- | -| `homeText` | `home-text` | The text label for the home breadcrumb item. | `string` | `'Home'` | -| `homeUrl` | `home-url` | The URL for the home breadcrumb item. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ---------------------- | ----------- | -------------------------------------------- | -------- | ----------- | +| `homeText` | `home-text` | The text label for the home breadcrumb item. | `string` | `'Home'` | +| `homeUrl` _(required)_ | `home-url` | The URL for the home breadcrumb item. | `string` | `undefined` | ## Dependencies diff --git a/packages/components/src/components/post-card-control/post-card-control.tsx b/packages/components/src/components/post-card-control/post-card-control.tsx index 7f10327547..14ec99107f 100644 --- a/packages/components/src/components/post-card-control/post-card-control.tsx +++ b/packages/components/src/components/post-card-control/post-card-control.tsx @@ -11,7 +11,7 @@ import { State, Watch, } from '@stencil/core'; -import { checkNonEmpty, checkOneOf } from '@/utils'; +import { checkNonEmpty, checkOneOf, checkType, checkEmptyOrType, checkEmptyOrOneOf } from '@/utils'; import { version } from '@root/package.json'; let cardControlIds = 0; @@ -70,7 +70,7 @@ export class PostCardControl { /** * Defines the description in the control-label. */ - @Prop() readonly description: string = null; + @Prop() readonly description?: string; /** * Defines the `type` attribute of the control. @@ -82,12 +82,12 @@ export class PostCardControl { * This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. * This is a required property, when the control is used with type `radio`. */ - @Prop() readonly name: string = null; + @Prop() readonly name?: string; /** * Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. */ - @Prop() readonly value: string = null; + @Prop() readonly value?: string; /** * Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms' data. @@ -103,13 +103,13 @@ export class PostCardControl { * Defines the validation `validity` of the control. * To reset validity to an undefined state, simply remove the attribute from the control. */ - @Prop({ mutable: true }) validity: null | 'true' | 'false' = null; + @Prop({ mutable: true }) validity?: 'true' | 'false'; /** * Defines the icon `name` inside the card. * If not set the icon will not show up. */ - @Prop() readonly icon: string = null; + @Prop() readonly icon?: string; /** * An event emitted whenever the components checked state is toggled. @@ -130,7 +130,7 @@ export class PostCardControl { */ @Method() async reset() { - this.validity = null; + this.validity = undefined; this.controlSetChecked(this.initialChecked); } @@ -145,12 +145,51 @@ export class PostCardControl { @Watch('label') validateControlLabel() { - checkNonEmpty(this, 'label'); + if (!checkNonEmpty(this, 'label')) { + checkType(this, 'label', 'string'); + } + } + + @Watch('description') + validateControlDescription() { + checkEmptyOrType(this, 'description', 'string'); } @Watch('type') validateControlType() { - checkOneOf(this, 'type', ['checkbox', 'radio']); + if (!checkNonEmpty(this, 'type')) { + checkOneOf(this, 'type', ['checkbox', 'radio']); + } + } + + @Watch('name') + validateControlName() { + if (this.type == 'radio') { + if (!checkNonEmpty(this, 'name')) { + checkType(this, 'name', 'string'); + } + } + } + + @Watch('value') + validateControlValue() { + if (this.type == 'radio') { + if (!checkNonEmpty(this, 'value')) { + checkType(this, 'value', 'string'); + } + } + } + + @Watch('validity') + validateValidity() { + checkEmptyOrOneOf(this, 'validity', ['true', 'false']); + } + + @Watch('icon') + validateControlIcon() { + if (!checkNonEmpty(this, 'icon')) { + checkType(this, 'icon', 'string'); + } } @Watch('checked') @@ -348,7 +387,7 @@ export class PostCardControl { 'is-checked': this.checked, 'is-disabled': this.disabled, 'is-focused': this.focused, - 'is-valid': this.validity !== null && this.validity !== 'false', + 'is-valid': this.validity !== undefined && this.validity !== 'false', 'is-invalid': this.validity === 'false', }} > @@ -403,7 +442,11 @@ export class PostCardControl { componentDidLoad() { this.validateControlLabel(); + this.validateControlName(); + this.validateControlValue(); + this.validateControlDescription(); this.validateControlType(); + this.validateControlIcon(); } formAssociatedCallback() { diff --git a/packages/components/src/components/post-card-control/readme.md b/packages/components/src/components/post-card-control/readme.md index e94733e68f..e2a191b4ed 100644 --- a/packages/components/src/components/post-card-control/readme.md +++ b/packages/components/src/components/post-card-control/readme.md @@ -1,7 +1,5 @@ # post-card-control - - @@ -10,14 +8,14 @@ | Property | Attribute | Description | Type | Default | | -------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ----------- | | `checked` | `checked` | Defines the `checked` attribute of the control. If `true`, the control is selected at its value will be included in the forms' data. | `boolean` | `false` | -| `description` | `description` | Defines the description in the control-label. | `string` | `null` | +| `description` | `description` | Defines the description in the control-label. | `string` | `undefined` | | `disabled` | `disabled` | Defines the `disabled` attribute of the control. If `true`, the user can not interact with the control and the controls value will not be included in the forms' data. | `boolean` | `false` | -| `icon` | `icon` | Defines the icon `name` inside the card. If not set the icon will not show up. | `string` | `null` | +| `icon` | `icon` | Defines the icon `name` inside the card. If not set the icon will not show up. | `string` | `undefined` | | `label` _(required)_ | `label` | Defines the text in the control-label. | `string` | `undefined` | -| `name` | `name` | Defines the `name` attribute of the control. This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. | `string` | `null` | +| `name` | `name` | Defines the `name` attribute of the control. This is a required property, when the control should participate in a native `form`. If not specified, a native `form` will never contain this controls value. This is a required property, when the control is used with type `radio`. | `string` | `undefined` | | `type` _(required)_ | `type` | Defines the `type` attribute of the control. | `"checkbox" \| "radio"` | `undefined` | -| `validity` | `validity` | Defines the validation `validity` of the control. To reset validity to an undefined state, simply remove the attribute from the control. | `"false" \| "true"` | `null` | -| `value` | `value` | Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. | `string` | `null` | +| `validity` | `validity` | Defines the validation `validity` of the control. To reset validity to an undefined state, simply remove the attribute from the control. | `"false" \| "true"` | `undefined` | +| `value` | `value` | Defines the `value` attribute of the control. This is a required property, when the control is used with type `radio`. | `string` | `undefined` | ## Events diff --git a/packages/components/src/components/post-collapsible-trigger/post-collapsible-trigger.tsx b/packages/components/src/components/post-collapsible-trigger/post-collapsible-trigger.tsx index 9e20bf2c09..b449d78caa 100644 --- a/packages/components/src/components/post-collapsible-trigger/post-collapsible-trigger.tsx +++ b/packages/components/src/components/post-collapsible-trigger/post-collapsible-trigger.tsx @@ -16,15 +16,17 @@ export class PostCollapsibleTrigger { /** * Link the trigger to a post-collapsible with this id */ - @Prop({ reflect: true }) for: string; + + @Prop({ reflect: true }) for!: string; /** * Set the "aria-controls" and "aria-expanded" attributes on the trigger to match the state of the controlled post-collapsible */ @Watch('for') validateAriaAttributes() { - checkNonEmpty(this, 'for'); - checkType(this, 'for', 'string', 'The post-collapsible-trigger "for" prop should be a id.'); + if (!checkNonEmpty(this, 'for')) { + checkType(this, 'for', 'string', 'The post-collapsible-trigger "for" prop should be a id.'); + } } /** diff --git a/packages/components/src/components/post-collapsible-trigger/readme.md b/packages/components/src/components/post-collapsible-trigger/readme.md index 5ebbabbe44..cf349afcba 100644 --- a/packages/components/src/components/post-collapsible-trigger/readme.md +++ b/packages/components/src/components/post-collapsible-trigger/readme.md @@ -7,9 +7,9 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------- | --------- | --------------------------------------------------- | -------- | ----------- | -| `for` | `for` | Link the trigger to a post-collapsible with this id | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ------------------ | --------- | --------------------------------------------------- | -------- | ----------- | +| `for` _(required)_ | `for` | Link the trigger to a post-collapsible with this id | `string` | `undefined` | ## Methods diff --git a/packages/components/src/components/post-collapsible/post-collapsible.tsx b/packages/components/src/components/post-collapsible/post-collapsible.tsx index b991870091..5da2332e1c 100644 --- a/packages/components/src/components/post-collapsible/post-collapsible.tsx +++ b/packages/components/src/components/post-collapsible/post-collapsible.tsx @@ -11,7 +11,7 @@ import { } from '@stencil/core'; import { version } from '@root/package.json'; import { collapse, expand } from '@/animations/collapse'; -import { checkEmptyOrType, isMotionReduced } from '@/utils'; +import { checkType, isMotionReduced } from '@/utils'; /** * @slot default - Slot for placing content within the collapsible element. @@ -35,7 +35,7 @@ export class PostCollapsible { @Watch('collapsed') collapsedChange() { - checkEmptyOrType(this, 'collapsed', 'boolean'); + checkType(this, 'collapsed', 'boolean'); void this.toggle(!this.collapsed); } diff --git a/packages/components/src/components/post-footer/post-footer.tsx b/packages/components/src/components/post-footer/post-footer.tsx index 7972665fc5..f130e8b3cf 100644 --- a/packages/components/src/components/post-footer/post-footer.tsx +++ b/packages/components/src/components/post-footer/post-footer.tsx @@ -1,6 +1,7 @@ -import { Component, Element, h, Host, Prop, State } from '@stencil/core'; +import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; import { version } from '@root/package.json'; -import { breakpoint } from '@/utils'; +import { breakpoint } from '../../utils/breakpoints'; +import { checkNonEmpty, checkType } from '@/utils'; /** * @slot grid-{1|2|3|4}-title - Slot for the accordion headers (mobile). @@ -26,10 +27,21 @@ export class PostFooter { @State() isMobile: boolean = breakpoint.get('name') === 'mobile'; + @Watch('label') + validateLabel() { + if (!checkNonEmpty(this, 'label')) { + checkType(this, 'label', 'string'); + } + } + connectedCallback() { window.addEventListener('postBreakpoint:name', this.breakpointChange); } + componentWillLoad() { + this.validateLabel(); + } + disconnectedCallback() { window.removeEventListener('postBreakpoint:name', this.breakpointChange); } diff --git a/packages/components/src/components/post-icon/post-icon.tsx b/packages/components/src/components/post-icon/post-icon.tsx index 68bd6c05c4..cd5412612f 100644 --- a/packages/components/src/components/post-icon/post-icon.tsx +++ b/packages/components/src/components/post-icon/post-icon.tsx @@ -1,5 +1,7 @@ import { Component, Element, Host, h, Prop, Watch } from '@stencil/core'; -import { IS_BROWSER, checkNonEmpty, checkType, checkEmptyOrType, checkEmptyOrOneOf } from '@/utils'; + +import { IS_BROWSER, checkType, checkEmptyOrType, checkEmptyOrOneOf, checkNonEmpty } from '@/utils'; + import { version } from '@root/package.json'; type UrlDefinition = { @@ -36,7 +38,7 @@ export class PostIcon { /** * The name of the animation. */ - @Prop() readonly animation?: Animation | null = null; + @Prop() readonly animation?: Animation; @Watch('animation') validateAnimation(newValue = this.animation) { @@ -46,7 +48,7 @@ export class PostIcon { /** * The base path, where the icons are located (must be a public url).
Leave this field empty to use the default cdn url. */ - @Prop() readonly base?: string | null = null; + @Prop() readonly base?: string; @Watch('base') validateBase() { @@ -58,21 +60,11 @@ export class PostIcon { */ @Prop() readonly flipH?: boolean = false; - @Watch('flipH') - validateFlipH() { - checkEmptyOrType(this, 'flipH', 'boolean'); - } - /** * When set to `true`, the icon will be flipped vertically. */ @Prop() readonly flipV?: boolean = false; - @Watch('flipV') - validateFlipV() { - checkEmptyOrType(this, 'flipV', 'boolean'); - } - /** * The name/id of the icon (e.g. 1000, 1001, ...). */ @@ -80,14 +72,15 @@ export class PostIcon { @Watch('name') validateName() { - checkNonEmpty(this, 'name'); - checkType(this, 'name', 'string'); + if (!checkNonEmpty(this, 'name')) { + checkType(this, 'name', 'string'); + } } /** * The number of degree for the css rotate transformation. */ - @Prop() readonly rotate?: number | null = null; + @Prop() readonly rotate?: number; @Watch('rotate') validateRotate() { @@ -97,7 +90,7 @@ export class PostIcon { /** * The number for the css scale transformation. */ - @Prop() readonly scale?: number | null = null; + @Prop() readonly scale?: number; @Watch('scale') validateScale() { @@ -188,8 +181,6 @@ export class PostIcon { componentDidLoad() { this.validateBase(); this.validateName(); - this.validateFlipH(); - this.validateFlipV(); this.validateScale(); this.validateRotate(); this.validateAnimation(); diff --git a/packages/components/src/components/post-icon/readme.md b/packages/components/src/components/post-icon/readme.md index 4005f0b00d..c95dc0d4c7 100644 --- a/packages/components/src/components/post-icon/readme.md +++ b/packages/components/src/components/post-icon/readme.md @@ -9,13 +9,13 @@ some content | Property | Attribute | Description | Type | Default | | ------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----------- | -| `animation` | `animation` | The name of the animation. | `"cylon" \| "cylon-vertical" \| "fade" \| "spin" \| "spin-reverse" \| "throb"` | `null` | -| `base` | `base` | The base path, where the icons are located (must be a public url).
Leave this field empty to use the default cdn url. | `string` | `null` | +| `animation` | `animation` | The name of the animation. | `"cylon" \| "cylon-vertical" \| "fade" \| "spin" \| "spin-reverse" \| "throb"` | `undefined` | +| `base` | `base` | The base path, where the icons are located (must be a public url).
Leave this field empty to use the default cdn url. | `string` | `undefined` | | `flipH` | `flip-h` | When set to `true`, the icon will be flipped horizontally. | `boolean` | `false` | | `flipV` | `flip-v` | When set to `true`, the icon will be flipped vertically. | `boolean` | `false` | | `name` _(required)_ | `name` | The name/id of the icon (e.g. 1000, 1001, ...). | `string` | `undefined` | -| `rotate` | `rotate` | The number of degree for the css rotate transformation. | `number` | `null` | -| `scale` | `scale` | The number for the css scale transformation. | `number` | `null` | +| `rotate` | `rotate` | The number of degree for the css rotate transformation. | `number` | `undefined` | +| `scale` | `scale` | The number for the css scale transformation. | `number` | `undefined` | ## Dependencies diff --git a/packages/components/src/components/post-language-option/post-language-option.tsx b/packages/components/src/components/post-language-option/post-language-option.tsx index 126dfb2892..6af7ccbf32 100644 --- a/packages/components/src/components/post-language-option/post-language-option.tsx +++ b/packages/components/src/components/post-language-option/post-language-option.tsx @@ -9,9 +9,15 @@ import { Prop, Watch, } from '@stencil/core'; -import { checkEmptyOrType, checkType } from '@/utils'; +import { + checkEmptyOrType, + checkType, + checkNonEmpty, + checkEmptyOrOneOf, + checkEmptyOrUrl, +} from '@/utils'; import { version } from '@root/package.json'; -import { SwitchVariant } from '../post-language-switch/switch-variants'; +import { SwitchVariant, SWITCH_VARIANTS } from '../post-language-switch/switch-variants'; /** * @slot default - Slot for placing the content inside the anchor or button. @@ -30,28 +36,30 @@ export class PostLanguageOption { @Watch('code') validateCode() { - checkType(this, 'code', 'string'); + if (!checkNonEmpty(this, 'code')) { + checkType(this, 'code', 'string'); + } } /** * If set to `true`, the language option is considered the current language for the page. */ - @Prop({ mutable: true, reflect: true }) active: boolean; - - @Watch('active') - validateActiveProp() { - checkEmptyOrType(this, 'active', 'boolean'); - } + @Prop({ mutable: true, reflect: true }) active?: boolean; /** * To communicate the variant prop from the parent (post-language-switch) component to the child (post-language-option) component. See parent docs for a description about the property itself. */ - @Prop() variant?: SwitchVariant | null; + @Prop() variant?: SwitchVariant; + + @Watch('variant') + validateVariant() { + checkEmptyOrOneOf(this, 'variant', SWITCH_VARIANTS); + } /** * The full name of the language. For example, "Deutsch". */ - @Prop() name: string; + @Prop() name?: string; @Watch('name') validateName() { @@ -62,16 +70,15 @@ export class PostLanguageOption { * The URL used for the href attribute of the internal anchor. * This field is optional; if not provided, a button will be used internally instead of an anchor. */ - @Prop() url: string; + @Prop() url?: string; @Watch('url') validateUrl() { - checkEmptyOrType(this, 'url', 'string'); + checkEmptyOrUrl(this, 'url'); } componentDidLoad() { this.validateCode(); - this.validateActiveProp(); this.validateName(); this.validateUrl(); @@ -114,7 +121,7 @@ export class PostLanguageOption { } render() { - const lang = this.code.toLowerCase(); + const lang: string = this.code ? this.code.toLowerCase() : ''; const emitOnKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { diff --git a/packages/components/src/components/post-language-switch/post-language-switch.tsx b/packages/components/src/components/post-language-switch/post-language-switch.tsx index 8d7f747843..94b84a341a 100644 --- a/packages/components/src/components/post-language-switch/post-language-switch.tsx +++ b/packages/components/src/components/post-language-switch/post-language-switch.tsx @@ -1,5 +1,5 @@ import { Component, Element, Host, h, Prop, Watch, State, Listen } from '@stencil/core'; -import { checkEmptyOrOneOf, checkType, eventGuard } from '@/utils'; +import { checkEmptyOrOneOf, checkType, checkNonEmpty, eventGuard } from '@/utils'; import { version } from '@root/package.json'; import { SWITCH_VARIANTS, SwitchVariant } from './switch-variants'; import { nanoid } from 'nanoid'; @@ -22,21 +22,25 @@ export class PostLanguageSwitch { /** * A title for the list of language options */ - @Prop() caption: string; + @Prop() caption!: string; @Watch('caption') validateCaption() { - checkType(this, 'caption', 'string'); + if (!checkNonEmpty(this, 'caption')) { + checkType(this, 'caption', 'string'); + } } /** * A descriptive text for the list of language options */ - @Prop() description: string; + @Prop() description!: string; @Watch('description') validateDescription() { - checkType(this, 'description', 'string'); + if (!checkNonEmpty(this, 'description')) { + checkType(this, 'description', 'string'); + } } /** diff --git a/packages/components/src/components/post-language-switch/readme.md b/packages/components/src/components/post-language-switch/readme.md index d00bb0c33a..42ef11d6fa 100644 --- a/packages/components/src/components/post-language-switch/readme.md +++ b/packages/components/src/components/post-language-switch/readme.md @@ -5,11 +5,11 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| ------------- | ------------- | ----------------------------------------------------- | ------------------ | ----------- | -| `caption` | `caption` | A title for the list of language options | `string` | `undefined` | -| `description` | `description` | A descriptive text for the list of language options | `string` | `undefined` | -| `variant` | `variant` | Whether the component is rendered as a list or a menu | `"list" \| "menu"` | `'list'` | +| Property | Attribute | Description | Type | Default | +| -------------------------- | ------------- | ----------------------------------------------------- | ------------------ | ----------- | +| `caption` _(required)_ | `caption` | A title for the list of language options | `string` | `undefined` | +| `description` _(required)_ | `description` | A descriptive text for the list of language options | `string` | `undefined` | +| `variant` | `variant` | Whether the component is rendered as a list or a menu | `"list" \| "menu"` | `'list'` | ## Dependencies diff --git a/packages/components/src/components/post-logo/post-logo.tsx b/packages/components/src/components/post-logo/post-logo.tsx index b05918a549..dfcdcc05a4 100644 --- a/packages/components/src/components/post-logo/post-logo.tsx +++ b/packages/components/src/components/post-logo/post-logo.tsx @@ -16,7 +16,7 @@ export class PostLogo { /** * The URL to which the user is redirected upon clicking the logo. */ - @Prop() url: string | URL; + @Prop() url?: string | URL; @Watch('url') validateUrl() { diff --git a/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.tsx b/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.tsx index ca713e0681..ccd98c3e92 100644 --- a/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.tsx +++ b/packages/components/src/components/post-megadropdown-trigger/post-megadropdown-trigger.tsx @@ -1,6 +1,6 @@ import { Component, Element, Prop, h, Host, State, Watch } from '@stencil/core'; import { version } from '@root/package.json'; -import { checkType } from '@/utils'; +import { checkType, checkNonEmpty } from '@/utils'; import { eventGuard } from '@/utils/event-guard'; @Component({ @@ -39,7 +39,9 @@ export class PostMegadropdownTrigger { */ @Watch('for') validateControlFor() { - checkType(this, 'for', 'string'); + if (!checkNonEmpty(this, 'for')) { + checkType(this, 'for', 'string'); + } } private get megadropdown(): HTMLPostMegadropdownElement | null { @@ -67,29 +69,26 @@ export class PostMegadropdownTrigger { } }; - private handleToggleMegadropdown = (event: CustomEvent<{ isVisible: boolean; focusParent: boolean }>) => { - eventGuard( - this.host, - event, - { targetLocalName: 'post-megadropdown' }, - () => { - if ((event.target as HTMLPostMegadropdownElement).id === this.for) { - this.ariaExpanded = event.detail.isVisible; - - // Focus on the trigger parent of the dropdown after it's closed if the close button had been clicked - if (this.wasExpanded && !this.ariaExpanded && event.detail.focusParent) { - setTimeout(() => { - this.slottedButton?.focus(); - }, 100); - } - this.wasExpanded = this.ariaExpanded; - - if (this.slottedButton) { - this.slottedButton.setAttribute('aria-expanded', this.ariaExpanded.toString()); - } + private handleToggleMegadropdown = ( + event: CustomEvent<{ isVisible: boolean; focusParent: boolean }>, + ) => { + eventGuard(this.host, event, { targetLocalName: 'post-megadropdown' }, () => { + if ((event.target as HTMLPostMegadropdownElement).id === this.for) { + this.ariaExpanded = event.detail.isVisible; + + // Focus on the trigger parent of the dropdown after it's closed if the close button had been clicked + if (this.wasExpanded && !this.ariaExpanded && event.detail.focusParent) { + setTimeout(() => { + this.slottedButton?.focus(); + }, 100); + } + this.wasExpanded = this.ariaExpanded; + + if (this.slottedButton) { + this.slottedButton.setAttribute('aria-expanded', this.ariaExpanded.toString()); } } - ); + }); }; componentDidLoad() { diff --git a/packages/components/src/components/post-menu-trigger/post-menu-trigger.tsx b/packages/components/src/components/post-menu-trigger/post-menu-trigger.tsx index 96c0d0da61..ac9b233800 100644 --- a/packages/components/src/components/post-menu-trigger/post-menu-trigger.tsx +++ b/packages/components/src/components/post-menu-trigger/post-menu-trigger.tsx @@ -1,6 +1,6 @@ import { Component, Element, Prop, h, Host, State, Watch } from '@stencil/core'; import { version } from '@root/package.json'; -import { checkType, getRoot } from '@/utils'; +import { checkType, getRoot, checkNonEmpty } from '@/utils'; @Component({ tag: 'post-menu-trigger', @@ -33,7 +33,9 @@ export class PostMenuTrigger { */ @Watch('for') validateControlFor() { - checkType(this, 'for', 'string'); + if (!checkNonEmpty(this, 'for')) { + checkType(this, 'for', 'string'); + } } private get menu(): HTMLPostMenuElement | null { diff --git a/packages/components/src/components/post-menu/post-menu.tsx b/packages/components/src/components/post-menu/post-menu.tsx index 3de903b7d7..eb78f9f9f8 100644 --- a/packages/components/src/components/post-menu/post-menu.tsx +++ b/packages/components/src/components/post-menu/post-menu.tsx @@ -8,11 +8,13 @@ import { Method, Prop, State, + Watch, } from '@stencil/core'; import { Placement } from '@floating-ui/dom'; +import { PLACEMENT_TYPES } from '@/types'; import { version } from '@root/package.json'; import { getFocusableChildren } from '@/utils/get-focusable-children'; -import { getRoot } from '@/utils'; +import { getRoot, checkEmptyOrOneOf } from '@/utils'; import { eventGuard } from '@/utils/event-guard'; @Component({ @@ -44,6 +46,11 @@ export class PostMenu { */ @Prop() readonly placement?: Placement = 'bottom'; + @Watch('placement') + validatePlacement() { + checkEmptyOrOneOf(this, 'placement', PLACEMENT_TYPES); + } + /** * Holds the current visibility state of the menu. * This state is internally managed to track whether the menu is open (`true`) or closed (`false`), @@ -72,6 +79,7 @@ export class PostMenu { } componentDidLoad() { + this.validatePlacement(); if (this.popoverRef) { this.popoverRef.addEventListener('postToggle', this.handlePostToggle); } @@ -82,7 +90,6 @@ export class PostMenu { */ @Method() async toggle(target: HTMLElement) { - if (this.popoverRef) { await this.popoverRef.toggle(target); } else { @@ -149,7 +156,7 @@ export class PostMenu { this.lastFocusedElement.focus(); } }); - } + }, ); }; @@ -167,7 +174,7 @@ export class PostMenu { } let currentIndex = menuItems.findIndex(el => { - // Check if the item is currently focused within its rendered scope (document or shadow root) + // Check if the item is currently focused within its rendered scope (document or shadow root) return el === getRoot(el).activeElement; }); diff --git a/packages/components/src/components/post-popover/post-popover.tsx b/packages/components/src/components/post-popover/post-popover.tsx index eebd4f670d..a35cdb2c8b 100644 --- a/packages/components/src/components/post-popover/post-popover.tsx +++ b/packages/components/src/components/post-popover/post-popover.tsx @@ -1,7 +1,14 @@ -import { Component, Element, h, Host, Method, Prop } from '@stencil/core'; +import { Component, Element, h, Host, Method, Prop, Watch } from '@stencil/core'; import { Placement } from '@floating-ui/dom'; -import { IS_BROWSER, getAttributeObserver } from '@/utils'; +import { PLACEMENT_TYPES } from '@/types'; import { version } from '@root/package.json'; +import { + IS_BROWSER, + getAttributeObserver, + checkEmptyOrOneOf, + checkNonEmpty, + checkType, +} from '@/utils'; /** * @slot default - Slot for placing content inside the popover. @@ -57,6 +64,18 @@ export class PostPopover { // eslint-disable-next-line @stencil-community/ban-default-true @Prop() readonly arrow?: boolean = true; + @Watch('placement') + validatePlacement() { + checkEmptyOrOneOf(this, 'placement', PLACEMENT_TYPES); + } + + @Watch('closeButtonCaption') + validateCloseButtonCaption() { + if (!checkNonEmpty(this, 'closeButtonCaption')) { + checkType(this, 'closeButtonCaption', 'string'); + } + } + constructor() { this.localBeforeToggleHandler = this.beforeToggleHandler.bind(this); } @@ -79,6 +98,8 @@ export class PostPopover { } componentDidLoad() { + this.validatePlacement(); + this.validateCloseButtonCaption(); this.popoverRef.addEventListener('beforetoggle', this.localBeforeToggleHandler); } diff --git a/packages/components/src/components/post-popovercontainer/post-popovercontainer.tsx b/packages/components/src/components/post-popovercontainer/post-popovercontainer.tsx index 8a6a53b007..26a04973c8 100644 --- a/packages/components/src/components/post-popovercontainer/post-popovercontainer.tsx +++ b/packages/components/src/components/post-popovercontainer/post-popovercontainer.tsx @@ -1,5 +1,16 @@ -import { Component, Element, Event, EventEmitter, Host, Method, Prop, h } from '@stencil/core'; -import { IS_BROWSER } from '@/utils'; +import { + Component, + Element, + Event, + EventEmitter, + Host, + Method, + Prop, + h, + Watch, +} from '@stencil/core'; + +import { IS_BROWSER, checkEmptyOrOneOf, checkEmptyOrType } from '@/utils'; import { version } from '@root/package.json'; import { @@ -15,6 +26,8 @@ import { size, } from '@floating-ui/dom'; +import { PLACEMENT_TYPES } from '@/types'; + // Polyfill for popovers, can be removed when https://caniuse.com/?search=popover is green import { apply, isSupported } from '@oddbird/popover-polyfill/dist/popover-fn.js'; @@ -95,6 +108,21 @@ export class PostPopovercontainer { */ @Prop({ reflect: true }) readonly safeSpace?: 'triangle' | 'trapezoid'; + @Watch('placement') + validatePlacement() { + checkEmptyOrOneOf(this, 'placement', PLACEMENT_TYPES); + } + + @Watch('edgeGap') + validateEdgeGap() { + checkEmptyOrType(this, 'edgeGap', 'number'); + } + + @Watch('safeSpace') + validateSafeSpace() { + checkEmptyOrOneOf(this, 'safeSpace', ['triangle', 'trapezoid']); + } + /** * Updates cursor position for safe space feature when popover is open. * Sets CSS custom properties for dynamic styling of safe area. diff --git a/packages/components/src/components/post-rating/post-rating.tsx b/packages/components/src/components/post-rating/post-rating.tsx index 9065e71153..6de8115b4c 100644 --- a/packages/components/src/components/post-rating/post-rating.tsx +++ b/packages/components/src/components/post-rating/post-rating.tsx @@ -1,5 +1,16 @@ -import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core'; +import { + Component, + Element, + Event, + EventEmitter, + h, + Host, + Prop, + State, + Watch, +} from '@stencil/core'; import { version } from '@root/package.json'; +import { checkType, checkNonEmpty } from '@/utils'; @Component({ tag: 'post-rating', @@ -46,6 +57,27 @@ export class PostRating { */ @Event() postChange: EventEmitter<{ value: number }>; + @Watch('label') + validateLabel() { + if (!checkNonEmpty(this, 'label')) { + checkType(this, 'label', 'string'); + } + } + + @Watch('stars') + validateStars() { + if (!checkNonEmpty(this, 'stars')) { + checkType(this, 'stars', 'number'); + } + } + + @Watch('currentRating') + validateCurrentRating() { + if (!checkNonEmpty(this, 'currentRating')) { + checkType(this, 'currentRating', 'number'); + } + } + constructor() { this.keydownHandler = this.keydownHandler.bind(this); this.blurHandler = this.blurHandler.bind(this); @@ -106,6 +138,12 @@ export class PostRating { } } + componentWillLoad() { + this.validateLabel(); + this.validateStars(); + this.validateCurrentRating(); + } + render() { return ( diff --git a/packages/components/src/components/post-tab-header/post-tab-header.tsx b/packages/components/src/components/post-tab-header/post-tab-header.tsx index 587b054ceb..c2ee894cde 100644 --- a/packages/components/src/components/post-tab-header/post-tab-header.tsx +++ b/packages/components/src/components/post-tab-header/post-tab-header.tsx @@ -20,7 +20,7 @@ export class PostTabHeader { /** * The name of the panel controlled by the tab header. */ - @Prop({ reflect: true }) readonly panel: HTMLPostTabPanelElement['name']; + @Prop({ reflect: true }) readonly panel!: HTMLPostTabPanelElement['name']; @Watch('panel') validateFor() { diff --git a/packages/components/src/components/post-tab-header/readme.md b/packages/components/src/components/post-tab-header/readme.md index 48fb3378b2..6d09529475 100644 --- a/packages/components/src/components/post-tab-header/readme.md +++ b/packages/components/src/components/post-tab-header/readme.md @@ -7,9 +7,9 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------- | --------- | --------------------------------------------------- | -------- | ----------- | -| `panel` | `panel` | The name of the panel controlled by the tab header. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| -------------------- | --------- | --------------------------------------------------- | -------- | ----------- | +| `panel` _(required)_ | `panel` | The name of the panel controlled by the tab header. | `string` | `undefined` | ## Slots diff --git a/packages/components/src/components/post-tab-panel/post-tab-panel.tsx b/packages/components/src/components/post-tab-panel/post-tab-panel.tsx index 59f09acaf9..f736ab9c23 100644 --- a/packages/components/src/components/post-tab-panel/post-tab-panel.tsx +++ b/packages/components/src/components/post-tab-panel/post-tab-panel.tsx @@ -1,6 +1,7 @@ -import { Component, Element, h, Host, Prop, State } from '@stencil/core'; +import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; import { version } from '@root/package.json'; import { nanoid } from 'nanoid'; +import { checkNonEmpty, checkType } from '@/utils'; /** * @slot default - Slot for placing the content of the tab panel. @@ -19,9 +20,18 @@ export class PostTabPanel { /** * The name of the panel, used to associate it with a tab header. */ - @Prop({ reflect: true }) readonly name: string; + + @Prop({ reflect: true }) readonly name!: string; + + @Watch('name') + validateName() { + if (!checkNonEmpty(this, 'name')) { + checkType(this, 'name', 'string'); + } + } componentWillLoad() { + this.validateName(); // get the id set on the host element or use a random id by default this.panelId = `panel-${this.host.id || nanoid(6)}`; } diff --git a/packages/components/src/components/post-tab-panel/readme.md b/packages/components/src/components/post-tab-panel/readme.md index 440b621d52..7655490f14 100644 --- a/packages/components/src/components/post-tab-panel/readme.md +++ b/packages/components/src/components/post-tab-panel/readme.md @@ -7,9 +7,9 @@ ## Properties -| Property | Attribute | Description | Type | Default | -| -------- | --------- | -------------------------------------------------------------- | -------- | ----------- | -| `name` | `name` | The name of the panel, used to associate it with a tab header. | `string` | `undefined` | +| Property | Attribute | Description | Type | Default | +| ------------------- | --------- | -------------------------------------------------------------- | -------- | ----------- | +| `name` _(required)_ | `name` | The name of the panel, used to associate it with a tab header. | `string` | `undefined` | ## Slots diff --git a/packages/components/src/components/post-tabs/post-tabs.tsx b/packages/components/src/components/post-tabs/post-tabs.tsx index 1ea3895b12..445346e1e2 100644 --- a/packages/components/src/components/post-tabs/post-tabs.tsx +++ b/packages/components/src/components/post-tabs/post-tabs.tsx @@ -22,10 +22,10 @@ export class PostTabs { private isLoaded = false; private get tabs(): HTMLPostTabHeaderElement[] { - return Array.from(this.host.querySelectorAll('post-tab-header')).filter(tab => - tab.closest('post-tabs') === this.host - ); - } + return Array.from( + this.host.querySelectorAll('post-tab-header'), + ).filter(tab => tab.closest('post-tabs') === this.host); + } @Element() host: HTMLPostTabsElement; @@ -35,7 +35,7 @@ export class PostTabs { * * **Changing this value after initialization has no effect.** */ - @Prop() readonly activePanel: HTMLPostTabPanelElement['name']; + @Prop() readonly activePanel?: HTMLPostTabPanelElement['name']; /** * An event emitted after the active tab changes, when the fade in transition of its associated panel is finished. diff --git a/packages/components/src/components/post-tag/post-tag.tsx b/packages/components/src/components/post-tag/post-tag.tsx index 0deb0216f0..80f36ba364 100644 --- a/packages/components/src/components/post-tag/post-tag.tsx +++ b/packages/components/src/components/post-tag/post-tag.tsx @@ -1,6 +1,6 @@ import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core'; import { version } from '@root/package.json'; - +import { checkEmptyOrOneOf, checkEmptyOrType } from '@/utils'; /** * @slot default - Content to place in the `default` slot.

Markup accepted: inline content.

*/ @@ -17,19 +17,19 @@ export class PostTag { /** * Defines the color variant of the component. */ - @Prop() readonly variant: 'white' | 'info' | 'success' | 'error' | 'warning' | 'yellow'; + @Prop() readonly variant?: 'white' | 'info' | 'success' | 'error' | 'warning' | 'yellow'; /** * Defines the size of the component. */ - @Prop() readonly size: null | 'sm' = null; + @Prop() readonly size?: 'sm'; /** * Defines the icon `name` inside of the component. * If not set the icon will not show up. * To learn which icons are available, please visit our icon library. */ - @Prop() readonly icon: null | string = null; + @Prop() readonly icon: string; constructor() { this.setClasses = this.setClasses.bind(this); @@ -37,14 +37,21 @@ export class PostTag { @Watch('variant') variantChanged() { + checkEmptyOrOneOf(this, 'variant', ['white', 'info', 'success', 'error', 'warning', 'yellow']); this.setClasses(); } @Watch('size') sizeChanged() { + checkEmptyOrOneOf(this, 'size', ['sm']); this.setClasses(); } + @Watch('icon') + validateName() { + checkEmptyOrType(this, 'icon', 'string'); + } + private setClasses() { this.classes = [ 'tag', @@ -59,6 +66,12 @@ export class PostTag { this.setClasses(); } + componentWillLoad() { + this.validateName(); + this.variantChanged(); + this.sizeChanged(); + } + render() { return ( diff --git a/packages/components/src/components/post-tag/readme.md b/packages/components/src/components/post-tag/readme.md index 7596099d83..fbfb296e16 100644 --- a/packages/components/src/components/post-tag/readme.md +++ b/packages/components/src/components/post-tag/readme.md @@ -7,8 +7,8 @@ | Property | Attribute | Description | Type | Default | | --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------- | -| `icon` | `icon` | Defines the icon `name` inside of the component. If not set the icon will not show up. To learn which icons are available, please visit our icon library. | `string` | `null` | -| `size` | `size` | Defines the size of the component. | `"sm"` | `null` | +| `icon` | `icon` | Defines the icon `name` inside of the component. If not set the icon will not show up. To learn which icons are available, please visit our icon library. | `string` | `undefined` | +| `size` | `size` | Defines the size of the component. | `"sm"` | `undefined` | | `variant` | `variant` | Defines the color variant of the component. | `"error" \| "info" \| "success" \| "warning" \| "white" \| "yellow"` | `undefined` | diff --git a/packages/components/src/components/post-togglebutton/post-togglebutton.tsx b/packages/components/src/components/post-togglebutton/post-togglebutton.tsx index b310c9f2f3..16029d557b 100644 --- a/packages/components/src/components/post-togglebutton/post-togglebutton.tsx +++ b/packages/components/src/components/post-togglebutton/post-togglebutton.tsx @@ -1,6 +1,5 @@ -import { Component, Host, h, Prop, Watch, Element } from '@stencil/core'; +import { Component, Host, h, Prop, Element } from '@stencil/core'; import { version } from '@root/package.json'; -import { checkType } from '@/utils'; /** * @slot default - Slot for the content of the button. @@ -19,14 +18,7 @@ export class PostTogglebutton { */ @Prop({ mutable: true }) toggled: boolean = false; - @Watch('toggled') - validateToggled() { - checkType(this, 'toggled', 'boolean'); - } - componentWillLoad() { - this.validateToggled(); - // add event listener to not override listener that might be set on the host this.host.addEventListener('click', () => this.handleClick()); this.host.addEventListener('keydown', (e: KeyboardEvent) => this.handleKeydown(e)); diff --git a/packages/components/src/components/post-tooltip/post-tooltip.tsx b/packages/components/src/components/post-tooltip/post-tooltip.tsx index 070d280680..6847e966d9 100644 --- a/packages/components/src/components/post-tooltip/post-tooltip.tsx +++ b/packages/components/src/components/post-tooltip/post-tooltip.tsx @@ -1,7 +1,9 @@ import { Component, Element, h, Host, Method, Prop, Watch } from '@stencil/core'; import { Placement } from '@floating-ui/dom'; +import { PLACEMENT_TYPES } from '@/types'; +import 'long-press-event'; import isFocusable from 'ally.js/is/focusable'; -import { IS_BROWSER, checkEmptyOrType, getAttributeObserver } from '@/utils'; +import { IS_BROWSER, getAttributeObserver, checkEmptyOrOneOf } from '@/utils'; import { version } from '@root/package.json'; if (IS_BROWSER) { @@ -108,7 +110,7 @@ export class PostTooltip { @Prop() readonly placement?: Placement = 'top'; /** - * Wheter or not to display a little pointer arrow + * Whether or not to display a little pointer arrow */ @Prop() readonly arrow?: boolean = true; @@ -117,13 +119,13 @@ export class PostTooltip { */ @Prop() readonly delayed: boolean = false; - @Watch('delayed') - validateDelayed() { - checkEmptyOrType(this, 'delayed', 'boolean'); + @Watch('placement') + validatePlacement() { + checkEmptyOrOneOf(this, 'placement', PLACEMENT_TYPES); } connectedCallback() { - this.validateDelayed(); + this.validatePlacement(); } componentDidLoad() { diff --git a/packages/components/src/components/post-tooltip/readme.md b/packages/components/src/components/post-tooltip/readme.md index 4fd194c2fa..81e87a815a 100644 --- a/packages/components/src/components/post-tooltip/readme.md +++ b/packages/components/src/components/post-tooltip/readme.md @@ -9,7 +9,7 @@ | Property | Attribute | Description | Type | Default | | ----------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `arrow` | `arrow` | Wheter or not to display a little pointer arrow | `boolean` | `true` | +| `arrow` | `arrow` | Whether or not to display a little pointer arrow | `boolean` | `true` | | `delayed` | `delayed` | If `true`, the tooltip is displayed a few milliseconds after it is triggered | `boolean` | `false` | | `placement` | `placement` | Defines the placement of the tooltip according to the floating-ui options available at https://floating-ui.com/docs/computePosition#placement. Tooltips are automatically flipped to the opposite side if there is not enough available space and are shifted towards the viewport if they would overlap edge boundaries. | `"bottom" \| "bottom-end" \| "bottom-start" \| "left" \| "left-end" \| "left-start" \| "right" \| "right-end" \| "right-start" \| "top" \| "top-end" \| "top-start"` | `'top'` | diff --git a/packages/components/src/types/index.ts b/packages/components/src/types/index.ts index d595ecd976..9bc68de2c1 100644 --- a/packages/components/src/types/index.ts +++ b/packages/components/src/types/index.ts @@ -1 +1,2 @@ export * from './heading-levels'; +export * from './placement'; diff --git a/packages/components/src/types/placement.ts b/packages/components/src/types/placement.ts new file mode 100644 index 0000000000..50b33311e4 --- /dev/null +++ b/packages/components/src/types/placement.ts @@ -0,0 +1,14 @@ +export const PLACEMENT_TYPES = [ + 'top', + 'right', + 'bottom', + 'left', + 'top-start', + 'top-end', + 'right-start', + 'right-end', + 'bottom-start', + 'bottom-end', + 'left-start', + 'left-end', +] as const; diff --git a/packages/components/src/utils/property-checkers/check-non-empty.ts b/packages/components/src/utils/property-checkers/check-non-empty.ts index 9df7af2c01..220e45c19c 100644 --- a/packages/components/src/utils/property-checkers/check-non-empty.ts +++ b/packages/components/src/utils/property-checkers/check-non-empty.ts @@ -9,11 +9,12 @@ export function checkNonEmpty( const value = component[prop]; const defaultMessage = `The prop \`${String( prop, - )}\` of the \`${componentName}\` component is required.`; + )}\` of the \`${componentName}\` component is not defined.`; const message = customMessage || defaultMessage; if (EMPTY_VALUES.some(v => v === value)) { - throw new Error(message); + console.error(message); } + return EMPTY_VALUES.some(v => v === value); } diff --git a/packages/components/src/utils/property-checkers/check-one-of.ts b/packages/components/src/utils/property-checkers/check-one-of.ts index c71073eea4..d62c1dd25f 100644 --- a/packages/components/src/utils/property-checkers/check-one-of.ts +++ b/packages/components/src/utils/property-checkers/check-one-of.ts @@ -14,6 +14,6 @@ export function checkOneOf( const message = customMessage || defaultMessage; if (!possibleValues.includes(value)) { - throw new Error(message); + console.error(message); } } diff --git a/packages/components/src/utils/property-checkers/check-pattern.ts b/packages/components/src/utils/property-checkers/check-pattern.ts index a556717628..de7fe7359e 100644 --- a/packages/components/src/utils/property-checkers/check-pattern.ts +++ b/packages/components/src/utils/property-checkers/check-pattern.ts @@ -2,7 +2,7 @@ export function checkPattern( component: T, prop: keyof T, pattern: RegExp, - customMessage: string, + customMessage?: string, ) { const componentName = component.host.localName; const value = component[prop]; @@ -13,6 +13,6 @@ export function checkPattern( const message = customMessage || defaultMessage; if (typeof value !== 'string' || !pattern.test(value)) { - throw new Error(message); + console.error(message); } } diff --git a/packages/components/src/utils/property-checkers/check-type.ts b/packages/components/src/utils/property-checkers/check-type.ts index bf9180578d..7712ba11db 100644 --- a/packages/components/src/utils/property-checkers/check-type.ts +++ b/packages/components/src/utils/property-checkers/check-type.ts @@ -20,9 +20,9 @@ export function checkType( if (typeIsArray || valueIsArray) { if (valueIsArray !== typeIsArray) { - throw new Error(message); + console.error(message); } - } else if (typeof value !== type) { - throw new Error(message); + } else if (typeof value !== type || (typeof value == 'number' && isNaN(value))) { + console.error(message); } } diff --git a/packages/components/src/utils/property-checkers/check-url.ts b/packages/components/src/utils/property-checkers/check-url.ts index 2dad3e435c..76664448ba 100644 --- a/packages/components/src/utils/property-checkers/check-url.ts +++ b/packages/components/src/utils/property-checkers/check-url.ts @@ -5,19 +5,19 @@ export function checkUrl( ) { const componentName = component.host.localName; const value = component[prop]; - const defaultMessage = `The prop \`${String( prop, )}\` of the \`${componentName}\` component is invalid.`; const message = customMessage || defaultMessage; if (typeof value !== 'string' && !(value instanceof URL)) { - throw new Error(message); + console.error(message); + return; } try { new URL(value, 'https://www.post.ch'); } catch { - throw new Error(message); + console.error(message); } } diff --git a/packages/components/src/utils/property-checkers/tests/check-non-empty.spec.ts b/packages/components/src/utils/property-checkers/tests/check-non-empty.spec.ts index a2240ce007..5aca3afe9d 100644 --- a/packages/components/src/utils/property-checkers/tests/check-non-empty.spec.ts +++ b/packages/components/src/utils/property-checkers/tests/check-non-empty.spec.ts @@ -27,18 +27,24 @@ describe('checkNonEmpty', () => { const error = 'Is empty!'; describe('empty value', () => { - it('should not throw an error if the value is a non-empty value', () => { + it('should not log an error if the value is a non-empty value', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); for (const value of NON_EMPTY_VALUES) { const component = { host: { localName: 'post-component' } as HTMLElement, prop: value }; - expect(() => checkNonEmpty(component, 'prop', error)).not.toThrow(); + checkNonEmpty(component, 'prop', error); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(error); } + consoleErrorSpy.mockRestore(); }); - it('should throw an error if the value is an empty value', () => { + it('should log an error if the value is an empty value', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); for (const value of EMPTY_VALUES) { const component = { host: { localName: 'post-component' } as HTMLElement, prop: value }; - expect(() => checkNonEmpty(component, 'prop', error)).toThrow(error); + checkNonEmpty(component, 'prop', error); + expect(consoleErrorSpy).toHaveBeenCalledWith(error); } + consoleErrorSpy.mockRestore(); }); }); }); diff --git a/packages/components/src/utils/property-checkers/tests/check-one-of.spec.ts b/packages/components/src/utils/property-checkers/tests/check-one-of.spec.ts index 1270ea238e..1437e55bf0 100644 --- a/packages/components/src/utils/property-checkers/tests/check-one-of.spec.ts +++ b/packages/components/src/utils/property-checkers/tests/check-one-of.spec.ts @@ -4,16 +4,22 @@ describe('checkOneOf', () => { const possibleValues = ['A', 'B', 'C', 'D']; const error = 'Is not one of.'; - const runCheckForValue = (value: string) => () => { + const runCheckForValue = (value: string) => { const component = { host: { localName: 'post-component' } as HTMLElement, prop: value }; checkOneOf(component, 'prop', possibleValues, error); }; - it('should not throw an error if the value is one of the possible values', () => { - expect(runCheckForValue('A')).not.toThrow(); + it('should not log an error if the value is one of the possible values', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + runCheckForValue('A'); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(error)); + consoleErrorSpy.mockRestore(); }); - it('should throw the provided error if the value is not one of the possible values', () => { - expect(runCheckForValue('E')).toThrow(error); + it('should log the provided error if the value is not one of the possible values', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + runCheckForValue('E'); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); + consoleErrorSpy.mockRestore(); }); }); diff --git a/packages/components/src/utils/property-checkers/tests/check-pattern.spec.ts b/packages/components/src/utils/property-checkers/tests/check-pattern.spec.ts index f7ddeb5f7c..07d7a6858b 100644 --- a/packages/components/src/utils/property-checkers/tests/check-pattern.spec.ts +++ b/packages/components/src/utils/property-checkers/tests/check-pattern.spec.ts @@ -4,16 +4,20 @@ describe('checkPattern', () => { const pattern = /[a-z]{5}/; const error = 'Does not match pattern.'; - const runCheckForValue = (value: unknown) => () => { + const runCheckForValue = (value: unknown) => { const component = { host: { localName: 'post-component' } as HTMLElement, prop: value }; checkPattern(component, 'prop', pattern, error); }; - it('should not throw an error if the value matches the provided pattern', () => { - expect(runCheckForValue('hello')).not.toThrow(); + it('should not log an error if the value matches the provided pattern', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + runCheckForValue('hello'); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(error)); + consoleErrorSpy.mockRestore(); }); - it('should throw the provided error if the value is not a string', () => { + it('should log the provided error if the value is not a string', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ undefined, null, @@ -26,11 +30,16 @@ describe('checkPattern', () => { /* empty */ }, ].forEach(notString => { - expect(runCheckForValue(notString)).toThrow(error); + runCheckForValue(notString); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); - it('should throw the provided error if the value does not match the provided pattern', () => { - expect(runCheckForValue('WORLD')).toThrow(error); + it('should log the provided error if the value does not match the provided pattern', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + runCheckForValue('WORLD'); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); + consoleErrorSpy.mockRestore(); }); }); diff --git a/packages/components/src/utils/property-checkers/tests/check-type.spec.ts b/packages/components/src/utils/property-checkers/tests/check-type.spec.ts index 5d6d8eb955..431b9ff9c5 100644 --- a/packages/components/src/utils/property-checkers/tests/check-type.spec.ts +++ b/packages/components/src/utils/property-checkers/tests/check-type.spec.ts @@ -4,7 +4,7 @@ describe('checkType', () => { let type: PropertyType; let error: string; - const runCheckForValue = (value: unknown) => () => { + const runCheckForValue = (value: unknown) => { const component = { host: { localName: 'post-component' } as HTMLElement, prop: value }; checkType(component, 'prop', type, error); }; @@ -15,13 +15,17 @@ describe('checkType', () => { error = 'Not a boolean.'; }); - it('should not throw an error if the value is a boolean', () => { + it('should not log an error if the value is a boolean', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [true, false].forEach(boolean => { - expect(runCheckForValue(boolean)).not.toThrow(); + runCheckForValue(boolean); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); - it('should throw an error if the value is not a boolean', () => { + it('should log an error if the value is not a boolean', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ undefined, null, @@ -34,8 +38,10 @@ describe('checkType', () => { /* empty */ }, ].forEach(nonBoolean => { - expect(runCheckForValue(nonBoolean)).toThrow(error); + runCheckForValue(nonBoolean); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); }); @@ -45,13 +51,17 @@ describe('checkType', () => { error = 'Not a number.'; }); - it('should not throw an error if the value is a number', () => { - [42, 4.2, 4_200, 2.4434634e9, NaN].forEach(number => { - expect(runCheckForValue(number)).not.toThrow(); + it('should not log an error if the value is a number', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + [42, 4.2, 4_200, 2.4434634e9].forEach(number => { + runCheckForValue(number); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); - it('should throw an error if the value is not a number', () => { + it('should log an error if the value is not a number', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ undefined, null, @@ -63,8 +73,10 @@ describe('checkType', () => { /* empty */ }, ].forEach(nonNumber => { - expect(runCheckForValue(nonNumber)).toThrow(error); + runCheckForValue(nonNumber); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); }); @@ -74,13 +86,17 @@ describe('checkType', () => { error = 'Not a string.'; }); - it('should not throw an error if the value is a string', () => { + it('should not log an error if the value is a string', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); ['', 'string', '42', '¡¡Olé 🙌!!'].forEach(string => { - expect(runCheckForValue(string)).not.toThrow(); + runCheckForValue(string); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); - it('should throw an error if the value is not string', () => { + it('should log an error if the value is not a string', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ undefined, null, @@ -93,8 +109,10 @@ describe('checkType', () => { /* empty */ }, ].forEach(nonString => { - expect(runCheckForValue(nonString)).toThrow(error); + runCheckForValue(nonString); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); }); @@ -104,13 +122,17 @@ describe('checkType', () => { error = 'Not an array.'; }); - it('should not throw an error if the value is an array', () => { + it('should not log an error if the value is an array', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [[], [1, 'a']].forEach(array => { - expect(runCheckForValue(array)).not.toThrow(); + runCheckForValue(array); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); - it('should throw an error if the value is not an array', () => { + it('should log an error if the value is not an array', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ undefined, null, @@ -123,8 +145,10 @@ describe('checkType', () => { /* empty */ }, ].forEach(nonArray => { - expect(runCheckForValue(nonArray)).toThrow(error); + runCheckForValue(nonArray); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); }); @@ -134,13 +158,17 @@ describe('checkType', () => { error = 'Not an object.'; }); - it('should not throw an error if the value is an object', () => { + it('should not log an error if the value is an object', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [null, {}].forEach(object => { - expect(runCheckForValue(object)).not.toThrow(); + runCheckForValue(object); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); - it('should throw an error if the value is not an object', () => { + it('should log an error if the value is not an object', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ undefined, true, @@ -151,8 +179,10 @@ describe('checkType', () => { /* empty */ }, ].forEach(nonObject => { - expect(runCheckForValue(nonObject)).toThrow(error); + runCheckForValue(nonObject); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); }); @@ -162,7 +192,8 @@ describe('checkType', () => { error = 'Not a function.'; }); - it('should not throw an error if the value is a function', () => { + it('should not log an error if the value is a function', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ function () { /* empty */ @@ -171,14 +202,19 @@ describe('checkType', () => { /* empty */ }, ].forEach(fn => { - expect(runCheckForValue(fn)).not.toThrow(); + runCheckForValue(fn); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); - it('should throw an error if the value is not a function', () => { + it('should log an error if the value is not a function', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [undefined, null, true, 42, NaN, 'string', [], {}].forEach(nonFn => { - expect(runCheckForValue(nonFn)).toThrow(error); + runCheckForValue(nonFn); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(error)); }); + consoleErrorSpy.mockRestore(); }); }); }); diff --git a/packages/components/src/utils/property-checkers/tests/check-url.spec.ts b/packages/components/src/utils/property-checkers/tests/check-url.spec.ts index 0f5f5cb3ab..cdbe12255c 100644 --- a/packages/components/src/utils/property-checkers/tests/check-url.spec.ts +++ b/packages/components/src/utils/property-checkers/tests/check-url.spec.ts @@ -3,7 +3,8 @@ import { checkUrl } from '../check-url'; describe('checkUrl', () => { const errorMessage = 'Invalid URL'; - test('should not throw an error if the value is an URL string or an URL object', () => { + test('should not log an error if the value is an URL string or an URL object', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ 'https://www.example.com', new URL('https://www.example.com'), @@ -15,11 +16,14 @@ describe('checkUrl', () => { 'localhost:3000', ].forEach(validUrl => { const component = { host: { localName: 'post-component' } as HTMLElement, prop: validUrl }; - expect(() => checkUrl(component, 'prop', errorMessage)).not.toThrow(); + checkUrl(component, 'prop', errorMessage); + expect(consoleErrorSpy).not.toHaveBeenCalledWith(expect.stringContaining(errorMessage)); }); + consoleErrorSpy.mockRestore(); }); - test('should throw an error if the value is not an URL string or an URL object', () => { + test('should log an error if the value is not an URL string or an URL object', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); [ 123, true, @@ -31,7 +35,9 @@ describe('checkUrl', () => { ].forEach(invalidUrl => { const component = { host: { localName: 'post-component' } as HTMLElement, prop: invalidUrl }; // Type casting because we know that these are not valid arguments, it's just for testing - expect(() => checkUrl(component, 'prop', errorMessage)).toThrow(errorMessage); + checkUrl(component, 'prop', errorMessage); + expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining(errorMessage)); }); + consoleErrorSpy.mockRestore(); }); }); diff --git a/packages/documentation/src/stories/components/back-to-top/back-to-top.stories.ts b/packages/documentation/src/stories/components/back-to-top/back-to-top.stories.ts index 931351f537..0e1efccb0f 100644 --- a/packages/documentation/src/stories/components/back-to-top/back-to-top.stories.ts +++ b/packages/documentation/src/stories/components/back-to-top/back-to-top.stories.ts @@ -9,7 +9,6 @@ const meta: MetaComponent = { component: 'post-back-to-top', tags: ['package:WebComponents'], parameters: { - layout: 'fullscreen', design: { type: 'figma', url: 'https://www.figma.com/design/JIT5AdGYqv6bDRpfBPV8XR/Foundations-%26-Components-Next-Level?node-id=18-11', diff --git a/packages/documentation/src/stories/components/card-control/standard-html/card-control.stories.ts b/packages/documentation/src/stories/components/card-control/standard-html/card-control.stories.ts index a612eba497..1ce9b6b2f9 100644 --- a/packages/documentation/src/stories/components/card-control/standard-html/card-control.stories.ts +++ b/packages/documentation/src/stories/components/card-control/standard-html/card-control.stories.ts @@ -146,6 +146,7 @@ export const Default = { const cardClasses = mapClasses({ 'checked': args.checked, 'disabled': args.disabled, + 'is-valid': args.validation === 'is-valid', 'is-invalid': args.validation === 'is-invalid', 'checkbox-button-card': args.type === 'checkbox', 'radio-button-card': args.type === 'radio', diff --git a/packages/documentation/src/stories/components/popover/popover.stories.ts b/packages/documentation/src/stories/components/popover/popover.stories.ts index 423cd3f824..cd8fd9c830 100644 --- a/packages/documentation/src/stories/components/popover/popover.stories.ts +++ b/packages/documentation/src/stories/components/popover/popover.stories.ts @@ -53,7 +53,7 @@ const meta: MetaComponent = { 'Value can either be in `vw`, `px` or `%`. If no max-width is defined, the popover will extend to the width of its content.', table: { category: 'General', - defaultValue: { summary: '280px' } + defaultValue: { summary: '280px' }, }, }, palette: { @@ -104,6 +104,7 @@ function render(args: Args) { class="${args.palette}" id="${args.id}" placement="${args.placement}" + close-button-caption="${args.closeButtonCaption}" ?arrow="${args.arrow}" style="${args.maxWidth ? '--post-popover-max-width: ' + args.maxWidth : ''}" > diff --git a/packages/styles/src/variables/components/_tag.scss b/packages/styles/src/variables/components/_tag.scss index 5bef1bf8c8..e9684772ab 100644 --- a/packages/styles/src/variables/components/_tag.scss +++ b/packages/styles/src/variables/components/_tag.scss @@ -21,9 +21,9 @@ $tag-sm-icon-size: $tag-font-size; $tag-default-background: color.$gray-10; $tag-backgrounds: ( 'white': color.$white, - 'yellow': color.$yellow, + 'info': color.$info, 'success': color.$success, + 'error': color.$error, 'warning': color.$warning, - 'danger': color.$error, - 'info': color.$info, + 'yellow': color.$yellow, );