Skip to content

Commit

Permalink
Added ComboBoxPopup component
Browse files Browse the repository at this point in the history
  • Loading branch information
viktor-podzigun committed Mar 4, 2024
1 parent e142967 commit a4dc1e5
Show file tree
Hide file tree
Showing 4 changed files with 355 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/ComboBoxPopup.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Widgets } from "@farjs/blessed";
import { ListViewport } from "./ListViewport";

export interface ComboBoxPopupProps {
readonly left: number;
readonly top: number;
readonly width: number;
readonly items: string[];
readonly viewport: ListViewport;
setViewport(viewport: ListViewport): void;
readonly style: Widgets.Types.TStyle;
onClick(index: number): void;
}
86 changes: 86 additions & 0 deletions src/ComboBoxPopup.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* @typedef {import("./ComboBoxPopup").ComboBoxPopupProps} ComboBoxPopupProps
*/
import React from "react";
import SingleBorder from "./border/SingleBorder.mjs";
import ListView from "./ListView.mjs";
import ScrollBar from "./ScrollBar.mjs";

const h = React.createElement;

/**
* @param {ComboBoxPopupProps} props
*/
const ComboBoxPopup = (props) => {
const { singleBorderComp, listViewComp, scrollBarComp } = ComboBoxPopup;

const width = props.width;
const height = ComboBoxPopup.maxItems + 2;
const viewWidth = width - 2;
const theme = props.style;
const viewport = props.viewport;

return h(
"box",
{
clickable: true,
autoFocus: false,
width: width,
height: height,
left: props.left,
top: props.top,
onWheelup: () => {
props.setViewport(viewport.up());
},
onWheeldown: () => {
props.setViewport(viewport.down());
},
style: theme,
},

h(singleBorderComp, {
width: width,
height: height,
style: theme,
}),

h(listViewComp, {
left: 1,
top: 1,
width: viewWidth,
height: height - 2,
items: props.items.map(
(i) => ` ${i.slice(0, Math.min(viewWidth - 4, i.length))} `
),
viewport: viewport,
setViewport: props.setViewport,
style: theme,
onClick: props.onClick,
}),

viewport.length > viewport.viewLength
? h(scrollBarComp, {
left: width - 1,
top: 1,
length: viewport.viewLength,
style: theme,
value: viewport.offset,
extent: viewport.viewLength,
min: 0,
max: viewport.length - viewport.viewLength,
onChange: (offset) => {
props.setViewport(viewport.updated(offset));
},
})
: null
);
};

ComboBoxPopup.displayName = "ComboBoxPopup";
ComboBoxPopup.singleBorderComp = SingleBorder;
ComboBoxPopup.listViewComp = ListView;
ComboBoxPopup.scrollBarComp = ScrollBar;

ComboBoxPopup.maxItems = 8;

export default ComboBoxPopup;
255 changes: 255 additions & 0 deletions test/ComboBoxPopup.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/**
* @typedef {import('../src/ListViewport').ListViewport} ListViewport
* @typedef {import('../src/ComboBoxPopup').ComboBoxPopupProps} ComboBoxPopupProps
*/
import React from "react";
import TestRenderer from "react-test-renderer";
import { assertComponents, mockComponent } from "react-assert";
import assert from "node:assert/strict";
import mockFunction from "mock-fn";
import DefaultTheme from "../src/theme/DefaultTheme.mjs";
import { createListViewport } from "../src/ListViewport.mjs";
import SingleBorder from "../src/border/SingleBorder.mjs";
import ListView from "../src/ListView.mjs";
import ScrollBar from "../src/ScrollBar.mjs";
import ComboBoxPopup from "../src/ComboBoxPopup.mjs";

const h = React.createElement;

const { describe, it } = await (async () => {
// @ts-ignore
const module = process.isBun ? "bun:test" : "node:test";
// @ts-ignore
return process.isBun // @ts-ignore
? Promise.resolve({ describe: (_, fn) => fn(), it: test })
: import(module);
})();

ComboBoxPopup.singleBorderComp = mockComponent(SingleBorder);
ComboBoxPopup.listViewComp = mockComponent(ListView);
ComboBoxPopup.scrollBarComp = mockComponent(ScrollBar);
const { singleBorderComp, listViewComp, scrollBarComp } = ComboBoxPopup;

