React CSS animations that happen one after another using hooks - reactjs

I am trying to make something where I have 8 images, with 4 shown at a time. I need them to have an animation when they enter and an animation when they exit. The first 4 exit when one of them is clicked. But I only see the enter animation. I assume because the enter and exit animation happen at the same time.
Is there a way to add a delay or something similar using hooks to make one happen before the other that is compatible with internet explorer (super important)? Code shown below:
const [question, setQuestion] = React.useState(props.Questions[0]);
const [animate, setAnimate] = React.useState("enter");
React.useEffect(() => {
if (Array.isArray(props.Questions) && props.Questions.length) {
// switch to the next iteration by passing the next data to the <Question component
setAnimate("enter");
setQuestion(props.Questions[0]);
} else {
// if there are no more iterations (this looks dumb but needs to be here and works)
$("#mrForm").submit();
}
});
const onStubClick = (e, QuestionCode, StubName) => {
e.preventDefault();
// store selected stub to be submitted later
var newResponse = response.concat({ QuestionCode, StubName });
setResponse(newResponse);
setAnimate("exit");
// remove the first iteration of stubs that were already shown
props.Questions.splice(0, 1);
if (props.QuestionText.QuestionTextLabel.length > 1) {
// remove first iteration of questiontext if applicable
props.QuestionText.QuestionTextLabel.splice(0, 1);
props.QuestionText.QuestionTextImage.splice(0, 1);
// switch the question text
setQuestionLabel({ Label: props.QuestionText.QuestionTextLabel[0], Image: props.QuestionText.QuestionTextImage[0] });
}
};

Related

Triggering a lexical.js mentions menu programatically when clicking on a mention

What I need
Let's start with The mentions plugin taken from the docs.
I would like to enhance if with the following functionality:
Whenever I click on an existing MentionNode, the menu gets rendered (like it does when menuRenderFunction gets called), with the full list of options, regardless of queryString matching
Selecting an option from menu replaces said mention with the newly selected one
Is there a way to implement this while leaving LexicalTypeaheadMenuPlugin in control of the menu?
Thank you for your time 🙏🏻
What I've tried
I figured that maybe I could achieve my desired behaviour simply by returning the right QueryMatch from triggerFn. Something like this:
const x: FC = () => {
const nodeAtSelection = useNodeAtSelection() // Returns LexicalNode at selection
return (
<LexicalTypeaheadMenuPlugin<VariableTypeaheadOption>
triggerFn={(text, editor) => {
if ($isMentionsNode(nodeAtSelection)) {
// No idea how to implement `getQueryMatchForMentionsNode`,
// or whether it's even possible
return getQueryMatchForMentionsNode(nodeAtSelection, text, editor)
}
return checkForVariableBeforeCaret(text, editor)
}}
/>
)
}
I played around with it for about half an hour, unfortunately I couldn't really find any documentation for triggerFn or QueryMatch, and haven't really made any progress just by messing around.
I also thought of a potential solution the I think would work, but feels very hacky and I would prefer not to use it. I'll post it as an answer.
So here is my "dirty" solution that should work, but feels very hacky:
I could basically take the function which I provide to menuRenderFn prop and call it manually.
Let's say I render the plugin like this:
const menuRenderer = (
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }
) => { /* ... */}
return (
<LexicalTypeaheadMenuPlugin menuRenderFn={menuRenderer} /* ... other props */ />
)
I could then create a parallel environment for rendering menuRenderer, something like this:
const useParallelMenu = (
menuRenderer: MenuRenderFn<any>,
allOptions: TypeaheadOption[],
queryString: string
) => {
// I could get anchor element:
// 1. either by using document.querySelector("." + anchorClassName)
// 2. or by extracting it from inside `menuRenderFn`:
// menuRenderFn={(...params) => {
// extractedRef.current = params[0].current;
// return menuRenderer(...params)
// }}
const anchorEl = x
const [selectedIndex, setHighlightedIndex] = useState(0)
const nodeAtSelection = useNodeAtSelection() // Returns LexicalNode at selection
const selectOptionAndCleanUp = (option: TypeaheadOption) => {
// Replace nodeAtSelection with new MentionsNode from `option`
}
return () =>
$isMentionsNode(nodeAtSelection) &&
menuRenderer(
anchorEl,
{
selectedIndex,
setHighlightedIndex,
selectOptionAndCleanUp,
options: allOptions
},
queryString
)
}
On paper, this seems like a viable approach to me... but I would really prefer not to have to do this and instead let LexicalTypeaheadMenuPlugin manage the state of my menu, as it is intended to do.

How to use a main loop in a Gnome Extension?

