I want to flip a login component along with translation when i will click on a button.I used css transition and transform property and it is flipping but the problem is here that when it flips once again it is regaining its position .I want to flip once but it is flipping twice but the position i want to gain is correct.How can i flip over with translation when i will click on button.? This is login component and i added css also with it. Anyone can help me i will appreciate the support.Thanks
i want to flip it once . Below is my code
eslint-disable jsx-a11y/anchor-is-valid */
import {useEffect,useState} from 'react'
import {Outlet, Link} from 'react-router-dom'
import {toAbsoluteUrl} from '../../../_metronic/helpers'
import "./authLayout.css"
import data from "./ar.json"
import { useLang,setLanguage } from './../../../_metronic/i18n/Metronici18n';
type FlexDirection = 'ltr' | 'rtl';
const directionMap: { [key: string]: FlexDirection } = {
left: 'ltr',
right: 'rtl'
};
const AuthLayout = () => {
const lang = useLang()
const [animate, setAnimate] = useState(false);
const [animateLeft, setAnimateLeft] = useState(false);
const [direction, setDirection] = useState("left")
const [language, setLanguage] = useState('en');
const languageTranslations = {
en: {
username: 'username',
password: 'password',
signin : "Sign In",
continue:"CONTINUE"
},
ar: {
username: 'اسم االمستخدم',
password: 'كلمه السر',
signin :"تسجيل الدخول",
continue:"استمر"
},
};
const text = languageTranslations[language];
useEffect(() => {
const root = document.getElementById('root')
if (root) {
root.style.height = '100%'
}
return () => {
if (root) {
root.style.height = 'auto'
}
}
[])
const handleArabicDirection = () => {
setAnimate(animate === true)
if(direction === "left")
{
setAnimateLeft(animateLeft === false)
setLanguage(language === 'en' ? 'ar' : 'ar')
setDirection(direction === "left" ? "right" : "right")
}
else if(direction === "right"){
setAnimateLeft(animateLeft === true)
}
}
const handleEnglishDirection = () => {
if(direction === "left"){
setAnimateLeft(animateLeft === true)
}
else if(direction === "right"){
setAnimateLeft(animateLeft === false)
setDirection( "left" )
setLanguage(language === 'ar' ? 'en' : 'en')
}
setAnimate(animate === false)
}
useEffect(() => {
localStorage.setItem('text', JSON.stringify(text));
[text]);
return (
begin::Body */}
begin::Form */}
begin::Wrapper /}
<div className={w-lg-500px p-10' box ${animate ? 'animateRight' : animateLeft ? 'animateLeft' : ""} } style={{ direction: directionMap[direction], }} >
{/ {text.header} /}
{/ {text.content} /}
English
{/
Arabic
*/}
Arabic
end::Wrapper */}
end::Form */}
begin::Footer */}
begin::Links /}
{/
Terms
<a href='#' className='px-5' target='_blank'>
Plans
<a href='#' className='px-5' target='_blank'>
Contact Us
*/}
end::Links */}
end::Footer */}
end::Body */}
begin::Aside */}
<div
className='d-flex flex-lg-row-fluid w-lg-50 bgi-size-cover bgi-position-center order-1 order-lg-2'
style={{backgroundImage: url(${toAbsoluteUrl('/media/misc/auth-bg.png')})}}
>
begin::Content */}
begin::Logo */}
<img alt='Logo' src={toAbsoluteUrl('/media/logos/custom-1.png')} className='h-75px' />
end::Logo */}
begin::Image */}
<img
className='mx-auto w-275px w-md-50 w-xl-500px mb-10 mb-lg-20'
src={toAbsoluteUrl('/media/misc/auth-screens.png')}
alt=''
/>
end::Image */}
begin::Title */}
Fast, Efficient and Productive
end::Title */}
begin::Text */}
In this kind of post,{' '}
the blogger
introduces a person they’ve interviewed and provides some background information
about
the interviewee
and their work following this is a transcript of the interview.
end::Text */}
end::Content */}
end::Aside */}
)
}
export {AuthLayout}
.box {
transform: rotateY(0deg);
transition: transform 7s;
}
.animateRight {
transform: rotateY(360deg) !important;
}
.animateLeft {
transform: rotateY(-360deg) !important;
}
type here
Related
I am using Ionic capacitor to build my reactjs Web application. I have added a Capacitor App Apis to get the back event capture in my App.js file like this,
APPS.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
But when i press back button this back event is called 3 times instead of one. my App.js file is.
// import "./StyleSeets/LightTheme.css";
import "./StyleSeets/theme.css";
import "./App.css";
import React, { useEffect, useState } from "react";
import Drawer from "#mui/material/Drawer";
import { App as APPS } from "#capacitor/app";
import Login from "./Components/Login";
import ERPUtils from "./Components/ERPUtils";
import Main from "./Components/Main";
// ************************** back fire event handling ****************
const renderedScreenUIList = [];
function App() {
// const [ stylePath, setStylePath ] = useState("./StyleSeets/LightTheme.css");
const [renderPageType, setRenderPageType] = useState("login");
const [isMobileView, setIMobileView] = useState(window.innerWidth < 920);
const isConsoleOpen = false;
window.onresize = function () {
console.log(window.outerHeight - window.innerHeight);
if ((window.outerHeight - window.innerHeight) > 100) { console.log("open"); } else console.log("close");
};
const themeList = [
{ value: "#2776ed", label: "light" },
{ value: "#45b11c", label: "nature" },
{ value: "#2AB67B", label: "evening" },
{ value: "#add8e6", label: "sky" },
{ value: "#2b035b", label: "dark" }];
const languageList = [
{ value: "ENG", label: "English" },
{ value: "MAL", label: "മലയാളം" },
{ value: "HIND", label: "हिन्दी" }];
const [language, setLanguage] = useState("ENG");
const onThemeChangeCall = () => {
const themeName = sessionStorage.getItem("theme-name");
const themeElement = document.getElementById("app-theme");
themeElement.className = "App";
document.documentElement.setAttribute("data-theme", themeName);
};
APPS.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
const languageChanged = (type) => {
setLanguage(type);
};
useEffect(() => {
onThemeChangeCall();
}, []);
const changePage = (page) => {
setRenderPageType(page);
};
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const changeTheme = (type) => {
sessionStorage.setItem("theme-name", type);
document.documentElement.classList.add("color-theme-in-transition");
window.setTimeout(() => {
document.documentElement.classList.remove("color-theme-in-transition");
}, 1000);
onThemeChangeCall();
};
const toggleDrawer = (value) => {
setIsSettingsOpen(value);
};
const changeLanguage = (type) => {
sessionStorage.setItem("language-theme", type);
languageChanged(type);
};
useEffect(() => {
setIMobileView(window.innerWidth < 920);
}, [window.innerWidth]);
return (
<div className="App" id="app-theme">
{renderPageType === "login"
? (
<Login
onThemeChangeCall={onThemeChangeCall}
changePage={changePage}
languageChanged={languageChanged}
toggleDrawer={toggleDrawer}
/>
)
: null}
{renderPageType === "home"
? (
<Main
onThemeChangeCall={onThemeChangeCall}
changePage={changePage}
languageChanged={languageChanged}
toggleDrawer={toggleDrawer}
isMobileView={isMobileView}
/>
)
: null}
<Drawer
anchor="right"
open={isSettingsOpen}
onClose={() => toggleDrawer(false)}
>
<div className="p-2 " style={{ width: "250px", backgroundColor: "var(--primaryBackground)" }}>
<h6 className="border-bottom p-2">Themes</h6>
<div className="">
{!ERPUtils.isNullorWhiteSpace(themeList) ? themeList.map((el, index) => (
<div
key={index}
onKeyDown={(e) => {
if (ERPUtils.isKeyBoardEnterPressed(e)) {
changeTheme(el.label);
}
}}
role="button"
tabIndex={0}
className="p-2 d-flex align-items-center font-2 justify-content-start"
onClick={() => changeTheme(el.label)}
>
<span className="theme-thumbnail" style={{ backgroundColor: el.value }} />
<span>{(`${el.label}`).toLocaleUpperCase()}</span>
</div>
)) : null}
</div>
</div>
<div className="p-2" style={{ width: "250px", backgroundColor: "var(--primaryBackground)" }}>
<h6 className="border-bottom p-2">Language</h6>
<div className="">
{!ERPUtils.isNullorWhiteSpace(languageList) ? languageList.map((el, index) => (
<div
key={index}
onKeyDown={(e) => {
if (ERPUtils.isKeyBoardEnterPressed(e)) {
changeLanguage(el.value);
}
}}
role="button"
tabIndex={0}
className="p-2 d-flex align-items-center font-2 justify-content-start"
onClick={() => changeLanguage(el.value)}
>
<span className="language-thumbnail">{el?.label.split("")[0]}</span>
<span>{(`${el.label}`).toLocaleUpperCase()}</span>
</div>
)) : null}
</div>
</div>
</Drawer>
{/* <ThemeSwitcherProvider defaultTheme="light" themeMap={themes}> */}
{/* <link rel="stylesheet" type="text/css" href={stylePath} /> */}
{/* </ThemeSwitcherProvider> */}
</div>
);
}
export default App;
Kindly suggest a solution for this problem ?. If I call this event listener outside App.js file it only calls once.
With each page reload the listener is added again. To fix this you should call removeAllListeners() => Promise<void> once at the beginning:
App.removeAllListeners().then(() => {
App.addListener("backButton", () => {
if (renderedScreenUIList.length === 0) {
// APPS.exitApp();
alert("exit app");
} else {
alert("previous screen");
}
});
});
I have an array of job descriptions that I want to hide a part of each description and show it completely when a button is clicked using React hooks.
I am iterating over the array( consists of id and description) to show all the descriptions as a list in the component. There is a button right after each paragraph to show or hide the content.
readMore is used to hide/show the content and
activeIndex is used to keep track of clicked item index.
This is what I have done so far:
import React, { useState } from "react";
const Jobs = ({ data }) => {
const [readMore, setReadMore] = useState(false);
const [activeIndex, setActiveIndex] = useState(null);
const job = data.map((job, index) => {
const { id, description } = job;
return (
<article key={id}>
<p>
{readMore ? description : `${description.substring(0, 250)}...`}
<button
id={id}
onClick={() => {
setActiveIndex(index);
if (activeIndex === id) {
setReadMore(!readMore);
}
}}
>
{readMore ? "show less" : "show more"}
</button>
</p>
</article>
);
});
return <div>{job}</div>;
};
export default Jobs;
The problem is that when I click one button it toggles all the items in the list.
I want to show/hide content only when its own button clicked.
Can somebody tell me what I am doing wrong?
Thanks in advance.
Your readMore state is entirely redundant and is actually causing the issue. If you know the activeIndex, then you have all the info you need about what to show and not show!
import React, { useState } from "react";
const Jobs = ({ data }) => {
const [activeIndex, setActiveIndex] = useState(null);
const job = data.map((job, index) => {
const { id, description } = job;
return (
<article key={id}>
<p>
{activeIndex === index ? description : `${description.substring(0, 250)}...`}
<button
id={id}
onClick={() => {
if (activeIndex) {
setActiveIndex(null);
} else {
setActiveIndex(index);
}
}}
>
{activeIndex === index ? "show less" : "show more"}
</button>
</p>
</article>
);
});
return <div>{job}</div>;
};
export default Jobs;
Edit: The aforementioned solution only lets you open one item at a time. If you need multiple items, you need to maintain an accounting of all the indices that are active. I think a Set would be a perfect structure for this:
import React, { useState } from "react";
const Jobs = ({ data }) => {
const [activeIndices, setActiveIndices] = useState(new Set());
const job = data.map((job, index) => {
const { id, description } = job;
return (
<article key={id}>
<p>
{activeIndices.has(index) ? description : `${description.substring(0, 250)}...`}
<button
id={id}
onClick={() => {
const newIndices = new Set(activeIndices);
if (activeIndices.has(index)) {
newIndices.delete(index);
} else {
newIndices.add(index);
}
setActiveIndices(newIndices);
}}
>
{activeIndices.has(index) ? "show less" : "show more"}
</button>
</p>
</article>
);
});
return <div>{job}</div>;
};
export default Jobs;
Try this
{readMore && (activeIndex === id) ? description : `${description.substring(0, 250)}...`}
function Destination() {
const travels = [
{
title: "Home"
},
{
title: "Traveltype",
subItems: ["Local", "National", "International"]
},
{
title: "Contact",
subItems: ["Phone", "Mail", "Chat"]
}
];
const [activeIndex, setActiveIndex] = useState(null);
return (
<div className="menu-wrapper">
{travels.map((item, index) => {
return (
<div key={`${item.title}`}>
{item.title}
{item.subItems && (
<button
onClick={() => {
if (activeIndex) {
if (activeIndex !== index) {
setActiveIndex(index);
} else {
setActiveIndex(null);
}
} else {
setActiveIndex(index);
}
}}
>
{activeIndex === index ? `Hide` : `Expand`}
</button>
)}
{activeIndex === index && (
<ul>
{item.subItems &&
item.subItems.map((subItem) => {
return (
<li
key={`li-${item.title}-${subItem}`}
>
{subItem}
</li>
);
})}
</ul>
)}
</div>
);
})}
</div>
);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
I am learning to use react memo, I am applying it to a simple task application, my problem is that I do not know how to make the styles of the items in the list work well.
As you can see when completing a task, I don't know how to update the styles of the other items in the list so that it is one in white and one in black.
I tried many things but nothing worked :(
TaskItem.jsx
import React, { memo } from 'react'
import styled from "styled-components"
import { useSelector } from "react-redux";
import store from "../../redux/store";
//STYLES
const DIV = styled.div`
max-height: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0px" : "50px"
};
opacity: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0": "1"
};
padding: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.done ? "0px":"12px 15px"
};
overflow: hidden;
transition: opacity 0.5s, max-height 0.5s, padding 0.5s;
`;
const TR = styled.tr`
background-color: ${
(props) => {
//show completed and not completed tasks
if(useSelector((state)=> state.toggleDoneTasks.show)){
return props.index % 2 === 0 ? '#f3f3f3': 'none'
}
const tasksNotDone = props.tasks.filter((task) => !task.done)
const index = tasksNotDone.findIndex(t => t.id === props.task.id)
return index % 2 === 0 ? '#f3f3f3': 'none'
}
};
/*
&:nth-child(even) {background: #CCC};
&:nth-child(odd) {background: #FFF};
*/
border-bottom: ${
props => !useSelector((state)=> state.toggleDoneTasks.show) && props.task.done ? "none": "1px solid #dddddd"
};;
transition: visibility 0.5s;
cursor: pointer;
&:hover{
font-weight: bold;
color: #009879;
}
`;
function TaskRow({ task, toggleDoneTask, index, tasks }) {
return (
<TR task={task} tasks={tasks} index={index}>
<td>
<DIV done={task.done}>
{console.log('render', task)}
{task.title}
</DIV>
</td>
<td>
<DIV done={task.done}>
{task.description}
</DIV>
</td>
<td>
<DIV done={task.done}>
<input type="checkbox"
checked={task.done}
onChange={toggleDoneTask}
style={{cursor: 'pointer'}}
/>
</DIV>
</td>
</TR>
)
}
export default memo(TaskRow, (prev, next) => {
// store.getState().toggleDoneTasks.show
//COMPARE TASK OBJECT
const prevTaskKeys = Object.keys(prev.task);
const nextTaskKeys = Object.keys(next.task);
const sameLength = prevTaskKeys.length === nextTaskKeys.length;
const sameEntries = prevTaskKeys.every(key => {
return nextTaskKeys.includes(key) && prev.task[key] === next.task[key];
});
return sameLength && sameEntries;
})
Tasks.jsx
import React, { useEffect, useReducer } from "react";
import TaskItem from "./TaskItem";
function saveLocalStorage(tasks) {
localStorage.setItem("tasks", JSON.stringify(tasks));
}
function TasksReducer(taskItems, { type, task }) {
switch (type) {
case "UPDATE_TAKS": {
let taskItemsCopy = [...taskItems].map((task) => ({ ...task }));
let newItems = taskItemsCopy.map((t) => {
if (t.id === task.id) {
t.done = !t.done;
}
return t;
});
saveLocalStorage(newItems);
return newItems;
}
case "ADD_TASK": {
const newItems = [...taskItems, task];
saveLocalStorage(newItems);
return newItems;
}
default:
window.alert("INVALID ACTION");
break;
}
}
const initialState = JSON.parse(localStorage.getItem("tasks")) || [];
//STYLES
const styleTable = {
borderCollapse: "collapse",
margin: "25px 0",
fontSize: "0.9em",
fontFamily: "sans-serif",
minWidth: "400px",
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)"
};
const styleTr = {
backgroundColor: "#009879",
color: "#ffffff",
textAlign: "left"
};
const styleTh = {
padding: "12px 15px"
};
function Tasks({ newTask, show }) {
const [taskItems, dispatch] = useReducer(TasksReducer, initialState);
useEffect(() => {
if (!newTask) return;
newTaskHandler({ id: taskItems.length + 1, ...newTask });
}, [newTask]);
const newTaskHandler = (task) => {
dispatch({ type: "ADD_TASK", task });
};
const toggleDoneTask = (task) => {
dispatch({ type: "UPDATE_TAKS", task });
};
return (
<React.Fragment>
<h1>learning react </h1>
<table style={styleTable}>
<thead>
<tr style={styleTr}>
<th style={styleTh}>Title</th>
<th style={styleTh}>Description</th>
<th style={styleTh}>Done</th>
</tr>
</thead>
<tbody>
{taskItems.map(
(task, i) =>
(show || !task.done) && (
<TaskItem
tasks={taskItems}
index={i}
task={task}
key={task.id}
show={show}
toggleDoneTask={() => toggleDoneTask(task)}
/>
)
)}
</tbody>
</table>
</React.Fragment>
);
}
export default Tasks;
If you need the complete code:
About Memo and your app
Memo could be useful when you don't expect your component to change often. It comes with a cost that does these evaluations to verify if it should rerender.
In your case you would have a lot of rerenders because several components would need to be rerender to fit the correct background which is a downside to use Memo.
Besides, you would see these bugs, since other components won't rerender given their props won't change.
I would suggest to remove memo for this case.
About your show state
You see the accordion effect that you have right? You wouldn't see that effect if the component did unmount. That means the component never unmounts. You are using 2 show states, one state created at root at your App and another state that comes from your reducer.
Your button only flips the reducer show state. But to render TaskItem you use the stale show state created, that is always true. If you were using the redux state there wouldn't be any effect at all:
// show is always true unless you pass state from your reducer like:
// const show = useSelector((state) => state.toggleDoneTasks.show);
(show || !task.done) && (
<TaskItem
tasks={taskItems}
index={i}
task={task}
key={task.id}
show={show}
toggleDoneTask={() => toggleDoneTask(task)}
/>
)
so you should remove your React show state:
const [show, setShow] = useState(JSON.parse(localStorage.getItem('show')) || true)
Or remove your reducer, but for learning purposes you can keep the reducer.
Sandbox without the extra state, and no effects:
About the accordion effect
Given that you won't have anymore the accordion effect. For you to solve that you can either:
pick up a react animation library that handles transition effects on component mounting and unmounting of your preference;
render all Tasks (without (show || !task.done) condition). And keep track at each Task how many tasks are done until that given Task. With that you can do some logic like:
const indexToPass = show ? index : index - doneTasksBefore
...
<TR task={task} tasks={tasks} index={indexToPass}>
today I offer you a new challenge
my problem and the next
in the following code
I map objects in my database
I also map a list of items
and so basically I would like to pass my database and not the Items table
basically I want to do exactly the same thing as in the following code except that instead of using the items array, I would like to be able to use the data array which contains my database
do you have any idea how to do this?
I hope I was precise thanks to you for the help Neff
ps: Sorry for the length of the code i try to do my best to clean a little
import React, { Component } from 'react';
import { CardText, Card,Row, Col, Button } from 'reactstrap';
import axios from 'axios'
import GridLayout from 'react-grid-layout';
import SmsForm from './Sms/SMSForm'
import FruitList from './FruitList'
import './AdminPage.scss'
const UP = -1;
const DOWN = 1;
const entrypoint = process.env.REACT_APP_API_ENTRYPOINT+'/api';
class AdminPage extends Component {
constructor(props) {
super(props);
this.state = {
items: [
{ id: 1, name: "orange", bgColor: "#f9cb9c" },
{ id: 2, name: "lemon", bgColor: "#fee599" },
{ id: 3, name: "strawberry", bgColor: "#e06666" }
],
data: [],
}
handleMove = (id, direction) => {
const { items } = this.state;
const position = items.findIndex(i => i.id === id);
if (position < 0) {
throw new Error("Given item not found.");
} else if (
(direction === UP && position === 0) ||
(direction === DOWN && position === items.length - 1)
) {
return; // canot move outside of array
}
const item = items[position]; // save item for later
const newItems = items.filter(i => i.id !== id); // remove item from array
newItems.splice(position + direction, 0, item);
this.setState({ items: newItems });
};
// rest of the component
onHandleChange(event) {
const name = event.target.getAttribute('name');
this.setState({
message: { ...this.state.message, [name]: event.target.value }
});
}
getRandom = async () => {
const res = await axios.get(
entrypoint + "/alluserpls"
)
this.setState({ data: res.data })
}
componentDidMount() {
this.getRandom()
}
render() {
let datas = this.state.data.map(datass => {
const status = JSON.parse(localStorage.getItem("validated-order") || "{}")[datass.id];
return (
<div>
< Col sm="12" key={datass.id} className="wtfFuHereIsForOnlyBackGroundColorForCol12Nice">
<FruitList fruitList={this.state.items} onMove={this.handleMove} />
<GridLayout className="GridlayoutTextOnlyForGridOuiAndHeigthbecauseHeigthWasBug" layout={layout} cols={12} rowHeight={30} width={1200}>
<div key="a">
<Card body className="yoloCardBodyForbackGroundAndRaduisBorderForAdminPageWtf">
<CardText className="cardTextForAdminPageForDataName"> Commande de {datass.name}</CardText>
</Card>
</div>
</ Col>
</div>
)
})
return (
<div> <div>
<Row className="wtfHereIsAgainOnlyForRowAndMarginForAdminPageJustWtf">
<div className="isJustForOnlyPaddingOnRowForAdminPage" >
<div className="navBarGridMenuAdminPage">
<div className="thatIsOnlyForSpaceArroundOnDivAdminPage">
<CardText className="maybeColForAdminPageOuiOui"> Nouvelles commandes </CardText>
</div>
</div>
<div>
{datas}
</div>
</div>
</Row>
</div>
<div className="box">
</div>
</div>
)
}
}
export default AdminPage
here my second components
import React from "react";
const LEFT = -1;
const RIGHT = 1;
class FruitList extends React.Component {
render() {
const { fruitList, onMove } = this.props;
return (
<div style={{ display: "flex" }}>
{fruitList.map(item => (
<div
key={item.id}
style={{
backgroundColor: item.bgColor,
display: "flex"
}}
>
<div className="fruitsArrows">
<a onClick={() => onMove(item.id, LEFT)}>←</a>
</div>
<div className="fruitsId">{item.id}</div>
<div className="fruitsName">{item.name}</div>
<div className="fruitsArrows">
<a onClick={() => onMove(item.id, RIGHT)}>→</a>
</div>
</div>
))}
</div>
);
}
}
export default FruitList;
To delete particular list do like this-
pass your item(object).
<a onClick={() => onMove(item)}>→</a>
handleMove function
handleMove = (row) => {
let filtered = this.state.items.filter(item=>item.id!==row.id);
this.setState({ items: filtered});
};
Context
All of actions are keyboard only, no mouse click.
Example:
I have a 2 column table which has file and button.
When press enter key on button1, menu (contains download, view, delete) will popup. The focus is on button1
Press down arrow key now, the download background is blue highlighted.
My problem
I want download button to be focus instead of button1, when pressing down arrow on button1
Full code:
import React, {useState, useEffect, useRef, createRef, useContext} from 'react';
const AppContext = React.createContext({
name: 'AppContext'
});
const createMenuItemRefs = (items, rowInd) => {
// obj
let menuItemRefs = {};
// loop each
for (let i = 0; i < Object.keys(items).length; i++) {
// When assign createRef, no current
menuItemRefs[rowInd + i] = createRef();
}
return menuItemRefs;
};
function Menu({buttonName, parentRowIndex}) {
const [currRowInd, setCurrRowInd] = useState('');
const [open, setOpen] = useState(false);
// press down key, will get 1st item which at index 0
const [menuItemActiveIndex, setMenuItemActiveIndex] = useState(-1);
const menuItems = {download: 'download', view: 'view', delete: 'delete'};
const menuItemRefs = useRef(createMenuItemRefs(menuItems, parentRowIndex));
useEffect(() => {
if (open && menuItemActiveIndex >= 0 && parentRowIndex !== '') {
menuItemRefs.current[parentRowIndex + menuItemActiveIndex].focus();
}
}, [menuItemActiveIndex, open, parentRowIndex]);
// on the button level
const buttonIconKeyDown = (event, parentRowIndex) => {
if (event.keyCode === 13) {
// Enter pressed
console.log('enter is pressed');
setOpen(!open);
setCurrRowInd(parentRowIndex);
} else if (event.keyCode === 9) {
// tab away
console.log('tab away');
setOpen(!open);
setCurrRowInd('');
} else if (event.keyCode === 40) {
//test
console.log('down arrow');
// 38 is up arrow
// No scrolling
event.preventDefault();
// set to 1st item in 0 index
setMenuItemActiveIndex(0);
}
};
//test
console.log(
'menuItemRefs.current',
menuItemRefs.current,
'menuItemActiveIndex',
menuItemActiveIndex
);
return (
<div>
<button
onKeyDown={event => {
//test
console.log('parent buttonicon onkeydown: ');
buttonIconKeyDown(event, parentRowIndex);
}}
>
{buttonName}
</button>
{open && parentRowIndex === currRowInd && (
<ul style={{padding: '5px', margin: '10px', border: '1px solid #ccc'}}>
{Object.keys(menuItems).map((item, itemIndex) => {
if (itemIndex === menuItemActiveIndex)
return (
<li
key={itemIndex}
style={{
listStyle: 'none',
padding: '5px',
backgroundColor: 'blue'
}}
// put a ref
ref={element =>
(menuItemRefs.current[parentRowIndex + itemIndex] = element)
}
>
<button>{item}</button>
</li>
);
else
return (
<li
key={itemIndex}
style={{listStyle: 'none', padding: '5px'}}
ref={element =>
(menuItemRefs.current[parentRowIndex + itemIndex] = element)
}
>
<button>{item}</button>
</li>
);
})}
</ul>
)}
</div>
);
}
function TableElement() {
const items = [
{
file: 'file1',
button: 'button1'
},
{
file: 'file2',
button: 'button2'
},
{
file: 'file3',
button: 'button3'
}
];
return (
<table style={{borderCollapse: 'collapse', border: '1px solid black'}}>
<tbody>
{items.map((item, index) => {
return (
<tr key={index}>
<td style={{border: '1px solid black'}}>
{item.file}
</td>
<td style={{border: '1px solid black'}}>
<Menu buttonName={item.button} parentRowIndex={index} />
</td>
</tr>
);
})}
</tbody>
</table>
);
}
function App() {
const appContextObj = {};
return (
<>
<AppContext.Provider value={appContextObj}>
<TableElement />
</AppContext.Provider>
</>
);
}
export default App;
github
https://github.com/kenpeter/key-mouse-dropdown/tree/feature/focus
In your useEffect block
useEffect(() => {
if (open && menuItemActiveIndex >= 0 && parentRowIndex !== '') {
menuItemRefs.current[parentRowIndex + menuItemActiveIndex].children[0].focus();
}
}, [menuItemActiveIndex, open, parentRowIndex]);
You can call focus on your children in this case <button> instead of focusing on your <li> instead. Please note that if the code above will always focus on the first child of your ref current instance.
Bonus :
You can also use tabIndex={-1} on any elements to prevent it from being focused.