feat: replace form components (#51204)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
Muhammed Mustafa
2023-10-16 05:33:39 +03:00
committed by GitHub
parent dbcc2af39b
commit 2ccf52e6bc
28 changed files with 225 additions and 269 deletions
+10 -8
View File
@@ -1,19 +1,20 @@
import {
FormControl,
FormGroup,
ControlLabel,
Button
} from '@freecodecamp/react-bootstrap';
import { Button } from '@freecodecamp/react-bootstrap';
import React, { useState } from 'react';
import Helmet from 'react-helmet';
import type { TFunction } from 'i18next';
import { Trans, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Col, Row, Panel } from '@freecodecamp/ui';
import {
FormGroup,
FormControl,
ControlLabel,
Panel,
Col,
Row
} from '@freecodecamp/ui';
import Login from '../components/Header/components/login';
import { Spacer, Loader, FullWidthRow } from '../components/helpers';
import { reportUser } from '../redux/actions';
import {
@@ -123,6 +124,7 @@ function ShowUser({
<FormGroup controlId='report-user-textarea'>
<ControlLabel>{t('report.what')}</ControlLabel>
<FormControl
data-cy='report-user'
componentClass='textarea'
onChange={handleChange}
placeholder={t('report.details')}
@@ -1,10 +1,10 @@
import {
ControlLabel,
FormControl,
FormGroup,
ControlLabel,
Alert,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import { Alert } from '@freecodecamp/ui';
} from '@freecodecamp/ui';
import normalizeUrl from 'normalize-url';
import React from 'react';
import { Field } from 'react-final-form';
-46
View File
@@ -470,10 +470,6 @@ code {
white-space: break-spaces;
}
.help-block em {
font-size: 0.8rem;
}
hr {
border-top: 1px solid var(--quaternary-background);
}
@@ -510,10 +506,6 @@ hr {
background-color: var(--tertiary-background);
}
.help-block {
color: var(--quaternary-color);
}
.challenge-output span {
font-size: 1rem;
}
@@ -540,44 +532,6 @@ pre {
margin-bottom: 0;
}
.has-success .help-block,
.has-success .control-label,
.has-success .radio,
.has-success .checkbox,
.has-success .radio-inline,
.has-success .checkbox-inline,
.has-success.radio label,
.has-success.checkbox label,
.has-success.radio-inline label,
.has-success.checkbox-inline label {
color: var(--highlight-color);
}
.has-error .help-block,
.has-error .control-label,
.has-error .radio,
.has-error .checkbox,
.has-error .radio-inline,
.has-error .checkbox-inline,
.has-error.radio label,
.has-error.checkbox label,
.has-error.radio-inline label,
.has-error.checkbox-inline label {
color: var(--danger-color);
}
.has-error .form-control,
.has-warning .form-control,
.has-success .form-control {
border-color: var(--quaternary-background);
}
.has-error .form-control:focus,
.has-warning .form-control:focus,
.has-success .form-control:focus {
border-color: var(--tertiary-color);
}
blockquote footer,
blockquote small,
blockquote .small {
+11 -11
View File
@@ -1,15 +1,15 @@
import React, { Component } from 'react';
import {
FormGroup,
ControlLabel,
FormControl,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import { Alert } from '@freecodecamp/ui';
import React, { Component } from 'react';
HelpBlock,
Alert,
ControlLabel
} from '@freecodecamp/ui';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import isURL from 'validator/lib/isURL';
import { FullWidthRow, Spacer } from '../helpers';
import BlockSaveButton from '../helpers/form/block-save-button';
import type { CamperProps } from '../profile/components/camper';
@@ -114,7 +114,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
);
};
handleSubmit = (e: React.FormEvent) => {
handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { formValues } = this.state;
const { submitNewAbout } = this.props;
@@ -127,7 +127,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
}
};
handleNameChange = (e: React.FormEvent<HTMLInputElement>) => {
handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
return this.setState(state => ({
formValues: {
@@ -137,7 +137,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
}));
};
handleLocationChange = (e: React.FormEvent<HTMLInputElement>) => {
handleLocationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
return this.setState(state => ({
formValues: {
@@ -163,7 +163,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
isPictureUrlValid: state.formValues.picture === ''
}));
handlePictureChange = (e: React.FormEvent<HTMLInputElement>) => {
handlePictureChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
if (isURL(value, { require_protocol: true })) {
this.validationImage.src = encodeURI(value);
@@ -180,7 +180,7 @@ class AboutSettings extends Component<AboutProps, AboutState> {
}));
};
handleAboutChange = (e: React.FormEvent<HTMLInputElement>) => {
handleAboutChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value.slice(0);
return this.setState(state => ({
formValues: {
+21 -8
View File
@@ -1,11 +1,12 @@
import { Button } from '@freecodecamp/react-bootstrap';
import {
HelpBlock,
Alert,
FormGroup,
ControlLabel,
FormGroupProps,
FormControl,
Button
} from '@freecodecamp/react-bootstrap';
import { Alert } from '@freecodecamp/ui';
ControlLabel
} from '@freecodecamp/ui';
import { Link } from 'gatsby';
import React, { useState } from 'react';
import type { TFunction } from 'i18next';
@@ -44,6 +45,11 @@ interface EmailForm {
isPristine: boolean;
}
interface EmailValidation {
state: FormGroupProps['validationState'];
message: string;
}
function EmailSettings({
email,
isEmailVerified,
@@ -78,7 +84,7 @@ function EmailSettings({
};
}
function getValidationForNewEmail() {
function getValidationForNewEmail(): EmailValidation {
const { newEmail, currentEmail } = emailForm;
if (!maybeEmailRE.test(newEmail)) {
return {
@@ -102,7 +108,7 @@ function EmailSettings({
}
}
function getValidationForConfirmEmail() {
function getValidationForConfirmEmail(): EmailValidation {
const { confirmNewEmail, newEmail } = emailForm;
if (!maybeEmailRE.test(newEmail)) {
return {
@@ -181,6 +187,7 @@ function EmailSettings({
<FullWidthRow>
<form
id='form-update-email'
data-cy='form-update-email'
{...(!isDisabled
? { onSubmit: handleSubmit }
: { onSubmit: e => e.preventDefault() })}
@@ -198,13 +205,16 @@ function EmailSettings({
>
<ControlLabel>{t('settings.email.new')}</ControlLabel>
<FormControl
data-cy='email-input'
data-playwright-test-label='new-email-input'
onChange={createHandleEmailFormChange('newEmail')}
type='email'
value={newEmail}
/>
{newEmailValidationMessage ? (
<HelpBlock>{newEmailValidationMessage}</HelpBlock>
<HelpBlock data-cy='validation-message'>
{newEmailValidationMessage}
</HelpBlock>
) : null}
</FormGroup>
<FormGroup
@@ -213,13 +223,16 @@ function EmailSettings({
>
<ControlLabel>{t('settings.email.confirm')}</ControlLabel>
<FormControl
data-cy='confirm-email'
data-playwright-test-label='confirm-email-input'
onChange={createHandleEmailFormChange('confirmNewEmail')}
type='email'
value={confirmNewEmail}
/>
{confirmEmailValidationMessage ? (
<HelpBlock>{confirmEmailValidationMessage}</HelpBlock>
<HelpBlock data-cy='validation-message'>
{confirmEmailValidationMessage}
</HelpBlock>
) : null}
</FormGroup>
</div>
+14 -8
View File
@@ -1,15 +1,16 @@
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
HelpBlock,
FormControl,
FormGroup,
ControlLabel
} from '@freecodecamp/react-bootstrap';
import React, { Component } from 'react';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import isURL from 'validator/lib/isURL';
import {
FormControl,
FormGroup,
ControlLabel,
HelpBlock,
type FormGroupProps
} from '@freecodecamp/ui';
import { maybeUrlRE } from '../../utils';
@@ -34,6 +35,11 @@ type InternetState = {
originalValues: Socials;
};
interface URLValidation {
state: FormGroupProps['validationState'];
message: string;
}
function Info({ message }: { message: string }) {
return message ? <HelpBlock>{message}</HelpBlock> : null;
}
@@ -78,7 +84,7 @@ class InternetSettings extends Component<InternetProps, InternetState> {
return null;
}
getValidationStateFor(maybeURl = '') {
getValidationStateFor(maybeURl = ''): URLValidation {
const { t } = this.props;
if (!maybeURl || !maybeUrlRE.test(maybeURl)) {
return {
@@ -152,7 +158,7 @@ class InternetSettings extends Component<InternetProps, InternetState> {
return null;
};
renderCheck = (url: string, validation: string | null) =>
renderCheck = (url: string, validation: FormGroupProps['validationState']) =>
url && validation === 'success' ? (
<FormControl.Feedback>
<span>
+42 -18
View File
@@ -1,14 +1,15 @@
import {
Button,
FormGroup,
ControlLabel,
FormControl,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import { Button } from '@freecodecamp/react-bootstrap';
import { findIndex, find, isEqual } from 'lodash-es';
import { nanoid } from 'nanoid';
import React, { Component } from 'react';
import type { TFunction } from 'i18next';
import {
FormGroup,
FormControl,
ControlLabel,
HelpBlock,
FormGroupProps
} from '@freecodecamp/ui';
import { withTranslation } from 'react-i18next';
import isURL from 'validator/lib/isURL';
import { PortfolioProjectData } from '../../redux/prop-types';
@@ -32,6 +33,11 @@ type PortfolioState = {
unsavedItemId: string | null;
};
interface ProfileValidation {
state: FormGroupProps['validationState'];
message: string;
}
function createEmptyPortfolioItem(): PortfolioProjectData {
return {
id: nanoid(),
@@ -61,9 +67,9 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
createOnChangeHandler =
(id: string, key: 'description' | 'image' | 'title' | 'url') =>
(e: React.FormEvent<HTMLInputElement>) => {
(e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const userInput = (e.target as HTMLInputElement).value.slice();
const userInput = e.target.value.slice();
return this.setState(state => {
const { portfolio: currentPortfolio } = state;
const mutablePortfolio = currentPortfolio.slice(0);
@@ -114,7 +120,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
return isEqual(original, edited);
};
getDescriptionValidation(description: string) {
getDescriptionValidation(description: string): ProfileValidation {
const { t } = this.props;
const len = description.length;
const charsLeft = 288 - len;
@@ -136,10 +142,13 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
return { state: 'success', message: '' };
}
getTitleValidation(title: string) {
getTitleValidation(title: string): ProfileValidation {
const { t } = this.props;
if (!title) {
return { state: 'error', message: t('validation.title-required') };
return {
state: 'error',
message: t('validation.title-required')
};
}
const len = title.length;
if (len < 2) {
@@ -155,7 +164,10 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
const { t } = this.props;
const len = maybeUrl.length;
if (len >= 4 && !hasProtocolRE.test(maybeUrl)) {
return { state: 'error', message: t('validation.invalid-protocol') };
return {
state: 'error',
message: t('validation.invalid-protocol')
};
}
if (isImage && !maybeUrl) {
return { state: null, message: '' };
@@ -242,7 +254,6 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
if (isButtonDisabled) return null;
return this.updateItem(id);
};
return (
<FullWidthRow key={id}>
<form onSubmit={e => handleSubmit(e, id)} id='portfolio-items'>
@@ -258,8 +269,11 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
required={true}
type='text'
value={title}
data-cy='portfolio-title'
/>
{titleMessage ? <HelpBlock>{titleMessage}</HelpBlock> : null}
{titleMessage ? (
<HelpBlock data-cy='validation-message'>{titleMessage}</HelpBlock>
) : null}
</FormGroup>
<FormGroup
controlId={`${id}-url`}
@@ -271,8 +285,11 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
required={true}
type='url'
value={url}
data-cy='portfolio-url'
/>
{urlMessage ? <HelpBlock>{urlMessage}</HelpBlock> : null}
{urlMessage ? (
<HelpBlock data-cy='validation-message'>{urlMessage}</HelpBlock>
) : null}
</FormGroup>
<FormGroup
controlId={`${id}-image`}
@@ -283,8 +300,11 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
onChange={this.createOnChangeHandler(id, 'image')}
type='url'
value={image}
data-cy='portfolio-image'
/>
{imageMessage ? <HelpBlock>{imageMessage}</HelpBlock> : null}
{imageMessage ? (
<HelpBlock data-cy='validation-message'>{imageMessage}</HelpBlock>
) : null}
</FormGroup>
<FormGroup
controlId={`${id}-description`}
@@ -295,9 +315,12 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
componentClass='textarea'
onChange={this.createOnChangeHandler(id, 'description')}
value={description}
data-cy='portfolio-description'
/>
{descriptionMessage ? (
<HelpBlock>{descriptionMessage}</HelpBlock>
<HelpBlock data-cy='validation-message'>
{descriptionMessage}
</HelpBlock>
) : null}
</FormGroup>
<BlockSaveButton
@@ -339,6 +362,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
<p>{t('settings.share-projects')}</p>
<Spacer size='small' />
<Button
data-cy='add-portfolio'
block={true}
bsSize='lg'
bsStyle='primary'
+1 -6
View File
@@ -1,10 +1,5 @@
/* eslint-disable @typescript-eslint/unbound-method */
import {
ControlLabel,
FormControl,
FormGroup
} from '@freecodecamp/react-bootstrap';
import { Alert } from '@freecodecamp/ui';
import { FormControl, Alert, FormGroup, ControlLabel } from '@freecodecamp/ui';
import React, { Component } from 'react';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
+13 -11
View File
@@ -1,9 +1,4 @@
import {
FormGroup,
FormControl,
ControlLabel,
Button
} from '@freecodecamp/react-bootstrap';
import { Button } from '@freecodecamp/react-bootstrap';
import { Link } from 'gatsby';
import { isString } from 'lodash-es';
import React, { useState, type FormEvent, type ChangeEvent } from 'react';
@@ -15,7 +10,14 @@ import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import isEmail from 'validator/lib/isEmail';
import { Container, Col, Row } from '@freecodecamp/ui';
import {
Container,
FormGroup,
FormControl,
ControlLabel,
Col,
Row
} from '@freecodecamp/ui';
import { Spacer } from '../components/helpers';
import './update-email.css';
@@ -47,12 +49,12 @@ function UpdateEmail({ isNewEmail, t, updateMyEmail }: UpdateEmailProps) {
updateMyEmail(emailValue);
}
function onChange(event: ChangeEvent) {
const change = (event.target as HTMLInputElement).value;
if (!isString(change)) {
function onChange(event: ChangeEvent<HTMLInputElement>) {
const newEmailValue = event.target.value;
if (!isString(newEmailValue)) {
return null;
}
setEmailValue(change);
setEmailValue(newEmailValue);
return null;
}
@@ -1,16 +1,16 @@
import React, { useState } from 'react';
import {
Button,
FormGroup,
ControlLabel,
FormControl,
HelpBlock
} from '@freecodecamp/react-bootstrap';
import { Button } from '@freecodecamp/react-bootstrap';
import { ConnectedProps, connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { Trans, useTranslation } from 'react-i18next';
import {
ControlLabel,
FormControl,
FormGroup,
HelpBlock
} from '@freecodecamp/ui';
import { Spacer } from '../../../components/helpers';
import { isMicrosoftTranscriptLink } from '../../../../../shared/utils/validate';
+8 -8
View File
@@ -6,15 +6,15 @@ describe('Email input field', () => {
});
it('Should be possible to submit the new email', () => {
cy.get('[id=new-email]')
cy.get('[data-cy="email-input"]')
.type('bar@foo.com')
.should('have.attr', 'value', 'bar@foo.com');
cy.get('[id=confirm-email]')
cy.get('[data-cy="confirm-email"]')
.type('bar@foo.com')
.should('have.attr', 'value', 'bar@foo.com');
cy.get('[id=form-update-email]').within(() => {
cy.get('[data-cy="form-update-email"]').within(() => {
cy.contains('Save').click();
});
cy.contains(
@@ -23,16 +23,16 @@ describe('Email input field', () => {
});
it('Displays an error message when there are problems with the submitted emails', () => {
cy.get('[id=new-email]').type('bar@foo.com');
cy.get('[id=confirm-email]').type('foo@bar.com');
cy.get('[data-cy="email-input"]').type('bar@foo.com');
cy.get('[data-cy="confirm-email"]').type('foo@bar.com');
cy.get('[class=help-block]').contains(
cy.get('[data-cy="validation-message"]').contains(
'Both new email addresses must be the same'
);
cy.get('[id=new-email]').clear().type('foo@bar.com');
cy.get('[data-cy="email-input"]').clear().type('foo@bar.com');
cy.get('[class=help-block]').contains(
cy.get('[data-cy="validation-message"]').contains(
'This email is the same as your current email'
);
});
+22 -15
View File
@@ -1,44 +1,51 @@
describe('Add Portfolio Item', () => {
before(() => {
beforeEach(() => {
cy.task('seed');
cy.login();
});
it('should be possible to add a portfolio item', () => {
cy.visit('/settings');
cy.contains('Add a new portfolio Item').click();
cy.get('[data-cy="add-portfolio"]')
.contains('Add a new portfolio Item')
.click();
cy.get('.help-block').contains('A title is required');
cy.get('[id$="title"]').type('This is a portfolio item');
cy.get('[data-cy="validation-message"]').contains('A title is required');
cy.get('[data-cy="portfolio-title"]').type('This is a portfolio item');
cy.get('button').filter(':disabled').should('have.length.gt', 0);
cy.get('[id$="url"]').type('This is a portfolio item');
cy.get('.help-block').contains('URL must start with http or https');
cy.get('[id$="url"]').clear().type('http://google.com');
cy.get('[data-cy="portfolio-url"]').type('This is a portfolio item');
cy.get('[data-cy="validation-message"]').contains(
'URL must start with http or https'
);
cy.get('[data-cy="portfolio-url"]').clear().type('http://google.com');
cy.get('[id$="image"]').type('hello');
cy.get('.help-block').contains('URL must start with http or https');
cy.get('[id$="image"]')
cy.get('[data-cy="portfolio-image"]').type('hello');
cy.get('[data-cy="validation-message"]').contains(
'URL must start with http or https'
);
cy.get('[data-cy="portfolio-image"]')
.clear()
.type(
'https://cdn.freecodecamp.org/curriculum/cat-photo-app/lasagna.jpg'
);
cy.get('[id$="description"]').type(
cy.get('[data-cy="portfolio-description"]').type(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod metus velit, vel accumsan lorem facilisis ac. Maecenas vitae ultrices dolor. Fusce in lobortis arcu, vel congue risus. Sed id neque nec nibh hendrerit bibendum. Integer venenatie.'
);
cy.get('.help-block').contains(
cy.get('[data-cy="validation-message"]').contains(
'There is a maximum limit of 288 characters, you have 40 left'
);
cy.get('[id$="description"]').type(
cy.get('[data-cy="portfolio-description"]').type(
'Lorem ipsum dolor sit amet, consecteturs.'
);
cy.get('.help-block').contains(
cy.get('[data-cy="validation-message"]').contains(
'There is a maximum limit of 288 characters, you have 0 left'
);
cy.get('button').filter(':disabled').should('have.length.gt', 0);
cy.get('[id$="description"]').type('{backspace}');
cy.get('[data-cy="portfolio-description"]').type('{backspace}');
cy.get('button[type=submit]').contains('Save this portfolio item').click();
});
});
+1 -1
View File
@@ -11,7 +11,7 @@ describe('Report User', () => {
// cy.contains('Preview custom 404 page').click();
cy.contains("Flag This User's Account for Abuse").click();
cy.contains("Do you want to report twaha's portfolio for abuse?");
cy.get('[id=report-user-textarea]').type('Some details');
cy.get('[data-cy="report-user"]').type('Some details');
cy.contains('Submit the report').click();
cy.location().should(loc => {
expect(loc.pathname).to.eq('/learn');
@@ -3,9 +3,11 @@ import { FormContext } from '../form-group/form-group';
import { ControlLabelProps } from './types';
const hasSuccess = 'text-foreground-info';
const hasWarning = 'text-foreground-warning';
const hasError = 'text-foreground-danger';
const validationLabel = {
success: 'text-background-info',
warning: 'text-background-warning',
error: 'text-background-danger'
};
export const ControlLabel = ({
className,
@@ -15,14 +17,9 @@ export const ControlLabel = ({
}: ControlLabelProps): JSX.Element => {
const { controlId, validationState } = useContext(FormContext);
const labelStyle =
validationState === 'success'
? hasSuccess
: validationState === 'error'
? hasError
: validationState === 'warning'
? hasWarning
: undefined;
const labelStyle = validationState
? validationLabel[validationState]
: undefined;
const screenOnlyClass = srOnly ? 'sr-only' : undefined;
const defaultClasses = [labelStyle, screenOnlyClass, className].join(' ');
@@ -1,19 +1,16 @@
import React from 'react';
import { FormControlVariationProps } from './types';
export const FormControlFeedback = ({
children,
className,
testId
}: FormControlVariationProps): JSX.Element => {
...props
}: React.ComponentProps<'span'>): JSX.Element => {
const defaultClasses =
'absolute top-0 right-0 z-2 block w-8 h-8 leading-8 ' +
'text-center pointer-events-none text-green-700';
'absolute top-[30px] right-0 z-2 block w-8 h-8 leading-8 text-center pointer-events-none text-green-700';
const classes = [defaultClasses, className].join(' ');
const classes = [className, defaultClasses].join(' ');
return (
<span className={classes} data-testid={testId}>
<span className={classes} {...props}>
{children}
</span>
);
@@ -1,17 +1,15 @@
import React from 'react';
import { FormControlVariationProps } from './types';
export const FormControlStatic = ({
className,
children,
testId
}: FormControlVariationProps): JSX.Element => {
...props
}: React.ComponentProps<'p'>): JSX.Element => {
const defaultClasses = 'py-1.5 mb-0 min-h-43-px text-foreground-secondary';
const classes = [defaultClasses, className].join(' ');
return (
<p className={classes} data-testid={testId}>
<p className={classes} {...props}>
{children}
</p>
);
@@ -1,6 +1,6 @@
import React from 'react';
import { Story } from '@storybook/react';
import { FormControl, FormControlProps, FormControlVariationProps } from '.';
import { FormControl, FormControlProps } from '.';
const story = {
title: 'Example/FormControl',
@@ -42,7 +42,7 @@ Default.args = {
// default props go here
};
const StaticTemplate: Story<FormControlVariationProps> = args => {
const StaticTemplate: Story<React.ComponentProps<'p'>> = args => {
return <FormControl.Static {...args} />;
};
@@ -51,7 +51,7 @@ Static.args = {
children: 'foo@bar.com'
};
const FeedBackTemplate: Story<FormControlVariationProps> = args => {
const FeedBackTemplate: Story<React.ComponentProps<'span'>> = args => {
return <FormControl.Feedback {...args} />;
};
@@ -5,21 +5,21 @@ import { FormControl } from '.';
describe('<FormControl />', () => {
it('should render correctly', () => {
render(<FormControl testId='test' />);
expect(screen.getByTestId('test')).toBeInTheDocument();
render(<FormControl aria-label='test' />);
expect(screen.getByLabelText('test')).toBeInTheDocument();
});
});
describe('<FormControl.Static />', () => {
it('should render correctly', () => {
render(<FormControl.Static testId='test' />);
expect(screen.getByTestId('test')).toBeInTheDocument();
render(<FormControl.Static aria-label='test' />);
expect(screen.getByLabelText('test')).toBeInTheDocument();
});
});
describe('<FormControl.Feedback />', () => {
it('should render correctly', () => {
render(<FormControl.Feedback testId='test' />);
expect(screen.getByTestId('test')).toBeInTheDocument();
render(<FormControl.Feedback aria-label='test' />);
expect(screen.getByLabelText('test')).toBeInTheDocument();
});
});
@@ -9,46 +9,22 @@ import { FormControlProps } from './types';
// type Only relevant if componentClass is 'input'.
let variantClass: string;
const defaultClasses =
'outline-0 block w-full py-1.5 px-2.5 text-md text-foreground-primary ' +
'bg-background-primary bg-none rounded-none border-1 border-solid ' +
'border-background-quaternary shadow-none ' +
'transition ease-in-out duration-150 focus:border-foreground-tertiary';
'outline-0 block w-full py-1.5 px-2.5 text-md text-foreground-primary bg-background-primary bg-none rounded-none border-1 border-solid border-background-quaternary shadow-none transition ease-in-out duration-150 focus:border-foreground-tertiary';
const FormControl = ({
id,
className,
testId,
onChange,
value,
componentClass,
placeholder,
name,
required,
type,
...restProps
}: FormControlProps): JSX.Element => {
...props
}: FormControlProps<'input' | 'textarea'>): JSX.Element => {
const { controlId } = useContext(FormContext);
const { id, className } = props;
const Component = componentClass || 'input';
if (Component !== 'textarea') variantClass = ' h-8';
//row and componentClass
const classes = [defaultClasses, variantClass, className].join(' ');
const classes = [className, defaultClasses, variantClass].join(' ');
return (
<Component
id={id || controlId}
data-testid={testId}
className={classes}
value={value}
required={required}
onChange={onChange}
name={name}
placeholder={placeholder}
type={type}
{...restProps}
/>
);
return <Component id={id || controlId} className={classes} {...props} />;
};
FormControl.Feedback = FormControlFeedback;
@@ -1,2 +1,2 @@
export { FormControl } from './form-control';
export type { FormControlProps, FormControlVariationProps } from './types';
export type { FormControlProps } from './types';
+7 -32
View File
@@ -1,34 +1,9 @@
import React from 'react';
type FormControlElement = HTMLInputElement | HTMLTextAreaElement;
type ChangibleValues =
| {
value?: never;
onChange?: never;
readonly?: never;
}
| {
value?: string;
onChange?: never;
readonly: boolean;
}
| {
value?: string;
onChange: (event: React.ChangeEvent<FormControlElement>) => void;
readonly?: never;
};
export type FormControlProps = React.HTMLAttributes<FormControlElement> & {
testId?: string;
componentClass?: 'textarea' | 'input';
name?: string;
required?: boolean;
rows?: number;
type?: 'text' | 'email' | 'url';
} & ChangibleValues;
export type FormControlVariationProps = Pick<
FormControlProps,
'className' | 'children' | 'id' | 'testId'
>;
export type FormControlProps<
TElement extends
| keyof JSX.IntrinsicElements
| React.JSXElementConstructor<unknown> = 'input'
> = {
componentClass?: TElement | string;
} & React.ComponentProps<TElement>;
@@ -26,10 +26,10 @@ describe('<FormGroup>', () => {
it('provided controlId to label and control', () => {
render(
<FormGroup controlId='my-control' data-testid='test-id'>
<FormControl role='switch' />
<FormControl aria-label='test' />
</FormGroup>
);
const input = screen.getByRole('switch');
const input = screen.getByLabelText('test');
expect(input.id).toBe('my-control');
});
});
@@ -7,7 +7,7 @@ export type FormContextProps = Pick<
>;
export const FormContext = createContext<FormContextProps>({});
const defaultClasses = 'mb-3.5';
const defaultClasses = 'mb-3.5 relative';
export const FormGroup = ({
className,
@@ -1,7 +1,6 @@
import React from 'react';
import { Story } from '@storybook/react';
import { HelpBlock } from './help-block';
import { HelpBlockProps } from './types';
const story = {
title: 'Example/HelpBlock',
@@ -13,7 +12,7 @@ const story = {
}
};
const Template: Story<HelpBlockProps> = args => {
const Template: Story<React.ComponentPropsWithRef<'span'>> = args => {
return <HelpBlock {...args} />;
};
@@ -1,16 +1,28 @@
import React from 'react';
import { HelpBlockProps } from './types';
import React, { useContext } from 'react';
import { FormContext } from '../form-group/form-group';
export const HelpBlock = React.forwardRef<HTMLSpanElement, HelpBlockProps>(
({ className, children }, ref): JSX.Element => {
const defaultClasses = 'block mt-1 mb-2 text-foreground-quaternary';
const classes = [defaultClasses, className].join(' ');
return (
<span ref={ref} data-testid='help-block' className={classes}>
{children}
</span>
);
}
);
const defaultClasses = 'block mt-1 mb-2';
const validationLabel = {
success: 'text-background-info',
warning: 'text-background-warning',
error: 'text-background-danger'
};
export const HelpBlock = React.forwardRef<
HTMLSpanElement,
React.ComponentProps<'span'>
>(({ className, children, ...props }, ref): JSX.Element => {
const { validationState } = useContext(FormContext);
const labelStyle = validationState
? validationLabel[validationState]
: 'text-foreground-quaternary';
const classes = [className, defaultClasses, labelStyle].join(' ');
return (
<span ref={ref} data-testid='help-block' className={classes} {...props}>
{children}
</span>
);
});
HelpBlock.displayName = 'HelpBlock';
@@ -1,2 +1 @@
export { HelpBlock } from './help-block';
export type { HelpBlockProps } from './types';
@@ -1,4 +0,0 @@
export interface HelpBlockProps {
className?: string;
children?: React.ReactNode;
}
+4
View File
@@ -11,4 +11,8 @@ export { MenuItem } from './drop-down/menu-item';
export { Container } from './container';
export { Tabs, TabsList, TabsTrigger, TabsContent } from './tabs';
export { Col } from './col';
export { ControlLabel } from './control-label';
export { FormGroup, type FormGroupProps } from './form-group';
export { FormControl } from './form-control';
export { HelpBlock } from './help-block';
export { Row } from './row';