From 85d3eb1570eb4e7be98641c2d13212a36d9c6122 Mon Sep 17 00:00:00 2001 From: Chris Sauve Date: Sun, 22 Sep 2024 18:07:20 -0400 Subject: [PATCH] Improve clips property setters --- .changeset/neat-terms-smash.md | 5 ++ .../clips/source/elements/ClipsElement.ts | 37 +++++++++ packages/clips/source/elements/Grid/Grid.ts | 76 ++++++++++++------ packages/clips/source/elements/Image/Image.ts | 22 +++--- packages/clips/source/elements/Modal/Modal.ts | 33 ++++---- .../elements/SkeletonText/SkeletonText.ts | 29 ++++--- packages/clips/source/elements/Stack/Stack.ts | 19 +++-- packages/clips/source/elements/Text/Text.ts | 33 ++++---- .../source/elements/TextField/TextField.ts | 30 ++++---- packages/clips/source/elements/View/View.ts | 77 +++++++++++-------- 10 files changed, 238 insertions(+), 123 deletions(-) create mode 100644 .changeset/neat-terms-smash.md diff --git a/.changeset/neat-terms-smash.md b/.changeset/neat-terms-smash.md new file mode 100644 index 00000000..fcd54564 --- /dev/null +++ b/.changeset/neat-terms-smash.md @@ -0,0 +1,5 @@ +--- +'@watching/clips': patch +--- + +Improve clips property setters diff --git a/packages/clips/source/elements/ClipsElement.ts b/packages/clips/source/elements/ClipsElement.ts index c2faa21e..f3ee576b 100644 --- a/packages/clips/source/elements/ClipsElement.ts +++ b/packages/clips/source/elements/ClipsElement.ts @@ -10,6 +10,43 @@ export class ClipsElement< readonly __attributes?: Attributes; } +export function formatAttributeValue( + value: string | boolean | null | undefined, + options: {truthy?: NoInfer; false?: NoInfer; allowed: Set}, +) { + if (value === true || value === '') return options.truthy; + if (value === false) return options.false; + if (value == null) return undefined; + return restrictToAllowedValues(value, options.allowed); +} + +export function formatAutoAttributeValue( + value: string | boolean | null | undefined, + options: {truthy?: NoInfer; false?: NoInfer; allowed: Set}, +) { + return formatAttributeValue(value, { + truthy: 'auto' as T, + ...options, + }); +} + +export function formatAutoOrNoneAttributeValue( + value: string | boolean | null | undefined, + options: {truthy?: NoInfer; allowed: Set}, +) { + return formatAutoAttributeValue(value, { + ...options, + false: 'none' as T, + }); +} + +export type AttributeValueAsPropertySetter = + | T + | '' + | boolean + | null + | undefined; + export function isAllowedValue( value: string | null, allowed: Set, diff --git a/packages/clips/source/elements/Grid/Grid.ts b/packages/clips/source/elements/Grid/Grid.ts index 0667d906..e04ceceb 100644 --- a/packages/clips/source/elements/Grid/Grid.ts +++ b/packages/clips/source/elements/Grid/Grid.ts @@ -10,9 +10,10 @@ import { } from '@watching/design'; import { - attributeRestrictedToAllowedValues, backedByAttribute, - restrictToAllowedValues, + attributeRestrictedToAllowedValues, + formatAutoOrNoneAttributeValue, + type AttributeValueAsPropertySetter, } from '../ClipsElement.ts'; import { @@ -49,6 +50,8 @@ export interface GridProperties extends ViewProperties { export interface GridEvents extends ViewEvents {} +const DEFAULT_SPACING_VALUE = 'none'; + /** * A `Grid` is a container component that lays out sibling elements * using predetermined sizes. First, children are laid out along the @@ -90,37 +93,66 @@ export class Grid< get spacing(): SpacingKeyword { return ( - restrictToAllowedValues(this.getAttribute('spacing'), SPACING_KEYWORDS) ?? - 'none' + formatAutoOrNoneAttributeValue(this.getAttribute('spacing'), { + allowed: SPACING_KEYWORDS, + }) ?? DEFAULT_SPACING_VALUE ); } - set spacing(value: SpacingKeyword | boolean) { + set spacing(value: AttributeValueAsPropertySetter) { const resolvedValue = - value === true - ? 'auto' - : value === false || value == null - ? 'none' - : restrictToAllowedValues(value, SPACING_KEYWORDS); + formatAutoOrNoneAttributeValue(value, { + allowed: SPACING_KEYWORDS, + }) ?? DEFAULT_SPACING_VALUE; - if (resolvedValue === 'none') { + if (resolvedValue === DEFAULT_SPACING_VALUE) { this.removeAttribute('spacing'); - } else if (resolvedValue) { + } else { this.setAttribute('spacing', resolvedValue); } } - @backedByAttribute({ - name: 'inline-spacing', - ...attributeRestrictedToAllowedValues(SPACING_KEYWORDS), - }) - accessor inlineSpacing: SpacingKeyword | undefined; + get inlineSpacing(): SpacingKeyword { + return ( + formatAutoOrNoneAttributeValue(this.getAttribute('inline-spacing'), { + allowed: SPACING_KEYWORDS, + }) ?? DEFAULT_SPACING_VALUE + ); + } - @backedByAttribute({ - name: 'block-spacing', - ...attributeRestrictedToAllowedValues(SPACING_KEYWORDS), - }) - accessor blockSpacing: SpacingKeyword | undefined; + set inlineSpacing(value: AttributeValueAsPropertySetter) { + const resolvedValue = + formatAutoOrNoneAttributeValue(value, { + allowed: SPACING_KEYWORDS, + }); + + if (resolvedValue == null) { + this.removeAttribute('inline-spacing'); + } else { + this.setAttribute('inline-spacing', resolvedValue); + } + } + + get blockSpacing(): SpacingKeyword { + return ( + formatAutoOrNoneAttributeValue(this.getAttribute('block-spacing'), { + allowed: SPACING_KEYWORDS, + }) ?? DEFAULT_SPACING_VALUE + ); + } + + set blockSpacing(value: AttributeValueAsPropertySetter) { + const resolvedValue = + formatAutoOrNoneAttributeValue(value, { + allowed: SPACING_KEYWORDS, + }); + + if (resolvedValue == null) { + this.removeAttribute('block-spacing'); + } else { + this.setAttribute('block-spacing', resolvedValue); + } + } @backedByAttribute({ ...attributeRestrictedToAllowedValues(DIRECTION_KEYWORDS), diff --git a/packages/clips/source/elements/Image/Image.ts b/packages/clips/source/elements/Image/Image.ts index 4633fe61..99c2885a 100644 --- a/packages/clips/source/elements/Image/Image.ts +++ b/packages/clips/source/elements/Image/Image.ts @@ -1,8 +1,9 @@ import { ClipsElement, backedByAttribute, + formatAutoOrNoneAttributeValue, attributeRestrictedToAllowedValues, - restrictToAllowedValues, + type AttributeValueAsPropertySetter, } from '../ClipsElement.ts'; import { CORNER_RADIUS_KEYWORDS, @@ -97,6 +98,8 @@ export interface ImageProperties { export interface ImageEvents {} +const DEFAULT_CORNER_RADIUS_VALUE = 'none'; + /** * Image is used to visually style and provide semantic value for a small piece of image * content. @@ -155,20 +158,17 @@ export class Image get cornerRadius(): CornerRadiusKeyword { return ( - restrictToAllowedValues( - this.getAttribute('corner-radius'), - CORNER_RADIUS_KEYWORDS, - ) ?? 'none' + formatAutoOrNoneAttributeValue(this.getAttribute('corner-radius'), { + allowed: CORNER_RADIUS_KEYWORDS, + }) ?? DEFAULT_CORNER_RADIUS_VALUE ); } - set cornerRadius(value: CornerRadiusKeyword | boolean) { + set cornerRadius(value: AttributeValueAsPropertySetter) { const resolvedValue = - value === true - ? 'auto' - : value === false || value == null - ? 'none' - : restrictToAllowedValues(value, CORNER_RADIUS_KEYWORDS); + formatAutoOrNoneAttributeValue(value, { + allowed: CORNER_RADIUS_KEYWORDS, + }) ?? DEFAULT_CORNER_RADIUS_VALUE; if (resolvedValue === 'none') { this.removeAttribute('corner-radius'); diff --git a/packages/clips/source/elements/Modal/Modal.ts b/packages/clips/source/elements/Modal/Modal.ts index 8e527a3c..2e5606d5 100644 --- a/packages/clips/source/elements/Modal/Modal.ts +++ b/packages/clips/source/elements/Modal/Modal.ts @@ -2,7 +2,11 @@ import { SPACING_OR_NONE_KEYWORDS, type SpacingOrNoneKeyword, } from '@watching/design'; -import {ClipsElement, restrictToAllowedValues} from '../ClipsElement.ts'; +import { + ClipsElement, + formatAutoOrNoneAttributeValue, + type AttributeValueAsPropertySetter, +} from '../ClipsElement.ts'; export interface ModalAttributes { /** @@ -25,6 +29,8 @@ export interface ModalProperties { export interface ModalEvents {} +const DEFAULT_PADDING_VALUE = 'none'; + /** * A Modal is an overlay that blocks interaction with the rest of the page. The * user must take an action to dismiss the modal, either by pressing on the backdrop, @@ -44,23 +50,22 @@ export class Modal get padding(): SpacingOrNoneKeyword { return ( - restrictToAllowedValues( - this.getAttribute('padding'), - SPACING_OR_NONE_KEYWORDS, - ) ?? 'none' + formatAutoOrNoneAttributeValue(this.getAttribute('padding'), { + allowed: SPACING_OR_NONE_KEYWORDS, + }) ?? DEFAULT_PADDING_VALUE ); } - set padding(value: SpacingOrNoneKeyword | boolean | undefined) { - if (value == null || value === 'none' || value === false) { - this.removeAttribute('padding'); - } else { - const resolvedValue = - value === true - ? 'auto' - : restrictToAllowedValues(value, SPACING_OR_NONE_KEYWORDS); + set padding(value: AttributeValueAsPropertySetter) { + const resolvedValue = + formatAutoOrNoneAttributeValue(value, { + allowed: SPACING_OR_NONE_KEYWORDS, + }) ?? DEFAULT_PADDING_VALUE; - if (resolvedValue) this.setAttribute('padding', resolvedValue); + if (resolvedValue === 'none') { + this.removeAttribute('padding'); + } else if (resolvedValue) { + this.setAttribute('padding', resolvedValue); } } } diff --git a/packages/clips/source/elements/SkeletonText/SkeletonText.ts b/packages/clips/source/elements/SkeletonText/SkeletonText.ts index 0c62a062..d39cbd30 100644 --- a/packages/clips/source/elements/SkeletonText/SkeletonText.ts +++ b/packages/clips/source/elements/SkeletonText/SkeletonText.ts @@ -9,7 +9,9 @@ import type {CSSLiteralValue} from '../../styles.ts'; import { ClipsElement, backedByAttribute, + formatAutoAttributeValue, restrictToAllowedValues, + type AttributeValueAsPropertySetter, } from '../ClipsElement.ts'; export interface SkeletonTextAttributes { @@ -42,6 +44,8 @@ export interface SkeletonTextProperties { export interface SkeletonTextEvents {} +const DEFAULT_EMPHASIS_VALUE = 'auto'; + /** * Text is used to visually style and provide semantic value for a small piece of text * content. @@ -64,23 +68,24 @@ export class SkeletonText */ get emphasis(): TextEmphasisKeyword { return ( - restrictToAllowedValues( - this.getAttribute('emphasis'), - TEXT_EMPHASIS_KEYWORDS, - ) ?? 'auto' + formatAutoAttributeValue(this.getAttribute('emphasis'), { + allowed: TEXT_EMPHASIS_KEYWORDS, + }) ?? DEFAULT_EMPHASIS_VALUE ); } - set emphasis(value: TextEmphasisKeyword | boolean | undefined) { - if (value == null || value === false) { + set emphasis(value: AttributeValueAsPropertySetter) { + const resolvedValue = + formatAutoAttributeValue(value, { + allowed: TEXT_EMPHASIS_KEYWORDS, + truthy: 'strong', + false: 'auto', + }) ?? DEFAULT_EMPHASIS_VALUE; + + if (resolvedValue === DEFAULT_EMPHASIS_VALUE) { this.removeAttribute('emphasis'); } else { - const resolvedValue = - value === true - ? 'strong' - : restrictToAllowedValues(value, TEXT_EMPHASIS_KEYWORDS); - - if (resolvedValue) this.setAttribute('emphasis', resolvedValue); + this.setAttribute('emphasis', resolvedValue); } } diff --git a/packages/clips/source/elements/Stack/Stack.ts b/packages/clips/source/elements/Stack/Stack.ts index ef24a2e9..214a5e5e 100644 --- a/packages/clips/source/elements/Stack/Stack.ts +++ b/packages/clips/source/elements/Stack/Stack.ts @@ -10,9 +10,9 @@ import { } from '@watching/design'; import { - attributeRestrictedToAllowedValues, backedByAttribute, - restrictToAllowedValues, + attributeRestrictedToAllowedValues, + formatAutoOrNoneAttributeValue, } from '../ClipsElement.ts'; import { @@ -41,6 +41,8 @@ export interface StackProperties extends ViewProperties { export interface StackEvents extends ViewEvents {} +const DEFAULT_SPACING_VALUE = 'none'; + /** * A `Stack` is a container component that lays out sibling elements * beside one another. Children of a `Stack` keep their intrinsic size, @@ -79,16 +81,19 @@ export class Stack< get spacing(): SpacingKeyword { return ( - restrictToAllowedValues(this.getAttribute('spacing'), SPACING_KEYWORDS) ?? - 'none' + formatAutoOrNoneAttributeValue(this.getAttribute('spacing'), { + allowed: SPACING_KEYWORDS, + }) ?? DEFAULT_SPACING_VALUE ); } - set spacing(value: SpacingKeyword | boolean) { + set spacing(value: SpacingKeyword | '' | boolean | null | undefined) { const resolvedValue = - value === true ? 'auto' : value === false ? 'none' : value; + formatAutoOrNoneAttributeValue(value, { + allowed: SPACING_KEYWORDS, + }) ?? DEFAULT_SPACING_VALUE; - if (resolvedValue === 'none') { + if (resolvedValue === DEFAULT_SPACING_VALUE) { this.removeAttribute('spacing'); } else { this.setAttribute('spacing', resolvedValue); diff --git a/packages/clips/source/elements/Text/Text.ts b/packages/clips/source/elements/Text/Text.ts index 8b6a22fc..2e33f0bd 100644 --- a/packages/clips/source/elements/Text/Text.ts +++ b/packages/clips/source/elements/Text/Text.ts @@ -3,7 +3,11 @@ import { type TextEmphasisKeyword, } from '@watching/design'; -import {ClipsElement, restrictToAllowedValues} from '../ClipsElement.ts'; +import { + ClipsElement, + formatAutoAttributeValue, + type AttributeValueAsPropertySetter, +} from '../ClipsElement.ts'; export interface TextAttributes { /** @@ -23,6 +27,8 @@ export interface TextProperties { export interface TextEvents {} +const DEFAULT_EMPHASIS_VALUE = 'auto'; + /** * Text is used to visually style and provide semantic value for a small piece of text * content. @@ -45,23 +51,24 @@ export class Text */ get emphasis(): TextEmphasisKeyword { return ( - restrictToAllowedValues( - this.getAttribute('emphasis'), - TEXT_EMPHASIS_KEYWORDS, - ) ?? 'auto' + formatAutoAttributeValue(this.getAttribute('emphasis'), { + allowed: TEXT_EMPHASIS_KEYWORDS, + }) ?? DEFAULT_EMPHASIS_VALUE ); } - set emphasis(value: TextEmphasisKeyword | boolean | undefined) { - if (value == null || value === false) { + set emphasis(value: AttributeValueAsPropertySetter) { + const resolvedValue = + formatAutoAttributeValue(value, { + allowed: TEXT_EMPHASIS_KEYWORDS, + truthy: 'strong', + false: 'auto', + }) ?? DEFAULT_EMPHASIS_VALUE; + + if (resolvedValue === DEFAULT_EMPHASIS_VALUE) { this.removeAttribute('emphasis'); } else { - const resolvedValue = - value === true - ? 'strong' - : restrictToAllowedValues(value, TEXT_EMPHASIS_KEYWORDS); - - if (resolvedValue) this.setAttribute('emphasis', resolvedValue); + this.setAttribute('emphasis', resolvedValue); } } } diff --git a/packages/clips/source/elements/TextField/TextField.ts b/packages/clips/source/elements/TextField/TextField.ts index de36f748..03337e57 100644 --- a/packages/clips/source/elements/TextField/TextField.ts +++ b/packages/clips/source/elements/TextField/TextField.ts @@ -16,8 +16,9 @@ import { ClipsElement, backedByAttribute, backedByAttributeAsBoolean, - restrictToAllowedValues, + formatAttributeValue, attributeRestrictedToAllowedValues, + type AttributeValueAsPropertySetter, } from '../ClipsElement.ts'; // import {type SignalOrValue} from '../../signals.ts'; @@ -205,6 +206,8 @@ export interface TextFieldEvents { input: RemoteEvent; } +const DEFAULT_RESIZE_VALUE = 'none'; + /** * TextField is used to collect text input from a user. */ @@ -306,23 +309,24 @@ export class TextField get resize(): TextFieldResizeKeyword { return ( - restrictToAllowedValues( - this.getAttribute('resize'), - TEXT_FIELD_RESIZE_KEYWORDS, - ) ?? 'none' + formatAttributeValue(this.getAttribute('resize'), { + allowed: TEXT_FIELD_RESIZE_KEYWORDS, + }) ?? DEFAULT_RESIZE_VALUE ); } - set resize(value: TextFieldResizeKeyword | boolean | undefined) { - if (value == null || value === 'none' || value === false) { + set resize(value: AttributeValueAsPropertySetter) { + const resolvedValue = + formatAttributeValue(value, { + allowed: TEXT_FIELD_RESIZE_KEYWORDS, + false: 'none', + truthy: 'block', + }) ?? DEFAULT_RESIZE_VALUE; + + if (resolvedValue === 'none') { this.removeAttribute('resize'); } else { - const resolvedValue = - value === true - ? 'block' - : restrictToAllowedValues(value, TEXT_FIELD_RESIZE_KEYWORDS); - - if (resolvedValue) this.setAttribute('resize', resolvedValue); + this.setAttribute('resize', resolvedValue); } } } diff --git a/packages/clips/source/elements/View/View.ts b/packages/clips/source/elements/View/View.ts index 6766f836..388e389e 100644 --- a/packages/clips/source/elements/View/View.ts +++ b/packages/clips/source/elements/View/View.ts @@ -1,4 +1,8 @@ -import {ClipsElement, restrictToAllowedValues} from '../ClipsElement.ts'; +import { + ClipsElement, + formatAutoOrNoneAttributeValue, + type AttributeValueAsPropertySetter, +} from '../ClipsElement.ts'; import {type SpacingKeyword, SPACING_KEYWORDS} from '@watching/design'; @@ -25,6 +29,8 @@ export interface ViewProperties { export interface ViewEvents {} +const DEFAULT_PADDING_VALUE = 'none'; + /** * A View is a generic container component. Its contents will always be their * “natural” size, so this component can be useful in layout components (like `Layout`, `Tiles`, @@ -49,13 +55,17 @@ export class View< get padding(): SpacingKeyword { return ( - restrictToAllowedValues(this.getAttribute('padding'), SPACING_KEYWORDS) ?? - 'none' + formatAutoOrNoneAttributeValue(this.getAttribute('padding'), { + allowed: SPACING_KEYWORDS, + }) ?? DEFAULT_PADDING_VALUE ); } - set padding(value: SpacingKeyword | boolean) { - const resolvedValue = resolvePaddingValue(value); + set padding(value: AttributeValueAsPropertySetter) { + const resolvedValue = + formatAutoOrNoneAttributeValue(value, { + allowed: SPACING_KEYWORDS, + }) ?? DEFAULT_PADDING_VALUE; if (resolvedValue === 'none') { this.removeAttribute('padding'); @@ -65,70 +75,75 @@ export class View< } get paddingInlineStart(): SpacingKeyword | undefined { - return restrictToAllowedValues( + return formatAutoOrNoneAttributeValue( this.getAttribute('padding-inline-start'), - SPACING_KEYWORDS, + { + allowed: SPACING_KEYWORDS, + }, ); } - set paddingInlineStart(value: SpacingKeyword | boolean | undefined) { + set paddingInlineStart( + value: AttributeValueAsPropertySetter, + ) { this.#updatePaddingProperty('padding-inline-start', value); } get paddingInlineEnd(): SpacingKeyword | undefined { - return restrictToAllowedValues( + return formatAutoOrNoneAttributeValue( this.getAttribute('padding-inline-end'), - SPACING_KEYWORDS, + { + allowed: SPACING_KEYWORDS, + }, ); } - set paddingInlineEnd(value: SpacingKeyword | boolean | undefined) { + set paddingInlineEnd(value: AttributeValueAsPropertySetter) { this.#updatePaddingProperty('padding-inline-end', value); } get paddingBlockStart(): SpacingKeyword | undefined { - return restrictToAllowedValues( + return formatAutoOrNoneAttributeValue( this.getAttribute('padding-block-start'), - SPACING_KEYWORDS, + { + allowed: SPACING_KEYWORDS, + }, ); } - set paddingBlockStart(value: SpacingKeyword | boolean | undefined) { + set paddingBlockStart(value: AttributeValueAsPropertySetter) { this.#updatePaddingProperty('padding-block-start', value); } get paddingBlockEnd(): SpacingKeyword | undefined { - return restrictToAllowedValues( + return formatAutoOrNoneAttributeValue( this.getAttribute('padding-block-end'), - SPACING_KEYWORDS, + { + allowed: SPACING_KEYWORDS, + }, ); } - set paddingBlockEnd(value: SpacingKeyword | boolean | undefined) { + set paddingBlockEnd(value: AttributeValueAsPropertySetter) { this.#updatePaddingProperty('padding-block-end', value); } #updatePaddingProperty( - property: Extract, - value: SpacingKeyword | boolean | undefined, + attribute: Extract, + value: AttributeValueAsPropertySetter, ) { - if (value == null) { - this.removeAttribute(property); + const resolvedValue = formatAutoOrNoneAttributeValue(value, { + allowed: SPACING_KEYWORDS, + }); + + if (resolvedValue == null) { + this.removeAttribute(attribute); } else { - const resolvedValue = resolvePaddingValue(value); - if (resolvedValue) this.setAttribute(property, resolvedValue); + this.setAttribute(attribute, resolvedValue); } } } -function resolvePaddingValue( - value: string | boolean | undefined, -): SpacingKeyword | undefined { - if (value === true) return 'auto'; - if (value === false || value == null) return 'none'; - return restrictToAllowedValues(value, SPACING_KEYWORDS); -} - customElements.define('ui-view', View); declare global {