diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2023-05-22 15:08:47 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-05-22 20:02:56 +0000 |
commit | 3217fa45d0694e7769ebb435bb1bfb554b0b5274 (patch) | |
tree | 9b77d44e0c257762137c9bb581ef8daf4938675c /server/sonar-web/design-system | |
parent | 1dff9b8dbdb77baf69a41eb346d94dffdc4dc5c6 (diff) | |
download | sonarqube-3217fa45d0694e7769ebb435bb1bfb554b0b5274.tar.gz sonarqube-3217fa45d0694e7769ebb435bb1bfb554b0b5274.zip |
SONAR-19245 Improve accessibility of the SelectionCard component
Diffstat (limited to 'server/sonar-web/design-system')
3 files changed, 34 insertions, 23 deletions
diff --git a/server/sonar-web/design-system/src/components/RadioButton.tsx b/server/sonar-web/design-system/src/components/RadioButton.tsx index a49eb04eb18..5dd58756d73 100644 --- a/server/sonar-web/design-system/src/components/RadioButton.tsx +++ b/server/sonar-web/design-system/src/components/RadioButton.tsx @@ -89,6 +89,7 @@ export const RadioButtonStyled = styled.input` outline: ${themeBorder('focus', 'radioFocusOutline')}; } + &.is-checked, &:focus:checked, &:focus-visible:checked, &:hover:checked, @@ -100,6 +101,7 @@ export const RadioButtonStyled = styled.input` border: ${themeBorder('default', 'radioBorder')}; } + &.is-disabled, &:disabled { background: ${themeColor('radioDisabledBackground')}; border: ${themeBorder('default', 'radioDisabledBorder')}; @@ -107,6 +109,7 @@ export const RadioButtonStyled = styled.input` ${tw`sw-cursor-not-allowed`} + &.is-checked, &:checked { background-image: linear-gradient( to right, diff --git a/server/sonar-web/design-system/src/components/SelectionCard.tsx b/server/sonar-web/design-system/src/components/SelectionCard.tsx index 9f7fb7efe2d..31df423e578 100644 --- a/server/sonar-web/design-system/src/components/SelectionCard.tsx +++ b/server/sonar-web/design-system/src/components/SelectionCard.tsx @@ -19,11 +19,10 @@ */ import styled from '@emotion/styled'; import classNames from 'classnames'; -import { noop } from 'lodash'; import tw from 'twin.macro'; import { translate } from '../helpers/l10n'; import { themeBorder, themeColor, themeContrast, themeShadow } from '../helpers/theme'; -import { RadioButton } from './RadioButton'; +import { RadioButtonStyled } from './RadioButton'; import { LightLabel } from './Text'; import { RecommendedIcon } from './icons/RecommendedIcon'; @@ -55,7 +54,9 @@ export function SelectionCard(props: SelectionCardProps) { } = props; const isActionable = Boolean(onClick); return ( - <Wrapper + <StyledButton + aria-checked={selected} + aria-disabled={disabled} className={classNames( 'js-radio-card', { @@ -67,35 +68,39 @@ export function SelectionCard(props: SelectionCardProps) { className )} onClick={isActionable && !disabled ? onClick : undefined} - tabIndex={0} + role={isActionable ? 'radio' : 'presentation'} + tabIndex={disabled ? -1 : 0} > - <Content> + <StyledContent> {isActionable && ( <div className="sw-items-start sw-mt-1/2 sw-mr-2"> - <RadioButton checked={selected} disabled={disabled} onCheck={noop} value={title} /> + <RadioButtonStyled + as="i" + className={classNames({ 'is-checked': selected, 'is-disabled': disabled })} + /> </div> )} <div> - <Header> + <StyledLabel> {title} <LightLabel>{titleInfo}</LightLabel> - </Header> - <Body>{children}</Body> + </StyledLabel> + <StyledBody>{children}</StyledBody> </div> - </Content> + </StyledContent> {recommended && ( - <Recommended> + <StyledRecommended> <StyledRecommendedIcon className="sw-mr-1" /> <span className="sw-align-middle"> <strong>{translate('recommended')}</strong> {recommendedReason} </span> - </Recommended> + </StyledRecommended> )} - </Wrapper> + </StyledButton> ); } -const Wrapper = styled.div` +const StyledButton = styled.button` ${tw`sw-relative sw-flex sw-flex-col`} ${tw`sw-rounded-2`} ${tw`sw-box-border`} @@ -105,6 +110,8 @@ const Wrapper = styled.div` &:focus { outline: none; + border: ${themeBorder('default', 'selectionCardBorderHover')}; + box-shadow: ${themeShadow('sm')}; } &.card-vertical { @@ -133,12 +140,13 @@ const Wrapper = styled.div` } `; -const Content = styled.div` +const StyledContent = styled.div` ${tw`sw-my-4 sw-mx-3`} ${tw`sw-flex sw-grow`} + ${tw`sw-text-left`} `; -const Recommended = styled.div` +const StyledRecommended = styled.div` ${tw`sw-body-sm`} ${tw`sw-py-2 sw-px-4`} ${tw`sw-box-border`} @@ -153,8 +161,8 @@ const StyledRecommendedIcon = styled(RecommendedIcon)` ${tw`sw-align-middle`} `; -const Header = styled.h2` - ${tw`sw-flex sw-items-center`} +const StyledLabel = styled.label` + ${tw`sw-flex`} ${tw`sw-mb-3 sw-gap-2`} ${tw`sw-body-sm-highlight`} @@ -165,7 +173,7 @@ const Header = styled.h2` } `; -const Body = styled.div` +const StyledBody = styled.div` ${tw`sw-flex sw-grow`} ${tw`sw-flex-col sw-justify-between`} `; diff --git a/server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx b/server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx index ef37745d392..3239dadd0c4 100644 --- a/server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx @@ -26,7 +26,7 @@ import { SelectionCard } from '../SelectionCard'; it('should render option and be actionnable', async () => { const user = userEvent.setup(); const onClick = jest.fn(); - setupWithProps({ + renderSelectionCard({ onClick, children: <>The Option</>, }); @@ -43,7 +43,7 @@ it('should render option and be actionnable', async () => { it('should not be actionnable when disabled', async () => { const user = userEvent.setup(); const onClick = jest.fn(); - setupWithProps({ + renderSelectionCard({ onClick, disabled: true, children: <>The Option</>, @@ -58,7 +58,7 @@ it('should not be actionnable when disabled', async () => { }); it('should not be actionnable when no click handler', () => { - setupWithProps({ + renderSelectionCard({ children: <>The Option</>, }); @@ -66,7 +66,7 @@ it('should not be actionnable when no click handler', () => { expect(screen.queryByRole('radio')).not.toBeInTheDocument(); }); -function setupWithProps(props: Partial<FCProps<typeof SelectionCard>> = {}) { +function renderSelectionCard(props: Partial<FCProps<typeof SelectionCard>> = {}) { return render( <SelectionCard recommended={true} |