Skip to content

Commit

Permalink
feat(PasswordCreationInput): create password creation input component
Browse files Browse the repository at this point in the history
  • Loading branch information
alexbrillant committed Apr 20, 2022
1 parent 0275e97 commit 1e4215f
Show file tree
Hide file tree
Showing 22 changed files with 823 additions and 23 deletions.
9 changes: 5 additions & 4 deletions packages/react/src/components/feedbacks/invalid-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ const StyledIcon = styled.div`
interface InvalidFieldProps {
controlId: string;
feedbackMsg: string;
noIcon?: boolean;
}

function InvalidField({ controlId, feedbackMsg }: InvalidFieldProps): ReactElement {
function InvalidField({ controlId, feedbackMsg, noIcon }: InvalidFieldProps): ReactElement {
const { isMobile } = useDeviceContext();

return (
Expand All @@ -36,9 +37,9 @@ function InvalidField({ controlId, feedbackMsg }: InvalidFieldProps): ReactEleme
isMobile={isMobile}
role="alert"
>
<StyledIcon>
<Icon name="alertTriangle" size={isMobile ? '24' : '16'} />
</StyledIcon>
{!noIcon && (
<StyledIcon><Icon name="alertTriangle" size={isMobile ? '24' : '16'} /></StyledIcon>
)}
<StyledSpan>{feedbackMsg}</StyledSpan>
</Field>
);
Expand Down
19 changes: 11 additions & 8 deletions packages/react/src/components/field-container/field-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,27 +42,29 @@ const StyledHint = styled.span<{ isMobile: boolean }>`
`;

export interface FieldContainerProps {
className?: string;
children: ReactNode;
noMargin?: boolean;
className?: string;
fieldId: string;
hint?: string;
label?: string;
noInvalidFieldIcon?: boolean;
noMargin?: boolean;
tooltip?: TooltipProps;
valid: boolean;
validationErrorMessage: string;
hint?: string;
tooltip?: TooltipProps;
}

export function FieldContainer({
className,
children,
className,
fieldId,
label,
valid,
validationErrorMessage,
hint,
label,
noInvalidFieldIcon,
noMargin,
tooltip,
valid,
validationErrorMessage,
...props
}: FieldContainerProps): ReactElement {
const { isMobile } = useDeviceContext();
Expand All @@ -83,6 +85,7 @@ export function FieldContainer({
data-testid="text-input-error-msg"
controlId={fieldId}
feedbackMsg={validationErrorMessage}
noIcon={noInvalidFieldIcon}
/>
)}
{children}
Expand Down
5 changes: 5 additions & 0 deletions packages/react/src/components/icon/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import ChevronRight from 'feather-icons/dist/icons/chevron-right.svg';
import ChevronUp from 'feather-icons/dist/icons/chevron-up.svg';
import ChevronsLeft from 'feather-icons/dist/icons/chevrons-left.svg';
import ChevronsRight from 'feather-icons/dist/icons/chevrons-right.svg';
import Circle from 'feather-icons/dist/icons/circle.svg';
import Copy from 'feather-icons/dist/icons/copy.svg';
import Edit from 'feather-icons/dist/icons/edit-2.svg';
import ExternalLink from 'feather-icons/dist/icons/external-link.svg';
import Eye from 'feather-icons/dist/icons/eye.svg';
import EyeOff from 'feather-icons/dist/icons/eye-off.svg';
import HelpCircle from 'feather-icons/dist/icons/help-circle.svg';
import Home from 'feather-icons/dist/icons/home.svg';
import Info from 'feather-icons/dist/icons/info.svg';
Expand Down Expand Up @@ -68,12 +70,14 @@ const iconMapping = {
chevronUp: ChevronUp,
chevronsLeft: ChevronsLeft,
chevronsRight: ChevronsRight,
circle: Circle,
contracts: Contracts,
copy: Copy,
edit: Edit,
equisoft: Equisoft,
externalLink: ExternalLink,
eye: Eye,
eyeOff: EyeOff,
files: Files,
helpCircle: HelpCircle,
history: History,
Expand Down Expand Up @@ -107,6 +111,7 @@ export interface IconProps {
className?: string;
/** Name of the icon, has to be in IconName */
name: IconName;
focusable?: boolean;
/**
* Size will affect both width and height
* @default 24
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ exports[`CurrencyInput Component matches snapshot (en-CA) 1`] = `
letter-spacing: 0.015rem;
line-height: 1.5rem;
margin: 0;
min-height: 32px;
outline: none;
padding: var(--spacing-half) var(--spacing-1x);
padding: 0 var(--spacing-1x);
width: 100%;
}
Expand Down Expand Up @@ -148,8 +149,9 @@ exports[`CurrencyInput Component matches snapshot (en-US) 1`] = `
letter-spacing: 0.015rem;
line-height: 1.5rem;
margin: 0;
min-height: 32px;
outline: none;
padding: var(--spacing-half) var(--spacing-1x);
padding: 0 var(--spacing-1x);
width: 100%;
}
Expand Down Expand Up @@ -263,8 +265,9 @@ exports[`CurrencyInput Component matches snapshot (fr-CA) 1`] = `
letter-spacing: 0.015rem;
line-height: 1.5rem;
margin: 0;
min-height: 32px;
outline: none;
padding: var(--spacing-half) var(--spacing-1x);
padding: 0 var(--spacing-1x);
width: 100%;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { ChangeEvent, useState, VoidFunctionComponent, useMemo } from 'react';
import styled, { StyledProps } from 'styled-components';
import { FieldContainer } from '../field-container/field-container';
import { IconButton } from '../buttons/icon-button';
import { TextInput } from '../text-input/text-input';
import { useTranslation } from '../../i18n/use-translation';
import { getPasswordStrength } from './password-strength';
import { PasswordRule } from './password-rule';
import { getDefaultValidationConditions, ValidationCondition } from './validation-condition';
import { v4 as uuid } from '../../utils/uuid';
import { PasswordStrengthContainer } from './password-strength-container';
import { useDataAttributes } from '../../hooks/use-data-attributes';
import { Tooltip } from '../tooltip/tooltip';

