Skip to content

Commit

Permalink
feat: add validation props to input (#441)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Kostro <kostro.d@gmail.com>
  • Loading branch information
Sebastien-Ahkrin and stropitek authored Jan 10, 2023
1 parent f0d8118 commit 086f0b8
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 89 deletions.
171 changes: 129 additions & 42 deletions src/components/forms/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
import styled from '@emotion/styled';
import { InputHTMLAttributes, ReactNode } from 'react';
import { CSSProperties, InputHTMLAttributes, ReactNode } from 'react';

import { FullSpinner } from '../index';

import { useFieldsContext } from './context/FieldsContext';

type InputVariant = 'default' | 'small';

interface StyledProps {
variant: 'default' | 'small';
variant: InputVariant;
hasLeading: boolean;
hasTrailing: boolean;
}

function getSpecialSize(props: Pick<StyledProps, 'variant'>) {
return {
fontSize: props.variant === 'small' ? '1em' : '1.125em',
lineHeight: props.variant === 'small' ? '15px' : '17px',
};
}

const LabelStyled = styled.label<StyledProps>`
padding: ${(props) =>
props.variant === 'default'
? props.hasTrailing
? '4px 11px 4px 11px'
: '4px 11px'
? '2px 9px 4px 9px'
: '2px 9px'
: props.hasTrailing
? '0px 7px 0px 7px'
: '0 7px'};
? '1px 7px 1px 7px'
: '1px 7px'};
font-size: ${(props) => getSpecialSize(props).fontSize};
line-height: ${(props) => getSpecialSize(props).lineHeight};
font-size: ${(props) => (props.variant === 'small' ? '1em' : '1.125em')};
line-height: '17px';
background-color: white;
border-width: 1px;
Expand Down Expand Up @@ -55,16 +52,43 @@ const LabelStyled = styled.label<StyledProps>`
border-color: var(--custom-border-color);
`;

const GroupStyled = styled.div`
function getStyleColor(hasError: boolean, hasValid: boolean) {
if (hasError) {
return {
default: '#ffa39e',
hover: '#f95d55',
};
}

if (hasValid) {
return {
default: '#6adc24',
hover: '#62cb21',
};
}

return {
default: 'rgb(217, 217, 217)',
hover: '#4096ff',
};
}

const GroupStyled = styled.div<{ hasError: boolean; hasValid: boolean }>`
display: flex;
border-radius: 0.375rem;
margin-top: 0.25rem;
--custom-border-color: rgb(217, 217, 217);
.addon {
color: ${({ hasError }) => hasError && '#f95d55'};
}
--custom-border-color: ${({ hasError, hasValid }) =>
getStyleColor(hasError, hasValid).default};
:hover,
:focus-within {
--custom-border-color: #4096ff;
--custom-border-color: ${({ hasError, hasValid }) =>
getStyleColor(hasError, hasValid).hover};
}
`;

Expand Down Expand Up @@ -118,6 +142,12 @@ const TrailingInlineAddonStyled = styled.div`
padding-left: 0.5rem;
`;

const RootInput = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
`;

interface RenderAddon {
addon: ReactNode;
inline?: boolean;
Expand All @@ -128,41 +158,98 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {

leadingAddon?: RenderAddon;
trailingAddon?: RenderAddon;

help?: string;
error?: string;
valid?: true | string;

loading?: boolean;
}

export function Input(props: InputProps) {
const { variant, trailingAddon, leadingAddon, ...otherProps } = props;
const {
variant: variantProps,
trailingAddon,
leadingAddon,
help,
error,
valid,
loading,
...otherProps
} = props;

const { name, variant: contextVariant } = useFieldsContext();

const hasLeading = (leadingAddon && !leadingAddon.inline) || false;
const hasTrailing = (trailingAddon && !trailingAddon.inline) || false;
const variant = variantProps || contextVariant;

return (
<GroupStyled>
{leadingAddon && !leadingAddon.inline && (
<LeadingAddonStyled>{leadingAddon.addon}</LeadingAddonStyled>
)}
<LabelStyled
variant={variant || contextVariant}
hasLeading={hasLeading}
hasTrailing={hasTrailing}
>
{leadingAddon?.inline && (
<LeadingInlineAddonStyled>
{leadingAddon.addon}
</LeadingInlineAddonStyled>
<RootInput>
<GroupStyled hasError={!!error} hasValid={!!valid}>
{leadingAddon && !leadingAddon.inline && (
<LeadingAddonStyled>{leadingAddon.addon}</LeadingAddonStyled>
)}
<InputStyled id={name} name={name} {...otherProps} />
{trailingAddon?.inline && (
<TrailingInlineAddonStyled>
{trailingAddon.addon}
</TrailingInlineAddonStyled>

<LabelStyled
variant={variant}
hasLeading={hasLeading}
hasTrailing={hasTrailing}
>
{leadingAddon?.inline && (
<LeadingInlineAddonStyled className="addon">
{leadingAddon.addon}
</LeadingInlineAddonStyled>
)}
<InputStyled id={name} name={name} {...otherProps} />
{trailingAddon?.inline && (
<TrailingInlineAddonStyled className="addon">
{trailingAddon.addon}
</TrailingInlineAddonStyled>
)}

{loading && (
<TrailingInlineAddonStyled
style={{ height: variant === 'default' ? 20 : 10 }}
>
<FullSpinner
height={variant === 'default' ? 20 : 10}
width={variant === 'default' ? 20 : 10}
/>
</TrailingInlineAddonStyled>
)}
</LabelStyled>

{trailingAddon && !trailingAddon.inline && (
<TrailingAddonStyled>{trailingAddon.addon}</TrailingAddonStyled>
)}
</LabelStyled>
{trailingAddon && !trailingAddon.inline && (
<TrailingAddonStyled>{trailingAddon.addon}</TrailingAddonStyled>
)}
</GroupStyled>
</GroupStyled>

<SubText error={error} help={help} valid={valid} />
</RootInput>
);
}

function SubText(props: Pick<InputProps, 'help' | 'error' | 'valid'>) {
const { error, help, valid: validProps } = props;

const valid = typeof validProps === 'string' ? validProps : undefined;
const text = error || valid || help;

return <p style={{ color: getColor(error, validProps) }}>{text}</p>;
}

function getColor(
error?: string,
valid?: true | string,
): CSSProperties['color'] {
if (error) {
return '#f95d55';
}

if (valid && typeof valid !== 'boolean') {
return '#62cb21';
}

return 'gray';
}
35 changes: 30 additions & 5 deletions src/components/forms/context/FieldsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,30 @@ const context = createContext<FieldContext | null>(null);

const FieldContextRoot = styled.div`
display: flex;
flex-direction: row;
flex-flow: row;
min-width: 0;
margin: 0;
padding: 0;
gap: 5px;
align-items: center;
`;

const Label = styled.label<{ variant: FieldProps['variant'] }>`
position: relative;
display: inline-flex;
max-width: 100%;
line-height: ${(props) => (props.variant === 'small' ? '28px' : '32px')};
font-size: ${(props) => (props.variant === 'small' ? '1em' : '1.125em')};
white-space: nowrap;
text-align: end;
`;

const LabelContainer = styled.div`
flex-grow: 0;
display: inline-block;
overflow: hidden;
text-align: end;
vertical-align: middle;
white-space: nowrap;
`;

const FieldContextRequired = styled.span`
Expand All @@ -47,9 +68,13 @@ export function Field(props: FieldProps) {
return (
<context.Provider value={memoized}>
<FieldContextRoot>
<label htmlFor={name}>
{label} {required && <FieldContextRequired>*</FieldContextRequired>}:{' '}
</label>
<LabelContainer>
<Label htmlFor={name} variant={memoized.variant}>
{label} {required && <FieldContextRequired>*</FieldContextRequired>}
:{' '}
</Label>
</LabelContainer>

{children}
</FieldContextRoot>
</context.Provider>
Expand Down
23 changes: 14 additions & 9 deletions src/components/spinner/FullSpinner.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
/** @jsxImportSource @emotion/react */
import { css, keyframes } from '@emotion/react';
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';

const spin = keyframes`
100% {
transform: rotate(360deg);
}
`;

const spinnerStyle = css`
height: 40px;
width: 40px;
interface FullSpinnerProps {
width?: number;
height?: number;
}

const Spinner = styled.svg<FullSpinnerProps>`
height: ${({ height }) => `${height || 40}px`};
width: ${({ width }) => `${width || 40}px`};
animation: ${spin} 0.8s linear infinite;
`;

export function FullSpinner() {
export function FullSpinner(props: FullSpinnerProps) {
// First div is used when using nextjs/dynamic even after component is loaded
return (
<div style={{ height: '100%' }}>
Expand All @@ -26,8 +31,8 @@ export function FullSpinner() {
justifyContent: 'center',
}}
>
<svg
css={spinnerStyle}
<Spinner
{...props}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
Expand All @@ -45,7 +50,7 @@ export function FullSpinner() {
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</Spinner>
</div>
</div>
);
Expand Down
8 changes: 6 additions & 2 deletions stories/components/button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ export function Basic() {
export function ButtonGroupBasic() {
return (
<ButtonGroup>
<ButtonGroup.Button position="first" label="A" onClick={() => {}} />
<ButtonGroup.Button position="last" label="B" onClick={() => {}} />
<ButtonGroup.Button position="first" label="A" onClick={noop} />
<ButtonGroup.Button position="last" label="B" onClick={noop} />
</ButtonGroup>
);
}

function noop() {
// Do nothing
}
Loading

0 comments on commit 086f0b8

Please sign in to comment.