-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fa7d224
commit 4522b76
Showing
4 changed files
with
403 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
/** | ||
* @typedef {import("@farjs/blessed").Widgets.BlessedElement} BlessedElement | ||
*/ | ||
import React, { useLayoutEffect, useRef } from "react"; | ||
import DoubleBorder from "@farjs/ui/border/DoubleBorder.mjs"; | ||
import PopupOverlay from "@farjs/ui/popup/PopupOverlay.mjs"; | ||
import Theme from "@farjs/ui/theme/Theme.mjs"; | ||
|
||
const h = React.createElement; | ||
|
||
/** | ||
* @typedef {{ | ||
* readonly text: string; | ||
* onClose(): void; | ||
* }} FileListQuickSearchProps | ||
*/ | ||
|
||
/** | ||
* @param {FileListQuickSearchProps} props | ||
*/ | ||
const FileListQuickSearch = (props) => { | ||
const { doubleBorderComp } = FileListQuickSearch; | ||
|
||
const elementRef = /** @type {React.MutableRefObject<BlessedElement>} */ ( | ||
useRef() | ||
); | ||
|
||
const width = 25; | ||
const height = 3; | ||
const currTheme = Theme.useTheme(); | ||
const boxStyle = currTheme.popup.regular; | ||
const textStyle = currTheme.textBox.regular; | ||
const textWidth = width - 2; | ||
const text = props.text.slice(0, Math.min(textWidth - 1, props.text.length)); | ||
|
||
useLayoutEffect(() => { | ||
const el = elementRef.current; | ||
const screen = el.screen; | ||
const cursor = screen.cursor; | ||
if (cursor.shape !== "underline" || !cursor.blink) { | ||
// @ts-ignore | ||
screen.cursorShape("underline", true); | ||
} | ||
|
||
const program = screen.program; | ||
program.showCursor(); | ||
return () => { | ||
program.hideCursor(); | ||
}; | ||
}, []); | ||
|
||
function moveCursor() { | ||
const el = elementRef.current; | ||
el.screen.program.omove( | ||
/** @type {number} */ (el.aleft) + text.length, | ||
/** @type {number} */ (el.atop) | ||
); | ||
} | ||
|
||
useLayoutEffect(() => { | ||
moveCursor(); | ||
}, [text]); | ||
|
||
return h( | ||
"form", | ||
{ | ||
clickable: true, | ||
mouse: true, | ||
autoFocus: false, | ||
style: PopupOverlay.style, | ||
onResize: moveCursor, | ||
onClick: props.onClose, | ||
}, | ||
h( | ||
"box", | ||
{ | ||
clickable: true, | ||
autoFocus: false, | ||
width, | ||
height, | ||
top: "100%-3", | ||
left: 10, | ||
style: boxStyle, | ||
}, | ||
h(doubleBorderComp, { | ||
width, | ||
height, | ||
style: boxStyle, | ||
title: "Search", | ||
}), | ||
|
||
h("text", { | ||
ref: elementRef, | ||
width: textWidth, | ||
height: 1, | ||
top: 1, | ||
left: 1, | ||
style: textStyle, | ||
content: text, | ||
}) | ||
) | ||
); | ||
}; | ||
|
||
FileListQuickSearch.displayName = "FileListQuickSearch"; | ||
FileListQuickSearch.doubleBorderComp = DoubleBorder; | ||
|
||
export default FileListQuickSearch; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
/** | ||
* @typedef {import("../src/FileListQuickSearch.mjs").FileListQuickSearchProps} FileListQuickSearchProps | ||
*/ | ||
import React from "react"; | ||
import assert from "node:assert/strict"; | ||
import mockFunction from "mock-fn"; | ||
import TestRenderer from "react-test-renderer"; | ||
import { assertComponents, mockComponent } from "react-assert"; | ||
import DoubleBorder from "@farjs/ui/border/DoubleBorder.mjs"; | ||
import PopupOverlay from "@farjs/ui/popup/PopupOverlay.mjs"; | ||
import FileListTheme from "../src/theme/FileListTheme.mjs"; | ||
import withThemeContext from "../src/theme/withThemeContext.mjs"; | ||
import FileListQuickSearch from "../src/FileListQuickSearch.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); | ||
})(); | ||
|
||
FileListQuickSearch.doubleBorderComp = mockComponent(DoubleBorder); | ||
|
||
const { doubleBorderComp } = FileListQuickSearch; | ||
|
||
describe("FileListQuickSearch.test.mjs", () => { | ||
it("should call onClose when onClick", () => { | ||
//given | ||
let omoveArgs = /** @type {any[]} */ ([]); | ||
const omove = mockFunction((...args) => (omoveArgs = args)); | ||
let cursorShapeArgs = /** @type {any[]} */ ([]); | ||
const cursorShape = mockFunction((...args) => (cursorShapeArgs = args)); | ||
const showCursor = mockFunction(); | ||
const program = { omove, showCursor }; | ||
const cursor = { shape: "block", blink: true }; | ||
const screen = { program, cursor, cursorShape }; | ||
const textEl = { screen, aleft: 1, atop: 3 }; | ||
const onClose = mockFunction(); | ||
const props = getFileListQuickSearchProps("text", onClose); | ||
|
||
const formComp = /** @type {TestRenderer.ReactTestInstance} */ ( | ||
TestRenderer.create(withThemeContext(h(FileListQuickSearch, props)), { | ||
createNodeMock: (el) => (el.type === "text" ? textEl : null), | ||
}).root.children[0] | ||
); | ||
|
||
//when | ||
formComp.props.onClick(); | ||
|
||
//then | ||
assert.deepEqual(omove.times, 1); | ||
assert.deepEqual(omoveArgs, [5, 3]); | ||
assert.deepEqual(cursorShape.times, 1); | ||
assert.deepEqual(cursorShapeArgs, ["underline", true]); | ||
assert.deepEqual(showCursor.times, 1); | ||
assert.deepEqual(onClose.times, 1); | ||
}); | ||
|
||
it("should move cursor when onResize", () => { | ||
//given | ||
let omoveArgs = /** @type {any[]} */ ([]); | ||
const omove = mockFunction((...args) => (omoveArgs = args)); | ||
let cursorShapeArgs = /** @type {any[]} */ ([]); | ||
const cursorShape = mockFunction((...args) => (cursorShapeArgs = args)); | ||
const showCursor = mockFunction(); | ||
const program = { omove, showCursor }; | ||
const cursor = { shape: "underline", blink: false }; | ||
const screen = { program, cursor, cursorShape }; | ||
const textEl = { screen, aleft: 1, atop: 3 }; | ||
const props = getFileListQuickSearchProps("text"); | ||
|
||
const formComp = /** @type {TestRenderer.ReactTestInstance} */ ( | ||
TestRenderer.create(withThemeContext(h(FileListQuickSearch, props)), { | ||
createNodeMock: (el) => (el.type === "text" ? textEl : null), | ||
}).root.children[0] | ||
); | ||
assert.deepEqual(omove.times, 1); | ||
assert.deepEqual(omoveArgs, [5, 3]); | ||
|
||
textEl.aleft = 2; | ||
textEl.atop = 3; | ||
|
||
//when | ||
formComp.props.onResize(); | ||
|
||
//then | ||
assert.deepEqual(omove.times, 2); | ||
assert.deepEqual(omoveArgs, [6, 3]); | ||
assert.deepEqual(cursorShape.times, 1); | ||
assert.deepEqual(cursorShapeArgs, ["underline", true]); | ||
assert.deepEqual(showCursor.times, 1); | ||
}); | ||
|
||
it("should move cursor when update", () => { | ||
//given | ||
let omoveArgs = /** @type {any[]} */ ([]); | ||
const omove = mockFunction((...args) => (omoveArgs = args)); | ||
let cursorShapeArgs = /** @type {any[]} */ ([]); | ||
const cursorShape = mockFunction((...args) => (cursorShapeArgs = args)); | ||
const showCursor = mockFunction(); | ||
const program = { omove, showCursor }; | ||
const cursor = { shape: "underline", blink: false }; | ||
const screen = { program, cursor, cursorShape }; | ||
const textEl = { screen, aleft: 1, atop: 3 }; | ||
const props = getFileListQuickSearchProps("text"); | ||
|
||
const renderer = TestRenderer.create( | ||
withThemeContext(h(FileListQuickSearch, props)), | ||
{ | ||
createNodeMock: (el) => (el.type === "text" ? textEl : null), | ||
} | ||
); | ||
assert.deepEqual(omove.times, 1); | ||
assert.deepEqual(omoveArgs, [5, 3]); | ||
|
||
//when | ||
TestRenderer.act(() => { | ||
renderer.update( | ||
withThemeContext(h(FileListQuickSearch, { ...props, text: "text2" })) | ||
); | ||
}); | ||
|
||
//then | ||
assert.deepEqual(omove.times, 2); | ||
assert.deepEqual(omoveArgs, [6, 3]); | ||
assert.deepEqual(cursorShape.times, 1); | ||
assert.deepEqual(cursorShapeArgs, ["underline", true]); | ||
assert.deepEqual(showCursor.times, 1); | ||
}); | ||
|
||
it("should hide cursor when unmount", () => { | ||
//given | ||
let omoveArgs = /** @type {any[]} */ ([]); | ||
const omove = mockFunction((...args) => (omoveArgs = args)); | ||
let cursorShapeArgs = /** @type {any[]} */ ([]); | ||
const cursorShape = mockFunction((...args) => (cursorShapeArgs = args)); | ||
const showCursor = mockFunction(); | ||
const hideCursor = mockFunction(); | ||
const program = { omove, showCursor, hideCursor }; | ||
const cursor = { shape: "underline", blink: true }; | ||
const screen = { program, cursor, cursorShape }; | ||
const textEl = { screen, aleft: 1, atop: 3 }; | ||
const props = getFileListQuickSearchProps("text"); | ||
|
||
const renderer = TestRenderer.create( | ||
withThemeContext(h(FileListQuickSearch, props)), | ||
{ | ||
createNodeMock: (el) => (el.type === "text" ? textEl : null), | ||
} | ||
); | ||
|
||
//when | ||
TestRenderer.act(() => { | ||
renderer.unmount(); | ||
}); | ||
|
||
//then | ||
assert.deepEqual(omove.times, 1); | ||
assert.deepEqual(omoveArgs, [5, 3]); | ||
assert.deepEqual(cursorShape.times, 0); | ||
assert.deepEqual(cursorShapeArgs, []); | ||
assert.deepEqual(showCursor.times, 1); | ||
assert.deepEqual(hideCursor.times, 1); | ||
}); | ||
|
||
it("should render component", () => { | ||
//given | ||
let omoveArgs = /** @type {any[]} */ ([]); | ||
const omove = mockFunction((...args) => (omoveArgs = args)); | ||
let cursorShapeArgs = /** @type {any[]} */ ([]); | ||
const cursorShape = mockFunction((...args) => (cursorShapeArgs = args)); | ||
const showCursor = mockFunction(); | ||
const program = { omove, showCursor }; | ||
const cursor = { shape: "underline", blink: false }; | ||
const screen = { program, cursor, cursorShape }; | ||
const textEl = { screen, aleft: 1, atop: 3 }; | ||
const props = getFileListQuickSearchProps("some quick search text"); | ||
|
||
//when | ||
const result = TestRenderer.create( | ||
withThemeContext(h(FileListQuickSearch, props)), | ||
{ | ||
createNodeMock: (el) => (el.type === "text" ? textEl : null), | ||
} | ||
).root; | ||
|
||
//then | ||
assert.deepEqual(omove.times, 1); | ||
assert.deepEqual(omoveArgs, [23, 3]); | ||
assert.deepEqual(cursorShape.times, 1); | ||
assert.deepEqual(cursorShapeArgs, ["underline", true]); | ||
assert.deepEqual(showCursor.times, 1); | ||
assertFileListQuickSearch(result, props); | ||
}); | ||
}); | ||
|
||
/** | ||
* @param {string} text | ||
* @param {() => void} [onClose] | ||
* @returns {FileListQuickSearchProps} | ||
*/ | ||
function getFileListQuickSearchProps(text, onClose = mockFunction()) { | ||
return { | ||
text, | ||
onClose, | ||
}; | ||
} | ||
|
||
/** | ||
* @param {TestRenderer.ReactTestInstance} result | ||
* @param {FileListQuickSearchProps} props | ||
*/ | ||
function assertFileListQuickSearch(result, props) { | ||
assert.deepEqual(FileListQuickSearch.displayName, "FileListQuickSearch"); | ||
|
||
const currTheme = FileListTheme.defaultTheme; | ||
const width = 25; | ||
const height = 3; | ||
const boxStyle = currTheme.popup.regular; | ||
const textStyle = currTheme.textBox.regular; | ||
const textWidth = width - 2; | ||
|
||
assertComponents( | ||
result.children, | ||
h( | ||
"form", | ||
{ | ||
clickable: true, | ||
mouse: true, | ||
autoFocus: false, | ||
style: PopupOverlay.style, | ||
}, | ||
h( | ||
"box", | ||
{ | ||
clickable: true, | ||
autoFocus: false, | ||
width: width, | ||
height: height, | ||
top: "100%-3", | ||
left: 10, | ||
style: boxStyle, | ||
}, | ||
h(doubleBorderComp, { | ||
width, | ||
height, | ||
style: boxStyle, | ||
title: "Search", | ||
}), | ||
h("text", { | ||
width: textWidth, | ||
height: 1, | ||
top: 1, | ||
left: 1, | ||
style: textStyle, | ||
content: props.text.slice(0, Math.min(textWidth, props.text.length)), | ||
}) | ||
) | ||
) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.