Is it possible to make the HTML inside Tippy clickable? - reactjs

I'm working on a file sharing app. In the dashboard, I want to right click then a context menu appears and I can click on one of the options available in it. I can now display the context menu but it seems I can't click any button or element inside the menu.
Menu Component:
import Tippy from "#tippyjs/react/headless"
import { cloneElement, useState } from "react"
import { useLocation } from "react-router-dom"
import { followCursor } from "tippy.js"
import "tippy.js/dist/tippy.css"
import Options from "./Options"
type SubMenuProps = {
children: React.ReactElement
}
const NO_SUBMENU_PATHS = ["/", "/login", "/signup"]
const SubMenu = ({ children }: SubMenuProps) => {
const [visible, setVisible] = useState(false)
const { pathname } = useLocation()
const child = cloneElement(children, {
onContextMenu: (e: Event) => {
e.preventDefault()
setVisible(!visible)
},
})
if (NO_SUBMENU_PATHS.includes(pathname)) return <>{child}</>
return (
<Tippy
allowHTML
visible={visible}
onClickOutside={() => setVisible(false)}
render={Options}
followCursor="initial"
placement="right-start"
plugins={[followCursor]}
>
{child}
</Tippy>
)
}
export default SubMenu
I tried reading the docs and looking up online but found nothing helpful

Related

I want to get clientWidth of div element if it changes during render

I don't have access to sidebar div element because of microfrontend. I want to catch html renders during siderbar clientWidth changes. How can i access it.
Here are example code:
import { Box, BoxProps, HStack } from '#chakra-ui/react';
import React, { useEffect, useState } from 'react';
interface Props extends BoxProps {
leftActions?: React.ReactNode[];
rightActions?: React.ReactNode[];
}
export const FormFooter = ({ leftActions, rightActions, ...props }: Props) => {
const [sidebarWidth, setSidebarWidth] = useState(0);
useEffect(() => {
const sidebarWidth = document?.querySelector<any>('.css-ygpt8u')?.clientWidth;
console.log('sidebarWidth', sidebarWidth);
}, [document]);
return (
<Box
display={'flex'}
justifyContent={'space-between'}
{...props}
width={`calc(100vw - 460px)`}
right="84px"
backgroundColor="white"
>
<HStack key="form-left">{leftActions?.map((element) => element)}</HStack>
<HStack key="form-right">{rightActions?.map((element) => element)}</HStack>
</Box>
);
};
I tried to pass document inside array of useEffect but it gives me undefined.
One solution would be to use the dependency of typeof document instead of document alone.
useEffect(()=>{
Const clientWidth=document.getElementById(“someId”).clientWidth
},[typeof document])
Another solution is to use Refs on the sidebar div and its client width just as you do it with document. The difference is that refs directly change the Dom.

Need help creating a button component that when clicked, will scroll snap to the next section

I have created a generic section component that has things like a heading, copy, etc. being passed in as props, and a child button component that is supposed to move the user to the next heading when clicked. When a user clicks the top or bottom arrow, it should snap scroll them to the next section.
I'm having some trouble brainstorming how to tackle my issue above, I thought using document.getElementsByClassName('snap-start') would work, since each of my section components has that as a class name, but I'm using Nextjs, so I can't even use the document interface.
My components are ordered and embedded within each other like:
Index.tsx
 BodySection.tsx
  SnapScrollButton.tsx
BodySection.tsx code:
import React, { useState, useRef } from 'react';
import Carousel from './Carousel';
import SnapScrollButton from './SnapScrollButton';
type BodySectionProps = {
backgroundColor: string;
heading: string;
copy: string;
hasCarousel: boolean;
};
const BodySection: React.FunctionComponent<BodySectionProps> = (props) => {
const { backgroundColor, heading, copy, hasCarousel } = props;
const btnUseRef = useRef(document.getElementsByClassName('snap-start'));
const hasCopy = (copy: string) => {
if (copy != null) {
return <p>copy</p>;
}
};
return (
<div className={`p-8 h-full snap-start ${backgroundColor}`}>
<h1 className="text-7xl">{heading}</h1>
<p>{copy}</p>
<Carousel hasCarousel={hasCarousel} />
<SnapScrollButton btnRef={btnUseRef} />
</div>
);
};
export default BodySection;
SnapScrollButton.tsx code:
import React, { useEffect, useState, useRef } from 'react';
type SnapScrollButtonProps = {
btnRef: any;
};
const SnapScrollButton: React.FunctionComponent = () => {
const arrowRef = useRef<HTMLButtonElement>(null);
//document.getElementsByClassName('snap-start');
const moveToNextSection = () => {
arrowRef.current?.scrollIntoView();
};
return (
<div className="text-center">
<script>console.log(test);</script>
<button>V</button>
</div>
);
};
export default SnapScrollButton;
There are a couple of ways you could tackle this. You CAN use the document selector in NextJS, you just have to use it in the right place. If you use a useEffect hook, you can use the document selector and make a Nodelist of your sections and go from there.
Or you use a ref on your current section and when the button is clicked, scroll the window down the height of your current div compared to the window height.