I want to display the content of a file in the Gnome top bar and update the display when the content of the file changes.
For now I have just a skeleton extension that writes Hello world in the top bar.
I have tried to make a loop that should update the text:
File: extension.js
const {St, Clutter} = imports.gi;
const Main = imports.ui.main;
let panelButton;
function init () {
// Create a Button with "Hello World" text
panelButton = new St.Bin({
style_class : "panel-button",
});
let panelButtonText = new St.Label({
text : "Hello World",
y_align: Clutter.ActorAlign.CENTER,
});
panelButton.set_child(panelButtonText);
let i = "x"
const Mainloop = imports.mainloop;
let timeout = Mainloop.timeout_add_seconds(2.5, () => {
i = i + "x"
panelButtonText.set_text(i)
});
}
function enable () {
// Add the button to the panel
Main.panel._rightBox.insert_child_at_index(panelButton, 0);
}
function disable () {
// Remove the added button from panel
Main.panel._rightBox.remove_child(panelButton);
}
I expect the Hello world text to change multiple times but it stops at xx:
I have tried to do the same with date and time but it does not work either:
const GLib = imports.gi.GLib;
let now = GLib.DateTime.new_now_local();
let nowString = now.format("%Y-%m-%d %H:%M:%S");
panelButtonText.set_text(nowString)
Date and time does not update!
You'll need to return GLib.SOURCE_CONTINUE (true) for the event to keep looping, or GLib.SOURCE_REMOVE (false) for it to exit. Because you are not returning a value from your callback, it is being coerced from undefined to false and only run once.
More notes:
you will want to use GLib's functions now, not the MainLoop import, which is deprecated
you will want to store the returned GLib.Source ID (i.e. timeout), probably in the same scope as panelButton, so that you can remove it in disable().
There is a guide for using the mainloop on gjs.guide at https://gjs.guide/guides/gjs/asynchronous-programming.html#the-main-loop

Skipping through a video with a single keypress

