Fleet UI: Checkbox and accessibility styling, tab through select targets (#22943)

This commit is contained in:
RachelElysia
2024-10-17 10:01:23 -04:00
committed by GitHub
parent 56d4e596c7
commit 97ef401bea
21 changed files with 302 additions and 236 deletions
@@ -155,7 +155,6 @@ const SelectTargets = ({
const { isPremiumTier, isOnGlobalTeam, currentUser } = useContext(AppContext);
const [labels, setLabels] = useState<ILabelsByType | null>(null);
const [inputTabIndex, setInputTabIndex] = useState<number | null>(null);
const [searchText, setSearchText] = useState("");
const [debouncedSearchText, setDebouncedSearchText] = useState("");
const [isDebouncing, setIsDebouncing] = useState(false);
@@ -263,12 +262,6 @@ const SelectTargets = ({
labelsSummary && setLabels(parseLabels(labelsSummary));
}, [labelsSummary]);
useEffect(() => {
if (inputTabIndex === null && labelsSummary && teams) {
setInputTabIndex(labelsSummary.length + teams.length || 0);
}
}, [inputTabIndex, labelsSummary, teams]);
useEffect(() => {
setIsDebouncing(true);
debounceSearch(searchText);
@@ -485,7 +478,6 @@ const SelectTargets = ({
autofocus
searchResultsTableConfig={resultsTableConfig}
selectedHostsTableConifg={selectedHostsTableConfig}
tabIndex={inputTabIndex || 0}
searchText={searchText}
searchResults={searchResults || []}
isTargetsLoading={isFetchingSearchResults || isDebouncing}
@@ -91,7 +91,6 @@ const TargetsInput = ({
type="search"
iconSvg="search"
value={searchText}
tabIndex={tabIndex}
iconPosition="start"
label={label}
placeholder={placeholder}
@@ -0,0 +1,50 @@
.target-pill-selector {
padding: $pad-small;
background-color: $core-white;
border: none;
box-shadow: inset 0 0 0 1px $ui-fleet-black-25;
border-radius: $border-radius-medium;
cursor: pointer;
display: flex;
align-items: center;
margin-bottom: $pad-small;
&:not(:last-of-type) {
margin-right: $pad-small;
}
img {
max-width: 12px;
}
.plus-icon {
padding-right: 3px;
}
.selector-name {
margin-left: 8px;
font-size: $x-small;
flex: 1;
}
.selector-count {
margin-left: 8px;
font-size: $xxx-small;
font-weight: $bold;
}
&[data-selected="true"] {
background-color: $ui-vibrant-blue-10;
box-shadow: inset 0 0 0 1px $core-vibrant-blue;
}
&:hover {
box-shadow: inset 0 0 0 1px $core-vibrant-blue-over;
}
&:active {
box-shadow: inset 0 0 0 1px $core-vibrant-blue-down;
}
// When tabbing
&:focus-visible {
outline: 2px solid $ui-vibrant-blue-25;
outline-offset: 1px;
border-radius: 4px;
}
}
@@ -37,7 +37,7 @@ $shadow-transition-width: 10px;
background-attachment: local, local, scroll, scroll;
// End shadow
}
// applied to same element as data-table__table while loading
&__no-rows {
min-height: 272px;
@@ -68,6 +68,12 @@ $shadow-transition-width: 10px;
padding-left: 0;
}
}
// Cleaner when tabbing
a:focus-visible {
outline-offset: 0;
border-radius: $border-radius-medium;
}
}
thead {
@@ -1,10 +1,11 @@
import React, { ReactNode } from "react";
import React, { ReactNode, KeyboardEvent, useEffect, useRef } from "react";
import classnames from "classnames";
import { noop, pick } from "lodash";
import FormField from "components/forms/FormField";
import { IFormFieldProps } from "components/forms/FormField/FormField";
import TooltipWrapper from "components/TooltipWrapper";
import Icon from "components/Icon";
const baseClass = "fleet-checkbox";
@@ -17,14 +18,17 @@ export interface ICheckboxProps {
disabled?: boolean;
name?: string;
onChange?: any; // TODO: meant to be an event; figure out type for this
onBlur?: any;
value?: boolean;
onBlur?: (event: React.FocusEvent<HTMLDivElement>) => void;
value?: boolean | null;
wrapperClassName?: string;
indeterminate?: boolean;
parseTarget?: boolean;
tooltipContent?: React.ReactNode;
isLeftLabel?: boolean;
helpText?: React.ReactNode;
/** Use in table action only
* Do not use on forms as enter key reserved for submit */
enableEnterToCheck?: boolean;
}
const Checkbox = (props: ICheckboxProps) => {
@@ -36,22 +40,52 @@ const Checkbox = (props: ICheckboxProps) => {
name,
onChange = noop,
onBlur = noop,
value,
value = false,
wrapperClassName,
indeterminate,
indeterminate = false,
parseTarget,
tooltipContent,
isLeftLabel,
helpText,
enableEnterToCheck = false,
} = props;
const handleChange = () => {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.indeterminate = indeterminate;
}
}, [indeterminate]);
const handleChange = (
event: React.MouseEvent | React.KeyboardEvent
): void => {
event.preventDefault();
if (readOnly || disabled) return;
// If indeterminate, set to true; otherwise, toggle the current value
const newValue = indeterminate || !value;
if (parseTarget) {
// Returns both name and value
return onChange({ name, value: !value });
onChange({ name, value: newValue });
} else {
onChange(newValue);
}
return onChange(!value);
// Update the hidden input
if (inputRef.current) {
inputRef.current.checked = newValue;
}
};
/** Manual implementation of spacebar toggling checkboxes (default behavior)
* since we're using a custom div instead of a native checkbox
* Enter key intended to toggle table checkboxes only */
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
if (event.key === " " || (enableEnterToCheck && event.key === "Enter")) {
handleChange(event);
}
};
const checkBoxClass = classnames(
@@ -60,12 +94,6 @@ const Checkbox = (props: ICheckboxProps) => {
baseClass
);
const checkBoxTickClass = classnames(`${baseClass}__tick`, {
[`${baseClass}__tick--read-only`]: readOnly || disabled,
[`${baseClass}__tick--disabled`]: disabled,
[`${baseClass}__tick--indeterminate`]: indeterminate,
});
const checkBoxLabelClass = classnames(checkBoxClass, {
[`${baseClass}__label--read-only`]: readOnly || disabled,
[`${baseClass}__label--disabled`]: disabled,
@@ -77,21 +105,40 @@ const Checkbox = (props: ICheckboxProps) => {
type: "checkbox",
} as IFormFieldProps;
const getIconName = () => {
if (indeterminate) return "checkbox-indeterminate";
if (value) return "checkbox";
return "checkbox-unchecked";
};
return (
<FormField {...formFieldProps}>
<>
<label htmlFor={name} className={checkBoxLabelClass}>
<input
checked={value}
className={`${baseClass}__input`}
disabled={readOnly || disabled}
id={name}
name={name}
onChange={handleChange}
onBlur={onBlur}
type="checkbox"
<label htmlFor={name}>
<input
type="checkbox"
ref={inputRef}
name={name}
checked={value || undefined}
onChange={noop} // Empty onChange to avoid React warning
disabled={disabled || readOnly}
style={{ display: "none" }} // Hide the input
id={name}
/>
<div
role="checkbox"
aria-checked={indeterminate ? "mixed" : value || undefined}
aria-readonly={readOnly}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
className={checkBoxLabelClass}
onClick={handleChange}
onKeyDown={handleKeyDown}
onBlur={onBlur}
>
<Icon
name={getIconName()}
className={`${baseClass}__icon ${baseClass}__icon--${getIconName()}`}
/>
<span className={checkBoxTickClass} />
{tooltipContent ? (
<span className={`${baseClass}__label-tooltip tooltip`}>
<TooltipWrapper
@@ -102,10 +149,10 @@ const Checkbox = (props: ICheckboxProps) => {
</TooltipWrapper>
</span>
) : (
<span className={`${baseClass}__label`}>{children} </span>
<span className={`${baseClass}__label`}>{children}</span>
)}
</label>
</>
</div>
</label>
</FormField>
);
};
@@ -4,115 +4,50 @@
display: flex;
align-items: center;
&__input {
opacity: 0;
width: 16px;
height: 16px;
margin: 2px;
&:focus + .fleet-checkbox__tick {
&::after {
border-color: $core-vibrant-blue;
&:hover:not(.fleet-checkbox__label--disabled) {
svg {
.checkbox-state {
stroke: $core-vibrant-blue-over;
fill: $core-vibrant-blue-over;
}
}
&:checked + .fleet-checkbox__tick {
&::after {
background-color: $core-vibrant-blue;
border: solid 2px $core-vibrant-blue;
}
&:hover {
&::after {
background-color: $core-vibrant-blue-over;
border: solid 2px $core-vibrant-blue-over;
}
}
&--read-only {
&::after {
@include disabled-checkbox;
}
&:hover {
&::after {
@include disabled-checkbox;
}
}
}
&::before {
@include position(absolute, 50% null null 50%);
transform: rotate(45deg);
box-sizing: border-box;
display: block;
width: 7px;
height: 13px;
margin: -8px 0 0 -3px;
border: 2px solid $core-white;
border-top: 0;
border-left: 0;
content: "";
z-index: 9;
.checkbox-unchecked-state {
stroke: $core-vibrant-blue-over;
}
}
}
&__tick {
@include size(20px);
position: absolute;
display: inline-block;
cursor: pointer;
&::after {
@include size(20px);
transition: border 75ms ease-in-out, background 75ms ease-in-out;
border-radius: $border-radius;
border: solid 2px $ui-fleet-black-25;
content: "";
box-sizing: border-box;
display: block;
background-color: $core-white;
visibility: visible;
}
&:hover {
&::after {
border: solid 2px $core-vibrant-blue-over;
// During click only
&:active:not(.fleet-checkbox__label--disabled) {
svg {
.checkbox-state {
stroke: $core-vibrant-blue-down;
fill: $core-vibrant-blue-down;
}
.checkbox-unchecked-state {
stroke: $core-vibrant-blue-down;
}
}
}
&--disabled {
&::after {
background-color: $ui-fleet-black-25;
}
cursor: default;
// When tabbing
&:focus-visible:not(.fleet-checkbox__label--disabled) {
outline: none;
svg {
outline: 2px solid $ui-vibrant-blue-25;
outline-offset: 1px;
border-radius: 4px;
}
}
&--indeterminate {
&::after {
background-color: $core-vibrant-blue;
border: solid 1px $core-vibrant-blue;
&--disabled {
svg {
.checkbox-state {
stroke: $ui-fleet-black-25;
fill: $ui-fleet-black-25;
}
&:hover {
&::after {
&::after {
background-color: $core-vibrant-blue-over;
border: solid 1px $core-vibrant-blue-over;
}
}
}
&::before {
@include position(absolute, 50% null null 50%);
box-sizing: border-box;
display: block;
width: 10px;
margin: -1px 0 0 -5px;
border: 2px solid $core-white;
border-top: 0;
border-left: 0;
content: "";
.checkbox-unchecked-state {
stroke: $ui-fleet-black-25;
}
}
}
@@ -125,6 +60,16 @@
&--disabled {
color: $ui-fleet-black-50;
svg {
.checkbox-state {
stroke: $ui-fleet-black-25;
fill: $ui-fleet-black-25;
}
.checkbox-unchecked-state {
stroke: $ui-fleet-black-25;
}
}
}
}
@@ -144,13 +89,8 @@
flex-direction: row-reverse; // Switches the text to the left side of checkbox as all checkboxes are now display flex
.fleet-checkbox {
&__input {
float: right;
}
&__tick {
left: initial;
right: -8px;
&__icon {
padding-left: $pad-small;
}
&__label {
+43
View File
@@ -0,0 +1,43 @@
import React from "react";
import { COLORS, Colors } from "styles/var/colors";
interface ICheckboxProps {
color?: Colors;
}
const Checkbox = ({ color = "core-fleet-blue" }: ICheckboxProps) => {
return (
<svg width="16" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
className="checkbox-state"
x="1"
y="1.5"
width="14"
height="14"
rx="3"
fill={COLORS[color]}
stroke={COLORS[color]}
strokeWidth="2"
/>
<g clipPath="url(#checkbox)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.933 8.647c-.016 0-.033 0-.066.016a.828.828 0 0 1-.184-.066c.05-.033.134-.017.25.05Zm8.448-4.482c-.434-.233-.917.216-1.2.483-.65.633-1.2 1.366-1.816 2.032-.683.734-1.316 1.467-2.016 2.183-.4.4-.833.833-1.1 1.334-.6-.584-1.116-1.217-1.782-1.733-.483-.367-1.283-.634-1.267.25.034 1.149 1.05 2.382 1.8 3.165.316.334.733.683 1.216.7.583.033 1.183-.667 1.533-1.05.616-.666 1.117-1.416 1.683-2.099.733-.9 1.483-1.783 2.199-2.7.45-.566 1.866-1.965.75-2.565Z"
fill="#fff"
/>
</g>
<defs>
<clipPath id="checkbox">
<path
fill="#fff"
transform="translate(3.2 4.1)"
d="M0 0h9.6v8.8H0z"
/>
</clipPath>
</defs>
</svg>
);
};
export default Checkbox;
@@ -0,0 +1,29 @@
import React from "react";
import { COLORS, Colors } from "styles/var/colors";
interface ICheckboxIndeterminateProps {
color?: Colors;
}
const CheckboxIndeterminate = ({
color = "core-fleet-blue",
}: ICheckboxIndeterminateProps) => {
return (
<svg width="16" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
className="checkbox-state"
x="1"
y="1.5"
width="14"
height="14"
rx="3"
fill={COLORS[color]}
stroke={COLORS[color]}
strokeWidth="2"
/>
<rect x="3" y="7.5" width="10" height="2" rx="1" fill="#fff" />
</svg>
);
};
export default CheckboxIndeterminate;
@@ -0,0 +1,28 @@
import React from "react";
import { COLORS, Colors } from "styles/var/colors";
interface ICheckboxUncheckedProps {
color?: Colors;
}
const CheckboxUnchecked = ({
color = "ui-fleet-black-25",
}: ICheckboxUncheckedProps) => {
return (
<svg width="16" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
className="checkbox-unchecked-state"
x="1"
y="1.5"
width="14"
height="14"
rx="3"
fill="#fff"
stroke={COLORS[color]}
strokeWidth="2"
/>
</svg>
);
};
export default CheckboxUnchecked;
+6
View File
@@ -3,6 +3,9 @@ import ArrowInternalLink from "./ArrowInternalLink";
import Calendar from "./Calendar";
import CalendarCheck from "./CalendarCheck";
import Check from "./Check";
import Checkbox from "./Checkbox";
import CheckboxIndeterminate from "./CheckboxIndeterminate";
import CheckboxUnchecked from "./CheckboxUnchecked";
import ChevronLeft from "./ChevronLeft";
import ChevronRight from "./ChevronRight";
import ChevronUp from "./ChevronUp";
@@ -73,6 +76,9 @@ export const ICON_MAP = {
"chevron-up": ChevronUp,
"chevron-down": ChevronDown,
check: Check,
checkbox: Checkbox,
"checkbox-indeterminate": CheckboxIndeterminate,
"checkbox-unchecked": CheckboxUnchecked,
columns: Columns,
disable: Disable,
close: Close,
@@ -92,7 +92,7 @@ const generateTableHeaders = (
indeterminate: props.indeterminate,
onChange: () => cellProps.toggleAllRowsSelected(),
};
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
Cell: (cellProps: ICellProps): JSX.Element => {
const props = cellProps.row.getToggleRowSelectedProps();
@@ -100,7 +100,7 @@ const generateTableHeaders = (
value: props.checked,
onChange: () => cellProps.row.toggleRowSelected(),
};
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
disableHidden: true,
},
@@ -76,6 +76,11 @@
@media (max-width: 825px) {
padding: 15px 18px;
}
&:focus-visible {
outline-offset: -3px; // Cleaner when tabbing
border-radius: $border-radius-medium;
}
}
&__logo-wrapper {
@@ -93,10 +93,6 @@
width: calc(90% - 50px);
margin-bottom: $pad-medium;
}
&__disabled-usage-statistics-checkbox {
@include disabled;
}
}
}
}
@@ -72,11 +72,7 @@ const Statistics = ({
name="enableUsageStatistics"
value={isPremiumTier ? true : enableUsageStatistics} // Set to true for all premium customers
parseTarget
wrapperClassName={
isPremiumTier
? `${baseClass}__disabled-usage-statistics-checkbox`
: ""
}
disabled={isPremiumTier}
>
Enable usage statistics
</Checkbox>
@@ -90,7 +90,7 @@ const allHostTableHeaders: IHostTableColumnConfig[] = [
indeterminate: props.indeterminate,
onChange: () => cellProps.toggleAllRowsSelected(),
};
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
Cell: (cellProps: ISelectionCellProps) => {
const props = cellProps.row.getToggleRowSelectedProps();
@@ -98,7 +98,7 @@ const allHostTableHeaders: IHostTableColumnConfig[] = [
value: props.checked,
onChange: () => cellProps.row.toggleRowSelected(),
};
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
disableHidden: true,
},
@@ -76,7 +76,7 @@ const generateTableHeaders = (): IDataColumn[] => {
indeterminate: props.indeterminate,
onChange: () => cellProps.toggleAllRowsSelected(),
};
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
Cell: (cellProps: ICellProps): JSX.Element => {
const props = cellProps.row.getToggleRowSelectedProps();
@@ -84,7 +84,7 @@ const generateTableHeaders = (): IDataColumn[] => {
value: props.checked,
onChange: () => cellProps.row.toggleRowSelected(),
};
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
disableHidden: true,
},
@@ -299,7 +299,7 @@ const generateTableHeaders = (
const checkboxProps = viewingTeamPolicies
? teamCheckboxProps
: regularCheckboxProps;
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
Cell: (cellProps: ICellProps): JSX.Element => {
const inheritedPolicy = cellProps.row.original.team_id === null;
@@ -314,7 +314,7 @@ const generateTableHeaders = (
return <></>;
}
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
disableHidden: true,
});
@@ -57,42 +57,6 @@
display: flex;
align-items: center;
flex-wrap: wrap;
.target-pill-selector {
padding: $pad-small;
background-color: $core-white;
border: none;
box-shadow: inset 0 0 0 1px $ui-fleet-black-25;
border-radius: $border-radius-medium;
cursor: pointer;
display: flex;
align-items: center;
margin-bottom: $pad-small;
&:not(:last-of-type) {
margin-right: $pad-small;
}
img {
max-width: 12px;
}
.plus-icon {
padding-right: 3px;
}
.selector-name {
margin-left: 8px;
font-size: $x-small;
flex: 1;
}
.selector-count {
margin-left: 8px;
font-size: $xxx-small;
font-weight: $bold;
}
&[data-selected="true"] {
background-color: $ui-vibrant-blue-10;
box-shadow: inset 0 0 0 1px $core-vibrant-blue;
}
}
}
}
&__targets-button-wrap {
@@ -274,7 +274,7 @@ const generateTableHeaders = ({
(row.original.team_id ?? undefined) === currentTeamId,
});
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
Cell: (cellProps: ICellProps): JSX.Element => {
const isInheritedQuery =
@@ -290,7 +290,7 @@ const generateTableHeaders = ({
onChange: () => row.toggleRowSelected(),
};
// v4.35.0 Any team admin or maintainer now can add, edit, delete their team's queries
return <Checkbox {...checkboxProps} />;
return <Checkbox {...checkboxProps} enableEnterToCheck />;
},
disableHidden: true,
});
@@ -25,42 +25,6 @@
display: flex;
align-items: center;
flex-wrap: wrap;
.target-pill-selector {
padding: $pad-small;
background-color: $core-white;
border: none;
box-shadow: inset 0 0 0 1px $ui-fleet-black-25;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
margin-bottom: $pad-small;
&:not(:last-of-type) {
margin-right: $pad-small;
}
img {
max-width: 12px;
}
.plus-icon {
padding-right: 3px;
}
.selector-name {
margin-left: 8px;
font-size: $x-small;
flex: 1;
}
.selector-count {
margin-left: 8px;
font-size: $xxx-small;
font-weight: $bold;
}
&[data-selected="true"] {
background-color: $ui-vibrant-blue-10;
box-shadow: inset 0 0 0 1px $core-vibrant-blue;
}
}
}
}
&__targets-button-wrap {
+1
View File
@@ -147,6 +147,7 @@ $max-width: 2560px;
outline-offset: 3px;
outline-style: solid;
outline-width: 2px;
border-radius: 2px;
}
}