Skip to content

Conversation

@rivka-ungar
Copy link
Contributor

@rivka-ungar rivka-ungar commented Sep 29, 2025

User description

https://monday.monday.com/boards/3532714909/views/80492480/pulses/10052448934


PR Type

Bug fix


Description

  • Fix TypeScript types in Dropdown component callbacks

  • Remove unexported BaseListItemData from Storybook stories

  • Replace BaseListItemData with Item generic type


Diagram Walkthrough

flowchart LR
  A["BaseListItemData<Item>"] -- "replaced with" --> B["Item"]
  C["Storybook imports"] -- "updated to use" --> D["DropdownOption"]
Loading

File Walkthrough

Relevant files
Bug fix
Dropdown.types.ts
Fix callback parameter types                                                         

packages/core/src/components/DropdownNew/Dropdown.types.ts

  • Replace BaseListItemData with Item in callback type definitions
  • Update onOptionRemove, valueRenderer, optionRenderer, and
    onOptionSelect types
  • Simplify generic type usage across multi-select and single-select
    interfaces
+4/-4     
Documentation
Dropdown.stories.tsx
Update Storybook type imports                                                       

packages/core/src/components/DropdownNew/stories/Dropdown.stories.tsx

  • Remove import of unexported BaseListItemData type
  • Add import of DropdownOption type
  • Replace BaseListItemData>[] with
    DropdownOption>[]
+3/-4     

@qodo-merge-for-open-source
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 2 🔵🔵⚪⚪⚪
🧪 No relevant tests
🔒 No security concerns identified
 Summary findings: ⚠️ Title violates Conventional Commits
⚡ Recommended focus areas for review

Type Compatibility

Verify that all callsites expecting BaseListItemData<Item> in callbacks (onOptionRemove, valueRenderer, optionRenderer, onOptionSelect) still work when receiving Item directly. Ensure Item still includes required fields (value, label, etc.) wherever the renderer/handlers rely on them.

  onOptionRemove?: (option: Item) => void;
  /**
   * The function to call to render the selected value on single select mode.
   */
  valueRenderer?: never;
  /**
   * The default selected values for multi-select.
   */
  defaultValue?: Item[];
  /**
   * The controlled selected values for multi-select.
   */
  value?: Item[];
  /**
   * Callback fired when the selected values change in multi-select mode.
   */
  onChange?: (options: Item[]) => void;
}

interface SingleSelectSpecifics<Item extends BaseListItemData<Record<string, unknown>>> {
  /**
   * If true, the dropdown allows multiple selections. Defaults to false.
   */
  multi?: false;
  /**
   * If true, the dropdown allows multiple lines of selected items. (Not available for single select)
   */
  multiline?: never;
  /**
   * Callback fired when an option is removed in multi-select mode. (Not available for single select)
   */
  onOptionRemove?: never;
  /**
   * The function to call to render the selected value on single select mode.
   */
  valueRenderer?: (option: Item) => React.ReactNode;
  /**
   * The default selected value for single-select.
   */
  defaultValue?: Item;
  /**
   * The controlled selected value for single-select.
   */
  value?: Item;
  /**
   * Callback fired when the selected value changes in single-select mode.
   */
  onChange?: (option: Item) => void;
}

