diff --git a/packages/pluggableWidgets/combobox-web/CHANGELOG.md b/packages/pluggableWidgets/combobox-web/CHANGELOG.md index ae917dccb7..6ce3264a24 100644 --- a/packages/pluggableWidgets/combobox-web/CHANGELOG.md +++ b/packages/pluggableWidgets/combobox-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed + +- We improved behaviour of single selection with custom content. + ## [2.6.2] - 2025-10-29 ### Fixed diff --git a/packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js-snapshots/comboBoxRemoveAllSelection-chromium-linux.png b/packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js-snapshots/comboBoxRemoveAllSelection-chromium-linux.png index bd463f8f04..9704b9f5cf 100644 Binary files a/packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js-snapshots/comboBoxRemoveAllSelection-chromium-linux.png and b/packages/pluggableWidgets/combobox-web/e2e/Combobox.spec.js-snapshots/comboBoxRemoveAllSelection-chromium-linux.png differ diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx b/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx index 3b7f257318..88523b477d 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx @@ -21,6 +21,12 @@ async function getToggleButton(component: RenderResult): Promise { async function getInput(component: RenderResult): Promise { return (await component.findByRole("combobox")) as HTMLInputElement; } +async function getVisibleValueNode(component: RenderResult): Promise { + const custom = component.container.querySelector(".widget-combobox-placeholder-custom") as HTMLDivElement; + const text = component.container.querySelector(".widget-combobox-placeholder-text") as HTMLDivElement; + + return custom ?? text; +} describe("Combo box (Association)", () => { beforeAll(() => { @@ -108,12 +114,13 @@ describe("Combo box (Association)", () => { }); it("sets option to selected item", async () => { const component = render(); - const input = await getInput(component); + const value = await getVisibleValueNode(component); const toggleButton = await getToggleButton(component); fireEvent.click(toggleButton); const option1 = await component.findByText("obj_222"); fireEvent.click(option1); - expect(input.value).toEqual("obj_222"); + component.rerender(); + expect(value.textContent).toEqual("obj_222"); expect(defaultProps.attributeAssociation?.setValue).toHaveBeenCalled(); expect(component.queryAllByRole("option")).toHaveLength(0); expect(defaultProps.attributeAssociation?.value).toEqual({ id: "obj_222" }); @@ -121,17 +128,16 @@ describe("Combo box (Association)", () => { it("removes selected item", async () => { const component = render(); - const input = await getInput(component); - const labelText = await component.container.querySelector( - ".widget-combobox-placeholder-text .widget-combobox-caption-text" - ); const toggleButton = await getToggleButton(component); fireEvent.click(toggleButton); const option1 = await component.findByText("obj_222"); fireEvent.click(option1); - expect(input.value).toEqual("obj_222"); + component.rerender(); + + const value = await getVisibleValueNode(component); + expect(value.textContent).toEqual("obj_222"); expect(defaultProps.attributeAssociation?.setValue).toHaveBeenCalled(); expect(component.queryAllByRole("option")).toHaveLength(0); expect(defaultProps.attributeAssociation?.value).toEqual({ id: "obj_222" }); @@ -139,6 +145,11 @@ describe("Combo box (Association)", () => { const clearButton = await component.container.getElementsByClassName("widget-combobox-clear-button")[0]; fireEvent.click(clearButton); + component.rerender(); + + const labelText = await component.container.querySelector( + ".widget-combobox-placeholder-text .widget-combobox-caption-text" + ); expect(labelText?.innerHTML).toEqual(defaultProps.emptyOptionText?.value); expect(defaultProps.attributeAssociation?.value).toEqual(undefined); }); diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx b/packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx index a560f15de9..09d2565455 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx @@ -16,8 +16,16 @@ import Combobox from "../Combobox"; async function getToggleButton(component: RenderResult): Promise { return component.container.querySelector(".widget-combobox-down-arrow")!; } -async function getInput(component: RenderResult): Promise { - return (await component.findByRole("combobox")) as HTMLInputElement; + +async function getVisibleValueNode(component: RenderResult): Promise { + const custom = component.container.querySelector(".widget-combobox-placeholder-custom") as HTMLDivElement; + const text = component.container.querySelector(".widget-combobox-placeholder-text") as HTMLDivElement; + + return custom ?? text; +} + +async function findOptions(component: RenderResult): Promise { + return component.findAllByRole("option"); } describe("Combo box (Static values)", () => { @@ -89,8 +97,8 @@ describe("Combo box (Static values)", () => { }); it("renders combobox widget with selected value", async () => { const component = render(); - const input = await getInput(component); - expect(input.value).toEqual("caption1"); + const value = await getVisibleValueNode(component); + expect(value.textContent).toEqual("caption1"); }); it("toggles combobox menu on: input TOGGLE BUTTON", async () => { @@ -107,24 +115,29 @@ describe("Combo box (Static values)", () => { }); it("sets option to selected item", async () => { const component = render(); - const input = await getInput(component); + const value = await getVisibleValueNode(component); + expect(value.textContent).toEqual("caption1"); const toggleButton = await getToggleButton(component); fireEvent.click(toggleButton); - const option1 = await component.findByText("caption2"); - fireEvent.click(option1); - expect(input.value).toEqual("caption2"); + const options = await findOptions(component); + fireEvent.click(options[1]); + + component.rerender(); + + const value2 = await getVisibleValueNode(component); + expect(value2.textContent).toEqual("caption2"); expect(defaultProps.staticAttribute?.setValue).toHaveBeenCalled(); expect(component.queryAllByRole("option")).toHaveLength(0); expect(defaultProps.staticAttribute?.value).toEqual("value2"); }); it("removes selected item", async () => { const component = render(); - const input = await getInput(component); const toggleButton = await getToggleButton(component); fireEvent.click(toggleButton); - const options = await component.findAllByText("caption1"); - fireEvent.click(options[1]); - expect(input.value).toEqual("caption1"); + const options = await findOptions(component); + fireEvent.click(options[0]); + const value = await getVisibleValueNode(component); + expect(value.textContent).toEqual("caption1"); expect(component.queryAllByRole("option")).toHaveLength(0); expect(defaultProps.staticAttribute.value).toEqual("value1"); const clearButton = await component.container.getElementsByClassName("widget-combobox-clear-button")[0]; diff --git a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap index 21eff47ec7..df6f233d70 100644 --- a/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap +++ b/packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap @@ -23,9 +23,10 @@ exports[`Combo box (Association) renders combobox widget 1`] = ` class="widget-combobox-input" id="comboBox1" placeholder=" " + readonly="" role="combobox" tabindex="0" - value="obj_111" + value="" />
, actionAndChanges: UseComboboxStateChangeOptions) { const { changes, type } = actionAndChanges; switch (type) { + // clear input when user toggles (closes) dropdown. case useCombobox.stateChangeTypes.ToggleButtonClick: return { ...changes, - inputValue: - state.isOpen && selector.currentId ? selector.caption.get(selector.currentId) : "" + inputValue: "" }; + // when item is selected, downshift fills in input automatically, prevent that. + case useCombobox.stateChangeTypes.FunctionSelectItem: + case useCombobox.stateChangeTypes.ItemClick: case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem: + case useCombobox.stateChangeTypes.InputKeyDownEnter: return { ...changes, - inputValue: - changes.inputValue === selector.caption.emptyCaption - ? "" - : selector.caption.get(selector.currentId) + inputValue: "" }; case useCombobox.stateChangeTypes.InputFocus: @@ -88,15 +89,13 @@ export function useDownshiftSingleSelectProps( highlightedIndex: changes.selectedItem ? -1 : this.defaultHighlightedIndex }; + // clear input when user want to close the popup with escape (or it was closed programmatically) case useCombobox.stateChangeTypes.InputKeyDownEscape: case useCombobox.stateChangeTypes.FunctionCloseMenu: return { ...changes, isOpen: false, - inputValue: - changes.selectedItem || selector.currentId - ? selector.caption.get(selector.currentId) - : "" + inputValue: "" }; case useCombobox.stateChangeTypes.InputBlur: return state; diff --git a/packages/pluggableWidgets/combobox-web/src/ui/Combobox.scss b/packages/pluggableWidgets/combobox-web/src/ui/Combobox.scss index dffe8d97d6..f4d681c7fb 100644 --- a/packages/pluggableWidgets/combobox-web/src/ui/Combobox.scss +++ b/packages/pluggableWidgets/combobox-web/src/ui/Combobox.scss @@ -165,14 +165,27 @@ $cb-skeleton-dark: #d2d2d2; flex-grow: 1; border: none; padding: 0; - width: 100%; - &:not(:focus) { - opacity: 0; + // when there is no filter or the input is readonly + // collapse input so it it not visible + &-nofilter, + &:placeholder-shown:not(:focus) { + max-width: 0; + } + + &:placeholder-shown:focus:not(&-nofilter):has(+ .widget-combobox-placeholder-empty) { + max-width: 3px; + margin-right: -3px; } + } - &-nofilter { - cursor: pointer; + .widget-combobox-selected-items:not(.widget-combobox-boxes) { + .widget-combobox-input { + // when focused but empty - allow to show only cursor + &:placeholder-shown:focus:not(&-nofilter) { + max-width: 3px; + margin-right: -3px; + } } } @@ -233,11 +246,8 @@ $cb-skeleton-dark: #d2d2d2; &-text { color: var(--cb-text-color, var(--gray-darker, $cb-typography-color)); - position: absolute; inset-inline-start: 0; inset-inline-end: 0; - top: 0; - bottom: 0; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -269,7 +279,7 @@ $cb-skeleton-dark: #d2d2d2; inset-inline-end: 0; bottom: 0; } - &:focus:not(:placeholder-shown) + .widget-combobox-placeholder-custom { + &:not(:placeholder-shown) + .widget-combobox-placeholder-custom { display: none; } } @@ -294,7 +304,8 @@ $cb-skeleton-dark: #d2d2d2; input:placeholder-shown, input:not(:focus) { & + .widget-combobox-placeholder-text { - display: block; + display: flex; + align-items: center; } } }