refactor(client): migrate buttons on the settings page to ui-components (#53739)

This commit is contained in:
Huyen Nguyen
2024-03-08 14:15:03 +07:00
committed by GitHub
parent 095d44fc53
commit 138a80f6ca
31 changed files with 119 additions and 182 deletions

View File

@@ -1,4 +1,3 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faRightToBracket } from '@fortawesome/free-solid-svg-icons';
import React, { ReactNode } from 'react';
@@ -32,8 +31,7 @@ const Login = ({
const href = isSignedIn ? `${homeLocation}/learn` : `${apiLocation}/signin`;
return (
<Button
bsStyle='default'
<a
className={(block ? 'btn-cta-big btn-block' : '') + ' signup-btn btn-cta'}
data-test-label={dataTestLabel}
data-playwright-test-label='header-sign-in-button'
@@ -44,7 +42,7 @@ const Login = ({
<span className='sr-only'> {t('buttons.sign-in')}</span>
</span>
<span className='login-btn-text'>{children || t('buttons.sign-in')}</span>
</Button>
</a>
);
};

View File

@@ -342,6 +342,7 @@ li > button.nav-link-signout:not([aria-disabled='true']):is(:hover, :focus) {
max-height: var(--header-element-size);
min-width: var(--header-element-size);
padding: 0 4px;
text-decoration: none;
}
@media (min-width: 601px) {

View File

@@ -29,7 +29,7 @@ test('should render', () => {
const button = screen.getByText(/submit/i);
expect(button).toHaveAttribute('type', 'submit');
expect(button).toBeDisabled();
expect(button).toHaveAttribute('aria-disabled', 'true');
});
test('should render with default values', () => {

View File

@@ -3,7 +3,7 @@
exports[`<BlockSaveButton /> snapshot 1`] = `
<div>
<button
class="btn btn-primary btn-block"
class=" relative inline-block mt-[0.5px] border-solid border-3 active:before:w-full active:before:h-full active:before:absolute active:before:inset-0 active:before:border-3 active:before:border-transparent active:before:bg-gray-900 active:before:opacity-20 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 focus:outline-none focus-visible:ring focus-visible:ring-focus-outline-color text-center cursor-pointer no-underline block w-full border-foreground-secondary bg-background-quaternary text-foreground-secondary hover:bg-foreground-primary hover:text-background-primary hover:border-foreground-secondary dark:hover:bg-background-primary dark:hover:text-foreground-primary px-3 py-1.5 text-md"
type="submit"
>
buttons.save

View File

@@ -1,4 +1,4 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { Button, type ButtonProps } from '@freecodecamp/ui';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -9,7 +9,7 @@ function BlockSaveButton({
}: {
children?: React.ReactNode;
disabled?: boolean;
bgSize?: string;
bgSize?: ButtonProps['size'];
}): JSX.Element {
const { t } = useTranslation();
@@ -18,8 +18,7 @@ function BlockSaveButton({
block={true}
// the button is used to submit solutions in projects that require external URL
// these buttons don't use bgSize, that's why the bgSize is optional.
bsSize={bgSize}
bsStyle='primary'
size={bgSize}
type='submit'
{...restProps}
>

View File

@@ -1,51 +0,0 @@
.toggle-active.btn[disabled] {
background-color: var(--secondary-color);
color: var(--secondary-background);
opacity: 1;
}
:is(
.about-settings,
.privacy-settings,
.email-settings,
#usernameSettings,
#camper-identity,
#internet-presence,
#portfolio-items,
#honesty-policy
)
:is(
button[aria-disabled='true'],
button[aria-disabled='true']:is(:focus, :hover)
) {
background-color: var(--quaternary-background);
color: var(--secondary-color);
opacity: 0.65;
cursor: not-allowed;
}
.toggle-not-active {
background-color: var(--quaternary-background);
color: var(--secondary-color);
}
.toggle-not-active:hover {
color: var(--secondary-background);
}
.toggle-not-active:hover,
.toggle-not-active:focus {
background-color: var(--secondary-color);
}
.btn-group .btn.toggle-not-active,
.btn-group .btn.toggle-active {
border-color: var(--tertiary-color);
padding-inline: 30px;
display: flex;
}
.btn-group .btn-primary,
.btn-group .btn-primary:focus,
.btn-group .btn-primary:hover {
border-color: var(--secondary-color);
}

View File

@@ -285,16 +285,14 @@ fieldset[disabled] .btn-primary.focus {
.btn-cta {
background-color: #feac32;
background-image: linear-gradient(#fecc4c, #ffac33);
border-width: 3px;
border-color: #feac32;
border: 3px solid #feac32;
color: #0a0a23 !important;
}
.btn-cta:hover,
.btn-cta:focus,
.btn-cta:active:hover {
background-color: #fecc4c !important;
border-width: 3px;
border-color: #f1a02a;
border: 3px solid #f1a02a;
background-image: none;
color: #0a0a23 !important;
}

View File

@@ -44,13 +44,10 @@ exports[`<Honesty /> <Honesty /> snapshot when isHonest is false: Honesty 1`] =
</p>
</y>
<Button
active={false}
aria-disabled={false}
block={true}
bsClass="btn"
bsStyle="primary"
disabled={false}
onClick={[Function]}
variant="primary"
>
buttons.agree-honesty
</Button>
@@ -102,13 +99,10 @@ exports[`<Honesty /> <Honesty /> snapshot when isHonest is true: HonestyAccepted
</p>
</y>
<Button
active={false}
aria-disabled={true}
block={true}
bsClass="btn"
bsStyle="primary"
disabled={false}
disabled={true}
onClick={[Function]}
variant="primary"
>
buttons.accepted-honesty
</Button>

View File

@@ -267,8 +267,8 @@ class AboutSettings extends Component<AboutProps, AboutState> {
</FormGroup>
</div>
<BlockSaveButton
aria-disabled={this.isFormPristine()}
bgSize='lg'
disabled={this.isFormPristine()}
bgSize='large'
{...(this.isFormPristine() && { tabIndex: -1 })}
>
{t('buttons.save')}{' '}

View File

@@ -1,4 +1,3 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { Link, navigate } from 'gatsby';
import { find } from 'lodash-es';
import React, { MouseEvent, useState } from 'react';
@@ -7,7 +6,7 @@ import type { TFunction } from 'i18next';
import { createSelector } from 'reselect';
import ScrollableAnchor, { configureAnchors } from 'react-scrollable-anchor';
import { connect } from 'react-redux';
import { Table } from '@freecodecamp/ui';
import { Table, Button } from '@freecodecamp/ui';
import { regeneratePathAndHistory } from '../../../../shared/utils/polyvinyl';
import ProjectPreviewModal from '../../templates/Challenges/components/project-preview-modal';
@@ -180,12 +179,6 @@ const LegacyFullStack = (props: CertificationSettingsProps) => {
const certSlug = Certification.LegacyFullStack;
const certLocation = `/certification/${username}/${certSlug}`;
const buttonStyle = {
marginBottom: '30px',
padding: '6px 12px',
fontSize: '18px'
};
const createClickHandler =
(certSlug: keyof typeof certSlugTypeMap) =>
(e: MouseEvent<HTMLButtonElement>) => {
@@ -225,29 +218,28 @@ const LegacyFullStack = (props: CertificationSettingsProps) => {
</ul>
</div>
<div className={'col-xs-12'}>
<div>
{fullStackClaimable ? (
<Button
bsSize='sm'
bsStyle='primary'
className={'col-xs-12'}
size='small'
variant='primary'
block={true}
href={certLocation}
id={'button-' + certSlug}
// This floating promise is acceptable
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={createClickHandler(certSlug)}
style={buttonStyle}
target='_blank'
>
{isFullStackCert ? t('buttons.show-cert') : t('buttons.claim-cert')}
</Button>
) : (
<Button
bsSize='sm'
bsStyle='primary'
className={'col-xs-12'}
size='small'
variant='primary'
block={true}
disabled={true}
id={'button-' + certSlug}
style={buttonStyle}
target='_blank'
>
{t('buttons.claim-cert')}
</Button>
@@ -402,10 +394,11 @@ function CertificationSettings(props: CertificationSettingsProps) {
<td colSpan={2}>
<Button
block={true}
bsStyle='primary'
className={'col-xs-12'}
variant='primary'
href={certLocation}
data-cy={`btn-for-${certSlug}`}
// This floating promise is acceptable
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={clickHandler}
>
{isCert ? t('buttons.show-cert') : t('buttons.claim-cert')}{' '}

View File

@@ -1,15 +0,0 @@
.btn-danger {
background-color: var(--danger-color);
color: var(--danger-background);
border-color: var(--danger-background);
}
.btn-danger:hover,
.btn-danger:focus {
color: var(--danger-color);
background-color: var(--danger-background);
border-color: var(--danger-background);
}
.danger-zone p {
color: var(--danger-color);
}

View File

@@ -1,19 +1,16 @@
import { Button } from '@freecodecamp/react-bootstrap';
import React, { useState } from 'react';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { Panel } from '@freecodecamp/ui';
import { Panel, Button } from '@freecodecamp/ui';
import { deleteAccount, resetProgress } from '../../redux/settings/actions';
import { FullWidthRow, Spacer } from '../helpers';
import DeleteModal from './delete-modal';
import ResetModal from './reset-modal';
import './danger-zone.css';
interface DangerZoneProps {
deleteAccount: () => void;
resetProgress: () => void;
@@ -52,9 +49,8 @@ function DangerZone({ deleteAccount, resetProgress, t }: DangerZoneProps) {
<FullWidthRow>
<Button
block={true}
bsSize='lg'
bsStyle='danger'
className='btn-danger'
size='large'
variant='danger'
onClick={toggleResetModal}
type='button'
>
@@ -63,9 +59,8 @@ function DangerZone({ deleteAccount, resetProgress, t }: DangerZoneProps) {
<Spacer size='small' />
<Button
block={true}
bsSize='lg'
bsStyle='danger'
className='btn-danger'
size='large'
variant='danger'
onClick={toggleDeleteModal}
type='button'
>

View File

@@ -1,11 +1,10 @@
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import { Modal } from '@freecodecamp/react-bootstrap';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Button } from '@freecodecamp/ui';
import { Spacer } from '../helpers';
import './danger-zone.css';
type DeleteModalProps = {
delete: () => void;
onHide: () => void;
@@ -44,9 +43,8 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
<hr />
<Button
block={true}
bsSize='lg'
bsStyle='primary'
className='btn-invert'
size='large'
variant='primary'
onClick={props.onHide}
type='button'
>
@@ -55,9 +53,8 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
<Spacer size='small' />
<Button
block={true}
bsSize='lg'
bsStyle='danger'
className='btn-danger'
size='large'
variant='danger'
onClick={props.delete}
type='button'
>

View File

@@ -1,11 +1,11 @@
import { Button } from '@freecodecamp/react-bootstrap';
import {
HelpBlock,
Alert,
FormGroup,
FormGroupProps,
FormControl,
ControlLabel
ControlLabel,
Button
} from '@freecodecamp/ui';
import { Link } from 'gatsby';
import React, { useState } from 'react';
@@ -150,11 +150,14 @@ function EmailSettings({
<p className='large-p text-center'>{t('settings.email.missing')}</p>
</FullWidthRow>
<FullWidthRow>
<Link style={{ textDecoration: 'none' }} to='/update-email'>
<Button block={true} bsSize='lg' bsStyle='primary'>
{t('buttons.edit')}
</Button>
</Link>
<Button
block={true}
size='large'
variant='primary'
href='/update-email'
>
{t('buttons.edit')}
</Button>
</FullWidthRow>
</div>
);
@@ -237,8 +240,8 @@ function EmailSettings({
</FormGroup>
</div>
<BlockSaveButton
aria-disabled={isDisabled}
bgSize='lg'
disabled={isDisabled}
bgSize='large'
{...(isDisabled && { tabIndex: -1 })}
>
{t('buttons.save')}{' '}

View File

@@ -1,4 +1,4 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { Button } from '@freecodecamp/ui';
import React from 'react';
import TestRenderer from 'react-test-renderer';
import ShallowRenderer from 'react-test-renderer/shallow';

View File

@@ -1,7 +1,6 @@
import { Button } from '@freecodecamp/react-bootstrap';
import React from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Panel } from '@freecodecamp/ui';
import { Panel, Button } from '@freecodecamp/ui';
import { FullWidthRow } from '../helpers';
import SectionHeader from './section-header';
@@ -39,8 +38,8 @@ const Honesty = ({ isHonest, updateIsHonest }: HonestyProps): JSX.Element => {
</Panel>
<Button
block={true}
bsStyle='primary'
aria-disabled={isHonest}
variant='primary'
disabled={isHonest}
onClick={() => !isHonest && updateIsHonest({ isHonest: true })}
>
{buttonText}

View File

@@ -292,8 +292,8 @@ class InternetSettings extends Component<InternetProps, InternetState> {
</FormGroup>
</div>
<BlockSaveButton
aria-disabled={isDisabled}
bgSize='lg'
disabled={isDisabled}
bgSize='large'
{...(isDisabled && { tabIndex: -1 })}
>
{t('buttons.save')}{' '}

View File

@@ -1,4 +1,3 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { findIndex, find, isEqual } from 'lodash-es';
import { nanoid } from 'nanoid';
import React, { Component } from 'react';
@@ -8,7 +7,8 @@ import {
FormControl,
ControlLabel,
HelpBlock,
FormGroupProps
FormGroupProps,
Button
} from '@freecodecamp/ui';
import { withTranslation } from 'react-i18next';
import isURL from 'validator/lib/isURL';
@@ -340,8 +340,8 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
) : null}
</FormGroup>
<BlockSaveButton
aria-disabled={isButtonDisabled}
bgSize='lg'
disabled={isButtonDisabled}
bgSize='large'
{...(isButtonDisabled && { tabIndex: -1 })}
>
{t('buttons.save-portfolio')}
@@ -349,8 +349,8 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
<Spacer size='small' />
<Button
block={true}
bsSize='lg'
bsStyle='danger'
size='large'
variant='danger'
onClick={() => this.handleRemoveItem(id)}
type='button'
>
@@ -380,8 +380,8 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
<Button
data-cy='add-portfolio'
block={true}
bsSize='lg'
bsStyle='primary'
size='large'
variant='primary'
disabled={unsavedItemId !== null}
onClick={this.handleAdd}
type='button'

View File

@@ -1,10 +1,10 @@
import { Button } from '@freecodecamp/react-bootstrap';
import React, { useState } from 'react';
import { useTranslation, withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { Button } from '@freecodecamp/ui';
import { userSelector } from '../../redux/selectors';
import type { ProfileUI } from '../../redux/prop-types';
@@ -145,11 +145,11 @@ function PrivacySettings({ submitProfileUI, user }: PrivacyProps): JSX.Element {
</div>
<Button
type='submit'
bsSize='lg'
bsStyle='primary'
size='large'
variant='primary'
data-cy='save-privacy-settings'
block={true}
aria-disabled={!madeChanges}
disabled={!madeChanges}
{...(!madeChanges && { tabIndex: -1 })}
>
{t('buttons.save')}{' '}
@@ -162,8 +162,8 @@ function PrivacySettings({ submitProfileUI, user }: PrivacyProps): JSX.Element {
<p>{t('settings.data')}</p>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
size='large'
variant='primary'
download={`${user.username}.json`}
href={`data:text/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(user)

View File

@@ -1,6 +1,7 @@
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import { Modal } from '@freecodecamp/react-bootstrap';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from '@freecodecamp/ui';
import { Spacer } from '../helpers';
@@ -35,9 +36,8 @@ function ResetModal(props: ResetModalProps): JSX.Element {
<hr />
<Button
block={true}
bsSize='lg'
bsStyle='primary'
className='btn-invert'
size='large'
variant='primary'
onClick={props.onHide}
type='button'
>
@@ -46,9 +46,8 @@ function ResetModal(props: ResetModalProps): JSX.Element {
<Spacer size='small' />
<Button
block={true}
bsSize='lg'
bsStyle='danger'
className='btn-danger'
size='large'
variant='danger'
onClick={props.reset}
type='button'
>

View File

@@ -1,7 +1,6 @@
import React from 'react';
import ToggleCheck from '../../assets/icons/toggle-check';
import type { ToggleSettingProps } from './toggle-radio-setting';
import '../helpers/toggle-button.css';
import './toggle-setting.css';
export default function ToggleButtonSetting({
@@ -35,7 +34,6 @@ export default function ToggleButtonSetting({
aria-pressed={flag}
{...(!flag && { onClick: toggleFlag })}
value='1'
className='toggle-button-right'
>
<span>
{onLabel}
@@ -47,7 +45,6 @@ export default function ToggleButtonSetting({
aria-pressed={!flag}
{...(flag && { onClick: toggleFlag })}
value='2'
className='toggle-button-left'
>
<span>
{offLabel}

View File

@@ -1,5 +1,4 @@
import React from 'react';
import '../helpers/toggle-button.css';
import './toggle-setting.css';
export type ToggleSettingProps = {

View File

@@ -221,8 +221,8 @@ class UsernameSettings extends Component<UsernameProps, UsernameState> {
this.renderAlerts(validating, error, isValidUsername)}
<FullWidthRow>
<BlockSaveButton
aria-disabled={isDisabled}
bgSize='lg'
disabled={isDisabled}
bgSize='large'
{...(isDisabled && { tabIndex: -1 })}
>
{t('buttons.save')}{' '}

View File

@@ -1,4 +1,3 @@
import { Button } from '@freecodecamp/react-bootstrap';
import { Link } from 'gatsby';
import { isString } from 'lodash-es';
import React, { useState, type FormEvent, type ChangeEvent } from 'react';
@@ -16,7 +15,8 @@ import {
FormControl,
ControlLabel,
Col,
Row
Row,
Button
} from '@freecodecamp/ui';
import { Spacer } from '../components/helpers';
@@ -102,8 +102,8 @@ function UpdateEmail({ isNewEmail, t, updateMyEmail }: UpdateEmailProps) {
</FormGroup>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
size='large'
variant='primary'
disabled={getEmailValidationState() !== 'success'}
type='submit'
>

View File

@@ -89,6 +89,11 @@ describe('project submission', () => {
// cy.url().should('not.have.string', url);
});
});
// Access to the clipboard reliably works in Electron browser.
// In other browsers, there are popups asking for permission
// thus we should only run these tests in Electron
// Ref: https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/testing-dom__clipboard
it(
'JavaScript projects can be submitted and then viewed in /settings and on the certifications',
{ browser: 'electron' },

View File

@@ -39,6 +39,7 @@ describe('Front End Development Libraries Superblock', () => {
cy.url().should('match', /\/settings\/?#certification-settings/);
});
});
describe('After submitting all 5 projects', () => {
before(() => {
cy.task('seed');

View File

@@ -32,6 +32,9 @@ describe('Picture input field', () => {
}
);
cy.wait(500);
cy.get('#camper-identity > .btn').should('not.be.disabled');
cy.get('#camper-identity')
.find('button')
.contains('Save')
.should('not.be.disabled');
});
});

View File

@@ -12,7 +12,9 @@ describe('Add 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('button')
.contains('Save this portfolio item')
.should('not.be.disabled');
cy.get('[data-cy="portfolio-url"]').type('This is a portfolio item');
cy.get('[data-cy="validation-message"]').contains(
@@ -43,7 +45,10 @@ describe('Add Portfolio Item', () => {
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('button')
.contains('Save this portfolio item')
.should('not.be.disabled');
cy.get('[data-cy="portfolio-description"]').type('{backspace}');
cy.get('button[type=submit]').contains('Save this portfolio item').click();

View File

@@ -33,7 +33,7 @@ describe('<Button />', () => {
).toHaveAttribute('type', 'submit');
});
it('should trigger the onClick prop on click', async () => {
it('should trigger the onClick prop on click if the component is a button element', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Hello world</Button>);
@@ -98,4 +98,20 @@ describe('<Button />', () => {
// Ensure that a link element is not rendered
expect(link).not.toBeInTheDocument();
});
it('should trigger the onClick prop on click if the component is an anchor element', async () => {
const onClick = jest.fn();
render(
<Button href='https://www.freecodecamp.org' onClick={onClick}>
freeCodeCamp
</Button>
);
const link = screen.getByRole('link', { name: /freeCodeCamp/i });
await userEvent.click(link);
expect(onClick).toHaveBeenCalledTimes(1);
});
});

View File

@@ -189,6 +189,7 @@ export const HeadlessButton = React.forwardRef<
download={download}
target={target}
ref={ref as React.Ref<HTMLAnchorElement>}
onClick={onClick}
{...rest}
>
{children}

View File

@@ -1,6 +1,6 @@
// Use this file as the entry point for component export
export { Alert, type AlertProps } from './alert';
export { Button } from './button';
export { Button, type ButtonProps } from './button';
export { CloseButton } from './close-button';
export { Image } from './image';
export { Table } from './table';