Skip to content

feat(DATAUI-3610): add suggest#2596

Open
getDimaMal wants to merge 13 commits into
gravity-ui:mainfrom
getDimaMal:feat/DATAUI-3610_add-suggest
Open

feat(DATAUI-3610): add suggest#2596
getDimaMal wants to merge 13 commits into
gravity-ui:mainfrom
getDimaMal:feat/DATAUI-3610_add-suggest

Conversation

@getDimaMal
Copy link
Copy Markdown
Contributor

No description provided.

@gravity-ui
Copy link
Copy Markdown
Contributor

gravity-ui Bot commented Feb 26, 2026

Preview is ready.

@gravity-ui
Copy link
Copy Markdown
Contributor

gravity-ui Bot commented Feb 26, 2026

🎭 Component Tests Report is ready.

@getDimaMal getDimaMal force-pushed the feat/DATAUI-3610_add-suggest branch 5 times, most recently from ca4d0eb to 869d946 Compare February 27, 2026 12:32
@getDimaMal getDimaMal requested a review from ogonkov as a code owner March 2, 2026 11:41
@getDimaMal getDimaMal force-pushed the feat/DATAUI-3610_add-suggest branch from 313deb2 to 190b2ee Compare March 2, 2026 11:42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess input should have clear button?

import {Flex} from '../layout';

export type SuggestProps<T> = TextInputProps & {
items?: ListItemData<T>[];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would expect Suggest being almost identical to Select in almost every aspect except for the trigger (which is TextInput in this case) and the lack of filter in Popup

So the property should be called options and other props should be copied as is as well (with some exceptions)

I would expect the same visuals and behaviors as well

@amje what do you think?

There's also an inline mode in common's Suggest. Do you plan do add it as well?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Ruminat Which props do you want to copy? I agree with the naming of items

Copy link
Copy Markdown
Contributor

@Ruminat Ruminat Mar 6, 2026

Choose a reason for hiding this comment

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

@korvin89 These, I guess:

// at least these props, I guess
Pick<SelectProps,
    | "onUpdate"
    | "renderOption"
    | "renderOptionGroup"
    | "renderEmptyOptions"
    | "renderPopup"
    | "renderCounter"
    | "getOptionHeight"
    | "getOptionGroupHeight"
    | "value"
    | "defaultValue"
    | "options"
    | "error"
    | "errorMessage"
    | "errorPlacement"
    | "validationState"
    | "multiple"
    | "filter"
    | "onFilterChange"
    | "disablePortal"
    | "loading"
    | "onLoadMore"
    | "id"
    | "hasCounter"
    | "renderCounter"
    | "disabled"
>

// Would like to see these ones as well
type NiceToHaveProps = {
    readonly?: boolean;
    inline?: boolean;
};

@getDimaMal getDimaMal force-pushed the feat/DATAUI-3610_add-suggest branch from eba7320 to 6e9ebd7 Compare March 10, 2026 08:33
@getDimaMal getDimaMal force-pushed the feat/DATAUI-3610_add-suggest branch from eaae402 to 79cba7f Compare March 10, 2026 09:11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please add readme-ru like the other components

popupClassName?: PopupProps['className'];
popupPlacement?: PopupProps['placement'];
popupQa?: PopupProps['qa'];
popupWidth?: 'fit' | 'auto' | number;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's make an interface here like in Select

popupWidth?: 'fit' | number;

import {Flex} from '../layout';

export type SuggestProps<T> = TextInputProps & {
items?: ListItemData<T>[];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's probably call it options like in Select


export type SuggestProps<T> = TextInputProps & {
items?: ListItemData<T>[];
onItemClick?: ListProps<T>['onItemClick'];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Accordingly, this is onOptionClick

popupPlacement?: PopupProps['placement'];
popupQa?: PopupProps['qa'];
popupWidth?: 'fit' | 'auto' | number;
renderItem?: ListProps<T>['renderItem'];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

renderItem -> renderOption

ref: React.Ref<HTMLSpanElement>,
) {
const listRef = React.useRef<List<T>>(null);
const [open, setOpen] = React.useState(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's use useOpenState here

export const useOpenState = (props: UseOpenProps) => {

[textInputProps],
);

const handleKeyDown = React.useCallback(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's probably worth processing the esc click here

@korvin89
Copy link
Copy Markdown
Contributor

The component implements a classic combobox pattern (text input + popup list), but the required ARIA markup is missing entirely.

At minimum, the TextInput should receive:

<TextInput
  controlProps={{
    role: 'combobox',
    'aria-expanded': open,
    'aria-controls': popupId,
    'aria-autocomplete': 'list',
    'aria-activedescendant': activeIndex !== undefined
      ? `${popupId}-item-${activeIndex}`
      : undefined,
  }}

  ...

<Popup id={popupId} ...>
  <List .../>
</Popup>
/>

@@ -0,0 +1 @@
export {Suggest} from './Suggest';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

export SuggestProps

import {Suggest} from '@gravity-ui/uikit';
```

`Suggest` is a lightweight building block for “type to filter + popup list” scenarios. It renders a `TextInput` and a
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add this sentence please: Suggest is a basic implementation of "combobox" pattern

`Suggest` supports all `TextInput` props. The table below lists the props specific to `Suggest`:

| Name | Description | Default | Type |
| :------------- | :-------------------------------------------------- | :------ | :----------------------------------------------------- |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use the same table structure as in all other readmes: Name, Description, Type, Default, where Type and Default are center aligned

| Name | Description | Default | Type |
| :------------- | :-------------------------------------------------- | :------ | :----------------------------------------------------- |
| items | Items rendered by `List` | | `Array<ListItemData<T>>` |
| onItemClick | Fires when list item is clicked | | `ListProps<T>['onItemClick']` |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe we shouldn't expose List types, it will be deprecated soon, lets' just copy them

@amje
Copy link
Copy Markdown
Contributor

amje commented Mar 14, 2026

There is no point of showing popup with no list:
Снимок экрана 2026-03-14 в 15 55 01

title: 'Components/Inputs/Suggest',
component: Suggest,
argTypes: {},
parameters: {},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This breaks the story. Controls are not interactive:

Image

};

export const Default: Story = {
render: () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You should pass args from here to the component props

const items = React.useMemo(() => baseFilter(value), [value]);
const handelItemClick = React.useCallback((item: TItem) => {
setValue(item.content);
console.log('onItemClick', item);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of using console.log use Storybook's dedicated actions

render: () => {
const [value, setValue] = React.useState('');
return (
<Flex direction="column" gap={4}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For stories layout we have Showcase* components. Please use them

onUpdate={setValue}
placeholder="Search..."
renderItem={(item) => <div>{item.content}</div>}
value={value}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Instead of repeating props, utilize Storybook's args for story. Pass common props to the Default story, and then reuse them in other stories. See Button's stories as a reference

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You should rebase main branch, test names are different now

const smokeScenarios = createSmokeScenarios<TestSuggestProps>(defaultProps, {
size: ['s', 'm', 'l', 'xl'],
disabled: [true],
hasClear: [true],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's incorrect usage. Please follow the same structure as in other tests

await expectScreenshot({themes: ['light']});
});

test('smoke opened', {tag: ['@smoke']}, async ({mount, page, expectScreenshot}) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why it's a separate smoke test?

await expectScreenshot({themes: ['light']});
});

test('smoke customPopup', {tag: ['@smoke']}, async ({mount, page, expectScreenshot}) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's not a smoke test. Smoke test is just simple snapshot of props combinations. For custom popup do a normal visual test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants