Navigation in DetailsList not possible with Monaco editor - reactjs

Hi I'm using the DetailsList and I want to be able to move my selection from column to column using tab.
But I came across this issue on Github:
https://github.com/microsoft/fluentui/issues/4690
Arrow keys needs to be used to navigate across the list but unfortunately I'm using a Monaco Editor in the list and the arrow key is blocked inside the Editor...
I would like to know if there is way to disable the List to set the TabIndex to -1
or
if Monaco can release the arrow key when the cursor is at the end of the text (Like a textbox).

I got something working following this rationale:
listen to the onKeydown event on monaco editor
identify the position of the caret
know the total of lines
get the string of a specific line
move the focus out from monaco editor
Knowing these then you can check if the caret is at the end of the last line and move the focus when the user press the right arrow key. I also added the code to check when the caret is at the very beginning and move the focus to the cell to the left.
This is the code I ended up with
import * as React from "react";
import "./styles.css";
import { DetailsList, IColumn } from "#fluentui/react";
import MonacoEditor from "react-monaco-editor";
export default function App() {
const columns: IColumn[] = [
{
key: "name",
minWidth: 50,
maxWidth: 50,
name: "Name",
onRender: (item, index) => (
<input id={`name-row-${index}`} value={item.name} />
)
},
{
key: "type",
minWidth: 200,
name: "Type",
onRender: (item, index) => {
return (
<MonacoEditor
editorDidMount={(editor, monaco) => {
editor.onKeyDown((event) => {
if (event.code === "ArrowRight") {
const { column, lineNumber } = editor.getPosition();
const model = editor.getModel();
if (lineNumber === model?.getLineCount()) {
const lastString = model?.getLineContent(lineNumber);
if (column > lastString?.length) {
const nextInput = document.getElementById(
`default-value-row-${index}`
);
(nextInput as HTMLInputElement).focus();
}
}
}
if (event.code === "ArrowLeft") {
const { column, lineNumber } = editor.getPosition();
if (lineNumber === 1 && column === 1) {
const previousInput = document.getElementById(
`name-row-${index}`
);
(previousInput as HTMLInputElement).focus();
}
}
});
}}
value={item.type}
/>
);
}
},
{
key: "defaultValue",
minWidth: 100,
name: "Default Value",
onRender: (item, index) => (
<input id={`default-value-row-${index}`} value={item.defaultValue} />
)
}
];
const items = [{ name: "name", type: "type", defaultValue: "name" }];
return <DetailsList columns={columns} items={items} />;
}
You can see it working in this codesandbox https://codesandbox.io/s/wild-smoke-vy61m?file=/src/App.tsx
monaco-editor seems to be something quite complex, probably you'll have to improve this code in order to support other interactions (ex: I don't know if this works when code is folded)

Related

Material UI Autocomplete Dropdown option not working after switching tabs

In my new project, I am using material UI autocomplete within tabs, and using useImmer hooks for state management. Values in the autocomplete are populated through map function and everything works properly. However, the dropdown functionality is not working after switching the Tabs.
The values are reaching to this component as
const Dropdownlist = ({ defaultRates, value, onChange, index }) => {
return (
<Autocomplete
{...defaultRates}
size="small"
inputValue={value}
value={value}
autoSelect={true}
clearOnEscape={true}
onChange={(event, newValue) => {
onChange( newValue, index );
}}
renderInput={(params) => <TextField {...params} />}
/>
);
};
export default Dropdownlist;
Values of 'defaultRates' was built using
const ratings =
Rates.map((charge) => {
return ({ id: charge.rateid, label: charge.rate });
});
const defaultRates = {
options: ratings,
getOptionLabel: (option) => option.label,
};
Then,
const Rates = [
{
rateid: 101,
rate:"10"
},
{
rateid: 102,
rate:"30"
},
{
rateid: 103,
rate:"1"
},
{
rateid: 104,
rate:"2"
},
];
export default Rates;
Finally, On Change functionality
const onChange = (e,i) => {
let newState;
if(e)
{
const { id, label } = e;
newState = transactions.map((item, index) => {
var tds = (label/100)*item.amount;
if (index === i) {
return {
id: item.id,
transaction: item.transaction,
amount: item.amount,
name: item.name,
type: item.type,
ts:item.ts,
tr:label,
tds: tds,
error:false,
};
} else {
return item;
}
});
setTransactions(newState);
}
}
In the first tab I have many autocomplete dropdown and the selected values are also using in the second tab. If I switch to Tab2 and return back to Tab1, I can see the selected values there. But If I want to change the selected value, nothing happens while clicking the dropdown icon. Please let me know if anyone ever experienced in this context. Would like to know if I using Material UI autocomplete parameters in the right way?
I have gone through Material UI documentation and Autocomplete params. Please advise if it is a state management issue or Mat UI bug?