Context API using the initial value not the one set in UseState

I just started using typescript with react and tried to deal with ContextAPI with the typescript.
So Far I've set a context and tried to use a provider inside my app.tsx.
My context file is looking like this:
import { createContext, ReactNode, useState } from "react";
type props = {
children: ReactNode
}
type GlobalContextType = {
currentValue: number;
setCurrentValue: (value: number) => void;
}
const INITIAL_VALUE = {
currentValue: 1,
setCurrentValue: () => {},
}
export const GlobalContext = createContext<GlobalContextType>(INITIAL_VALUE);
export const GlobalProvider = ({ children }: props) => {
const [currentValue, setCurrentValue] = useState(3);
return(
<GlobalContext.Provider value={{ currentValue, setCurrentValue }}>
{ children }
</GlobalContext.Provider>
)
}
while my app.tsx file is looking like this:
import './App.css';
import { useContext } from 'react';
import { GlobalContext, GlobalProvider } from './ContextAPI/GlobalContext';
function App() {
const { currentValue, setCurrentValue } = useContext(GlobalContext);
return (
<GlobalProvider>
<h1>{ currentValue }</h1>
<button onClick={() => setCurrentValue(currentValue + 1)}>Increment</button>
</GlobalProvider>
);
}
export default App;
There are few things that didn't work as I expected.
First: when I go to my localhost page it displays the inital value, which is 1, not the
one that I set using useState(3).
Second: When I click the button, it doesn't update the value.
I imagine that I'm always using the initial state value and not the one that I'm trying to set inside the provider.
Try to set the function increment in the context API then call it in the app right just after onClick event

React: A service returning a ui component (like toast)?

