diff --git a/client/src/client-only-routes/show-user.tsx b/client/src/client-only-routes/show-user.tsx index 6a67c2fb163..a9f7c02234f 100644 --- a/client/src/client-only-routes/show-user.tsx +++ b/client/src/client-only-routes/show-user.tsx @@ -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({ {t('report.what')} { ); }; - handleSubmit = (e: React.FormEvent) => { + handleSubmit = (e: React.FormEvent) => { e.preventDefault(); const { formValues } = this.state; const { submitNewAbout } = this.props; @@ -127,7 +127,7 @@ class AboutSettings extends Component { } }; - handleNameChange = (e: React.FormEvent) => { + handleNameChange = (e: React.ChangeEvent) => { const value = (e.target as HTMLInputElement).value.slice(0); return this.setState(state => ({ formValues: { @@ -137,7 +137,7 @@ class AboutSettings extends Component { })); }; - handleLocationChange = (e: React.FormEvent) => { + handleLocationChange = (e: React.ChangeEvent) => { const value = (e.target as HTMLInputElement).value.slice(0); return this.setState(state => ({ formValues: { @@ -163,7 +163,7 @@ class AboutSettings extends Component { isPictureUrlValid: state.formValues.picture === '' })); - handlePictureChange = (e: React.FormEvent) => { + handlePictureChange = (e: React.ChangeEvent) => { 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 { })); }; - handleAboutChange = (e: React.FormEvent) => { + handleAboutChange = (e: React.ChangeEvent) => { const value = (e.target as HTMLInputElement).value.slice(0); return this.setState(state => ({ formValues: { diff --git a/client/src/components/settings/email.tsx b/client/src/components/settings/email.tsx index 5be10377326..04e120ee46e 100644 --- a/client/src/components/settings/email.tsx +++ b/client/src/components/settings/email.tsx @@ -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({ e.preventDefault() })} @@ -198,13 +205,16 @@ function EmailSettings({ > {t('settings.email.new')} {newEmailValidationMessage ? ( - {newEmailValidationMessage} + + {newEmailValidationMessage} + ) : null} {t('settings.email.confirm')} {confirmEmailValidationMessage ? ( - {confirmEmailValidationMessage} + + {confirmEmailValidationMessage} + ) : null} diff --git a/client/src/components/settings/internet.tsx b/client/src/components/settings/internet.tsx index cfb970f90a9..998ef1edcf4 100644 --- a/client/src/components/settings/internet.tsx +++ b/client/src/components/settings/internet.tsx @@ -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 ? {message} : null; } @@ -78,7 +84,7 @@ class InternetSettings extends Component { 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 { return null; }; - renderCheck = (url: string, validation: string | null) => + renderCheck = (url: string, validation: FormGroupProps['validationState']) => url && validation === 'success' ? ( diff --git a/client/src/components/settings/portfolio.tsx b/client/src/components/settings/portfolio.tsx index 41a7f0253b0..de3fb41ccaf 100644 --- a/client/src/components/settings/portfolio.tsx +++ b/client/src/components/settings/portfolio.tsx @@ -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 { createOnChangeHandler = (id: string, key: 'description' | 'image' | 'title' | 'url') => - (e: React.FormEvent) => { + (e: React.ChangeEvent) => { 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 { 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 { 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 { 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 { if (isButtonDisabled) return null; return this.updateItem(id); }; - return ( handleSubmit(e, id)} id='portfolio-items'> @@ -258,8 +269,11 @@ class PortfolioSettings extends Component { required={true} type='text' value={title} + data-cy='portfolio-title' /> - {titleMessage ? {titleMessage} : null} + {titleMessage ? ( + {titleMessage} + ) : null} { required={true} type='url' value={url} + data-cy='portfolio-url' /> - {urlMessage ? {urlMessage} : null} + {urlMessage ? ( + {urlMessage} + ) : null} { onChange={this.createOnChangeHandler(id, 'image')} type='url' value={image} + data-cy='portfolio-image' /> - {imageMessage ? {imageMessage} : null} + {imageMessage ? ( + {imageMessage} + ) : null} { componentClass='textarea' onChange={this.createOnChangeHandler(id, 'description')} value={description} + data-cy='portfolio-description' /> {descriptionMessage ? ( - {descriptionMessage} + + {descriptionMessage} + ) : null} { {t('settings.share-projects')} ) { + const newEmailValue = event.target.value; + if (!isString(newEmailValue)) { return null; } - setEmailValue(change); + setEmailValue(newEmailValue); return null; } diff --git a/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx b/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx index c8a2fce0d57..d52e3e46c8f 100644 --- a/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx +++ b/client/src/templates/Challenges/ms-trophy/link-ms-user.tsx @@ -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'; diff --git a/cypress/e2e/default/settings/email-change.ts b/cypress/e2e/default/settings/email-change.ts index 0d499feee4a..b0be0484c89 100644 --- a/cypress/e2e/default/settings/email-change.ts +++ b/cypress/e2e/default/settings/email-change.ts @@ -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' ); }); diff --git a/cypress/e2e/default/settings/portfolio.ts b/cypress/e2e/default/settings/portfolio.ts index ea31efd2137..e8994b762f3 100644 --- a/cypress/e2e/default/settings/portfolio.ts +++ b/cypress/e2e/default/settings/portfolio.ts @@ -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(); }); }); diff --git a/cypress/e2e/default/user/report-user.ts b/cypress/e2e/default/user/report-user.ts index 4fe68b6aabc..cc1c23384f2 100644 --- a/cypress/e2e/default/user/report-user.ts +++ b/cypress/e2e/default/user/report-user.ts @@ -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'); diff --git a/tools/ui-components/src/control-label/control-label.tsx b/tools/ui-components/src/control-label/control-label.tsx index af33a927867..67f01b7a55e 100644 --- a/tools/ui-components/src/control-label/control-label.tsx +++ b/tools/ui-components/src/control-label/control-label.tsx @@ -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(' '); diff --git a/tools/ui-components/src/form-control/form-control-feedback.tsx b/tools/ui-components/src/form-control/form-control-feedback.tsx index bd2a07529d7..2227e76e6cb 100644 --- a/tools/ui-components/src/form-control/form-control-feedback.tsx +++ b/tools/ui-components/src/form-control/form-control-feedback.tsx @@ -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 ( - + {children} ); diff --git a/tools/ui-components/src/form-control/form-control-static.tsx b/tools/ui-components/src/form-control/form-control-static.tsx index 3a6ba924e3d..2d65e4a05f5 100644 --- a/tools/ui-components/src/form-control/form-control-static.tsx +++ b/tools/ui-components/src/form-control/form-control-static.tsx @@ -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 ( - + {children} ); diff --git a/tools/ui-components/src/form-control/form-control.stories.tsx b/tools/ui-components/src/form-control/form-control.stories.tsx index d4dc12e8ba5..708718866a7 100644 --- a/tools/ui-components/src/form-control/form-control.stories.tsx +++ b/tools/ui-components/src/form-control/form-control.stories.tsx @@ -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 = args => { +const StaticTemplate: Story> = args => { return ; }; @@ -51,7 +51,7 @@ Static.args = { children: 'foo@bar.com' }; -const FeedBackTemplate: Story = args => { +const FeedBackTemplate: Story> = args => { return ; }; diff --git a/tools/ui-components/src/form-control/form-control.test.tsx b/tools/ui-components/src/form-control/form-control.test.tsx index c1666143f82..c4cf7106fb5 100644 --- a/tools/ui-components/src/form-control/form-control.test.tsx +++ b/tools/ui-components/src/form-control/form-control.test.tsx @@ -5,21 +5,21 @@ import { FormControl } from '.'; describe('', () => { it('should render correctly', () => { - render(); - expect(screen.getByTestId('test')).toBeInTheDocument(); + render(); + expect(screen.getByLabelText('test')).toBeInTheDocument(); }); }); describe('', () => { it('should render correctly', () => { - render(); - expect(screen.getByTestId('test')).toBeInTheDocument(); + render(); + expect(screen.getByLabelText('test')).toBeInTheDocument(); }); }); describe('', () => { it('should render correctly', () => { - render(); - expect(screen.getByTestId('test')).toBeInTheDocument(); + render(); + expect(screen.getByLabelText('test')).toBeInTheDocument(); }); }); diff --git a/tools/ui-components/src/form-control/form-control.tsx b/tools/ui-components/src/form-control/form-control.tsx index f3f95597564..e16431f4f0c 100644 --- a/tools/ui-components/src/form-control/form-control.tsx +++ b/tools/ui-components/src/form-control/form-control.tsx @@ -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 ( - - ); + return ; }; FormControl.Feedback = FormControlFeedback; diff --git a/tools/ui-components/src/form-control/index.ts b/tools/ui-components/src/form-control/index.ts index 9a39cc761ea..0c7d256da6e 100644 --- a/tools/ui-components/src/form-control/index.ts +++ b/tools/ui-components/src/form-control/index.ts @@ -1,2 +1,2 @@ export { FormControl } from './form-control'; -export type { FormControlProps, FormControlVariationProps } from './types'; +export type { FormControlProps } from './types'; diff --git a/tools/ui-components/src/form-control/types.ts b/tools/ui-components/src/form-control/types.ts index 61930502714..eec01d8c4fb 100644 --- a/tools/ui-components/src/form-control/types.ts +++ b/tools/ui-components/src/form-control/types.ts @@ -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) => void; - readonly?: never; - }; - -export type FormControlProps = React.HTMLAttributes & { - 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 = 'input' +> = { + componentClass?: TElement | string; +} & React.ComponentProps; diff --git a/tools/ui-components/src/form-group/form-group.test.tsx b/tools/ui-components/src/form-group/form-group.test.tsx index 847e9352a6d..44085d7bd98 100644 --- a/tools/ui-components/src/form-group/form-group.test.tsx +++ b/tools/ui-components/src/form-group/form-group.test.tsx @@ -26,10 +26,10 @@ describe('', () => { it('provided controlId to label and control', () => { render( - + ); - const input = screen.getByRole('switch'); + const input = screen.getByLabelText('test'); expect(input.id).toBe('my-control'); }); }); diff --git a/tools/ui-components/src/form-group/form-group.tsx b/tools/ui-components/src/form-group/form-group.tsx index 901a85110d8..d1e0d4373da 100644 --- a/tools/ui-components/src/form-group/form-group.tsx +++ b/tools/ui-components/src/form-group/form-group.tsx @@ -7,7 +7,7 @@ export type FormContextProps = Pick< >; export const FormContext = createContext({}); -const defaultClasses = 'mb-3.5'; +const defaultClasses = 'mb-3.5 relative'; export const FormGroup = ({ className, diff --git a/tools/ui-components/src/help-block/help-block.stories.tsx b/tools/ui-components/src/help-block/help-block.stories.tsx index 267957204e0..0683958fbf9 100644 --- a/tools/ui-components/src/help-block/help-block.stories.tsx +++ b/tools/ui-components/src/help-block/help-block.stories.tsx @@ -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 = args => { +const Template: Story> = args => { return ; }; diff --git a/tools/ui-components/src/help-block/help-block.tsx b/tools/ui-components/src/help-block/help-block.tsx index 436ffa3a7db..413d70362b2 100644 --- a/tools/ui-components/src/help-block/help-block.tsx +++ b/tools/ui-components/src/help-block/help-block.tsx @@ -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( - ({ className, children }, ref): JSX.Element => { - const defaultClasses = 'block mt-1 mb-2 text-foreground-quaternary'; - const classes = [defaultClasses, className].join(' '); - return ( - - {children} - - ); - } -); +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 ( + + {children} + + ); +}); HelpBlock.displayName = 'HelpBlock'; diff --git a/tools/ui-components/src/help-block/index.ts b/tools/ui-components/src/help-block/index.ts index f83e648751a..f28c12c3c97 100644 --- a/tools/ui-components/src/help-block/index.ts +++ b/tools/ui-components/src/help-block/index.ts @@ -1,2 +1 @@ export { HelpBlock } from './help-block'; -export type { HelpBlockProps } from './types'; diff --git a/tools/ui-components/src/help-block/types.ts b/tools/ui-components/src/help-block/types.ts deleted file mode 100644 index 755dabfe4cd..00000000000 --- a/tools/ui-components/src/help-block/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface HelpBlockProps { - className?: string; - children?: React.ReactNode; -} diff --git a/tools/ui-components/src/index.ts b/tools/ui-components/src/index.ts index 4edf81c7c19..bbc3b3d4698 100644 --- a/tools/ui-components/src/index.ts +++ b/tools/ui-components/src/index.ts @@ -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';
{t('settings.share-projects')}
+
{children}