Skip to content

Commit a4dc1e5

Browse files
Added ComboBoxPopup component
1 parent e142967 commit a4dc1e5

File tree

4 files changed

+355
-0
lines changed

4 files changed

+355
-0
lines changed

src/ComboBoxPopup.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Widgets } from "@farjs/blessed";
2+
import { ListViewport } from "./ListViewport";
3+
4+
export interface ComboBoxPopupProps {
5+
readonly left: number;
6+
readonly top: number;
7+
readonly width: number;
8+
readonly items: string[];
9+
readonly viewport: ListViewport;
10+
setViewport(viewport: ListViewport): void;
11+
readonly style: Widgets.Types.TStyle;
12+
onClick(index: number): void;
13+
}

src/ComboBoxPopup.mjs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @typedef {import("./ComboBoxPopup").ComboBoxPopupProps} ComboBoxPopupProps
3+
*/
4+
import React from "react";
5+
import SingleBorder from "./border/SingleBorder.mjs";
6+
import ListView from "./ListView.mjs";
7+
import ScrollBar from "./ScrollBar.mjs";
8+
9+
const h = React.createElement;
10+
11+
/**
12+
* @param {ComboBoxPopupProps} props
13+
*/
14+
const ComboBoxPopup = (props) => {
15+
const { singleBorderComp, listViewComp, scrollBarComp } = ComboBoxPopup;
16+
17+
const width = props.width;
18+
const height = ComboBoxPopup.maxItems + 2;
19+
const viewWidth = width - 2;
20+
const theme = props.style;
21+
const viewport = props.viewport;
22+
23+
return h(
24+
"box",
25+
{
26+
clickable: true,
27+
autoFocus: false,
28+
width: width,
29+
height: height,
30+
left: props.left,
31+
top: props.top,
32+
onWheelup: () => {
33+
props.setViewport(viewport.up());
34+
},
35+
onWheeldown: () => {
36+
props.setViewport(viewport.down());
37+
},
38+
style: theme,
39+
},
40+
41+
h(singleBorderComp, {
42+
width: width,
43+
height: height,
44+
style: theme,
45+
}),
46+
47+
h(listViewComp, {
48+
left: 1,
49+
top: 1,
50+
width: viewWidth,
51+
height: height - 2,
52+
items: props.items.map(
53+
(i) => ` ${i.slice(0, Math.min(viewWidth - 4, i.length))} `
54+
),
55+
viewport: viewport,
56+
setViewport: props.setViewport,
57+
style: theme,
58+
onClick: props.onClick,
59+
}),
60+
61+
viewport.length > viewport.viewLength
62+
? h(scrollBarComp, {
63+
left: width - 1,
64+
top: 1,
65+
length: viewport.viewLength,
66+
style: theme,
67+
value: viewport.offset,
68+
extent: viewport.viewLength,
69+
min: 0,
70+
max: viewport.length - viewport.viewLength,
71+
onChange: (offset) => {
72+
props.setViewport(viewport.updated(offset));
73+
},
74+
})
75+
: null
76+
);
77+
};
78+
79+
ComboBoxPopup.displayName = "ComboBoxPopup";
80+
ComboBoxPopup.singleBorderComp = SingleBorder;
81+
ComboBoxPopup.listViewComp = ListView;
82+
ComboBoxPopup.scrollBarComp = ScrollBar;
83+
84+
ComboBoxPopup.maxItems = 8;
85+
86+
export default ComboBoxPopup;

