Make table scroll with selected table row with onKeyDown (React) - javascript

I'm having some major difficulties trying to make my table scroll when going through
the table rows with onKeyDown. What seems to be happening is that the event doesn't actually change when using the keyboard, even though selected is highlighting the selected row properly. If a table row in the middle of the table is clicked, the table will scroll only the first time the keyboard up or down is pressed. (which makes sense since its a new event).
I've tried wrapping the function handleKeyDown in useCallback in case it lost a reference between renders, but it didn't matter.
Does anyone have any suggestions on how I can get a new event on key press?
I'm leaving the main functions here to have a look at, however you can find a working
Sandbox with the problem here: https://codesandbox.io/s/basictable-demo-material-ui-forked-54frpm?file=/demo.tsx
const handleKeyDown = (event) => {
event.preventDefault();
console.log(event.target);
scrollIntoView(event.target);
if (event.key === "ArrowUp") {
if (selected === rows[0].id) return;
setSelected(getNextRow(rows, selected, "up"));
}
if (event.key === "ArrowDown") {
if (selected === rows[rows.length - 1].id) return;
setSelected(getNextRow(rows, selected, "down"));
}
};
<TableRow
key={row.id}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
selected={isRowSelected(row.id)}
onClick={() => handleSetSelected(row.id)}
onKeyDown={handleKeyDown}
tabIndex={0}
const isRowSelected = (rowId) => {
return rowId === selected;
};
const scrollIntoView = (element) => {
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
}
};
const getNextRow = (rows, selected, direction) => {
const index = rows.findIndex((row) => row.id === selected);
if (direction === "up") return rows[index - 1].id;
if (direction === "down") return rows[index + 1].id;
}
The events are logged into the console. If you click the first row, press the down arrow on your keyboard 2-3 times, you can inspect the logged elements and confirm they are all from the row that was actually clicked on, and not the ones selected with the keyboard.
Thanks for your help in advance.
Stephan Bakkelund Valois

I found a working solution that doesn't depend on the keyDown event itself.
As we're already calculating the next row with the function getNextRow that returns the id of our next row, we can utilize this to make it scroll.
First, we need to add the id our table row component:
<TableRow
....
id={row.id}
>
We can send the id we got from getNextRow to the scrollIntoView function, and fetch the html element with a querySelector, and that way scroll to the correct row:
const scrollIntoView = (id: string) => {
const el = document.querySelector(`#${id}`)
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
}

Related

How can I get an EventListener on a Select dropdown using a loop to work as expected

I have some HTML tabs which use radio buttons to control them. However I am using a select drop down menu with some javascript to control them on mobile screen sizes.
// Get select element for mobile navigation
const select = document.getElementById("location");
// Event listener for selecting tabs event for mobile
select.addEventListener("change", (e) => {
const target = e.target.value;
const venueTabs = document.querySelectorAll(".tabs__radio");
for (let i = 0; i < venueTabs.length; i++) {
if (venueTabs[i].id === target) {
venueTabs[i].setAttribute("checked", "checked");
console.log(venueTabs[i], target);
} else if (venueTabs[i].id !== target) {
venueTabs[i].removeAttribute("checked");
}
}
});
My eventListener seems to work and is logging out what is expected. It compares the tab div id to the event target.
However this only seems to work when I test each select option twice, on the 3rd attempt the tabbed content disappears (css switches to display: none).
I can't seem to work out what is causing the error.
I have set up a code sandbox with my code https://codesandbox.io/s/nice-ramanujan-2yq1r?file=/src/index.js to help debug it. If the drop down menus isn't showing, you may have to view it for mobile/ below 700px wide & it'll display the select drop down menu.
Can anyone help identify what is causing this bug? I previously had hard coded the EventListener that worked perfectly, it looked like this
select.addEventListener("change", (e) => {
const target = e.target;
const tabone = document.getElementById("tab0");
const tabtwo = document.getElementById("tab1");
const tabthree = document.getElementById("tab2");
const tabfour = document.getElementById("tab3");
if (target.value === "tab0") {
tabtwo.removeAttribute("checked");
tabthree.removeAttribute("checked");
tabfour.removeAttribute("checked");
tabone.setAttribute("checked", "checked");
} else if (target.value === "tab1") {
tabthree.removeAttribute("checked");
tabfour.removeAttribute("checked");
tabone.removeAttribute("checked");
tabtwo.setAttribute("checked", "checked");
} else if (target.value === "tab2") {
tabfour.removeAttribute("checked");
tabone.removeAttribute("checked");
tabtwo.removeAttribute("checked");
tabthree.setAttribute("checked", "checked");
} else if (target.value === "tab3") {
tabone.removeAttribute("checked");
tabtwo.removeAttribute("checked");
tabthree.removeAttribute("checked");
tabfour.setAttribute("checked", "checked");
}
});
However it's not dynamic enough to take any number of tabs that may exist.
This doesn't need any looping over all the radio buttons to begin with - just select the one element you want to set checked via its id:
select.addEventListener("change", (e) => {
const target = e.target.value;
const venueTab = document.querySelector("#"+target);
venueTab.checked = true;
});

