diff --git a/frontend/components/LiveQuery/SelectTargets.tsx b/frontend/components/LiveQuery/SelectTargets.tsx index 513d931bda..566a8e35da 100644 --- a/frontend/components/LiveQuery/SelectTargets.tsx +++ b/frontend/components/LiveQuery/SelectTargets.tsx @@ -155,7 +155,6 @@ const SelectTargets = ({ const { isPremiumTier, isOnGlobalTeam, currentUser } = useContext(AppContext); const [labels, setLabels] = useState(null); - const [inputTabIndex, setInputTabIndex] = useState(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} diff --git a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx b/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx index e54830081e..65b21efcbe 100644 --- a/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx +++ b/frontend/components/LiveQuery/TargetsInput/TargetsInput.tsx @@ -91,7 +91,6 @@ const TargetsInput = ({ type="search" iconSvg="search" value={searchText} - tabIndex={tabIndex} iconPosition="start" label={label} placeholder={placeholder} diff --git a/frontend/components/LiveQuery/_styles.scss b/frontend/components/LiveQuery/_styles.scss new file mode 100644 index 0000000000..30d8922f4c --- /dev/null +++ b/frontend/components/LiveQuery/_styles.scss @@ -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; + } +} diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index ecb3ccce2e..77cba3ae92 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -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 { diff --git a/frontend/components/forms/fields/Checkbox/Checkbox.tsx b/frontend/components/forms/fields/Checkbox/Checkbox.tsx index b7455880b8..a34f403421 100644 --- a/frontend/components/forms/fields/Checkbox/Checkbox.tsx +++ b/frontend/components/forms/fields/Checkbox/Checkbox.tsx @@ -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) => 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(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): 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 ( - <> - ); }; diff --git a/frontend/components/forms/fields/Checkbox/_styles.scss b/frontend/components/forms/fields/Checkbox/_styles.scss index 1a79585c65..8e8d023422 100644 --- a/frontend/components/forms/fields/Checkbox/_styles.scss +++ b/frontend/components/forms/fields/Checkbox/_styles.scss @@ -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 { diff --git a/frontend/components/icons/Checkbox.tsx b/frontend/components/icons/Checkbox.tsx new file mode 100644 index 0000000000..47a3fb2c34 --- /dev/null +++ b/frontend/components/icons/Checkbox.tsx @@ -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 ( + + + + + + + + + + + + ); +}; + +export default Checkbox; diff --git a/frontend/components/icons/CheckboxIndeterminate.tsx b/frontend/components/icons/CheckboxIndeterminate.tsx new file mode 100644 index 0000000000..5b1de8c3bf --- /dev/null +++ b/frontend/components/icons/CheckboxIndeterminate.tsx @@ -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 ( + + + + + ); +}; + +export default CheckboxIndeterminate; diff --git a/frontend/components/icons/CheckboxUnchecked.tsx b/frontend/components/icons/CheckboxUnchecked.tsx new file mode 100644 index 0000000000..600072bd90 --- /dev/null +++ b/frontend/components/icons/CheckboxUnchecked.tsx @@ -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 ( + + + + ); +}; + +export default CheckboxUnchecked; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index d9cbb63a8f..f61fa59e30 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -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, diff --git a/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx b/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx index e161d057f3..b93aea08df 100644 --- a/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx +++ b/frontend/components/queries/PackQueriesTable/PackQueriesTable/PackQueriesTableConfig.tsx @@ -92,7 +92,7 @@ const generateTableHeaders = ( indeterminate: props.indeterminate, onChange: () => cellProps.toggleAllRowsSelected(), }; - return ; + return ; }, Cell: (cellProps: ICellProps): JSX.Element => { const props = cellProps.row.getToggleRowSelectedProps(); @@ -100,7 +100,7 @@ const generateTableHeaders = ( value: props.checked, onChange: () => cellProps.row.toggleRowSelected(), }; - return ; + return ; }, disableHidden: true, }, diff --git a/frontend/components/top_nav/SiteTopNav/_styles.scss b/frontend/components/top_nav/SiteTopNav/_styles.scss index 8afc255365..ba57a327eb 100644 --- a/frontend/components/top_nav/SiteTopNav/_styles.scss +++ b/frontend/components/top_nav/SiteTopNav/_styles.scss @@ -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 { diff --git a/frontend/pages/admin/OrgSettingsPage/_styles.scss b/frontend/pages/admin/OrgSettingsPage/_styles.scss index b4a07f55cb..9919e5865f 100644 --- a/frontend/pages/admin/OrgSettingsPage/_styles.scss +++ b/frontend/pages/admin/OrgSettingsPage/_styles.scss @@ -93,10 +93,6 @@ width: calc(90% - 50px); margin-bottom: $pad-medium; } - - &__disabled-usage-statistics-checkbox { - @include disabled; - } } } } diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Statistics/Statistics.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Statistics/Statistics.tsx index dd01e01e4e..47a4127693 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Statistics/Statistics.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Statistics/Statistics.tsx @@ -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 diff --git a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx index 7666f46876..336cfb51bb 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx @@ -90,7 +90,7 @@ const allHostTableHeaders: IHostTableColumnConfig[] = [ indeterminate: props.indeterminate, onChange: () => cellProps.toggleAllRowsSelected(), }; - return ; + return ; }, Cell: (cellProps: ISelectionCellProps) => { const props = cellProps.row.getToggleRowSelectedProps(); @@ -98,7 +98,7 @@ const allHostTableHeaders: IHostTableColumnConfig[] = [ value: props.checked, onChange: () => cellProps.row.toggleRowSelected(), }; - return ; + return ; }, disableHidden: true, }, diff --git a/frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTableConfig.tsx b/frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTableConfig.tsx index e573b8dd4c..645b5fb3b2 100644 --- a/frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTableConfig.tsx +++ b/frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTableConfig.tsx @@ -76,7 +76,7 @@ const generateTableHeaders = (): IDataColumn[] => { indeterminate: props.indeterminate, onChange: () => cellProps.toggleAllRowsSelected(), }; - return ; + return ; }, 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 ; + return ; }, disableHidden: true, }, diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx index 39dea4b872..76e785d741 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx @@ -299,7 +299,7 @@ const generateTableHeaders = ( const checkboxProps = viewingTeamPolicies ? teamCheckboxProps : regularCheckboxProps; - return ; + return ; }, Cell: (cellProps: ICellProps): JSX.Element => { const inheritedPolicy = cellProps.row.original.team_id === null; @@ -314,7 +314,7 @@ const generateTableHeaders = ( return <>; } - return ; + return ; }, disableHidden: true, }); diff --git a/frontend/pages/policies/PolicyPage/_styles.scss b/frontend/pages/policies/PolicyPage/_styles.scss index 78becff3e6..3fc8c5f81b 100644 --- a/frontend/pages/policies/PolicyPage/_styles.scss +++ b/frontend/pages/policies/PolicyPage/_styles.scss @@ -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 { diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index b38b394735..bb9689dd3f 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -274,7 +274,7 @@ const generateTableHeaders = ({ (row.original.team_id ?? undefined) === currentTeamId, }); - return ; + return ; }, 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 ; + return ; }, disableHidden: true, }); diff --git a/frontend/pages/queries/live/LiveQueryPage/_styles.scss b/frontend/pages/queries/live/LiveQueryPage/_styles.scss index c7da417ba6..820c54c3a0 100644 --- a/frontend/pages/queries/live/LiveQueryPage/_styles.scss +++ b/frontend/pages/queries/live/LiveQueryPage/_styles.scss @@ -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 { diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss index 31bb7259bd..5653206640 100644 --- a/frontend/styles/var/mixins.scss +++ b/frontend/styles/var/mixins.scss @@ -147,6 +147,7 @@ $max-width: 2560px; outline-offset: 3px; outline-style: solid; outline-width: 2px; + border-radius: 2px; } }