Skip to content

Commit 4e5a071

Browse files
committed
feat: improve single select with custom content
1 parent 92a510d commit 4e5a071

File tree

8 files changed

+79
-45
lines changed

8 files changed

+79
-45
lines changed

packages/pluggableWidgets/combobox-web/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Changed
10+
11+
- We improved behaviour of single selection with custom content.
12+
913
## [2.6.2] - 2025-10-29
1014

1115
### Fixed

packages/pluggableWidgets/combobox-web/src/__tests__/SingleSelection.spec.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ async function getToggleButton(component: RenderResult): Promise<Element> {
2121
async function getInput(component: RenderResult): Promise<HTMLInputElement> {
2222
return (await component.findByRole("combobox")) as HTMLInputElement;
2323
}
24+
async function getVisibleValueNode(component: RenderResult): Promise<HTMLDivElement> {
25+
const custom = component.container.querySelector(".widget-combobox-placeholder-custom") as HTMLDivElement;
26+
const text = component.container.querySelector(".widget-combobox-placeholder-text") as HTMLDivElement;
27+
28+
return custom ?? text;
29+
}
2430

2531
describe("Combo box (Association)", () => {
2632
beforeAll(() => {
@@ -108,37 +114,42 @@ describe("Combo box (Association)", () => {
108114
});
109115
it("sets option to selected item", async () => {
110116
const component = render(<Combobox {...defaultProps} />);
111-
const input = await getInput(component);
117+
const value = await getVisibleValueNode(component);
112118
const toggleButton = await getToggleButton(component);
113119
fireEvent.click(toggleButton);
114120
const option1 = await component.findByText("obj_222");
115121
fireEvent.click(option1);
116-
expect(input.value).toEqual("obj_222");
122+
component.rerender(<Combobox {...defaultProps} />);
123+
expect(value.textContent).toEqual("obj_222");
117124
expect(defaultProps.attributeAssociation?.setValue).toHaveBeenCalled();
118125
expect(component.queryAllByRole("option")).toHaveLength(0);
119126
expect(defaultProps.attributeAssociation?.value).toEqual({ id: "obj_222" });
120127
});
121128
it("removes selected item", async () => {
122129
const component = render(<Combobox {...defaultProps} />);
123130

124-
const input = await getInput(component);
125-
const labelText = await component.container.querySelector(
126-
".widget-combobox-placeholder-text .widget-combobox-caption-text"
127-
);
128131
const toggleButton = await getToggleButton(component);
129132
fireEvent.click(toggleButton);
130133

131134
const option1 = await component.findByText("obj_222");
132135
fireEvent.click(option1);
133136

134-
expect(input.value).toEqual("obj_222");
137+
component.rerender(<Combobox {...defaultProps} />);
138+
139+
const value = await getVisibleValueNode(component);
140+
expect(value.textContent).toEqual("obj_222");
135141
expect(defaultProps.attributeAssociation?.setValue).toHaveBeenCalled();
136142
expect(component.queryAllByRole("option")).toHaveLength(0);
137143
expect(defaultProps.attributeAssociation?.value).toEqual({ id: "obj_222" });
138144

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

148+
component.rerender(<Combobox {...defaultProps} />);
149+
150+
const labelText = await component.container.querySelector(
151+
".widget-combobox-placeholder-text .widget-combobox-caption-text"
152+
);
142153
expect(labelText?.innerHTML).toEqual(defaultProps.emptyOptionText?.value);
143154
expect(defaultProps.attributeAssociation?.value).toEqual(undefined);
144155
});

packages/pluggableWidgets/combobox-web/src/__tests__/StaticSelection.spec.tsx

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,16 @@ import Combobox from "../Combobox";
1616
async function getToggleButton(component: RenderResult): Promise<Element> {
1717
return component.container.querySelector(".widget-combobox-down-arrow")!;
1818
}
19-
async function getInput(component: RenderResult): Promise<HTMLInputElement> {
20-
return (await component.findByRole("combobox")) as HTMLInputElement;
19+
20+
async function getVisibleValueNode(component: RenderResult): Promise<HTMLDivElement> {
21+
const custom = component.container.querySelector(".widget-combobox-placeholder-custom") as HTMLDivElement;
22+
const text = component.container.querySelector(".widget-combobox-placeholder-text") as HTMLDivElement;
23+
24+
return custom ?? text;
25+
}
26+
27+
async function findOptions(component: RenderResult): Promise<Element[]> {
28+
return component.findAllByRole("option");
2129
}
2230

2331
describe("Combo box (Static values)", () => {
@@ -89,8 +97,8 @@ describe("Combo box (Static values)", () => {
8997
});
9098
it("renders combobox widget with selected value", async () => {
9199
const component = render(<Combobox {...defaultProps} />);
92-
const input = await getInput(component);
93-
expect(input.value).toEqual("caption1");
100+
const value = await getVisibleValueNode(component);
101+
expect(value.textContent).toEqual("caption1");
94102
});
95103

96104
it("toggles combobox menu on: input TOGGLE BUTTON", async () => {
@@ -107,24 +115,29 @@ describe("Combo box (Static values)", () => {
107115
});
108116
it("sets option to selected item", async () => {
109117
const component = render(<Combobox {...defaultProps} />);
110-
const input = await getInput(component);
118+
const value = await getVisibleValueNode(component);
119+
expect(value.textContent).toEqual("caption1");
111120
const toggleButton = await getToggleButton(component);
112121
fireEvent.click(toggleButton);
113-
const option1 = await component.findByText("caption2");
114-
fireEvent.click(option1);
115-
expect(input.value).toEqual("caption2");
122+
const options = await findOptions(component);
123+
fireEvent.click(options[1]);
124+
125+
component.rerender(<Combobox {...defaultProps} />);
126+
127+
const value2 = await getVisibleValueNode(component);
128+
expect(value2.textContent).toEqual("caption2");
116129
expect(defaultProps.staticAttribute?.setValue).toHaveBeenCalled();
117130
expect(component.queryAllByRole("option")).toHaveLength(0);
118131
expect(defaultProps.staticAttribute?.value).toEqual("value2");
119132
});
120133
it("removes selected item", async () => {
121134
const component = render(<Combobox {...defaultProps} />);
122-
const input = await getInput(component);
123135
const toggleButton = await getToggleButton(component);
124136
fireEvent.click(toggleButton);
125-
const options = await component.findAllByText("caption1");
126-
fireEvent.click(options[1]);
127-
expect(input.value).toEqual("caption1");
137+
const options = await findOptions(component);
138+
fireEvent.click(options[0]);
139+
const value = await getVisibleValueNode(component);
140+
expect(value.textContent).toEqual("caption1");
128141
expect(component.queryAllByRole("option")).toHaveLength(0);
129142
expect(defaultProps.staticAttribute.value).toEqual("value1");
130143
const clearButton = await component.container.getElementsByClassName("widget-combobox-clear-button")[0];

packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/SingleSelection.spec.tsx.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ exports[`Combo box (Association) renders combobox widget 1`] = `
2020
aria-expanded="false"
2121
aria-required="true"
2222
autocomplete="off"
23-
class="widget-combobox-input"
23+
class="widget-combobox-input widget-combobox-input-item-selected"
2424
id="comboBox1"
2525
placeholder=" "
26+
readonly=""
2627
role="combobox"
2728
tabindex="0"
28-
value="obj_111"
29+
value=""
2930
/>
3031
<div
3132
class="widget-combobox-placeholder-text"

packages/pluggableWidgets/combobox-web/src/__tests__/__snapshots__/StaticSelection.spec.tsx.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ exports[`Combo box (Static values) renders combobox widget 1`] = `
2020
aria-expanded="false"
2121
aria-required="true"
2222
autocomplete="off"
23-
class="widget-combobox-input"
23+
class="widget-combobox-input widget-combobox-input-item-selected"
2424
id="comboBox1"
2525
placeholder=" "
26+
readonly=""
2627
role="combobox"
2728
tabindex="0"
28-
value="caption1"
29+
value=""
2930
/>
3031
<div
3132
class="widget-combobox-placeholder-text"

packages/pluggableWidgets/combobox-web/src/components/SingleSelection/SingleSelection.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function SingleSelection({
6363
const inputProps = getInputProps(
6464
{
6565
disabled: selector.readOnly,
66-
readOnly: selector.options.filterType === "none",
66+
readOnly: selector.options.filterType === "none" || !!selector.currentId,
6767
ref: inputRef,
6868
"aria-required": ariaRequired.value,
6969
"aria-label": !hasLabel && options.ariaLabel ? options.ariaLabel : undefined
@@ -88,7 +88,10 @@ export function SingleSelection({
8888
>
8989
<input
9090
className={classNames("widget-combobox-input", {
91-
"widget-combobox-input-nofilter": selector.options.filterType === "none"
91+
"widget-combobox-input-nofilter":
92+
selector.options.filterType === "none" ||
93+
selector.readOnly ||
94+
(!selector.clearable && !!selector.currentId)
9295
})}
9396
tabIndex={tabIndex}
9497
{...inputProps}

packages/pluggableWidgets/combobox-web/src/hooks/useDownshiftSingleSelectProps.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,21 @@ export function useDownshiftSingleSelectProps(
6464
stateReducer(state: UseComboboxState<string>, actionAndChanges: UseComboboxStateChangeOptions<string>) {
6565
const { changes, type } = actionAndChanges;
6666
switch (type) {
67+
// clear input when user toggles (closes) dropdown.
6768
case useCombobox.stateChangeTypes.ToggleButtonClick:
6869
return {
6970
...changes,
70-
inputValue:
71-
state.isOpen && selector.currentId ? selector.caption.get(selector.currentId) : ""
71+
inputValue: ""
7272
};
7373

74+
// when item is selected, downshift fills in input automatically, prevent that.
75+
case useCombobox.stateChangeTypes.FunctionSelectItem:
76+
case useCombobox.stateChangeTypes.ItemClick:
7477
case useCombobox.stateChangeTypes.ControlledPropUpdatedSelectedItem:
78+
case useCombobox.stateChangeTypes.InputKeyDownEnter:
7579
return {
7680
...changes,
77-
inputValue:
78-
changes.inputValue === selector.caption.emptyCaption
79-
? ""
80-
: selector.caption.get(selector.currentId)
81+
inputValue: ""
8182
};
8283

8384
case useCombobox.stateChangeTypes.InputFocus:
@@ -88,15 +89,13 @@ export function useDownshiftSingleSelectProps(
8889
highlightedIndex: changes.selectedItem ? -1 : this.defaultHighlightedIndex
8990
};
9091

92+
// clear input when user want to close the popup with escape (or it was closed programmatically)
9193
case useCombobox.stateChangeTypes.InputKeyDownEscape:
9294
case useCombobox.stateChangeTypes.FunctionCloseMenu:
9395
return {
9496
...changes,
9597
isOpen: false,
96-
inputValue:
97-
changes.selectedItem || selector.currentId
98-
? selector.caption.get(selector.currentId)
99-
: ""
98+
inputValue: ""
10099
};
101100
case useCombobox.stateChangeTypes.InputBlur:
102101
return state;

packages/pluggableWidgets/combobox-web/src/ui/Combobox.scss

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,18 @@ $cb-skeleton-dark: #d2d2d2;
165165
flex-grow: 1;
166166
border: none;
167167
padding: 0;
168-
width: 100%;
169168

170-
&:not(:focus) {
171-
opacity: 0;
169+
// when there is no filter or the input is readonly
170+
// collapse input so it it not visible
171+
&-nofilter,
172+
&:placeholder-shown:not(:focus) {
173+
max-width: 0;
172174
}
173175

174-
&-nofilter {
175-
cursor: pointer;
176+
// when focused but empty - allow to show only cursor
177+
&:placeholder-shown:focus:not(&-nofilter) {
178+
max-width: 3px;
179+
margin-right: -3px;
176180
}
177181
}
178182

@@ -233,11 +237,8 @@ $cb-skeleton-dark: #d2d2d2;
233237

234238
&-text {
235239
color: var(--cb-text-color, var(--gray-darker, $cb-typography-color));
236-
position: absolute;
237240
inset-inline-start: 0;
238241
inset-inline-end: 0;
239-
top: 0;
240-
bottom: 0;
241242
text-overflow: ellipsis;
242243
overflow: hidden;
243244
white-space: nowrap;
@@ -269,7 +270,7 @@ $cb-skeleton-dark: #d2d2d2;
269270
inset-inline-end: 0;
270271
bottom: 0;
271272
}
272-
&:focus:not(:placeholder-shown) + .widget-combobox-placeholder-custom {
273+
&:not(:placeholder-shown) + .widget-combobox-placeholder-custom {
273274
display: none;
274275
}
275276
}
@@ -294,7 +295,8 @@ $cb-skeleton-dark: #d2d2d2;
294295
input:placeholder-shown,
295296
input:not(:focus) {
296297
& + .widget-combobox-placeholder-text {
297-
display: block;
298+
display: flex;
299+
align-items: center;
298300
}
299301
}
300302
}

0 commit comments

Comments
 (0)