Skip to content

Commit cb75cb1

Browse files
authored
Merge pull request #41 from jobara/feat/keyboard-support-input-area
feat: keyboard support for input area
2 parents 385a81f + 89113e5 commit cb75cb1

File tree

8 files changed

+127
-32
lines changed

8 files changed

+127
-32
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ The front end of the project is built with [Preact](https://preactjs.com/).
1212
To work on the project, you need to install [NodeJS and NPM](https://nodejs.org/en/download/)
1313
for your operating system.
1414

15+
_**Note:** If you'd like to make use of RAG (optional), you'll also need to ensure that [CMake](http://cmake.org)
16+
is installed. CMake is required for installing `faiss` which is pulled in by the dependency `faiss-node`._
17+
1518
Then, clone the project from GitHub. [Create a fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo)
1619
with your GitHub account, then enter the following in your command line
1720
(make sure to replace `your-username` with your username):
@@ -51,7 +54,7 @@ The website will be available at [http://localhost:3000](http://localhost:3000).
5154

5255
RAG (Retrieval-Augmented Generation) is an AI technique designed to enhance the accuracy of generative models by
5356
incorporating factual knowledge from external sources. It requires loading factual knowledge into a vector store
54-
that will be quried to provide relevant information to the language model as a context.
57+
that will be queried to provide relevant information to the language model as a context.
5558

5659
By default, the use of RAG is turned off in the system. The `enableRag` flag is set to `false` by default in the
5760
[config/config.ts](./config/config.ts).
@@ -90,6 +93,12 @@ Follow these steps to complete a one-time setup to enable RAG in the system:
9093

9194
Follow the instruction in the [Start a Server](./README.md#start-a-server) section.
9295

96+
#### Troubleshooting
97+
98+
In order to use RAG, `fais-node` must be installed. `faise-node` requires that [CMake](https://cmake.org/)
99+
be installed on the machine first. If this has not been done, install CMake and re-run the application install
100+
steps.
101+
93102
### Lint
94103

95104
To lint the source code, run:

src/client/CommandCursorBackward.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import { VNode } from "preact";
1313
import { html } from "htm/preact";
1414
import { BlissSymbol } from "./BlissSymbol";
15-
import { changeEncodingContents } from "./GlobalData";
15+
import { decrementCursor } from "./ContentBmwEncoding";
1616
import { BlissSymbolInfoType, LayoutInfoType } from "./index.d";
1717
import { generateGridStyle, speak } from "./GlobalUtils";
1818

@@ -33,15 +33,7 @@ export function CommandCursorBackward (props: CommandCursorBackwardProps): VNode
3333
const gridStyles = generateGridStyle(columnStart, columnSpan, rowStart, rowSpan);
3434

3535
const cellClicked = (): void => {
36-
// Move the caret position back one. Note that the new caretPosition can
37-
// equal -1 indicating that the caret is before the first symbol in the
38-
// `payloads` array. But, it cannot be less than -1.
39-
if (changeEncodingContents.value.caretPosition > -1) {
40-
changeEncodingContents.value = {
41-
payloads: changeEncodingContents.value.payloads,
42-
caretPosition: changeEncodingContents.value.caretPosition - 1
43-
};
44-
}
36+
decrementCursor();
4537
speak(label);
4638
};
4739

src/client/CommandCursorForward.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import { VNode } from "preact";
1313
import { html } from "htm/preact";
1414
import { BlissSymbol } from "./BlissSymbol";
15-
import { changeEncodingContents } from "./GlobalData";
15+
import { incrementCursor } from "./ContentBmwEncoding";
1616
import { BlissSymbolInfoType, LayoutInfoType } from "./index.d";
1717
import { generateGridStyle, speak } from "./GlobalUtils";
1818

@@ -33,12 +33,7 @@ export function CommandCursorForward (props: CommandCursorForwardProps): VNode {
3333
const gridStyles = generateGridStyle(columnStart, columnSpan, rowStart, rowSpan);
3434

3535
const cellClicked = (): void => {
36-
if (changeEncodingContents.value.caretPosition < changeEncodingContents.value.payloads.length - 1) {
37-
changeEncodingContents.value = {
38-
payloads: changeEncodingContents.value.payloads,
39-
caretPosition: changeEncodingContents.value.caretPosition + 1
40-
};
41-
}
36+
incrementCursor();
4237
speak(label);
4338
};
4439

src/client/ContentBmwEncoding.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import { render, screen } from "@testing-library/preact";
1313
import "@testing-library/jest-dom";
1414
import { html } from "htm/preact";
15-
import { ContentBmwEncoding } from "./ContentBmwEncoding";
15+
import { ContentBmwEncoding} from "./ContentBmwEncoding";
1616
import { initAdaptivePaletteGlobals } from "./GlobalData";
1717

1818
test("The BMW Encoding content area is rendered correctly", async (): Promise<void> => {

src/client/ContentBmwEncoding.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import { html } from "htm/preact";
1414
import { BlissSymbol } from "./BlissSymbol";
1515
import { changeEncodingContents } from "./GlobalData";
1616
import { ContentBmwEncodingType, EncodingType } from "./index.d";
17-
import { generateGridStyle } from "./GlobalUtils";
17+
import { generateGridStyle, clamp, speak } from "./GlobalUtils";
1818
import "./ContentBmwEncoding.scss";
1919

2020
export const INPUT_AREA_ID = "bmw-encoding-area"; // better way?
2121

22+
const isApplePlatform = navigator.platform.startsWith("Mac") || navigator.platform.startsWith("iPhone") || navigator.platform.startsWith("iPad");
23+
2224
type ContentBmwEncodingProps = {
2325
id: string,
2426
options: ContentBmwEncodingType
@@ -66,6 +68,64 @@ function generateMarkupArray (payloadArray: Array<EncodingType>, caretPos: numbe
6668
});
6769
}
6870

71+
function moveCursor (positionChange: number = 1) {
72+
// Note: the new caretPosition can equal -1 indicating that the caret is before the
73+
// first symbol in the `payloads` array. But, it cannot be less than -1.
74+
const newPosition = clamp(changeEncodingContents.value.caretPosition + positionChange, -1, changeEncodingContents.value.payloads.length - 1);
75+
changeEncodingContents.value = {
76+
payloads: changeEncodingContents.value.payloads,
77+
caretPosition: newPosition
78+
};
79+
};
80+
81+
export function incrementCursor () {
82+
moveCursor(1);
83+
}
84+
85+
export function decrementCursor () {
86+
moveCursor(-1);
87+
}
88+
89+
export function moveCursorToHome () {
90+
moveCursor(Number.NEGATIVE_INFINITY);
91+
};
92+
93+
export function moveCursorToEnd () {
94+
moveCursor(Number.POSITIVE_INFINITY);
95+
};
96+
97+
function handleKeyDown(event: KeyboardEvent) {
98+
if ((!(isApplePlatform && event.metaKey) && event.key === "ArrowLeft") || event.key === "ArrowDown") {
99+
decrementCursor();
100+
speak("backward");
101+
}
102+
103+
if ((!(isApplePlatform && event.metaKey) && event.key === "ArrowRight") || event.key === "ArrowUp") {
104+
incrementCursor();
105+
speak("forward");
106+
}
107+
108+
if (
109+
event.key === "Home" ||
110+
(event.ctrlKey && event.key === "a") ||
111+
(isApplePlatform && event.metaKey && event.key === "ArrowLeft")
112+
) {
113+
event.preventDefault();
114+
moveCursorToHome();
115+
speak("move cursor to start");
116+
}
117+
118+
if (
119+
event.key === "End" ||
120+
(event.ctrlKey && event.key === "e") ||
121+
(isApplePlatform && event.metaKey && event.key === "ArrowRight")
122+
) {
123+
event.preventDefault();
124+
moveCursorToEnd();
125+
speak("move cursor to end");
126+
}
127+
}
128+
69129
export function ContentBmwEncoding (props: ContentBmwEncodingProps): VNode {
70130
const { id, options } = props;
71131
const { columnStart, columnSpan, rowStart, rowSpan } = options;
@@ -76,7 +136,15 @@ export function ContentBmwEncoding (props: ContentBmwEncodingProps): VNode {
76136
);
77137

78138
return html`
79-
<div id="${id}" class="bmwEncodingArea" role="textbox" aria-label="Input Area" aria-readonly="true" style="${gridStyles}">
139+
<div
140+
id="${id}"
141+
class="bmwEncodingArea"
142+
role="textbox"
143+
aria-label="Input Area"
144+
aria-readonly="true"
145+
style="${gridStyles}"
146+
tabindex="0"
147+
onKeyDown=${handleKeyDown}>
80148
${contentsMarkupArray}
81149
</div>
82150
`;

src/client/GlobalUtils.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,24 @@
99
* https://github.com/inclusive-design/adaptive-palette/blob/main/LICENSE
1010
*/
1111

12-
import { generateGridStyle } from "./GlobalUtils";
12+
import { generateGridStyle, clamp } from "./GlobalUtils";
1313

1414
describe("Test global utility functions", (): void => {
1515

1616
test("Test generateGridStyle()", (): void => {
1717
expect(generateGridStyle(2, 1, 3, 2)).toBe("grid-column: 2 / span 1;grid-row: 3 / span 2;");
1818
expect(generateGridStyle(undefined, undefined, undefined, undefined)).toBe("grid-column: undefined / span undefined;grid-row: undefined / span undefined;");
1919
});
20+
21+
test("Test clamp function where value is below min", (): void => {
22+
expect(clamp(-1, 0, 1)).toBe(0);
23+
});
24+
25+
test("Test clamp function where value is above max", (): void => {
26+
expect(clamp(2, 0, 1)).toBe(1);
27+
});
28+
29+
test("Test clamp function where value is in range", (): void => {
30+
expect(clamp(1, 0, 2)).toBe(1);
31+
});
2032
});

src/client/GlobalUtils.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,26 @@ function insertWordAtCaret (wordToAdd: SymbolEncodingType, symbolSet: SymbolEnco
102102
return newSymbolSet;
103103
}
104104

105+
/**
106+
* Returns the inputted value constrained to the `min` and `max` values.
107+
* The returned value will:
108+
* - `min` if `value` was less than `min`
109+
* - `max` if the `value` was greater than `max`
110+
* - `value` if it fell within the `min` `max` range.
111+
*
112+
* @param {number} value - The value to evaluate
113+
* @param {number} min - The minimum value to be returned
114+
* @param {number} max - The maximum value to be returned
115+
* @returns {number} - The constrained value
116+
*/
117+
function clamp (value: number, min: number, max: number) {
118+
return Math.min(Math.max(value, min), max);
119+
}
120+
105121
export {
106122
generateGridStyle,
107123
speak,
108124
loadPaletteFromJsonFile,
109-
insertWordAtCaret
125+
insertWordAtCaret,
126+
clamp
110127
};

src/client/index.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { html } from "htm/preact";
1313
import { initAdaptivePaletteGlobals, adaptivePaletteGlobals } from "./GlobalData";
1414
import { loadPaletteFromJsonFile, speak } from "./GlobalUtils";
1515
import { goBackImpl } from "./CommandGoBackCell";
16+
import { INPUT_AREA_ID } from "./ContentBmwEncoding";
1617
import "./index.scss";
1718

1819
// Initialize any globals used elsewhere in the code.
@@ -54,15 +55,16 @@ window.addEventListener("keydown", (event) => {
5455
}
5556
});
5657

58+
const textInputTypes = [
59+
"date", "datetime-local", "email", "month", "number", "password", "search",
60+
"tel", "text", "time", "url", "week"
61+
];
62+
5763
function elementAllowsTextEntry (element) {
58-
return (
59-
(element.type === "text") || (element.type === "email") ||
60-
(element.type === "month") || (element.type === "number") ||
61-
(element.type === "password") || (element.type === "search") ||
62-
(element.type === "tel") || (element.type === "url") ||
63-
(element.type === "week") ||
64-
(element instanceof HTMLTextAreaElement) ||
65-
(element instanceof HTMLSelectElement) ||
66-
(element.getAttribute("role") === "textbox")
64+
return element.id !== INPUT_AREA_ID && (
65+
textInputTypes.includes(element.type) ||
66+
element instanceof HTMLTextAreaElement ||
67+
element instanceof HTMLSelectElement ||
68+
element.getAttribute("role") === "textbox"
6769
);
6870
}

0 commit comments

Comments
 (0)