From 578b37716b6cbcbfebd67db8c10a8ee355fc0105 Mon Sep 17 00:00:00 2001 From: muerwre Date: Tue, 30 Jul 2019 12:10:37 +0700 Subject: [PATCH] added text input --- src/components/input/Icon/index.tsx | 24 ++ src/components/input/InputText/index.tsx | 91 +++++ src/components/input/LoaderCircle/index.tsx | 37 ++ src/components/input/LoaderCircle/styles.scss | 19 + src/redux/types.ts | 18 + src/styles/inputs.scss | 334 ++++++++++++++++++ src/styles/variables.scss | 4 + 7 files changed, 527 insertions(+) create mode 100644 src/components/input/Icon/index.tsx create mode 100644 src/components/input/InputText/index.tsx create mode 100644 src/components/input/LoaderCircle/index.tsx create mode 100644 src/components/input/LoaderCircle/styles.scss create mode 100644 src/styles/inputs.scss diff --git a/src/components/input/Icon/index.tsx b/src/components/input/Icon/index.tsx new file mode 100644 index 00000000..01d52f8f --- /dev/null +++ b/src/components/input/Icon/index.tsx @@ -0,0 +1,24 @@ +import React, { FC } from 'react'; +import { IIcon } from '~/redux/types'; + +type IProps = React.SVGAttributes & { + size?: number; + icon: IIcon; +}; + +export const Icon: FC = ({ + size = 20, + icon, + ...props +}) => ( + + + +); diff --git a/src/components/input/InputText/index.tsx b/src/components/input/InputText/index.tsx new file mode 100644 index 00000000..35d79b88 --- /dev/null +++ b/src/components/input/InputText/index.tsx @@ -0,0 +1,91 @@ +import React, { + FC, + ChangeEvent, + useCallback, + useState, useEffect, +} from 'react'; +import * as styles from '~/styles/inputs.scss'; +import classNames from 'classnames'; +import { Icon } from '~/components/input/Icon'; +import { IInputTextProps } from '~/redux/types'; +import { LoaderCircle } from '~/components/input/LoaderCircle'; + +const InputText: FC = ({ + wrapperClassName, + className = '', + handler, + required = false, + status, + title, + error, + value = '', + onRef, + is_loading, + ...props +}) => { + const [focused, setFocused] = useState(false); + const [inner_ref, setInnerRef] = useState(); + + const onInput = useCallback( + ({ target }: ChangeEvent) => handler(target.value), + [handler], + ); + + const onFocus = useCallback(() => setFocused(true), [focused]); + const onBlur = useCallback(() => setFocused(false), [focused]); + + useEffect(() => { + if (onRef) onRef(inner_ref); + }, [inner_ref, onRef]); + + return ( +
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ { + title &&
{title}
+ } + { + error &&
{error}
+ } +
+ ); +}; + +export { InputText }; diff --git a/src/components/input/LoaderCircle/index.tsx b/src/components/input/LoaderCircle/index.tsx new file mode 100644 index 00000000..c1e7c96c --- /dev/null +++ b/src/components/input/LoaderCircle/index.tsx @@ -0,0 +1,37 @@ +import React, { FC } from 'react'; +import * as styles from './styles.scss'; + +interface IProps { + size?: number; +} + +export const LoaderCircle: FC = ({ size = 24 }) => ( +
+ + + { + [...new Array(8)].map((el, i) => ( + + )) + } + + +
+); + +/* +
+ + + + +
+ */ diff --git a/src/components/input/LoaderCircle/styles.scss b/src/components/input/LoaderCircle/styles.scss new file mode 100644 index 00000000..97a8132a --- /dev/null +++ b/src/components/input/LoaderCircle/styles.scss @@ -0,0 +1,19 @@ +.icon { + fill: transparentize(black, 0.6); + stroke: none; +} + +@keyframes spin { + 0% { transform: rotate(0); } + 100% { transform: rotate(360deg); } +} + +@keyframes fade { + 0% { opacity: 1; transform: scale(1); } + 100% { opacity: 0.1; transform: scale(4); } +} + +.wrap { + animation: spin infinite steps(9, end) 1s; + display: inline-flex; +} diff --git a/src/redux/types.ts b/src/redux/types.ts index 50ec4bf4..0cd935f0 100644 --- a/src/redux/types.ts +++ b/src/redux/types.ts @@ -1,4 +1,22 @@ +import { DetailedHTMLProps, InputHTMLAttributes } from "react"; + export type ITag = { title: string; feature?: 'red' | 'blue' | 'green' | 'olive' | 'black'; } + +export type IInputTextProps = DetailedHTMLProps, HTMLInputElement> & { + wrapperClassName?: string; + handler?: (value: string) => void; + required?: boolean; + title?: string; + error?: string; + can_negative?: boolean; + status?: string; + maskChar?: string; + mask?: string; + onRef?: (ref: any) => void; + is_loading?: boolean; +}; + +export type IIcon = string; diff --git a/src/styles/inputs.scss b/src/styles/inputs.scss new file mode 100644 index 00000000..8f48c439 --- /dev/null +++ b/src/styles/inputs.scss @@ -0,0 +1,334 @@ +.input_text_wrapper { + position: relative; + min-height: 40px; + border-radius: $input_radius; + box-shadow: $input_shadow; + flex: 1; + display: flex; + opacity: 1; + transition: opacity 0.25s; + z-index: 1; + + &::before { + content: ' '; + background: linear-gradient(270deg, white $gap, transparentize(white, 1)); + position: absolute; + width: $gap * 2; + height: $input_height - 4px; + top: 2px; + right: 2px; + transform: translateX(0); + transition: transform 0.25s; + border-radius: 0 $input_radius $input_radius 0; + pointer-events: none; + touch-action: none; + } + + :global(.react-datepicker-wrapper) { + flex: 1; + padding: 0 18px; + } + + &:hover { + opacity: 1; + } + + &.focused { + opacity: 1; + z-index: 999; + + &.has_status .status { + flex-basis: 0; + + div { + opacity: 0; + } + } + + &.select { + .title { + opacity: 0; + } + } + + .title { + color: transparentize(black, 0.3); + } + } + + input { + width: 100%; + } + + .input { + display: flex; + align-items: center; + justify-content: stretch; + padding: 0 18px; + flex: 1 0 0; + outline: none; + } + + &.required { + &::after { + content: ' '; + width: 5px; + height: 5px; + border-radius: 3px; + top: 8px; + left: 8px; + position: absolute; + + background: $red_gradient; + } + } + + &.has_loader { + &::before { transform: translateX(-40px); } + + .loader { + flex-basis: 40px; + } + } + + &.has_status { + &::before { transform: translateX(-40px); } + &.focused::before { transform: translateX(0); } + + .status { + flex-basis: 40px; + } + + .title { + padding-right: 40px; + } + + &.focused { + .title { + padding-right: 16px; + color: black; + } + } + } + + &.focused.has_status.has_loader { + &::before { transform: translateX(-80px); } + &.focused::before { transform: translateX(-40px); } + } + + &.has_error { + box-shadow: $input_shadow_error; + + .title { + color: transparentize(red, 0.4) !important; + } + + input, textarea { + color: $red; + } + } + + &.numeric { + flex: 0 0 120px; + + .input { + padding: 0 10px; + } + + .plus { + cursor: pointer; + } + + input { + margin: 0 10px; + flex: 0 0 40px; + text-align: center; + } + } + + &.select { + .input { + padding: 0 10px; + } + + .value { + padding: 0 8px; + } + } + + .password_revealer { + width: 40px; + height: 40px; + position: absolute; + top: 0; + right: 0; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + font-size: 24px; + user-select: none; + color: white; + opacity: 0.5; + transition: opacity 0.25s; + + &:hover { + opacity: 1; + } + } + + .input_text, .textarea { + outline: none; + border: none; + font: inherit; + box-sizing: border-box; + background: transparent; + color: black; + flex: 1; + resize: none; + } + + .textarea { + padding: 12px 0; + box-sizing: border-box; + width: 100%; + } + + .status, .loader { + flex: 0 0 0; + transition: flex-basis 500ms; + position: relative; + overflow: hidden; + pointer-events: none; + touch-action: none; + + & > div { + position: absolute; + left: 0; + top: 0; + width: $input_height; + height: $input_height; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.25s; + opacity: 0; + + &:global(.active) { + opacity: 1; + } + } + } + + .title { + font: $font; + position: absolute; + left: 0; + width: 100%; + top: 12px; + bottom: auto; + padding: 0 14px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: flex-start; + transition: top 0.25s, bottom 0.25s, font 0.25s, color 0.25s; + pointer-events: none; + touch-action: none; + color: transparentize(black, 0.3); + text-transform: capitalize; + + span { + background: white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 5px; + } + } + + &.focused .title, &.has_value .title { + font: $font_12_regular; + top: -10px; + bottom: auto; + } + + &.has_value { + box-shadow: $input_shadow_filled; + + .title { + color: #C2C2C2; + } + + &.focused { + .title { + color: transparentize(black, 0.3); + } + } + } + + .error { + font: $font_12_regular; + bottom: -6px; + left: 15px; + position: absolute; + color: $red; + + span { + background: white; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0 5px; + } + } + + .error_icon { + fill: $red; + stroke: $red; + } + + .success_icon { + fill: $green; + stroke: $green; + } +} + +.options { + position: absolute; + top: 0; + left: 0; + width: 100%; + border-radius: $input_radius; + box-shadow: $input_shadow; + background: white; + z-index: 10; +} + +.option:hover { + background: transparentize($red, 0.8); +} + +.option_title { + text-transform: capitalize; + color: transparentize(black, 0.5); + pointer-events: none; +} + +.options { + .option, .option_title { + height: $input_height; + display: flex; + align-items: center; + justify-content: flex-start; + padding: 0 10px; + cursor: pointer; + transition: background-color 0.1s; + border-radius: $input_radius; + } + + .option_title { + box-shadow: $input_shadow; + border-radius: $input_radius; + } +} diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 7ed447f6..95e811c3 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -58,6 +58,10 @@ $node_shadow: transparentize(black, 0.8) 1px 2px; $tag_height: 22px; +$input_shadow: inset white 0 0 0 1px; +$input_shadow_error: inset $red 0 0 0 1px; +$input_shadow_filled: $input_shadow; + @mixin outer_shadow() { box-shadow: inset transparentize(white, 0.95) 0 1px, inset transparentize(black, 0.5) 0 -1px;