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
8 changes: 8 additions & 0 deletions .changeset/field-props.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@cube-dev/ui-kit': minor
---

Added unified support for `fieldProps`, `fieldStyles`, `labelProps`, and `labelStyles` across all field components. The `fieldStyles` and `labelStyles` props serve as shorthands for `fieldProps.styles` and `labelProps.styles` respectively, with shorthand props taking priority. All merging logic is centralized in the `wrapWithField` helper.

**Breaking changes:**
- Removed `wrapperStyles` prop from TextInputBase and Select components (use `styles` prop instead for the root element).
6 changes: 6 additions & 0 deletions .changeset/on-open-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@cube-dev/ui-kit': patch
---

Added `onOpenChange` callback prop to Picker, FilterPicker, ComboBox, and Select components. This callback is invoked when the popover/overlay open state changes, receiving a boolean parameter indicating the new open state.

2 changes: 0 additions & 2 deletions src/components/fields/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,7 @@ function Checkbox(
return wrapWithField(checkboxField, domRef, {
...props,
children: null,
labelStyles,
inputStyles,
styles,
});
}

Expand Down
1 change: 0 additions & 1 deletion src/components/fields/Checkbox/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ function CheckboxGroup(props: WithNullableValue<CubeCheckboxGroupProps>, ref) {
children: null,
fieldProps: groupProps,
labelProps: mergeProps(baseLabelProps, labelProps),
styles,
});
}

Expand Down
17 changes: 9 additions & 8 deletions src/components/fields/ComboBox/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,6 @@ export interface CubeComboBoxProps<T>
inputStyles?: Styles;
/** Custom styles for the trigger button */
triggerStyles?: Styles;
/** Custom styles for the field wrapper */
fieldStyles?: Styles;
/** Custom styles for the listbox */
listBoxStyles?: Styles;
/** Custom styles for the popover overlay */
Expand Down Expand Up @@ -247,6 +245,8 @@ export interface CubeComboBoxProps<T>
* @default true when items are provided, false when using JSX children
*/
sortSelectedToTop?: boolean;
/** Callback called when the popover open state changes */
onOpenChange?: (isOpen: boolean) => void;
}

