Merge pull request #14085 from MetaMask/Version-v10.11.3
Version v10.11.3 RCfeature/default_network_editable
commit
a87c84e952
@ -0,0 +1 @@ |
||||
export { default } from './srp-input'; |
@ -0,0 +1,235 @@ |
||||
import { ethers } from 'ethers'; |
||||
import React, { useCallback, useState } from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import { useI18nContext } from '../../../hooks/useI18nContext'; |
||||
import TextField from '../../ui/text-field'; |
||||
import { clearClipboard } from '../../../helpers/utils/util'; |
||||
import ActionableMessage from '../../ui/actionable-message'; |
||||
import Dropdown from '../../ui/dropdown'; |
||||
import Typography from '../../ui/typography'; |
||||
import ShowHideToggle from '../../ui/show-hide-toggle'; |
||||
import { TYPOGRAPHY } from '../../../helpers/constants/design-system'; |
||||
import { parseSecretRecoveryPhrase } from './parse-secret-recovery-phrase'; |
||||
|
||||
const { isValidMnemonic } = ethers.utils; |
||||
|
||||
const defaultNumberOfWords = 12; |
||||
|
||||
export default function SrpInput({ onChange }) { |
||||
const [srpError, setSrpError] = useState(''); |
||||
const [pasteFailed, setPasteFailed] = useState(false); |
||||
const [draftSrp, setDraftSrp] = useState( |
||||
new Array(defaultNumberOfWords).fill(''), |
||||
); |
||||
const [showSrp, setShowSrp] = useState( |
||||
new Array(defaultNumberOfWords).fill(false), |
||||
); |
||||
const [numberOfWords, setNumberOfWords] = useState(defaultNumberOfWords); |
||||
|
||||
const t = useI18nContext(); |
||||
|
||||
const onSrpChange = useCallback( |
||||
(newDraftSrp) => { |
||||
let newSrpError = ''; |
||||
const joinedDraftSrp = newDraftSrp.join(' '); |
||||
|
||||
if (newDraftSrp.some((word) => word !== '')) { |
||||
if (newDraftSrp.some((word) => word === '')) { |
||||
newSrpError = t('seedPhraseReq'); |
||||
} else if (!isValidMnemonic(joinedDraftSrp)) { |
||||
newSrpError = t('invalidSeedPhrase'); |
||||
} |
||||
} |
||||
|
||||
setDraftSrp(newDraftSrp); |
||||
setSrpError(newSrpError); |
||||
onChange(newSrpError ? '' : joinedDraftSrp); |
||||
}, |
||||
[setDraftSrp, setSrpError, t, onChange], |
||||
); |
||||
|
||||
const toggleShowSrp = useCallback((index) => { |
||||
setShowSrp((currentShowSrp) => { |
||||
const newShowSrp = currentShowSrp.slice(); |
||||
if (newShowSrp[index]) { |
||||
newShowSrp[index] = false; |
||||
} else { |
||||
newShowSrp.fill(false); |
||||
newShowSrp[index] = true; |
||||
} |
||||
return newShowSrp; |
||||
}); |
||||
}, []); |
||||
|
||||
const onSrpWordChange = useCallback( |
||||
(index, newWord) => { |
||||
if (pasteFailed) { |
||||
setPasteFailed(false); |
||||
} |
||||
const newSrp = draftSrp.slice(); |
||||
newSrp[index] = newWord.trim(); |
||||
onSrpChange(newSrp); |
||||
}, |
||||
[draftSrp, onSrpChange, pasteFailed], |
||||
); |
||||
|
||||
const onSrpPaste = useCallback( |
||||
(rawSrp) => { |
||||
const parsedSrp = parseSecretRecoveryPhrase(rawSrp); |
||||
let newDraftSrp = parsedSrp.split(' '); |
||||
|
||||
if (newDraftSrp.length > 24) { |
||||
setPasteFailed(true); |
||||
return; |
||||
} else if (pasteFailed) { |
||||
setPasteFailed(false); |
||||
} |
||||
|
||||
let newNumberOfWords = numberOfWords; |
||||
if (newDraftSrp.length !== numberOfWords) { |
||||
if (newDraftSrp.length < 12) { |
||||
newNumberOfWords = 12; |
||||
} else if (newDraftSrp.length % 3 === 0) { |
||||
newNumberOfWords = newDraftSrp.length; |
||||
} else { |
||||
newNumberOfWords = |
||||
newDraftSrp.length + (3 - (newDraftSrp.length % 3)); |
||||
} |
||||
setNumberOfWords(newNumberOfWords); |
||||
} |
||||
|
||||
if (newDraftSrp.length < newNumberOfWords) { |
||||
newDraftSrp = newDraftSrp.concat( |
||||
new Array(newNumberOfWords - newDraftSrp.length).fill(''), |
||||
); |
||||
} |
||||
setShowSrp(new Array(newNumberOfWords).fill(false)); |
||||
onSrpChange(newDraftSrp); |
||||
clearClipboard(); |
||||
}, |
||||
[numberOfWords, onSrpChange, pasteFailed, setPasteFailed], |
||||
); |
||||
|
||||
const numberOfWordsOptions = []; |
||||
for (let i = 12; i <= 24; i += 3) { |
||||
numberOfWordsOptions.push({ |
||||
name: t('srpInputNumberOfWords', [`${i}`]), |
||||
value: `${i}`, |
||||
}); |
||||
} |
||||
|
||||
return ( |
||||
<div className="import-srp__container"> |
||||
<label className="import-srp__srp-label"> |
||||
<Typography variant={TYPOGRAPHY.H4}> |
||||
{t('secretRecoveryPhrase')} |
||||
</Typography> |
||||
</label> |
||||
<ActionableMessage |
||||
className="import-srp__paste-tip" |
||||
iconFillColor="#037dd6" // This is `--color-info-default`
|
||||
message={t('srpPasteTip')} |
||||
useIcon |
||||
/> |
||||
<Dropdown |
||||
className="import-srp__number-of-words-dropdown" |
||||
onChange={(newSelectedOption) => { |
||||
const newNumberOfWords = parseInt(newSelectedOption, 10); |
||||
if (Number.isNaN(newNumberOfWords)) { |
||||
throw new Error('Unable to parse option as integer'); |
||||
} |
||||
|
||||
let newDraftSrp = draftSrp.slice(0, newNumberOfWords); |
||||
if (newDraftSrp.length < newNumberOfWords) { |
||||
newDraftSrp = newDraftSrp.concat( |
||||
new Array(newNumberOfWords - newDraftSrp.length).fill(''), |
||||
); |
||||
} |
||||
setNumberOfWords(newNumberOfWords); |
||||
setShowSrp(new Array(newNumberOfWords).fill(false)); |
||||
onSrpChange(newDraftSrp); |
||||
}} |
||||
options={numberOfWordsOptions} |
||||
selectedOption={`${numberOfWords}`} |
||||
/> |
||||
<div className="import-srp__srp"> |
||||
{[...Array(numberOfWords).keys()].map((index) => { |
||||
const id = `import-srp__srp-word-${index}`; |
||||
return ( |
||||
<div key={index} className="import-srp__srp-word"> |
||||
<label htmlFor={id} className="import-srp__srp-word-label"> |
||||
<Typography>{`${index + 1}.`}</Typography> |
||||
</label> |
||||
<TextField |
||||
id={id} |
||||
data-testid={id} |
||||
type={showSrp[index] ? 'text' : 'password'} |
||||
onChange={(e) => { |
||||
e.preventDefault(); |
||||
onSrpWordChange(index, e.target.value); |
||||
}} |
||||
value={draftSrp[index]} |
||||
autoComplete="off" |
||||
onPaste={(event) => { |
||||
const newSrp = event.clipboardData.getData('text'); |
||||
|
||||
if (newSrp.trim().match(/\s/u)) { |
||||
event.preventDefault(); |
||||
onSrpPaste(newSrp); |
||||
} else { |
||||
onSrpWordChange(index, newSrp); |
||||
} |
||||
}} |
||||
/> |
||||
<ShowHideToggle |
||||
id={`${id}-checkbox`} |
||||
ariaLabelHidden={t('srpWordHidden')} |
||||
ariaLabelShown={t('srpWordShown')} |
||||
shown={showSrp[index]} |
||||
data-testid={`${id}-checkbox`} |
||||
onChange={() => toggleShowSrp(index)} |
||||
title={t('srpToggleShow')} |
||||
/> |
||||
</div> |
||||
); |
||||
})} |
||||
</div> |
||||
{srpError ? ( |
||||
<ActionableMessage |
||||
className="import-srp__srp-error" |
||||
iconFillColor="#d73a49" // This is `--color-error-default`
|
||||
message={srpError} |
||||
type="danger" |
||||
useIcon |
||||
/> |
||||
) : null} |
||||
{pasteFailed ? ( |
||||
<ActionableMessage |
||||
className="import-srp__srp-too-many-words-error" |
||||
iconFillColor="#d73a49" // This is `--color-error-default`
|
||||
message={t('srpPasteFailedTooManyWords')} |
||||
primaryAction={{ |
||||
label: t('dismiss'), |
||||
onClick: () => setPasteFailed(false), |
||||
}} |
||||
type="danger" |
||||
useIcon |
||||
/> |
||||
) : null} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
SrpInput.propTypes = { |
||||
/** |
||||
* Event handler for SRP changes. |
||||
* |
||||
* This is only called with a valid, well-formated (i.e. exactly one space |
||||
* between each word) SRP or with an empty string. |
||||
* |
||||
* This is called each time the draft SRP is updated. If the draft SRP is |
||||
* valid, this is called with a well-formatted version of that draft SRP. |
||||
* Otherwise, this is called with an empty string. |
||||
*/ |
||||
onChange: PropTypes.func.isRequired, |
||||
}; |
@ -0,0 +1,71 @@ |
||||
.import-srp { |
||||
&__container { |
||||
display: grid; |
||||
grid-template-areas: |
||||
"title dropdown" |
||||
"paste-tip paste-tip" |
||||
"input input" |
||||
"error error" |
||||
"too-many-words-error too-many-words-error"; |
||||
} |
||||
|
||||
@media (max-width: 767px) { |
||||
&__container { |
||||
grid-template-areas: |
||||
"title" |
||||
"dropdown" |
||||
"paste-tip" |
||||
"input" |
||||
"error" |
||||
"too-many-words-error"; |
||||
} |
||||
} |
||||
|
||||
&__srp-label { |
||||
grid-area: title; |
||||
} |
||||
|
||||
&__number-of-words-dropdown { |
||||
grid-area: dropdown; |
||||
} |
||||
|
||||
&__paste-tip { |
||||
margin-bottom: 8px; |
||||
grid-area: paste-tip; |
||||
width: auto; |
||||
margin-left: auto; |
||||
margin-right: auto; |
||||
} |
||||
|
||||
&__srp { |
||||
display: grid; |
||||
grid-template-columns: 1fr 1fr 1fr; |
||||
grid-area: input; |
||||
} |
||||
|
||||
@media (max-width: 767px) { |
||||
&__srp { |
||||
grid-template-columns: 1fr; |
||||
} |
||||
} |
||||
|
||||
&__srp-word { |
||||
display: flex; |
||||
align-items: center; |
||||
margin: 8px; |
||||
} |
||||
|
||||
&__srp-word-label { |
||||
width: 2em; |
||||
} |
||||
|
||||
&__srp-error { |
||||
margin-top: 4px; |
||||
grid-area: error; |
||||
} |
||||
|
||||
&__srp-too-many-words-error { |
||||
margin-top: 4px; |
||||
grid-area: too-many-words-error; |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
import React from 'react'; |
||||
import SrpInput from '.'; |
||||
|
||||
export default { |
||||
title: 'Components/App/SrpInput', |
||||
id: __filename, |
||||
component: SrpInput, |
||||
argTypes: { |
||||
onChange: { action: 'changed' }, |
||||
}, |
||||
}; |
||||
|
||||
const Template = (args) => { |
||||
return <SrpInput {...args} />; |
||||
}; |
||||
|
||||
export const DefaultStory = Template.bind({}); |
||||
|
||||
DefaultStory.storyName = 'Default'; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
|
||||
const IconEyeSlash = ({ |
||||
size = 24, |
||||
color = 'currentColor', |
||||
ariaLabel, |
||||
className, |
||||
}) => ( |
||||
// This SVG is copied from `@fortawesome/fontawesome-free@5.13.0/regular/eye-slash.svg`.
|
||||
<svg |
||||
width={size} |
||||
height={size} |
||||
fill={color} |
||||
className={className} |
||||
aria-label={ariaLabel} |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
viewBox="0 0 640 512" |
||||
> |
||||
<path d="M634 471L36 3.51A16 16 0 0 0 13.51 6l-10 12.49A16 16 0 0 0 6 41l598 467.49a16 16 0 0 0 22.49-2.49l10-12.49A16 16 0 0 0 634 471zM296.79 146.47l134.79 105.38C429.36 191.91 380.48 144 320 144a112.26 112.26 0 0 0-23.21 2.47zm46.42 219.07L208.42 260.16C210.65 320.09 259.53 368 320 368a113 113 0 0 0 23.21-2.46zM320 112c98.65 0 189.09 55 237.93 144a285.53 285.53 0 0 1-44 60.2l37.74 29.5a333.7 333.7 0 0 0 52.9-75.11 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64c-36.7 0-71.71 7-104.63 18.81l46.41 36.29c18.94-4.3 38.34-7.1 58.22-7.1zm0 288c-98.65 0-189.08-55-237.93-144a285.47 285.47 0 0 1 44.05-60.19l-37.74-29.5a333.6 333.6 0 0 0-52.89 75.1 32.35 32.35 0 0 0 0 29.19C89.72 376.41 197.08 448 320 448c36.7 0 71.71-7.05 104.63-18.81l-46.41-36.28C359.28 397.2 339.89 400 320 400z" /> |
||||
</svg> |
||||
); |
||||
|
||||
IconEyeSlash.propTypes = { |
||||
/** |
||||
* The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc |
||||
*/ |
||||
size: PropTypes.number, |
||||
/** |
||||
* The color of the icon accepts design token css variables |
||||
*/ |
||||
color: PropTypes.string, |
||||
/** |
||||
* An additional className to assign the Icon |
||||
*/ |
||||
className: PropTypes.string, |
||||
/** |
||||
* The aria-label of the icon for accessibility purposes |
||||
*/ |
||||
ariaLabel: PropTypes.string, |
||||
}; |
||||
|
||||
export default IconEyeSlash; |
@ -0,0 +1,43 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
|
||||
const IconEye = ({ |
||||
size = 24, |
||||
color = 'currentColor', |
||||
ariaLabel, |
||||
className, |
||||
}) => ( |
||||
// This SVG copied from `@fortawesome/fontawesome-free@5.13.0/regular/eye.svg`.
|
||||
<svg |
||||
width={size} |
||||
height={size} |
||||
fill={color} |
||||
className={className} |
||||
aria-label={ariaLabel} |
||||
xmlns="http://www.w3.org/2000/svg" |
||||
viewBox="0 0 576 512" |
||||
> |
||||
<path d="M288 144a110.94 110.94 0 0 0-31.24 5 55.4 55.4 0 0 1 7.24 27 56 56 0 0 1-56 56 55.4 55.4 0 0 1-27-7.24A111.71 111.71 0 1 0 288 144zm284.52 97.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400c-98.65 0-189.09-55-237.93-144C98.91 167 189.34 112 288 112s189.09 55 237.93 144C477.1 345 386.66 400 288 400z" /> |
||||
</svg> |
||||
); |
||||
|
||||
IconEye.propTypes = { |
||||
/** |
||||
* The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc |
||||
*/ |
||||
size: PropTypes.number, |
||||
/** |
||||
* The color of the icon accepts design token css variables |
||||
*/ |
||||
color: PropTypes.string, |
||||
/** |
||||
* An additional className to assign the Icon |
||||
*/ |
||||
className: PropTypes.string, |
||||
/** |
||||
* The aria-label of the icon for accessibility purposes |
||||
*/ |
||||
ariaLabel: PropTypes.string, |
||||
}; |
||||
|
||||
export default IconEye; |
@ -0,0 +1 @@ |
||||
export { default } from './show-hide-toggle'; |
@ -0,0 +1,34 @@ |
||||
.show-hide-toggle { |
||||
position: relative; |
||||
display: inline-flex; |
||||
|
||||
&__input { |
||||
appearance: none; |
||||
|
||||
+ .show-hide-toggle__label { |
||||
cursor: pointer; |
||||
user-select: none; |
||||
} |
||||
|
||||
/* Focused when tabbing with keyboard */ |
||||
&:focus, |
||||
&:focus-visible { |
||||
outline: none; |
||||
|
||||
+ .show-hide-toggle__label { |
||||
outline: Highlight auto 1px; |
||||
} |
||||
} |
||||
|
||||
&:disabled { |
||||
+ label { |
||||
opacity: 0.5; |
||||
cursor: auto; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&__icon { |
||||
color: var(--color-icon-default); |
||||
} |
||||
} |
@ -0,0 +1,86 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import classnames from 'classnames'; |
||||
|
||||
import IconEye from '../icon/icon-eye'; |
||||
import IconEyeSlash from '../icon/icon-eye-slash'; |
||||
|
||||
const ShowHideToggle = ({ |
||||
id, |
||||
shown, |
||||
onChange, |
||||
ariaLabelHidden, |
||||
ariaLabelShown, |
||||
className, |
||||
'data-testid': dataTestId, |
||||
disabled, |
||||
title, |
||||
}) => { |
||||
return ( |
||||
<div className={classnames('show-hide-toggle', className)}> |
||||
<input |
||||
className="show-hide-toggle__input" |
||||
id={id} |
||||
type="checkbox" |
||||
checked={shown} |
||||
onChange={onChange} |
||||
data-testid={dataTestId} |
||||
disabled={disabled} |
||||
/> |
||||
<label htmlFor={id} className="show-hide-toggle__label" title={title}> |
||||
{shown ? ( |
||||
<IconEye |
||||
ariaLabel={ariaLabelShown} |
||||
className="show-hide-toggle__icon" |
||||
/> |
||||
) : ( |
||||
<IconEyeSlash |
||||
ariaLabel={ariaLabelHidden} |
||||
className="show-hide-toggle__icon" |
||||
/> |
||||
)} |
||||
</label> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
ShowHideToggle.propTypes = { |
||||
/** |
||||
* The id of the ShowHideToggle for htmlFor |
||||
*/ |
||||
id: PropTypes.string.isRequired, |
||||
/** |
||||
* If the ShowHideToggle is in the "shown" state or not |
||||
*/ |
||||
shown: PropTypes.bool.isRequired, |
||||
/** |
||||
* The onChange handler of the ShowHideToggle |
||||
*/ |
||||
onChange: PropTypes.func.isRequired, |
||||
/** |
||||
* The aria-label of the icon representing the "hidden" state |
||||
*/ |
||||
ariaLabelHidden: PropTypes.string.isRequired, |
||||
/** |
||||
* The aria-label of the icon representing the "shown" state |
||||
*/ |
||||
ariaLabelShown: PropTypes.string.isRequired, |
||||
/** |
||||
* An additional className to give the ShowHideToggle |
||||
*/ |
||||
className: PropTypes.string, |
||||
/** |
||||
* The data test id of the input |
||||
*/ |
||||
'data-testid': PropTypes.string, |
||||
/** |
||||
* Whether the input is disabled or not |
||||
*/ |
||||
disabled: PropTypes.bool, |
||||
/** |
||||
* The title for the toggle. This is shown in a tooltip on hover. |
||||
*/ |
||||
title: PropTypes.string, |
||||
}; |
||||
|
||||
export default ShowHideToggle; |
@ -0,0 +1,51 @@ |
||||
import React from 'react'; |
||||
import { useArgs } from '@storybook/client-api'; |
||||
import ShowHideToggle from '.'; |
||||
|
||||
export default { |
||||
title: 'Components/UI/ShowHideToggle', // title should follow the folder structure location of the component. Don't use spaces.
|
||||
id: __filename, |
||||
argTypes: { |
||||
id: { |
||||
control: 'text', |
||||
}, |
||||
ariaLabelHidden: { |
||||
control: 'text', |
||||
}, |
||||
ariaLabelShown: { |
||||
control: 'text', |
||||
}, |
||||
className: { |
||||
control: 'text', |
||||
}, |
||||
dataTestId: { |
||||
control: 'text', |
||||
}, |
||||
disabled: { |
||||
control: 'boolean', |
||||
}, |
||||
onChange: { |
||||
action: 'onChange', |
||||
}, |
||||
shown: { |
||||
control: 'boolean', |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
export const DefaultStory = (args) => { |
||||
const [{ shown }, updateArgs] = useArgs(); |
||||
const handleOnToggle = () => { |
||||
updateArgs({ shown: !shown }); |
||||
}; |
||||
return <ShowHideToggle {...args} shown={shown} onChange={handleOnToggle} />; |
||||
}; |
||||
|
||||
DefaultStory.args = { |
||||
id: 'showHideToggle', |
||||
ariaLabelHidden: 'hidden', |
||||
ariaLabelShown: 'shown', |
||||
shown: false, |
||||
}; |
||||
|
||||
DefaultStory.storyName = 'Default'; |
@ -0,0 +1,314 @@ |
||||
import React from 'react'; |
||||
import { isInaccessible, render } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import ShowHideToggle from '.'; |
||||
|
||||
describe('ShowHideToggle', () => { |
||||
beforeEach(() => { |
||||
jest.resetAllMocks(); |
||||
}); |
||||
|
||||
it('should set title', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByTitle } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
onChange={onChange} |
||||
title="example-title" |
||||
/>, |
||||
); |
||||
|
||||
expect(queryByTitle('example-title')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should set test ID', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByTestId } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
onChange={onChange} |
||||
data-testid="example-test-id" |
||||
/>, |
||||
); |
||||
|
||||
expect(queryByTestId('example-test-id')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should show correct aria-label when shown', () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByLabelText } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
|
||||
expect(queryByLabelText('hidden')).not.toBeInTheDocument(); |
||||
expect(queryByLabelText('shown')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should show correct aria-label when hidden', () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByLabelText } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown={false} |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
|
||||
expect(queryByLabelText('hidden')).toBeInTheDocument(); |
||||
expect(queryByLabelText('shown')).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should show correct checkbox state when shown', () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
|
||||
expect(queryByRole('checkbox')).toBeChecked(); |
||||
}); |
||||
|
||||
it('should show correct checkbox state when hidden', () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown={false} |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
|
||||
expect(queryByRole('checkbox')).not.toBeChecked(); |
||||
}); |
||||
|
||||
describe('enabled', () => { |
||||
it('should show checkbox as enabled', () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
|
||||
expect(queryByRole('checkbox')).toBeEnabled(); |
||||
}); |
||||
|
||||
it('should be accessible', () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
|
||||
expect(isInaccessible(queryByRole('checkbox'))).toBeFalsy(); |
||||
}); |
||||
|
||||
describe('shown', () => { |
||||
it('should call onChange when clicked', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
await userEvent.click(queryByRole('checkbox')); |
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1); |
||||
}); |
||||
|
||||
it('should call onChange on space', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
queryByRole('checkbox').focus(); |
||||
await userEvent.keyboard('[Space]'); |
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1); |
||||
}); |
||||
}); |
||||
|
||||
describe('hidden', () => { |
||||
it('should call onChange when clicked', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown={false} |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
await userEvent.click(queryByRole('checkbox')); |
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1); |
||||
}); |
||||
|
||||
it('should call onChange on space', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown={false} |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
queryByRole('checkbox').focus(); |
||||
await userEvent.keyboard('[Space]'); |
||||
|
||||
expect(onChange).toHaveBeenCalledTimes(1); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('disabled', () => { |
||||
it('should show checkbox as disabled', () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
disabled |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
|
||||
expect(queryByRole('checkbox')).toBeDisabled(); |
||||
}); |
||||
|
||||
it('should be accessible', () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
disabled |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
|
||||
expect(isInaccessible(queryByRole('checkbox'))).toBeFalsy(); |
||||
}); |
||||
|
||||
describe('shown', () => { |
||||
it('should not call onChange when clicked', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
disabled |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
await userEvent.click(queryByRole('checkbox')); |
||||
|
||||
expect(onChange).not.toHaveBeenCalled(); |
||||
}); |
||||
|
||||
it('should not call onChange on space', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown |
||||
disabled |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
queryByRole('checkbox').focus(); |
||||
await userEvent.keyboard('[Space]'); |
||||
|
||||
expect(onChange).not.toHaveBeenCalled(); |
||||
}); |
||||
}); |
||||
|
||||
describe('hidden', () => { |
||||
it('should not call onChange when clicked', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown={false} |
||||
disabled |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
await userEvent.click(queryByRole('checkbox')); |
||||
|
||||
expect(onChange).not.toHaveBeenCalled(); |
||||
}); |
||||
|
||||
it('should not call onChange on space', async () => { |
||||
const onChange = jest.fn(); |
||||
const { queryByRole } = render( |
||||
<ShowHideToggle |
||||
id="example" |
||||
ariaLabelHidden="hidden" |
||||
ariaLabelShown="shown" |
||||
shown={false} |
||||
disabled |
||||
onChange={onChange} |
||||
/>, |
||||
); |
||||
queryByRole('checkbox').focus(); |
||||
await userEvent.keyboard('[Space]'); |
||||
|
||||
expect(onChange).not.toHaveBeenCalled(); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue