From fa7dc0b247026daba2bf2b78dc21c61a109bfb68 Mon Sep 17 00:00:00 2001 From: satbir121 <39981226+satbir121@users.noreply.github.com> Date: Fri, 14 Aug 2020 10:28:03 +0530 Subject: [PATCH] Text and TextInput component ads (#249) * Adding blank components. * Adding Todo note for tree * Adding todo note for LighteningMenu * ads button component * Adding storybook support. * storybook integrated with button component * ads button component props completed * button component icon and loading logic implemented * button component completed * Added a text knob. * Adding default text for button. * Merging theme and other fixes. * Fixed info button. * Better types. * Adding background param to components. * Re-using vsariant for callount. * Added props for Text input. * Adding text component. * feedback changes added in button and icon component * type any removed in button component * Text component created with typography and story * Fixing spinner issues * Textinput component ads (#252) * text input component with story is implemented * Update README.md * Fixing spinner issues * Fixing hexToRGBA import * story error fixed and text component commented for now * props condition fixed and added error input story * unused import removed Co-authored-by: Rohit Kumawat Co-authored-by: Nikhil Nandagopal * story spacing added and component name fixed * feedback changes implemented * forward ref added * feedback changes implemented * text and textinput stories updated * pr comments resolved Co-authored-by: Rohit Kumawat Co-authored-by: Nikhil Nandagopal --- app/client/.storybook/addons.js | 3 +- app/client/src/components/ads/Button.tsx | 49 +----- app/client/src/components/ads/Spinner.tsx | 3 +- app/client/src/components/ads/Text.tsx | 56 ++++++- app/client/src/components/ads/TextInput.tsx | 141 ++++++++++++++++-- app/client/src/components/ads/common.tsx | 34 ++++- .../src/components/stories/Button.stories.tsx | 2 +- .../src/components/stories/Text.stories.tsx | 74 +++++++++ .../components/stories/TextInput.stories.tsx | 50 +++++++ app/client/src/constants/DefaultTheme.tsx | 18 ++- 10 files changed, 363 insertions(+), 67 deletions(-) create mode 100644 app/client/src/components/stories/Text.stories.tsx create mode 100644 app/client/src/components/stories/TextInput.stories.tsx diff --git a/app/client/.storybook/addons.js b/app/client/.storybook/addons.js index 63bd2d5681..0503703e34 100644 --- a/app/client/.storybook/addons.js +++ b/app/client/.storybook/addons.js @@ -2,4 +2,5 @@ import "@storybook/addon-knobs/register"; import "@storybook/addon-notes/register"; import "@storybook/addon-contexts/register"; import "storybook-addon-designs/register"; -import '@storybook/addon-backgrounds/register'; \ No newline at end of file +import '@storybook/addon-backgrounds/register'; +import '@storybook/addon-actions/register'; \ No newline at end of file diff --git a/app/client/src/components/ads/Button.tsx b/app/client/src/components/ads/Button.tsx index d0bc61d055..9713ee73c6 100644 --- a/app/client/src/components/ads/Button.tsx +++ b/app/client/src/components/ads/Button.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { CommonComponentProps } from "./common"; +import { CommonComponentProps, hexToRgba, ThemeProp } from "./common"; import styled from "styled-components"; import { IconName, Icon } from "./Icon"; import Spinner from "./Spinner"; @@ -7,13 +7,8 @@ import { mediumButton, smallButton, largeButton, - Theme, } from "../../constants/DefaultTheme"; -export type ThemeProp = { - theme: Theme; -}; - export enum Category { primary = "primary", secondary = "secondary", @@ -65,40 +60,6 @@ type ButtonProps = CommonComponentProps & { size?: Size; }; -function hexToRgb( - hex: string, -): { - r: number; - g: number; - b: number; -} { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result - ? { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16), - } - : { - r: -1, - g: -1, - b: -1, - }; -} - -// const darken = (color: Color, intensity: number) => { -// return new tinycolor(color).darken(intensity).toString(); -// }; - -// const lighten = (color: Color, intensity: number) => { -// return new tinycolor(color).lighten(intensity).toString(); -// }; - -const hexToRgba = (color: string, alpha: number) => { - const value = hexToRgb(color); - return `rgba(${value.r}, ${value.g}, ${value.b}, ${alpha});`; -}; - const stateStyles = ( props: ThemeProp & ButtonProps, state: string, @@ -113,7 +74,7 @@ const stateStyles = ( borderColorTertiary, txtColorTertiary; - if (props.isLoading || props.isDisabled) { + if (props.isLoading || props.disabled) { switch (props.category) { case Category.primary: if (props.variant) { @@ -302,7 +263,7 @@ const StyledButton = styled("button")` color: ${props => btnColorStyles(props, "hover").txtColor}; border: ${props => btnColorStyles(props, "hover").border}; cursor: ${props => - props.isLoading || props.isDisabled ? `not-allowed` : `pointer`}; + props.isLoading || props.disabled ? `not-allowed` : `pointer`}; .ads-icon { margin-right: ${props => props.text && props.icon ? `${props.theme.spaces[4]}px` : `0`} @@ -317,7 +278,7 @@ const StyledButton = styled("button")` color: ${props => btnColorStyles(props, "active").txtColor}; border: ${props => btnColorStyles(props, "active").border}; cursor: ${props => - props.isLoading || props.isDisabled ? `not-allowed` : `pointer`}; + props.isLoading || props.disabled ? `not-allowed` : `pointer`}; .ads-icon { path { fill: ${props => btnColorStyles(props, "active").txtColor}; @@ -340,7 +301,7 @@ Button.defaultProps = { variant: Variant.success, size: Size.small, isLoading: false, - isDisabled: false, + disabled: false, }; export const VisibilityWrapper = styled.div` diff --git a/app/client/src/components/ads/Spinner.tsx b/app/client/src/components/ads/Spinner.tsx index 269d923aa2..137382a5c5 100644 --- a/app/client/src/components/ads/Spinner.tsx +++ b/app/client/src/components/ads/Spinner.tsx @@ -1,6 +1,7 @@ import React from "react"; import styled, { keyframes } from "styled-components"; -import { Size, ThemeProp } from "./Button"; +import { ThemeProp } from "./common"; +import { Size } from "./Button"; export const sizeHandler = (props: ThemeProp & SpinnerProp) => { let iconSize = 0; diff --git a/app/client/src/components/ads/Text.tsx b/app/client/src/components/ads/Text.tsx index 6121a5500e..d1eda51125 100644 --- a/app/client/src/components/ads/Text.tsx +++ b/app/client/src/components/ads/Text.tsx @@ -1,11 +1,53 @@ -import React from "react"; +import styled from "styled-components"; +import { ThemeProp } from "./common"; + +export enum TextType { + P1 = "p1", + P2 = "p2", + P3 = "p3", + H1 = "h1", + H2 = "h2", + H3 = "h3", + H4 = "h4", + H5 = "h5", + H6 = "h6", +} export type TextProps = { - type: "p1" | "p2" | "p3" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; - underline: boolean; - italic: boolean; + type: TextType; + underline?: boolean; + italic?: boolean; }; -export default function Text(props: TextProps) { - return ; -} +const typeSelector = (props: TextProps & ThemeProp): string => { + let color = ""; + switch (props.type) { + case TextType.P1: + color = props.theme.colors.blackShades[6]; + break; + case TextType.P2: + color = props.theme.colors.blackShades[6]; + break; + case TextType.P3: + color = props.theme.colors.blackShades[6]; + break; + default: + color = props.theme.colors.blackShades[7]; + break; + } + return color; +}; + +const Text = styled.span` + text-decoration: ${props => (props.underline ? "underline" : "unset")}; + font-style: ${props => (props.italic ? "italic" : "normal")}; + font-family: ${props => props.theme.fonts[2]}; + font-weight: ${props => props.theme.typography[props.type].fontWeight}; + font-size: ${props => props.theme.typography[props.type].fontSize}px; + line-height: ${props => props.theme.typography[props.type].lineHeight}px; + letter-spacing: ${props => + props.theme.typography[props.type].letterSpacing}px; + color: ${props => typeSelector(props)}; +`; + +export default Text; diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx index a762a3eeb4..b572399a6c 100644 --- a/app/client/src/components/ads/TextInput.tsx +++ b/app/client/src/components/ads/TextInput.tsx @@ -1,15 +1,134 @@ -import { CommonComponentProps } from "./common"; +import React, { forwardRef, Ref, useCallback, useMemo, useState } from "react"; +import { CommonComponentProps, hexToRgba } from "./common"; +import styled from "styled-components"; +import { theme } from "../../constants/DefaultTheme"; +import Text, { TextType } from "./Text"; -type TextProps = CommonComponentProps & { +export type TextInputProps = CommonComponentProps & { placeholder?: string; - value: string; - hasError: boolean; - disabled: boolean; - validator: (value: string) => { isValid: boolean; message: string }; - onChange: (value: string) => void; - cypressSelector?: string; + fill?: boolean; + defaultValue?: string; + validator?: (value: string) => { isValid: boolean; message: string }; + onChange?: (value: string) => void; }; -export default function Text(props: TextProps) { - return null; -} +type boxReturnType = { + bgColor: string; + color: string; + borderColor: string; +}; + +const boxStyles = (props: TextInputProps, isValid: boolean): boxReturnType => { + let bgColor = theme.colors.blackShades[0]; + let color = theme.colors.blackShades[9]; + let borderColor = theme.colors.blackShades[0]; + + if (props.disabled) { + bgColor = theme.colors.blackShades[2]; + color = theme.colors.blackShades[6]; + borderColor = theme.colors.blackShades[2]; + } + if (!isValid) { + bgColor = hexToRgba(theme.colors.danger.main, 0.1); + color = theme.colors.danger.main; + borderColor = theme.colors.danger.main; + } + return { bgColor, color, borderColor }; +}; + +const StyledInput = styled.input< + TextInputProps & { inputStyle: boxReturnType; isValid: boolean } +>` + width: ${props => (props.fill ? "100%" : "260px")}; + border-radius: 0; + outline: 0; + box-shadow: none; + margin-bottom: ${props => props.theme.spaces[1]}px; + border: 1px solid ${props => props.inputStyle.borderColor}; + font-family: ${props => props.theme.fonts[3]}; + padding: ${props => props.theme.spaces[4]}px + ${props => props.theme.spaces[6]}px; + background-color: ${props => props.inputStyle.bgColor}; + color: ${props => props.inputStyle.color}; + + &::placeholder { + color: ${props => props.theme.colors.blackShades[5]}; + } + &:focus { + border: 1px solid + ${props => + props.isValid + ? props.theme.colors.info.main + : props.theme.colors.danger.main}; + box-shadow: 0px 0px 0px 4px rgba(203, 72, 16, 0.18); + } + &:disabled { + cursor: not-allowed; + } +`; + +const InputWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + + span { + color: ${props => props.theme.colors.danger.main}; + } +`; + +const TextInput = forwardRef( + (props: TextInputProps, ref: Ref) => { + const initialValidation = () => { + let validationObj = { isValid: true, message: "" }; + if (props.defaultValue && props.validator) { + validationObj = props.validator(props.defaultValue); + } + return validationObj; + }; + + const [validation, setValidation] = useState<{ + isValid: boolean; + message: string; + }>(initialValidation()); + + const inputStyle = useMemo(() => boxStyles(props, validation.isValid), [ + props.disabled, + validation, + ]); + + const memoizedChangeHandler = useCallback( + el => { + props.validator && setValidation(props.validator(el.target.value)); + return props.onChange && props.onChange(el.target.value); + }, + [props], + ); + + const ErrorMessage = {validation.message}; + + return ( + + + {validation.isValid ? null : ErrorMessage} + + ); + }, +); + +TextInput.defaultProps = { + fill: false, +}; + +TextInput.displayName = "TextInput"; + +export default TextInput; diff --git a/app/client/src/components/ads/common.tsx b/app/client/src/components/ads/common.tsx index 23a2c57866..c542433752 100644 --- a/app/client/src/components/ads/common.tsx +++ b/app/client/src/components/ads/common.tsx @@ -1,5 +1,37 @@ +import { Theme } from "../../constants/DefaultTheme"; + export interface CommonComponentProps { isLoading?: boolean; //default false cypressSelector?: string; - isDisabled?: boolean; //default false + disabled?: boolean; //default false } + +export type ThemeProp = { + theme: Theme; +}; + +export const hexToRgb = ( + hex: string, +): { + r: number; + g: number; + b: number; +} => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : { + r: -1, + g: -1, + b: -1, + }; +}; + +export const hexToRgba = (color: string, alpha: number) => { + const value = hexToRgb(color); + return `rgba(${value.r}, ${value.g}, ${value.b}, ${alpha});`; +}; diff --git a/app/client/src/components/stories/Button.stories.tsx b/app/client/src/components/stories/Button.stories.tsx index fd39bd187b..ecf8b9d7d7 100644 --- a/app/client/src/components/stories/Button.stories.tsx +++ b/app/client/src/components/stories/Button.stories.tsx @@ -24,7 +24,7 @@ export const withDynamicProps = () => ( )} icon={select("iconName", ["delete", "user"], undefined)} isLoading={boolean("Loading", false)} - isDisabled={boolean("Disabled", false)} + disabled={boolean("Disabled", false)} text={text("text", "Get")} > ); diff --git a/app/client/src/components/stories/Text.stories.tsx b/app/client/src/components/stories/Text.stories.tsx new file mode 100644 index 0000000000..284e074e85 --- /dev/null +++ b/app/client/src/components/stories/Text.stories.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { boolean, select, text, withKnobs } from "@storybook/addon-knobs"; +import Text, { TextType } from "../ads/Text"; +import styled from "styled-components"; + +export default { + title: "Text", + component: Text, + decorators: [withKnobs], +}; + +const StyledDiv = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + margin-right: 50px; + + span { + align-self: left; + margin-bottom: 10px; + } +`; + +export const Typography = () => ( +
+ + Hi there, I am h1 element. + Hi there, I am h2 element. + Hi there, I am h3 element. + Hi there, I am h4 element. + Hi there, I am h5 element. + Hi there, I am h6 element. + + +
+ + + Hi there, I am p1 element. + Hi there, I am p2 element. + Hi there, I am p3 element. + +
+); + +const ValueWrapper = (props: { type: TextType; value: string }) => ( + + {props.value} + +); + +export const SingleText = () => ( + +); diff --git a/app/client/src/components/stories/TextInput.stories.tsx b/app/client/src/components/stories/TextInput.stories.tsx new file mode 100644 index 0000000000..109a6546fc --- /dev/null +++ b/app/client/src/components/stories/TextInput.stories.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { withKnobs, boolean, text } from "@storybook/addon-knobs"; +import TextInput from "../ads/TextInput"; +import { action } from "@storybook/addon-actions"; + +export default { + title: "Text Input", + component: TextInput, + decorators: [withKnobs], +}; + +const callValidator1 = () => { + return { + isValid: true, + message: "This is a warning text for the above field.", + }; +}; + +export const TextInputStory = () => ( +
+ callValidator1()} + > +
+); + +const callValidator2 = () => { + return { + isValid: false, + message: "This is a warning text for the above field.", + }; +}; + +export const ErrorTextInputStory = () => ( +
+ console.log(value)} + defaultValue={text("defaultValue", "This is wrong")} + validator={() => callValidator2()} + > +
+); diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 8222f19876..a450b01281 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -431,44 +431,56 @@ export const theme: Theme = { h1: { fontSize: 20, lineHeight: 27, + letterSpacing: "normal", + fontWeight: 500, }, h2: { fontSize: 18, lineHeight: 25, + letterSpacing: "normal", + fontWeight: 500, }, h3: { fontSize: 17, lineHeight: 22, + letterSpacing: "normal", + fontWeight: 500, }, h4: { fontSize: 16, lineHeight: 21, letterSpacing: -0.24, + fontWeight: 500, }, h5: { fontSize: 14, lineHeight: 19, letterSpacing: -0.24, + fontWeight: 500, }, h6: { fontSize: 12, lineHeight: 14, letterSpacing: 0.8, + fontWeight: 500, }, p1: { fontSize: 14, lineHeight: 19, letterSpacing: -0.24, + fontWeight: "normal", }, p2: { fontSize: 13, lineHeight: 17, letterSpacing: -0.24, + fontWeight: "normal", }, p3: { fontSize: 12, lineHeight: 16, letterSpacing: -0.221538, + fontWeight: "normal", }, btnLarge: { fontSize: 13, @@ -591,7 +603,11 @@ export const theme: Theme = { lightningborder: Colors.ALABASTER, }, lineHeights: [0, 14, 16, 18, 22, 24, 28, 36, 48, 64, 80], - fonts: [FontFamilies.DMSans, FontFamilies.FiraCode], + fonts: [ + FontFamilies.DMSans, + FontFamilies.FiraCode, + FontFamilies.HomePageRedesign, + ], borders: [ { thickness: 1,