aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/design-system
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2023-05-22 15:08:47 +0200
committersonartech <sonartech@sonarsource.com>2023-05-22 20:02:56 +0000
commit3217fa45d0694e7769ebb435bb1bfb554b0b5274 (patch)
tree9b77d44e0c257762137c9bb581ef8daf4938675c /server/sonar-web/design-system
parent1dff9b8dbdb77baf69a41eb346d94dffdc4dc5c6 (diff)
downloadsonarqube-3217fa45d0694e7769ebb435bb1bfb554b0b5274.tar.gz
sonarqube-3217fa45d0694e7769ebb435bb1bfb554b0b5274.zip
SONAR-19245 Improve accessibility of the SelectionCard component
Diffstat (limited to 'server/sonar-web/design-system')
-rw-r--r--server/sonar-web/design-system/src/components/RadioButton.tsx3
-rw-r--r--server/sonar-web/design-system/src/components/SelectionCard.tsx46
-rw-r--r--server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx8
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}