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
7 changes: 7 additions & 0 deletions packages/connect-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

# Changelog

## [2.3.0] - 2025-12-07

### Added

- Dark mode support for select components with new theme color tokens (`optionHover`, `optionSelected`, `optionSelectedHover`, `appIconBackground`)
- App icon backgrounds in `SelectApp` for better visibility in dark mode

## [2.2.0] - 2025-11-29

### Added
Expand Down
2 changes: 1 addition & 1 deletion packages/connect-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/connect-react",
"version": "2.2.0",
"version": "2.3.0",
"description": "Pipedream Connect library for React",
"files": [
"dist"
Expand Down
47 changes: 23 additions & 24 deletions packages/connect-react/src/components/ControlApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,27 @@ export function ControlApp({ app }: ControlAppProps) {
getProps, select, theme,
} = useCustomize();

const {
surface,
border,
text,
textStrong,
hoverBg,
selectedBg,
selectedHoverBg,
} = resolveSelectColors(theme.colors);
// Memoize color resolution to avoid recalculating on every render
const resolvedColors = useMemo(() => resolveSelectColors(theme.colors), [
theme.colors,
]);

// Memoize base select styles - only recalculate when colors or boxShadow change
const baseSelectStyles = useMemo(() => createBaseSelectStyles<SelectValue>({
colors: {
surface: resolvedColors.surface,
border: resolvedColors.border,
text: resolvedColors.text,
textStrong: resolvedColors.textStrong,
hoverBg: resolvedColors.hoverBg,
selectedBg: resolvedColors.selectedBg,
selectedHoverBg: resolvedColors.selectedHoverBg,
},
boxShadow: theme.boxShadow,
}), [
resolvedColors,
theme.boxShadow,
]);

const baseStyles: CSSProperties = {
color: theme.colors.neutral60,
Expand All @@ -79,27 +91,14 @@ export function ControlApp({ app }: ControlAppProps) {
gridArea: "control",
};

const selectStyles = createBaseSelectStyles<SelectValue>({
colors: {
surface,
border,
text,
textStrong,
hoverBg,
selectedBg,
selectedHoverBg,
},
boxShadow: theme.boxShadow,
});

const baseSelectProps: BaseReactSelectProps<SelectValue> = {
components: {
Option: BaseOption,
},
styles: {
...selectStyles,
...baseSelectStyles,
control: (base, state) => ({
...(selectStyles.control?.(base, state) ?? base),
...(baseSelectStyles.control?.(base, state) ?? base),
gridArea: "control",
}),
},
Expand Down
75 changes: 51 additions & 24 deletions packages/connect-react/src/components/ControlSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@ import {
useState,
useRef,
} from "react";
import type {
CSSObjectWithLabel, MenuListProps,
} from "react-select";
import type { MenuListProps } from "react-select";
import Select, {
components,
Props as ReactSelectProps,
} from "react-select";
import CreatableSelect from "react-select/creatable";
import type { BaseReactSelectProps } from "../hooks/customization-context";
import { useCustomize } from "../hooks/customization-context";
import { useFormFieldContext } from "../hooks/form-field-context";
import type {
Expand All @@ -28,6 +25,10 @@ import {
isArrayOfLabelValueWrapped,
isLabelValueWrapped,
} from "../utils/label-value";
import {
createBaseSelectStyles,
resolveSelectColors,
} from "../utils/select-styles";
import { LoadMoreButton } from "./LoadMoreButton";

// XXX T and ConfigurableProp should be related
Expand Down Expand Up @@ -55,6 +56,29 @@ export function ControlSelect<T extends PropOptionValue>({
const {
select, theme,
} = useCustomize();

// Memoize color resolution to avoid recalculating on every render
const resolvedColors = useMemo(() => resolveSelectColors(theme.colors), [
theme.colors,
]);

// Memoize base select styles - only recalculate when colors or boxShadow change
const baseSelectStyles = useMemo(() => createBaseSelectStyles<LabelValueOption<T>, boolean>({
colors: {
surface: resolvedColors.surface,
border: resolvedColors.border,
text: resolvedColors.text,
textStrong: resolvedColors.textStrong,
hoverBg: resolvedColors.hoverBg,
selectedBg: resolvedColors.selectedBg,
selectedHoverBg: resolvedColors.selectedHoverBg,
},
boxShadow: theme.boxShadow,
}), [
resolvedColors,
theme.boxShadow,
]);

const [
selectOptions,
setSelectOptions,
Expand All @@ -77,16 +101,6 @@ export function ControlSelect<T extends PropOptionValue>({
value,
])

const baseSelectProps: BaseReactSelectProps<LabelValueOption<T>, boolean> = {
styles: {
container: (base): CSSObjectWithLabel => ({
...base,
gridArea: "control",
boxShadow: theme.boxShadow.input,
}),
},
};

const selectValue: LabelValueOption<T> | LabelValueOption<T>[] | null = useMemo(() => {
if (rawValue == null) {
return null;
Expand Down Expand Up @@ -130,7 +144,15 @@ export function ControlSelect<T extends PropOptionValue>({
selectOptions,
]);

const props = select.getProps("controlSelect", baseSelectProps)
// Get customization props from context
// We pass our dark mode base styles as the base, so user customizations merge on top
const customizationProps = select.getProps<
"controlSelect",
LabelValueOption<T>,
boolean
>("controlSelect", {
styles: baseSelectStyles,
})

// Use ref to store latest onLoadMore callback
// This allows stable component reference while calling current callback
Expand All @@ -145,10 +167,10 @@ export function ControlSelect<T extends PropOptionValue>({
showLoadMoreButtonRef.current = showLoadMoreButton;

// Memoize custom components to prevent remounting
// Recompute when caller/customizer supplies new component overrides
// Merge: customization context components -> caller overrides -> our MenuList wrapper
const finalComponents = useMemo(() => {
const mergedComponents = {
...(props.components ?? {}),
...(customizationProps.components ?? {}),
...(componentsOverride ?? {}),
};
const ParentMenuList = mergedComponents.MenuList ?? components.MenuList;
Expand All @@ -174,8 +196,8 @@ export function ControlSelect<T extends PropOptionValue>({
MenuList: CustomMenuList,
};
}, [
props.components,
componentsOverride,
customizationProps.components,
]);

const handleCreate = (inputValue: string) => {
Expand Down Expand Up @@ -254,21 +276,26 @@ export function ControlSelect<T extends PropOptionValue>({
getOptionLabel={(option) => sanitizeOption(option).label}
getOptionValue={(option) => String(sanitizeOption(option).value)}
onChange={handleChange}
{...props}
// Apply customization context values as defaults
classNamePrefix={customizationProps.classNamePrefix || "react-select"}
classNames={customizationProps.classNames}
theme={customizationProps.theme}
Comment on lines +279 to +282
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using ?? instead of || for classNamePrefix.

Using || treats empty string as falsy, so customizationProps.classNamePrefix = "" (intentionally no prefix) would fall back to "react-select". Using ?? would only fallback for null/undefined, preserving explicit empty string customization.

-      classNamePrefix={customizationProps.classNamePrefix || "react-select"}
+      classNamePrefix={customizationProps.classNamePrefix ?? "react-select"}
🤖 Prompt for AI Agents
In packages/connect-react/src/components/ControlSelect.tsx around lines 279 to
282, the code uses the || operator for classNamePrefix which treats an explicit
empty string as falsy and will wrongly fall back to "react-select"; change this
to the nullish coalescing operator (??) so that only null or undefined trigger
the default while preserving an intentional empty string, i.e., replace the ||
usage with ?? for classNamePrefix.

// Spread selectProps after defaults so callers can override theme/classNames/classNamePrefix
{...selectProps}
{...additionalProps}
// These must come AFTER spreads to avoid being overridden
classNamePrefix="react-select"
menuPortalTarget={
typeof document !== "undefined"
? document.body
: null
}
menuPosition="fixed"
styles={{
// eslint-disable-next-line react/prop-types
...props.styles,
...selectProps?.styles,
...customizationProps.styles,
...(selectProps?.styles ?? {}),
container: (base) => ({
...base,
gridArea: "control",
}),
menuPortal: (base) => ({
...base,
zIndex: 99999,
Expand Down
50 changes: 25 additions & 25 deletions packages/connect-react/src/components/SelectApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,30 +98,30 @@ export function SelectApp({
loadMore,
]);

const {
surface,
border,
text,
textStrong,
hoverBg,
selectedBg,
selectedHoverBg,
appIconBg,
} = resolveSelectColors(theme.colors);
// Memoize color resolution to avoid recalculating on every render
const resolvedColors = useMemo(() => resolveSelectColors(theme.colors), [
theme.colors,
]);

// Memoize base select styles - only recalculate when colors or boxShadow change
const baseSelectStyles = useMemo(() => createBaseSelectStyles<App>({
colors: {
surface: resolvedColors.surface,
border: resolvedColors.border,
text: resolvedColors.text,
textStrong: resolvedColors.textStrong,
hoverBg: resolvedColors.hoverBg,
selectedBg: resolvedColors.selectedBg,
selectedHoverBg: resolvedColors.selectedHoverBg,
},
boxShadow: theme.boxShadow,
}), [
resolvedColors,
theme.boxShadow,
]);

const baseSelectProps: BaseReactSelectProps<App> = {
styles: createBaseSelectStyles<App>({
colors: {
surface,
border,
text,
textStrong,
hoverBg,
selectedBg,
selectedHoverBg,
},
boxShadow: theme.boxShadow,
}),
styles: baseSelectStyles,
};

const selectProps = select.getProps("selectApp", baseSelectProps);
Expand All @@ -139,7 +139,7 @@ export function SelectApp({
style={{
height: 24,
width: 24,
backgroundColor: appIconBg,
backgroundColor: resolvedColors.appIconBg,
borderRadius: 6,
padding: 2,
}}
Expand All @@ -163,7 +163,7 @@ export function SelectApp({
style={{
height: 24,
width: 24,
backgroundColor: appIconBg,
backgroundColor: resolvedColors.appIconBg,
borderRadius: 6,
padding: 2,
}}
Expand Down Expand Up @@ -197,7 +197,7 @@ export function SelectApp({
Option,
SingleValue,
MenuList,
appIconBg,
resolvedColors.appIconBg,
]);
return (
<Select
Expand Down
43 changes: 22 additions & 21 deletions packages/connect-react/src/components/SelectComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,27 @@ export function SelectComponent({
select, theme,
} = useCustomize();

const {
surface,
border,
text,
textStrong,
hoverBg,
selectedBg,
selectedHoverBg,
} = resolveSelectColors(theme.colors);
// Memoize color resolution to avoid recalculating on every render
const resolvedColors = useMemo(() => resolveSelectColors(theme.colors), [
theme.colors,
]);

// Memoize base select styles - only recalculate when colors or boxShadow change
const baseSelectStyles = useMemo(() => createBaseSelectStyles<Component>({
colors: {
surface: resolvedColors.surface,
border: resolvedColors.border,
text: resolvedColors.text,
textStrong: resolvedColors.textStrong,
hoverBg: resolvedColors.hoverBg,
selectedBg: resolvedColors.selectedBg,
selectedHoverBg: resolvedColors.selectedHoverBg,
},
boxShadow: theme.boxShadow,
}), [
resolvedColors,
theme.boxShadow,
]);

const isLoadingMoreRef = useRef(isLoadingMore);
isLoadingMoreRef.current = isLoadingMore;
Expand All @@ -83,18 +95,7 @@ export function SelectComponent({
]);

const baseSelectProps: BaseReactSelectProps<Component> = {
styles: createBaseSelectStyles<Component>({
colors: {
surface,
border,
text,
textStrong,
hoverBg,
selectedBg,
selectedHoverBg,
},
boxShadow: theme.boxShadow,
}),
styles: baseSelectStyles,
};

const selectProps = select.getProps("selectComponent", baseSelectProps);
Expand Down
Loading
Loading