From 324058932387cdeedbefd47d741704d9a6cb828c Mon Sep 17 00:00:00 2001
From: ashk1996 <125904756+ashk1996@users.noreply.github.com>
Date: Tue, 3 Dec 2024 16:33:38 +0100
Subject: [PATCH 1/2] feat(ci): improving release process using release-please
(#1176)
---
.github/workflows/release.yml | 29 ++++++++++++
docs/DEPLOYMENT.md | 89 ++++++++++++++++++++++++++++++-----
2 files changed, 107 insertions(+), 11 deletions(-)
create mode 100644 .github/workflows/release.yml
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..6cd272908
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,29 @@
+name: Release Please
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '18.x'
+
+ - name: Release Please Action
+ uses: googleapis/release-please-action@v4
+ id: release
+ with:
+ release-type: node
+ package-name: '@ashk1996/boiler'
+ token: ${{ secrets.GITHUB_TOKEN }}
+ target-branch: main
+ branch: main
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
index 7168b6e52..605b92839 100644
--- a/docs/DEPLOYMENT.md
+++ b/docs/DEPLOYMENT.md
@@ -1,14 +1,22 @@
# Deployment
+
This chapter should provide the user with all needed information around the deployment of your project.
-Currently, the B01LER project is getting deployed to [render.com](https://render.com) periodically. In the near future, we intend on implementing a more formal deployment schedule so that the latest version of the project will be available to view and interact with.
+Currently, the B01LER project is getting deployed to [render.com](https://render.com) periodically. In the near future,
+we intend on implementing a more formal deployment schedule so that the latest version of the project will be available
+to view and interact with.
-The project can be viewed [here](https://b01ler.onrender.com/). This link allows you to experiment with the project and learn about how you can use components via Storybook.
+The project can be viewed [here](https://b01ler.onrender.com/). This link allows you to experiment with the project and
+learn about how you can use components via Storybook.
-We also deploy our JS Example app to [Render](https://b01ler.onrender.com/js-example-app). This application shows you how our components look when implemented in a vanilla Javascript application.
+We also deploy our JS Example app to [Render](https://b01ler.onrender.com/js-example-app). This application shows you
+how our components look when implemented in a vanilla Javascript application.
## Content
+
- [Tooling](#tooling)
+- [Release Please](#release-please)
+- [Conventional Commit Messages](#conventional-commit-messages)
- [How to deploy](#how-to-deploy)
- [Versioning](#versioning)
- [Release Management](#release-management)
@@ -16,22 +24,81 @@ We also deploy our JS Example app to [Render](https://b01ler.onrender.com/js-exa
- [Support](#support)
## Tooling
-Please explain what kind of tools you use for your deployment, why you have choosen them and how to work with them.
+This project uses release-please and conventional commit messages for automated release creation and deploys the package
+to the npm registry.
+
+We use the [Git-Flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) branching model:
+
+```mermaid
+ gitGraph
+ commit id: "a"
+ commit id: "b"
+ branch develop
+ checkout develop
+ checkout develop
+ branch feature1
+ checkout feature1
+ commit id: "c"
+ checkout develop
+ merge feature1
+ branch feature2
+ checkout feature2
+ commit id: "d"
+ checkout develop
+ merge feature2
+ checkout main
+ merge develop
+ branch "release"
+ checkout release
+ commit id: "1.0.0" tag: "release"
+ checkout main
+ merge release
+ checkout develop
+ merge main
+```
+
+## Release Please
+
+Release Please automates CHANGELOG generation, the creation of GitHub releases, and version bumps for your projects.
+Release Please does so by parsing the git history, looking for Conventional Commit messages, and creating release PRs.
+
+The tool runs on every update on the `main` branch and creates a release PR which needs to be manually be merged to
+create the release.
+
+The updated `main` branch then needs to be merged back, please use rebase, into the `develop` branch.
+
+## Conventional Commit Messages
+
+The commits must be compliant with with the
+[Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). The commit header should not
+exceed a maximum character count of 140. The scope is allowed to be one of the following options: 'all', 'ui-library',
+'icons', 'figma-design-tokens', 'tokens', or 'storybook'.
## How to deploy
-In this section you should discribe the deployment process for your project. Think about how developers who are unfamiliar with the project can deploy it to the respective enviroments.
+One needs to create a new PR from `develop` to `main`. Once the PR is approved and merged the GitHub workflow `release`
+will be started.
-## Versioning
-Please describe briefly how you manage the versioning of your project.
+The `release` workflow will first run the tests and after they were successful a new release branch is created together
+with a PR from the release branch into `main`.
+
+The newly created release PR needs to be manually merged into main to finish the package creation and release
+publishing.
+After the release PR is merged the `develop` branch also needs to be updated by rebasing it onto the `main` branch.
-## Release Management
-If you have a release management setup in place, please describe the process and who to contact if support is needed.
+## Versioning
+
+Please see [conventional commit messages](#conventional-commit-messages)
## Deployment Schedule
-Please write down your deployment schedule. Consider using screenshots or graphs for better visualization.
+
+There is no deployment schedule. A new release is created whenever we have multiple new small features or a new big
+feature that we want to release.
## Support
-If support is needed while deploying BO1LER, please refer to our [README feedback section](/README.md#tipping_hand_person-help--feedback) where we list the many ways that we can be reached to support you.
+
+If support is needed while deploying BO1LER, please refer to our
+[README feedback section](/README.md#tipping_hand_person-help--feedback) where we list the many ways that we can be
+reached to support you.
From 21856a3203a41695c324c943ba56c3cb9ddc8573 Mon Sep 17 00:00:00 2001
From: ashk1996 <125904756+ashk1996@users.noreply.github.com>
Date: Tue, 3 Dec 2024 16:36:02 +0100
Subject: [PATCH 2/2] feat(ui-library): introduce property sanitizer using
preact and controller (#1175)
---
.../src/components/button-text/index.ts | 56 ++++++++----
.../src/components/input-field-text/index.ts | 67 ++++++++++----
.../src/components/textarea/index.ts | 90 +++++++++++++------
.../src/utils/lit/sanitization-controller.ts | 48 ++++++++++
packages/ui-library/src/utils/lit/sanitize.ts | 51 +++++++++++
packages/ui-library/src/utils/lit/signals.ts | 11 ++-
6 files changed, 259 insertions(+), 64 deletions(-)
create mode 100644 packages/ui-library/src/utils/lit/sanitization-controller.ts
create mode 100644 packages/ui-library/src/utils/lit/sanitize.ts
diff --git a/packages/ui-library/src/components/button-text/index.ts b/packages/ui-library/src/components/button-text/index.ts
index e19636687..17d602e50 100644
--- a/packages/ui-library/src/components/button-text/index.ts
+++ b/packages/ui-library/src/components/button-text/index.ts
@@ -30,6 +30,8 @@ import {
} from '../../globals/events.js';
import { LitElementCustom, ElementInterface } from '../../utils/lit/element.js';
import { ifDefined } from 'lit/directives/if-defined.js';
+import { makeSanitizer } from '../../utils/lit/sanitize.js';
+import { SanitizationController } from '../../utils/lit/sanitization-controller.js';
export type BlrButtonTextEventHandlers = {
blrFocus?: (event: BlrFocusEvent) => void;
@@ -37,23 +39,46 @@ export type BlrButtonTextEventHandlers = {
blrClick?: (event: BlrClickEvent) => void;
};
+const propertySanitizer = makeSanitizer((unsanitized: BlrButtonTextType) => ({
+ iconPosition: unsanitized.iconPosition ?? 'leading',
+ sizeVariant: unsanitized.sizeVariant ?? 'md',
+ buttonDisplay: unsanitized.buttonDisplay ?? 'inline-block',
+}));
+
/**
* @fires blrFocus Button received focus
* @fires blrBlur Button lost focus
* @fires blrClick Button was clicked
*/
export class BlrButtonText extends LitElementCustom {
+ private sanitizedController: SanitizationController<
+ BlrButtonTextType,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ any
+ >;
+
+ constructor() {
+ super();
+ this.sanitizedController = new SanitizationController<
+ BlrButtonTextType,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ any
+ >({
+ host: this,
+ sanitize: propertySanitizer,
+ });
+ }
static styles = [styleCustom, staticActionStyles];
@property() accessor label = 'Button Label';
- @property() accessor icon: SizelessIconType | undefined = undefined;
- @property() accessor iconPosition: IconPositionVariant | undefined = 'leading';
+ @property() accessor icon: SizelessIconType | undefined;
+ @property() accessor iconPosition: IconPositionVariant | undefined;
@property({ type: Boolean }) accessor loading!: boolean;
@property({ type: Boolean }) accessor disabled!: boolean;
@property() accessor buttonTextId: string | undefined;
@property() accessor variant: ActionVariantType = 'primary';
- @property() accessor sizeVariant: ActionSizesType | undefined = 'md';
- @property() accessor buttonDisplay: DisplayType | undefined = 'inline-block';
+ @property() accessor sizeVariant: ActionSizesType | undefined;
+ @property() accessor buttonDisplay: DisplayType | undefined;
@property() accessor theme: ThemeType = 'Light_value';
@@ -80,27 +105,28 @@ export class BlrButtonText extends LitElementCustom {
};
protected render() {
- if (this.sizeVariant && this.buttonDisplay) {
+ const sanitized = this.sanitizedController.values;
+ if (sanitized.sizeVariant && this.buttonDisplay) {
const classes = classMap({
'blr-semantic-action': true,
'blr-button-text': true,
[this.variant]: this.variant,
- [this.sizeVariant]: this.sizeVariant,
+ [sanitized.sizeVariant]: sanitized.sizeVariant,
'disabled': this.disabled,
'loading': this.loading,
- [this.buttonDisplay]: this.buttonDisplay,
+ [sanitized.buttonDisplay]: sanitized.buttonDisplay,
[this.theme]: this.theme,
});
const iconClasses = classMap({
'icon': true,
- 'leading-icon-class': this.iconPosition === 'leading',
- 'trailing-icon-class': this.iconPosition === 'trailing',
+ 'leading-icon-class': sanitized.iconPosition === 'leading',
+ 'trailing-icon-class': sanitized.iconPosition === 'trailing',
});
const flexContainerClasses = classMap({
'flex-container': true,
- [this.sizeVariant]: this.sizeVariant,
+ [sanitized.sizeVariant]: sanitized.sizeVariant,
[this.theme]: this.theme,
});
@@ -118,7 +144,7 @@ export class BlrButtonText extends LitElementCustom {
'buttons',
'loader',
'sizevariant',
- this.sizeVariant,
+ sanitized.sizeVariant,
]).toLowerCase() as FormSizesType;
const iconSizeVariant = getComponentConfigToken([
@@ -126,11 +152,11 @@ export class BlrButtonText extends LitElementCustom {
'buttontext',
'icon',
'sizevariant',
- this.sizeVariant,
- ]) as SizesType;
+ sanitized.sizeVariant,
+ ]).toLowerCase() as SizesType;
const labelAndIconGroup = html`
- ${this.icon && this.iconPosition === 'leading'
+ ${this.icon && sanitized.iconPosition === 'leading'
? BlrIconRenderFunction(
{
icon: calculateIconName(this.icon, iconSizeVariant),
@@ -144,7 +170,7 @@ export class BlrButtonText extends LitElementCustom {
)
: nothing}
${this.label}
- ${this.icon && this.iconPosition === 'trailing'
+ ${this.icon && sanitized.iconPosition === 'trailing'
? BlrIconRenderFunction(
{
icon: calculateIconName(this.icon, iconSizeVariant),
diff --git a/packages/ui-library/src/components/input-field-text/index.ts b/packages/ui-library/src/components/input-field-text/index.ts
index 60be3e8c2..9bc40e5ce 100644
--- a/packages/ui-library/src/components/input-field-text/index.ts
+++ b/packages/ui-library/src/components/input-field-text/index.ts
@@ -26,6 +26,8 @@ import {
import { LitElementCustom, ElementInterface } from '../../utils/lit/element.js';
import { BlrIconEventHandlers } from '../icon/index.js';
import { ifDefined } from 'lit/directives/if-defined.js';
+import { makeSanitizer } from '../../utils/lit/sanitize.js';
+import { SanitizationController } from '../../utils/lit/sanitization-controller.js';
export type BlrInputFieldTextEventHandlers = {
blrFocus?: (event: BlrFocusEvent) => void;
@@ -34,6 +36,12 @@ export type BlrInputFieldTextEventHandlers = {
blrSelect?: (event: BlrSelectEvent) => void;
};
+const propertySanitizer = makeSanitizer((unsanitized: BlrInputFieldTextType) => ({
+ type: unsanitized.type ?? 'text',
+ sizeVariant: unsanitized.sizeVariant ?? 'md',
+ theme: unsanitized.theme ?? 'Light_value',
+}));
+
/**
* @fires blrFocus InputFieldText received focus
* @fires blrBlur InputFieldText lost focus
@@ -41,13 +49,30 @@ export type BlrInputFieldTextEventHandlers = {
* @fires blrSelect Text in InputFieldText got selected
*/
export class BlrInputFieldText extends LitElementCustom {
+ private sanitizedController: SanitizationController<
+ BlrInputFieldTextType,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ any
+ >;
+
+ constructor() {
+ super();
+ this.sanitizedController = new SanitizationController<
+ BlrInputFieldTextType,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ any
+ >({
+ host: this,
+ sanitize: propertySanitizer,
+ });
+ }
static styles = [styleCustom];
@query('input')
protected accessor _inputFieldTextNode!: HTMLInputElement;
@property() accessor inputFieldTextId!: string;
- @property() accessor type: InputTypes = 'text';
+ @property() accessor type: InputTypes | undefined;
@property() accessor arialabel!: string;
@property({ type: Boolean }) accessor hasLabel!: boolean;
@property() accessor label!: string;
@@ -56,7 +81,7 @@ export class BlrInputFieldText extends LitElementCustom {
@property() accessor placeholder: string | undefined;
@property({ type: Boolean }) accessor disabled: boolean | undefined;
@property({ type: Boolean }) accessor readonly: boolean | undefined;
- @property() accessor sizeVariant: FormSizesType | undefined = 'md';
+ @property() accessor sizeVariant: FormSizesType | undefined;
@property({ type: Boolean }) accessor required: boolean | undefined;
@property({ type: Number }) accessor maxLength: number | undefined;
@property() accessor pattern: string | undefined;
@@ -69,9 +94,9 @@ export class BlrInputFieldText extends LitElementCustom {
@property() accessor errorMessageIcon: SizelessIconType | undefined;
@property() accessor name!: string;
- @property() accessor theme: ThemeType = 'Light_value';
+ @property() accessor theme: ThemeType | undefined;
- @state() protected accessor currentType: InputTypes = this.type;
+ @state() protected accessor currentType: InputTypes | undefined;
@state() protected accessor isFocused = false;
protected willUpdate(_changedProperties: PropertyValueMap
| Map): void {
@@ -155,7 +180,7 @@ export class BlrInputFieldText extends LitElementCustom {
'icon-input': true,
[this.sizeVariant!]: this.sizeVariant!,
'no-pointer-events': Boolean(this.disabled || this.type !== 'password'),
- [this.theme]: this.theme,
+ [this.theme!]: this.theme!,
});
const iconName: SizelessIconType | undefined =
@@ -174,19 +199,20 @@ export class BlrInputFieldText extends LitElementCustom {
},
);
}
-
protected render() {
- if (this.sizeVariant) {
+ const sanitized = this.sanitizedController.values;
+
+ if (sanitized.sizeVariant) {
const classes = classMap({
'blr-input-field-text': true,
- [this.sizeVariant]: this.sizeVariant,
- [this.theme]: this.theme,
+ [sanitized.sizeVariant]: sanitized.sizeVariant,
+ [sanitized.theme]: this.theme,
});
const inputClasses = classMap({
'error-input': this.hasError || false,
'disabled': this.disabled || false,
- [this.sizeVariant]: this.sizeVariant,
+ [sanitized.sizeVariant]: sanitized.sizeVariant,
});
const inputContainerClasses = classMap({
@@ -194,16 +220,16 @@ export class BlrInputFieldText extends LitElementCustom {
'error-input': this.hasError || false,
'disabled': this.disabled || false,
'readonly': this.readonly ? true : false,
- [this.sizeVariant]: this.sizeVariant,
- [this.theme]: this.theme,
+ [sanitized.sizeVariant]: sanitized.sizeVariant,
+ [sanitized.theme]: sanitized.theme,
});
const captionContent = html`
${this.hasHint && (this.hintMessage || this.hintMessageIcon)
? BlrFormCaptionRenderFunction({
variant: 'hint',
- theme: this.theme,
- sizeVariant: this.sizeVariant,
+ theme: sanitized.theme,
+ sizeVariant: sanitized.sizeVariant,
message: this.hintMessage,
icon: this.hintMessageIcon,
})
@@ -211,7 +237,7 @@ export class BlrInputFieldText extends LitElementCustom {
${this.hasError && (this.errorMessage || this.errorMessageIcon)
? BlrFormCaptionRenderFunction({
variant: 'error',
- theme: this.theme,
+ theme: sanitized.theme,
sizeVariant: this.sizeVariant,
message: this.errorMessage,
icon: this.errorMessageIcon,
@@ -226,10 +252,10 @@ export class BlrInputFieldText extends LitElementCustom {
${BlrFormLabelRenderFunction({
label: this.label,
- sizeVariant: this.sizeVariant,
+ sizeVariant: sanitized.sizeVariant,
labelAppendix: this.labelAppendix,
forValue: this.inputFieldTextId,
- theme: this.theme,
+ theme: sanitized.theme,
hasError: Boolean(this.hasError),
})}
@@ -242,7 +268,7 @@ export class BlrInputFieldText extends LitElementCustom {
id=${this.inputFieldTextId}
name="${ifDefined(this.name)}"
aria-label=${this.arialabel}
- type="${this.currentType}"
+ type="${sanitized.type === 'text' ? this.currentType : sanitized.type}"
.value="${this.value}"
placeholder="${ifDefined(this.placeholder)}"
?disabled="${this.disabled}"
@@ -260,7 +286,10 @@ export class BlrInputFieldText extends LitElementCustom {
${this.renderInputIcon()}
${(this.hasHint && this.hintMessage) || (this.hasError && this.errorMessage)
- ? BlrFormCaptionGroupRenderFunction({ theme: this.theme, sizeVariant: this.sizeVariant }, captionContent)
+ ? BlrFormCaptionGroupRenderFunction(
+ { theme: sanitized.theme, sizeVariant: sanitized.sizeVariant },
+ captionContent,
+ )
: nothing}
`;
diff --git a/packages/ui-library/src/components/textarea/index.ts b/packages/ui-library/src/components/textarea/index.ts
index f9b22c7fb..a3ab78c3f 100644
--- a/packages/ui-library/src/components/textarea/index.ts
+++ b/packages/ui-library/src/components/textarea/index.ts
@@ -24,6 +24,8 @@ import {
createBlrTextValueChangeEvent,
} from '../../globals/events.js';
import { LitElementCustom, ElementInterface } from '../../utils/lit/element.js';
+import { makeSanitizer } from '../../utils/lit/sanitize.js';
+import { SanitizationController } from '../../utils/lit/sanitization-controller.js';
export type BlrTextareaEventHandlers = {
blrFocus?: (event: BlrFocusEvent) => void;
@@ -32,6 +34,16 @@ export type BlrTextareaEventHandlers = {
blrSelect?: (event: BlrSelectEvent) => void;
};
+const propertySanitizer = makeSanitizer((unsanitized: BlrTextareaType) => ({
+ sizeVariant: unsanitized.sizeVariant ?? 'md',
+ warningLimitType: unsanitized.warningLimitType ?? 'warningLimitInt',
+ warningLimitInt: unsanitized.warningLimitInt ?? 105,
+ warningLimitPer: unsanitized.warningLimitPer ?? 75,
+ resize: unsanitized.resize ?? 'none',
+ hasHint: unsanitized.hasHint ?? true,
+ theme: unsanitized.theme ?? 'Light_value',
+}));
+
/**
* @fires blrFocus Textarea received focus
* @fires blrBlur Textarea lost focus
@@ -39,6 +51,24 @@ export type BlrTextareaEventHandlers = {
* @fires blrSelect Text in Textarea got selected
*/
export class BlrTextarea extends LitElementCustom {
+ private sanitizedController: SanitizationController<
+ BlrTextareaType,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ any
+ >;
+
+ constructor() {
+ super();
+ this.sanitizedController = new SanitizationController<
+ BlrTextareaType,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ any
+ >({
+ host: this,
+ sanitize: propertySanitizer,
+ });
+ }
+
static styles = [staticFormStyles, staticStyles];
@query('textarea')
@@ -53,13 +83,13 @@ export class BlrTextarea extends LitElementCustom {
@property({ type: Boolean }) accessor disabled: boolean | undefined;
@property({ type: Boolean }) accessor readonly: boolean | undefined;
@property({ type: Boolean }) accessor hasLabel: boolean | undefined;
- @property() accessor sizeVariant: FormSizesType | undefined = 'md';
+ @property() accessor sizeVariant: FormSizesType | undefined;
@property({ type: Boolean }) accessor required: boolean | undefined;
@property({ type: Number }) accessor maxLength: number | undefined;
@property({ type: Number }) accessor minLength: number | undefined;
- @property() accessor warningLimitType: WarningLimits = 'warningLimitInt';
- @property({ type: Number }) accessor warningLimitInt = 105;
- @property({ type: Number }) accessor warningLimitPer = 75;
+ @property() accessor warningLimitType: WarningLimits | undefined;
+ @property({ type: Number }) accessor warningLimitInt: number | undefined;
+ @property({ type: Number }) accessor warningLimitPer: number | undefined;
@property() accessor pattern: string | undefined;
@property({ type: Boolean }) accessor hasError: boolean | undefined;
@property() accessor errorMessage: string | undefined;
@@ -69,12 +99,12 @@ export class BlrTextarea extends LitElementCustom {
@property() accessor hintMessage: string | undefined;
@property({ type: Boolean }) accessor hasCounter: boolean | undefined;
@property() accessor hintMessageIcon: SizelessIconType | undefined;
- @property() accessor resize: ResizeType = 'none';
+ @property() accessor resize: ResizeType | undefined;
@property({ type: Number }) accessor rows: number | undefined;
@property({ type: Number }) accessor cols: number | undefined;
@property() accessor name: string | undefined;
@property() accessor textAreaDisplay: DisplayType | undefined = 'block';
- @property() accessor theme: ThemeType = 'Light_value';
+ @property() accessor theme: ThemeType | undefined;
@state() protected accessor count = 0;
@query('textarea') protected accessor textareaElement: HTMLTextAreaElement | null = null;
@@ -113,11 +143,15 @@ export class BlrTextarea extends LitElementCustom {
protected determinateCounterVariant(): CounterVariantType {
let counterVariant: CounterVariantType = 'neutral';
- if (this.maxLength) {
- if (this.warningLimitType === 'warningLimitPer' && this.count >= (this.maxLength / 100) * this.warningLimitPer) {
- counterVariant = 'warn';
- } else if (this.warningLimitType === 'warningLimitInt' && this.count >= this.warningLimitInt) {
- counterVariant = 'warn';
+ if (this.maxLength !== undefined) {
+ if (this.warningLimitType === 'warningLimitPer' && this.warningLimitPer !== undefined) {
+ if (this.count >= (this.maxLength / 100) * this.warningLimitPer) {
+ counterVariant = 'warn';
+ }
+ } else if (this.warningLimitType === 'warningLimitInt' && this.warningLimitInt !== undefined) {
+ if (this.count >= this.warningLimitInt) {
+ counterVariant = 'warn';
+ }
}
if (this.count >= this.maxLength) {
@@ -153,33 +187,35 @@ export class BlrTextarea extends LitElementCustom {
};
protected render() {
- if (this.sizeVariant && this.textAreaDisplay) {
+ const sanitized = this.sanitizedController.values;
+
+ if (sanitized.sizeVariant && this.textAreaDisplay) {
const classes = classMap({
'blr-textarea': true,
- [this.theme]: true,
+ [sanitized.theme]: true,
'error': this.hasError || false,
- [this.sizeVariant]: this.sizeVariant,
+ [sanitized.sizeVariant]: sanitized.sizeVariant,
[this.textAreaDisplay]: this.textAreaDisplay,
});
const textareaClasses = classMap({
'textarea-input-control': true,
- [this.theme]: true,
+ [sanitized.theme]: true,
'error': this.hasError || false,
- [this.resize]: this.resize,
- [this.sizeVariant]: this.sizeVariant,
+ [sanitized.resize]: sanitized.resize,
+ [sanitized.sizeVariant]: sanitized.sizeVariant,
[this.textAreaDisplay]: this.textAreaDisplay,
'disabled': this.disabled || false,
});
const textareaInfoContainer = classMap({
'blr-textarea-info-container': true,
- [this.theme]: this.theme,
+ [sanitized.theme]: sanitized.theme,
'hint': this.hasHint || false,
'error': this.hasError || false,
'error-message': this.errorMessage || false,
'hint-message': this.hintMessage || false,
- [this.sizeVariant]: this.sizeVariant,
+ [sanitized.sizeVariant]: sanitized.sizeVariant,
});
const counterVariant = this.determinateCounterVariant();
@@ -188,7 +224,7 @@ export class BlrTextarea extends LitElementCustom {
${this.hasHint && (this.hintMessage || this.hintMessageIcon)
? BlrFormCaptionRenderFunction({
variant: 'hint',
- theme: this.theme,
+ theme: sanitized.theme,
sizeVariant: this.sizeVariant,
message: this.hintMessage,
icon: this.hintMessageIcon,
@@ -197,8 +233,8 @@ export class BlrTextarea extends LitElementCustom {
${this.hasError && (this.errorMessage || this.errorMessageIcon)
? BlrFormCaptionRenderFunction({
variant: 'error',
- theme: this.theme,
- sizeVariant: this.sizeVariant,
+ theme: sanitized.theme,
+ sizeVariant: sanitized.sizeVariant,
message: this.errorMessage,
icon: this.errorMessageIcon,
})
@@ -211,10 +247,10 @@ export class BlrTextarea extends LitElementCustom {
? html`
${BlrFormLabelRenderFunction({
label: this.label,
- sizeVariant: this.sizeVariant,
+ sizeVariant: sanitized.sizeVariant,
labelAppendix: this.labelAppendix,
forValue: this.textAreaId,
- theme: this.theme,
+ theme: sanitized.theme,
hasError: Boolean(this.hasError),
})}
`
@@ -243,7 +279,7 @@ export class BlrTextarea extends LitElementCustom {
${(this.hasHint && this.hintMessage) || (this.hasError && this.errorMessage)
? BlrFormCaptionGroupRenderFunction(
- { sizeVariant: this.sizeVariant, theme: this.theme },
+ { sizeVariant: sanitized.sizeVariant, theme: sanitized.theme },
getCaptionContent(),
)
: nothing}
@@ -252,8 +288,8 @@ export class BlrTextarea extends LitElementCustom {
variant: counterVariant,
value: this.count,
maxValue: this.maxLength || 0,
- sizeVariant: this.sizeVariant,
- theme: this.theme,
+ sizeVariant: sanitized.sizeVariant,
+ theme: sanitized.theme,
})
: nothing}
diff --git a/packages/ui-library/src/utils/lit/sanitization-controller.ts b/packages/ui-library/src/utils/lit/sanitization-controller.ts
new file mode 100644
index 000000000..a42c55eb2
--- /dev/null
+++ b/packages/ui-library/src/utils/lit/sanitization-controller.ts
@@ -0,0 +1,48 @@
+import { ReactiveController, ReactiveControllerHost } from 'lit';
+import { SignalHub, registerSignal } from '../../utils/lit/signals.js';
+import { LitElementCustom } from '../../utils/lit/element.js';
+import { SanitizeFunction, SanitizeFunctionResult } from './sanitize.js';
+import { Signal } from '@lit-labs/preact-signals';
+export class SanitizationController>
+ implements ReactiveController
+{
+ private _host: ReactiveControllerHost & LitElementCustom;
+ private _sanitize: SanitizeFunction;
+ private _sanitizedSignal: Signal;
+
+ constructor({
+ host,
+ sanitize,
+ }: {
+ host: ReactiveControllerHost & LitElementCustom;
+ sanitize: SanitizeFunction;
+ }) {
+ this._host = host;
+ this._sanitize = sanitize;
+
+ // Initialize signal with the result of initial sanitization
+ this._sanitizedSignal = new Signal(this._sanitize(this._host as TTarget));
+
+ registerSignal(
+ this._host.signals as SignalHub<{ sanitizedValues: TResult }>,
+ 'sanitizedValues',
+ this._sanitizedSignal,
+ );
+
+ this._host.addController(this);
+ }
+
+ private _sanitizeValues(): void {
+ const sanitizedValues = this._sanitize(this._host as TTarget);
+ this._sanitizedSignal.value = sanitizedValues;
+ }
+
+ public get values(): Readonly {
+ return this._sanitizedSignal.value;
+ }
+
+ hostConnected(): void {
+ // Perform sanitization when the host connects
+ this._sanitizeValues();
+ }
+}
diff --git a/packages/ui-library/src/utils/lit/sanitize.ts b/packages/ui-library/src/utils/lit/sanitize.ts
new file mode 100644
index 000000000..ab8925333
--- /dev/null
+++ b/packages/ui-library/src/utils/lit/sanitize.ts
@@ -0,0 +1,51 @@
+/**
+ * ## Usage
+ * ```typescript
+ * type User = {
+ * name: string
+ * addresses?: Address[]
+ * status?: 'offline' | 'online'
+ * }
+ *
+ * const sanitizeUser = makeSanitizer((target: User) => ({
+ * status: typeof target.status === 'string' ? target.status : 'offline',
+ * addresses: Array.isArray(target.addresses) ? target.addresses : [],
+ * }));
+ *
+ * sanitizeUser({
+ * name: 'John'
+ * })
+ * // ^ Yields
+ * // {
+ * // addresses: [],
+ * // status: 'offline'
+ * // }
+ *
+ * sanitizeUser({
+ * name: 'John',
+ * addresses: [{ street: 'Rogue Street, 13' }],
+ * status: 'online'
+ * })
+ * // ^ Yields
+ * // {
+ * // addresses: [{ street: 'Rogue Street, 13' }],
+ * // status: 'online'
+ * // }
+ *
+ * ```
+ */
+export function makeSanitizer>(
+ sanitize: SanitizeFunction,
+): (unsanitized: TTarget) => TSanitizerReturn {
+ return sanitize;
+}
+
+export type Sanitized = {
+ readonly [P in keyof TTarget]: Exclude;
+};
+
+export type SanitizeFunctionResult = Partial>;
+
+export type SanitizeFunction> = (
+ unsanitized: TTarget,
+) => Sanitized;
diff --git a/packages/ui-library/src/utils/lit/signals.ts b/packages/ui-library/src/utils/lit/signals.ts
index 00ac0b17e..529e0589c 100644
--- a/packages/ui-library/src/utils/lit/signals.ts
+++ b/packages/ui-library/src/utils/lit/signals.ts
@@ -1,13 +1,18 @@
-import { Signal, ReadonlySignal } from '@lit-labs/preact-signals';
+import { Signal } from '@lit-labs/preact-signals';
import { LitElementCustom } from './element.js';
-export function registerSignal(hub: SignalHub, key: keyof TProps, signal: Signal) {
+export function registerSignal(
+ hub: SignalHub,
+ key: keyof TProps,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ signal: Signal,
+) {
Object.defineProperty(hub, key, { value: signal });
}
export type SignalHub = Omit<
{
- readonly [P in keyof TProps]-?: ReadonlySignal;
+ readonly [P in keyof TProps]-?: Signal;
},
keyof LitElementCustom
>;