import { html, PropertyValues, unsafeCSS } from "lit"; import { property, query, state } from "lit/decorators.js"; import { FRoot } from "../../mixins/components/f-root/f-root"; import eleStyle from "./f-button.scss?inline"; import globalStyle from "./f-button-global.scss?inline"; import { unsafeSVG } from "lit-html/directives/unsafe-svg.js"; import loader from "../../mixins/svg/loader"; import { classMap } from "lit-html/directives/class-map.js"; import LightenDarkenColor from "../../utils/get-lighten-darken-color"; import { validateHTMLColor } from "validate-color"; import { validateHTMLColorName } from "validate-color"; import getCustomFillColor from "../../utils/get-custom-fill-color"; import getTextContrast from "../../utils/get-text-contrast"; import { FIcon } from "../f-icon/f-icon"; import { FCounter } from "../f-counter/f-counter"; import { flowElement } from "./../../utils"; import { injectCss } from "@cldcvr/flow-core-config"; export type FButtonState = | "primary" | "neutral" | "success" | "warning" | "danger" | "inherit" | `custom, ${string}`; injectCss("f-button", globalStyle); /** * @summary Buttons allow users to perform an action or to initiate a new function. */ @flowElement("f-button") export class FButton extends FRoot { /** * css loaded from scss file */ static styles = [ unsafeCSS(eleStyle), unsafeCSS(globalStyle), ...FIcon.styles, ...FCounter.styles ]; /** * @attribute local state for managing custom fill. */ @state() fill = ""; /** * @attribute label property defines the text label on a button. Label of a button is always uppercase. */ @property({ type: String }) label!: string; /** * @attribute category of button */ @property({ reflect: true, type: String }) category?: "fill" | "outline" | "transparent" | "packed" = "fill"; /** * @attribute The medium size is the default and recommended option. */ @property({ reflect: true, type: String }) size?: "large" | "medium" | "small" | "x-small" = "medium"; /** * @attribute The states on buttons are to indicate various degrees of emphasis of the action. */ @property({ reflect: true, type: String }) state?: FButtonState = "primary"; /** * @attribute variant of button. */ @property({ reflect: true, type: String }) variant?: "round" | "curved" | "block" = "round"; /** * @attribute Icon-left enables an icon on the left of the label of a button. */ @property({ reflect: true, type: String, attribute: "icon-left" }) iconLeft?: string; /** * @attribute Icon-right enables an icon on the right of the label of a button. */ @property({ reflect: true, type: String, attribute: "icon-right" }) iconRight?: string; /** * @attribute Counter property enables a counter on the button. */ @property({ reflect: true, type: Number }) counter?: string; /** * @attribute Loader icon replaces the content of the button . */ @property({ reflect: true, type: Boolean }) loading?: boolean = false; /** * @attribute The disabled attribute can be set to keep a user from clicking on the button. */ @property({ reflect: true, type: Boolean }) disabled?: boolean = false; /** * @attribute Set true if want to wrap content if there is no space in button */ @property({ reflect: true, type: Boolean, attribute: "label-wrap" }) labelWrap?: boolean = false; /** * icon element reference */ @query("f-icon") iconElement!: FIcon; /** * counter element reference */ @query("f-counter") counterElement?: FCounter; /** * compute counter size based on button size */ get counterSize() { if (this.size === "small") { return "medium"; } if (this.size === "x-small") { return "small"; } return this.size; } /** * compute textColor when custom color of tag is defined. */ get textColor() { return getTextContrast(this.fill) === "dark-text" ? "#202a36" : "#fcfcfd"; } /** * mention required fields here for generating vue types */ readonly required = ["label"]; /** * validation for all atrributes */ validateProperties() { if (!this.label) { throw new Error("f-button : label is mandatory field"); } if ( this.state?.includes("custom") && this.fill && !validateHTMLColor(this.fill) && !validateHTMLColorName(this.fill) ) { throw new Error("f-button : enter correct color-name or hex-color-code"); } } /** * apply inline styles to shadow-dom for custom fill. */ applyStyles() { if (this.fill) { if (this.loading) { if (this.category === "fill") { return `background-color: ${LightenDarkenColor( this.fill, -150 )}; border: 1px solid ${LightenDarkenColor(this.fill, -150)}; color: transparent; fill: ${ this.fill }`; } else if (this.category === "outline") { return `background: transparent; border: 1px solid ${this.fill}; fill: ${this.fill};`; } else { return `background: transparent; border: none; fill: ${this.fill};`; } } else { if (this.category === "fill") { return `background: ${this.fill}; border: 1px solid ${this.fill}; color: ${this.textColor}`; } else if (this.category === "outline") { return `background: transparent; border: 1px solid ${this.fill}; color: ${this.fill}`; } else { return `background: transparent; border: none; color: ${this.fill}`; } } } else return ""; } render() { /** * creating local fill variable out of state prop. */ this.fill = getCustomFillColor(this.state ?? ""); /** * checks if host element's `:before` has shimmer by accessing computedstyles */ const hasShimmer = (getComputedStyle(this, "::before") as any)["animation-name"] === "shimmer"; /** * if hasShimmer true then add class */ if (hasShimmer) { this.classList.add("hasShimmer"); } /** * validate properties before render */ this.validateProperties(); /** * classes to apply on icon , based on category */ // const iconClasses = { // "fill-button-surface": this.category === "fill", // }; const iconClasses = { "fill-button-surface": this.category === "fill" && !this.fill ? true : false, "fill-button-surface-light": this.fill && this.category === "fill" && getTextContrast(this.fill) === "light-text" ? true : false, "fill-button-surface-dark": this.fill && this.category === "fill" && getTextContrast(this.fill) === "dark-text" ? true : false }; /** * create iconLeft if available */ const iconLeft = this.iconLeft ? html`` : ""; /** * create iconRight if available */ const iconRight = this.iconRight ? html`` : ""; /** * create counter if available */ const counterClasses = { "fill-button-surface": !this.fill && this.category === "fill" ? true : false, "fill-button-surface-light": this.category === "fill" && this.fill && getTextContrast(this.fill) === "light-text" ? true : false, "fill-button-surface-dark": this.category === "fill" && this.fill && getTextContrast(this.fill) === "dark-text" ? true : false }; const counter = this.counter ? html`` : ""; /** * render loading if required */ if (this.loading) { return html``; } /** * Final html to render */ return html``; } protected updated(changedProperties: PropertyValues) { super.updated(changedProperties); /** * Force update child element */ this.iconElement?.requestUpdate(); this.counterElement?.requestUpdate(); } } /** * Required for typescript */ declare global { interface HTMLElementTagNameMap { "f-button": FButton; } }