From e0088db2b3648ec4518339ede32e6809c601bb26 Mon Sep 17 00:00:00 2001 From: Bruce Blaser Date: Wed, 12 Apr 2023 04:40:00 -0700 Subject: [PATCH] fix(a11y): settings toggles (#49664) Co-authored-by: Sboonny Co-authored-by: Sboonny --- client/src/assets/icons/toggle-check.tsx | 103 ++++++------ client/src/components/settings/email.tsx | 4 +- .../settings/keyboard-shortcuts.tsx | 4 +- client/src/components/settings/privacy.tsx | 25 +-- client/src/components/settings/sound.tsx | 4 +- client/src/components/settings/theme.tsx | 4 +- .../settings/toggle-button-setting.tsx | 66 ++++++++ .../settings/toggle-radio-setting.tsx | 72 +++++++++ .../components/settings/toggle-setting.css | 151 ++++++++++++++---- .../components/settings/toggle-setting.tsx | 56 ------- cypress/support/commands.ts | 4 +- 11 files changed, 331 insertions(+), 162 deletions(-) create mode 100644 client/src/components/settings/toggle-button-setting.tsx create mode 100644 client/src/components/settings/toggle-radio-setting.tsx delete mode 100644 client/src/components/settings/toggle-setting.tsx diff --git a/client/src/assets/icons/toggle-check.tsx b/client/src/assets/icons/toggle-check.tsx index 8c2fb184e60..79b768cd7b7 100644 --- a/client/src/assets/icons/toggle-check.tsx +++ b/client/src/assets/icons/toggle-check.tsx @@ -1,63 +1,56 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; - function ToggleCheck( props: JSX.IntrinsicAttributes & React.SVGProps ): JSX.Element { - const { t } = useTranslation(); - return ( - <> - {t('icons.toggle')} - - - {t('icons.toggle')} - - - - - - - + ); } diff --git a/client/src/components/settings/email.tsx b/client/src/components/settings/email.tsx index 6d3c8898fec..16ab0555f85 100644 --- a/client/src/components/settings/email.tsx +++ b/client/src/components/settings/email.tsx @@ -22,7 +22,7 @@ import BlockSaveButton from '../helpers/form/block-save-button'; import FullWidthRow from '../helpers/full-width-row'; import Spacer from '../helpers/spacer'; import SectionHeader from './section-header'; -import ToggleSetting from './toggle-setting'; +import ToggleButtonSetting from './toggle-button-setting'; const mapStateToProps = () => ({}); const mapDispatchToProps = (dispatch: Dispatch) => @@ -228,7 +228,7 @@ function EmailSettings({
- e.preventDefault()} data-testid='fcc-enable-shortcuts-setting' > - ({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -46,6 +46,7 @@ function PrivacySettings({ submitProfileUI, user }: PrivacyProps): JSX.Element { function submitNewProfileSettings(e: React.FormEvent) { e.preventDefault(); + if (!madeChanges) return; submitProfileUI(privacyValues); setMadeChanges(false); } @@ -57,7 +58,7 @@ function PrivacySettings({ submitProfileUI, user }: PrivacyProps): JSX.Element {

{t('settings.privacy')}

- - - - - - - - - - e.preventDefault()}> - e.preventDefault()} > - +
+ + {action} + +
+

{action}

+ {explain ?

{explain}

: null} +
+
+ + +
+
+
+ ); +} + +ToggleButtonSetting.displayName = 'ToggleButtonSetting'; diff --git a/client/src/components/settings/toggle-radio-setting.tsx b/client/src/components/settings/toggle-radio-setting.tsx new file mode 100644 index 00000000000..a6b67d075a1 --- /dev/null +++ b/client/src/components/settings/toggle-radio-setting.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import '../helpers/toggle-button.css'; +import './toggle-setting.css'; + +export type ToggleSettingProps = { + action: string; + explain?: string; + flag: boolean; + flagName: string; + toggleFlag: () => void; + offLabel: string; + onLabel: string; +}; + +export default function ToggleRadioSetting({ + action, + explain, + flag, + flagName, + toggleFlag, + ...restProps +}: ToggleSettingProps): JSX.Element { + const firstRadioId = `radioA${flagName}`; + const secondRadioId = `radioB${flagName}`; + + return ( +
+
+ + {action} + +
+

{action}

+ {explain ?

{explain}

: null} +
+
+ + +
+
+
+ ); +} + +ToggleRadioSetting.displayName = 'ToggleRadioSetting'; diff --git a/client/src/components/settings/toggle-setting.css b/client/src/components/settings/toggle-setting.css index e7ba31526ea..a530e67c77a 100644 --- a/client/src/components/settings/toggle-setting.css +++ b/client/src/components/settings/toggle-setting.css @@ -1,49 +1,142 @@ +.toggle-setting-container { + margin-bottom: 15px; +} + .toggle-setting-container .form-group { display: flex; justify-content: space-between; margin-bottom: 5px; } -.toggle-setting-container .toggle-label { - display: flex; - flex-direction: column; - justify-content: center; -} - -.toggle-setting-container .btn-group { - padding-inline: 5px; -} - .toggle-setting-container > .form-group > * { flex: 1 1 0; } -.toggle-setting-container .btn-group { +.toggle-description { + max-width: 50%; +} + +.toggle-description > p:first-child { + font-weight: 700; + margin-bottom: 0.5rem; +} + +.toggle-description > p:not(:first-child) { + font-style: italic; + font-size: 0.8rem; +} + +.toggle-setting-container fieldset { display: flex; - justify-content: flex-end; - align-items: flex-start; + justify-content: space-between; } -@media (max-width: 440px) { - .toggle-setting-container .form-group { +.toggle-button-group { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: min-content; + gap: 3px; +} + +.toggle-button-group button { + padding: 5px 1.5rem; + border: 3px solid var(--secondary-color); + white-space: nowrap; + display: grid; + align-items: baseline; + justify-content: center; + min-width: 6rem; +} + +.toggle-button-group button[aria-pressed='true'] { + background-color: var(--secondary-color); + color: var(--secondary-background); +} + +.toggle-button-group button > span { + position: relative; +} + +.toggle-button-group button > span > svg { + position: absolute; + right: -1.5rem; + margin-top: 0.1rem; +} + +.toggle-button-group button:first-of-type > span > svg, +[dir='rtl'] .toggle-button-group button > span > svg { + right: unset; + left: -1.25rem; +} + +[dir='rtl'] .toggle-button-group button:first-of-type > span > svg { + left: unset; + right: -1.5rem; +} + +.toggle-button-group button[aria-pressed='true']:hover { + cursor: default; +} + +.toggle-radio-group { + display: flex; + align-items: start; +} + +.toggle-radio-group label { + font-weight: normal; + display: flex; + align-items: center; +} + +.toggle-radio-group input { + -webkit-appearance: none; + appearance: none; + height: 0.8rem; + width: 0.8rem; + border-radius: 50%; + background: transparent; + border: 2px solid var(--secondary-color); + margin: 0; +} + +.toggle-radio-group [data-checked='true'] input { + background: var(--secondary-color); +} + +.toggle-radio-group [data-checked='true'] span { + font-weight: 700; +} + +.toggle-radio-group input:focus-visible { + outline-offset: 1px; +} + +.toggle-radio-group label[data-checked='false']:hover, +.toggle-radio-group label[data-checked='false'] input:hover { + cursor: pointer; +} + +.toggle-radio-group label + label { + margin-inline-start: 2rem; +} + +.toggle-radio-group input { + margin-inline-end: 0.35rem; + accent-color: var(--secondary-color); +} + +@media (max-width: 35rem) { + .toggle-setting-container fieldset { flex-direction: column; + margin-bottom: 1rem; } - .toggle-setting-container > .form-group > * { - flex: 0 1 auto; + .toggle-description { + max-width: none; } - .toggle-setting-container .toggle-label { - justify-content: flex-start; - } - - .toggle-setting-container .help-block { - margin-bottom: 0px; - } - - .toggle-setting-container .btn-group { - justify-content: flex-start; - margin-top: 5px; - margin-bottom: 5px; + .toggle-description > p:not(:first-child) { + margin-bottom: 0.5rem; } } diff --git a/client/src/components/settings/toggle-setting.tsx b/client/src/components/settings/toggle-setting.tsx deleted file mode 100644 index 36a2b33488c..00000000000 --- a/client/src/components/settings/toggle-setting.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { - FormGroup, - ControlLabel, - HelpBlock -} from '@freecodecamp/react-bootstrap'; -import React from 'react'; - -import { Spacer } from '../helpers'; -import TB from '../helpers/toggle-button'; - -import './toggle-setting.css'; - -type ToggleSettingProps = { - action: string; - explain?: string; - flag: boolean; - flagName: string; - toggleFlag: () => void; - offLabel: string; - onLabel: string; -}; - -export default function ToggleSetting({ - action, - explain, - flag, - flagName, - toggleFlag, - ...restProps -}: ToggleSettingProps): JSX.Element { - return ( - <> -
- - - {action} - {explain ? ( - - {explain} - - ) : null} - - - -
- - - ); -} - -ToggleSetting.displayName = 'ToggleSetting'; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index e1121adf333..0907b0bbde3 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -15,9 +15,9 @@ const preserveSession = () => { const setPrivacyTogglesToPublic = () => { cy.get('#privacy-settings') - .find('.toggle-not-active') + .find('[type=radio][value=2]') .each(element => { - cy.wrap(element).click().should('have.class', 'toggle-active'); + cy.wrap(element).click().should('be.checked'); }); cy.get('[data-cy=save-privacy-settings]').click(); cy.get('#honesty-policy').find('button').click();