How to clear all text in Slate.JS editor

For the life of me, I can't figure out how to clear all of the text in an Editor component from slate.js.
I have tried:
Transforms.delete(editor, {}); -> doesn't do anything
editor.deleteBackward("line"); -> only deletes one line, not all
I have also tried manually rerendering the editor component and that unfortunately doesn't update it to its initial state :(
I have been tearing through the slate js docs and can't find anything anywhere! If anyone has any ideas, would be very happy.
This is how editor is implemented:
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
<Editable
key={stateKey}
onKeyDown={(event: any) => handleKeyDown(event)}
style={{ overflowWrap: "anywhere", width: "100%" }}
onPaste={(e) => {
if (e.clipboardData) {
if (e.clipboardData.files.length > 0) {
setFiles([...files, ...Array.from(e.clipboardData.files)]);
e.preventDefault();
}
}
}}
decorate={decorate}
renderLeaf={renderLeaf}
placeholder="What's happening?"
/>
You can mutate editor.children and set it to an "empty" value.
const editor = useMemo(() => withHistory(withReact(createEditor())), []);
editor.children = [
{
type: "paragraph",
children: [{ text: "" }]
}
];
Note: Slate needs a minimum of an array with one empty text node to render correctly.
You can use
import { Transforms } from "slate";
// useref
const editorRef = useRef()
if (!editorRef.current) editorRef.current = withHistory(withReact(createEditor()))
const editor = editorRef.current
const reset = () =>{
// loop delete all
editor.children.map(item => {
Transforms.delete(editor, { at: [0] })
})
// reset init
editor.children = [
{
type: "p",
children: [{ text: "" }]
}];
}

How do you prevent a re-created React component from losing state?

The example in this sandbox is a contrived example, but it illustrates the point.
Clicking the "Add column" button is supposed to add a new column. It works the first time, but doesn't work after that. You'll notice from the log that this issue has to do with the fact that columns is always in its original state. Therefore, a column is always being added to this original state, not the current state.
I imagine this issue is related to the fact that the column header is being re-created on each call to renderHeader, but I'm unsure about how to pass the state to the newly created header component.
There are 2 ways to update your state:
The first way:
setColumns(newColumns)
The second way:
setColumns(columns => newColumns)
You should use the second way because the new state depend on the current state.
onClick={() => {
setColumns((columns) => {
console.log(columns.map((column) => column.field));
const newColumnName = `Column ${columns.length}`;
const newColumns = [...columns];
newColumns.splice(
columns.length - 1,
0,
createColumn(newColumnName)
);
return newColumns;
});
}}
Your component:
import React, { useState } from "react";
import { DataGrid } from "#mui/x-data-grid";
import Button from "#mui/material/Button";
import "./styles.css";
export default function App() {
const [rows] = useState([
{ id: 1, "Column 1": 1, "Column 2": 2 },
{ id: 2, "Column 1": 3, "Column 2": 3 },
{ id: 3, "Column 1": 4, "Column 2": 5 }
]);
const createColumn = (name) => {
return {
field: name,
align: "center",
editable: true,
sortable: false
};
};
const [columns, setColumns] = useState([
createColumn("Column 1"),
createColumn("Column 2"),
{
field: "Add a split",
width: 150,
sortable: false,
renderHeader: (params) => {
return (
<Button
variant="contained"
onClick={() => {
setColumns((columns) => { // <== use callback
console.log(columns.map((column) => column.field));
const newColumnName = `Column ${columns.length}`;
const newColumns = [...columns];
newColumns.splice(
columns.length - 1,
0,
createColumn(newColumnName)
);
return newColumns;
});
}}
>
Add column
</Button>
);
}
}
]);
return (
<div className="App">
<DataGrid
className="App-data-grid"
rows={rows}
columns={columns}
disableSelectionOnClick
disableColumnMenu
/>
</div>
);
}
https://codesandbox.io/s/mui-sandbox-forked-1im0p?file=/src/App.js:0-1562
I think this has to do with the way the closure for (params) => {...} captures the value of columns. At the same time, you have this strange circular thing going on: useState() returns columns and also uses columns in one of its parameters. You will need to find a way to break this circularity.

