Fleet UI: Accessibility button actions, tabbing (#22916)

This commit is contained in:
RachelElysia
2024-10-25 11:02:52 -04:00
committed by GitHub
parent 6bc0b5dcd9
commit 2a756088f1
38 changed files with 503 additions and 240 deletions
@@ -0,0 +1,92 @@
import React from "react";
import { screen } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import ActionsDropdown from "./ActionsDropdown";
const DROPDOWN_OPTIONS = [
{ disabled: false, label: "Edit", value: "edit-query" },
{ disabled: false, label: "Show query", value: "show-query" },
{ disabled: true, label: "Delete", value: "delete-query" },
];
const PLACEHOLDER = "Actions";
const ON_CHANGE = (value: string) => {
console.log(value);
};
describe("Actions dropdown", () => {
it("renders dropdown placeholder and options", async () => {
const { user } = renderWithSetup(
<ActionsDropdown
options={DROPDOWN_OPTIONS} // Test
placeholder={PLACEHOLDER}
onChange={ON_CHANGE}
/>
);
await user.click(screen.getByText("Actions"));
expect(screen.queryAllByText(/edit/i)[1]).toBeInTheDocument(); // Aria shows Edit twice since it's focused
expect(screen.queryByText(/show query/i)).toBeInTheDocument();
expect(screen.queryByText(/delete/i)).toBeInTheDocument();
});
it("renders dropdown as disabled when disabled prop is true", () => {
renderWithSetup(
<ActionsDropdown
options={DROPDOWN_OPTIONS}
placeholder={PLACEHOLDER}
onChange={ON_CHANGE}
disabled // Test
/>
);
expect(screen.getByRole("combobox")).toBeDisabled();
});
it("calls onChange with correct value when an option is selected", async () => {
const mockOnChange = jest.fn();
const { user } = renderWithSetup(
<ActionsDropdown
options={DROPDOWN_OPTIONS}
placeholder={PLACEHOLDER}
onChange={mockOnChange}
/>
);
await user.click(screen.getByText("Actions"));
await user.click(screen.getByText("Edit"));
expect(mockOnChange).toHaveBeenCalledWith("edit-query");
});
it("renders disabled option as non-selectable", async () => {
const { user } = renderWithSetup(
<ActionsDropdown
options={DROPDOWN_OPTIONS}
placeholder={PLACEHOLDER}
onChange={ON_CHANGE}
/>
);
await user.click(screen.getByText("Actions"));
const deleteOption = screen.getByText("Delete");
expect(deleteOption).toHaveAttribute("aria-disabled", "true");
});
it("closes the dropdown when clicking outside", async () => {
const { user } = renderWithSetup(
<ActionsDropdown
options={DROPDOWN_OPTIONS}
placeholder={PLACEHOLDER}
onChange={ON_CHANGE}
/>
);
await user.click(screen.getByText("Actions"));
expect(screen.getByText("Edit")).toBeVisible();
await user.click(document.body);
expect(screen.queryByText(/edit/i)).not.toBeInTheDocument();
});
});
@@ -0,0 +1,252 @@
import React from "react";
import Select, {
StylesConfig,
DropdownIndicatorProps,
OptionProps,
components,
} from "react-select-5";
import { PADDING } from "styles/var/padding";
import { COLORS } from "styles/var/colors";
import classnames from "classnames";
import { IDropdownOption } from "interfaces/dropdownOption";
import Icon from "components/Icon";
import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper";
const baseClass = "actions-dropdown";
interface IActionsDropdownProps {
options: IDropdownOption[];
placeholder: string;
onChange: (value: string) => void;
disabled?: boolean;
isSearchable?: boolean;
className?: string;
menuAlign?: "right" | "left" | "default";
}
const getOptionBackgroundColor = (state: any) => {
return state.isSelected || state.isFocused
? COLORS["ui-vibrant-blue-10"]
: "transparent";
};
const getLeftMenuAlign = (menuAlign: "right" | "left" | "default") => {
switch (menuAlign) {
case "right":
return "auto";
case "left":
return "0";
default:
return "-12px";
}
};
const getRightMenuAlign = (menuAlign: "right" | "left" | "default") => {
switch (menuAlign) {
case "right":
return "0";
default:
return "undefined";
}
};
const CustomDropdownIndicator = (
props: DropdownIndicatorProps<any, false, any>
) => {
const { isFocused, selectProps } = props;
// no access to hover state here from react-select so that is done in the scss
// file of ActionsDropdown.
const color =
isFocused || selectProps.menuIsOpen
? "core-fleet-blue"
: "core-fleet-black";
return (
<components.DropdownIndicator {...props} className={baseClass}>
<Icon
name="chevron-down"
color={color}
className={`${baseClass}__icon`}
/>
</components.DropdownIndicator>
);
};
const CustomOption: React.FC<OptionProps<IDropdownOption, false>> = (props) => {
const { innerProps, innerRef, data, isDisabled } = props;
const optionContent = (
<div
className={`${baseClass}__option`}
ref={innerRef}
{...innerProps}
tabIndex={isDisabled ? -1 : 0} // Tabbing skipped when disabled
aria-disabled={isDisabled}
>
{data.label}
{data.helpText && (
<span className={`${baseClass}__help-text`}>{data.helpText}</span>
)}
</div>
);
return (
<components.Option {...props}>
{data.tooltipContent ? (
<DropdownOptionTooltipWrapper tipContent={data.tooltipContent}>
{optionContent}
</DropdownOptionTooltipWrapper>
) : (
optionContent
)}
</components.Option>
);
};
const ActionsDropdown = ({
options,
placeholder,
onChange,
disabled,
isSearchable = false,
className,
menuAlign = "default",
}: IActionsDropdownProps): JSX.Element => {
const dropdownClassnames = classnames(baseClass, className);
const handleChange = (newValue: IDropdownOption | null) => {
if (newValue) {
onChange(newValue.value.toString());
}
};
const customStyles: StylesConfig<IDropdownOption, false> = {
container: (provided) => ({
...provided,
width: "80px",
}),
control: (provided, state) => ({
...provided,
display: "flex",
flexDirection: "row",
width: "max-content",
padding: "8px 0",
backgroundColor: "initial",
border: 0,
boxShadow: "none",
cursor: "pointer",
"&:hover": {
boxShadow: "none",
".actions-dropdown-select__placeholder": {
color: COLORS["core-vibrant-blue-over"],
},
".actions-dropdown-select__indicator path": {
stroke: COLORS["core-vibrant-blue-over"],
},
},
"&:active .actions-dropdown-select__indicator path": {
stroke: COLORS["core-vibrant-blue-down"],
},
// TODO: Figure out a way to apply separate &:focus-visible styling
// Currently only relying on &:focus styling for tabbing through app
...(state.menuIsOpen && {
".actions-dropdown-select__indicator svg": {
transform: "rotate(180deg)",
transition: "transform 0.25s ease",
},
}),
}),
placeholder: (provided, state) => ({
...provided,
color: state.isFocused
? COLORS["core-fleet-blue"]
: COLORS["core-fleet-black"],
fontSize: "14px",
lineHeight: "normal",
paddingLeft: 0,
marginTop: "1px",
}),
dropdownIndicator: (provided) => ({
...provided,
display: "flex",
padding: "2px",
svg: {
transition: "transform 0.25s ease",
},
}),
menu: (provided) => ({
...provided,
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
borderRadius: "4px",
zIndex: 6,
overflow: "hidden",
border: 0,
marginTop: 0,
minWidth: "158px",
maxHeight: "220px",
position: "absolute",
left: getLeftMenuAlign(menuAlign),
right: getRightMenuAlign(menuAlign),
animation: "fade-in 150ms ease-out",
}),
menuList: (provided) => ({
...provided,
padding: PADDING["pad-small"],
}),
valueContainer: (provided) => ({
...provided,
padding: 0,
}),
option: (provided, state) => ({
...provided,
padding: "10px 8px",
fontSize: "14px",
backgroundColor: getOptionBackgroundColor(state),
"&:hover": {
backgroundColor: state.isDisabled
? "transparent"
: COLORS["ui-vibrant-blue-10"],
},
"&:active": {
backgroundColor: state.isDisabled
? "transparent"
: COLORS["ui-vibrant-blue-10"],
},
...(state.isDisabled && {
color: COLORS["ui-fleet-black-50"],
fontStyle: "italic",
// pointerEvents: "none", // Prevents any mouse interaction
}),
}),
};
return (
<div className={baseClass}>
<Select<IDropdownOption, false>
options={options}
placeholder={placeholder}
onChange={handleChange}
isDisabled={disabled}
isSearchable={isSearchable}
styles={customStyles}
components={{
DropdownIndicator: CustomDropdownIndicator,
IndicatorSeparator: () => null,
Option: CustomOption,
SingleValue: () => null, // Doesn't replace placeholder text with selected text
// Note: react-select doesn't support skipping disabled options when keyboarding through
}}
controlShouldRenderValue={false} // Doesn't change placeholder text to selected text
isOptionSelected={() => false} // Hides any styling on selected option
className={dropdownClassnames}
classNamePrefix={`${baseClass}-select`}
isOptionDisabled={(option) => !!option.disabled}
/>
</div>
);
};
export default ActionsDropdown;
@@ -0,0 +1,6 @@
// All styling in customStyles part of react-select-5
.actions-dropdown-select__control {
&:focus-visible {
background-color: $core-fleet-blue;
}
}
@@ -0,0 +1 @@
export { default } from "./ActionsDropdown";
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useState, useRef } from "react";
import classnames from "classnames";
import Button from "components/buttons/Button";
@@ -74,18 +74,32 @@ export const FileUploader = ({
fileDetails,
}: IFileUploaderProps) => {
const [isFileSelected, setIsFileSelected] = useState(!!fileDetails);
const fileInputRef = useRef<HTMLInputElement>(null);
const classes = classnames(baseClass, className, {
[`${baseClass}__file-preview`]: isFileSelected,
});
const buttonVariant = buttonType === "button" ? "brand" : "text-icon";
const triggerFileInput = () => {
fileInputRef.current?.click();
};
const onFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
onFileUpload(files);
setIsFileSelected(true);
e.target.value = "";
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
triggerFileInput();
}
};
const renderGraphics = () => {
@@ -113,6 +127,9 @@ export const FileUploader = ({
variant={buttonVariant}
isLoading={isLoading}
disabled={disabled}
customOnKeyDown={handleKeyDown}
tabIndex={0}
onClick={triggerFileInput}
>
<label htmlFor="upload-file">
{buttonType === "link" && <Icon name="upload" />}
@@ -120,6 +137,7 @@ export const FileUploader = ({
</label>
</Button>
<input
ref={fileInputRef}
accept={accept}
id="upload-file"
type="file"
+1 -1
View File
@@ -113,7 +113,7 @@ const Modal = ({
<span>{title}</span>
{!disableClosingModal && (
<div className={`${baseClass}__ex`}>
<Button className="button button--unstyled" onClick={onExit}>
<Button variant="unstyled" onClick={onExit}>
<Icon name="close" color="core-fleet-black" size="medium" />
</Button>
</div>
@@ -1,33 +0,0 @@
import React from "react";
import { screen } from "@testing-library/react";
import { renderWithSetup } from "test/test-utils";
import DropdownCell from "./DropdownCell";
const DROPDOWN_OPTIONS = [
{ disabled: false, label: "Edit", value: "edit-query" },
{ disabled: false, label: "Show query", value: "show-query" },
{ disabled: true, label: "Delete", value: "delete-query" },
];
const PLACEHOLDER = "Actions";
const ON_CHANGE = (value: string) => {
console.log(value);
};
describe("Dropdown cell", () => {
it("renders dropdown placeholder and options", async () => {
const { user } = renderWithSetup(
<DropdownCell
options={DROPDOWN_OPTIONS}
placeholder={PLACEHOLDER}
onChange={ON_CHANGE}
/>
);
await user.click(screen.getByText("Actions"));
expect(screen.getByText(/edit/i)).toBeInTheDocument();
expect(screen.getByText(/show query/i)).toBeInTheDocument();
expect(screen.getByText(/delete/i)).toBeInTheDocument();
});
});
@@ -1,37 +0,0 @@
import React from "react";
// ignore TS error for now until these are rewritten in ts.
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import { IDropdownOption } from "interfaces/dropdownOption";
const baseClass = "dropdown-cell";
interface IDropdownCellProps {
options: IDropdownOption[];
placeholder: string;
onChange: (value: string) => void;
disabled?: boolean;
}
const DropdownCell = ({
options,
placeholder,
onChange,
disabled,
}: IDropdownCellProps): JSX.Element => {
return (
<div className={baseClass}>
<Dropdown
onChange={onChange}
placeholder={placeholder}
searchable={false}
options={options}
disabled={disabled ?? false}
/>
</div>
);
};
export default DropdownCell;
@@ -1,87 +0,0 @@
.dropdown-cell {
width: 80px;
.form-field {
margin: 0;
}
.Select {
position: relative;
border: 0;
height: auto;
&.is-focused,
&:hover {
border: 0;
}
&.is-focused:not(.is-open) {
.Select-control {
background-color: initial;
}
}
&.is-disabled {
.Select-control {
.Select-placeholder {
@include disabled;
}
}
}
.Select-control {
display: flex;
background-color: initial;
height: auto;
justify-content: space-between;
border: 0;
cursor: pointer;
&:hover {
box-shadow: none;
}
&:hover .Select-placeholder {
color: $core-vibrant-blue;
}
.Select-placeholder {
color: $core-fleet-black;
font-size: 14px;
line-height: normal;
padding-left: 0;
margin-top: 1px;
}
.Select-input {
height: auto;
}
.Select-arrow-zone {
display: flex;
}
}
.Select-menu-outer {
margin-top: $pad-xsmall;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
border-radius: $border-radius;
z-index: 6;
overflow: hidden;
border: 0;
width: 188px;
left: unset;
top: unset;
max-height: 220px;
padding: $pad-small;
position: absolute;
left: -12px;
}
&.is-open {
.Select-control .Select-placeholder {
color: $core-vibrant-blue;
}
}
}
}
@@ -1 +0,0 @@
export { default } from "./DropdownCell";
@@ -85,6 +85,7 @@ const TeamsDropdown = ({
onChange={onChange}
onOpen={onOpen}
onClose={onClose}
tabIndex={0}
/>
);
}
@@ -1,4 +1,4 @@
import React from "react";
import React, { Children } from "react";
import classnames from "classnames";
import Spinner from "components/Spinner";
@@ -35,6 +35,7 @@ export interface IButtonProps {
tabIndex?: number;
type?: "button" | "submit" | "reset";
title?: string;
/** Default: "brand" */
variant?: ButtonVariant;
onClick?:
| ((value?: any) => void)
@@ -44,6 +45,7 @@ export interface IButtonProps {
| React.KeyboardEvent<HTMLButtonElement>
) => void);
isLoading?: boolean;
customOnKeyDown?: (e: React.KeyboardEvent) => void;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -57,7 +59,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
static defaultProps = {
size: "",
type: "button",
variant: "default",
variant: "brand",
};
componentDidMount(): void {
@@ -115,6 +117,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
title,
variant,
isLoading,
customOnKeyDown,
} = this.props;
const fullClassName = classnames(
baseClass,
@@ -136,7 +139,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
className={fullClassName}
disabled={disabled}
onClick={handleClick}
onKeyDown={handleKeyDown}
onKeyDown={customOnKeyDown || handleKeyDown}
tabIndex={tabIndex}
type={type}
title={title}
@@ -301,8 +301,13 @@ $base-class: "button";
color: $core-vibrant-blue-down;
}
&:focus {
&:focus-visible {
color: $core-vibrant-blue-over;
border: 1px;
border-radius: 2px; // Visble when tabbing
background: var(--Core-White, #fff);
outline: none;
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #d9d9fe;
}
&:hover,
@@ -1,5 +1,5 @@
.dropdown-button {
padding: 8px 24px 8px 0;
padding: 8px 0;
&__wrapper {
display: flex;
position: relative;
@@ -1,4 +1,7 @@
.Select > .Select-menu-outer {
// Used with old react-select dropdown and
// New react-select-5 ActionsDropdown.tsx
.Select > .Select-menu-outer,
.actions-dropdown {
.is-disabled * {
color: $ui-fleet-black-50;
}
@@ -12,7 +12,7 @@ import { IScheduledQuery } from "interfaces/scheduled_query";
import { IDropdownOption } from "interfaces/dropdownOption";
import Checkbox from "components/forms/fields/Checkbox";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import ActionsDropdown from "components/ActionsDropdown";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import PerformanceImpactCell from "components/TableContainer/DataTable/PerformanceImpactCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
@@ -54,7 +54,7 @@ interface IPerformanceImpactCellProps extends IRowProps {
};
}
interface IDropdownCellProps extends IRowProps {
interface IActionsDropdownProps extends IRowProps {
cell: {
value: IDropdownOption[];
};
@@ -68,7 +68,7 @@ interface IDataColumn {
Cell:
| ((props: ICellProps) => JSX.Element)
| ((props: IPerformanceImpactCellProps) => JSX.Element)
| ((props: IDropdownCellProps) => JSX.Element);
| ((props: IActionsDropdownProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
}
@@ -182,8 +182,8 @@ const generateTableHeaders = (
Header: "",
disableSortBy: true,
accessor: "actions",
Cell: (cellProps: IDropdownCellProps) => (
<DropdownCell
Cell: (cellProps: IActionsDropdownProps) => (
<ActionsDropdown
options={cellProps.cell.value}
onChange={(value: string) =>
actionSelectHandler(value, cellProps.row.original)
@@ -71,7 +71,7 @@ const UserMenu = ({
return (
<div className={baseClass} data-testid="user-menu">
<DropdownButton options={dropdownItems}>
<DropdownButton options={dropdownItems} variant="unstyled">
<AvatarTopNav
className={`${baseClass}__avatar-image`}
user={{ gravatar_url_dark: currentUser.gravatar_url_dark }}
@@ -1,6 +1,8 @@
.user-menu {
button {
background-color: transparent;
margin-right: 24px;
transition: transform 0.3s ease;
&.focus-visible {
border: 1px solid $ui-vibrant-blue-10;
@@ -18,6 +20,20 @@
svg {
transition: transform 0.25s ease;
}
&:hover svg {
animation: bounceDown 0.3s ease-in-out;
}
@keyframes bounceDown {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(3px);
}
}
}
.dropdown-button__options {
@@ -16,10 +16,9 @@ import { buildQueryStringFromParams } from "utilities/url";
import { internationalTimeFormat } from "utilities/helpers";
import { uploadedFromNow } from "utilities/date_format";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import Card from "components/Card";
import Graphic from "components/Graphic";
import ActionsDropdown from "components/ActionsDropdown";
import TooltipWrapper from "components/TooltipWrapper";
import DataSet from "components/DataSet";
import Icon from "components/Icon";
@@ -183,7 +182,7 @@ interface IActionsDropdownProps {
onEditSoftwareClick: () => void;
}
const ActionsDropdown = ({
const SoftwareActionsDropdown = ({
isSoftwarePackage,
onDownloadClick,
onDeleteClick,
@@ -207,16 +206,17 @@ const ActionsDropdown = ({
return (
<div className={`${baseClass}__actions`}>
<Dropdown
<ActionsDropdown
className={`${baseClass}__host-actions-dropdown`}
onChange={onSelect}
placeholder="Actions"
searchable={false}
isSearchable={false}
options={
isSoftwarePackage
? SOFTWARE_PACKAGE_DROPDOWN_OPTIONS
: APP_STORE_APP_DROPDOWN_OPTIONS
? [...SOFTWARE_PACKAGE_DROPDOWN_OPTIONS]
: [...APP_STORE_APP_DROPDOWN_OPTIONS]
}
menuAlign="right"
/>
</div>
);
@@ -353,7 +353,7 @@ const SoftwarePackageCard = ({
</div>
)}
{showActions && (
<ActionsDropdown
<SoftwareActionsDropdown
isSoftwarePackage={!!softwarePackage}
onDownloadClick={onDownloadClick}
onDeleteClick={onDeleteClick}
@@ -1,7 +1,7 @@
import React from "react";
import TextCell from "components/TableContainer/DataTable/TextCell";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import ActionsDropdown from "components/ActionsDropdown";
import {
IJiraIntegration,
@@ -31,7 +31,7 @@ interface ICellProps extends IRowProps {
};
}
interface IDropdownCellProps extends IRowProps {
interface IActionsDropdownProps extends IRowProps {
cell: {
value: IDropdownOption[];
};
@@ -43,7 +43,7 @@ interface IDataColumn {
accessor: string;
Cell:
| ((props: ICellProps) => JSX.Element)
| ((props: IDropdownCellProps) => JSX.Element);
| ((props: IActionsDropdownProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
sortType?: string;
@@ -98,8 +98,8 @@ const generateTableHeaders = (
Header: "",
disableSortBy: true,
accessor: "actions",
Cell: (cellProps: IDropdownCellProps) => (
<DropdownCell
Cell: (cellProps: IActionsDropdownProps) => (
<ActionsDropdown
options={cellProps.cell.value}
onChange={(value: string) =>
actionSelectHandler(value, cellProps.row.original)
@@ -6,7 +6,7 @@ import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config";
import { IDropdownOption } from "interfaces/dropdownOption";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import ActionsDropdown from "components/ActionsDropdown";
import TextCell from "components/TableContainer/DataTable/TextCell";
import TooltipWrapper from "components/TooltipWrapper";
@@ -163,7 +163,7 @@ export const generateTableConfig = (
// but we don't use it.
accessor: "id",
Cell: (cellProps) => (
<DropdownCell
<ActionsDropdown
options={generateActions()}
onChange={(value: string) =>
actionSelectHandler(value, cellProps.row.original)
@@ -6,7 +6,7 @@ import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config";
import { IDropdownOption } from "interfaces/dropdownOption";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import ActionsDropdown from "components/ActionsDropdown";
import TextCell from "components/TableContainer/DataTable/TextCell";
import RenewDateCell from "../../../components/RenewDateCell";
@@ -104,7 +104,7 @@ export const generateTableConfig = (
// but we don't use it.
accessor: "id",
Cell: (cellProps) => (
<DropdownCell
<ActionsDropdown
options={generateActions()}
onChange={(value: string) =>
actionSelectHandler(value, cellProps.row.original)
@@ -114,6 +114,7 @@ const IdpSection = () => {
disabled={!completedForm}
onClick={onSubmit}
className="button-wrap"
variant="brand"
>
<span data-tip data-for="save-button">
Save
@@ -1,7 +1,7 @@
import React from "react";
import ReactTooltip from "react-tooltip";
import TextCell from "components/TableContainer/DataTable/TextCell/TextCell";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import ActionsDropdown from "components/ActionsDropdown";
import CustomLink from "components/CustomLink";
import { IUser, UserRole } from "interfaces/user";
import { ITeam } from "interfaces/team";
@@ -29,7 +29,7 @@ interface ICellProps extends IRowProps {
};
}
interface IDropdownCellProps extends IRowProps {
interface IActionsDropdownProps extends IRowProps {
cell: {
value: IDropdownOption[];
};
@@ -41,7 +41,7 @@ interface IDataColumn {
accessor: string;
Cell:
| ((props: ICellProps) => JSX.Element)
| ((props: IDropdownCellProps) => JSX.Element);
| ((props: IActionsDropdownProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
sortType?: string;
@@ -174,8 +174,8 @@ const generateColumnConfigs = (
Header: "",
disableSortBy: true,
accessor: "actions",
Cell: (cellProps: IDropdownCellProps) => (
<DropdownCell
Cell: (cellProps: IActionsDropdownProps) => (
<ActionsDropdown
options={cellProps.cell.value}
onChange={(value: string) =>
actionSelectHandler(value, cellProps.row.original)
@@ -2,7 +2,7 @@ import React from "react";
import LinkCell from "components/TableContainer/DataTable/LinkCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import DropdownCell from "components/ActionsDropdown";
import { ITeam } from "interfaces/team";
import { IDropdownOption } from "interfaces/dropdownOption";
import PATHS from "router/paths";
@@ -12,7 +12,7 @@ import { IDropdownOption } from "interfaces/dropdownOption";
import { generateRole, generateTeam, greyCell } from "utilities/helpers";
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
import { COLORS } from "styles/var/colors";
import DropdownCell from "../../../../../components/TableContainer/DataTable/DropdownCell";
import ActionsDropdown from "../../../../../components/ActionsDropdown";
interface IHeaderProps {
column: {
@@ -33,7 +33,7 @@ interface ICellProps extends IRowProps {
};
}
interface IDropdownCellProps extends IRowProps {
interface IActionsDropdownProps extends IRowProps {
cell: {
value: IDropdownOption[];
};
@@ -45,7 +45,7 @@ interface IDataColumn {
accessor: string;
Cell:
| ((props: ICellProps) => JSX.Element)
| ((props: IDropdownCellProps) => JSX.Element);
| ((props: IActionsDropdownProps) => JSX.Element);
disableHidden?: boolean;
disableSortBy?: boolean;
}
@@ -200,8 +200,8 @@ const generateTableHeaders = (
Header: "",
disableSortBy: true,
accessor: "actions",
Cell: (cellProps: IDropdownCellProps) => (
<DropdownCell
Cell: (cellProps: IActionsDropdownProps) => (
<ActionsDropdown
options={cellProps.cell.value}
onChange={(value: string) =>
actionSelectHandler(value, cellProps.row.original)
@@ -54,7 +54,7 @@ const HostStatusWebhookPreviewModal = ({
/>
</div>
<div className="modal-cta-wrap">
<Button type="button" onClick={toggleModal}>
<Button type="button" onClick={toggleModal} variant="brand">
Done
</Button>
</div>
@@ -30,7 +30,7 @@ const CustomLabelGroupHeading = (
const handleInputClick = (
event: React.MouseEvent<HTMLInputElement, MouseEvent>
) => {
onClickLabelSearchInput(event);
onClickLabelSearchInput && onClickLabelSearchInput(event);
inputRef.current?.focus();
event.stopPropagation();
};
@@ -17,11 +17,6 @@
padding: 0px;
border: none;
margin-left: 0;
img {
padding: 0px;
margin: 0px;
}
}
.premium-icon-tip {
@@ -24,12 +24,12 @@ declare module "react-select-5/dist/declarations/src/Select" {
IsMulti extends boolean,
Group extends GroupBase<Option>
> {
labelQuery: string;
canAddNewLabels: boolean;
onAddLabel: () => void;
onChangeLabelQuery: (event: React.ChangeEvent<HTMLInputElement>) => void;
onClickLabelSearchInput: React.MouseEventHandler<HTMLInputElement>;
onBlurLabelSearchInput: React.FocusEventHandler<HTMLInputElement>;
labelQuery?: string;
canAddNewLabels?: boolean;
onAddLabel?: () => void;
onChangeLabelQuery?: (event: React.ChangeEvent<HTMLInputElement>) => void;
onClickLabelSearchInput?: React.MouseEventHandler<HTMLInputElement>;
onBlurLabelSearchInput?: React.FocusEventHandler<HTMLInputElement>;
}
}
@@ -131,6 +131,10 @@ const LabelFilterSelect = ({
if (e.key === "Escape") {
setMenuIsOpen(false);
selectRef.current?.blur();
} else if (e.key === "Tab" && !e.shiftKey) {
// Allow tabbing out of the component
setMenuIsOpen(false);
selectRef.current?.blur();
} else {
setMenuIsOpen(true);
}
@@ -4,7 +4,6 @@ import strUtils from "utilities/strings";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import CustomLink from "components/CustomLink";
const baseClass = "delete-host-modal";
@@ -59,12 +58,7 @@ const DeleteHostModal = ({
};
return (
<Modal
title="Delete host"
onExit={onCancel}
onEnter={onSubmit}
className={baseClass}
>
<Modal title="Delete host" onExit={onCancel} className={baseClass}>
<>
<p>
This will remove the record of <b>{hostText()}</b>.{largeVolumeText()}
@@ -116,7 +116,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Query").parentElement?.parentElement?.parentElement
).toHaveClass("is-disabled");
).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -153,7 +153,7 @@ describe("Host Actions Dropdown", () => {
await user.click(screen.getByText("Actions"));
expect(
screen.getByText("Query").parentElement?.parentElement?.parentElement
).toHaveClass("is-disabled");
).toHaveClass("actions-dropdown-select__option--is-disabled");
});
it("renders the Query action as disabled when a host is updating", async () => {
@@ -180,7 +180,7 @@ describe("Host Actions Dropdown", () => {
await user.click(screen.getByText("Actions"));
expect(screen.getByText("Query").parentElement).toHaveClass(
"is-disabled"
"actions-dropdown-select__option--is-disabled"
);
});
});
@@ -388,7 +388,7 @@ describe("Host Actions Dropdown", () => {
debug();
expect(screen.getByText("Turn off MDM").parentElement).toHaveClass(
"is-disabled"
"actions-dropdown-select__option--is-disabled"
);
});
@@ -590,7 +590,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Lock").parentElement?.parentElement?.parentElement
).toHaveClass("is-disabled");
).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -845,7 +845,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Unlock").parentElement?.parentElement?.parentElement
).toHaveClass("is-disabled");
).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -981,7 +981,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Wipe").parentElement?.parentElement?.parentElement
).toHaveClass("is-disabled");
).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -1055,7 +1055,7 @@ describe("Host Actions Dropdown", () => {
screen
.getByText("Run script")
.parentElement?.parentElement?.parentElement?.classList.contains(
"is-disabled"
"actions-dropdown-select__option--is-disabled"
)
).toBeFalsy();
@@ -1098,7 +1098,7 @@ describe("Host Actions Dropdown", () => {
expect(
screen.getByText("Run script").parentElement?.parentElement
?.parentElement
).toHaveClass("is-disabled");
).toHaveClass("actions-dropdown-select__option--is-disabled");
await waitFor(() => {
waitFor(() => {
@@ -4,8 +4,7 @@ import { MdmEnrollmentStatus } from "interfaces/mdm";
import permissions from "utilities/permissions";
import { AppContext } from "context/app";
// @ts-ignore
import Dropdown from "components/forms/fields/Dropdown";
import ActionsDropdown from "components/ActionsDropdown";
import { generateHostActionOptions } from "./helpers";
import { HostMdmDeviceStatusUIState } from "../../helpers";
@@ -81,12 +80,12 @@ const HostActionsDropdown = ({
return (
<div className={baseClass}>
<Dropdown
<ActionsDropdown
className={`${baseClass}__host-actions-dropdown`}
onChange={onSelect}
placeholder="Actions"
searchable={false}
options={options}
menuAlign="right"
/>
</div>
);
@@ -5,7 +5,7 @@ import { IHostScript, ILastExecution } from "interfaces/script";
import { IUser } from "interfaces/user";
import Icon from "components/Icon";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import ActionsDropdown from "components/ActionsDropdown";
import {
isGlobalAdmin,
isTeamMaintainer,
@@ -24,7 +24,7 @@ interface IStatusCellProps {
};
}
interface IDropdownCellProps {
interface IActionsDropdownProps {
cell: {
value: IDropdownOption[];
};
@@ -94,7 +94,7 @@ export const generateTableColumnConfigs = (
Header: "",
disableSortBy: true,
accessor: "actions",
Cell: (cellProps: IDropdownCellProps) => {
Cell: (cellProps: IActionsDropdownProps) => {
if (scriptsDisabled) {
// create a basic span that doesn't use the dropdown component (which relies on react-select
// and makes it difficult for us to style the disabled tooltip underline on the placeholder text.
@@ -120,7 +120,7 @@ export const generateTableColumnConfigs = (
cellProps.row.original
);
return (
<DropdownCell
<ActionsDropdown
options={opts}
onChange={(value: string) =>
onSelectAction(value, cellProps.row.original)
@@ -23,7 +23,7 @@ import PATHS from "router/paths";
import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell";
import TextCell from "components/TableContainer/DataTable/TextCell";
import SoftwareNameCell from "components/TableContainer/DataTable/SoftwareNameCell";
import DropdownCell from "components/TableContainer/DataTable/DropdownCell";
import ActionsDropdown from "components/ActionsDropdown";
import VulnerabilitiesCell from "pages/SoftwarePage/components/VulnerabilitiesCell";
import VersionCell from "pages/SoftwarePage/components/VersionCell";
@@ -237,7 +237,7 @@ export const generateSoftwareTableHeaders = ({
} = original;
return (
<DropdownCell
<ActionsDropdown
placeholder="Actions"
options={generateActions({
userHasSWWritePermission,
+5
View File
@@ -26,4 +26,9 @@ export const COLORS = {
"status-success": "#3DB67B",
"status-warning": "#F8CD6B",
"status-error": "#ED6E85",
"core-vibrant-blue-over": "#5d5ae7",
"core-vibrant-blue-down": "#4b4ab4",
"ui-vibrant-blue-25": "#d9d9fe",
"ui-vibrant-blue-10": "#f1f0ff",
};
+7
View File
@@ -294,6 +294,13 @@ $max-width: 2560px;
.Select-input {
height: auto;
// When tabbing
&:focus-visible {
outline: 2px solid $ui-vibrant-blue-25;
outline-offset: 1px;
border-radius: 4px;
}
}
.Select-arrow-zone {
+19
View File
@@ -0,0 +1,19 @@
const pxToRem = (px: number): string => {
const baseSize = 16; // Assuming the base font size is 16px
return `${px / baseSize}rem`;
};
export const PADDING = {
"pad-auto": "auto",
"pad-xxsmall": pxToRem(2),
"pad-xsmall": pxToRem(4),
"pad-small": pxToRem(8),
"pad-icon": pxToRem(14),
"pad-medium": pxToRem(16),
"pad-large": pxToRem(24),
"pad-xlarge": pxToRem(32),
"pad-xxlarge": pxToRem(40),
"pad-xxxlarge": pxToRem(80),
};
export type Padding = keyof typeof PADDING;