Skip to content

Commit dda9504

Browse files
authored
fix(webapp): require explicit native button types (#4692)
## Summary Add explicit types to native dashboard buttons and enforce `react/button-has-type`. This prevents action buttons from accidentally submitting a surrounding form. Shared button primitives retain their caller-selected submit and reset semantics with documented lint exceptions. Base: [#4691](#4691)
1 parent a7a1e74 commit dda9504

13 files changed

Lines changed: 17 additions & 0 deletions

File tree

.oxlintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
{
116116
"files": ["apps/webapp/app/**/*.ts", "apps/webapp/app/**/*.tsx"],
117117
"rules": {
118+
"react/button-has-type": "error",
118119
"react/jsx-no-useless-fragment": "error",
119120
"react/no-unstable-nested-components": "error",
120121
"react/rules-of-hooks": "error",

apps/webapp/app/components/code/TSQLResultsTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,7 @@ function HeaderCellContent({
970970
)}
971971
{onFilterClick && (
972972
<button
973+
type="button"
973974
onClick={(e) => {
974975
e.stopPropagation();
975976
onFilterClick();

apps/webapp/app/components/navigation/SideMenuItem.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export function SideMenuItem({
240240
}
241241

242242
/** Button styled to match {@link SideMenuItem}, for entries that open a dialog rather than navigate. */
243+
/* oxlint-disable react/button-has-type -- Callers can select button, reset, or submit semantics. */
243244
export const SideMenuItemButton = forwardRef<
244245
HTMLButtonElement,
245246
{
@@ -276,3 +277,4 @@ export const SideMenuItemButton = forwardRef<
276277
</button>
277278
);
278279
});
280+
/* oxlint-enable react/button-has-type */

apps/webapp/app/components/primitives/AppliedFilter.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export function AppliedFilter({
6767
</div>
6868
{removable && (
6969
<button
70+
type="button"
7071
className={cn(
7172
"group flex size-6 items-center justify-center focus-custom",
7273
variantClassName.clear

apps/webapp/app/components/primitives/Buttons.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ type ButtonPropsType = Pick<
374374
> &
375375
React.ComponentProps<typeof ButtonContent>;
376376

377+
/* oxlint-disable react/button-has-type -- Callers can select button, reset, or submit semantics. */
377378
export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
378379
({ type, disabled, autoFocus, onClick, "aria-label": ariaLabel, ...props }, ref) => {
379380
const innerRef = useRef<HTMLButtonElement>(null);
@@ -435,6 +436,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonPropsType>(
435436
return buttonElement;
436437
}
437438
);
439+
/* oxlint-enable react/button-has-type */
438440

439441
type LinkPropsType = Pick<
440442
LinkProps,

apps/webapp/app/components/primitives/Popover.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ function PopoverSectionHeader({
5151
);
5252
}
5353

54+
/* oxlint-disable react/button-has-type -- The trigger supports form button semantics. */
5455
const PopoverMenuItem = React.forwardRef<
5556
HTMLButtonElement | HTMLAnchorElement,
5657
{
@@ -141,6 +142,7 @@ const PopoverMenuItem = React.forwardRef<
141142
}
142143
);
143144
PopoverMenuItem.displayName = "PopoverMenuItem";
145+
/* oxlint-enable react/button-has-type */
144146

145147
function PopoverCustomTrigger({
146148
isOpen,

apps/webapp/app/components/primitives/Select.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ export function SelectButtonItem({
582582
}: SelectButtonItemProps) {
583583
const render = (
584584
<button
585+
type="button"
585586
onClick={onClick}
586587
className={cn("block w-full text-left", selectItemClasses, props.className)}
587588
/>

apps/webapp/app/components/primitives/Table.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
433433
<div className={cn(flexClasses, "gap-2")}>
434434
{leadingContent}
435435
<button
436+
type="button"
436437
onClick={onClick}
437438
className={cn(
438439
"inline-flex cursor-pointer items-center gap-2 focus:outline-hidden",
@@ -446,6 +447,7 @@ export const TableCell = forwardRef<HTMLTableCellElement, TableCellProps>(
446447
</div>
447448
) : (
448449
<button
450+
type="button"
449451
onClick={onClick}
450452
className={cn("cursor-pointer focus:outline-hidden", flexClasses, actionClassName)}
451453
tabIndex={isTabbableCell ? 0 : -1}

apps/webapp/app/components/primitives/Toast.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export function ToastUI({
128128
{actionNode}
129129
</div>
130130
<button
131+
type="button"
131132
className={cn(
132133
"-mr-1 ms-auto rounded p-2 text-text-dimmed transition hover:text-text-bright",
133134
title && "-mt-1"

apps/webapp/app/components/runs/v3/ai/AIChatMessages.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ export function ToolUseRow({ tool }: { tool: ToolUse }) {
344344
>
345345
{availableTabs.map((tab) => (
346346
<button
347+
type="button"
347348
key={tab}
348349
onClick={() => handleTabClick(tab)}
349350
className={`px-2.5 py-1 text-[11px] capitalize transition-colors ${

0 commit comments

Comments
 (0)