diff --git a/client/modules/IDE/components/Preferences/OnOffToggle.jsx b/client/modules/IDE/components/Preferences/OnOffToggle.jsx new file mode 100644 index 0000000000..ae220da139 --- /dev/null +++ b/client/modules/IDE/components/Preferences/OnOffToggle.jsx @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +const OnOffToggle = ({ value, setValue, name, translationKey, children }) => { + const { t } = useTranslation(); + + return ( +
+ setValue(true)} + aria-label={t(`Preferences.${translationKey}OnARIA`)} + name={name} + id={`${name}-on`} + className="preference__radio-button" + value="On" + checked={value} + /> + + setValue(false)} + aria-label={t(`Preferences.${translationKey}OffARIA`)} + name={name} + id={`${name}-off`} + className="preference__radio-button" + value="Off" + checked={!value} + /> + + {children} +
+ ); +}; + +OnOffToggle.propTypes = { + /** + * `true` if turned on, `false` if off. + */ + value: PropTypes.bool.isRequired, + /** + * Function to call when the value is changed. + * Will receive the new `boolean` value. + */ + setValue: PropTypes.func.isRequired, + /** + * Used as the HTML `name` attribute of the form elements, + * and also used to formulate the `id`s. + */ + name: PropTypes.string.isRequired, + /** + * Common prefix for looking up the "On" and "Off" ARIA labels. + * If the ARIA label is t('Preferences.LintWarningOnARIA'), + * then the `translationKey` is `LintWarning`. + */ + translationKey: PropTypes.string, + /** + * Can insert additional elements in the same
as the inputs. + * Typically not used. + */ + children: PropTypes.node +}; + +OnOffToggle.defaultProps = { + translationKey: '', + children: null +}; + +export default OnOffToggle; diff --git a/client/modules/IDE/components/Preferences/OnOffToggle.test.jsx b/client/modules/IDE/components/Preferences/OnOffToggle.test.jsx new file mode 100644 index 0000000000..05f76bb0b7 --- /dev/null +++ b/client/modules/IDE/components/Preferences/OnOffToggle.test.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { render, screen, fireEvent } from '../../../../test-utils'; +import OnOffToggle from './OnOffToggle'; + +describe('OnOffToggle', () => { + const setValue = jest.fn(); + + const subject = (initialValue = false) => { + const result = render( + + ); + return { + ...result, + onButton: screen.getByLabelText('On'), + offButton: screen.getByLabelText('Off') + }; + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('highlights "On" when value is true', () => { + const { onButton, offButton } = subject(true); + + expect(onButton).toBeChecked(); + expect(offButton).not.toBeChecked(); + }); + + it('highlights "Off" when value is false', () => { + const { onButton, offButton } = subject(false); + + expect(onButton).not.toBeChecked(); + expect(offButton).toBeChecked(); + }); + + it('does nothing when clicking the active value', () => { + const { onButton } = subject(true); + + fireEvent.click(onButton); + + expect(setValue).not.toHaveBeenCalled(); + }); + + it('calls setValue when clicking the inactive value', () => { + const { offButton } = subject(true); + + fireEvent.click(offButton); + + expect(setValue).toHaveBeenCalledTimes(1); + expect(setValue).toHaveBeenCalledWith(false); + + // Note: the checked state will not change until the `value` prop changes. + }); +}); diff --git a/client/modules/IDE/components/Preferences/index.jsx b/client/modules/IDE/components/Preferences/index.jsx index fa5859400e..c5ef87b4a8 100644 --- a/client/modules/IDE/components/Preferences/index.jsx +++ b/client/modules/IDE/components/Preferences/index.jsx @@ -18,6 +18,7 @@ import { setAutocompleteHinter, setLinewrap } from '../../actions/preferences'; +import OnOffToggle from './OnOffToggle'; export default function Preferences() { const { t } = useTranslation(); @@ -196,143 +197,43 @@ export default function Preferences() {

{t('Preferences.Autosave')}

-
- dispatch(setAutosave(true))} - aria-label={t('Preferences.AutosaveOnARIA')} - name="autosave" - id="autosave-on" - className="preference__radio-button" - value="On" - checked={autosave} - /> - - dispatch(setAutosave(false))} - aria-label={t('Preferences.AutosaveOffARIA')} - name="autosave" - id="autosave-off" - className="preference__radio-button" - value="Off" - checked={!autosave} - /> - -
+ dispatch(setAutosave(value))} + name="autosave" + translationKey="Autosave" + />

{t('Preferences.AutocloseBracketsQuotes')}

-
- dispatch(setAutocloseBracketsQuotes(true))} - aria-label={t('Preferences.AutocloseBracketsQuotesOnARIA')} - name="autoclosebracketsquotes" - id="autoclosebracketsquotes-on" - className="preference__radio-button" - value="On" - checked={autocloseBracketsQuotes} - /> - - dispatch(setAutocloseBracketsQuotes(false))} - aria-label={t('Preferences.AutocloseBracketsQuotesOffARIA')} - name="autoclosebracketsquotes" - id="autoclosebracketsquotes-off" - className="preference__radio-button" - value="Off" - checked={!autocloseBracketsQuotes} - /> - -
+ dispatch(setAutocloseBracketsQuotes(value))} + name="autoclosebracketsquotes" + translationKey="AutocloseBracketsQuotes" + />

