Skip to content

Commit ce1dae5

Browse files
committed
feat: Added fullscreen and scroll
1 parent b0912d4 commit ce1dae5

5 files changed

Lines changed: 130 additions & 57 deletions

File tree

src/Form.tsx

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { DescriptionRenderer } from './DescriptionRenderer.js';
88
import { canSubmit } from './canSubmit.js';
99
import { SubmitButton } from './SubmitButton.js';
1010
import { Button } from './Button.js';
11+
import { ScrollArea } from './ScrollArea.js';
12+
import { FullScreen } from './FullScreen.js';
1113

1214
export const Form: React.FC<FormProps> = props => {
1315
const isControlled = props.value !== undefined;
@@ -18,6 +20,7 @@ export const Form: React.FC<FormProps> = props => {
1820
const [editingField, setEditingField] = useState<string>();
1921
const canSubmitForm = useMemo(() => canSubmit(props.form, value), [value, props.form]);
2022
const focusManager = useFocusManager();
23+
const [focusedElement, setFocusedElement] = useState(0);
2124

2225
useEffect(() => {
2326
focusManager.enableFocus();
@@ -37,21 +40,15 @@ export const Form: React.FC<FormProps> = props => {
3740
.map(field => (field.initialValue !== undefined ? { [field.name]: field.initialValue } : {}))
3841
.reduce((obj1, obj2) => ({ ...obj1, ...obj2 }), {}));
3942
})
40-
41-
42-
// setValueAndPropagate({
43-
// ...value,
44-
// ...props.form.sections
45-
// .map(section =>
46-
// section.fields
47-
// .map(field => (field.initialValue !== undefined ? { [field.name]: field.initialValue } : {}))
48-
// .reduce((obj1, obj2) => ({ ...obj1, ...obj2 }), {})
49-
// )
50-
// .reduce((obj1, obj2) => ({ ...obj1, ...obj2 }), {}),
51-
// });
5243
}
5344
}, []);
5445

