Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable kebab dropdown keyboard accessibility #2265

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
245 changes: 142 additions & 103 deletions framework/PageActions/PageActionDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { ButtonVariant, Tooltip } from '@patternfly/react-core';
import {
ButtonVariant,
Divider,
Dropdown,
DropdownItem,
DropdownPosition,
DropdownSeparator,
DropdownToggle,
KebabToggle,
} from '@patternfly/react-core/deprecated';
import { CircleIcon } from '@patternfly/react-icons';
DropdownList,
DropdownPopperProps,
Icon,
MenuToggle,
MenuToggleElement,
Tooltip,
} from '@patternfly/react-core';
import { CircleIcon, EllipsisVIcon } from '@patternfly/react-icons';
import { ComponentClass, FunctionComponent, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { PFColorE, getPatternflyColor } from '../components/pfcolors';
import { getID } from '../hooks/useID';
Expand Down Expand Up @@ -40,7 +42,7 @@ interface PageActionDropdownProps<T extends object> {
isDisabled?: string | undefined;
label?: string;
onOpen?: (label: string, open: boolean) => void;
position?: DropdownPosition;
position?: DropdownPopperProps['position'];
selectedItem?: T;
selectedItems?: T[];
tooltip?: string;
Expand Down Expand Up @@ -97,64 +99,16 @@ export function PageActionDropdown<T extends object>(props: PageActionDropdownPr
const id = getID(props.label ?? 'actions-dropdown');

if (actions.length === 0) return <></>;
const Icon = icon;
const toggleIcon = Icon ? <Icon /> : label;

const isPrimary =
variant === ButtonVariant.primary || (hasBulkActions && !!selectedItems?.length);
/** Turn primary button to secondary if there are items selected */
const isSecondary =
variant === ButtonVariant.primary && !hasBulkActions && !!selectedItems?.length;
const Toggle =
label || Icon ? (
<DropdownToggle
id="toggle-dropdown"
isDisabled={!!isDisabled}
onToggle={() => setDropdownOpen(!dropdownOpen)}
toggleVariant={isSecondary ? 'secondary' : isPrimary ? 'primary' : undefined}
toggleIndicator={Icon && iconOnly ? null : undefined}
style={isPrimary && !label ? { color: 'var(--pf-v5-global--Color--light-100)' } : {}}
icon={Icon ? <Icon /> : undefined}
data-cy={id}
>
{iconOnly ? undefined : label}
</DropdownToggle>
) : (
<KebabToggle
className="toggle-kebab"
isDisabled={!!isDisabled}
onToggle={() => setDropdownOpen(!dropdownOpen)}
toggleVariant={isPrimary ? 'primary' : undefined}
style={isPrimary && !label ? { color: 'var(--pf-v5-global--Color--light-100)' } : {}}
data-cy={id}
>
{toggleIcon}
</KebabToggle>
);
const dropdown = (
<Dropdown
onSelect={() => setDropdownOpen(false)}
toggle={Toggle}
isOpen={dropdownOpen}
isPlain={!label || iconOnly}
dropdownItems={actions.map((action, index) => (
<PageDropdownActionItem
key={'label' in action ? action.label : `action-${index}`}
action={action}
selectedItems={selectedItems ?? []}
selectedItem={selectedItem}
hasIcons={hasIcons}
hasSwitches={hasSwitches}
index={index}
data-cy={id}
/>
))}
position={position}
// ZIndex 400 is needed for PF table stick headers
style={{ zIndex: dropdownOpen ? 400 : undefined }}
/>
);
let tooltipContent;
const isKebab = Boolean(!label && !icon);
const CustomIcon = icon;

let tooltipContent;
if (isDisabled) {
tooltipContent = isDisabled;
} else if (tooltip) {
Expand All @@ -165,9 +119,63 @@ export function PageActionDropdown<T extends object>(props: PageActionDropdownPr
tooltipContent = undefined;
}

const dropdownMenuLabel: string | JSX.Element | undefined =
iconOnly && CustomIcon ? (
<Icon>
<CustomIcon />
</Icon>
) : (
label
);

return (
<Tooltip content={tooltipContent} trigger={tooltipContent ? undefined : 'manual'}>
{dropdown}
<Dropdown
isOpen={dropdownOpen}
onSelect={() => setDropdownOpen(false)}
onOpenChange={(isOpen) => setDropdownOpen(isOpen)}
popperProps={{
direction: 'down',
position: position ?? 'right',
enableFlip: true,
}}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
id={isKebab ? 'toggle-kebab' : 'toggle-dropdown'}
isDisabled={!!isDisabled}
aria-label={isKebab ? 'kebab dropdown toggle' : 'dropdown toggle'}
variant={isSecondary ? 'secondary' : isPrimary ? 'primary' : 'plain'}
onClick={() => setDropdownOpen(!dropdownOpen)}
isExpanded={dropdownOpen}
style={isPrimary && !label ? { color: 'var(--pf-v5-global--Color--light-100)' } : {}}
icon={
CustomIcon ? (
<Icon>
<CustomIcon />
</Icon>
) : undefined
}
>
{dropdownMenuLabel ?? <EllipsisVIcon />}
</MenuToggle>
)}
>
<DropdownList>
{actions.map((action, index) => (
<PageDropdownActionItem
key={'label' in action ? action.label : `action-${index}`}
action={action}
selectedItems={selectedItems ?? []}
selectedItem={selectedItem}
hasIcons={hasIcons}
hasSwitches={hasSwitches}
index={index}
data-cy={id}
/>
))}
</DropdownList>
</Dropdown>
</Tooltip>
);
}
Expand All @@ -187,8 +195,8 @@ function PageDropdownActionItem<T extends object>(props: {

switch (action.type) {
case PageActionType.Button: {
let Icon: ComponentClass | FunctionComponent | undefined = action.icon;
if (!Icon && hasIcons) Icon = TransparentIcon;
let CustomIcon: ComponentClass | FunctionComponent | undefined = action.icon;
if (!CustomIcon && hasIcons) CustomIcon = TransparentIcon;
let tooltip;

if (isDisabled) {
Expand All @@ -207,37 +215,54 @@ function PageDropdownActionItem<T extends object>(props: {
isButtonDisabled = true;
}
return (
<Tooltip key={action.label} content={tooltip} trigger={tooltip ? undefined : 'manual'}>
<StyledDropdownItem $hasSwitches={hasSwitches} $isDanger={Boolean(action.isDanger)}>
<DropdownItem
icon={Icon ? <Icon /> : undefined}
onClick={() => {
switch (action.selection) {
case PageActionSelection.None:
action.onClick();
break;
case PageActionSelection.Single:
if (selectedItem) action.onClick(selectedItem);
break;
case PageActionSelection.Multiple:
if (selectedItems) action.onClick(selectedItems);
break;
}
}}
isAriaDisabled={isButtonDisabled}
id={getID(action)}
data-cy={getID(action)?.split('.').join('-')}
>
{action.label}
</DropdownItem>
</StyledDropdownItem>
</Tooltip>
<StyledDropdownItem $hasSwitches={hasSwitches} $isDanger={Boolean(action.isDanger)}>
<DropdownItem
id={getID(action)}
data-cy={getID(action)?.split('.').join('-')}
isDisabled={Boolean(isButtonDisabled)}
isAriaDisabled={Boolean(isButtonDisabled)}
onClick={() => {
switch (action.selection) {
case PageActionSelection.None:
action.onClick();
break;
case PageActionSelection.Single:
if (selectedItem) action.onClick(selectedItem);
break;
case PageActionSelection.Multiple:
if (selectedItems) action.onClick(selectedItems);
break;
}
}}
style={{
color:
action.isDanger && !isDisabled ? getPatternflyColor(PFColorE.Danger) : undefined,
}}
tooltipProps={{
content: tooltip,
trigger: tooltip ? undefined : 'manual',
}}
>
{CustomIcon ? (
<Icon
size="lg"
iconSize="md"
style={{
paddingRight: '.5rem',
}}
>
<CustomIcon />
</Icon>
) : undefined}
{action.label}
</DropdownItem>
</StyledDropdownItem>
);
}

case PageActionType.Link: {
let Icon: ComponentClass | FunctionComponent | undefined = action.icon;
if (!Icon && hasIcons) Icon = TransparentIcon;
let CustomIcon: ComponentClass | FunctionComponent | undefined = action.icon;
if (!CustomIcon && hasIcons) CustomIcon = TransparentIcon;
const tooltip = isDisabled ? isDisabled : action.tooltip;
let to: string;

Expand All @@ -256,17 +281,31 @@ function PageDropdownActionItem<T extends object>(props: {
}

return (
<Tooltip key={action.label} content={tooltip} trigger={tooltip ? undefined : 'manual'}>
<DropdownItem
isAriaDisabled={Boolean(isDisabled)}
icon={Icon ? <Icon /> : undefined}
component={<Link to={to}>{action.label}</Link>}
style={{
color:
action.isDanger && !isDisabled ? getPatternflyColor(PFColorE.Danger) : undefined,
}}
/>
</Tooltip>
<DropdownItem
isDisabled={Boolean(isDisabled)}
isAriaDisabled={Boolean(isDisabled)}
to={to}
style={{
color: action.isDanger && !isDisabled ? getPatternflyColor(PFColorE.Danger) : undefined,
}}
tooltipProps={{
content: tooltip,
trigger: tooltip ? undefined : 'manual',
}}
>
{CustomIcon ? (
<Icon
size="lg"
iconSize="md"
style={{
paddingRight: '.5rem',
}}
>
<CustomIcon />
</Icon>
) : undefined}
{action.label}
</DropdownItem>
);
}

Expand Down Expand Up @@ -295,7 +334,7 @@ function PageDropdownActionItem<T extends object>(props: {
}

case PageActionType.Seperator:
return <DropdownSeparator key={`separator-${index}`} />;
return <Divider component="li" key={`separator-${index}`} />;
}
}

Expand Down