Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/components/counter/ch-counter.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:host {
/**
* @prop --ch-counter-status__warning-color:
* Specifies the color when is near to limit
* @default currentColor
*/
--ch-counter-status__warning-color: currentColor;

/**
* @prop --ch-counter-status__error-color:
* Specifies the color when limit was reached
* @default currentColor
*/
--ch-counter-status__error-color: currentColor;
}

.counter-warning {
color: var(--ch-counter-status__warning-color);
}
.counter-error {
color: var(--ch-counter-status__warning-color);
}
64 changes: 64 additions & 0 deletions src/components/counter/ch-counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Component, Element, Host, Prop, State, Watch, h } from "@stencil/core";

@Component({
tag: "ch-counter",
styleUrl: "ch-counter.scss",
shadow: true
})
export class ChCounter {
@Element() hostElement!: HTMLChCounterElement;
/**
* Represents the value of the input field.
*/
@State() currentLength: number = 0;

/**
* Represents the maximum length of the input field.
*/
@State() maxLength: number = 0;

/**
* Represents the initial value of the input field.
* Note: This is required when component is updated by a parent component.
*/
@Prop() readonly initialValue: string = "";
@Watch("initialValue")
initialValueChanged(newValue: string) {
this.currentLength = newValue.length;
}

componentWillLoad() {
const chEdit = this.hostElement.querySelector("ch-edit");

chEdit?.addEventListener("input", (ev: Event) => {
this.currentLength = (ev.target as HTMLInputElement).value.length;
});

this.currentLength = (chEdit?.value ?? this.initialValue).length;
this.maxLength = chEdit?.maxLength ?? 0;
}

render() {
const remainingChars = this.maxLength - this.currentLength;
const isNearLimit = remainingChars <= 20;
const isAtLimit = remainingChars <= 0;
return (
<Host>
<slot></slot>

{this.maxLength > 0 && (
<div part="counter-container">
<span
class={{
"counter-text": true,
"counter-warning": isNearLimit && !isAtLimit,
"counter-error": isAtLimit
}}
part="counter-text"
>{`${this.currentLength} / ${this.maxLength}`}</span>
</div>
)}
</Host>
);
}
}
158 changes: 158 additions & 0 deletions src/components/counter/tests/ch-counter.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { E2EElement, E2EPage, newE2EPage } from "@stencil/core/testing";
import { testDefaultProperties } from "../../../testing/utils.e2e";

testDefaultProperties("ch-counter", {
initialValue: ""
});

describe("[ch-counter][basic]", () => {
let page: E2EPage;
let counterRef: E2EElement;

beforeEach(async () => {
page = await newE2EPage({
html: `<ch-counter></ch-counter>`,
failOnConsoleError: true
});

counterRef = await page.find("ch-counter");
});

it("should have Shadow DOM", () =>
expect(counterRef.shadowRoot).toBeTruthy());

it("should render without counter display when no ch-edit is present", async () => {
const counterContainer = await page.find(
"ch-counter >>> [part='counter-container']"
);
expect(counterContainer).toBeNull();
});

it("should render without counter display when ch-edit has no maxLength", async () => {
await page.setContent(`
<ch-counter>
<ch-edit></ch-edit>
</ch-counter>
`);
await page.waitForChanges();

const counterContainer = await page.find(
"ch-counter >>> [part='counter-container']"
);
expect(counterContainer).toBeNull();
});
});

describe("[ch-counter][functionality]", () => {
let page: E2EPage;
let counterRef: E2EElement;
let inputRef: E2EElement;

beforeEach(async () => {
page = await newE2EPage({
html: `
<ch-counter>
<ch-edit max-length="100" ></ch-edit>
</ch-counter>
`,
failOnConsoleError: true
});

counterRef = await page.find("ch-counter");
inputRef = await page.find("ch-edit >>> input");
});

it("should display counter when ch-edit has maxLength", async () => {
const counterContainer = await page.find(
"ch-counter >>> [part='counter-container']"
);
const counterText = await page.find("ch-counter >>> [part='counter-text']");

expect(counterContainer).toBeTruthy();
expect(counterText).toBeTruthy();
expect(await counterText.textContent).toBe("0 / 100");
});

it("should update counter when typing in ch-edit", async () => {
await inputRef.type("Hello world");
await page.waitForChanges();

const counterText = await page.find("ch-counter >>> [part='counter-text']");
expect(await counterText.textContent).toBe("11 / 100");
});

it("should show warning state when approaching limit", async () => {
const longText = "a".repeat(81);
await inputRef.type(longText);
await page.waitForChanges();

const counterText = await page.find("ch-counter >>> [part='counter-text']");
expect(await counterText.textContent).toBe("81 / 100");
expect(await counterText.getAttribute("class")).toContain(
"counter-warning"
);
});

it("should show error state when at limit", async () => {
const longText = "a".repeat(100);
await inputRef.type(longText);
await page.waitForChanges();

const counterText = await page.find("ch-counter >>> [part='counter-text']");
expect(await counterText.textContent).toBe("100 / 100");
expect(await counterText.getAttribute("class")).toContain("counter-error");
});

it("should show error state when exceeding limit", async () => {
const longText = "a".repeat(105);
await inputRef.type(longText);
await page.waitForChanges();

const counterText = await page.find("ch-counter >>> [part='counter-text']");
expect(await counterText.textContent).toBe("100 / 100");
expect(await counterText.getAttribute("class")).toContain("counter-error");
});

it("should handle initial value correctly", async () => {
await page.setContent(`
<ch-counter initial-value="Initial text">
<ch-edit max-length="100"></ch-edit>
</ch-counter>
`);
await page.waitForChanges();

const counterText = await page.find("ch-counter >>> [part='counter-text']");
expect(await counterText.textContent).toBe("12 / 100");
});

it("should update counter when initial value changes", async () => {
await counterRef.setProperty("initialValue", "Updated text");
await page.waitForChanges();

const counterText = await page.find("ch-counter >>> [part='counter-text']");
expect(await counterText.textContent).toBe("12 / 100");
});

it("should work with multiline ch-edit", async () => {
await page.setContent(`
<ch-counter>
<ch-edit multiline max-length="50"></ch-edit>
</ch-counter>
`);
await page.waitForChanges();

const textarea = await page.find("ch-edit >>> textarea");
const counterText = await page.find("ch-counter >>> [part='counter-text']");

expect(counterText).toBeTruthy();
expect(await counterText.textContent).toBe("0 / 50");

await textarea.type("Multiline text\nwith newlines");
await page.waitForChanges();

const counterTextUpdated = await page.find(
"ch-counter >>> [part='counter-text']"
);
expect(await counterTextUpdated.textContent).toBe("28 / 50");
});
});
102 changes: 102 additions & 0 deletions src/showcase/assets/components/counter/counter.showcase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { h } from "@stencil/core";