46+
const onChangeTab = (tab) => {
47+
setCurrentTab(tab);
48+
focusManager.focus('0');
49+
setFocusedElement(0)
50+
}
51+
5552
const setValueAndPropagate = (index: number, newValue: Record<string, unknown>) => {
5653
value[index] = newValue
5754
setValue(structuredClone(value));
@@ -61,8 +58,18 @@ export const Form: React.FC<FormProps> = props => {
6158
useInput(
6259
(input, key) => {
6360
if (key.upArrow) {
61+
if (focusedElement - 1 <= 0) {
62+
return;
63+
}
64+
65+
setFocusedElement((focusedElement) => focusedElement - 1);
6466
focusManager.focusPrevious();
6567
} else if (key.downArrow) {
68+
if (focusedElement + 1 > sections[currentTab].fields.length + 2) {
69+
return;
70+
}
71+
72+
setFocusedElement((focusedElement) => focusedElement + 1);
6673
focusManager.focusNext();
6774
}
6875
},
@@ -89,44 +96,69 @@ export const Form: React.FC<FormProps> = props => {
8996
setValue([...value]);
9097
}
9198

99+
const [size, setSize] = useState({
100+
columns: process.stdout.columns,
101+
rows: process.stdout.rows,
102+
});
103+
104+
useEffect(() => {
105+
function onResize() {
106+
setSize({
107+
columns: process.stdout.columns,
108+
rows: process.stdout.rows,
109+
});
110+
}
111+
112+
process.stdout.on("resize", onResize);
113+
return () => {
114+
process.stdout.off("resize", onResize);
115+
};
116+
}, []);
117+
92118
return (
93-
!isSubmitted && <Box width="100%" height="90%" flexDirection="column" overflowY="hidden">
94-
<FormHeader {...props} form={{ ...props.form, sections }} currentTab={currentTab} onChangeTab={setCurrentTab} editingField={editingField} />
95-
{!editingField && sections[currentTab].description && (
96-
<Box marginX={4}>
97-
<DescriptionRenderer description={props.form.sections[currentTab]?.description} />
98-
</Box>
99-
)}
100-
<Box flexDirection="column">
101-
{currentTab > props.form.sections.length - 1
102-
? null
103-
: sections[currentTab].fields.map((field, index) => (
104-
<FormFieldRenderer
105-
field={field}
106-
key={field.name + currentTab}
107-
form={props.form}
108-
value={value[currentTab][field.name]}
109-
onChange={v => setValueAndPropagate(currentTab, { ...value[currentTab], [field.name]: v })}
110-
onSetEditingField={setEditingField}
111-
editingField={editingField}
112-
customManagers={props.customManagers}
113-
/>
114-
))}
115-
<Box flexDirection="row-reverse">
116-
<Button label="Add Item (duplicate)" onClicked={() => duplicateCurrentItem()}/>
117-
</Box>
118-
<Box flexDirection="row-reverse">
119-
<Button label="Remove" onClicked={() => removeCurrentItem()}/>
120-
</Box>
119+
<FullScreen>
120+
<Box width="100%" height="90%" flexDirection="column" overflowY="hidden">
121+
<FormHeader {...props} form={{ ...props.form, sections }} currentTab={currentTab} onChangeTab={onChangeTab} editingField={editingField} />
122+
<ScrollArea height={size.rows - 6} key={currentTab} isStart={focusedElement === 0}>
123+
{!editingField && sections[currentTab].description && (
124+
<Box marginX={4}>
125+
<DescriptionRenderer description={props.form.sections[currentTab]?.description} />
126+
</Box>
127+
)}
128+
<Box flexDirection="column">
129+
{currentTab > props.form.sections.length - 1
130+
? null
131+
: sections[currentTab].fields.map((field, index) => (
132+
<FormFieldRenderer
133+
id={index + ''}
134+
field={field}
135+
key={field.name + currentTab}
136+
form={props.form}
137+
value={value[currentTab][field.name]}
138+
onChange={v => setValueAndPropagate(currentTab, { ...value[currentTab], [field.name]: v })}
139+
onSetEditingField={setEditingField}
140+
editingField={editingField}
141+
customManagers={props.customManagers}
142+
/>
143+
))}
144+
<Box flexDirection="row-reverse">
145+
<Button label="Add Item (duplicate)" onClicked={() => duplicateCurrentItem()}/>
146+
</Box>
147+
<Box flexDirection="row-reverse">
148+
<Button label="Remove" onClicked={() => removeCurrentItem()}/>
149+
</Box>
150+
</Box>
151+
{!editingField && (
152+
<Box flexDirection="row-reverse">
153+
<SubmitButton canSubmit={canSubmitForm} onSubmit={() => {
154+
props.onSubmit?.(value)
155+
setIsSubmitted(true);
156+
}}/>
157+
</Box>
158+
)}
159+
</ScrollArea>
121160
</Box>
122-
{!editingField && (
123-
<Box flexDirection="row-reverse">
124-
<SubmitButton canSubmit={canSubmitForm} onSubmit={() => {
125-
props.onSubmit?.(value)
126-
setIsSubmitted(true);
127-
}}/>
128-
</Box>
129-
)}
130-
</Box>
161+
</FullScreen>
162+
131163
);
132164
};

src/FormFieldRenderer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const FormFieldRenderer: React.FC<FormFieldRendererProps<any>> = props =>
3030
setError(undefined);
3131
};
3232

33-
const { isFocused } = useFocus({});
33+
const { isFocused } = useFocus({id: props.id});
3434

3535
useInput(
3636
(input, key) => {

src/FullScreen.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Box } from 'ink';
2+
import React from 'react';
3+
import { useEffect, useState } from 'react';
4+
5+
export function FullScreen(props) {
6+
const [size, setSize] = useState({
7+
columns: process.stdout.columns,
8+
rows: process.stdout.rows,
9+
});
10+
11+
useEffect(() => {
12+
function onResize() {
13+
setSize({
14+
columns: process.stdout.columns,
15+
rows: process.stdout.rows,
16+
});
17+
}
18+
19+
process.stdout.on("resize", onResize);
20+
process.stdout.write("\x1b[?1049h");
21+
process.stdout.write("\x1b[?1000h");
22+
return () => {
23+
process.stdout.off("resize", onResize);
24+
process.stdout.write("\x1b[?1049l");
25+
process.stdout.write("\x1b[?1000l");
26+
};
27+
}, []);
28+
29+
return (
30+
<Box width={size.columns} height={size.rows}>
31+
{props.children}
32+
</Box>
33+
);
34+
}

src/ScrollArea.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Box, measureElement, useInput } from 'ink';
2-
import React from 'react';
1+
import { Box, measureElement, useFocusManager, useInput } from 'ink';
2+
import React, { ReactNode } from 'react';
33

44
const reducer = (state, action) => {
55
switch (action.type) {
@@ -14,26 +14,28 @@ const reducer = (state, action) => {
1414
...state,
1515
scrollTop: Math.min(
1616
state.innerHeight - state.height,
17-
state.scrollTop + 1
17+
state.scrollTop + 3
1818
)
1919
};
2020

2121
case 'SCROLL_UP':
2222
return {
2323
...state,
24-
scrollTop: Math.max(0, state.scrollTop - 1)
24+
scrollTop: Math.max(0, state.scrollTop - 3)
2525
};
2626

2727
default:
2828
return state;
2929
}
3030
};
3131

32-
export function ScrollArea({height, children}) {
32+
export function ScrollArea({height, isStart, children}) {
3333
const [state, dispatch] = React.useReducer(reducer, {
34-
height,
35-
scrollTop: 0
34+
height: 10,
35+
scrollTop: 0,
3636
});
37+
const focusManager = useFocusManager();
38+
focusManager.enableFocus();
3739

3840
const innerRef = React.useRef();
3941

@@ -47,6 +49,10 @@ export function ScrollArea({height, children}) {
4749
}, []);
4850

4951
useInput((_input, key) => {
52+
if (isStart || (children.length + 2) * 4 < height) {
53+
return;
54+
}
55+
5056
if (key.downArrow) {
5157
dispatch({
5258
type: 'SCROLL_DOWN'

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export interface FormFieldManager<T extends FormField> {
174174
}
175175

176176
export type FormFieldRendererProps<T extends FormField> = {
177+
id: string;
177178
field: T;
178179
form: FormStructure;
179180
value?: ValueOfField<T>;

0 commit comments

Comments
 (0)