From c67565c2cbb60d38a7769130712413a2b65d7378 Mon Sep 17 00:00:00 2001 From: Vikas Awaghade <67629551+vikas-cldcvr@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:41:14 +0530 Subject: [PATCH] FLOW-970 f-color-picker component (#203) * FLOW-970 f-color-picker component created * FLOW-970 playground controls added * FLOW-970 playground controls added * FLOW-970 color picker integrated with form builder * FLOW-970 unit tests added * FLOW-970 changelog updated --- packages/flow-core/CHANGELOG.md | 6 + packages/flow-core/package.json | 5 +- .../f-color-picker/f-color-picker-global.scss | 9 + .../f-color-picker/f-color-picker.scss | 35 ++++ .../f-color-picker/f-color-picker.test.ts | 28 +++ .../f-color-picker/f-color-picker.ts | 197 ++++++++++++++++++ .../flow-core/src/components/f-div/f-div.ts | 16 +- .../components/f-form-field/f-form-field.ts | 9 +- packages/flow-core/src/index.ts | 1 + packages/flow-core/vite.config.ts | 9 +- packages/flow-form-builder/CHANGELOG.md | 6 + packages/flow-form-builder/package.json | 2 +- .../components/f-form-builder/fields/color.ts | 35 ++++ .../components/f-form-builder/fields/index.ts | 4 +- .../flow-form-builder/src/modules/helpers.ts | 6 +- packages/flow-form-builder/src/types.ts | 11 +- pnpm-lock.yaml | 7 + stories/flow-core/f-color-picker.stories.ts | 169 +++++++++++++++ .../flow-form-builder/f-formbuilder-field.ts | 28 ++- 19 files changed, 551 insertions(+), 32 deletions(-) create mode 100644 packages/flow-core/src/components/f-color-picker/f-color-picker-global.scss create mode 100644 packages/flow-core/src/components/f-color-picker/f-color-picker.scss create mode 100644 packages/flow-core/src/components/f-color-picker/f-color-picker.test.ts create mode 100644 packages/flow-core/src/components/f-color-picker/f-color-picker.ts create mode 100644 packages/flow-form-builder/src/components/f-form-builder/fields/color.ts create mode 100644 stories/flow-core/f-color-picker.stories.ts diff --git a/packages/flow-core/CHANGELOG.md b/packages/flow-core/CHANGELOG.md index 629c7ed67..696fa6418 100644 --- a/packages/flow-core/CHANGELOG.md +++ b/packages/flow-core/CHANGELOG.md @@ -2,6 +2,12 @@ # Change Log +## [2.6.0] - 2023-12-04 + +### Minor Changes + +- `f-color-picker` element added. + ## [2.5.0] - 2023-11-27 ### Minor Changes diff --git a/packages/flow-core/package.json b/packages/flow-core/package.json index 970ba3436..fe734e655 100644 --- a/packages/flow-core/package.json +++ b/packages/flow-core/package.json @@ -1,6 +1,6 @@ { "name": "@cldcvr/flow-core", - "version": "2.5.0", + "version": "2.6.0", "description": "Core package of flow design system", "module": "dist/flow-core.es.js", "main": "dist/flow-core.cjs.js", @@ -32,7 +32,8 @@ "lit": "^3.1.0", "lodash-es": "^4.17.21", "rxjs": "^7.5.7", - "validate-color": "^2.2.1" + "validate-color": "^2.2.1", + "vanilla-colorful": "^0.7.2" }, "devDependencies": { "@custom-elements-manifest/analyzer": "^0.8.1", diff --git a/packages/flow-core/src/components/f-color-picker/f-color-picker-global.scss b/packages/flow-core/src/components/f-color-picker/f-color-picker-global.scss new file mode 100644 index 000000000..45585d06e --- /dev/null +++ b/packages/flow-core/src/components/f-color-picker/f-color-picker-global.scss @@ -0,0 +1,9 @@ +@import "./../../mixins/scss/mixins"; + +f-color-picker { + @include base(); + width: fit-content; + height: fit-content; + max-width: 100%; + max-height: 100%; +} diff --git a/packages/flow-core/src/components/f-color-picker/f-color-picker.scss b/packages/flow-core/src/components/f-color-picker/f-color-picker.scss new file mode 100644 index 000000000..43344888a --- /dev/null +++ b/packages/flow-core/src/components/f-color-picker/f-color-picker.scss @@ -0,0 +1,35 @@ +@use "sass:map"; +$states: ( + "primary": ( + "border": 1px solid var(--color-primary-default) + ), + "default": ( + "border": none + ), + "success": ( + "border": 1px solid var(--color-success-default) + ), + "warning": ( + "border": 1px solid var(--color-warning-default) + ), + "danger": ( + "border": 1px solid var(--color-danger-default) + ) +); + +:host { + #f-color-picker-input { + cursor: pointer; + &.focused { + border: 1px solid var(--color-primary-default); + } + &[read-only] { + cursor: default; + } + @each $state, $color in $states { + &[border-state="#{$state}"]:not(.focused) { + border: map.get($color, "border"); + } + } + } +} diff --git a/packages/flow-core/src/components/f-color-picker/f-color-picker.test.ts b/packages/flow-core/src/components/f-color-picker/f-color-picker.test.ts new file mode 100644 index 000000000..5ade58917 --- /dev/null +++ b/packages/flow-core/src/components/f-color-picker/f-color-picker.test.ts @@ -0,0 +1,28 @@ +import { html, fixture, expect } from "@open-wc/testing"; + +// import flow-core elements +import "@cldcvr/flow-core"; + +import { FColorPicker, FInput } from "@cldcvr/flow-core"; + +describe("f-color-picker", () => { + // check if component is defined + it("is defined", () => { + const el = document.createElement("f-color-picker"); + expect(el).instanceOf(FColorPicker); + }); + it("should render with default attributes", async () => { + const el = await fixture(html``); + + expect(el.getAttribute("variant")).to.equal("curved"); + expect(el.getAttribute("size")).to.equal("medium"); + expect(el.getAttribute("state")).to.equal("default"); + }); + + it("should display color value in input box", async () => { + const el = await fixture(html``); + + const input = el.shadowRoot?.querySelector("#hex-input"); + expect(input.value).to.equal("#CCCCCC"); + }); +}); diff --git a/packages/flow-core/src/components/f-color-picker/f-color-picker.ts b/packages/flow-core/src/components/f-color-picker/f-color-picker.ts new file mode 100644 index 000000000..6d94427b8 --- /dev/null +++ b/packages/flow-core/src/components/f-color-picker/f-color-picker.ts @@ -0,0 +1,197 @@ +import { html, PropertyValueMap, unsafeCSS } from "lit"; +import { property, query, state } from "lit/decorators.js"; +import globalStyle from "./f-color-picker-global.scss?inline"; +import eleStyle from "./f-color-picker.scss?inline"; +import { FRoot } from "../../mixins/components/f-root/f-root"; +import { flowElement } from "./../../utils"; +import { injectCss } from "@cldcvr/flow-core-config"; + +import "vanilla-colorful"; +import { FPopover } from "../f-popover/f-popover"; +import { FDiv } from "../f-div/f-div"; +import { FInput } from "../f-input/f-input"; +import { classMap } from "lit-html/directives/class-map.js"; + +injectCss("f-color-picker", globalStyle); + +export type FColorPickerState = "primary" | "default" | "success" | "warning" | "danger"; +export type FColorPickerInputEvent = { + value?: string; +}; + +@flowElement("f-color-picker") +export class FColorPicker extends FRoot { + /** + * css loaded from scss file + */ + static styles = [ + unsafeCSS(eleStyle), + unsafeCSS(globalStyle), + ...FPopover.styles, + ...FDiv.styles, + ...FInput.styles + ]; + + /** + * @attribute Variants are various visual representations of an input field. + */ + @property({ reflect: true, type: String }) + variant?: "curved" | "round" | "block" = "curved"; + + /** + * @attribute States are used to communicate purpose and connotations. + */ + @property({ reflect: true, type: String }) + state?: FColorPickerState = "default"; + + /** + * @attribute f-input can have 2 sizes. By default size is inherited by the parent f-field. + */ + @property({ reflect: true, type: String }) + size?: "medium" | "small" = "medium"; + + /** + * @attribute Defines the value of an f-input. Validation rules are applied on the value depending on the type property of the f-text-input. + */ + @property({ reflect: true, type: String }) + value?: string; + + /** + * @attribute Shows disabled state of input element + */ + @property({ reflect: true, type: Boolean }) + disabled?: boolean = false; + + /** + * @attribute When true the user can not select the input element. + */ + @property({ reflect: true, type: Boolean, attribute: "read-only" }) + readOnly?: boolean = false; + + @query("#f-color-picker-popover") + popoverElement!: FPopover; + + @state() + isOpen = false; + + @query("#f-color-picker-input") + inputElement!: FDiv; + + defaultHexColor: string = "#000000"; + + handleColorChange(event: CustomEvent<{ value: string }>) { + this.value = event.detail.value; + this.dispatchInputEvent(this.value); + } + handleFocus(_event: FocusEvent) { + if (!this.readOnly) { + this.isOpen = true; + } + } + handleOverlayClick() { + this.isOpen = false; + } + + handleHexInput(event: CustomEvent) { + this.value = event.detail.value; + this.dispatchInputEvent(this.value); + } + handleKeydown(event: KeyboardEvent) { + if (event.key === "Enter") { + this.isOpen = false; + } + } + + dispatchInputEvent(value?: string) { + const event = new CustomEvent("input", { + detail: { + value + }, + bubbles: true, + composed: true + }); + + this.dispatchEvent(event); + } + + get isValueEmpty() { + return ( + this.value === undefined || this.value === null || (this.value && this.value.trim() === "") + ); + } + + sizeMap = { + small: "28px", + medium: "36px" + }; + get colorPickerTypeTemplate() { + return html` + + + + `; + } + + render() { + const classes = { focused: this.isOpen, "no-color": this.isValueEmpty }; + // render empty string, since there no need of any child element + return html` + + + + + + + + + + + ${this.colorPickerTypeTemplate} + + + `; + } + protected updated(changedProperties: PropertyValueMap | Map): void { + super.updated(changedProperties); + this.popoverElement.target = this.inputElement; + } +} + +/** + * Required for typescript + */ +declare global { + interface HTMLElementTagNameMap { + "f-color-picker": FColorPicker; + } +} diff --git a/packages/flow-core/src/components/f-div/f-div.ts b/packages/flow-core/src/components/f-div/f-div.ts index 924166e51..62fa92c80 100644 --- a/packages/flow-core/src/components/f-div/f-div.ts +++ b/packages/flow-core/src/components/f-div/f-div.ts @@ -360,13 +360,15 @@ export class FDiv extends FRoot { this.checkHighlight(); } - if (this.variant === "round") { - this.style.borderRadius = `${this.offsetHeight / 2}px`; - } else if (this.variant === "curved") { - this.style.borderRadius = `4px`; - } else { - this.style.borderRadius = "0px"; - } + void this.updateComplete.then(() => { + if (this.variant === "round") { + this.style.borderRadius = `${this.offsetHeight / 2}px`; + } else if (this.variant === "curved") { + this.style.borderRadius = `4px`; + } else { + this.style.borderRadius = "0px"; + } + }); } } diff --git a/packages/flow-core/src/components/f-form-field/f-form-field.ts b/packages/flow-core/src/components/f-form-field/f-form-field.ts index c2d09952a..f181bddd1 100644 --- a/packages/flow-core/src/components/f-form-field/f-form-field.ts +++ b/packages/flow-core/src/components/f-form-field/f-form-field.ts @@ -33,14 +33,7 @@ export class FFormField extends FRoot { - + diff --git a/packages/flow-core/src/index.ts b/packages/flow-core/src/index.ts index a416c71b0..abfc8ab2c 100644 --- a/packages/flow-core/src/index.ts +++ b/packages/flow-core/src/index.ts @@ -47,6 +47,7 @@ export * from "./components/f-document-viewer/f-document-viewer"; export * from "./mixins/components/f-root/f-root"; export * from "./components/f-form-field/f-form-field"; export * from "./components/f-input/f-input-light"; +export * from "./components/f-color-picker/f-color-picker"; export { html } from "lit"; diff --git a/packages/flow-core/vite.config.ts b/packages/flow-core/vite.config.ts index bcb704ca1..175727bee 100644 --- a/packages/flow-core/vite.config.ts +++ b/packages/flow-core/vite.config.ts @@ -16,7 +16,14 @@ export default defineConfig({ // If we want to publish standalone components we don't externalize lit, // if you are going to use lit in your own project, you can make it a dep instead. // external: /^lit/, <-- comment this line - external: ["axios", "emoji-mart", /^lit/, "rxjs", "@cldcvr/flow-core-config"] + external: [ + "axios", + "emoji-mart", + /^lit/, + "rxjs", + "@cldcvr/flow-core-config", + "vanilla-colorful" + ] } } }); diff --git a/packages/flow-form-builder/CHANGELOG.md b/packages/flow-form-builder/CHANGELOG.md index 9e6ad2725..9d251f307 100644 --- a/packages/flow-form-builder/CHANGELOG.md +++ b/packages/flow-form-builder/CHANGELOG.md @@ -2,6 +2,12 @@ # Change Log +## [2.2.0] - 2023-12-04 + +### Minor Changes + +- `f-color-picker` field integrated + ## [2.1.0] - 2023-11-27 ### Minor Changes diff --git a/packages/flow-form-builder/package.json b/packages/flow-form-builder/package.json index cf1809ca1..3dfc44228 100644 --- a/packages/flow-form-builder/package.json +++ b/packages/flow-form-builder/package.json @@ -1,6 +1,6 @@ { "name": "@cldcvr/flow-form-builder", - "version": "2.1.0", + "version": "2.2.0", "description": "Form builder for the flow design system", "module": "dist/flow-form-builder.es.js", "main": "dist/flow-form-builder.cjs.js", diff --git a/packages/flow-form-builder/src/components/f-form-builder/fields/color.ts b/packages/flow-form-builder/src/components/f-form-builder/fields/color.ts new file mode 100644 index 000000000..19223449d --- /dev/null +++ b/packages/flow-form-builder/src/components/f-form-builder/fields/color.ts @@ -0,0 +1,35 @@ +import { html } from "lit"; +import { FFormInputElements, FormBuilderColorPickerField, FormBuilderField } from "../../../types"; +import { Ref, ref } from "lit/directives/ref.js"; +import { ifDefined } from "lit-html/directives/if-defined.js"; +import { getSlots } from "../../../modules/helpers"; +export default function ( + name: string, + _field: FormBuilderField, + fieldRef: Ref, + value: unknown +) { + const field = _field as FormBuilderColorPickerField; + return html` + + ${getSlots(name, field)} + + `; +} diff --git a/packages/flow-form-builder/src/components/f-form-builder/fields/index.ts b/packages/flow-form-builder/src/components/f-form-builder/fields/index.ts index ccd3c18b8..037ee9fc2 100644 --- a/packages/flow-form-builder/src/components/f-form-builder/fields/index.ts +++ b/packages/flow-form-builder/src/components/f-form-builder/fields/index.ts @@ -15,6 +15,7 @@ import separator from "./separator"; import emoji from "./emoji"; import hidden from "./hidden"; import datetime from "./datetime"; +import color from "./color"; const all: Record = { text, @@ -37,7 +38,8 @@ const all: Record = { separator, emoji, hidden, - datetime + datetime, + color }; export default all; diff --git a/packages/flow-form-builder/src/modules/helpers.ts b/packages/flow-form-builder/src/modules/helpers.ts index 6e4be987d..f30557b9b 100644 --- a/packages/flow-form-builder/src/modules/helpers.ts +++ b/packages/flow-form-builder/src/modules/helpers.ts @@ -27,7 +27,8 @@ import { FSelect, FSuggest, FSwitch, - FTextArea + FTextArea, + FColorPicker } from "@cldcvr/flow-core"; import checkboxGroupGlobalStyles from "./../components/f-checkbox-group/f-checkbox-group-global.scss?inline"; @@ -167,9 +168,10 @@ export function getEssentialFlowCoreStyles(): CSSResult[] { ...FSuggest.styles, ...FSwitch.styles, ...FTextArea.styles, + ...FColorPicker.styles, unsafeCSS(checkboxGroupGlobalStyles), unsafeCSS(radioGroupGlobalStyles), unsafeCSS(fieldSeparatorGlobalStyles), unsafeCSS(formObjectGlobalStyles) - ]; + ] as CSSResult[]; } diff --git a/packages/flow-form-builder/src/types.ts b/packages/flow-form-builder/src/types.ts index eeb90c6d4..fb147bff9 100644 --- a/packages/flow-form-builder/src/types.ts +++ b/packages/flow-form-builder/src/types.ts @@ -19,7 +19,8 @@ import { FDateTimePickerState, FDateOption, DateDisableType, - FRootTooltip + FRootTooltip, + FColorPickerState } from "@cldcvr/flow-core"; import { BetweenParams } from "./modules/validation/rules/between"; import { Subject } from "rxjs"; @@ -56,6 +57,11 @@ export type FormBuilderArrayField = FormBuilderBaseField & { label: FormBuilderLabel; allowEmpty?: boolean; }; +export type FormBuilderColorPickerField = FormBuilderBaseField & { + type: "color"; + state?: FColorPickerState; + readOnly?: boolean; +}; export type FormBuilderSeparatorField = FormBuilderFieldEvents & { id?: string; // id to uniquely identify in DOM state?: FDividerState; @@ -267,7 +273,8 @@ export type FormBuilderField = | FormBuilderSeparatorField | FormBuilderEmojiField | FormBuilderHiddenField - | FormBuilderDatetimeField; // add other field types + | FormBuilderDatetimeField + | FormBuilderColorPickerField; // add other field types export type FormBuilderShowCondition = (value: T) => boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5ef4d865..5776b29b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -263,6 +263,9 @@ importers: validate-color: specifier: ^2.2.1 version: 2.2.4 + vanilla-colorful: + specifier: ^0.7.2 + version: 0.7.2 devDependencies: '@custom-elements-manifest/analyzer': specifier: ^0.8.1 @@ -14630,6 +14633,10 @@ packages: spdx-expression-parse: 3.0.1 dev: true + /vanilla-colorful@0.7.2: + resolution: {integrity: sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg==} + dev: false + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} diff --git a/stories/flow-core/f-color-picker.stories.ts b/stories/flow-core/f-color-picker.stories.ts new file mode 100644 index 000000000..691eff628 --- /dev/null +++ b/stories/flow-core/f-color-picker.stories.ts @@ -0,0 +1,169 @@ +import { html } from "lit-html"; + +export default { + title: "@cldcvr/flow-core/f-color-picker", + + parameters: { + controls: { + hideNoControlsWarning: true + } + } +}; + +export const Playground = { + render: (args: Record) => { + return html``; + }, + + name: "Playground", + + parameters: { + docs: { + inlineStories: false, + iframeHeight: 200 + } + }, + + argTypes: { + variant: { + control: "select", + options: ["curved", "round", "block"] + }, + state: { + control: "select", + options: ["default", "success", "warning", "danger", "primary"] + }, + size: { + control: "select", + options: ["medium", "small"] + }, + value: { + control: "text" + }, + disabled: { + control: "boolean" + }, + ["read-only"]: { + control: "boolean" + } + }, + + args: { + variant: "curved", + state: "default", + size: "medium", + value: "#000", + disabled: false, + ["read-only"]: false + } +}; + +export const Variant = { + render: () => { + const variants = ["curved", "round", "block"]; + + return html` + + ${variants.map( + item => + html` + Label (variant="${item}") + This is a Subtext (Helper Text) + Optional + + ` + )} + + `; + }, + + name: "variant" +}; + +export const Size = { + render: () => { + const sizes = ["small", "medium"]; + + return html` + + ${sizes.map( + item => + html` + Label (size="${item}") + This is a Subtext (Helper Text) + ` + )} + + `; + }, + + name: "size" +}; + +export const State = { + render: () => { + const states = [ + ["default", "primary", "success"], + ["danger", "warning"] + ]; + + return html` + + ${states.map( + item => + html` + ${item.map( + state => + html` + Label (state="${state}") + This is a Subtext (Helper Text) + ` + )} + ` + )} + + `; + }, + + name: "state" +}; + +export const Flags = { + render: () => { + const flagNames: Record = { + 0: "Disabled", + 1: "Readonly" + }; + + return html` + + ${[0, 1].map( + item => + html` + ${flagNames[item]} + This is a Subtext (Helper Text) + ` + )} + + `; + }, + + name: "Flags" +}; diff --git a/stories/flow-form-builder/f-formbuilder-field.ts b/stories/flow-form-builder/f-formbuilder-field.ts index d3eb4ceca..ffcb496ea 100644 --- a/stories/flow-form-builder/f-formbuilder-field.ts +++ b/stories/flow-form-builder/f-formbuilder-field.ts @@ -13,16 +13,27 @@ const field: FormBuilderField = { description: "following fields are used for demo purpose only" }, fields: { - number: { - type: "number", - validationRules: [ - { - name: "max-value", - params: { - max: 5 + paymentInfo: { + type: "object", + fields: { + cardNumber: { + type: "number", + validationRules: [ + { + name: "max-value", + params: { + max: 5 + } + } + ] + }, + color: { + type: "color", + label: { + title: "Color" } } - ] + } }, hiddenField: { type: "hidden", @@ -62,6 +73,7 @@ const field: FormBuilderField = { state: "success", suffix: "suggested" }, + logo: { label: { title: "Logo" }, type: "emoji",