I have a React video player project and I have a way of skipping the video by 1 second when a keyDown event occurs. This is effective because if you hold the key down multiple events are fired so the video continues to skip at e.g. 5-second intervals up until the user releases the key.
I now want to implement it differently so when a user taps the key down once and then, even if they lift their finger the video carries on skipping until the user presses the key down once again (so they don't need to hold the key down)
How do I do this? This is a section of my code so far. It's actually for a Smart TV APP so any reference you read regards focus is just the navigation pattern for selecting divs with your TV remote. The main bit of code is inside the keyDown function that then calls handleSkipForward or handleSkipBack. Also the videoElement ref is shared across functional components using store.VideoElement.
I was thinking of something like having a boolean state hook for skipping but when that state is true how do I repeatedly request a skip of 5 seconds or is there something within the video element that you can just set to progress through a video at a set rate?
focusKey,
elementRef: containerRef,
onKeyDown: (event) => {
// if (!playerControlsVisible) {
// event.stopPropagation()
// event.preventDefault()
// return
// }
if (event.key === "Enter" || event.key === " ") {
skipDirection === "Forward"
? handleSkipForwardAction()
: handleSkipBackwardAction()
}
},
onFocus: (event) => {},
})
function handleSkipForwardAction() {
if (store.videoElement.currentTime + skipTime < duration) {
store.videoElement.currentTime += skipTime
} else if (store.videoElement.currentTime < duration) {
store.videoElement.currentTime = duration
} else return
}
function handleSkipBackwardAction() {
if (store.videoElement.currentTime - skipTime > 0) {
store.videoElement.currentTime -= skipTime
} else if (store.videoElement.currentTime > 0) {
store.videoElement.currentTime = 0
} else return
}```
It should be simple. What you need to do is implement the setInterval function.
You can just add an interval (infinite loop) and store the Interval ID on a state so that you can stop that infinite loop using the clearInterval function.
onKeyDown: (event) => {
if (event.key === "Enter" || event.key === " ") {
if (moveForward){
clearInterval(moveForward);
// Assuming you use Functional Component and `useState` hooks.
setMoveForward(null);
return;
}
const action = skipDirection === "Forward"
? handleSkipForwardAction
: handleSkipBackwardAction;
setMoveForward(setInterval(action,100));
// 100 is in milisecond. so 1000 = 1 second.
// 100 = 10 function call in 1 second. Meaning 6 second skip/second.
// You can adjust the calculation on your own.
}
},
This is not tested, so do let me know if it works or not. But the idea is to use a looping function when the user clicks the specific button and stop the looping function when the user clicks the specific button again.
References:
setInterval
clearInterval

Event handler not picking up correct state value

I am developing a simple Battleships board game and need to increment a state variable when a player clicks on a cell with a valid move.
CodeSandbox: https://codesandbox.io/s/trusting-leakey-6ek9o
All of the code pertaining to this question is found within the following file: >src>GameboardSetup>GameboardSetup.js
The state variable in question is called placedShips. It is designed to keep track of how many ships have been placed onto the board. The variable is initialised to 0 and is supposed to increment up until it reaches a set integer value:
const [placedShips, setPlacedShips] = useState(0);
When the user clicks on the grid, an onClick handler fires. If the cell was a valid cell, then a ship should be placed into an array and the value of placedShips should increment. When the value increments, a new ship is then selected to be placed on a subsequent click. Each new ship has a different length.
Currently, when the user clicks on a valid grid cell, a ship is correctly placed into the array. The issue arises on subsequent valid clicks, whereby the same ship is then placed into the array. This issue is being driven by the onClick handler apparently not receiving an updated state value for placedShips.
While I can see from a { useEffect } hook that the state is in fact increment, the placedShips variable within the event handler I believe is constantly set to 0. Below is how I believe I have validated this issue.
Here is the onClick event handler, containing a console log for the state variable:
const onClickHandler = (e) => {
console.log(placedShips)
let direction = ships[placedShips].direction;
let start = parseInt(e.target.id);
let end = start + ships[placedShips].length - 1;
console.log(playerGameboard.checkIfShipPresent());
if ((playerGameboard.checkValidCoordinates(direction, start, end)) && (!playerGameboard.checkIfShipPresent(direction, start, end))) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips(oldValue => oldValue + 1);
}
}
and here is the { useEffect } hook with another console log for the state variable:
useEffect(() => {
console.log(placedShips)
}, [placedShips])
By selecting multiple valid cells, this is the console log output:
GameboardSetup.js:70 0 <--- Event handler console log
GameboardSetup.js:74 false
GameboardSetup.js:143 1 <--- useEffect console log
GameboardSetup.js:70 0
GameboardSetup.js:74 false
GameboardSetup.js:143 2
GameboardSetup.js:70 0
GameboardSetup.js:74 false
GameboardSetup.js:143 3
You can see that the event handler console log always reports placedShips as 0. Whereas the { useEffect } hook shows it incrementing with each successive click.
Apologies for the longwinded question, I have been stuck on this problem for 2 weeks now and haven't made any significant progress with it. Any advice would be hugely appreciated.
Thank you!
Your onClickHandle is not updated when the UI Render. Move it into your return. and it works fine. No need to use humanSetUpGrid. Just get the result from createUiGrid. So cells will be updated when app render.
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return (
<div
className="cell"
id={counter}
onClick={onClickHandler}
onMouseOut={onMouseOutHandler}
onMouseOver={onMouseOverHandler}
/>
);
});
return result;
};
<div className="setup-grid">
<Table grid={createUiGrid()} />
</div>

Cypress while loop [duplicate]

I have 15 buttons on a page. I need to test each button.
I tried a simple for loop, like
for (var i = 1; i < 15; i++) {
cy.get("[=buttonid=" + i + "]").click()
}
But Cypress didn't like this. How would I write for loops in Cypress?
To force an arbitrary loop, I create an array with the indices I want, and then call cy.wrap
var genArr = Array.from({length:15},(v,k)=>k+1)
cy.wrap(genArr).each((index) => {
cy.get("#button-" + index).click()
})
Lodash is bundled with Cypress and methods are used with Cypress._ prefix.
For this instance, you'll be using the _.times. So your code will look something like this:
Cypress._.times(15, (k) => {
cy.get("[=buttonid=" + k + "]").click()
})
You can achieve something similar to a "for loop" by using recursion.
I just posted a solution here: How to use a while loop in cypress? The control of is NOT entering the loop when running this spec file? The way I am polling the task is correct?
Add this to your custom commands:
Cypress.Commands.add('recursionLoop', {times: 'optional'}, function (fn, times) {
if (typeof times === 'undefined') {
times = 0;
}
cy.then(() => {
const result = fn(++times);
if (result !== false) {
cy.recursionLoop(fn, times);
}
});
});
Then you can use it by creating a function that returns false when you want to stop iterating.
cy.recursionLoop(times => {
cy.wait(1000);
console.log(`Iteration: ${times}`);
console.log('Here goes your code.');
return times < 5;
});
While cy.wrap().each() will work (one of the answers given for this question), I wanted to give an alternate way that worked for me. cy.wrap().each() will work, but regular while/for loops will not work with cypress because of the async nature of cypress. Cypress doesn't wait for everything to complete in the loop before starting the loop again. You can however do recursive functions instead and that waits for everything to complete before it hits the method/function again.
Here is a simple example to explain this. You could check to see if a button is visible, if it is visible you click it, then check again to see if it is still visible, and if it is visible you click it again, but if it isn't visible it won't click it. This will repeat, the button will continue to be clicked until the button is no longer visible. Basically the method/function is called over and over until the conditional is no longer met, which accomplishes the same thing as a for/while loop, but actually works with cypress.
clickVisibleButton = () => {
cy.get( 'body' ).then( $mainContainer => {
const isVisible = $mainContainer.find( '#idOfElement' ).is( ':visible' );
if ( isVisible ) {
cy.get( '#idOfElement' ).click();
this.clickVisibleButton();
}
} );
}
Then obviously call the this.clickVisibleButton() in your test. I'm using typescript and this method is setup in a class, but you could do this as a regular function as well.
// waits 2 seconds for each attempt
refreshQuote(attempts) {
let arry = []
for (let i = 0; i < attempts; i++) { arry.push(i) }
cy.wrap(arry).each(() => {
cy.get('.quote-wrapper').then(function($quoteBlock) {
if($quoteBlock.text().includes('Here is your quote')) {
}
else {
cy.get('#refreshQuoteButton').click()
cy.wait(2000)
}
})
})
}
Try template literals using backticks:
for(let i = 0; i < 3; i++){
cy.get(`ul li:nth-child(`${i}`)).click();
}

Resources