Requirement: Show toast on bottom-right corner of the screen on success/error/warning/info.
I can create a toast component and place it on any component where I want to show toasts, but this requires me to put Toast component on every component where I intend to show toasts. Alternatively I can place it on the root component and somehow manage show/hide (maintain state).
What I am wondering is having something similar to following
export class NotificationService {
public notify = ({message, notificationType, timeout=5, autoClose=true, icon=''}: Notification) => {
let show: boolean = true;
let onClose = () => {//do something};
if(autoClose) {
//set timeout
}
return show ? <Toast {...{message, notificationType, onClose, icon}} /> : </>;
}
}
And call this service where ever I need to show toasts.
Would this be the correct way to achieve the required functionality?
You can use AppContext to manage the state of your toast and a hook to trigger it whenever you want.
ToastContext:
import React, { createContext, useContext, useState } from 'react';
export const ToastContext = createContext();
export const useToastState = () => {
return useContext(ToastContext);
};
export default ({ children }) => {
const [toastState, setToastState] = useState(false);
const toastContext = { toastState, setToastState };
return <ToastContext.Provider value={toastContext}>{children}</ToastContext.Provider>;
};
App:
<ToastProvider>
<App/>
<Toast show={toastState}/>
</ToastProvider>
Then anywhere within your app you can do:
import {useToastState} from 'toastContext'
const {toastState, setToastState} = useToastState();
setToastState(!toastState);

React.js Dropdown component doesn't hide on button click

I'm facing a problem in the making of clickable Dropdown component. My task is to show a menu when a button is clicked and hide the menu when the user clicks anywhere in the document or whether a click on the same button, also all components should be functional components.
I'm using 3rd party package named classnames which help to conditionally join CSS classes, also using a React ContextAPI to pass props to Dropdown child components.
Dropdown component depends upon 2 child components.
DropdownToggle -
(Renders a clickable button)
DropdownMenu -
(Renders a div with menu items)
Problem:
Whenever I open a menu and click anywhere in the document menu works perfectly, but when I open a menu and want to hide with a button click it didn't work. I think the problem is inside the useEffect hook of the Dropdown component.
Codesandbox
Demo:
Here is the main App component which renders all components.
App.js
import React, { Component } from "react";
import Dropdown from "./Dropdown";
import DropdownToggle from "./DropdownToggle";
import DropdownMenu from "./DropdownMenu";
import "./dropdown.css";
// App component
class App extends Component {
state = {
isOpen: false
};
toggle = () => {
alert("Button is clicked");
this.setState({
isOpen: !this.state.isOpen
});
};
render() {
return (
<div className="app">
<Dropdown isOpen={this.state.isOpen} toggle={this.toggle}>
<DropdownToggle>Dropdown</DropdownToggle>
<DropdownMenu>
<div>Item 1</div>
<div>Item 2</div>
</DropdownMenu>
</Dropdown>
</div>
);
}
}
export default App;
Main src code:
DropdownContext.js
import {createContext} from 'react';
// It is used on child components.
export const DropdownContext = createContext({});
// Wrap Dropdown with this Provider.
export const DropdownProvider = DropdownContext.Provider;
Dropdown.js
import React, { useEffect } from "react";
import classNames from "classnames";
import { DropdownProvider } from "./DropdownContext";
/**
* Returns a new object with the key/value pairs from `obj` that are not in the array `omitKeys`.
* #param obj
* #param omitKeys
*/
const omit = (obj, omitKeys) => {
const result = {};
// Get object properties as an array
const propsArray = Object.keys(obj);
propsArray.forEach(key => {
// Searches the array for the specified item, if the item is not found it returns -1 then
// construct a new object and return it.
if (omitKeys.indexOf(key) === -1) {
result[key] = obj[key];
}
});
return result;
};
// Dropdown component
const Dropdown = props => {
// Populate context value based on the props
const getContextValue = () => {
return {
toggle: props.toggle,
isOpen: props.isOpen
};
};
// toggle function
const toggle = e => {
// Execute toggle function which is came from the parent component
return props.toggle(e);
};
// handle click for the document object
const handleDocumentClick = e => {
// Execute toggle function of the parent
toggle(e);
};
// Remove event listeners
const removeEvents = () => {
["click", "touchstart"].forEach(event =>
document.removeEventListener(event, handleDocumentClick, true)
);
};
// Add event listeners
const addEvents = () => {
["click", "touchstart"].forEach(event =>
document.addEventListener(event, handleDocumentClick, true)
);
};
useEffect(() => {
const handleProps = () => {
if (props.isOpen) {
addEvents();
} else {
removeEvents();
}
};
// mount
handleProps();
// unmount
return () => {
removeEvents();
};
}, [props.isOpen]);
// Condense all other attributes except toggle `prop`.
const { className, isOpen, ...attrs } = omit(props, ["toggle"]);
// Conditionally join all classes
const classes = classNames(className, "dropdown", { show: isOpen });
return (
<DropdownProvider value={getContextValue()}>
<div className={classes} {...attrs} />
</DropdownProvider>
);
};
export default Dropdown;
Dropdown component has a parent i.e. a Provider whenever Provider values will change child components will access those values.
Secondly, on the DOM it will render a div which consists of Dropdown markup structure.
DropdownToggle.js
import React, {useContext} from 'react';
import classNames from 'classnames';
import {DropdownContext} from './DropdownContext';
// DropdownToggle component
const DropdownToggle = (props) => {
const {toggle} = useContext(DropdownContext);
const onClick = (e) => {
// If props onClick is not undefined
if (props.onClick) {
// execute the function
props.onClick(e);
}
toggle(e);
};
const {className, ...attrs} = props;
const classes = classNames(className);
return (
// All children would be render inside this. e.g. `svg` & `text`
<button type="button" className={classes} onClick={onClick} {...attrs}/>
);
};
export default DropdownToggle;
DropdownMenu.js
import React, { useContext } from "react";
import classNames from "classnames";
import { DropdownContext } from "./DropdownContext";
// DropdownMenu component
const DropdownMenu = props => {
const { isOpen } = useContext(DropdownContext);
const { className, ...attrs } = props;
// add show class if isOpen is true
const classes = classNames(className, "dropdown-menu", { show: isOpen });
return (
// All children would be render inside this `div`
<div className={classes} {...attrs} />
);
};
export default DropdownMenu;
Jayce444 answer is correct. When you click the button, it fires once, then the event bubbles up to the document and fires again.
I just want to add another alternative solution for you. You can use useRef hook to create a reference of Dropdown node and check if the current event target is button element or not. Add this code to your Dropdown.js file.
import React, { useRef } from "react";
const Dropdown = props => {
const containerRef = useRef(null);
// get reference of the current div
const getReferenceDomNode = () => {
return containerRef.current;
};
// handle click for the document object
const handleDocumentClick = e => {
const container = getReferenceDomNode();
if (container.contains(e.target) && container !== e.target) {
return;
}
toggle(e);
};
//....
return (
<DropdownProvider value={getContextValue()}>
<div className={classes} {...attrs} ref={containerRef} />
</DropdownProvider>
);
};
export default Dropdown;
The toggling function is linked to both the document, and the button itself. So when you click the button, it fires once, then the event bubbles up to the document and fires again. Gotta be careful attaching event listeners to the entire document object. Add a line to stop the event propagation in your Dropdown.js file:
// toggle function
const toggle = e => {
// Execute toggle function which is came from the parent component
e.stopPropagation(); // this stops it bubbling up to the document and firing again
return props.toggle(e);
};

Resources