-
Notifications
You must be signed in to change notification settings - Fork 0
Enhance Select component value type #135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,9 +5,9 @@ import { useOverlayTriggerState } from 'react-stately'; | |
| import { PopoverContent, PopoverTrigger } from './popover'; | ||
| import { cn } from './utils'; | ||
|
|
||
| export interface SelectOption { | ||
| export interface SelectOption<T = string> { | ||
| label: string; | ||
| value: string; | ||
| value: T; | ||
| } | ||
|
|
||
| export interface SelectUIComponents { | ||
|
|
@@ -22,10 +22,11 @@ export interface SelectUIComponents { | |
| ChevronIcon?: React.ComponentType<React.SVGProps<SVGSVGElement>>; | ||
| } | ||
|
|
||
| export interface SelectProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> { | ||
| options: SelectOption[]; | ||
| value?: string; | ||
| onValueChange?: (value: string) => void; | ||
| export interface SelectProps<T = string> | ||
| extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> { | ||
| options: SelectOption<T>[]; | ||
| value?: T; | ||
| onValueChange?: (value: T) => void; | ||
|
Comment on lines
+25
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainMirror the React.Key constraint on SelectProps; optionally firm up controlled vs. uncontrolled API
Apply this diff: -export interface SelectProps<T = string>
+export interface SelectProps<T extends React.Key = string>
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value' | 'onChange'> {
options: SelectOption<T>[];
- value?: T;
- onValueChange?: (value: T) => void;
+ value?: T;
+ onValueChange?: (value: T) => void;Optional controlled/uncontrolled typing (outside this hunk) you can adopt later: type Controlled<T extends React.Key = string> = {
value: T;
onValueChange: (value: T) => void;
};
type Uncontrolled<T extends React.Key = string> = {
defaultValue?: T;
onValueChange?: (value: T) => void;
};
export type SelectProps<T extends React.Key = string> =
BaseButtonProps & { options: SelectOption<T>[] } & (Controlled<T> | Uncontrolled<T>);To ensure downstream wrappers propagate the generic, especially remix-hook-form/select.tsx, run: I can open a follow-up PR to generify the remix wrapper if needed. 🏁 Script executed: #!/bin/bash
# Verify remix-hook-form wrapper uses the generic UISelectProps<T> and re-exports generic SelectProps<T>
fd -a 'select\.tsx$' packages/components/src/remix-hook-form | while read -r f; do
echo "---- $f"
rg -n -C2 'UISelectProps<|export interface SelectProps(.*)<|export function Select(.*)<' "$f"
doneLength of output: 1387 Propagate The remix-hook-form wrappers for • packages/components/src/remix-hook-form/select.tsx
-export interface SelectProps extends Omit<UISelectProps, 'value' | 'onValueChange'> {
+export interface SelectProps<T extends React.Key = string>
+ extends Omit<UISelectProps<T>, 'value' | 'onValueChange'> {
name: string;
label?: string;
}
-export function Select(props: SelectProps) {
+export function Select<T extends React.Key = string>(props: SelectProps<T>) {
return <UISelect {...props} />;
}• packages/components/src/remix-hook-form/canada-province-select.tsx
Example diff for one wrapper: -import { Select as UISelect, type SelectProps as UISelectProps } from '../ui/select';
-
-export interface CanadaProvinceSelectProps
- extends Omit<UISelectProps, 'value' | 'onValueChange'> {}
-
-export function CanadaProvinceSelect(props: CanadaProvinceSelectProps) {
+import { Select as UISelect, type SelectProps as UISelectProps } from '../ui/select';
+
+export interface CanadaProvinceSelectProps<T extends React.Key = string>
+ extends Omit<UISelectProps<T>, 'value' | 'onValueChange'> {}
+
+export function CanadaProvinceSelect<T extends React.Key = string>(
+ props: CanadaProvinceSelectProps<T>
+) {
return <UISelect options={canadaProvinceOptions} {...props} />;
}Essential: Apply the same generic 🤖 Prompt for AI Agents |
||
| placeholder?: string; | ||
| disabled?: boolean; | ||
| className?: string; | ||
|
|
@@ -34,7 +35,7 @@ export interface SelectProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonE | |
| components?: Partial<SelectUIComponents>; | ||
| } | ||
|
|
||
| export function Select({ | ||
| export function Select<T = string>({ | ||
| options, | ||
| value, | ||
| onValueChange, | ||
|
|
@@ -45,7 +46,7 @@ export function Select({ | |
| itemClassName, | ||
| components, | ||
| ...buttonProps | ||
| }: SelectProps) { | ||
| }: SelectProps<T>) { | ||
| const popoverState = useOverlayTriggerState({}); | ||
| const listboxId = React.useId(); | ||
| const [query, setQuery] = React.useState(''); | ||
|
|
@@ -174,7 +175,7 @@ export function Select({ | |
| const isSelected = option.value === value; | ||
| const isEnterCandidate = query.trim() !== '' && enterCandidate?.value === option.value && !isSelected; | ||
| return ( | ||
| <li key={option.value} className="list-none"> | ||
| <li key={String(option.value)} className="list-none"> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use the actual value as the React key (no String cast) once T is React.Key Casting to String masks duplicate keys for non-primitives. With T constrained to React.Key, prefer the raw value for stable, unique keys. Apply this diff: - <li key={String(option.value)} className="list-none">
+ <li key={option.value} className="list-none">
🤖 Prompt for AI Agents |
||
| <Item | ||
| ref={isSelected ? selectedItemRef : undefined} | ||
| onClick={() => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Constrain T to React.Key to avoid key collisions and invalid data- attribute values*
Allowing any T (including objects) introduces concrete problems:
Constrain T to React.Key (string | number) to keep the API flexible while safe.
Apply this diff:
If you truly need object values, consider adding a getKey: (value: T) => React.Key and equals?: (a: T, b: T) => boolean; but constraining to React.Key is the simplest, safest path for now.
📝 Committable suggestion
🤖 Prompt for AI Agents