diff --git a/components/empty-state/empty-state-action-button.js b/components/empty-state/empty-state-action-button.js index ae70f9f12ff..624756542a2 100644 --- a/components/empty-state/empty-state-action-button.js +++ b/components/empty-state/empty-state-action-button.js @@ -2,13 +2,14 @@ import '../button/button.js'; import '../button/button-subtle.js'; import { css, html, LitElement, nothing } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; +import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js'; /** * `d2l-empty-state-action-button` is an empty state action component that can be placed inside of the default slot of `empty-state-simple` or `empty-state-illustrated` to add a button action to the component. * @fires d2l-empty-state-action - Dispatched when the action button is clicked * @fires d2l-empty-state-illustrated-check - Internal event */ -class EmptyStateActionButton extends LitElement { +class EmptyStateActionButton extends PropertyRequiredMixin(LitElement) { static get properties() { return { @@ -16,7 +17,7 @@ class EmptyStateActionButton extends LitElement { * REQUIRED: The action text to be used in the button * @type {string} */ - text: { type: String }, + text: { type: String, required: true }, /** * This will change the action button to use a primary button instead of the default subtle button. The primary attribute is only valid when `d2l-empty-state-action-button` is placed within `d2l-empty-state-illustrated` components * @type {boolean} @@ -37,8 +38,6 @@ class EmptyStateActionButton extends LitElement { constructor() { super(); this._illustrated = false; - this._missingTextErrorHasBeenThrown = false; - this._validatingTextTimeout = null; } connectedCallback() { @@ -53,11 +52,6 @@ class EmptyStateActionButton extends LitElement { }); } - firstUpdated(changedProperties) { - super.firstUpdated(changedProperties); - this._validateText(); - } - render() { let actionButton = nothing; if (this.text) { @@ -84,20 +78,6 @@ class EmptyStateActionButton extends LitElement { this.dispatchEvent(new CustomEvent('d2l-empty-state-action')); } - _validateText() { - clearTimeout(this._validatingTextTimeout); - // don't error immediately in case it doesn't get set immediately - this._validatingTextTimeout = setTimeout(() => { - this._validatingTextTimeout = null; - const hasText = (typeof this.text === 'string') && this.text.length > 0; - - if (!hasText && !this._missingTextErrorHasBeenThrown) { - this._missingTextErrorHasBeenThrown = true; - setTimeout(() => { throw new Error(': missing required "text" attribute.'); }); - } - }, 3000); - } - } customElements.define('d2l-empty-state-action-button', EmptyStateActionButton); diff --git a/components/empty-state/empty-state-action-link.js b/components/empty-state/empty-state-action-link.js index 96b080129b7..c2471843b16 100644 --- a/components/empty-state/empty-state-action-link.js +++ b/components/empty-state/empty-state-action-link.js @@ -1,11 +1,12 @@ import { html, LitElement, nothing } from 'lit'; import { bodyCompactStyles } from '../typography/styles.js'; import { linkStyles } from '../link/link.js'; +import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js'; /** * `d2l-empty-state-action-link` is an empty state action component that can be placed inside of the default slot of `empty-state-simple` or `empty-state-illustrated` to add a link action to the component. */ -class EmptyStateActionLink extends LitElement { +class EmptyStateActionLink extends PropertyRequiredMixin(LitElement) { static get properties() { return { @@ -13,12 +14,12 @@ class EmptyStateActionLink extends LitElement { * REQUIRED: The action text to be used in the subtle button * @type {string} */ - text: { type: String }, + text: { type: String, required: true }, /** * REQUIRED: The action URL or URL fragment of the link * @type {string} */ - href: { type: String }, + href: { type: String, required: true }, }; } @@ -26,18 +27,6 @@ class EmptyStateActionLink extends LitElement { return [bodyCompactStyles, linkStyles]; } - constructor() { - super(); - this._missingHrefErrorHasBeenThrown = false; - this._missingTextErrorHasBeenThrown = false; - this._validatingAttributesTimeout = null; - } - - firstUpdated(changedProperties) { - super.firstUpdated(changedProperties); - this._validateAttributes(); - } - render() { const actionLink = this.text && this.href ? html` @@ -47,27 +36,6 @@ class EmptyStateActionLink extends LitElement { return html`${actionLink}`; } - _validateAttributes() { - clearTimeout(this._validatingAttributesTimeout); - // don't error immediately in case it doesn't get set immediately - this._validatingAttributesTimeout = setTimeout(() => { - this._validatingAttributesTimeout = null; - - const hasHref = (typeof this.href === 'string') && this.href.length > 0; - const hasText = (typeof this.text === 'string') && this.text.length > 0; - - if (!hasHref && !this._missingHrefErrorHasBeenThrown) { - this._missingHrefErrorHasBeenThrown = true; - setTimeout(() => { throw new Error(': missing required "href" attribute.'); }); - } - - if (!hasText && !this._missingTextErrorHasBeenThrown) { - this._missingTextErrorHasBeenThrown = true; - setTimeout(() => { throw new Error(': missing required "text" attribute.'); }); - } - }, 3000); - } - } customElements.define('d2l-empty-state-action-link', EmptyStateActionLink); diff --git a/components/empty-state/empty-state-illustrated.js b/components/empty-state/empty-state-illustrated.js index 6c5e4e8365f..93109078d72 100644 --- a/components/empty-state/empty-state-illustrated.js +++ b/components/empty-state/empty-state-illustrated.js @@ -3,6 +3,7 @@ import { html, LitElement, nothing } from 'lit'; import { bodyCompactStyles } from '../typography/styles.js'; import { classMap } from 'lit/directives/class-map.js'; import { loadSvg } from '../../generated/empty-state/presetIllustrationLoader.js'; +import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js'; import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js'; import { runAsync } from '../../directives/run-async/run-async.js'; import { styleMap } from 'lit/directives/style-map.js'; @@ -15,7 +16,7 @@ const illustrationAspectRatio = 500 / 330; * @slot - Slot for empty state actions * @slot illustration - Slot for custom SVG content if `illustration-name` property is not set */ -class EmptyStateIllustrated extends LitElement { +class EmptyStateIllustrated extends PropertyRequiredMixin(LitElement) { static get properties() { return { @@ -23,7 +24,7 @@ class EmptyStateIllustrated extends LitElement { * REQUIRED: A description giving details about the empty state * @type {string} */ - description: { type: String }, + description: { type: String, required: true }, /** * The name of the preset image you would like to display in the component * @type {string} @@ -33,7 +34,7 @@ class EmptyStateIllustrated extends LitElement { * REQUIRED: A title for the empty state * @type {string} */ - titleText: { type: String, attribute: 'title-text' }, + titleText: { type: String, attribute: 'title-text', required: true }, _contentHeight: { state: true }, _titleSmall: { state: true } }; @@ -46,11 +47,8 @@ class EmptyStateIllustrated extends LitElement { constructor() { super(); this._contentHeight = 330; - this._missingDescriptionErrorHasBeenThrown = false; - this._missingTitleTextErrorHasBeenThrown = false; this._resizeObserver = new ResizeObserver(this._onResize.bind(this)); this._titleSmall = false; - this._validatingAttributesTimeout = null; } connectedCallback() { @@ -65,11 +63,6 @@ class EmptyStateIllustrated extends LitElement { this._resizeObserver.disconnect(); } - firstUpdated(changedProperties) { - super.firstUpdated(changedProperties); - this._validateAttributes(); - } - render() { const illustrationContainerStyle = this._getIllustrationContainerStyle(); const titleClass = this._getTitleClass(); @@ -126,30 +119,6 @@ class EmptyStateIllustrated extends LitElement { }); } - _validateAttributes() { - clearTimeout(this._validatingAttributesTimeout); - // don't error immediately in case it doesn't get set immediately - this._validatingAttributesTimeout = setTimeout(() => { - this._validatingAttributesTimeout = null; - const hasTitleText = (typeof this.titleText === 'string') && this.titleText.length > 0; - const hasDescription = (typeof this.description === 'string') && this.description.length > 0; - - if (!hasTitleText && !this._missingTitleTextErrorHasBeenThrown) { - this._missingTitleTextErrorHasBeenThrown = true; - setTimeout(() => { - throw new Error(': missing required "titleText" attribute.'); - }); - } - - if (!hasDescription && !this._missingDescriptionErrorHasBeenThrown) { - this._missingDescriptionErrorHasBeenThrown = true; - setTimeout(() => { - throw new Error(': missing required "description" attribute.'); - }); - } - }, 3000); - } - } customElements.define('d2l-empty-state-illustrated', EmptyStateIllustrated); diff --git a/components/empty-state/empty-state-simple.js b/components/empty-state/empty-state-simple.js index f1e5c8337aa..621a9881458 100644 --- a/components/empty-state/empty-state-simple.js +++ b/components/empty-state/empty-state-simple.js @@ -2,13 +2,14 @@ import '../button/button-subtle.js'; import { emptyStateSimpleStyles, emptyStateStyles } from './empty-state-styles.js'; import { html, LitElement } from 'lit'; import { bodyCompactStyles } from '../typography/styles.js'; +import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js'; import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js'; /** * The `d2l-empty-state-simple` component is an empty state component that displays a description. An empty state action component can be placed inside of the default slot to add an optional action. * @slot - Slot for empty state actions */ -class EmptyStateSimple extends RtlMixin(LitElement) { +class EmptyStateSimple extends PropertyRequiredMixin(RtlMixin(LitElement)) { static get properties() { return { @@ -16,7 +17,7 @@ class EmptyStateSimple extends RtlMixin(LitElement) { * REQUIRED: A description giving details about the empty state * @type {string} */ - description: { type: String }, + description: { type: String, required: true }, }; } @@ -24,17 +25,6 @@ class EmptyStateSimple extends RtlMixin(LitElement) { return [bodyCompactStyles, emptyStateStyles, emptyStateSimpleStyles]; } - constructor() { - super(); - this._missingDescriptionErrorHasBeenThrown = false; - this._validatingDescriptionTimeout = null; - } - - firstUpdated(changedProperties) { - super.firstUpdated(changedProperties); - this._validateDescription(); - } - render() { return html`

${this.description}

@@ -42,20 +32,6 @@ class EmptyStateSimple extends RtlMixin(LitElement) { `; } - _validateDescription() { - clearTimeout(this._validatingDescriptionTimeout); - // don't error immediately in case it doesn't get set immediately - this._validatingDescriptionTimeout = setTimeout(() => { - this._validatingDescriptionTimeout = null; - const hasDescription = (typeof this.description === 'string') && this.description.length > 0; - - if (!hasDescription && !this._missingDescriptionErrorHasBeenThrown) { - this._missingDescriptionErrorHasBeenThrown = true; - setTimeout(() => { throw new Error(': missing required "description" attribute.'); }); - } - }, 3000); - } - } customElements.define('d2l-empty-state-simple', EmptyStateSimple); diff --git a/components/inputs/input-text.js b/components/inputs/input-text.js index 80bc1d77e49..2343dad36e6 100644 --- a/components/inputs/input-text.js +++ b/components/inputs/input-text.js @@ -12,6 +12,7 @@ import { inputStyles } from './input-styles.js'; import { LabelledMixin } from '../../mixins/labelled/labelled-mixin.js'; import { offscreenStyles } from '../offscreen/offscreen.js'; import { PerfMonitor } from '../../helpers/perfMonitor.js'; +import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js'; import { RtlMixin } from '../../mixins/rtl/rtl-mixin.js'; import { SkeletonMixin } from '../skeleton/skeleton-mixin.js'; import { styleMap } from 'lit/directives/style-map.js'; @@ -24,7 +25,7 @@ import { styleMap } from 'lit/directives/style-map.js'; * @fires change - Dispatched when an alteration to the value is committed (typically after focus is lost) by the user * @fires input - Dispatched immediately after changes by the user */ -class InputText extends FocusMixin(LabelledMixin(FormElementMixin(SkeletonMixin(RtlMixin(LitElement))))) { +class InputText extends PropertyRequiredMixin(FocusMixin(LabelledMixin(FormElementMixin(SkeletonMixin(RtlMixin(LitElement)))))) { static get properties() { return { @@ -152,7 +153,18 @@ class InputText extends FocusMixin(LabelledMixin(FormElementMixin(SkeletonMixin( * Accessible label for the unit which will not be visually rendered * @type {string} */ - unitLabel: { attribute: 'unit-label', type: String }, + unitLabel: { + attribute: 'unit-label', + required: { + dependentProps: ['unit'], + message: (_value, elem) => `: missing required attribute "unit-label" for unit "${elem.unit}"`, + validator: (_value, elem, hasValue) => { + const hasUnit = (typeof elem.unit === 'string') && elem.unit.length > 0; + return !(hasUnit && elem.unit !== '%' && !hasValue); + } + }, + type: String + }, /** * Value of the input * @type {string} @@ -270,7 +282,6 @@ class InputText extends FocusMixin(LabelledMixin(FormElementMixin(SkeletonMixin( this._intersectionObserver = null; this._isIntersecting = false; this._lastSlotWidth = 0; - this._missingUnitLabelErrorHasBeenThrown = false; this._prevValue = ''; this._handleBlur = this._handleBlur.bind(this); @@ -278,7 +289,6 @@ class InputText extends FocusMixin(LabelledMixin(FormElementMixin(SkeletonMixin( this._handleMouseEnter = this._handleMouseEnter.bind(this); this._handleMouseLeave = this._handleMouseLeave.bind(this); this._perfMonitor = new PerfMonitor(this); - this._validatingUnitTimeout = null; } get value() { return this._value; } @@ -355,7 +365,6 @@ class InputText extends FocusMixin(LabelledMixin(FormElementMixin(SkeletonMixin( super.firstUpdated(changedProperties); this._setValue(this.value, true); - this._validateUnit(); const container = this.shadowRoot && this.shadowRoot.querySelector('.d2l-input-text-container'); if (!container) return; @@ -484,7 +493,6 @@ class InputText extends FocusMixin(LabelledMixin(FormElementMixin(SkeletonMixin( changedProperties.forEach((oldVal, prop) => { if (prop === 'unit' || prop === 'unitLabel') { this._updateInputLayout(); - this._validateUnit(); } else if (prop === 'validationError') { if (oldVal && this.validationError) { const tooltip = this.shadowRoot.querySelector('d2l-tooltip'); @@ -655,20 +663,5 @@ class InputText extends FocusMixin(LabelledMixin(FormElementMixin(SkeletonMixin( } - _validateUnit() { - if (this._missingUnitLabelErrorHasBeenThrown) return; - clearTimeout(this._validatingUnitTimeout); - // don't error immediately in case it doesn't get set immediately - this._validatingUnitTimeout = setTimeout(() => { - this._validatingUnitTimeout = null; - const hasUnit = (typeof this.unit === 'string') && this.unit.length > 0; - const hasUnitLabel = (typeof this.unitLabel === 'string') && this.unitLabel.length > 0; - if (hasUnit && this.unit !== '%' && !hasUnitLabel) { - this._missingUnitLabelErrorHasBeenThrown = true; - throw new Error(`: missing required attribute "unit-label" for unit "${this.unit}"`); - } - }, 3000); - } - } customElements.define('d2l-input-text', InputText);