Skip to content

Commit 9dadd6a

Browse files
OpenStaxClaudeclaudeCopilot
authored andcommitted
Refactor: Extract filterTransientProps utility function
Created a reusable utility function to filter out styled-components transient props (starting with '$') before spreading to DOM elements. Changes: - Created filterTransientProps utility in utils/filterTransientProps.ts - Replaced all inline filter logic with utility function calls in: - Topbar/styled.tsx (6 components) - Toolbar/styled.tsx (PlainButton) - components/Button.tsx (PlainButton) Benefits: - Eliminates code duplication across components - Centralizes transient prop filtering logic - Improves maintainability and consistency Addresses review comment from @RoyEJohnson on PR #2846 🤖 Generated with [Claude Code](https://claude.com/claude-code) Update styled.tsx Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Co-Authored-By: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 78f0916 commit 9dadd6a

4 files changed

Lines changed: 36 additions & 61 deletions

File tree

src/app/components/Button.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import classNames from 'classnames';
33
import { isDefined } from '../guards';
44
import theme from '../theme';
55
import { linkColor, linkHover } from './Typography/Links.constants';
6+
import { filterTransientProps } from '../content/components/utils/filterTransientProps';
67
// Note: Button.css is imported globally from src/app/index.tsx to ensure consistent
78
// CSS ordering across code-split chunks and to avoid PrettyFormatPluginError in jest snapshots
89

