Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/combobox-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ async function getToggleButton(component: RenderResult): Promise<Element> {
async function getInput(component: RenderResult): Promise<HTMLInputElement> {
return (await component.findByRole("combobox")) as HTMLInputElement;
}
async function getVisibleValueNode(component: RenderResult): Promise<HTMLDivElement> {
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(() => {
Expand Down Expand Up @@ -108,37 +114,42 @@ describe("Combo box (Association)", () => {
});
it("sets option to selected item", async () => {
const component = render(<Combobox {...defaultProps} />);
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(<Combobox {...defaultProps} />);
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" });
});
it("removes selected item", async () => {
const component = render(<Combobox {...defaultProps} />);

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(<Combobox {...defaultProps} />);

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" });

const clearButton = await component.container.getElementsByClassName("widget-combobox-clear-button")[0];
fireEvent.click(clearButton);

component.rerender(<Combobox {...defaultProps} />);

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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@ import Combobox from "../Combobox";
async function getToggleButton(component: RenderResult): Promise<Element> {
return component.container.querySelector(".widget-combobox-down-arrow")!;
}
async function getInput(component: RenderResult): Promise<HTMLInputElement> {
return (await component.findByRole("combobox")) as HTMLInputElement;

async function getVisibleValueNode(component: RenderResult): Promise<HTMLDivElement> {
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<Element[]> {
return component.findAllByRole("option");
}

describe("Combo box (Static values)", () => {
Expand Down Expand Up @@ -89,8 +97,8 @@ describe("Combo box (Static values)", () => {
});
it("renders combobox widget with selected value", async () => {
const component = render(<Combobox {...defaultProps} />);
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 () => {
Expand All @@ -107,24 +115,29 @@ describe("Combo box (Static values)", () => {
});
it("sets option to selected item", async () => {
const component = render(<Combobox {...defaultProps} />);
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(<Combobox {...defaultProps} />);

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(<Combobox {...defaultProps} />);
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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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=""
/>
<div
class="widget-combobox-placeholder-text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ exports[`Combo box (Static values) renders combobox widget 1`] = `
class="widget-combobox-input"
id="comboBox1"
placeholder=" "
readonly=""
role="combobox"
tabindex="0"
value="caption1"
value=""
/>
<div
class="widget-combobox-placeholder-text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function SingleSelection({
const inputProps = getInputProps(
{
disabled: selector.readOnly,
readOnly: selector.options.filterType === "none",
readOnly: selector.options.filterType === "none" || !!selector.currentId,
ref: inputRef,
"aria-required": ariaRequired.value,
"aria-label": !hasLabel && options.ariaLabel ? options.ariaLabel : undefined
Expand All @@ -88,7 +88,10 @@ export function SingleSelection({
>
<input
className={classNames("widget-combobox-input", {
"widget-combobox-input-nofilter": selector.options.filterType === "none"
"widget-combobox-input-nofilter":
selector.options.filterType === "none" ||
selector.readOnly ||
(!selector.clearable && !!selector.currentId)
})}
tabIndex={tabIndex}
{...inputProps}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,21 @@ export function useDownshiftSingleSelectProps(
stateReducer(state: UseComboboxState<string>, actionAndChanges: UseComboboxStateChangeOptions<string>) {
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:
Expand All @@ -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;
Expand Down
31 changes: 21 additions & 10 deletions packages/pluggableWidgets/combobox-web/src/ui/Combobox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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;
}
}
}
Expand Down
Loading