describe("ComboBoxPopup.test.mjs", () => {
it("should call setViewport when box.onWheelup", () => {
//given
const setViewport = mockFunction((vp) => {
assertListViewport(
vp,
props.viewport.offset,
focused,
props.viewport.length,
props.viewport.viewLength
);
});
const props = getComboBoxPopupProps({
...defaultProps,
index: 1,
setViewport,
});
const comp = TestRenderer.create(h(ComboBoxPopup, props)).root;
const boxEl = comp.findByType("box");
const focused = 0;
assert.notDeepEqual(props.viewport.focused, focused);

//when
boxEl.props.onWheelup();

//then
assert.deepEqual(setViewport.times, 1);
});

it("should call setViewport when box.onWheeldown", () => {
//given
const setViewport = mockFunction((vp) => {
assertListViewport(
vp,
props.viewport.offset,
focused,
props.viewport.length,
props.viewport.viewLength
);
});
const props = getComboBoxPopupProps({ ...defaultProps, setViewport });
const comp = TestRenderer.create(h(ComboBoxPopup, props)).root;
const boxEl = comp.findByType("box");
const focused = 1;
assert.notDeepEqual(props.viewport.focused, focused);

//when
boxEl.props.onWheeldown();

//then
assert.deepEqual(setViewport.times, 1);
});

it("should call setViewport when onChange in ScrollBar", () => {
//given
const setViewport = mockFunction((vp) => {
assertListViewport(
vp,
offset,
props.viewport.focused,
props.viewport.length,
props.viewport.viewLength
);
});
const props = getComboBoxPopupProps({
...defaultProps,
items: new Array(15).fill("item"),
setViewport,
});
assert.deepEqual(props.items.length > ComboBoxPopup.maxItems, true);
const comp = TestRenderer.create(h(ComboBoxPopup, props)).root;
const scrollBar = comp.findByType(scrollBarComp);
const offset = 1;
assert.notDeepEqual(props.viewport.offset, offset);

//when
scrollBar.props.onChange(offset);

//then
assert.deepEqual(setViewport.times, 1);
});

it("should render without ScrollBar", () => {
//given
const props = getComboBoxPopupProps();

//when
const result = TestRenderer.create(h(ComboBoxPopup, props)).root;

//then
assertComboBoxPopup(result, props, false);
});

it("should render with ScrollBar", () => {
//given
const props = getComboBoxPopupProps({
...defaultProps,
items: new Array(15).fill("item"),
});

//when
const result = TestRenderer.create(h(ComboBoxPopup, props)).root;

//then
assertComboBoxPopup(result, props, true);
});
});

/**
* @typedef {{
* index: number,
* items: string[],
* setViewport(viewport: ListViewport): void,
* onClick(index: number): void
* }} DefaultProps
* @type {DefaultProps}
*/
const defaultProps = {
index: 0,
items: ["item 1", "item 2"],
setViewport: () => {},
onClick: () => {},
};

/**
* @param {DefaultProps} props
* @returns {ComboBoxPopupProps}
*/
function getComboBoxPopupProps(props = defaultProps) {
return {
items: props.items,
left: 1,
top: 2,
width: 11,
viewport: createListViewport(
props.index,
props.items.length,
ComboBoxPopup.maxItems
),
setViewport: props.setViewport,
style: DefaultTheme.popup.menu,
onClick: props.onClick,
};
}

/**
* @param {ListViewport} result
* @param {number} offset
* @param {number} focused
* @param {number} length
* @param {number} viewLength
*/
function assertListViewport(result, offset, focused, length, viewLength) {
assert.deepEqual(result.offset, offset);
assert.deepEqual(result.focused, focused);
assert.deepEqual(result.length, length);
assert.deepEqual(result.viewLength, viewLength);
}

/**
* @param {TestRenderer.ReactTestInstance} result
* @param {ComboBoxPopupProps} props
* @param {boolean} showScrollBar
*/
function assertComboBoxPopup(result, props, showScrollBar) {
assert.deepEqual(ComboBoxPopup.displayName, "ComboBoxPopup");

const width = props.width;
const height = ComboBoxPopup.maxItems + 2;
const viewWidth = width - 2;
const theme = props.style;

assertComponents(
result.children,
h(
"box",
{
clickable: true,
autoFocus: false,
width: width,
height: height,
left: props.left,
top: props.top,
style: theme,
},
...[
h(singleBorderComp, {
width: width,
height: height,
style: theme,
}),

h(listViewComp, {
left: 1,
top: 1,
width: viewWidth,
height: height - 2,
items: props.items.map(
(i) => ` ${i.slice(0, Math.min(viewWidth - 4, i.length))} `
),
viewport: props.viewport,
setViewport: props.setViewport,
style: theme,
onClick: props.onClick,
}),

showScrollBar
? h(scrollBarComp, {
left: width - 1,
top: 1,
length: ComboBoxPopup.maxItems,
style: theme,
value: 0,
extent: ComboBoxPopup.maxItems,
min: 0,
max: props.items.length - ComboBoxPopup.maxItems,
onChange: () => {},
})
: null,
].filter((h) => h)
)
);
}
1 change: 1 addition & 0 deletions test/all.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
await import("./Button.test.mjs");
await import("./ButtonsPanel.test.mjs");
await import("./CheckBox.test.mjs");
await import("./ComboBoxPopup.test.mjs");
await import("./ListBox.test.mjs");
await import("./ListView.test.mjs");
await import("./ListViewport.test.mjs");
Expand Down

0 comments on commit a4dc1e5

Please sign in to comment.