[MM-55270] fix(accessibility): "channels" button in global header is not selectable with keyboard (#29224)

Этот коммит содержится в:
Vicktor
2025-01-23 20:27:13 +03:00
коммит произвёл GitHub
родитель d345e92136
Коммит 09fedc6cca
6 изменённых файлов: 154 добавлений и 50 удалений

Просмотреть файл

@@ -11,12 +11,22 @@ exports[`components/global/product_switcher should have an active button state w
onClick={[Function]}
>
<ProductMenuButton
active={true}
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
/>
<ProductBranding />
style={
Object {
"backgroundColor": "rgba(var(--sidebar-text-rgb), 0.16)",
"color": "rgba(var(--sidebar-text-rgb), 0.56)",
}
}
>
<ProductsIcon
color="rgba(var(--sidebar-text-rgb), 0.56)"
size={20}
/>
<ProductBranding />
</ProductMenuButton>
</ProductMenuContainer>
<Menu
ariaLabel="switcherOpen"
@@ -75,12 +85,22 @@ exports[`components/global/product_switcher should match snapshot 1`] = `
onClick={[Function]}
>
<ProductMenuButton
active={true}
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
/>
<ProductBranding />
style={
Object {
"backgroundColor": "rgba(var(--sidebar-text-rgb), 0.16)",
"color": "rgba(var(--sidebar-text-rgb), 0.56)",
}
}
>
<ProductsIcon
color="rgba(var(--sidebar-text-rgb), 0.56)"
size={20}
/>
<ProductBranding />
</ProductMenuButton>
</ProductMenuContainer>
<Menu
ariaLabel="switcherOpen"
@@ -139,12 +159,22 @@ exports[`components/global/product_switcher should match snapshot with product s
onClick={[Function]}
>
<ProductMenuButton
active={true}
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
/>
<ProductBranding />
style={
Object {
"backgroundColor": "rgba(var(--sidebar-text-rgb), 0.16)",
"color": "rgba(var(--sidebar-text-rgb), 0.56)",
}
}
>
<ProductsIcon
color="rgba(var(--sidebar-text-rgb), 0.56)"
size={20}
/>
<ProductBranding />
</ProductMenuButton>
</ProductMenuContainer>
<Menu
ariaLabel="switcherOpen"
@@ -203,12 +233,22 @@ exports[`components/global/product_switcher should match snapshot without licens
onClick={[Function]}
>
<ProductMenuButton
active={true}
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
/>
<ProductBrandingTeamEdition />
style={
Object {
"backgroundColor": "rgba(var(--sidebar-text-rgb), 0.16)",
"color": "rgba(var(--sidebar-text-rgb), 0.56)",
}
}
>
<ProductsIcon
color="rgba(var(--sidebar-text-rgb), 0.56)"
size={20}
/>
<ProductBrandingTeamEdition />
</ProductMenuButton>
</ProductMenuContainer>
<Menu
ariaLabel="switcherOpen"
@@ -267,12 +307,22 @@ exports[`components/global/product_switcher should render once when there are no
onClick={[Function]}
>
<ProductMenuButton
active={true}
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
/>
<ProductBranding />
style={
Object {
"backgroundColor": "rgba(var(--sidebar-text-rgb), 0.16)",
"color": "rgba(var(--sidebar-text-rgb), 0.56)",
}
}
>
<ProductsIcon
color="rgba(var(--sidebar-text-rgb), 0.56)"
size={20}
/>
<ProductBranding />
</ProductMenuButton>
</ProductMenuContainer>
<Menu
ariaLabel="switcherOpen"
@@ -331,12 +381,22 @@ exports[`components/global/product_switcher should render the correct amount of
onClick={[Function]}
>
<ProductMenuButton
active={true}
aria-controls="product-switcher-menu"
aria-expanded={true}
aria-label="Product switch menu"
/>
<ProductBranding />
style={
Object {
"backgroundColor": "rgba(var(--sidebar-text-rgb), 0.16)",
"color": "rgba(var(--sidebar-text-rgb), 0.56)",
}
}
>
<ProductsIcon
color="rgba(var(--sidebar-text-rgb), 0.56)"
size={20}
/>
<ProductBranding />
</ProductMenuButton>
</ProductMenuContainer>
<Menu
ariaLabel="switcherOpen"

Просмотреть файл

@@ -2,51 +2,63 @@
exports[`components/ProductBranding should show correct icon glyph when we are on Boards 1`] = `
<ProductBrandingContainer
tabIndex={0}
tabIndex={-1}
>
<ProductBoardsIcon
size={24}
/>
<Heading
className="sr-only"
element="h1"
margin="none"
size={200}
>
Boards
</Heading>
<ProductBrandingHeading>
Boards
</ProductBrandingHeading>
</ProductBrandingContainer>
`;
exports[`components/ProductBranding should show correct icon glyph when we are on Channels 1`] = `
<ProductBrandingContainer
tabIndex={0}
tabIndex={-1}
>
<ProductChannelsIcon
size={24}
/>
<Heading
className="sr-only"
element="h1"
margin="none"
size={200}
>
Channels
</Heading>
<ProductBrandingHeading>
Channels
</ProductBrandingHeading>
</ProductBrandingContainer>
`;
exports[`components/ProductBranding should show correct icon glyph when we are on Playbooks 1`] = `
<ProductBrandingContainer
tabIndex={0}
tabIndex={-1}
>
<ProductPlaybooksIcon
size={24}
/>
<Heading
className="sr-only"
element="h1"
margin="none"
size={200}
>
Playbooks
</Heading>
<ProductBrandingHeading>
Playbooks
</ProductBrandingHeading>
</ProductBrandingContainer>
`;

Просмотреть файл

@@ -9,13 +9,22 @@ import glyphMap, {ProductChannelsIcon} from '@mattermost/compass-icons/component
import {useCurrentProduct} from 'utils/products';
const ProductBrandingContainer = styled.div`
const ProductBrandingContainer = styled.span`
display: flex;
align-items: center;
`;
> * + * {
margin-left: 8px;
}
// Every style here except for 'margin-left' and 'font-family'is from the deprecated 'Heading' element.
// https://github.com/mattermost/compass-components/blob/362e96a4eb3489efc8c1852652859ef14a51eb64/src/components/heading/Heading.mixins.ts#L9-L74
const ProductBrandingHeading = styled.span`
font-family: 'Metropolis';
font-size: 16px;
line-height: 24px;
font-weight: bold;
margin: 0;
color: inherit;
margin-left: 8px;
`;
const ProductBranding = (): JSX.Element => {
@@ -24,15 +33,22 @@ const ProductBranding = (): JSX.Element => {
const Icon = currentProduct?.switcherIcon ? glyphMap[currentProduct.switcherIcon] : ProductChannelsIcon;
return (
<ProductBrandingContainer tabIndex={0}>
<ProductBrandingContainer tabIndex={-1}>
<Icon size={24}/>
{/* Heading for screen readers since an h1 shouldn't be inside a button */}
<Heading
element='h1'
size={200}
margin='none'
className='sr-only'
>
{currentProduct ? currentProduct.switcherText : 'Channels'}
</Heading>
<ProductBrandingHeading>
{currentProduct ? currentProduct.switcherText : 'Channels'}
</ProductBrandingHeading>
</ProductBrandingContainer>
);
};

Просмотреть файл

@@ -6,7 +6,7 @@ import styled from 'styled-components';
import Logo from 'components/common/svg_images_components/logo_dark_blue_svg';
const ProductBrandingTeamEditionContainer = styled.div`
const ProductBrandingTeamEditionContainer = styled.span`
display: flex;
align-items: center;
@@ -21,12 +21,10 @@ const StyledLogo = styled(Logo)`
}
`;
const Badge = styled.div`
const Badge = styled.span`
display: flex;
align-self: center;
padding: 2px 6px;
position: relative;
top: 1px;
border-radius: var(--radius-s);
margin-left: 12px;
background: rgba(var(--sidebar-text-rgb), 0.08);
@@ -40,7 +38,7 @@ const Badge = styled.div`
const ProductBrandingTeamEdition = (): JSX.Element => {
return (
<ProductBrandingTeamEditionContainer tabIndex={0}>
<ProductBrandingTeamEditionContainer tabIndex={-1}>
<StyledLogo
width={116}
height={20}

Просмотреть файл

@@ -159,7 +159,7 @@ describe('components/global/product_switcher', () => {
useStateSpy.mockImplementation(() => [false, setState]);
wrapper.find(ProductMenuContainer).simulate('click');
expect(wrapper.find(ProductMenuButton).props().active).toEqual(true);
expect(wrapper.find(ProductMenuButton).props()['aria-expanded']).toEqual(true);
expect(wrapper).toMatchSnapshot();
});

Просмотреть файл

@@ -6,7 +6,9 @@ import {useIntl} from 'react-intl';
import {useDispatch, useSelector} from 'react-redux';
import styled from 'styled-components';
import IconButton from '@mattermost/compass-components/components/icon-button'; // eslint-disable-line no-restricted-imports
import {
ProductsIcon,
} from '@mattermost/compass-icons/components';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
@@ -41,21 +43,29 @@ export const ProductMenuContainer = styled.nav`
}
`;
export const ProductMenuButton = styled(IconButton).attrs(() => ({
export const ProductMenuButton = styled.button.attrs(() => ({
id: 'product_switch_menu',
icon: 'products',
size: 'sm',
// we currently need this, since not passing a onClick handler is disabling the IconButton
// this is a known issue and is being tracked by UI platform team
// TODO@UI: remove the onClick, when it is not a mandatory prop anymore
onClick: () => {},
inverted: true,
compact: true,
type: 'button',
}))`
> i::before {
font-size: 20px;
letter-spacing: 20px;
display: flex;
align-items: center;
background: transparent;
border: none;
border-radius: 4px;
padding: 3px 6px 3px 5px;
&:hover, &:focus {
color: rgba(var(--sidebar-text-rgb), 0.56);
background-color: rgba(var(--sidebar-text-rgb), 0.08);
}
&:active {
color: rgba(var(--sidebar-text-rgb), 0.56);
background-color: rgba(var(--sidebar-text-rgb), 0.16);
}
> * + * {
margin-left: 8px;
}
`;
@@ -110,13 +120,21 @@ const ProductMenu = (): JSX.Element => {
>
<ProductMenuContainer onClick={handleClick}>
<ProductMenuButton
active={switcherOpen}
aria-expanded={switcherOpen}
aria-label={formatMessage({id: 'global_header.productSwitchMenu', defaultMessage: 'Product switch menu'})}
aria-controls='product-switcher-menu'
/>
{license.IsLicensed === 'false' && <ProductBrandingTeamEdition/>}
{license.IsLicensed === 'true' && <ProductBranding/>}
style={switcherOpen ? {
backgroundColor: 'rgba(var(--sidebar-text-rgb), 0.16)',
color: 'rgba(var(--sidebar-text-rgb), 0.56)',
} : {}}
>
<ProductsIcon
size={20}
color='rgba(var(--sidebar-text-rgb), 0.56)'
/>
{license.IsLicensed === 'false' && <ProductBrandingTeamEdition/>}
{license.IsLicensed === 'true' && <ProductBranding/>}
</ProductMenuButton>
</ProductMenuContainer>
<Menu
listId={'product-switcher-menu-dropdown'}