position absolute - float within screen limits (React) - css

I'm trying to create an App with a global dictionary; so that when a word that appears in the dictionary is hovered than a small box appears next to it with a definition.
The problem is that the text in the dictionary can appear any where on the screen, and I need to align the floating box so that it will not be displayed out side of the screen
Similar to this
only that I need to be able to style the floating box, like this
Note that the box display outside of the screen:
I tired to use ui material ToolTip
but it throws
TypeError
Cannot read property 'className' of undefined
I solved a similar problem before with jQuery, where I dynamically calculated the position of the box, relative to the screen and the current element.
but I don't know how to do it in react, mainly since I don't know how to get the position of the current element dynamical.
Please help

To give an idea where to start, have a look at useCallback and refs for React. With the given information from node.getBoundingClientRect(), you could calculate if your tooltip is outside the visible area of the browser.
// edit: useCallback won't work in this case, because the visibility is triggered by a css hover and the dimensions are not yet available for the hidden tooltip. Here is a possible solution with useRef and use useEffect though:
function ToolTip({ word, description }) {
const [left, setLeft] = useState(0);
const [hover, setHover] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
const { right } = ref.current.getBoundingClientRect();
if (window.innerWidth < right) {
setLeft(window.innerWidth - right);
}
}
}, [hover]);
return (
<span
desc={description}
className="dashed"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => {
setHover(false);
setLeft(0);
}}
>
{word}
<div ref={ref} className="explain" style={{ left }}>
<h2>{word}</h2>
{description}
</div>
</span>
);
}
Codepen example: https://codesandbox.io/s/lk60yj307

I was able to do it with
https://www.npmjs.com/package/#syncfusion/ej2-react-popups
But I still wonder what is the correct way to do it in code.

Related

change global css element

i've made this banner like screen that appears when my site is loaded, but here's the thing, i don't want no scrollbar while this opening animation it's happening, i only want to show the other components (the scrollbar and the whole site) once the gsap animation finishes, how could i proceed? thanks! (i tried to create a function to control those global elements, is it a way?)
So if I understand correctly you need the Banner to be displayed until the site is loaded. Maybe you are making some API calls or in general, you are planning to show the banner for let's say 3 sec and post that you want your actual components to be displayed.
You can try below approch:
export const APP = (): JSX.Element => {
const [isAnimationInProgress, SetAnimationState] = React.useState(true);
React.useEffect(() => {
// You can have your page load API calls done here
// Or wait for 'X' seconds
// Post that set the AnimationState to false to render actual components
setAnimationState(false);
})
return (
{
isAnimationInProgress && <Banner />
}
{
!isAnimationInProgress && <ActualComponent />
}
)
}
Regarding scrollbars, including overflow: hidden; in style for the banner should do the work if you are getting scrollbars for the Banner component.

css transform: translate transition behaving strangely

