Files
fleet/frontend/components/forms/fields/Checkbox/Checkbox.tsx
T
Magnus Jensen f85187bcd3 Added danger variant to checkbox (#49806)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves
https://fleetdm.slack.com/archives/C02A8BRABB5/p1784765147322239?thread_ts=1784727509.435919&cid=C02A8BRABB5



https://github.com/user-attachments/assets/e6bdeeb9-9ec3-4ba6-9669-6ea18ec8362d




# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [ ] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a danger variant for checkboxes, with red styling for default,
hover, and active states.
* Updated release and wipe confirmation checkboxes to clearly indicate
destructive actions.
  * Added Storybook controls for checkbox values and variants.

* **Bug Fixes**
  * Standardized checkbox values to use boolean states only.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-23 19:18:57 +02:00

214 lines
5.9 KiB
TypeScript

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 { IInputFieldParseTarget } from "interfaces/form_field";
import TooltipWrapper from "components/TooltipWrapper";
import Icon from "components/Icon";
const baseClass = "fleet-checkbox";
export type CheckboxVariant = "default" | "danger";
interface ICheckboxPropsBase {
children?: ReactNode;
className?: string;
/** readOnly displays a non-editable field */
readOnly?: boolean;
/** disabled displays a greyed out non-editable field */
disabled?: boolean;
name?: string;
onBlur?: (event: React.FocusEvent<HTMLDivElement>) => void;
value?: boolean;
wrapperClassName?: string;
indeterminate?: boolean;
/** to display over the checkbox label */
labelTooltipContent?: React.ReactNode;
/** to allow hovering over the tooltip e.g. links within it, default: false to not block form */
labelTooltipClickable?: boolean;
/** to display over the checkbox icon */
iconTooltipContent?: React.ReactNode;
isLeftLabel?: boolean;
helpText?: React.ReactNode;
variant?: CheckboxVariant;
/** Use in table action only
* Do not use on forms as enter key reserved for submit */
enableEnterToCheck?: boolean;
}
/** When parseTarget is true, onChange receives { name, value }.
* Uses IInputFieldParseTarget for compatibility with shared input change handlers. */
interface ICheckboxParseTargetProps extends ICheckboxPropsBase {
parseTarget: true;
onChange?: (target: IInputFieldParseTarget<boolean>) => void;
}
/** When parseTarget is false/unset, onChange receives a boolean */
interface ICheckboxDirectProps extends ICheckboxPropsBase {
parseTarget?: false;
onChange?: (value: boolean) => void;
}
export type ICheckboxProps = ICheckboxParseTargetProps | ICheckboxDirectProps;
const Checkbox = (props: ICheckboxProps) => {
const {
children,
className,
readOnly = false,
disabled = false,
name,
onChange = noop,
onBlur = noop,
value = false,
wrapperClassName,
indeterminate = false,
parseTarget,
labelTooltipContent,
labelTooltipClickable = false,
iconTooltipContent,
isLeftLabel,
enableEnterToCheck = false,
variant = "default",
} = props;
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) {
onChange({ name, value: newValue });
} else {
onChange(newValue);
}
// 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(
{ inverse: isLeftLabel },
className,
baseClass,
{ [`${baseClass}--${variant}`]: variant !== "default" }
);
const checkBoxLabelClass = classnames(checkBoxClass, {
[`${baseClass}__label--read-only`]: readOnly || disabled,
[`${baseClass}__label--disabled`]: disabled,
});
const formFieldProps = {
...pick(props, ["helpText", "label", "error", "name"]),
className: wrapperClassName,
type: "checkbox",
} as IFormFieldProps;
const getIconName = () => {
if (indeterminate) return "checkbox-indeterminate";
if (value) return "checkbox";
return "checkbox-unchecked";
};
const renderIcon = () => {
const icon = (
<Icon
name={getIconName()}
className={`${baseClass}__icon ${baseClass}__icon--${getIconName()}`}
/>
);
if (iconTooltipContent) {
return (
<TooltipWrapper
tipContent={iconTooltipContent}
clickable={labelTooltipClickable}
underline={false}
showArrow
position="right"
tipOffset={8}
>
{icon}
</TooltipWrapper>
);
}
return icon;
};
const renderLabel = () => {
if (!children) return null;
return labelTooltipContent ? (
<span className={`${baseClass}__label-tooltip tooltip`}>
<TooltipWrapper
tipContent={labelTooltipContent}
clickable={labelTooltipClickable} // Allow interaction with link in tooltip
>
{children}
</TooltipWrapper>
</span>
) : (
<span className={`${baseClass}__label`}>{children}</span>
);
};
return (
<FormField {...formFieldProps}>
<label htmlFor={name}>
<input
type="checkbox"
ref={inputRef}
name={name}
checked={!!value}
onChange={noop} // Empty onChange to avoid React warning
disabled={disabled || readOnly}
style={{ display: "none" }} // Hide the input
id={name}
/>
<div
role="checkbox"
aria-label={name}
aria-checked={indeterminate ? "mixed" : value ?? undefined}
aria-readonly={readOnly}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
className={checkBoxLabelClass}
onClick={handleChange}
onKeyDown={handleKeyDown}
onBlur={onBlur}
>
{renderIcon()}
{renderLabel()}
</div>
</label>
</FormField>
);
};
export default Checkbox;