Skip to content

Commit

Permalink
more localization
Browse files Browse the repository at this point in the history
  • Loading branch information
gkindel committed Oct 21, 2024
1 parent 60e5b0b commit 54be8ad
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 37 deletions.
55 changes: 41 additions & 14 deletions src/module/PasswordInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { Register, ValidationError } from './validators/useFormValidator.ts';
import { Register, ValidationError, ValidationErrorType } from './validators/useFormValidator.ts';
import { validateMatch, validatePassword } from './validators/validators/PasswordValidation.ts';

const StyledLabel = styled.label<{ $hasError?: boolean }>`
Expand All @@ -26,15 +26,29 @@ const StyledErrors = styled.div`
color: red;
`;

export interface PasswordInputProps {
/** the form input name to be used */
name: string;
/** label for the primary password field */
type Labels = {
/** label for the password field */
passwordText?: string;
/** label for the confirm password field */
confirmText?: string;
/** labels for error messages by type */
errors?: {
[type: ValidationErrorType]: string;
};
};
export interface PasswordInputProps {
/** the form input name to be used */
name: string;
/** localization strings */
labels?: Labels;
/** useFormValidator registration callback to invoked on submit */
register?: Register;
/** seeds password field with a value */
passwordValue?: string;
/** seeds confirmation field with a value */
confirmValue?: string;
/** validate on input change*/
auto?: boolean;
}

/** Renders a pair of password inputs with associated error messages.
Expand All @@ -48,15 +62,16 @@ export interface PasswordInputProps {
* */
export const PasswordInput: React.FC<PasswordInputProps> = ({
name,
passwordText = 'Password',
confirmText = 'Confirm Password',
register,
labels = {},
passwordValue = '',
confirmValue = '',
auto = false,
}) => {
const [primary, setPrimary] = useState('');
const [secondary, setSecondary] = useState('');
const [primary, setPrimary] = useState(passwordValue);
const [secondary, setSecondary] = useState(confirmValue);
const [passwordErrors, setPasswordErrors] = useState<ValidationError[]>([]);
const [confirmErrors, setConfirmErrors] = useState<ValidationError[]>([]);

const validationCallback = useCallback(() => {
const passwordErrors = validatePassword(primary);
setPasswordErrors(passwordErrors);
Expand All @@ -65,11 +80,20 @@ export const PasswordInput: React.FC<PasswordInputProps> = ({
return [...passwordErrors, ...confirmErrors];
}, [primary, secondary]);

useEffect(() => {
if (auto) validationCallback();
}, [validationCallback, auto, primary, secondary]);

// register our validator
useEffect(() => {
if (register) register(name, validationCallback);
}, [name, validationCallback]);

// validate immediately if passed in values
useEffect(() => {
if (register) register(name, validationCallback);
}, [name, validationCallback]);

const _handlePrimaryChange = useCallback(
(e: React.ChangeEvent): void => {
setPrimary((e.currentTarget as HTMLInputElement).value);
Expand All @@ -84,19 +108,21 @@ export const PasswordInput: React.FC<PasswordInputProps> = ({
[setSecondary],
);

const errorLabels = labels.errors || {};

return (
<>
<StyledErrors role={'alert'} aria-live="assertive">
{passwordErrors.map((e: ValidationError) => (
<div key={e.type}>{e.message}</div>
<div key={e.type}>{errorLabels[e.type] || e.message}</div>
))}
{confirmErrors.map((e: ValidationError) => (
<div key={e.type}>{e.message}</div>
<div key={e.type}>{errorLabels[e.type] || e.message}</div>
))}
</StyledErrors>

<StyledLabel $hasError={passwordErrors.length > 0}>
<span>{passwordText}</span>
<span>{labels.passwordText || 'Password'}</span>
<input
type={'password'}
data-testid={'input_password'}
Expand All @@ -107,7 +133,8 @@ export const PasswordInput: React.FC<PasswordInputProps> = ({
</StyledLabel>

<StyledLabel $hasError={confirmErrors.length > 0}>
<span>{confirmText}</span>
<span>{labels.confirmText || 'Confirm Password'}</span>

<input type={'password'} data-testid={'confirm_password'} value={secondary} onChange={_handleSecondaryChange} />
</StyledLabel>
</>
Expand Down
67 changes: 44 additions & 23 deletions src/stories/PasswordInput.stories.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,61 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import {PasswordInput} from "../module/PasswordInput.tsx";

import { PasswordInput } from '../module/PasswordInput.tsx';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Example/PasswordInput',
component: PasswordInput,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
name: 'input_name',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { register: fn() },
title: 'Example/PasswordInput',
component: PasswordInput,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
name: 'input_name',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { register: fn() },
} satisfies Meta<typeof PasswordInput>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
name: 'input_name',
},
args: {
name: 'input_name',
},
};

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Localization: Story = {
args: {
name: 'input_name',
passwordText: 'Contraseña',
confirmText: 'Confirmar Contraseña',
export const Errors: Story = {
args: {
name: 'input_name',
passwordValue: ' ',
confirmValue: ' z',
auto: true,
},
};

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Localization: Story = {
args: {
name: 'input_name',
passwordValue: ' ',
confirmValue: ' z',
labels: {
passwordText: 'Contraseña',
confirmText: 'Confirmar Contraseña',
errors: {
REQUIRED_UPPERCASE: 'La contraseña requiere al menos 1 carácter en mayúscula',
REQUIRED_LOWERCASE: 'La contraseña requiere al menos 1 carácter en minúscula',
REQUIRED_LENGTH: 'La contraseña requiere al menos 6 caracteres',
REQUIRED_NUMBER: 'La contraseña requiere al menos 1 número',
REQUIRED_SPECIAL: 'La contraseña requiere al menos 1 carácter especial (!@#$%^&*()_-+={[}]|:;"\'<,>.)',
REQUIRED_MATCH: 'La contraseña debe coincidir',
},
},
auto: true,
},
};

0 comments on commit 54be8ad

Please sign in to comment.