On this sandbox, I've recreated the classic sliding-puzzle game.
On my GameBlock component, I'm using a combination of css transform: translate(x,y) and transition: transform in order to animate the sliding game-pieces:
const StyledGameBlock = styled.div<{
index: number;
isNextToSpace: boolean;
backgroundColor: string;
}>`
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: ${BLOCK_SIZE}px;
height: ${BLOCK_SIZE}px;
background-color: ${({ backgroundColor }) => backgroundColor};
${({ isNextToSpace }) => isNextToSpace && "cursor: pointer"};
${({ index }) => css`
transform: translate(
${getX(index) * BLOCK_SIZE}px,
${getY(index) * BLOCK_SIZE}px
);
`}
transition: transform 400ms;
`;
Basically, I'm using the block's current index on the board in order to calculate it's x and y values which change the transform: translate value of the block when it's being moved.
While this does manage to trigger a smooth transition when sliding the block to the top, to the right and to the left - for some reason, sliding the block from top to bottom doesn't transition smoothly.
Any ideas what's causing this exception?
React, lists and keys
What you're seeing is the result of a mount/unmount of the <GameBlock /> components.
Although you're passing a key prop to the component, React is unsure that you're still rendering the same element.
If I have to guess why react is uncertain, I would put the culprit at:
Changing the array sort with:
const previousSpace = gameBlocks[spaceIndex];
gameBlocks[spaceIndex] = gameBlocks[index];
gameBlocks[index] = previousSpace;
having different virtual DOM results using the conditional on isSpace:
({ correctIndex, currentIndex, isSpace, isNextToSpace }) => isSpace ? null : ( <GameBlock ....
Usually in applications, we don't mind a re-mount since it's pretty fast. When we attach an animation, we don't want any re-mounts since they mess with the css-transitions.
in order for react to be certain it's the same node and no re-mount is needed. we should take care that; between renders; the virtual dom stays mostly the same.
we can achieve that not doing anything fancy in the render of the list, and passing down the same keys between renders.
Pass isSpace down
Instead of changing the the rendered DOM nodes, we want the list render to always return an equal amount of nodes, with the exact same keys for each Node, in the same order.
simply passing 'isSpace' down and styling as display:none; should do the trick.
<GameBlock
...
isSpace={isSpace}
...
>
const StyledGameBlock = styled.div<{ ....}>`
...
display: ${({isSpace})=> isSpace? 'none':'flex'};
...
`;
Making sure to not change the arraysort
React considers the gameBlocks array to be modified, the keys are in a different order. Thus triggering unmount/mount of the rendered <GameBlock/> components.
We can make sure that react considers this array to be unmodified, by only changing the properties of the items in the list and not the sort itself.
in your case, we can leave all properties as is, only changing the currentIndex for the blocks that are moved/swapped with each other.
const onMove = useCallback(
(index) => {
const newSpaceIndex = gameBlocks[index].currentIndex; // the space will get the current index of the clicked block.
const movedBlockNewIndex = gameBlocks[spaceIndex].currentIndex; // the clicked block will get the index of the space.
setState({
spaceIndex: spaceIndex, // the space will always have the same index in the array.
gameBlocks: gameBlocks.map((block) => {
const isMovingBlock = index === block.correctIndex; // check if this block is the one that was clicked
const isSpaceBlock =
gameBlocks[spaceIndex].currentIndex === block.currentIndex; // check if this block is the space block.
let newCurrentIndex = block.currentIndex; // most blocks will stay in their spot.
if (isMovingBlock) {
newCurrentIndex = movedBlockNewIndex; // the moving block will swap with the space.
}
if (isSpaceBlock) {
newCurrentIndex = newSpaceIndex; // the space will swap with the moving block
}
return {
...block,
currentIndex: newCurrentIndex,
isNextToSpace: getIsNextToSpace(newCurrentIndex, newSpaceIndex)
};
})
});
},
[gameBlocks, spaceIndex]
);
...
// we have to be sure to call onMove the with the index of the clicked block.
() => onMove(correctIndex)
The only things we've changed are is the currentIndex of the clicked block and the space.
sandbox:
sandbox example based on your provided sandbox.
closing thoughts: I think your code was easy to read and understand, good job on that!
Additionally to the excellent answer and explanations #Lars provided, I wanted to share visual proof that certain <GameBlock /> components are indeed unmounted or changed in order, causing the hiccup in the CSS animation.
As you can see, when focussing one of the blocks and sliding down, the element changes its position in the DOM.

cdkDragPreview and cdkDragPlaceholder not working as expected with horizontal elements

I'm using display-inline in my elements as I want them to lay out horizontally instead of vertically as they do in most of the Angular examples. That said, the CSS behaves very weird. I'm specifically having two issues.
When I click an element to drag it, it grows (shrinks back to regular size after dropped). I'm not sure exactly why this happens but it is definitely not desired. I've tried numerous things to fix this via both css and adding a cdkDragPreview element with matchSize present (this seems to be the method Angular recommends). All of those efforts failed. I came across the following bug report that seems similar to my issue: https://github.com/angular/components/issues/19060. I noted that the bug was closed, so I don't know if that means it has been fixed.
When I start to drag an element from the bottom drop list, the remaining items move around sporadically while that element is still in the drop list (when it goes out of the bottom drop list they behave as I would expect them to). I created a hide style for the cdkDragPlaceholder as this seems to be how Angular provides control of this but it only helped with the top drop lists and seemed to have no effect on the bottom.
Here is a link that illustrates both issues on StackBlitz: https://stackblitz.com/edit/spuzzler. I'm guessing that my issue can be fixed with CSS, but I can't figure out how.
Create a cdkDropList for each word. My idea is to have an outer div and an inner div that is really the element that is dragged. More over, I fixed the size of the outer div. So, when you drag, there's no re-order of the words (simply leaves an empty space instead of the word you drag)
You can see the result in this stackblitz
<div #contenedor class="categories" cdkDropListGroup>
<ng-container *ngFor="let item of items;let i=index">
<div class="categories-item" cdkDropList
cdkDropListOrientation="horizontal"
[cdkDropListData]="{item:item,index:i}" (cdkDropListDropped)="drop($event)" >
<div class="inner" cdkDrag>
<div *cdkDragPlaceholder></div>
<div class="categories-item-drag" *cdkDragPreview matchSize="true" >
<div class="inner">{{item}}</div>
</div>
{{item}}
</div>
</div>
</ng-container>
</div>
I use an observable that returns an array or words. In subscribe I equal to item and, using a setTimeout() add the size to the outter div
export class AppComponent implements OnInit {
#ViewChildren(CdkDropList, { read: ElementRef }) pills: QueryList<ElementRef>;
constructor(private renderer: Renderer2) {}
items: any[];
positions: any[];
ngOnInit() {
this.getParragraf().subscribe(res => {
this.items = res;
setTimeout(() => {
this.pills.forEach(x => {
this.renderer.setStyle(
x.nativeElement,
"width",
x.nativeElement.getBoundingClientRect().width + "px"
);
});
});
});
}
drop(event: CdkDragDrop<any>) {
this.items.splice(event.previousContainer.data.index, 1);
this.items.splice(event.container.data.index,0,event.previousContainer.data.item)
/* if we want to interchange the words, replace the two lines by*/
//this.items[event.previousContainer.data.index]=event.container.data.item
//this.items[event.container.data.index]=event.previousContainer.data.item
//event.currentIndex=0;
}
getParragraf() {
return of(
"Let him who walks in the dark, who has no light, trust in the name of the Lord and rely on his God.".split(
" "
)
);
}
}
Updated Really you needn't make a cdkDropListGroup, you can take advantage of [cdkDropListConnectedTo]. For this, you have two arrays: words and items
if res is an array of strings, you can have
this.items = res.map((x,index)=>({value:x,ok:false,id:'id'+index}));
this.words=res.map(x=>({o:Math.random(),value:x}))
.sort((a,b)=>a.o-b.o)
.map(x=>(
{value:x.value,
connected:this.items.filter(w=>x.value==w.value).map(x=>x.id)
}))
and use item.value,item.id and word.value,word.connected
See a new stackblitz

Option box width size change in react-select

I would like to change react-select option box width.
example here=>
If my content is larger than the option, its show horizontal scroll but I don't want horizontal scroll. How can I change the size of option box width?
Another thing is How can I show the selected value as CscID one even option box is showing CscID + CscDesc? Now when I select the option, its CscID + CscDesc is showing in the selected box.
Here is my Select =>
const formatOptionLabel = ({ CscID, CscDesc }) => (
<div style={{ display: "flex"}}>
<div>{CscID}</div>
<div>{CscDesc}</div>
</div>
);
const customStyles = {
control: styles => ({ ...styles, }),
option: (styles) => {
return {
...styles,
width: '10000px', //For testing
};
},
};
<Select
styles={customStyles}
formatOptionLabel={formatOptionLabel}
getOptionValue={option =>
`${option.CscID}`
}
options={datasource}
/>
Yes, I can edit the width of option box.
const customStyles = {
control: styles => ({ ...styles,
}),
option: styles => ({ ...styles,
}),
menu: styles => ({ ...styles,
width: '500px'
})
};
According to the documentation, Its called menu. If you want to update the other style, check here=> Style Keys
And this option is super helpful for inspecting the menu box => menuIsOpen={true}
You normally don't change width of the wrapping element in your use case. As the text limit can be any so you wont set the width of select according to the text but the text according to your select's width. Meaning the select will have a fixed width and the text exceeding the the fixed width must be converted to ellipsis through css like
.options-select {
overflow: ellipsis
}
Ellipsis on hover must show the fullname in a tooltip for better UX.
For re-size problem, you may refer to this existing answer: React Select auto size width
For second problem, I don't think it is possible by using react-Select.
If you go to github and look further into the source code of react-Select, you may find that the ValueType of value property is expected to be same as OptionsType of options.
To be precise, the code is "export type ValueType = OptionType | OptionsType | null | void;", which could be found in react-select/packages/react-select/src/types.js
For your convenience, this is the link towards its source code, which can help you figure out more hidden behaviors: https://github.com/JedWatson/react-select/tree/master/packages/react-select/src

CSS Transitions don't survive React Component changing inner DOM structure

I have illustrated the problem in this CodePen
const Component = ({ structure }) => {
switch (structure) {
case 'nested':
return (
<div>
<AnimatedComponent />
</div>
);
case 'flat':
return
<AnimatedComponent />
;
}
};
There's some logic in AnimatedComponent that changes the styling of the Component in an animated fashion, e.g. change the background color from black to red over a duration of 1 second. The animation is started by changing a color class on AnimatedComponent. There is CSS to handle the animation given the changed class.
When changing the DOM structure from nested to flat, the HTML element is destroyed and recreated, the transition starting state is lost (aka the browser doesn't know which class was set before because the element was newly created).
What I want React to do is to change the DOM structure with moving elements in new positions, not destroying and recreating them.
Is this possible?
I tried to use the key props on <AnimatedComponent />, but it only fixes the flash of DOM change. Animation is skipped. See Codepen. Thanks Thomas Rooney for this suggestion.
Can I tell React to apply the class changes just one tick after the position of the DOM element was changed?
Can I tell React to apply the class changes just one tick after the position of the DOM element was changed?
Yes, this is precisely what the setTimeout function is for. Copying your second example, where you fixed the flickering, wrapping your color action dispatch with setTimeout (with no time value, which defaults to 0), seems to fix your issue.
onColorClick: () => {
setTimeout(() => {
dispatch({type: 'TOGGLE_COLOR'})
})
},
codepen
Update: I've noticed it's a bit more reliable to add some time before the color change (second argument in setTimeout, (fn, ms). I believe this is because setState is also happening asynchronously.
onColorClick: () => {
setTimeout(() => {
dispatch({type: 'TOGGLE_COLOR'})
}, 100) <-- play around with this value
},

Resources