chore: update select component (#38954)

![CleanShot 2025-02-03 at 13 51
45](https://github.com/user-attachments/assets/4c7a7a67-c1fc-4fe7-afbb-2342aea27fcc)

Few known bugs:
1. --The placeholder value is cleared when the user is searching. This
is happening cause we are using hack to put search into dropdown and it
is conflicting with native behavior of rc-select--


[](https://github.com/user-attachments/assets/4d40607f-c9c9-4060-9086-cc9d8dc49553)

/ok-to-test tags="@tag.All"

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a grouped dropdown with checkboxes for multi-select, making
option organization more intuitive.
- **Enhancements**
	- Upgraded dropdown search with auto-focus and dynamic filtering.
- Improved tag display with responsive limits and an updated "info"
style.
	- Added configuration options to control the number of visible tags.
- **Documentation**
- Expanded examples to showcase the new grouped and checkbox-enhanced
dropdown features.
- **Style**
- Refined styling and animations for dropdown states, ensuring a fluid
and consistent user experience.
- **Bug Fixes**
- Adjusted selectors in tests to improve interaction with dropdowns,
enhancing test reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/13173050535>
> Commit: 33634093ddb9b6d699d8f9c50297c4245bea21fb
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13173050535&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Thu, 06 Feb 2025 07:34:34 UTC
<!-- end of auto-generated comment: Cypress test results  -->
This commit is contained in:
Pawan Kumar 2025-02-06 13:10:25 +05:30 committed by GitHub
parent fd4b6d420a
commit 6f2f11b40b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 399 additions and 120 deletions

View File

@ -28,8 +28,7 @@ describe(
EditorNavigation.SelectEntityByName("Chart1", EntityType.Widget, {}, [
"Container1",
]);
cy.get(viewWidgetsPage.chartType).last().click({ force: true });
cy.get(".t--dropdown-option").children().contains("Column chart").click();
cy.selectDropdownValue(viewWidgetsPage.chartType, "Column chart");
cy.get(
".t--property-control-charttype span.rc-select-selection-item span",
)

View File

@ -82,19 +82,13 @@ describe(
it("Update Placement and Verify buttons alignments", function () {
// check first button placement
cy.selectDropdownValue(
".t--property-control-placement .rc-select-selection-item",
"Between",
);
cy.selectDropdownValue(".t--property-control-placement", "Between");
// 1st btn
cy.get(firstButton)
.last()
.should("have.css", "justify-content", "space-between");
// update dropdown value
cy.selectDropdownValue(
".t--property-control-placement .rc-select-selection-item",
"Start",
);
cy.selectDropdownValue(".t--property-control-placement", "Start");
cy.get(firstButton).last().should("have.css", "justify-content", "start");
// other button style stay same
cy.get(menuButton).should("have.css", "justify-content", "center");

View File

@ -27,12 +27,13 @@ describe(
it("2. should check for type of value and widget", () => {
cy.openPropertyPane(widgetName);
cy.get(".t--property-control-currency").click();
cy.get(".t--property-control-currency").type("usd");
cy.openSelectDropdown(".t--property-control-currency");
cy.searchSelectDropdown("usd");
cy.selectDropdownValue(
".t--property-control-currency input",
".t--property-control-currency",
"USD - US Dollar",
);
function enterAndTest(text, expected) {
cy.get(widgetInput).clear();
cy.wait(300);
@ -53,7 +54,7 @@ describe(
});
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "1");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "1");
[
//[input, {{CurrencyInput1.text}}:{{CurrencyInput1.value}}:{{CurrencyInput1.isValid}}:{{typeof CurrencyInput1.text}}:{{typeof CurrencyInput1.value}}:{{CurrencyInput1.countryCode}}:{{CurrencyInput1.currencyCode}}]
@ -66,7 +67,7 @@ describe(
});
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "2");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "2");
[
//[input, {{CurrencyInput1.text}}:{{CurrencyInput1.value}}:{{CurrencyInput1.isValid}}:{{typeof CurrencyInput1.text}}:{{typeof CurrencyInput1.value}}:{{CurrencyInput1.countryCode}}:{{CurrencyInput1.currencyCode}}]
@ -80,10 +81,10 @@ describe(
cy.get(".currency-change-dropdown-trigger").should("contain", "$");
cy.openPropertyPane(widgetName);
cy.get(".t--property-control-currency").click();
cy.get(".t--property-control-currency").type("ind");
cy.openSelectDropdown(".t--property-control-currency");
cy.searchSelectDropdown("ind");
cy.selectDropdownValue(
".t--property-control-currency input",
".t--property-control-currency",
"INR - Indian Rupee",
);
enterAndTest("100.22", "100.22:100.22:true:string:number:IN:INR");
@ -100,12 +101,13 @@ describe(
.last()
.click();
enterAndTest("100.22", "100.22:100.22:true:string:number:GB:GBP");
enterAndTest("100.22", "100.22:100.22:true:string:number:GB:GBP");
cy.get(".t--input-currency-change").should("contain", "£");
});
it("3. should accept 0 decimal option", () => {
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "0");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "0");
cy.closePropertyPane();
cy.wait(500);
cy.openPropertyPane(widgetName);
@ -142,7 +144,7 @@ describe(
`{{CurrencyInput1.text}}:{{CurrencyInput1.value}}`,
);
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "0");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "0");
[
//[input, {{CurrencyInput1.text}}:{{CurrencyInput1.value}}]
@ -157,7 +159,7 @@ describe(
});
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "1");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "1");
[
//[input, {{CurrencyInput1.text}}:{{CurrencyInput1.value}}]
["100", "100:100"],
@ -171,7 +173,7 @@ describe(
});
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "2");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "2");
[
//[input, {{CurrencyInput1.text}}:{{CurrencyInput1.value}}]
["100", "100:100"],
@ -205,7 +207,7 @@ describe(
}
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "0");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "0");
[
//[input, expected]
@ -221,7 +223,7 @@ describe(
});
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "1");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "1");
[
//[input, expected]
["100", "100"],
@ -238,7 +240,7 @@ describe(
});
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-decimalsallowed input", "2");
cy.selectDropdownValue(".t--property-control-decimalsallowed", "2");
[
//[input, expected]
["100", "100"],

View File

@ -29,7 +29,7 @@ describe(
);
agHelper.AssertText(
commonlocators.filePickerDataFormat,
`${commonlocators.filePickerDataFormat} .rc-select-selection-item .ads-v2-text`,
"text",
"Array of Objects (CSV, XLS(X), JSON, TSV)",
);

View File

@ -52,13 +52,13 @@ describe(
it("3. Validate DataType - NUMBER can be entered into Input widget", () => {
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-datatype input", "Number");
cy.selectDropdownValue(".t--property-control-datatype", "Number");
cy.get(".t--property-control-required label")
.last()
.click({ force: true });
cy.selectDropdownValue(".t--property-control-datatype input", "Number");
cy.selectDropdownValue(".t--property-control-datatype", "Number");
[
{
input: "invalid",
@ -101,7 +101,7 @@ describe(
it("4. Validate DataType - PASSWORD can be entered into Input widget", () => {
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-datatype input", "Password");
cy.selectDropdownValue(".t--property-control-datatype", "Password");
[
{
input: "test",
@ -136,7 +136,7 @@ describe(
it("5. Validate DataType - EMAIL can be entered into Input widget", () => {
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-datatype input", "Email");
cy.selectDropdownValue(".t--property-control-datatype", "Email");
cy.get(".t--property-control-required label")
.last()

View File

@ -27,7 +27,7 @@ describe(
cy.wait(300);
cy.get(widgetInput).should("contain.value", "");
cy.selectDropdownValue(".t--property-control-datatype input", "Number");
cy.selectDropdownValue(".t--property-control-datatype", "Number");
cy.get(widgetInput).clear();
cy.get(widgetInput).type("1.0010{enter}"); //Clicking enter submits the form here
@ -37,7 +37,7 @@ describe(
it("3. Validate DataType - TEXT can be entered into Input widget", () => {
cy.selectDropdownValue(
".t--property-control-datatype input",
".t--property-control-datatype",
"Single-line text",
);
[
@ -112,7 +112,7 @@ describe(
it("4. Validate DataType - NUMBER can be entered into Input widget", () => {
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-datatype input", "Number");
cy.selectDropdownValue(".t--property-control-datatype", "Number");
[
{
input: "invalid",
@ -157,7 +157,7 @@ describe(
.last()
.click({ force: true });
cy.selectDropdownValue(".t--property-control-datatype input", "Number");
cy.selectDropdownValue(".t--property-control-datatype", "Number");
[
{
input: "invalid",
@ -200,7 +200,7 @@ describe(
it("5. Validate DataType - PASSWORD can be entered into Input widget", () => {
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-datatype input", "Password");
cy.selectDropdownValue(".t--property-control-datatype", "Password");
[
{
input: "test",
@ -273,7 +273,7 @@ describe(
it("6. Validate DataType - EMAIL can be entered into Input widget", () => {
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(".t--property-control-datatype input", "Email");
cy.selectDropdownValue(".t--property-control-datatype", "Email");
[
{
input: "test",
@ -361,7 +361,7 @@ describe(
it("8. onSubmit should be triggered with the whole input value", () => {
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(
".t--property-control-datatype input",
".t--property-control-datatype",
"Single-line text",
);
cy.get(".t--property-control-required label")
@ -415,7 +415,7 @@ describe(
"anotherText:anotherText:true",
);
cy.selectDropdownValue(".t--property-control-datatype input", "Number");
cy.selectDropdownValue(".t--property-control-datatype", "Number");
cy.updateCodeInput(".t--property-control-defaultvalue", `{{1}}`);
// wait for evaluations
@ -439,7 +439,7 @@ describe(
// Init isDirty
cy.openPropertyPane(widgetName);
cy.selectDropdownValue(
".t--property-control-datatype input",
".t--property-control-datatype",
"Single-line text",
);
cy.updateCodeInput(".t--property-control-defaultvalue", "a");
@ -490,7 +490,7 @@ describe(
cy.get(widgetInput).should("have.attr", "autocomplete", "off");
//select a non email or password option
cy.selectDropdownValue(".t--property-control-datatype input", "text");
cy.selectDropdownValue(".t--property-control-datatype", "text");
//autofill toggle should not be present as this restores autofill to be enabled
cy.get(".t--property-control-allowautofill input").should("not.exist");
//autocomplete attribute should not be present in the text widget

View File

@ -2,17 +2,7 @@ import * as _ from "../../../../../support/Objects/ObjectsCore";
const widgetName = "phoneinputwidget";
const widgetInput = `.t--widget-${widgetName} input`;
const searchAndSelectOption = (optionValue) => {
cy.get(".t--property-control-defaultcountrycode input")
.last()
.scrollIntoView()
.click({ force: true })
.type(optionValue.substring(0, 3));
cy.get(".t--dropdown-option")
.children()
.contains(optionValue)
.click({ force: true });
};
describe(
"Phone input widget - ",
{ tags: ["@tag.Widget", "@tag.PhoneInput", "@tag.Binding"] },
@ -21,7 +11,7 @@ describe(
_.agHelper.AddDsl("emptyDSL");
});
it("1. Add new dropdown widget", () => {
it("1. Add new phone input widget", () => {
cy.dragAndDropToCanvas(widgetName, { x: 300, y: 300 });
cy.get(`.t--widget-${widgetName}`).should("exist");
cy.dragAndDropToCanvas("textwidget", { x: 300, y: 500 });
@ -39,7 +29,12 @@ describe(
cy.get(".t--widget-textwidget").should("contain", "(999) 999-9999:US:+1");
cy.openPropertyPane(widgetName);
searchAndSelectOption("Afghanistan (+93)");
cy.openSelectDropdown(".t--property-control-defaultcountrycode");
cy.searchSelectDropdown("Afg");
cy.selectDropdownValue(
".t--property-control-defaultcountrycode",
"Afghanistan (+93)",
);
cy.get(`.t--widget-${widgetName} input`).clear();
cy.wait(500);
cy.get(`.t--widget-${widgetName} input`).type("1234567890");
@ -62,14 +57,24 @@ describe(
cy.get(".t--property-control-enableformatting label")
.last()
.click({ force: true });
searchAndSelectOption("United States / Canada (+1)");
cy.openSelectDropdown(".t--property-control-defaultcountrycode");
cy.searchSelectDropdown("United States / Canada");
cy.selectDropdownValue(
".t--property-control-defaultcountrycode",
"United States / Canada (+1)",
);
cy.get(`.t--widget-${widgetName} input`).clear();
cy.wait(500);
cy.get(`.t--widget-${widgetName} input`).type("9999999999");
cy.get(".t--widget-textwidget").should("contain", "9999999999:US:+1");
cy.openPropertyPane(widgetName);
searchAndSelectOption("India (+91)");
cy.openSelectDropdown(".t--property-control-defaultcountrycode");
cy.searchSelectDropdown("India");
cy.selectDropdownValue(
".t--property-control-defaultcountrycode",
"India (+91)",
);
cy.get(`.t--widget-${widgetName} input`).clear();
cy.wait(500);
cy.get(`.t--widget-${widgetName} input`).type("1234567890");
@ -131,13 +136,11 @@ describe(
// Select the Currency dropdown option from property pane
// and enter a value that has space and returns 0 results
cy.get(".t--property-control-defaultcountrycode input")
.first()
.click()
.type("AFDB (+93)");
cy.openSelectDropdown(".t--property-control-defaultcountrycode");
cy.searchSelectDropdown("AFDB");
// assert that the dropdown is still option
cy.get(".t--property-control-defaultcountrycode input").should(
cy.get(".ads-v2-select__dropdown .rc-select-item-empty").should(
"be.visible",
);
});

View File

@ -1,7 +1,7 @@
{
"imageWidget": ".t--draggable-imagewidget",
"chartWidget": ".t--draggable-chartwidget",
"chartType": ".t--property-control-charttype input",
"chartType": ".t--property-control-charttype",
"chartTypeText": ".t--property-control-charttype",
"mapType": ".t--property-control-maptype .rc-select-selection-item",
"destin": ".appsmith_widget_01ewdomru7",
@ -36,4 +36,4 @@
"pickMyLocation": ".t--draggable-mapwidget div[title='Pick My Location']",
"mapChartEntityLabels": ".t--draggable-mapchartwidget text",
"listWidget": ".t--draggable-listwidget"
}
}

View File

@ -5,7 +5,7 @@
"multiSelectWidget": ".t--draggable-multiselectwidgetv2",
"togglebutton": "input[type='checkbox']",
"showStepArrowsToggleCheckBox": ".t--property-control-showsteparrows input[type='checkbox']",
"inputPropsDataType": ".t--property-control-datatype input",
"inputPropsDataType": ".t--property-control-datatype",
"inputdatatypeplaceholder": ".t--property-control-placeholder",
"buttonWidget": ".t--draggable-buttonwidget",
"buttonColor": ".t--property-control-buttoncolor [data-testid='t--color-picker-input']",
@ -31,7 +31,7 @@
"labelColor": ".t--property-control-labelcolor input",
"inputval": ".t--draggable-inputwidgetv2 span.t--widget-name",
"dataclass": ".bp3-input",
"datatype": ".t--property-control-datatype input",
"datatype": ".t--property-control-datatype",
"rowHeight": ".t--property-control-defaultrowheight .rc-select-selection-search-input ",
"innertext": ".t--draggable-inputwidgetv2 input",
"defaultinput": ".t--property-control-defaultinput",
@ -232,9 +232,9 @@
"counterclockwise": ".t--property-control-counterclockwise input[type='checkbox']",
"serversideFilteringInput": ".t--property-control-serversidefiltering input[type='checkbox']",
"propertyPaneSaveButton": ".t--property-pane-section-collapse-savebutton",
"firstEditInput":"[data-colindex=0][data-rowindex=0] .t--inlined-cell-editor input.bp3-input",
"cellControlSwitch" : ".t--property-control-cellwrapping .ads-v2-switch",
"propertyControlLabel" : ".t--property-control-label",
"firstEditInput": "[data-colindex=0][data-rowindex=0] .t--inlined-cell-editor input.bp3-input",
"cellControlSwitch": ".t--property-control-cellwrapping .ads-v2-switch",
"propertyControlLabel": ".t--property-control-label",
"todayText": "span:contains('Today')",
"dayPickerToday": ".DayPicker-Day--today"
}
}

View File

@ -126,12 +126,12 @@
"filePickerRemoveButton": ".uppy-Dashboard-Item-action--remove",
"AddMoreFiles": ".uppy-DashboardContent-addMoreCaption",
"filePickerOnFilesSelected": ".t--property-control-onfilesselected",
"dataType": ".t--property-control-datatype input",
"recaptchaVersion": ".t--property-control-googlerecaptchaversion input",
"dataType": ".t--property-control-datatype",
"recaptchaVersion": ".t--property-control-googlerecaptchaversion",
"recaptchaVersionText": ".t--property-control-googlerecaptchaversion span.rc-select-selection-item span",
"filePickerDataFormat": ".t--property-control-dataformat .rc-select-selection-item",
"filePickerDataFormat": ".t--property-control-dataformat",
"helperText": ".t--property-control-helperText",
"jsonFormFieldType": ".t--property-control-fieldtype input",
"jsonFormFieldType": ".t--property-control-fieldtype",
"jsonFormAddNewCustomFieldBtn": ".t--property-control-fieldconfiguration .t--add-column-btn",
"evaluateMsg": ".t--evaluatedPopup-error",
"globalSearchModal": "[data-testid='t--global-search-modal']",
@ -207,7 +207,7 @@
"fixed": "Fixed",
"autoHeight": "Auto Height",
"autoHeightWithLimits": "Auto Height with limits",
"heightDropdown": "[data-guided-tour-iid='dynamicHeight'] input",
"heightDropdown": "[data-guided-tour-iid='dynamicHeight']",
"minHeight": "minheight\\(inrows\\)",
"maxHeight": "maxheight\\(inrows\\)",
"overlayMin": "[data-testid='t--auto-height-overlay-min']",
@ -248,4 +248,4 @@
"downloadFileType": "button[class*='t--open-dropdown-Select-file-type'] > span:first-of-type",
"listToggle": "[data-testid='t--list-toggle']",
"showBindingsMenu": "//*[@id='entity-properties-container']"
}
}

View File

@ -575,12 +575,14 @@ export class AggregateHelper {
});
}
public WaitUntilEleAppear(selector: string) {
// Note: isVisible is required in case where item exists but is not visible ( hidden by css ),
// For e.g - search input in select widget is not visible,
public WaitUntilEleAppear(selector: string, isVisible = true) {
cy.waitUntil(
() =>
this.GetElement(selector)
.should("exist")
.should("be.visible")
.should(isVisible ? "be.visible" : "not.be.visible")
.its("length")
.should("be.gte", 1),
{

View File

@ -450,10 +450,16 @@ export class GitSync {
public CheckMergeConflicts(destinationBranch: string) {
this.agHelper.AssertElementExist(this.locators.quickActionsPullBtn);
this.agHelper.GetNClick(this.locators.quickActionsMergeBtn);
this.agHelper.WaitUntilEleAppear(this.locators.opsMergeBranchSelectMenu);
this.agHelper.WaitUntilEleAppear(
this.locators.opsMergeBranchSelectMenu,
false,
);
this.agHelper.WaitUntilEleDisappear(this.locators.opsMergeLoader);
this.assertHelper.AssertNetworkStatus("@getBranch", 200);
this.agHelper.WaitUntilEleAppear(this.locators.opsMergeBranchSelectMenu);
this.agHelper.WaitUntilEleAppear(
this.locators.opsMergeBranchSelectMenu,
false,
);
this.agHelper.GetNClick(this.locators.opsMergeBranchSelectMenu, 0, true);
this.agHelper.AssertContains(destinationBranch);
this.agHelper.GetNClickByContains(

View File

@ -86,14 +86,45 @@ Cypress.Commands.add("selectDateFormat", (value) => {
.click({ force: true });
});
Cypress.Commands.add("openSelectDropdown", (element) => {
let isDropdownAlreadyOpen = false;
cy.get(element)
.invoke("html")
.then((html) => {
if (html.includes("rc-select-open")) {
isDropdownAlreadyOpen = true;
}
})
.then(() => {
if (!isDropdownAlreadyOpen) {
cy.get(element).last().scrollIntoView().click({ force: true });
cy.get(`${element} .rc-select-selection-search-input`)
.last()
.click({ force: true });
}
});
});
Cypress.Commands.add("selectDropdownValue", (element, value) => {
cy.get(element).last().scrollIntoView().click({ force: true });
cy.openSelectDropdown(element);
cy.get(".t--dropdown-option")
.children()
.contains(value)
.click({ force: true });
});
Cypress.Commands.add("searchSelectDropdown", (value) => {
cy.get(".ads-v2-select__dropdown .ads-v2-input__input-section-input").click();
cy.get(".ads-v2-select__dropdown .ads-v2-input__input-section-input").should(
"have.focus",
);
cy.get(".ads-v2-select__dropdown .ads-v2-input__input-section-input").type(
value,
);
});
Cypress.Commands.add("assertDateFormat", () => {
cy.get(".t--draggable-datepickerwidget2 input")
.first()

View File

@ -388,4 +388,4 @@
"@types/react": "^17.0.2",
"postcss": "8.4.31"
}
}
}

View File

@ -27,7 +27,7 @@ function Checkbox(props: CheckboxProps) {
isFocusVisible={isFocusVisible}
isIndeterminate={isIndeterminate}
>
{children}
<span>{children}</span>
<input {...inputProps} {...focusProps} ref={ref} />
<span className={CheckboxClassNameSquare} />
</StyledCheckbox>

View File

@ -51,6 +51,10 @@ Select allows users to make single or multiple selections from a list of options
<Canvas of={SelectStories.SelectInvalidStory} />
### Grouping with Checkbox in options
<Canvas of={SelectStories.SelectWithCheckboxAndGroup} />
## Best practices
The select component should:

View File

@ -1,9 +1,10 @@
import React, { useState } from "react";
import { Select, Option } from "./Select";
import { Select, Option, OptGroup } from "./Select";
import { Icon } from "../Icon";
import { Checkbox } from "../Checkbox";
import type { SelectProps } from "./Select.types";
import type { StoryObj } from "@storybook/react";
import type { DefaultOptionType } from "rc-select/lib/Select";
export default {
title: "ADS/Components/Select",
@ -970,3 +971,75 @@ export function SelectWithCheckbox() {
</Select>
);
}
const groupOptions = [
{
label: "Group 1",
options: [
{
label: "Option 1",
value: "value 11",
},
{
label: "Very long label to force a line break in the option",
value: "Very long label to force a line break in the option",
},
],
},
{
label: "Group 2",
options: Array.from({ length: 1000 }, (_, i) => ({
label: `Option ${i + 1}`,
value: `value ${i + 1}`,
})),
},
];
export function SelectWithCheckboxAndGroup() {
const [selectedOptions, setSelectedOptions] = useState<DefaultOptionType[]>(
[],
);
return (
<Select
isMultiSelect
onDeselect={(value, unselectedOption) =>
setSelectedOptions(
selectedOptions.filter((opt) => opt.value !== unselectedOption.value),
)
}
onSelect={(value, newSelectedOption) =>
setSelectedOptions([...selectedOptions, newSelectedOption])
}
placeholder="Select options"
showSearch
value={selectedOptions}
virtual
>
{groupOptions.map((group, groupIndex) => (
<OptGroup key={`${group.label}-${groupIndex}`} label={group.label}>
{group.options.map((option, optionIndex) => (
<Option
key={`${option.value}-${groupIndex}-${optionIndex}`}
label={option.label}
title={option.label}
value={option.value}
>
<Checkbox
// making it read only as it is interfering with selection of options of rc-select
isReadOnly
isSelected={Boolean(
selectedOptions.find(
(selectedOption) => selectedOption.value == option.value,
),
)}
>
{option.label}
</Checkbox>
</Option>
))}
</OptGroup>
))}
</Select>
);
}

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useRef, useState } from "react";
import RCSelect, {
Option as RCOption,
OptGroup as RCOptGroup,
@ -12,6 +12,7 @@ import { SelectClassName, SelectDropdownClassName } from "./Select.constants";
import { Tag } from "../Tag";
import type { SelectProps } from "./Select.types";
import { Spinner } from "../Spinner";
import { SearchInput } from "../SearchInput";
/*
TODO:
@ -29,15 +30,20 @@ function Select(props: SelectProps) {
isLoading = false,
isMultiSelect,
isValid,
maxTagCount = 2,
maxTagCount = isMultiSelect
? props.value?.length > 1
? "responsive"
: 1
: undefined,
maxTagPlaceholder,
maxTagTextLength = 5,
placeholder = "Please select an option",
showSearch = false,
size = "md",
virtual = false,
...rest
} = props;
const searchRef = useRef<HTMLInputElement>(null);
const [searchValue, setSearchValue] = useState("");
const getMaxTagPlaceholder = (omittedValues: any[]) => {
return `+${omittedValues.length}`;
@ -51,6 +57,24 @@ function Select(props: SelectProps) {
return <Icon name="arrow-down-s-line" size="md" />;
}
const handleDropdownVisibleChange = (open: boolean) => {
if (open) {
// this is a hack to get the search input to focus when the dropdown is opened
// the reason is, rc-select does not support putting the search input in the dropdown
// and rc-select focus its native searchinput element on dropdown open, but we need to focus the search input
// so we use a timeout to focus the search input after the dropdown is opened
setTimeout(() => {
if (!searchRef.current) return;
searchRef.current?.focus();
}, 200);
return;
}
setSearchValue("");
};
return (
<RCSelect
{...rest}
@ -64,20 +88,40 @@ function Select(props: SelectProps) {
SelectDropdownClassName + `--${size}`,
dropdownClassName,
)}
dropdownRender={(menu: any) => {
return (
<div>
{showSearch && (
<SearchInput
onChange={setSearchValue}
placeholder="Type to search..."
ref={searchRef}
size="md"
value={searchValue}
/>
)}
<div>{menu}</div>
</div>
);
}}
inputIcon={<InputIcon />}
maxTagCount={maxTagCount}
maxTagPlaceholder={maxTagPlaceholder || getMaxTagPlaceholder}
maxTagTextLength={maxTagTextLength}
menuItemSelectedIcon=""
mode={isMultiSelect ? "multiple" : undefined}
mode={isMultiSelect ? "tags" : undefined}
onDropdownVisibleChange={handleDropdownVisibleChange}
placeholder={placeholder}
searchValue={searchValue}
showArrow
showSearch={showSearch}
tagRender={(props) => {
if (rest.tagRender) {
return rest.tagRender(props);
}
const { closable, label, onClose } = props;
return (
<Tag isClosable={closable} onClose={onClose}>
<Tag isClosable={closable} kind="info" onClose={onClose}>
{label}
</Tag>
);

View File

@ -4,10 +4,12 @@
width: 100px;
position: relative;
}
.rc-select-disabled,
.rc-select-disabled input {
cursor: not-allowed;
}
.rc-select-show-arrow.rc-select-loading .rc-select-arrow-icon::after {
box-sizing: border-box;
width: 12px;
@ -20,27 +22,35 @@
margin-top: 4px;
animation: rcSelectLoadingIcon 0.5s infinite;
}
.rc-select .rc-select-selection-placeholder {
opacity: 0.4;
pointer-events: none;
}
.rc-select .rc-select-selection-search-input {
appearance: none;
opacity: 0;
}
.rc-select .rc-select-selection-search-input::-webkit-search-cancel-button {
display: none;
appearance: none;
}
.rc-select-single .rc-select-selector {
display: flex;
position: relative;
}
.rc-select-single .rc-select-selector .rc-select-selection-search {
width: 100%;
}
.rc-select-single .rc-select-selector .rc-select-selection-search-input {
width: 100%;
}
.rc-select-single .rc-select-selector .rc-select-selection-item,
.rc-select-single .rc-select-selector .rc-select-selection-placeholder {
position: absolute;
@ -48,6 +58,7 @@
left: 3px;
pointer-events: none;
}
/* Selects the currently selected item when in the input box*/
.rc-select-single .rc-select-selector .rc-select-selection-item,
.rc-select-single .rc-select-selector .rc-select-selection-placeholder,
@ -55,6 +66,7 @@
overflow: hidden;
text-overflow: ellipsis;
}
.rc-select-single:not(.rc-select-customize-input)
.rc-select-selector
.rc-select-selection-search-input {
@ -63,12 +75,14 @@
width: 100%;
padding: 0;
}
.rc-select-multiple .rc-select-selector {
display: flex;
flex-wrap: wrap;
padding: 1px;
border: 1px solid #000;
}
.rc-select-multiple .rc-select-selector .rc-select-selection-item {
flex: none;
background: #bbb;
@ -76,28 +90,34 @@
margin-right: 2px;
padding: 0 8px;
}
.rc-select-multiple .rc-select-selector .rc-select-selection-item-disabled {
cursor: not-allowed;
opacity: 0.5;
}
.rc-select-multiple .rc-select-selector .rc-select-selection-overflow {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.rc-select-multiple .rc-select-selector .rc-select-selection-overflow-item {
flex: none;
max-width: 100%;
}
.rc-select-multiple .rc-select-selector .rc-select-selection-search {
position: relative;
max-width: 100%;
}
.rc-select-multiple .rc-select-selector .rc-select-selection-search-input,
.rc-select-multiple .rc-select-selector .rc-select-selection-search-mirror {
padding: 1px;
font-family: var(--ads-v2-font-family);
}
.rc-select-multiple .rc-select-selector .rc-select-selection-search-mirror {
position: absolute;
z-index: 999;
@ -107,29 +127,35 @@
top: 0;
visibility: hidden;
}
.rc-select-multiple .rc-select-selector .rc-select-selection-search-input {
border: none;
outline: none;
background: rgba(255, 0, 0, 0.2);
width: 100%;
}
.rc-select-allow-clear.rc-select-multiple .rc-select-selector {
padding-right: 20px;
}
.rc-select-allow-clear .rc-select-clear {
position: absolute;
right: 20px;
top: 0;
}
.rc-select-show-arrow.rc-select-multiple .rc-select-selector {
padding-right: 20px;
}
.rc-select-show-arrow .rc-select-arrow {
pointer-events: none;
position: absolute;
right: 5px;
top: 0;
}
.rc-select-show-arrow .rc-select-arrow-icon::after {
content: "";
border: 5px solid transparent;
@ -145,60 +171,75 @@
position: absolute;
background: #fff;
}
.rc-select-dropdown-hidden {
display: none;
}
.rc-select-item {
font-size: 16px;
line-height: 1.5;
padding: 4px 16px;
}
.rc-select-item-group {
color: #999;
font-weight: bold;
font-size: 80%;
}
.rc-select-item-option {
position: relative;
}
.rc-select-item-option-grouped {
padding-left: 24px;
}
.rc-select-item-option .rc-select-item-option-state {
position: absolute;
right: 0;
top: 4px;
pointer-events: none;
}
.rc-select-item-option-active {
background: #ddd;
}
.rc-select-item-option-disabled {
color: #999;
}
.rc-select-item-empty {
text-align: center;
color: #999;
}
.rc-select-selection__choice-zoom {
transition: all 0.3s;
}
.rc-select-selection__choice-zoom-appear {
opacity: 0;
transform: scale(0.5);
}
.rc-select-selection__choice-zoom-appear.rc-select-selection__choice-zoom-appear-active {
opacity: 1;
transform: scale(1);
}
.rc-select-selection__choice-zoom-leave {
opacity: 1;
transform: scale(1);
}
.rc-select-selection__choice-zoom-leave.rc-select-selection__choice-zoom-leave-active {
opacity: 0;
transform: scale(0.5);
}
.rc-select-dropdown-slide-up-enter,
.rc-select-dropdown-slide-up-appear {
animation-duration: 0.3s;
@ -208,6 +249,7 @@
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
animation-play-state: paused;
}
.rc-select-dropdown-slide-up-leave {
animation-duration: 0.3s;
animation-fill-mode: both;
@ -216,6 +258,7 @@
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
animation-play-state: paused;
}
.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-bottomLeft,
.rc-select-dropdown-slide-up-appear.rc-select-dropdown-slide-up-appear-active.rc-select-dropdown-placement-bottomLeft,
.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-bottomRight,
@ -223,11 +266,13 @@
animation-name: rcSelectDropdownSlideUpIn;
animation-play-state: running;
}
.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-bottomLeft,
.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-bottomRight {
animation-name: rcSelectDropdownSlideUpOut;
animation-play-state: running;
}
.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-topLeft,
.rc-select-dropdown-slide-up-appear.rc-select-dropdown-slide-up-appear-active.rc-select-dropdown-placement-topLeft,
.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-topRight,
@ -235,71 +280,84 @@
animation-name: rcSelectDropdownSlideDownIn;
animation-play-state: running;
}
.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-topLeft,
.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-topRight {
animation-name: rcSelectDropdownSlideDownOut;
animation-play-state: running;
}
.rc-virtual-list-scrollbar {
width: 5px !important;
height: 5px !important;
}
.rc-virtual-list-scrollbar .rc-virtual-list-scrollbar-thumb {
background-color: var(--ads-v2-color-bg-emphasis) !important;
border-radius: 36px;
}
@keyframes rcSelectDropdownSlideUpIn {
0% {
opacity: 0;
transform-origin: 0% 0%;
transform: scaleY(0);
}
100% {
opacity: 1;
transform-origin: 0% 0%;
transform: scaleY(1);
}
}
@keyframes rcSelectDropdownSlideUpOut {
0% {
opacity: 1;
transform-origin: 0% 0%;
transform: scaleY(1);
}
100% {
opacity: 0;
transform-origin: 0% 0%;
transform: scaleY(0);
}
}
@keyframes rcSelectDropdownSlideDownIn {
0% {
transform: scaleY(0);
transform-origin: 100% 100%;
opacity: 0;
}
100% {
transform: scaleY(1);
transform-origin: 100% 100%;
opacity: 1;
}
}
@keyframes rcSelectDropdownSlideDownOut {
0% {
transform: scaleY(1);
transform-origin: 100% 100%;
opacity: 1;
}
100% {
transform: scaleY(0);
transform-origin: 100% 100%;
opacity: 0;
}
}
@keyframes rcSelectLoadingIcon {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}

View File

@ -19,7 +19,7 @@
}
.ads-v2-select.rc-select-show-search * {
cursor: text;
cursor: unset;
}
/* size sm */
@ -187,7 +187,10 @@
display: flex;
font-size: 12px;
align-items: center;
background: var(--ads-v2-colors-control-pill-default-bg);
background: var(--ads-v2-colors-content-surface-info-bg);
border: 1px solid var(--ads-v2-colors-content-surface-info-border);
color: var(--ads-v2-colors-content-label-info-fg);
line-height: normal;
}
/* typing space */
@ -208,11 +211,10 @@
border-radius: var(--ads-v2-border-radius);
border: 1px solid var(--ads-v2-colors-content-container-default-border);
box-shadow: var(--ads-v2-shadow-popovers);
padding: var(--ads-v2-spaces-2);
padding: 0;
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
overflow: auto;
z-index: 1001;
pointer-events: auto;
}
@ -226,15 +228,29 @@
--select-option-height: 36px;
padding: var(--select-option-padding);
margin-bottom: var(--ads-v2-spaces-1);
margin-inline: var(--ads-v2-spaces-2);
border-radius: var(--ads-v2-border-radius);
cursor: pointer;
/* TODO: remove !important after WDS fix their issue in tree select */
background-color: var(--select-option-color-bg) !important;
position: relative;
color: var(--ads-v2-colors-content-label-default-fg);
min-height: var(--select-option-height);
box-sizing: border-box;
display: flex;
align-items: center;
background-color: var(--select-option-color-bg);
}
.ads-v2-select__dropdown .rc-virtual-list {
padding-top: var(--ads-v2-spaces-2);
padding-bottom: var(--ads-v2-spaces-2);
}
/* if the dropdown first item is a group, dont add padding-top to virtual list */
.ads-v2-select__dropdown:has(
.rc-select-item:first-child:is(.rc-select-item-group)
)
.rc-virtual-list {
padding-top: 0;
}
/* Option group */
@ -249,8 +265,6 @@
border-radius: var(--ads-v2-border-radius);
font-weight: 500;
font-size: var(--ads-v2-font-size-4);
/* TODO: remove !important after WDS fix their issue in tree select */
background-color: var(--select-option-color-bg) !important;
position: relative;
color: var(--ads-v2-colors-content-label-default-fg);
min-height: var(--select-option-height);
@ -267,12 +281,9 @@
--select-option-height: 36px;
padding: var(--select-option-padding);
padding-left: var(--ads-v2-spaces-5);
margin-bottom: var(--ads-v2-spaces-1);
margin-left: var(--ads-v2-spaces-2);
border-radius: var(--ads-v2-border-radius);
cursor: pointer;
/* TODO: remove !important after WDS fix their issue in tree select */
background-color: var(--select-option-color-bg) !important;
position: relative;
color: var(--ads-v2-colors-content-label-default-fg);
min-height: var(--select-option-height);
@ -306,6 +317,18 @@
font-size: var(--select-option-font-size);
overflow: hidden;
overflow-wrap: break-word;
width: 100%;
}
.ads-v2-select__dropdown
.rc-select-item.rc-select-item-option
.rc-select-item-option-content
> label
> span {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Option hover */
@ -314,6 +337,13 @@
.rc-select-item.rc-select-item-option.rc-select-item-option-active {
--select-option-color-bg: var(--ads-v2-colors-content-surface-hover-bg);
outline: none;
--select-option-color-bg: var(--ads-v2-colors-content-surface-hover-bg);
}
/* selected */
.ads-v2-select__dropdown
.rc-select-item.rc-select-item-option.rc-select-item-option-selected {
--select-option-color-bg: var(--ads-v2-colors-content-surface-active-bg);
}
/* Option focus */
@ -328,11 +358,6 @@
outline-offset: var(--ads-v2-offset-outline);
}
/* Option active */
.ads-v2-select__dropdown .rc-select-item.rc-select-item-option:active {
--select-option-color-bg: var(--ads-v2-colors-content-surface-active-bg);
}
/* Option disabled */
.ads-v2-select__dropdown
.rc-select-item.rc-select-item-option.rc-select-item-option-disabled {
@ -348,8 +373,42 @@
color: var(--ads-v2-colors-content-helper-default-fg);
}
/* Selected option */
.ads-v2-select__dropdown
.rc-select-item.rc-select-item-option.rc-select-item-option-selected {
--select-option-color-bg: var(--ads-v2-colors-content-surface-active-bg);
/* search input */
.ads-v2-select__dropdown .ads-v2-search-input input {
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid var(--ads-v2-color-border-emphasis);
border-radius: 0;
outline: none !important;
}
/* this is required because we want to set the max width of first tag around 60% of the container width */
.ads-v2-select .rc-select-selector {
container-type: inline-size;
}
/* tags */
.ads-v2-select
.rc-select-selection-overflow-item:first-child
.ads-v2-tag
> span {
max-width: calc(60cqw - 16px);
}
.ads-v2-select .rc-select-selection-overflow-item .ads-v2-tag > span {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
max-width: calc(100% - 16px);
}
.ads-v2-select .rc-select-selection-overflow-item-suffix {
display: none;
}
.ads-v2-select__dropdown .rc-virtual-list {
padding-top: var(--ads-v2-spaces-2);
padding-bottom: var(--ads-v2-spaces-2);
}

View File

@ -78,6 +78,10 @@ export const StyledTag = styled.span<{
display: flex;
align-items: center;
& > span {
line-height: normal;
}
${({ isClosed }) => isClosed && `display: none;`}
`;
@ -85,9 +89,8 @@ export const StyledButton = styled(Button)`
--button-color-fg: var(--tag-color-fg);
--button-color-bg: inherit;
margin-left: var(--ads-v2-spaces-1);
margin-left: var(--ads-v2-spaces-2);
position: relative;
top: 1px; // align with text
cursor: pointer;
&:hover:not([data-disabled="true"]):not([data-loading="true"]) {

View File

@ -24,7 +24,7 @@ function Tag({
};
return (
<StyledTag isClosed={isClosed} kind={kind} {...rest}>
<StyledTag className="ads-v2-tag" isClosed={isClosed} kind={kind} {...rest}>
<Text color="inherit" kind="body-s">
{children}
</Text>
@ -36,6 +36,7 @@ function Tag({
isIconButton
kind="tertiary"
onClick={closeHandler}
onMouseDown={(e) => e.stopPropagation()}
size="sm"
startIcon="close-line"
/>

View File

@ -4,7 +4,7 @@ import type { Sizes } from "../__config__/types";
export type TagSizes = Extract<Sizes, "sm" | "md">;
// TODO: Update this to include "Kind" from __config__/types
export type TagKind = "neutral" | "special" | "premium";
export type TagKind = "neutral" | "special" | "premium" | "info";
export type TagProps = {
/** the size of the tag */

View File

@ -35,10 +35,6 @@ function TableOrSpreadsheetDropdown() {
</LabelWrapper>
<Select
data-testid="t--one-click-binding-table-selector"
dropdownStyle={{
minWidth: "350px",
maxHeight: "300px",
}}
isDisabled={disabled}
isLoading={isLoading}
isValid={!error}

View File

@ -58,6 +58,7 @@ const dropDownProps = {
isValid: true,
formValues: mockAction,
isLoading: false,
maxTagCount: 3,
};
describe("DropDownControl", () => {

View File

@ -310,6 +310,7 @@ function renderDropdown(
isDisabled={props.disabled}
isLoading={props.isLoading}
isMultiSelect={props?.isMultiSelect}
maxTagCount={props.maxTagCount}
onClear={clearAllOptions}
onDeselect={onRemoveOptions}
onSelect={(value) => onSelectOptions(value)}
@ -336,6 +337,7 @@ function renderOptionWithIcon(option: SelectOptionProps) {
aria-label={option.label}
disabled={option.disabled}
isDisabled={option.isDisabled}
label={option.label}
value={option.value}
>
{option.icon && <Icon color={option.color} name={option.icon} />}
@ -362,6 +364,7 @@ export interface DropDownControlProps extends ControlProps {
isLoading: boolean;
formValues: Partial<Action>;
setFirstOptionAsDefault?: boolean;
maxTagCount?: number;
}
interface ReduxDispatchProps {

View File

@ -43,7 +43,7 @@ function ThemeFontControl(props: ThemeFontControlProps) {
value={selectedOption}
>
{options.map((option, index) => (
<Option key={index} value={option}>
<Option key={index} label={option} value={option}>
<div className="flex space-x-2 w-full cursor-pointer items-center">
<FontText className="flex items-center justify-center">
Aa