const StyledFieldContainer = styled(FieldContainer)`
> :nth-child(2) {
margin-bottom: 0;
}
`;

const StyledUl = styled.ul`
font-size: 0.75rem;
margin: 0 0 var(--spacing-half) 0;
padding: 0;
`;

const PasswordContainer = styled.div`
border-radius: 0 var(--border-radius) var(--border-radius) 0;
display: flex;
flex-direction: row;
margin-bottom: calc(var(--spacing-1x) * 1.5);
position: relative;
> div:first-of-type:focus-within {
border-radius: var(--border-radius);
/* TODO change when updating thematization */
box-shadow: 0 0 0 2px #84c6ea;
outline: none;
input,
+ span > button {
/* TODO change when updating thematization */
border-color: #006296;
}
}
`;

export function getBorderColor({ isValid, theme }: StyledProps<{ isValid: boolean; }>): string {
if (isValid) {
return theme.greys['dark-grey'];
}

return theme.notifications['alert-2.1'];
}

const StyledInput = styled(TextInput)`
flex: 1;
margin-bottom: 0;
input {
::-ms-reveal {
display: none;
}
border-color: ${getBorderColor};
border-radius: var(--border-radius) 0 0 var(--border-radius);
border-width: 1px 0 1px 1px;
width: calc(100% - 2rem);
}
`;

const StyledIconButton = styled(IconButton)<{ isValid: boolean }>`
background-color: white;
border-color: ${getBorderColor};
border-radius: 0 var(--border-radius) var(--border-radius) 0;
border-width: 1px 1px 1px 0;
min-height: 2rem;
position: absolute;
transform: translateX(-2rem);
width: 2rem;
`;

interface PasswordCreationInputProps {
name?: string;
id?: string;
onChange(newPassword: string, isValid: boolean, event: ChangeEvent<HTMLInputElement>): void;
validations?: ValidationCondition[];
}

function isPasswordValid(conditions: ValidationCondition[], value: string): boolean {
return conditions.every((condition) => condition.isValid(value));
}

export const PasswordCreationInput: VoidFunctionComponent<PasswordCreationInputProps> = ({
id: providedId,
name,
onChange,
validations,
...otherProps
}) => {
const { t } = useTranslation('password-creation-input');
const [showPassword, setShowPassword] = useState(false);
const [password, setPassword] = useState('');
const isEmpty = password.length === 0;
const strength = getPasswordStrength(password);
const conditions = validations ?? getDefaultValidationConditions(t);
const passwordStrengthId = useMemo(() => uuid(), []);
const id = useMemo(() => providedId || uuid(), [providedId]);
const hintId = useMemo(() => uuid(), []);
const isValid = isPasswordValid(conditions, password);
const dataAttributes = useDataAttributes(otherProps);

const handleShowPassword = (): void => {
setShowPassword(!showPassword);
};

const handleChange = (event: ChangeEvent<HTMLInputElement>): void => {
const newPassword = event.target.value;
setPassword(newPassword);
const newPasswordIsValid = isPasswordValid(conditions, newPassword);
onChange(newPassword, newPasswordIsValid, event);
};

return (
<StyledFieldContainer
fieldId={id}
label={t('create-password')}
validationErrorMessage=""
noInvalidFieldIcon
valid={isValid}
>
<div id={hintId} aria-live="assertive" aria-hidden="true" aria-atomic="true">
<StyledUl>
{conditions.map((condition) => (
<PasswordRule
key={condition.label}
label={condition.label}
isValid={condition.isValid(password)}
isEmpty={isEmpty}
/>
))}
</StyledUl>
</div>
<PasswordContainer>
<StyledInput
id={id}
isValid={isValid || isEmpty}
name={name ?? 'password'}
autoComplete="new-password"
ariaDescribedBy={`${hintId} ${passwordStrengthId}`}
ariaInvalid={!isValid}
onChange={handleChange}
data-testid="password-input"
type={showPassword ? 'text' : 'password'}
{...dataAttributes /* eslint-disable-line react/jsx-props-no-spreading */}
/>
<Tooltip
desktopPlacement="top"
label={showPassword ? t('hide-password') : t('show-password')}
>
<StyledIconButton
isValid={isValid || isEmpty}
buttonType="tertiary"
aria-label={t('show-password')}
iconName={showPassword ? 'eyeOff' : 'eye'}
aria-pressed={showPassword}
data-testid="show-password-button"
type="button"
onClick={handleShowPassword}
/>
</Tooltip>
</PasswordContainer>
<PasswordStrengthContainer strength={strength} id={passwordStrengthId} />
</StyledFieldContainer>
);
};
Loading

0 comments on commit 1e4215f

Please sign in to comment.