feature/default_network_editable
commit
d5c693d9db
@ -1,24 +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-shown { |
||||
@include Paragraph; |
||||
&__srp { |
||||
display: grid; |
||||
grid-template-columns: 1fr 1fr 1fr; |
||||
grid-area: input; |
||||
} |
||||
|
||||
padding: 8px 16px; |
||||
@media (max-width: 767px) { |
||||
&__srp { |
||||
grid-template-columns: 1fr; |
||||
} |
||||
} |
||||
|
||||
&__srp-error { |
||||
margin-top: 4px; |
||||
&__srp-word { |
||||
display: flex; |
||||
align-items: center; |
||||
margin: 8px; |
||||
} |
||||
|
||||
&__show-srp { |
||||
margin-top: 16px; |
||||
margin-bottom: 16px; |
||||
&__srp-word-label { |
||||
width: 2em; |
||||
} |
||||
|
||||
&__show-srp-label { |
||||
margin-left: 8px; |
||||
&__srp-error { |
||||
margin-top: 4px; |
||||
grid-area: error; |
||||
} |
||||
|
||||
&__srp-too-many-words-error { |
||||
margin-top: 4px; |
||||
grid-area: too-many-words-error; |
||||
} |
||||
} |
||||
|
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