import {
ShowcaseRenderProperties,
ShowcaseStory,
ShowcaseTemplatePropertyInfo
} from "../types";
import { renderShowcaseProperties } from "../utils";

const state: Partial<HTMLChCounterElement> = {};

const showcaseRenderProperties: ShowcaseRenderProperties<HTMLChCounterElement> =
[
{
caption: "Styles",
properties: [
{
id: "customVars",
type: "style",
properties: [
{
id: "--ch-counter-status__warning-color",
caption: "--ch-counter-status__warning-color",
value: "#ffff00",
render: "input",
type: "string"
}
]
},
{
id: "customVars",
type: "style",
properties: [
{
id: "--ch-counter-status__error-color",
caption: "--ch-counter-status__error-color",
value: "#ff0000",
render: "input",
type: "string"
}
]
}
]
}
];

const render = () => (
<div class="checkbox-test-main-wrapper">
<fieldset class="fieldset-test form-test-edit">
<p class="heading-4">Textarea</p>
<ch-counter initialValue={state.initialValue}>
<ch-edit
class="input"
multiline
maxLength={50}
style={{ "block-size": "100px" }}
value="pepe"
></ch-edit>
</ch-counter>
</fieldset>

<fieldset class="fieldset-test form-test-edit">
<p class="heading-4">input</p>
<ch-counter initialValue="pepe">
<ch-edit class="input" maxLength={30} value="pepe"></ch-edit>
</ch-counter>
</fieldset>
</div>
);

const showcaseCounterPropertiesInfo: ShowcaseTemplatePropertyInfo<HTMLChCounterElement>[] =
[
{
name: "class",
fixed: true,
value: "navigation-list navigation-list-secondary",
type: "string"
}
];

export const counterShowcaseStory: ShowcaseStory<HTMLChCounterElement> = {
properties: showcaseRenderProperties,
markupWithoutUIModel: {
react: () => `<ch-counter ${renderShowcaseProperties(
state,
"react",
showcaseCounterPropertiesInfo
)}>
<ch-edit className="input" maxLength="20"></ch-edit>
</ch-counter>`,

stencil: () => `<ch-counter ${renderShowcaseProperties(
state,
"react",
showcaseCounterPropertiesInfo
)}>
<ch-edit class="input" maxLength="20"></ch-edit>
</ch-counter>`
},
render: render,
state: state
};
2 changes: 1 addition & 1 deletion src/showcase/assets/components/edit/edit.showcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ const render = () => (
autocapitalize={state.autocapitalize}
autocomplete={state.autocomplete}
autoGrow={state.autoGrow}
class="input"
class="input input-error"
debounce={state.debounce}
disabled={state.disabled}
maxLength={state.maxLength}
Expand Down
5 changes: 5 additions & 0 deletions src/showcase/assets/components/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ export const showcasePages: NavigationListModel = [
link: { url: "#switch" },
caption: "Switch",
metadata: EXPERIMENTAL
},
{
link: { url: "#counter" },
caption: "Counter",
metadata: EXPERIMENTAL
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions src/showcase/assets/components/showcase-stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { codeShowcaseStory } from "./code/code.showcase";
import { colorFieldShowcaseStory } from "./color-field/color-field.showcase";
import { colorPickerShowcaseStory } from "./color-picker/color-picker.showcase";
import { comboBoxShowcaseStory } from "./combo-box/combo-box.showcase";
import { counterShowcaseStory } from "./counter/counter.showcase";
import { dialogShowcaseStory } from "./dialog/dialog.showcase";
import { editShowcaseStory } from "./edit/edit.showcase";
import { flexibleLayoutShowcaseStory } from "./flexible-layout/flexible-layout.showcase";
Expand Down Expand Up @@ -53,6 +54,7 @@ export const showcaseStories = {
"color-field": colorFieldShowcaseStory,
"color-picker": colorPickerShowcaseStory,
"combo-box": comboBoxShowcaseStory,
counter: counterShowcaseStory,
dialog: dialogShowcaseStory,
edit: editShowcaseStory,
image: imageShowcaseStory,
Expand Down
1 change: 1 addition & 0 deletions src/showcase/assets/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export type ChameleonStories = {
"color-field": ShowcaseStory<HTMLChColorFieldElement>;
"color-picker": ShowcaseStory<HTMLChColorPickerElement>;
"combo-box": ShowcaseStory<HTMLChComboBoxRenderElement>;
counter: ShowcaseStory<HTMLChCounterElement>;
dialog: ShowcaseStory<HTMLChDialogElement>;
edit: ShowcaseStory<HTMLChEditElement>;
image: ShowcaseStory<HTMLChImageElement>;
Expand Down
Loading