const PROP_STYLES = [...BASE_STYLES, ...OUTER_STYLES, ...COLOR_STYLES];
Expand Down Expand Up @@ -1005,7 +1005,6 @@ export const ComboBox = forwardRef(function ComboBox<T extends object>(
triggerStyles,
listBoxStyles,
overlayStyles,
fieldStyles,
suffix,
hideTrigger,
message,
Expand Down Expand Up @@ -1038,6 +1037,7 @@ export const ComboBox = forwardRef(function ComboBox<T extends object>(
containerPadding = 8,
onSelectionChange: externalOnSelectionChange,
sortSelectedToTop: sortSelectedToTopProp,
onOpenChange,
onFocus,
onBlur,
onKeyDown,
Expand Down Expand Up @@ -1182,6 +1182,11 @@ export const ComboBox = forwardRef(function ComboBox<T extends object>(
}
}, [isPopoverOpen, effectiveSelectedKey]);

// Call onOpenChange when popover state changes
useEffect(() => {
onOpenChange?.(isPopoverOpen);
}, [isPopoverOpen]);

// Filtering hook
const { filterFn, isFilterActive, setIsFilterActive } = useComboBoxFiltering({
effectiveInputValue,
Expand Down Expand Up @@ -1852,15 +1857,11 @@ export const ComboBox = forwardRef(function ComboBox<T extends object>(
);

const { children: _, ...propsWithoutChildren } = props;
const finalProps = {
...propsWithoutChildren,
styles: fieldStyles,
};

return wrapWithField<Omit<CubeComboBoxProps<T>, 'children'>>(
comboBoxField,
ref,
finalProps,
propsWithoutChildren,
);
}) as unknown as (<T>(
props: CubeComboBoxProps<T> & { ref?: ForwardedRef<HTMLDivElement> },
Expand Down
1 change: 0 additions & 1 deletion src/components/fields/DatePicker/DateInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ function DateInput<T extends DateValue>(

return wrapWithField(component, domRef, {
...props,
styles,
labelProps: mergeProps(props.labelProps, labelProps),
});
}
Expand Down
1 change: 0 additions & 1 deletion src/components/fields/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ function DatePicker<T extends DateValue>(

return wrapWithField(component, domRef, {
...props,
styles,
labelProps: mergeProps(props.labelProps, labelProps),
});
}
Expand Down
1 change: 0 additions & 1 deletion src/components/fields/DatePicker/DateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ function DateRangePicker<T extends DateValue>(

return wrapWithField(component, domRef, {
...props,
styles,
labelProps: mergeProps(props.labelProps, labelProps),
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,6 @@ function DateRangeSeparatedPicker<T extends DateValue>(

return wrapWithField(component, domRef, {
...props,
styles,
labelProps: mergeProps(props.labelProps, labelProps),
});
}
Expand Down
1 change: 0 additions & 1 deletion src/components/fields/DatePicker/TimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ function TimeInput<T extends TimeValue>(

return wrapWithField(timeInput, domRef, {
...props,
styles,
labelProps: mergeProps(labelProps, userLabelProps),
});
}
Expand Down
1 change: 0 additions & 1 deletion src/components/fields/FileInput/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,6 @@ function FileInput(props: CubeFileInputProps, ref) {

return wrapWithField(fileInput, domRef, {
...props,
styles,
});
}

Expand Down
4 changes: 1 addition & 3 deletions src/components/fields/FilterListBox/FilterListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -953,12 +953,10 @@ export const FilterListBox = forwardRef(function FilterListBox<
</FilterListBoxWrapperElement>
);

const finalProps = { ...props, styles: undefined };

return wrapWithField<Omit<CubeFilterListBoxProps<T>, 'children'>>(
filterListBoxField,
ref,
finalProps,
props,
);
}) as unknown as (<T>(
props: CubeFilterListBoxProps<T> & { ref?: ForwardedRef<HTMLDivElement> },
Expand Down
13 changes: 6 additions & 7 deletions src/components/fields/FilterPicker/FilterPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export interface CubeFilterPickerProps<T>
* @default true when items are provided, false when using JSX children
*/
sortSelectedToTop?: boolean;
/** Callback called when the popover open state changes */
onOpenChange?: (isOpen: boolean) => void;
}

const PROP_STYLES = [...BASE_STYLES, ...OUTER_STYLES, ...COLOR_STYLES];
Expand Down Expand Up @@ -252,6 +254,7 @@ export const FilterPicker = forwardRef(function FilterPicker<T extends object>(
searchValue,
onSearchChange,
sortSelectedToTop: sortSelectedToTopProp,
onOpenChange,
isButton = false,
form,
...otherProps
Expand Down Expand Up @@ -665,8 +668,9 @@ export const FilterPicker = forwardRef(function FilterPicker<T extends object>(
selectionsWhenClosed.current = { ...latestSelectionRef.current };
cachedItemsOrder.current = null;
}
onOpenChange?.(state.isOpen);
}
}, [state.isOpen, isPopoverOpen]);
}, [state.isOpen, isPopoverOpen, onOpenChange]);

// Add keyboard support for arrow keys to open the popover
const { keyboardProps } = useKeyboard({
Expand Down Expand Up @@ -951,15 +955,10 @@ export const FilterPicker = forwardRef(function FilterPicker<T extends object>(
</FilterPickerWrapper>
);

const finalProps = {
...props,
styles: undefined,
};

return wrapWithField<Omit<CubeFilterPickerProps<T>, 'children' | 'tooltip'>>(
filterPickerField,
ref as any,
finalProps,
props,
);
}) as unknown as (<T>(
props: CubeFilterPickerProps<T> & { ref?: ForwardedRef<HTMLElement> },
Expand Down
4 changes: 1 addition & 3 deletions src/components/fields/ListBox/ListBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1029,12 +1029,10 @@ export const ListBox = forwardRef(function ListBox<T extends object>(
</ListBoxWrapperElement>
);

const finalProps = { ...props, styles: undefined };

return wrapWithField<Omit<CubeListBoxProps<T>, 'children'>>(
listBoxField,
ref,
finalProps,
props,
);
}) as unknown as (<T>(
props: CubeListBoxProps<T> & { ref?: ForwardedRef<HTMLDivElement> },
Expand Down
14 changes: 9 additions & 5 deletions src/components/fields/Picker/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import {
tasty,
} from '../../../tasty';
import { generateRandomId } from '../../../utils/random';
import { mergeProps } from '../../../utils/react';
import { useEventBus } from '../../../utils/react/useEventBus';
import { CubeItemButtonProps, ItemAction, ItemButton } from '../../actions';
import { CubeItemProps } from '../../content/Item';
Expand Down Expand Up @@ -115,6 +114,8 @@ export interface CubePickerProps<T>
* @default true when items are provided, false when using JSX children
*/
sortSelectedToTop?: boolean;
/** Callback called when the popover open state changes */
onOpenChange?: (isOpen: boolean) => void;
}

const PROP_STYLES = [...BASE_STYLES, ...OUTER_STYLES, ...COLOR_STYLES];
Expand Down Expand Up @@ -229,6 +230,7 @@ export const Picker = forwardRef(function Picker<T extends object>(
isClearable,
onClear,
sortSelectedToTop,
onOpenChange,
isButton = false,
listStateRef: externalListStateRef,
...otherProps
Expand Down Expand Up @@ -333,6 +335,11 @@ export const Picker = forwardRef(function Picker<T extends object>(
selectionMode,
]);

// Call onOpenChange when popover state changes
useEffect(() => {
onOpenChange?.(isPopoverOpen);
}, [isPopoverOpen]);

// Sort items with selected on top if enabled
const getSortedItems = useCallback((): typeof items => {
if (!items || !shouldSortSelectedToTop) return items;
Expand Down Expand Up @@ -764,10 +771,7 @@ export const Picker = forwardRef(function Picker<T extends object>(
return wrapWithField<Omit<CubePickerProps<T>, 'children' | 'tooltip'>>(
pickerField,
ref as any,
{
...props,
styles: undefined,
},
props,
);
}) as unknown as (<T>(
props: CubePickerProps<T> & { ref?: ForwardedRef<HTMLElement> },
Expand Down
38 changes: 38 additions & 0 deletions src/components/fields/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Field Components

Form field components with built-in validation, accessibility, and React Aria integration.

## Text Input Fields
- **TextInput** - Basic text input
- **PasswordInput** - Password input with visibility toggle
- **SearchInput** - Search input with clear button
- **TextArea** - Multi-line text input
- **NumberInput** - Numeric input with step controls
- **Input** - Low-level input component

## Selection Fields
- **Select** - Single selection dropdown
- **Picker** - Single/multi selection with search and customization
- **ComboBox** - Autocomplete text input with suggestions
- **ListBox** - Multi-select list with keyboard navigation
- **FilterListBox** - Multi-select with filtering and grouping
- **FilterPicker** - Dropdown multi-select with filtering

## Choice Fields
- **Checkbox** / **CheckboxGroup** - Boolean or multi-choice selection
- **RadioGroup** / **Radio** - Single choice from multiple options
- **Switch** - Boolean toggle

## Range & Date Fields
- **Slider** / **RangeSlider** - Single or range value selection
- **DatePicker** - Single date selection
- **DateRangePicker** - Date range selection
- **DateRangeSeparatedPicker** - Separate start/end date inputs
- **DateInput** / **TimeInput** - Manual date/time entry

## Other
- **FileInput** - File upload with drag & drop
- **TextInputMapper** - Dynamic input type mapping

All fields support form integration, validation rules, error states, and accessibility features.

1 change: 0 additions & 1 deletion src/components/fields/RadioGroup/RadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ function RadioGroup(props: WithNullableValue<CubeRadioGroupProps>, ref) {
children: null,
fieldProps,
labelProps: mergeProps(baseLabelProps, labelProps),
styles: props.fieldStyles,
});
}

Expand Down
16 changes: 9 additions & 7 deletions src/components/fields/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,6 @@ export interface CubeSelectBaseProps<T>
triggerStyles?: Styles;
listBoxStyles?: Styles;
overlayStyles?: Styles;
/**
* @deprecated Use `styles` instead
*/
wrapperStyles?: Styles;
direction?: 'top' | 'bottom';
shouldFlip?: boolean;
/** Minimum padding in pixels between the popover and viewport edges */
Expand All @@ -218,6 +214,8 @@ export interface CubeSelectBaseProps<T>
* @default false
*/
isButton?: boolean;
/** Callback called when the popover open state changes */
onOpenChange?: (isOpen: boolean) => void;
}

export interface CubeSelectProps<T> extends CubeSelectBaseProps<T> {
Expand Down Expand Up @@ -267,7 +265,6 @@ function Select<T extends object>(
inputStyles,
triggerStyles,
optionStyles,
wrapperStyles,
listBoxStyles,
overlayStyles,
suffix,
Expand All @@ -287,6 +284,7 @@ function Select<T extends object>(
labelSuffix,
suffixPosition = 'before',
isClearable,
onOpenChange,
isButton = false,
form,
...otherProps
Expand Down Expand Up @@ -318,6 +316,11 @@ function Select<T extends object>(
}
}, [state.isOpen, emit, selectId]);

// Call onOpenChange when open state changes
useEffect(() => {
onOpenChange?.(state.isOpen);
}, [state.isOpen]);

styles = extractStyles(otherProps, PROP_STYLES, styles);

ref = useCombinedRefs(ref);
Expand Down Expand Up @@ -416,7 +419,7 @@ function Select<T extends object>(
let selectField = (
<SelectWrapperElement
mods={modifiers}
styles={{ ...wrapperStyles, ...styles }}
styles={styles}
data-size={size}
data-type={type}
data-theme={theme}
Expand Down Expand Up @@ -499,7 +502,6 @@ function Select<T extends object>(
mergeProps(
{
...props,
styles: labelStyles,
},
{ labelProps },
),
Expand Down
4 changes: 3 additions & 1 deletion src/components/fields/Slider/Slider.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ export default {
minValue: 0,
maxValue: 20,
step: 2,
width: '200px',
fieldStyles: {
width: '200px',
},
},
} as Meta<CubeSliderProps>;

Expand Down
1 change: 0 additions & 1 deletion src/components/fields/Slider/SliderBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,6 @@ function SliderBase(allProps: SliderBaseProps, ref: DOMRef<HTMLDivElement>) {

return wrapWithField(sliderField, ref, {
...props,
// styles,
extra,
labelProps: mergeProps(labelProps, userLabelProps),
});
Expand Down
Loading
Loading