{t('Preferences.AutocompleteHinter')}

-
- dispatch(setAutocompleteHinter(true))} - aria-label={t('Preferences.AutocompleteHinterOnARIA')} - name="autocompletehinter" - id="autocompletehinter-on" - className="preference__radio-button" - value="On" - checked={autocompleteHinter} - /> - - dispatch(setAutocompleteHinter(false))} - aria-label={t('Preferences.AutocompleteHinterOffARIA')} - name="autocompletehinter" - id="autocompletehinter-off" - className="preference__radio-button" - value="Off" - checked={!autocompleteHinter} - /> - -
+ dispatch(setAutocompleteHinter(value))} + name="autocompletehinter" + translationKey="AutocompleteHinter" + />

{t('Preferences.WordWrap')}

-
- dispatch(setLinewrap(true))} - aria-label={t('Preferences.LineWrapOnARIA')} - name="linewrap" - id="linewrap-on" - className="preference__radio-button" - value="On" - checked={linewrap} - /> - - dispatch(setLinewrap(false))} - aria-label={t('Preferences.LineWrapOffARIA')} - name="linewrap" - id="linewrap-off" - className="preference__radio-button" - value="Off" - checked={!linewrap} - /> - -
+ dispatch(setLinewrap(value))} + name="linewrap" + translationKey="LineWrap" + />
@@ -340,66 +241,23 @@ export default function Preferences() {

{t('Preferences.LineNumbers')}

-
- dispatch(setLineNumbers(true))} - aria-label={t('Preferences.LineNumbersOnARIA')} - name="line numbers" - id="line-numbers-on" - className="preference__radio-button" - value="On" - checked={lineNumbers} - /> - - dispatch(setLineNumbers(false))} - aria-label={t('Preferences.LineNumbersOffARIA')} - name="line numbers" - id="line-numbers-off" - className="preference__radio-button" - value="Off" - checked={!lineNumbers} - /> - -
+ dispatch(setLineNumbers(value))} + name="line numbers" + translationKey="LineNumbers" + />

{t('Preferences.LintWarningSound')}

-
- dispatch(setLintWarning(true))} - aria-label={t('Preferences.LintWarningOnARIA')} - name="lint warning" - id="lint-warning-on" - className="preference__radio-button" - value="On" - checked={lintWarning} - /> - - dispatch(setLintWarning(false))} - aria-label={t('Preferences.LintWarningOffARIA')} - name="lint warning" - id="lint-warning-off" - className="preference__radio-button" - value="Off" - checked={!lintWarning} - /> - + dispatch(setLintWarning(value))} + name="lint warning" + translationKey="LintWarning" + > -
+