Semantic UI dropdown not setting value after selecting option

I am using React semantic ui. I am rendering a dropdown in Fieldset. I have written code such that, once a option is selected, the options is updated such that the selected option is removed from the list. But when I select an option from the dropdown, the selected value is not displayed, rather it shows empty.
Here is my code:
This is my dropdown code:
<Dropdown
name={`rows.${index}.mainField`}
className={"dropdown fieldDropdown"}
widths={2}
placeholder="Field"
fluid
selection
options={mainFieldOptions}
value={row.mainField}
onChange={(e, { value }) => {
setFieldValue(`rows.${index}.mainField`, value)
updateDropDownOptions(value)
}
}
/>
My options:
let mainField = [
{ key: "org", text: "org", value: "org" },
{ key: "role", text: "role", value: "role" },
{ key: "emailId", text: "emailId", value: "emailId" },
]
Also, I have:
const [mainFieldOptions, setMainFieldOptions] = useState(mainField)
And,
const updateDropDownOptions = (value:any) => {
let updatedOptions: { key: string; text: string; value: string }[] = []
mainFieldOptions.forEach(option => {
if(option.key != value){
updatedOptions.push({ key:option.key , text:option.key, value:option.key })
}
})
setMainFieldOptions(updatedOptions)
console.log("mainfield", mainField)
}
In onChange, if I dont call updateDropDownOptions() method, the dropdown value is set. But when I call the method, its giving blank value. Please help.
There are few changes required in your code,
You are pushing the entire initialValues when you are adding a row which is an [{}] but you need to push only {} so change your code to initialValues[0] in your push method.
Its not needed to maintain a additional state for the options. You can filter the options based on the selected option in other rows which is available in the values.rows .
Util for filtering the options
const getMainFieldOptions = (rows, index) => {
const selectedOptions = rows.filter((row, rowIndex) => rowIndex !== index);
const filteredOptions = mainField.filter(mainFieldOption => !selectedOptions.find(selectedOption => mainFieldOption.value === selectedOption.mainField));
return filteredOptions;
}
Call this util when rendering each row
values.rows.length > 0 &&
values.rows.map((row, index) => {
const mainFieldOptions = getMainFieldOptions(values.rows, index);
Working Sandbox

How to select text to copy without triggering click event in reactjs

I am using react-table. I have defined onRowClick() function for a column. Here select text should highlight the text and clicking have to redirect to another page. Now when I try to select the text, its getting redirected. How to select text without triggering click event?
Following is my onRowClick function:
onRowClick = (state, rowInfo, columnInfo) => {
return {
onClick: (e, handleOriginal) => {
if (columnInfo.id) {
this.props.history.push(`/downloads/${rowInfo.original.id}`);
} else if (handleOriginal) {
handleOriginal();
}
}
};
}
The following is my react-table component:
<ReactTable
manual
getTdProps = {this.onRowClick}
data = {results}
onFetchData = {this.onFetchData}
sortable = {false}
showPagination = {false}
noDataText = 'no data found'
columns = {[
{
Header: 'Id',
maxWidth: 50,
accessor: "id",
Cell: (props) => <span className="btn-link pointer">{props.value} </span>
},
{
Header: 'Processed on',
maxWidth: 165,
accessor: "created_at",
Cell: (props) => <span> {this.getDateTime(props.value)} </span>
}
]
/>
Clicking on id column should redirect to the details page. Selecting text should select the id text.
I think onclick cannot be prevented but your desired result can be obtained by using Window.getSelection() method.
The Window.getSelection() method returns a Selection object representing the range of text selected by the user or the current position of the caret.
By using this method you can get the selected text and then you can calculate its length as:
window.getSelection().toString()
And then you can modify your onRowClick method as given below:
onRowClick = (state, rowInfo, columnInfo) => {
return {
onClick: (e, handleOriginal) => {
let selection = window.getSelection().toString();
if(selection.length <= 0) {
if (columnInfo.id && selection.length > 0) {
console.log("columnInfo.id", columnInfo.id);
this.props.history.push(`/downloads/${rowInfo.original.id}`);
} else if (handleOriginal) {
handleOriginal();
console.log("columnInfo.id", "nothing");
}
}
}
};
};
I have created a working demo.

Resources