@@ -176,14 +177,7 @@ export function ButtonGroup({
176177

177178
export const PlainButton = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
178179
function PlainButton({ className, ...props }, ref) {
179-
// Filter out transient props (starting with $) to prevent them from being forwarded to the DOM
180-
// Styled-components uses transient props for style-only props that shouldn't appear as HTML attributes
181-
const safeProps = Object.keys(props).reduce((acc, key) => {
182-
if (!key.startsWith('$')) {
183-
acc[key] = props[key as keyof typeof props];
184-
}
185-
return acc;
186-
}, {} as Record<string, unknown>) as React.ButtonHTMLAttributes<HTMLButtonElement>;
180+
const safeProps = filterTransientProps(props) as React.ButtonHTMLAttributes<HTMLButtonElement>;
187181
return (
188182
<button
189183
{...safeProps}

src/app/content/components/Toolbar/styled.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import styled from 'styled-components/macro';
33
import classNames from 'classnames';
44
import theme from '../../../theme';
55
import { toolbarIconColor } from '../constants';
6+
import { filterTransientProps } from '../utils/filterTransientProps';
67

78
interface IconProps extends React.SVGAttributes<SVGSVGElement> {
89
className?: string;
@@ -118,14 +119,7 @@ interface PlainButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
118119

119120
export const PlainButton = React.forwardRef<HTMLButtonElement, PlainButtonProps>(
120121
({ className, style, children, ...props }, ref) => {
121-
// Filter out transient props (starting with $) to prevent them from being forwarded to the DOM
122-
// Styled-components uses transient props for style-only props that shouldn't appear as HTML attributes
123-
const safeProps = Object.keys(props).reduce((acc, key) => {
124-
if (!key.startsWith('$')) {
125-
acc[key] = (props as Record<string, unknown>)[key];
126-
}
127-
return acc;
128-
}, {} as Record<string, unknown>) as React.ButtonHTMLAttributes<HTMLButtonElement>;
122+
const safeProps = filterTransientProps(props) as React.ButtonHTMLAttributes<HTMLButtonElement>;
129123

130124
return (
131125
<button

src/app/content/components/Topbar/styled.tsx

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { FilterDropdown } from '../popUp/Filters';
2727
import { toolbarIconStyles } from '../Toolbar/iconStyles';
2828
import { buttonMinWidth, PlainButton } from '../Toolbar/Toolbar.legacy';
2929
import { disablePrintClass } from '../utils/disablePrint';
30+
import { filterTransientProps } from '../utils/filterTransientProps';
3031
import { isVerticalNavOpenConnector } from '../utils/sidebar';
3132

3233
interface IconProps extends React.SVGAttributes<SVGSVGElement> {
@@ -142,14 +143,7 @@ interface MenuButtonProps {
142143

143144
export function MenuButton(props: MenuButtonProps) {
144145
const intl = useIntl();
145-
146-
// Filter transient props before spreading to DOM
147-
const safeProps = Object.keys(props).reduce<Record<string, unknown>>((acc, key) => {
148-
if (!key.startsWith('$')) {
149-
acc[key] = (props as Record<string, unknown>)[key];
150-
}
151-
return acc;
152-
}, {});
146+
const safeProps = filterTransientProps(props as Record<string, unknown>);
153147

154148
return (
155149
<PlainButton
@@ -177,14 +171,7 @@ export function SearchButton(
177171
{ desktop, mobile, ariaLabelId, colorSchema, className, style, ...props }: SearchButtonProps
178172
) {
179173
const intl = useIntl();
180-
181-
// Filter transient props before spreading to DOM
182-
const safeProps = Object.keys(props).reduce<Record<string, unknown>>((acc, key) => {
183-
if (!key.startsWith('$')) {
184-
acc[key] = (props as Record<string, unknown>)[key];
185-
}
186-
return acc;
187-
}, {});
174+
const safeProps = filterTransientProps(props);
188175

189176
// Get search icon color from theme
190177
const iconColor = colorSchema ? theme.color.primary[colorSchema].foreground : toolbarIconColor.base;
@@ -229,13 +216,7 @@ interface CloseButtonProps {
229216
}
230217

231218
export function CloseButton({ desktop, formSubmitted, className, style, ...props }: CloseButtonProps) {
232-
// Filter transient props before spreading to DOM
233-
const safeProps = Object.keys(props).reduce<Record<string, unknown>>((acc, key) => {
234-
if (!key.startsWith('$')) {
235-
acc[key] = (props as Record<string, unknown>)[key];
236-
}
237-
return acc;
238-
}, {});
219+
const safeProps = filterTransientProps(props);
239220

240221
return (
241222
<PlainButton
@@ -270,13 +251,7 @@ interface CloseButtonNewProps {
270251
}
271252

272253
export function CloseButtonNew({ children, className, style, ...props }: CloseButtonNewProps) {
273-
// Filter transient props before spreading to DOM
274-
const safeProps = Object.keys(props).reduce<Record<string, unknown>>((acc, key) => {
275-
if (!key.startsWith('$')) {
276-
acc[key] = (props as Record<string, unknown>)[key];
277-
}
278-
return acc;
279-
}, {});
254+
const safeProps = filterTransientProps(props);
280255

281256
return (
282257
<button
@@ -361,13 +336,7 @@ export function SearchInput({ desktop, mobile, autoFocus, className, style, ...p
361336
}
362337
}, [autoFocus]);
363338

364-
// Filter transient props before spreading to DOM
365-
const safeProps = Object.keys(props).reduce<Record<string, unknown>>((acc, key) => {
366-
if (!key.startsWith('$')) {
367-
acc[key] = (props as Record<string, unknown>)[key];
368-
}
369-
return acc;
370-
}, {});
339+
const safeProps = filterTransientProps(props);
371340

372341
return (
373342
<input
@@ -621,14 +590,7 @@ export function TextResizerChangeButton(
621590
{ ariaLabelId, children, className, style, ...props }: TextResizerChangeButtonProps
622591
) {
623592
const intl = useIntl();
624-
625-
// Filter transient props before spreading to DOM
626-
const safeProps = Object.keys(props).reduce<Record<string, unknown>>((acc, key) => {
627-
if (!key.startsWith('$')) {
628-
acc[key] = (props as Record<string, unknown>)[key];
629-
}
630-
return acc;
631-
}, {});
593+
const safeProps = filterTransientProps(props);
632594

633595
return (
634596
<PlainButton
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Filters out transient props (props starting with '$') that are used by styled-components
3+
* but should not be passed to DOM elements.
4+
*
5+
* @param props - The props object to filter
6+
* @returns A new object with transient props removed
7+
*
8+
* @example
9+
* ```tsx
10+
* function MyComponent(props: MyProps) {
11+
* const safeProps = filterTransientProps(props);
12+
* return <div {...safeProps}>Content</div>;
13+
* }
14+
* ```
15+
*/
16+
export function filterTransientProps<T extends Record<string, unknown>>(
17+
props: T
18+
): Record<string, unknown> {
19+
return Object.keys(props).reduce<Record<string, unknown>>((acc, key) => {
20+
if (!key.startsWith('$')) {
21+
acc[key] = props[key];
22+
}
23+
return acc;
24+
}, {});
25+
}

0 commit comments

Comments
 (0)