test/ComboBoxPopup.test.mjs

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/**
2+
* @typedef {import('../src/ListViewport').ListViewport} ListViewport
3+
* @typedef {import('../src/ComboBoxPopup').ComboBoxPopupProps} ComboBoxPopupProps
4+
*/
5+
import React from "react";
6+
import TestRenderer from "react-test-renderer";
7+
import { assertComponents, mockComponent } from "react-assert";
8+
import assert from "node:assert/strict";
9+
import mockFunction from "mock-fn";
10+
import DefaultTheme from "../src/theme/DefaultTheme.mjs";
11+
import { createListViewport } from "../src/ListViewport.mjs";
12+
import SingleBorder from "../src/border/SingleBorder.mjs";
13+
import ListView from "../src/ListView.mjs";
14+
import ScrollBar from "../src/ScrollBar.mjs";
15+
import ComboBoxPopup from "../src/ComboBoxPopup.mjs";
16+
17+
const h = React.createElement;
18+
19+
const { describe, it } = await (async () => {
20+
// @ts-ignore
21+
const module = process.isBun ? "bun:test" : "node:test";
22+
// @ts-ignore
23+
return process.isBun // @ts-ignore
24+
? Promise.resolve({ describe: (_, fn) => fn(), it: test })
25+
: import(module);
26+
})();
27+
28+
ComboBoxPopup.singleBorderComp = mockComponent(SingleBorder);
29+
ComboBoxPopup.listViewComp = mockComponent(ListView);
30+
ComboBoxPopup.scrollBarComp = mockComponent(ScrollBar);
31+
const { singleBorderComp, listViewComp, scrollBarComp } = ComboBoxPopup;
32+
33+
describe("ComboBoxPopup.test.mjs", () => {
34+
it("should call setViewport when box.onWheelup", () => {
35+
//given
36+
const setViewport = mockFunction((vp) => {
37+
assertListViewport(
38+
vp,
39+
props.viewport.offset,
40+
focused,
41+
props.viewport.length,
42+
props.viewport.viewLength
43+
);
44+
});
45+
const props = getComboBoxPopupProps({
46+
...defaultProps,
47+
index: 1,
48+
setViewport,
49+
});
50+
const comp = TestRenderer.create(h(ComboBoxPopup, props)).root;
51+
const boxEl = comp.findByType("box");
52+
const focused = 0;
53+
assert.notDeepEqual(props.viewport.focused, focused);
54+
55+
//when
56+
boxEl.props.onWheelup();
57+
58+
//then
59+
assert.deepEqual(setViewport.times, 1);
60+
});
61+
62+
it("should call setViewport when box.onWheeldown", () => {
63+
//given
64+
const setViewport = mockFunction((vp) => {
65+
assertListViewport(
66+
vp,
67+
props.viewport.offset,
68+
focused,
69+
props.viewport.length,
70+
props.viewport.viewLength
71+
);
72+
});
73+
const props = getComboBoxPopupProps({ ...defaultProps, setViewport });
74+
const comp = TestRenderer.create(h(ComboBoxPopup, props)).root;
75+
const boxEl = comp.findByType("box");
76+
const focused = 1;
77+
assert.notDeepEqual(props.viewport.focused, focused);
78+
79+
//when
80+
boxEl.props.onWheeldown();
81+
82+
//then
83+
assert.deepEqual(setViewport.times, 1);
84+
});
85+
86+
it("should call setViewport when onChange in ScrollBar", () => {
87+
//given
88+
const setViewport = mockFunction((vp) => {
89+
assertListViewport(
90+
vp,
91+
offset,
92+
props.viewport.focused,
93+
props.viewport.length,
94+
props.viewport.viewLength
95+
);
96+
});
97+
const props = getComboBoxPopupProps({
98+
...defaultProps,
99+
items: new Array(15).fill("item"),
100+
setViewport,
101+
});
102+
assert.deepEqual(props.items.length > ComboBoxPopup.maxItems, true);
103+
const comp = TestRenderer.create(h(ComboBoxPopup, props)).root;
104+
const scrollBar = comp.findByType(scrollBarComp);
105+
const offset = 1;
106+
assert.notDeepEqual(props.viewport.offset, offset);
107+
108+
//when
109+
scrollBar.props.onChange(offset);
110+
111+
//then
112+
assert.deepEqual(setViewport.times, 1);
113+
});
114+
115+
it("should render without ScrollBar", () => {
116+
//given
117+
const props = getComboBoxPopupProps();
118+
119+
//when
120+
const result = TestRenderer.create(h(ComboBoxPopup, props)).root;
121+
122+
//then
123+
assertComboBoxPopup(result, props, false);
124+
});
125+
126+
it("should render with ScrollBar", () => {
127+
//given
128+
const props = getComboBoxPopupProps({
129+
...defaultProps,
130+
items: new Array(15).fill("item"),
131+
});
132+
133+
//when
134+
const result = TestRenderer.create(h(ComboBoxPopup, props)).root;
135+
136+
//then
137+
assertComboBoxPopup(result, props, true);
138+
});
139+
});
140+
141+
/**
142+
* @typedef {{
143+
* index: number,
144+
* items: string[],
145+
* setViewport(viewport: ListViewport): void,
146+
* onClick(index: number): void
147+
* }} DefaultProps
148+
* @type {DefaultProps}
149+
*/
150+
const defaultProps = {
151+
index: 0,
152+
items: ["item 1", "item 2"],
153+
setViewport: () => {},
154+
onClick: () => {},
155+
};
156+
157+
/**
158+
* @param {DefaultProps} props
159+
* @returns {ComboBoxPopupProps}
160+
*/
161+
function getComboBoxPopupProps(props = defaultProps) {
162+
return {
163+
items: props.items,
164+
left: 1,
165+
top: 2,
166+
width: 11,
167+
viewport: createListViewport(
168+
props.index,
169+
props.items.length,
170+
ComboBoxPopup.maxItems
171+
),
172+
setViewport: props.setViewport,
173+
style: DefaultTheme.popup.menu,
174+
onClick: props.onClick,
175+
};
176+
}
177+
178+
/**
179+
* @param {ListViewport} result
180+
* @param {number} offset
181+
* @param {number} focused
182+
* @param {number} length
183+
* @param {number} viewLength
184+
*/
185+
function assertListViewport(result, offset, focused, length, viewLength) {
186+
assert.deepEqual(result.offset, offset);
187+
assert.deepEqual(result.focused, focused);
188+
assert.deepEqual(result.length, length);
189+
assert.deepEqual(result.viewLength, viewLength);
190+
}
191+
192+
/**
193+
* @param {TestRenderer.ReactTestInstance} result
194+
* @param {ComboBoxPopupProps} props
195+
* @param {boolean} showScrollBar
196+
*/
197+
function assertComboBoxPopup(result, props, showScrollBar) {
198+
assert.deepEqual(ComboBoxPopup.displayName, "ComboBoxPopup");
199+
200+
const width = props.width;
201+
const height = ComboBoxPopup.maxItems + 2;
202+
const viewWidth = width - 2;
203+
const theme = props.style;
204+
205+
assertComponents(
206+
result.children,
207+
h(
208+
"box",
209+
{
210+
clickable: true,
211+
autoFocus: false,
212+
width: width,
213+
height: height,
214+
left: props.left,
215+
top: props.top,
216+
style: theme,
217+
},
218+
...[
219+
h(singleBorderComp, {
220+
width: width,
221+
height: height,
222+
style: theme,
223+
}),
224+
225+
h(listViewComp, {
226+
left: 1,
227+
top: 1,
228+
width: viewWidth,
229+
height: height - 2,
230+
items: props.items.map(
231+
(i) => ` ${i.slice(0, Math.min(viewWidth - 4, i.length))} `
232+
),
233+
viewport: props.viewport,
234+
setViewport: props.setViewport,
235+
style: theme,
236+
onClick: props.onClick,
237+
}),
238+
239+
showScrollBar
240+
? h(scrollBarComp, {
241+
left: width - 1,
242+
top: 1,
243+
length: ComboBoxPopup.maxItems,
244+
style: theme,
245+
value: 0,
246+
extent: ComboBoxPopup.maxItems,
247+
min: 0,
248+
max: props.items.length - ComboBoxPopup.maxItems,
249+
onChange: () => {},
250+
})
251+
: null,
252+
].filter((h) => h)
253+
)
254+
);
255+
}

test/all.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
await import("./Button.test.mjs");
22
await import("./ButtonsPanel.test.mjs");
33
await import("./CheckBox.test.mjs");
4+
await import("./ComboBoxPopup.test.mjs");
45
await import("./ListBox.test.mjs");
56
await import("./ListView.test.mjs");
67
await import("./ListViewport.test.mjs");

0 commit comments

Comments
 (0)