Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/rules/fleet-frontend.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Render software title names via `getDisplayedSoftwareName(name, display_name)` f
- Use `classnames()` for conditional classes
- Style files use underscore prefix: `_styles.scss`
- Prefer `gap` over `margin` for spacing between sibling elements when the parent is `display: flex`/`grid`. Use the layout mixins from `frontend/styles/var/mixins.scss`: `vertical-card-layout`, `vertical-form-layout`, `vertical-modal-layout`, `vertical-page-layout`, `vertical-page-tab-panel-layout`, `vertical-data-set-layout`
- **Row hover-reveal actions** (a button/dropdown that only appears when the containing row is hovered): add `className="row-hover-button"` to the element. The fade + `:focus-visible` reveal is defined for `tr` in `frontend/components/TableContainer/_styles.scss` and for `.paginated-list__row` in `frontend/components/PaginatedList/_styles.scss` — don't hand-roll a local `opacity: 0` / `:hover { opacity: 1 }` per-consumer. Never fade only `.children-wrapper` — bordered/filled `Button` variants (e.g. `secondary`) leave an empty button frame behind.

## Forms
Cap free-text inputs' `maxLength` to the backend column length (check `server/datastore/mysql/schema.sql`, don't guess) via `inputOptions={{ maxLength: NAME_MAX_LENGTH }}` on `InputField`, using a local constant.
Expand Down
24 changes: 24 additions & 0 deletions frontend/components/PaginatedList/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@
min-width: 0;
/* This is crucial for proper shrinking */
}

// Fade the entire button (border + fill + children), not just its children,
// so bordered/filled variants like `secondary` don't leave an empty box behind.
.row-hover-button {
opacity: 0;
// Suppress pointer events while hidden so the invisible button can't
// intercept clicks meant for the row. Tab navigation still works —
// pointer-events only gates mouse/touch, not keyboard focus.
pointer-events: none;
transition: opacity 250ms;

// Reveal on the button's own keyboard focus, not just row hover —
// otherwise tabbing to a hidden row-hover button never shows it.
&:focus-visible {
opacity: 1;
pointer-events: auto;
}
}
Comment on lines +75 to +91

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, sounds good


&:not(.paginated-list__row--disabled):hover {
background: $ui-off-white;
cursor: pointer;
Expand All @@ -82,6 +101,11 @@
.policy-row__preview-button {
visibility: visible;
}

.row-hover-button {
opacity: 1;
pointer-events: auto;
}
}

&:first-child {
Expand Down
9 changes: 9 additions & 0 deletions frontend/components/TableContainer/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -258,19 +258,27 @@
tr {
.row-hover-button {
opacity: 0;
// Suppress pointer events while hidden so the invisible button can't
// intercept clicks meant for the row. Tab navigation still works —
// pointer-events only gates mouse/touch, not keyboard focus.
pointer-events: none;
transition: 250ms;
text-overflow: none;

// Reveal on the button's own keyboard focus, not just row hover/focus —
// otherwise tabbing directly to a hidden row-hover button never shows it.
&:focus-visible {
opacity: 1;
pointer-events: auto;
}

// React-select's dropdown opacity must be controlled at input level for keyboard nav
// So must be controlled at input level here as well
&.actions-dropdown {
opacity: 1;
// React-select handles its own keyboard nav; keep it hit-testable
// so focus/blur bookkeeping continues to work even before row hover.
pointer-events: auto;
.actions-dropdown-select__control {
opacity: 0;

Expand All @@ -285,6 +293,7 @@
&:focus-visible {
.row-hover-button {
opacity: 1;
pointer-events: auto;
}
.row-hover-button.actions-dropdown {
.actions-dropdown-select__control {
Expand Down
5 changes: 5 additions & 0 deletions frontend/components/buttons/Button/_styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ $base-class: "button";
opacity: 0;
}

// Wraps children so the loading spinner can swap in without a layout shift.
// Do NOT hide-then-reveal buttons by fading `.children-wrapper` alone —
// bordered/filled variants (`secondary` etc.) will leave an empty button
// box behind. Fade the whole button, or use `.row-hover-button`
// (see PaginatedList / TableContainer styles).
.children-wrapper {
display: flex;
flex-direction: row;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const RunScriptBatchPaginatedList = ({
<>
<a>{script.name}</a>
<Button
className="row-hover-button"
variant="secondary"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,6 @@
display: flex;
justify-content: space-between;
padding: $pad-small $pad-large;
.button > .children-wrapper {
opacity: 0;
transition: opacity 250ms;
}
&:hover {
.button > .children-wrapper {
opacity: 1;
}
}
}
.loading-spinner.centered {
margin: auto;
Expand Down
Loading