Why does cursor move to start of input field when value is changed dynamically?

I have an input field in my react application like below. suggestedTerm and searchTerm are coming from component's state. searchTerm state is being set in onChange handle. suggestedTerm state is being set when i navigate up or down in autocomplete suggestion list.
<input value={suggestedTerm || searchTerm}
onChange={handleInputChange}
onFocus={() => {
setShowFlyout(true);
clearActiveSuggestion();
}}
onKeyDown={handleInputKeyDown}
ref={searchInput}
/>
Here through onKeyDown handler, i am handling up and down arrow key events to navigate through the autosuggestion suggestions list that is being produced while keeping the focus on the input field. That was done basically to cater accesibility.
The requirement is to set the selected suggestion on the input field as we navigate through the autosuggestion list. However, the issue i am facing here is that my cursor moves to the beginning of the input field whenever i set suggestedTerm state which in return sets the input field's value while navigating up using up arrow key. This does not happen when i navigate through down key.
Attaching here my up key and down key logic
if ((e.key === "ArrowUp" || e.keyCode === 38) && !isEmpty(suggestions)) {
const focusedItem = getActiveSuggestion();
const index = focusedItem[0].index - 1;
if (!isEmpty(focusedItem) && focusedItem[0] && focusedItem[0].index > 0) {
setActiveSuggestion(index);
} else {
//Clear active suggestions if up is pressed while focus is on first element
clearActiveSuggestion();
}
if ((e.key === "ArrowDown" || e.keyCode === 40) && !isEmpty(suggestions)) {
const focusedItem = getActiveSuggestion();
//Set first suggestion active
if (isEmpty(focusedItem)) setActiveSuggestion(0);
else {
if (
!isEmpty(focusedItem) &&
focusedItem[0] &&
focusedItem[0].index < (suggestions && suggestions.length - 1)
) {
setActiveSuggestion(focusedItem[0].index + 1);
} else {
//Set first suggestion active when focus is on last item already and down key is pressed
setActiveSuggestion(0);
}
}
}
const clearActiveSuggestion = () => {
setSuggestions(suggestions.map(suggestion => ({ ...suggestion, active: false })));
setSuggestedTerm("");};
const getActiveSuggestion = () => {
return suggestions.filter(suggestion => suggestion.active);};
const setActiveSuggestion = activeItemIndex => {
const updatedSuggestion = suggestions.map(suggestion => {
if(activeItemIndex === suggestion.index)
setSuggestedTerm(suggestion.dq);
return {...suggestion, active: activeItemIndex === suggestion.index};
});
setSuggestions(updatedSuggestion);};
First, i do not understand why my input field is setting cursor to start of the input field even though i am changing the state and state change should reset value of input field.
Second, i searched a number of ways to manually set the cursor using setSelection method and manually setting input field's value using ref but nothing is changing the behavior.
Can anybody figure out the issue here?
Thanks
Figured out the root cause. Actually, it is a default behavior of an input field to take cursor to start of the string in an input field when up arrow is pressed on it so i simply added
e.preventDefault();
and that prevented the default behavior of the input field to move the cursor. It had nothing to do with how i am setting the state or setting the selectionRange of the input manually.

React - checkbox not update when multiple unselect

When I click the + button to expand the group and click the checkbox of group 1, it executes as expected.
But the problem is,
If I click the checkbox of group 1 first and then expand the group by clicking + button,
It shows all user clicked which is correct, if I clicked the checkbox of group 1 again, the checkbox of group 1 become unchecked, but the checkboxes of users do not get unchecked.
Reproduce step:
refresh the page > Click group 1 checkbox > click + to expand > click group 1 checkbox again > then you'll see user checkbox do not become unselected
SandBox Link below:
https://codesandbox.io/s/dazzling-antonelli-gl9rm
after Unselecting group 1, 2 checkboxes of users do not become unselected:
Your isExist method returns undefined instead of false, fix it to return always either true/false.
isExist = (id, group) => {
if (!this.props.selected) {
console.log("selected = null");
return false;
}
return (
this.props.selected.find((ele) => ele.id === group + id) !== undefined
);
};
This way you don't have to manually convert its return value to boolean using !!.
I suggest to also refactor your handleSelected method to
handleSelected = async (e) => {
const { selected } = this.state;
if (e.target.checked) {
let temp = { id: e.target.id, name: e.target.name };
return this.setState({ selected: [...selected, temp] });
}
this.setState({
selected: selected.filter(({ id }) => id !== e.target.id)
});
};
The issue you are experiencing is a result of a problem in the first render - as you can see - you have "undefined Contact". You need to solve this issue, and then the grouping will work as you desire.

Using tab key to move focus from data grid to next page element

Using AG-Grid, I need to be able to hit the tab key and have the focused element change from the grid/cell currently selected, to the next element on the page outside of the grid. The problem is that the tab key seems to be locked within the grid, and will not move outside of the data table to the next element.
I have an even listener on the cells that stores the last focused cell (used to store the last location to be able to tab back into the grid to the previously focused cell), but need to have the next focused cell be outside of the data grid:
const cells = document.getElementsByClassName('ag-cell');
[...cells].map(cell => {
cell.addEventListener("keydown", function(e) {
if(e.key === "Tab") {
let lastCell = cell.attributes[2].value;
console.log("Last Cell Focused: ", lastCell)
}
})
})
How can I remove the focus selection from the grid on keypress to the next focusable page element?
Here's a plnkr link to the current grid: Link
=====================================================
UPDATE
I've updated my code, and instead of attaching an event listener to every cell, it's now looking for the event triggered on the document. However, I'm still running into the issue that it's not getting the last_cell value and seeing the focus-visible class on hasFocusVisible.
//on arrow right if last_call === header that has 'focus-visible', set focus to first cell in body
const headerCells = document.getElementsByClassName('ag-header-cell-label');
const last_cell = headerCells[headerCells.length-1].attributes[2];
const hasFocusVisible = document.querySelector('.ag-header-cell-label').classList.contains('focus-visible');
document.addEventListener("keydown", function(e) {
if(e.key === "ArrowRight") {
// if(hasFocusVisible && last_cell) {
console.log("EVENT TRIGGERED FROM: ", event.target, "Last Cell Value: ", last_cell, hasFocusVisible);
//if last_call and 'ag-header-cell-label' has 'focus-visible', set focus to first cell in body
const bodyCell = document.getElementsByClassName('ag-cell')[0];
// }
}
});
UPDATED Plnkr: Link
====================================================
UPDATE 2
I've updated the element selector to the following:
const last_cell = document.querySelector('.ag-header-cell:last-child');
const hasFocusVisible = document.querySelector('.ag-header-cell-label').classList.contains('.focus-visible');
document.addEventListener("keydown", function(e) {
console.log('document.activeElement', document.activeElement)
const activeElement = document.activeElement;
if(e.key === "ArrowRight" && activeElement) {
if(last_cell) {
console.log("EVENT TRIGGERED FROM: ", event.target, "Last Cell Value: ", last_cell, hasFocusVisible);
//if last_call and 'ag-header-cell-label' has 'focus-visible', set focus to first cell in body
const bodyCell = document.getElementsByClassName('ag-cell')[0];
}
}
else if(e.key === "ArrowDown"){
//look for first child in first row with same id as header and set focus
document.querySelector('.ag-cell').focus();
}
});
however, the hasFocusVisible variable is always coming up false when logging out the div that has the focus-visible class. I'm not sure if I have my logic incorrect, or its not able to get the focus-visible class on the ag-header-cell-label when the event listener is fired.
If tab works within the cells, don't add a listener to every cell, just add a single one to your document, and make it move focus to whatever you know is next on the page manually. For instance:
var b = document.querySelector('button');
b.passThrough = true;
b.update = pass => {
b.passThrough = pass;
b.textContent = "click me to " + (b.passThrough ? "block" : "allow") + " tabbing";
}
b.addEventListener('click', e => b.update(!b.passThrough));
b.update(b.passThrough);
var focussable = Array.from(
document.querySelectorAll([
'button',
'[href]',
'input',
'select',
'textarea',
'[tabindex]:not([tabindex="-1"])'
].join(','))
);
// let's pretend this is your last cell.
var p = document.querySelector('p');
// make it kill off keydown events, BUT, also have it redirect focus
// to "the next focussable element", so you can see what that code looks like.
p.addEventListener('keydown', e => e.preventDefault());
document.addEventListener('keydown', e => {
if (b.passThrough && e.target === p) {
var next = focussable.indexOf(p) + 1;
focussable[next % focussable.length].focus();
}
});
<button>toggle</button>
<p tabindex=0>first</p>
second
third
Run this snippet, click the button, hit tab, notice that the tab event is now trapped (like in your cells). Now, click the button again, hit tab, hit tab again: notice it seems like the event is no longer trapped, when it fact it is: the event for the element itself is getting killed off, but the event listener for the document now explicitly moves focus for us.

How to disable hotkeys when inputting?

When the user presses a certain key, a component shows. I have four such components. When the user is typing or editing, I want to disable the hotkeys.
I have this code in each of the four components
componentDidMount(){
document.body.addEventListener("keypress", (e) => {
if (e.key === "t") { // "n" "w" "," for the others
this.setState({opened: !this.state.opened});
}
});
}
I only want to disable hotkeys when the user is typing or editing. Is there a way to know if any input is in focus? Or the other way, can we add the event listeners only if all the inputs are 'on blur'?
So we need to know if any of the inputs on the page are in focus and if any of them is focused then we just will not do anything to show or hide components.
Let's assume that our component has in the state some property which indicates that some input on the page is focused, let's call it isFocus.
So, we need to collect all inputs on the page, iterate over them all and assign to each input the focus and the blur event handlers, so we will be able to know when to change the isFocus property in the state.
First of all, we need to collect all of the inputs on the page, we do it with:
const inputs = document.getElementsByTagName('input').
Iterate over them all and assign the focus and blur event handlers:
for (let input of inputs) {
input.addEventListener('focus', () => this.setState({isFocus: true}));
input.addEventListener('blur', () => this.setState({isFocus: false}));
}
And finally, let's change the condition for the keypress event:
document.addEventListener('keypress', e => {
if (!this.state.isFocus && e.key === "t") {
this.setState({opened: !this.state.opened});
}
});
Everything together will look like this:
componentDidMount() {
const inputs = document.getElementsByTagName('input');
for (let input of inputs) {
input.addEventListener('focus', () => this.setState({isFocus: true}));
input.addEventListener('blur', () => this.setState({isFocus: false}));
}
document.addEventListener('keypress', e => {
if (!this.state.isFocus && e.key === "t") {
this.setState({opened: !this.state.opened});
}
});
}
Hope this helps. Good luck.
You could move the current open component state to the most upward component, like the following:
state: {
openComponent: null
}
your hotkey function would look like this:
hotkey(e){
const componentKeyHandlers = {
"n": {openComponent: "componentN"},
"m": {openComponent: "componentM"}
}
if (e.keyCode === 27) { //ESC
this.setState({openComponent: null});
} else if (!this.state.openComponent) {
this.setState(componentKeyHandlers[e.key]);
}
}
I'm assuming you could only have ONE open component each time. Also, you could close them by hitting ESC.
For each component, its visibility would be controlled by comparing props.openComponent to its name, given that the current state component is passed down to each one via props.
This way you don't need to unregister the hotkey function. When you start typing with an open component, the setState is going to be ignored due to the if (!this.state.openComponent) condition.

Categories