1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46:40 +07:00

added text input

This commit is contained in:
muerwre 2019-07-30 12:10:37 +07:00
parent 5aca2508ea
commit 578b37716b
7 changed files with 527 additions and 0 deletions

View file

@ -0,0 +1,24 @@
import React, { FC } from 'react';
import { IIcon } from '~/redux/types';
type IProps = React.SVGAttributes<SVGElement> & {
size?: number;
icon: IIcon;
};
export const Icon: FC<IProps> = ({
size = 20,
icon,
...props
}) => (
<svg
width={size}
height={size}
viewBox={`0 0 24 24`}
preserveAspectRatio="xMidYMid slice"
{...props}
style={{ ...props.style, outline: 'none' }}
>
<use xlinkHref={`#${icon}`} />
</svg>
);

View file

@ -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<IInputTextProps> = ({
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<HTMLInputElement>) => 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 (
<div className={classNames(
styles.input_text_wrapper,
wrapperClassName,
{
[styles.required]: required,
[styles.focused]: focused,
[styles.has_status]: !!status || !!error,
[styles.has_value]: !!value,
[styles.has_error]: !!error,
[styles.has_loader]: is_loading,
},
)}>
<div className={styles.input}>
<input
type="text"
onChange={onInput}
className={classNames(styles.input_text, className)}
onFocus={onFocus}
onBlur={onBlur}
value={value || ''}
ref={setInnerRef}
{...props}
/>
</div>
<div className={styles.status}>
<div className={classNames(styles.success_icon, { active: status === 'success' })}>
<Icon icon="check" size={20} />
</div>
<div className={classNames(styles.error_icon, { active: status === 'error' || !!error })}>
<Icon icon="close" size={20} />
</div>
</div>
<div className={styles.loader}>
<div className={classNames({ active: is_loading })}>
<LoaderCircle size={20} />
</div>
</div>
{
title && <div className={styles.title}><span>{title}</span></div>
}
{
error && <div className={styles.error}><span>{error}</span></div>
}
</div>
);
};
export { InputText };

View file

@ -0,0 +1,37 @@
import React, { FC } from 'react';
import * as styles from './styles.scss';
interface IProps {
size?: number;
}
export const LoaderCircle: FC<IProps> = ({ size = 24 }) => (
<div className={styles.wrap}>
<svg width={size} height={size} viewBox="0 0 24 24">
<g strokeWidth={0.5}>
{
[...new Array(8)].map((el, i) => (
<path
d="M11,2 L11,6 C11,6.55228475 11.4477153,7 12,7 C12.5522847,7 13,6.55228475 13,6 L13,2 C13,1.44771525 12.5522847,1 12,1 C11.4477153,1 11,1.44771525 11,2 Z"
opacity={0.125 * (8 - i)}
transform={`rotate(${Math.floor(45 * i)} 12 12)`}
// style={{
// animationDelay: `${-100 * (8 - i)}ms`,
// }}
key={i}
/>
))
}
</g>
</svg>
</div>
);
/*
<div className={styles.wrap}>
<svg className={styles.icon} width={size} height={size}>
<path d={describeArc(size / 2, size / 2, size / 2, 0, 90)} />
<path d={describeArc(size / 2, size / 2, size / 2, 180, 270)} />
</svg>
</div>
*/

View file

@ -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;
}

View file

@ -1,4 +1,22 @@
import { DetailedHTMLProps, InputHTMLAttributes } from "react";
export type ITag = { export type ITag = {
title: string; title: string;
feature?: 'red' | 'blue' | 'green' | 'olive' | 'black'; feature?: 'red' | 'blue' | 'green' | 'olive' | 'black';
} }
export type IInputTextProps = DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, 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;

334
src/styles/inputs.scss Normal file
View file

@ -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;
}
}

View file

@ -58,6 +58,10 @@ $node_shadow: transparentize(black, 0.8) 1px 2px;
$tag_height: 22px; $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() { @mixin outer_shadow() {
box-shadow: inset transparentize(white, 0.95) 0 1px, box-shadow: inset transparentize(white, 0.95) 0 1px,
inset transparentize(black, 0.5) 0 -1px; inset transparentize(black, 0.5) 0 -1px;