diff --git a/packages/connect-react/CHANGELOG.md b/packages/connect-react/CHANGELOG.md index 375c746079490..37de7d347fa80 100644 --- a/packages/connect-react/CHANGELOG.md +++ b/packages/connect-react/CHANGELOG.md @@ -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 diff --git a/packages/connect-react/package.json b/packages/connect-react/package.json index d7039ff9eeaaa..0c6c3de6408f3 100644 --- a/packages/connect-react/package.json +++ b/packages/connect-react/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/connect-react", - "version": "2.2.0", + "version": "2.3.0", "description": "Pipedream Connect library for React", "files": [ "dist" diff --git a/packages/connect-react/src/components/ControlApp.tsx b/packages/connect-react/src/components/ControlApp.tsx index 484a098bbab8d..a392370d86510 100644 --- a/packages/connect-react/src/components/ControlApp.tsx +++ b/packages/connect-react/src/components/ControlApp.tsx @@ -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({ + 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, @@ -79,27 +91,14 @@ export function ControlApp({ app }: ControlAppProps) { gridArea: "control", }; - const selectStyles = createBaseSelectStyles({ - colors: { - surface, - border, - text, - textStrong, - hoverBg, - selectedBg, - selectedHoverBg, - }, - boxShadow: theme.boxShadow, - }); - const baseSelectProps: BaseReactSelectProps = { components: { Option: BaseOption, }, styles: { - ...selectStyles, + ...baseSelectStyles, control: (base, state) => ({ - ...(selectStyles.control?.(base, state) ?? base), + ...(baseSelectStyles.control?.(base, state) ?? base), gridArea: "control", }), }, diff --git a/packages/connect-react/src/components/ControlSelect.tsx b/packages/connect-react/src/components/ControlSelect.tsx index 9c037aca2a33d..dfb6723aee2c5 100644 --- a/packages/connect-react/src/components/ControlSelect.tsx +++ b/packages/connect-react/src/components/ControlSelect.tsx @@ -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 { @@ -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 @@ -55,6 +56,29 @@ export function ControlSelect({ 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, 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, @@ -77,16 +101,6 @@ export function ControlSelect({ value, ]) - const baseSelectProps: BaseReactSelectProps, boolean> = { - styles: { - container: (base): CSSObjectWithLabel => ({ - ...base, - gridArea: "control", - boxShadow: theme.boxShadow.input, - }), - }, - }; - const selectValue: LabelValueOption | LabelValueOption[] | null = useMemo(() => { if (rawValue == null) { return null; @@ -130,7 +144,15 @@ export function ControlSelect({ 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, + boolean + >("controlSelect", { + styles: baseSelectStyles, + }) // Use ref to store latest onLoadMore callback // This allows stable component reference while calling current callback @@ -145,10 +167,10 @@ export function ControlSelect({ 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; @@ -174,8 +196,8 @@ export function ControlSelect({ MenuList: CustomMenuList, }; }, [ - props.components, componentsOverride, + customizationProps.components, ]); const handleCreate = (inputValue: string) => { @@ -254,11 +276,13 @@ export function ControlSelect({ 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} + // 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 @@ -266,9 +290,12 @@ export function ControlSelect({ } 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, diff --git a/packages/connect-react/src/components/SelectApp.tsx b/packages/connect-react/src/components/SelectApp.tsx index 1f546a495e981..b1f79f462ab35 100644 --- a/packages/connect-react/src/components/SelectApp.tsx +++ b/packages/connect-react/src/components/SelectApp.tsx @@ -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({ + 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 = { - styles: createBaseSelectStyles({ - colors: { - surface, - border, - text, - textStrong, - hoverBg, - selectedBg, - selectedHoverBg, - }, - boxShadow: theme.boxShadow, - }), + styles: baseSelectStyles, }; const selectProps = select.getProps("selectApp", baseSelectProps); @@ -139,7 +139,7 @@ export function SelectApp({ style={{ height: 24, width: 24, - backgroundColor: appIconBg, + backgroundColor: resolvedColors.appIconBg, borderRadius: 6, padding: 2, }} @@ -163,7 +163,7 @@ export function SelectApp({ style={{ height: 24, width: 24, - backgroundColor: appIconBg, + backgroundColor: resolvedColors.appIconBg, borderRadius: 6, padding: 2, }} @@ -197,7 +197,7 @@ export function SelectApp({ Option, SingleValue, MenuList, - appIconBg, + resolvedColors.appIconBg, ]); return (