Skip to content

Commit

Permalink
a11y: split button aria
Browse files Browse the repository at this point in the history
* Add accessible split button component
* Refactor Library split button
  • Loading branch information
OEvgeny committed Apr 30, 2022
1 parent 5c07d7f commit da7e713
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 45 deletions.
113 changes: 113 additions & 0 deletions Composer/packages/lib/ui-shared/src/components/SplitButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx } from '@emotion/core';
import styled from '@emotion/styled';
import { FluentTheme } from '@fluentui/theme';
import { DefaultButton, IButtonProps } from '@fluentui/react/lib/Button';

interface SplitButtonProps
extends Omit<
IButtonProps,
| 'split'
| 'splitButtonMenuProps'
| 'buttonType'
| 'getSplitButtonClassNames'
| 'renderPersistedMenuHiddenOnMount'
| 'primaryActionButtonProps'
> {
splitButtonAriaLabel: string;
}

const SplitButtonContainer = styled.div`
position: relative;
display: inline-flex;
label: split-button-container;
`;

const SplitButtonDelimiter = styled.span(
({ primary, disabled }: { primary?: boolean; disabled?: boolean }) => `
position: absolute;
width: 1px;
right: 33px;
top: 8px;
bottom: 8px;
background-color: ${
disabled
? FluentTheme.semanticColors.disabledText
: primary
? FluentTheme.palette.white
: FluentTheme.palette.neutralPrimary
};
label: split-button-delimiter;
`
);

const menuButtonStyles = {
root: {
minWidth: '32px',
width: '32px',
marginLeft: '-2px',
},
label: {
display: 'none',
},
};

/**
* Accessible analog of the Fluent split button component.
*
* Fluent team doesn't have plans to fix a11y issues of the split button in the current version
* as it'd introduce breaking changes.
* For more details see: https://github.com/microsoft/fluentui/issues/21904.
*
* The component intended to mimic Fluent Button API, so it's easier to switch back when accessibility
* concerns are addressed in Fluent.
*
* Unlike Fluent split button this component consists of two buttons. This allows to operate with buttons
* independently and addresses issues with nested button controls not being accessible using screen readers.
*/
export const SplitButton: React.FC<SplitButtonProps> = ({
primary,
menuAs,
menuIconProps,
menuProps,
menuTriggerKeyCode,
contextMenu,
onMenuClick,
onAfterMenuDismiss,
onContextMenu,
onRenderMenuIcon,
persistMenu,
onContextMenuCapture,
splitButtonAriaLabel,
disabled,
...props
}) => (
<SplitButtonContainer>
<DefaultButton disabled={disabled} primary={primary} {...props} />
<DefaultButton
aria-label={splitButtonAriaLabel}
contextMenu={contextMenu}
disabled={disabled}
menuAs={menuAs}
menuIconProps={menuIconProps}
menuProps={menuProps}
menuTriggerKeyCode={menuTriggerKeyCode}
persistMenu={persistMenu}
primary={primary}
styles={menuButtonStyles}
onAfterMenuDismiss={onAfterMenuDismiss}
onContextMenu={onContextMenu}
onContextMenuCapture={onContextMenuCapture}
onMenuClick={onMenuClick}
onRenderMenuIcon={onRenderMenuIcon}
/>
<SplitButtonDelimiter disabled={disabled} primary={primary} />
</SplitButtonContainer>
);

SplitButton.displayName = 'SplitButton';
1 change: 1 addition & 0 deletions Composer/packages/lib/ui-shared/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export * from './CopyableText';
export * from './SectionTitle';
export * from './HelpTooltip';
export * from './Field';
export * from './SplitButton';
76 changes: 31 additions & 45 deletions extensions/packageManager/src/pages/Library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

/** @jsx jsx */
import { jsx } from '@emotion/core';
import styled from '@emotion/styled';
import React, { useState, Fragment, useEffect } from 'react';
import formatMessage from 'format-message';
import {
Link,
PrimaryButton,
DefaultButton,
Pivot,
PivotItem,
Expand All @@ -32,7 +32,7 @@ import {
useTelemetryClient,
TelemetryClient,
} from '@bfc/extension-client';
import { Toolbar, IToolbarItem, LoadingSpinner, DisplayMarkdownDialog } from '@bfc/ui-shared';
import { Toolbar, IToolbarItem, LoadingSpinner, DisplayMarkdownDialog, SplitButton } from '@bfc/ui-shared';
import ReactMarkdown from 'react-markdown';

import {
Expand Down Expand Up @@ -62,6 +62,23 @@ export interface PackageSourceFeed extends IDropdownOption {
readonly?: boolean;
}

const InstallButtonText = styled.span`
overflow: hidden;
display: inline-block;
label: install-button-text;
`;

const InstallButtonVersion = styled.span`
max-width: 80px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
label: install-button-version;
`;

const Library: React.FC = () => {
const [items, setItems] = useState<LibraryRef[]>([]);
const { projectId, reloadProject, projectCollection: allProjectCollection, stopBot } = useProjectApi();
Expand Down Expand Up @@ -576,6 +593,8 @@ const Library: React.FC = () => {
updateFeeds(response.data);
};

const InstallButton = versionOptions != undefined ? SplitButton : DefaultButton;

return (
<ScrollablePane scrollbarVisibility={ScrollbarVisibility.auto}>
<Dialog
Expand Down Expand Up @@ -779,65 +798,32 @@ const Library: React.FC = () => {
{selectedItem.authors}
</Stack.Item>
<Stack.Item align="center" grow={1} styles={{ root: { textAlign: 'right' } }}>
<PrimaryButton
<InstallButton
primary
disabled={!ejectedRuntime || !selectedItem.isCompatible}
menuProps={versionOptions}
split={versionOptions != undefined}
styles={{ root: { maxWidth: 180, textOverflow: 'ellipsis' } }}
splitButtonAriaLabel="See install options"
onClick={install}
>
{/* display "v1.0 installed" if installed, or "install v1.1" if not" */}
{isInstalled(selectedItem) && selectedVersion === installedVersion(selectedItem) ? (
<span>
<span
css={{
maxWidth: 80,
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
display: 'inline-block',
}}
title={selectedVersion}
>
{selectedVersion}
</span>
&nbsp;
<span css={{ display: 'inline-block', overflow: 'hidden' }}>{strings.installed}</span>
<InstallButtonVersion>{selectedVersion}</InstallButtonVersion>&nbsp;
<InstallButtonText>{strings.installed}</InstallButtonText>
</span>
) : isUpdate ? (
<span>
<span css={{ display: 'inline-block', overflow: 'hidden' }}>{strings.updateButton}</span>
<span
css={{
maxWidth: 80,
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
display: 'inline-block',
}}
title={selectedVersion}
>
{selectedVersion}
</span>
<InstallButtonText>{strings.updateButton}</InstallButtonText>&nbsp;
<InstallButtonVersion>{selectedVersion}</InstallButtonVersion>
</span>
) : (
<span>
<span css={{ display: 'inline-block', overflow: 'hidden' }}>{strings.installButton}</span>&nbsp;
<span
css={{
maxWidth: 80,
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
display: 'inline-block',
}}
title={selectedVersion}
>
{selectedVersion}
</span>
<InstallButtonText>{strings.installButton}</InstallButtonText>&nbsp;
<InstallButtonVersion>{selectedVersion}</InstallButtonVersion>
</span>
)}
</PrimaryButton>
</InstallButton>
</Stack.Item>
</Stack>

Expand Down

0 comments on commit da7e713

Please sign in to comment.