export type BaseDropdownProps<Item extends BaseListItemData<Record<string, unknown>>> = VibeComponentProps & {
  /**
   * The list of options available in the list.
   */
  options: DropdownGroupOption<Item>;
  /**
   * Props to be passed to the Tooltip component that wraps the dropdown.
   */
  tooltipProps?: Partial<TooltipProps>;
  /**
   * If true, displays dividers between grouped options.
   */
  withGroupDivider?: boolean;
  /**
   * If true, makes the group title sticky.
   */
  stickyGroupTitle?: boolean;
  /**
   * The size of the dropdown.
   */
  size?: DropdownSizes;
  /**
   * The direction of the dropdown.
   */
  dir?: DropdownDirection;
  /**
   * If true, the dropdown is searchable.
   */
  searchable?: boolean;
  /**
   * The function to call to render an option.
   */
  optionRenderer?: (option: Item) => React.ReactNode;
  /**
   * The function to call to render the menu.
   */
  menuRenderer?: (props: {
    children: React.ReactNode;
    filteredOptions: ListGroup<Item>[];
    selectedItems: Item[];
    getItemProps: (options: any) => Record<string, unknown>;
  }) => React.ReactNode;
  /**
   * The message to display when there are no options.
   */
  noOptionsMessage?: string | React.ReactNode;
  /**
   * The placeholder to display when the dropdown is empty.
   */
  placeholder?: string;
  /**
   * If true, the dropdown is disabled.
   */
  disabled?: boolean;
  /**
   * If true, the dropdown is read only.
   */
  readOnly?: boolean;
  /**
   * If true, the dropdown is in an error state.
   */
  error?: boolean;
  /**
   * The helper text to display below the dropdown.
   */
  helperText?: string;
  /**
   * If true, the dropdown is required.
   */
  required?: boolean;
  /**
   * The label to display above the dropdown.
   */
  label?: string;
  /**
   * The ARIA label for the dropdown.
   */
  ariaLabel?: string;
  /**
   * The ARIA label for the dropdown input.
   */
  inputAriaLabel?: string;
  /**
   * The ARIA label for the menu container.
   */
  menuAriaLabel?: string;
  /**
   * The ARIA label for the clear button.
   */
  clearAriaLabel?: string;
  /**
   * The current value of the input field.
   */
  inputValue?: string;
  /**
   * The maximum height of the dropdown menu.
   */
  maxMenuHeight?: number;
  /**
   * If true, controls the menu open state.
   */
  isMenuOpen?: boolean;
  /**
   * If true, closes the menu when an option is selected.
   */
  closeMenuOnSelect?: boolean;
  /**
   * If true, the dropdown menu will be auto focused.
   */
  autoFocus?: boolean;
  /**
   * If true, the dropdown will have a clear button.
   */
  clearable?: boolean;
  /**
   * Callback fired when the dropdown loses focus.
   */
  onBlur?: (event: React.FocusEvent<HTMLDivElement>) => void;
  /**
   * Callback fired when the clear button is clicked.
   */
  onClear?: () => void;
  /**
   * Callback fired when the dropdown gains focus.
   */
  onFocus?: (event: React.FocusEvent<HTMLDivElement>) => void;
  /**
   * Callback fired when the dropdown input value changes.
   */
  onInputChange?: (input: string | null) => void;
  /**
   * Callback fired when a key is pressed inside the dropdown.
   */
  onKeyDown?: (event: React.KeyboardEvent<HTMLDivElement>) => void;
  /**
   * Callback fired when the dropdown menu opens.
   */
  onMenuOpen?: () => void;
  /**
   * Callback fired when the dropdown menu closes.
   */
  onMenuClose?: () => void;
  /**
   * Callback fired when an option is selected.
   */
  onOptionSelect?: (option: Item) => void;
Story Types

Confirm that replacing BaseListItemData with DropdownOption aligns with the actual runtime shape used by Dropdown. Validate the Record<string, unknown> generic is appropriate and doesn’t hide missing required keys for options used in stories.

import { type BaseDropdownProps, type DropdownOption } from "../../DropdownNew/Dropdown.types";
import { FixedSizeList as List } from "react-window";

type Story = StoryObj<typeof Dropdown>;

const metaSettings = createStoryMetaSettingsDecorator({
  component: Dropdown,
  actionPropsArray: [
    "onMenuOpen",
    "onMenuClose",
    "onFocus",
    "onBlur",
    "onChange",
    "openMenuOnFocus",
    "onOptionRemove",
    "onOptionSelect",
    "onClear",
    "onInputChange",
    "onKeyDown"
  ]
});

const meta: Meta<typeof Dropdown> = {
  title: "Components/Dropdown [Alpha]",
  component: Dropdown,
  argTypes: metaSettings.argTypes,
  decorators: metaSettings.decorators
};

export default meta;

const dropdownTemplate = (props: BaseDropdownProps<any>) => {
  const options = useMemo(
    () => [
      { value: 1, label: "Option 1" },
      { value: 2, label: "Option 2" },
      { value: 3, label: "Option 3" }
    ],
    []
  );

  return (
    <div style={{ height: "150px", width: "300px" }}>
      <Dropdown options={options} label="Label" helperText="Helper text" {...props} />
    </div>
  );
};

export const Overview: Story = {
  render: dropdownTemplate.bind({}),
  args: {
    id: "overview-dropdown",
    ariaLabel: "Overview dropdown",
    placeholder: "Placeholder text here",
    clearAriaLabel: "Clear"
  },
  parameters: {
    docs: {
      liveEdit: {
        isEnabled: false
      }
    }
  }
};

export const Sizes: Story = {
  render: () => {
    const options = useMemo(
      () => [
        { value: 1, label: "Option 1" },
        { value: 2, label: "Option 2" },
        { value: 3, label: "Option 3" }
      ],
      []
    );
    return (
      <>
        <div style={{ width: "300px" }}>
          <Dropdown
            id="sizes-large"
            ariaLabel="Large dropdown"
            options={options}
            placeholder="Placeholder text here"
            size="large"
            clearAriaLabel="Clear"
          />
        </div>
        <div style={{ width: "300px" }}>
          <Dropdown
            id="sizes-medium"
            ariaLabel="Medium dropdown"
            options={options}
            placeholder="Placeholder text here"
            size="medium"
            clearAriaLabel="Clear"
          />
        </div>
        <div style={{ width: "300px" }}>
          <Dropdown
            id="sizes-small"
            ariaLabel="Small dropdown"
            options={options}
            placeholder="Placeholder text here"
            size="small"
            clearAriaLabel="Clear"
          />
        </div>
      </>
    );
  }
};

export const States: Story = {
  render: () => (
    <Flex direction="row" gap="medium">
      <Flex direction="column" gap="medium">
        <div style={{ width: "300px" }}>
          <Dropdown
            id="states-default"
            ariaLabel="Default dropdown"
            options={[]}
            placeholder="Default"
            clearAriaLabel="Clear"
          />
        </div>
        <div style={{ width: "300px" }}>
          <Dropdown
            id="states-disabled"
            ariaLabel="Disabled dropdown"
            options={[]}
            placeholder="Disabled"
            disabled
            clearAriaLabel="Clear"
          />
        </div>
      </Flex>
      <Flex direction="column" gap="medium">
        <div style={{ width: "300px" }}>
          <Dropdown
            id="states-error"
            ariaLabel="Error dropdown"
            options={[]}
            placeholder="Error"
            error
            clearAriaLabel="Clear"
          />
        </div>
        <div style={{ width: "300px" }}>
          <Dropdown
            id="states-readonly"
            ariaLabel="Readonly dropdown"
            options={[]}
            placeholder="Readonly"
            readOnly
            clearAriaLabel="Clear"
          />
        </div>
      </Flex>
    </Flex>
  )
};

export const MultiSelect: Story = {
  render: () => {
    const options = useMemo(
      () => [
        {
          value: "1",
          label: "Option 1"
        },
        {
          value: "2",
          label: "Option 2"
        },
        {
          value: "3",
          label: "Option 3"
        },
        {
          value: "4",
          label: "Option 4"
        }
      ],
      []
    );

    return (
      <Flex gap="large" align="start" justify="start">
        <Flex direction="column" gap="medium">
          <Text>Single line with hidden options</Text>
          <div style={{ width: "350px", marginBottom: "50px" }}>
            <Dropdown
              placeholder="Single line multi state"
              defaultValue={[options[0], options[1], options[2]]}
              options={options}
              multi
              clearAriaLabel="Clear"
            />
          </div>
        </Flex>
        <Flex direction="column" gap="medium">
          <Text>Multiple lines</Text>
          <div style={{ width: "350px", marginBottom: "50px" }}>
            <Dropdown
              placeholder="Multiple line multi state"
              defaultValue={[options[0], options[1], options[2]]}
              options={options}
              multi
              multiline
              clearAriaLabel="Clear"
            />
          </div>
        </Flex>
      </Flex>
    );
  }
};

export const DropdownWithIconOrAvatar: Story = {
  render: () => {
    const optionsIcons: any = useMemo(
      () => [
        {
          value: "email",
          label: "Email",
          startElement: {
            type: "icon",
            value: Email
          }
        },
        {
          value: "attach",
          label: "Attach",
          startElement: {
            type: "icon",
            value: Attach
          }
        }
      ],
      []
    );
    const optionsAvatar: any = useMemo(
      () => [
        {
          value: "Julia",
          label: "Julia Martinez",
          startElement: {
            type: "avatar",
            value: person1
          }
        },
        {
          value: "Sophia",
          label: "Sophia Johnson",
          startElement: {
            type: "avatar",
            value: person2
          }
        },
        {
          value: "Marco",
          label: "Marco DiAngelo",
          startElement: {
            type: "avatar",
            value: person3
          }
        }
      ],
      []
    );

    return (
      <Flex gap="large" align="start" justify="start">
        <Flex direction="column" gap="medium">
          <Text>Single value</Text>
          <div style={{ width: "350px", marginBottom: "10px" }}>
            <Dropdown defaultValue={optionsIcons[0]} options={optionsIcons} clearAriaLabel="Clear" />
          </div>
          <div style={{ width: "350px", marginBottom: "10px" }}>
            <Dropdown defaultValue={optionsAvatar[0]} options={optionsAvatar} clearAriaLabel="Clear" />
          </div>
        </Flex>
        <Flex direction="column" gap="medium">
          <Text>Multiple values</Text>
          <div style={{ width: "350px", marginBottom: "10px" }}>
            <Dropdown defaultValue={[optionsIcons[0]]} options={optionsIcons} multi clearAriaLabel="Clear" />
          </div>
          <div style={{ width: "350px", marginBottom: "10px" }}>
            <Dropdown defaultValue={[optionsAvatar[0]]} options={optionsAvatar} multi clearAriaLabel="Clear" />
          </div>
        </Flex>
      </Flex>
    );
  },
  parameters: {
    docs: {
      liveEdit: {
        scope: { person1, person2, person3 }
      }
    }
  }
};

export const Searchable: Story = {
  render: () => {
    const options = useMemo(
      () =>
        Array.from({ length: 10 }, (_, i) => ({
          value: `Option ${i + 1}`,
          label: `Option ${i + 1}`
        })),
      []
    );

    return (
      <div style={{ width: "300px" }}>
        <Dropdown
          placeholder={"Searchable for an item"}
          options={options}
          searchable
          maxMenuHeight={170}
          clearAriaLabel="Clear"
        />
      </div>
    );
  }
};

export const DropdownWithGroups: Story = {
  render: () => {
    const options = useMemo(
      () =>
        Array.from({ length: 2 }, (_, groupIndex) => ({
          label: `Group ${groupIndex + 1}`,
          options: Array.from({ length: 3 }, (_, optionIndex) => ({
            value: `${groupIndex * 2 + optionIndex + 1}`,
            label: `Option ${groupIndex * 2 + optionIndex + 1}`
          }))
        })),
      []
    );

    const optionsWithoutGroupLabel = useMemo(
      () =>
        Array.from({ length: 2 }, (_, groupIndex) => ({
          options: Array.from({ length: 2 }, (_, optionIndex) => ({
            value: `${groupIndex * 2 + optionIndex + 1}`,
            label: `Option ${groupIndex * 2 + optionIndex + 1}`
          }))
        })),
      []
    );

    return (
      <Flex gap="medium" align="start" justify="start">
        <Flex direction="column" gap="medium">
          <Text>Group by divider</Text>
          <div style={{ width: "300px" }}>
            <Dropdown
              placeholder="Group by divider"
              options={optionsWithoutGroupLabel}
              withGroupDivider
              maxMenuHeight={170}
              clearAriaLabel="Clear"
            />
          </div>
        </Flex>
        <Flex direction="column" gap="medium">
          <Text>Group by category title</Text>
          <div style={{ width: "300px" }}>
            <Dropdown
              placeholder="Group by category title"
              options={options}
              maxMenuHeight={170}
              clearAriaLabel="Clear"
            />
          </div>
        </Flex>
        <Flex direction="column" gap="medium">
          <Text>Group by category title sticky</Text>
          <div style={{ width: "300px" }}>
            <Dropdown
              placeholder="Group by category title sticky"
              options={options}
              stickyGroupTitle
              maxMenuHeight={170}
              clearAriaLabel="Clear"
            />
          </div>
        </Flex>
      </Flex>
    );
  }
};

export const DropdownItemWithElements: Story = {
  render: () => {
    const startOptions: DropdownOption<Record<string, unknown>>[] = useMemo(
      () => [
        {
          value: "icon",
          label: "Label with icon",
          startElement: {
            type: "icon",
            value: Email
          }
        },
        {
          value: "avatar",
          label: "Label with avatar",
          startElement: {
            type: "avatar",
            value: person1
          }
        },
        {
          value: "indent",
          label: "Label with indent",
          startElement: {
            type: "indent"
          }
        }
      ],
      []
    );

    const endOptions: DropdownOption<Record<string, unknown>>[] = useMemo(
      () => [

@github-actions
Copy link
Contributor

📦 Bundle Size Analysis

✅ No bundle size changes detected.

Unchanged Components
Component Base PR Diff
Accordion 6.33KB 6.33KB 0B ➖
AccordionItem 67.8KB 67.8KB 0B ➖
AlertBanner 72.54KB 72.54KB 0B ➖
AlertBannerButton 19KB 19KB 0B ➖
AlertBannerLink 15.38KB 15.38KB 0B ➖
AlertBannerText 65.15KB 65.15KB 0B ➖
AttentionBox 74.01KB 74.01KB 0B ➖
AttentionBoxLink 15.26KB 15.26KB 0B ➖
Avatar 67.97KB 67.97KB 0B ➖
AvatarGroup 95.34KB 95.34KB 0B ➖
Badge 43.56KB 43.56KB 0B ➖
Box 9.29KB 9.29KB 0B ➖
BreadcrumbItem 65.83KB 65.83KB 0B ➖
BreadcrumbMenu 69.95KB 69.95KB 0B ➖
BreadcrumbMenuItem 78.67KB 78.67KB 0B ➖
BreadcrumbsBar 5.78KB 5.78KB 0B ➖
Button 18.72KB 18.72KB 0B ➖
ButtonGroup 69.96KB 69.96KB 0B ➖
Checkbox 68.14KB 68.14KB 0B ➖
Chips 76.67KB 76.67KB 0B ➖
Clickable 6.27KB 6.27KB 0B ➖
ColorPicker 75.86KB 75.86KB 0B ➖
ColorPickerContent 75.12KB 75.12KB 0B ➖
Combobox 85.57KB 85.57KB 0B ➖
Counter 42.36KB 42.36KB 0B ➖
DatePicker 134.12KB 134.12KB 0B ➖
Dialog 54.03KB 54.03KB 0B ➖
DialogContentContainer 6.31KB 6.31KB 0B ➖
Divider 5.54KB 5.54KB 0B ➖
Dropdown 125.24KB 125.24KB 0B ➖
menu 59.95KB 59.95KB 0B ➖
option 92.72KB 92.72KB 0B ➖
singleValue 92.74KB 92.74KB 0B ➖
EditableHeading 67.75KB 67.75KB 0B ➖
EditableText 67.75KB 67.75KB 0B ➖
EmptyState 72.39KB 72.39KB 0B ➖
ExpandCollapse 67.52KB 67.52KB 0B ➖
Flex 7.49KB 7.49KB 0B ➖
FormattedNumber 5.87KB 5.87KB 0B ➖
GridKeyboardNavigationContext 4.65KB 4.65KB 0B ➖
Heading 66.51KB 66.51KB 0B ➖
HiddenText 5.41KB 5.41KB 0B ➖
CustomSvgIcon 11.86KB 11.86KB 0B ➖
Icon 13.82KB 13.82KB 0B ➖
IconButton 69.55KB 69.55KB 0B ➖
Info 73.86KB 73.86KB 0B ➖
Label 69.95KB 69.95KB 0B ➖
LayerProvider 2.95KB 2.95KB 0B ➖
LegacyModal 76.37KB 76.37KB 0B ➖
LegacyModalContent 66.56KB 66.56KB 0B ➖
LegacyModalFooter 3.45KB 3.45KB 0B ➖
LegacyModalFooterButtons 20.48KB 20.48KB 0B ➖
LegacyModalHeader 72.52KB 72.52KB 0B ➖
Link 15.02KB 15.02KB 0B ➖
List 74.41KB 74.41KB 0B ➖
ListItem 67.03KB 67.03KB 0B ➖
ListItemAvatar 68.1KB 68.1KB 0B ➖
ListItemIcon 14.12KB 14.12KB 0B ➖
ListTitle 66.52KB 66.52KB 0B ➖
Loader 5.83KB 5.83KB 0B ➖
Menu 8.63KB 8.63KB 0B ➖
MenuDivider 5.64KB 5.64KB 0B ➖
MenuGridItem 7.22KB 7.22KB 0B ➖
MenuItem 78.7KB 78.7KB 0B ➖
MenuItemButton 71.76KB 71.76KB 0B ➖
MenuTitle 66.83KB 66.83KB 0B ➖
MenuButton 67.59KB 67.59KB 0B ➖
MultiStepIndicator 53.07KB 53.07KB 0B ➖
NumberField 74.59KB 74.59KB 0B ➖
LinearProgressBar 7.45KB 7.45KB 0B ➖
RadioButton 67.34KB 67.34KB 0B ➖
Search 72.15KB 72.15KB 0B ➖
Skeleton 6.19KB 6.19KB 0B ➖
Slider 75.58KB 75.58KB 0B ➖
SplitButton 68.42KB 68.42KB 0B ➖
SplitButtonMenu 8.83KB 8.83KB 0B ➖
Steps 73.08KB 73.08KB 0B ➖
Table 7.3KB 7.3KB 0B ➖
TableBody 68.29KB 68.29KB 0B ➖
TableCell 66.59KB 66.59KB 0B ➖
TableContainer 5.36KB 5.36KB 0B ➖
TableHeader 5.71KB 5.71KB 0B ➖
TableHeaderCell 73.77KB 73.77KB 0B ➖
TableRow 5.6KB 5.6KB 0B ➖
TableRowMenu 70.3KB 70.3KB 0B ➖
TableVirtualizedBody 72.94KB 72.94KB 0B ➖
Tab 65.23KB 65.23KB 0B ➖
TabList 8.92KB 8.92KB 0B ➖
TabPanel 5.35KB 5.35KB 0B ➖
TabPanels 5.91KB 5.91KB 0B ➖
TabsContext 5.54KB 5.54KB 0B ➖
Text 66.41KB 66.41KB 0B ➖
TextArea 67.66KB 67.66KB 0B ➖
TextField 70.96KB 70.96KB 0B ➖
TextWithHighlight 65.58KB 65.58KB 0B ➖
ThemeProvider 4.69KB 4.69KB 0B ➖
Tipseen 72.83KB 72.83KB 0B ➖
TipseenContent 73.36KB 73.36KB 0B ➖
TipseenImage 73.15KB 73.15KB 0B ➖
TipseenMedia 73.04KB 73.04KB 0B ➖
TipseenWizard 75.58KB 75.58KB 0B ➖
Toast 75.64KB 75.64KB 0B ➖
ToastButton 18.89KB 18.89KB 0B ➖
ToastLink 15.25KB 15.25KB 0B ➖
Toggle 68.03KB 68.03KB 0B ➖
Tooltip 64.4KB 64.4KB 0B ➖
TransitionView 37.73KB 37.73KB 0B ➖
VirtualizedGrid 12.61KB 12.61KB 0B ➖
VirtualizedList 12.36KB 12.36KB 0B ➖

📊 Summary:

  • Total Base Size: 5.12MB
  • Total PR Size: 5.12MB
  • Total Difference: +0B

@rivka-ungar rivka-ungar merged commit a207bb2 into master Sep 29, 2025
17 checks passed
@rivka-ungar rivka-ungar deleted the dropdown-remove-baselistitemdata-from-stories-10052448934 branch September 29, 2025 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants