Clear button styles (#49292)

**Related issue:** Resolves #49276

**New features**
- Added new "Secondary" (bordered, off-white fill) and "Subdued"
(borderless, low-emphasis) button variants to match the Figma spec,
alongside the existing Primary style.
- Allowed rows to be selected in Controls > OS updates.

**Cleanup**
- Once nothing referenced the old styles anymore, fully removed the old
`text-icon`, `brand-inverse-icon`, `inverse-alert`, `inverse`, and
`icon` button variants (type, styles, and Storybook entries) from the
shared `Button` component.
- Removed the `iconStroke` prop, which had become a no-op once the old
variants it supported were gone.
- Renamed `ActionsDropdown`'s variants
(`button`/`brand-button`/`small-button`) to
`subdued`/`primary`/`secondary` to match the same naming used everywhere
else.
- Replaced a one-off dropdown implementation on the Software title page
with the shared `ActionsDropdown` component, instead of maintaining
duplicate styling logic.
- Changed the button name on Host details > Reports > Report details
from "View data for all hosts" to "View report for all hosts" (to match
the previous page's Actions drop-down options).


# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.


## Testing

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

<img width="1475" height="241" alt="Screenshot 2026-07-21 at 06 35 49"
src="https://github.com/user-attachments/assets/7cfbd444-7837-40e8-854e-bc5989d57d85"
/>
<img width="661" height="306" alt="Screenshot 2026-07-21 at 06 37 18"
src="https://github.com/user-attachments/assets/5d0c4873-8179-4089-b115-7e8cd3a53b4d"
/>
<img width="1427" height="423" alt="Screenshot 2026-07-21 at 06 37 30"
src="https://github.com/user-attachments/assets/a4850a60-f44a-4902-b45e-0094f23a52f8"
/>
<img width="1427" height="640" alt="Screenshot 2026-07-21 at 06 37 46"
src="https://github.com/user-attachments/assets/738a4a7f-cd7d-4162-b659-6f649c32204d"
/>
<img width="1445" height="479" alt="Screenshot 2026-07-22 at 07 03 22"
src="https://github.com/user-attachments/assets/4f672dc0-5c6d-4eb8-8465-ed5233fcd1b2"
/>
<img width="811" height="871" alt="Screenshot 2026-07-21 at 06 41 20"
src="https://github.com/user-attachments/assets/5421c96e-2dab-492a-af26-be0e5a7791ca"
/>
This commit is contained in:
Steven Palmesano
2026-07-23 07:11:59 -05:00
committed by GitHub
parent 4972732c75
commit 62ed3583e6
319 changed files with 1038 additions and 1169 deletions
@@ -99,64 +99,4 @@ describe("DropdownWrapper Component", () => {
expect(screen.getByText(/no results found/i)).toBeInTheDocument();
});
test("doesn't render selected value when variant is button", async () => {
const buttonText = "Click me";
render(
<DropdownWrapper
options={sampleOptions}
value="option1"
onChange={mockOnChange}
name="test-dropdown"
label="Test Dropdown"
placeholder={buttonText}
variant="button"
/>
);
// Check if the button text is rendered
expect(screen.getByText(buttonText)).toBeInTheDocument();
// Open the dropdown
await userEvent.click(screen.getByText(buttonText));
// Select Option 2
await userEvent.click(screen.getByText(/option 2/i));
// Check if the button text is still rendered and not replaced by the selected option
expect(screen.getByText(buttonText)).toBeInTheDocument();
expect(screen.queryByText(/option 2/i)).not.toBeInTheDocument();
});
// Regression test for #45853 — react-select tracks the last click as
// selectValue and re-focuses it on reopen, leaving the previously-clicked
// option visually highlighted. Button variant overrides this by forcing
// value to null.
test("button variant does not mark a previously-clicked option as selected on reopen", async () => {
const buttonText = "Actions";
render(
<DropdownWrapper
options={sampleOptions}
onChange={mockOnChange}
name="test-dropdown"
placeholder={buttonText}
variant="button"
/>
);
// Open, click Option 2, then reopen
await userEvent.click(screen.getByText(buttonText));
await userEvent.click(screen.getByText(/option 2/i));
await userEvent.click(screen.getByText(buttonText));
// On reopen, no menu option should carry the selected state.
// react-select adds `--is-selected` to the option matching `value`; the
// fix forces value to null for button variant so this class never appears.
const options = document.querySelectorAll(".react-select__option");
expect(options.length).toBeGreaterThan(0);
options.forEach((option) =>
expect(option.className).not.toMatch(/--is-selected/)
);
});
});
@@ -99,7 +99,7 @@ export interface CustomOptionType {
iconName?: IconNames;
}
type DropdownWrapperVariant = "table-filter" | "button";
type DropdownWrapperVariant = "table-filter";
export interface IDropdownWrapper {
options: CustomOptionType[];
@@ -118,8 +118,7 @@ export interface IDropdownWrapper {
placeholder?: string;
/** E.g. scroll to view dropdown menu in a scrollable parent container */
onMenuOpen?: () => void;
/** Table filter dropdowns have filter icon and height: 40px
* Button dropdowns have hover/active state, padding, height matching actual buttons, and no selected option styling */
/** Table filter dropdowns have filter icon and height: 40px */
variant?: DropdownWrapperVariant;
/** This makes the menu fit all text without wrapping,
* aligning right to fit text on screen */
@@ -141,19 +140,6 @@ const getOptionBackgroundColor = (
return "transparent";
};
const getOptionFontWeight = (
state: OptionProps<CustomOptionType, false>,
variant?: DropdownWrapperVariant
) => {
// For "button" dropdowns, selected options are not styled differently
if (variant === "button") {
return "normal";
}
// For other variants, selected options are bold
return state.isSelected ? "600" : "normal";
};
/** generates the default custom styles for the dropdown component.
* NOTE: we export this from DropdownWrapper components so that other more
* customisable dropdown components can use this for consistency in styling */
@@ -165,76 +151,20 @@ export const generateCustomDropdownStyles = (
): StylesConfig<CustomOptionType, false> => {
return {
container: (provided) => {
const buttonVariantContainer = {
borderRadius: "6px",
"&:active": {
backgroundColor: COLORS["ui-fleet-black-5"],
},
height: "38px",
};
return {
...provided,
width: "100%",
height: "36px",
...(variant === "button" && buttonVariantContainer),
};
},
control: (provided, state) => {
if (variant === "button") {
return {
backgroundColor: "initial",
borderColor: "none",
display: "flex",
flexDirection: "row",
width: "max-content",
padding: PADDING["pad-small"],
border: 0,
borderRadius: "6px",
boxShadow: "none",
cursor: "pointer",
".dropdown-wrapper__indicator path": {
stroke: COLORS["ui-fleet-black-75"],
},
"&:hover": {
backgroundColor: COLORS["ui-fleet-black-5"],
boxShadow: "none",
".dropdown-wrapper__placeholder": {
color: COLORS["ui-fleet-black-75-over"],
},
".dropdown-wrapper__indicator path": {
stroke: COLORS["ui-fleet-black-75-over"],
},
},
// Note: state.isFocused is intentionally not used as a highlight
// trigger here. react-select's internal input retains focus after
// the menu closes (outside click, post-modal-close, etc.), so
// styling on it would leave the trigger visually highlighted
// indefinitely (#45853). Hover + menuIsOpen cover the meaningful
// interactive states for an action button.
...(state.menuIsOpen && {
backgroundColor: COLORS["ui-fleet-black-5"],
".dropdown-wrapper__placeholder": {
color: COLORS["ui-fleet-black-75-down"],
},
".dropdown-wrapper__indicator path": {
stroke: COLORS["ui-fleet-black-75-down"],
},
".dropdown-wrapper__indicator svg": {
transform: "rotate(180deg)",
transition: "transform 0.25s ease",
},
}),
...(variant === "button" && { height: "22px" }),
};
}
return {
...provided,
display: "flex",
flexDirection: "row",
width: "100%",
minHeight: "36px", // react-select-5 defaults control minHeight to 38px
backgroundColor: COLORS["core-fleet-white"],
paddingLeft: "8px", // TODO: Update to match styleguide of (16px) when updating rest of UI (8px)
paddingRight: "8px",
@@ -308,23 +238,10 @@ export const generateCustomDropdownStyles = (
}),
};
},
placeholder: (provided, state) => {
const buttonVariantPlaceholder = {
color: state.isFocused
? COLORS["ui-fleet-black-75-over"]
: COLORS["ui-fleet-black-75"],
fontSize: "13px",
fontWeight: "600",
lineHeight: "normal",
paddingLeft: 0,
opacity: isDisabled ? 0.5 : 1,
marginTop: variant === "button" ? "-1px" : "1px", // TODO: Figure out vertical centering to not need pixel fix
};
placeholder: (provided) => {
return {
...provided,
fontSize: "13px",
...(variant === "button" && buttonVariantPlaceholder),
};
},
input: (provided) => {
@@ -383,7 +300,7 @@ export const generateCustomDropdownStyles = (
...provided,
padding: 0,
display: "flex",
gap: PADDING[variant === "button" ? "pad-xsmall" : "pad-small"],
gap: PADDING["pad-small"],
flexWrap: "nowrap", // This ensures the value is on a single line and truncated
}),
option: (provided, state) => ({
@@ -392,7 +309,7 @@ export const generateCustomDropdownStyles = (
fontSize: "13px",
borderRadius: "4px",
backgroundColor: getOptionBackgroundColor(state),
fontWeight: getOptionFontWeight(state, variant),
fontWeight: state.isSelected ? "600" : "normal",
color: COLORS["core-fleet-black"],
"&:hover": {
backgroundColor: state.isDisabled
@@ -462,7 +379,6 @@ const DropdownWrapper = ({
}: IDropdownWrapper) => {
const wrapperClassNames = classnames(baseClass, className, {
[`${baseClass}__table-filter`]: variant === "table-filter",
[`${baseClass}__button`]: variant === "button",
[`${wrapperClassname}`]: !!wrapperClassname,
});
@@ -481,13 +397,6 @@ const DropdownWrapper = ({
// Ability to handle value of type string or CustomOptionType
const getCurrentValue = () => {
// Button variant is a fire-and-forget action menu (selection isn't displayed
// via `controlShouldRenderValue`). Forcing null prevents react-select from
// tracking the last click as the selected value and re-focusing it on reopen
// (#45853).
if (variant === "button") {
return null;
}
if (typeof value === "string") {
return options.find((option) => option.value === value) || null;
}
@@ -565,7 +474,6 @@ const DropdownWrapper = ({
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
placeholder={placeholder}
onMenuOpen={onMenuOpen}
controlShouldRenderValue={variant !== "button"} // Control doesn't change placeholder to selected value
// Resolve accessible name: explicit prop wins, otherwise fall back
// to the placeholder (usually "Select X"), otherwise the required
// `name` (often a kebab-case identifier — least readable but
@@ -20,14 +20,4 @@
height: 36px;
}
}
// Button-variant: background while focused so keyboard tabbing is
// visible. Mirrors the menuIsOpen background applied via emotion in
// DropdownWrapper.tsx. Inputs always match :focus-visible, so this
// also applies on click — that's fine since it matches the open state.
&__button {
.react-select__control:has(input:focus-visible) {
background-color: $ui-fleet-black-5;
}
}
}