I have Select from antd styled with styled components with different icon. Everything works perfectly, except click on new icon inside of select doesn't trigger the dropdown to open or close.
<Styled.SortSelect
size={size ? size : 'large'}
defaultValue={defaultValue}
suffixIcon={<Styled.Icon />}
getPopupContainer={trigger => {
return trigger;
}}
>
{options.map((option: string) => {
return (
<Styled.SortOption className="custom-option" data-testid="sort-option" key={option} value={option}>
{option}
</Styled.SortOption>
);
})}
</Styled.SortSelect>
Simple demo
https://codesandbox.io/s/broken-arrow-click-nfpc7?file=/src/index.tsx
It seems to be a bug. Switching to an older version of antd e.g. 4.1.3 seems to solve your error
I think it's a bug. But in the meantime a workaround could be to handle whether the select is open yourself via an onClick on the icon like this:
export const SortSelect = ({ defaultValue, size, options }: Props) => {
const [open, setOpen] = useState(false);
return (
<Styled.SortSelect
size={size ? size : "large"}
defaultValue={defaultValue}
suffixIcon={<Styled.Icon onClick={() => setOpen(!open)} />}
open={open}
getPopupContainer={trigger => {
return trigger;
}}
>
{options.map((option: string) => {
return (
<Styled.SortOption
className="custom-option"
data-testid="sort-option"
key={option}
value={option}
>
{option}
</Styled.SortOption>
);
})}
</Styled.SortSelect>
);
};
Related
I'm creating an Accordion with react spring, the accordion is ready and working fine, but I can not make it to allow the accordion to open one item at the time, right now open all the three item in the same time, I wish to be able to open only one.
here is the codesand -> https://codesandbox.io/s/prod-sun-ttix7?file=/src/App.tsx
You just have to pull up the open state into the App component and and use the key or index as (open) indicator, like this:
App component
function App() {
const [openKey, setOpenKey] = useState()
const handleToggle = key => {
setOpenKey(openKey !== key ? key : null)
}
return (
<Container>
{data &&
data.map(({ name, content }) => (
<ServiceItem
key={name}
name={name}
content={content}
toggle={handleToggle}
open={openKey === name}
/>
))}
</Container>
);
}
ServiceItem component
const ServiceItem = ({ name, content, toggle, open }: ServiceItemProps): JSX.Element => {
return (
<div key={name}>
<Item onClick={() => toggle(name)}>
<Text className="text-15 regular laptop:text-20 laptop:regular">
{name}
</Text>
<Icon className="text-15 regular laptop:text-20 laptop:regular">
{!open ? "+" : "-"}
</Icon>
</Item>
<Expandable open={open}>
<ContentContainer>
<React.Fragment key={name}>
<Value>{content}</Value>
</React.Fragment>
</ContentContainer>
</Expandable>
</div>
);
};
Here is a working example: https://codesandbox.io/s/infallible-banzai-fnncw.
I am trying to understand the best way of conditionally showing a Tooltip, based on if a sibling component Popper is open or not. I want to to show it by default on hover of its child the ButtonBase. I want the tooltip to never be open if the Popper is open. The tooltip title is acting as a summary of what's selected in the options list in the Popper when its closed, having it open with the Popper open is not ideal and cluttered. I am new to hooks so trying to understand how I can incorporate a hook to set the tooltipOpen state correctly with the conditional need.
export default function TooltipWithPopper() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState(null);
const [value, setValue] = React.useState([options[1], options[11]]);
const [pendingValue, setPendingValue] = React.useState([]);
const [tooltipOpen, setTooltipOpen] = React.useState(false);
const handleClick = (event) => {
setPendingValue(value);
setAnchorEl(event.currentTarget);
setTooltipOpen(false);
};
const handleClose = (event, reason) => {
if (reason === "toggleInput") {
return;
}
setValue(pendingValue);
if (anchorEl) {
anchorEl.focus();
}
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? "github-label" : undefined;
return (
<React.Fragment>
<div className={classes.root}>
<Tooltip title={value.map((i) => i.title).join(", ")}>
<ButtonBase
disableRipple
className={classes.button}
aria-describedby={id}
onClick={handleClick}
>
<span>Label</span>
{value.length}/{options.length}
</ButtonBase>
</Tooltip>
</div>
<Popper
id={id}
open={open}
anchorEl={anchorEl}
placement="bottom-start"
className={classes.popper}
>
<Autocomplete
open
onClose={handleClose}
multiple
classes={{
paper: classes.paper,
option: classes.option,
popperDisablePortal: classes.popperDisablePortal
}}
value={pendingValue}
onChange={(event, newValue) => {
setPendingValue(newValue);
}}
disableCloseOnSelect
disablePortal
renderTags={() => null}
noOptionsText="No labels"
.....
/>
</Popper>
</React.Fragment>
);
}
Here is a demo of the tooltip being applied to the trigger element. How can I set it to only be open depending on another components' state? I've tried adding a setTooltipOpen(false) call when the handleClick is called when opens the Popper.
Demo: https://codesandbox.io/s/material-demo-forked-0wgyh?file=/demo.js:0-6181
You can control the Tooltip open prop value with your tooltipOpen state (implementation is up to you) and provide conditions that if the Popper is open, then automatically, the Tooltip open prop value computation will disregard the tooltipOpen state and assign false.
In my example below, I control the tooltipOpen state via onMouseEnter && onMouseLeave events
<Tooltip
open={open === true ? false : tooltipOpen}
title={value.map((i) => i.title).join(", ")}
>
<ButtonBase
disableRipple
className={classes.button}
aria-describedby={id}
onClick={handleClick}
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
<span>Label</span>
{value.length}/{options.length}
</ButtonBase>
</Tooltip>
I use #brainhubeu/react-carousel library for implementing carousel component in my App. Here is example of code:
export const Carousel = ({
options,
open,
onClose,
value,
onChange,
}: Props) => {
const [showSendWindow, setShowSendWindow] = useState<boolean>(false)
const handleCloseCarousel = useCallback(
(e: KeyboardEvent) => {
if (e.keyCode === 27) {
onClose(false)
}
},
[onClose],
)
useEffect(() => {
document.addEventListener('keydown', handleCloseCarousel)
return () => {
document.removeEventListener('keydown', handleCloseCarousel)
}
}, [handleCloseCarousel])
return open ? (
<Backdrop>
{showSendWindow && (
<SendFileModal onClose={() => setShowSendWindow(false)} />
)}
<SliderWrapper>
<Close onClick={() => onClose(false)}>
<Icon color='#FFFFFF' height={25} type='Cross' width={25} />
</Close>
<ImageCarousel
animationSpeed={0}
plugins={[
{
resolve: arrowsPlugin,
options: Arrows(options),
},
]}
value={value}
onChange={onChange}
>
{options.map((e, index) => (
<Wrapper key={index}>
<Image image={e.url}>
<Dropdown
overlay={() =>
FileDropdow(e.url, e.name, () => setShowSendWindow(true))
}
placement='bottomRight'
>
<Dots>•••</Dots>
</Dropdown>
</Image>
<Description>{e.comment}</Description>
</Wrapper>
))}
</ImageCarousel>
</SliderWrapper>
</Backdrop>
) : null
}
I found out that problem is in ImageCarousel component, because if delete it, then warning disappears, but Carousel doesn't work properly. I get next warning in console:
Warning: Cannot update a component (Ne) while rendering a different
component (hn). To locate the bad setState() call inside hn
How can I avoid it?
I'm building an infinite loading list of users with react-window. In the list, every item has an icon button from Material-UI for further action.
But I can't mount the menu near the icon as the icon button would be re-rendered when setting anchorEl for the menu to be opened. A gif clip:
The question is related to React Material-UI menu anchor broken by react-window list but has more HOC. The code is listed here. I wish I could use my codesandbox for demonstration but the react-measure keeps growing height.
function App() {
const [anchorEl, setAnchorEl] = useState(null);
const openMenu = React.useCallback(e => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
console.log("target", e.currentTarget);
}, []);
const handleClose = () => {
setAnchorEl(null);
};
const [items, setItems] = React.useState([]);
const isItemLoaded = index => {
const c = index < items.length;
// console.log("isItemLoaded", index, c);
return c;
};
const loadMoreItems = (startIndex, stopIndex) => {
console.log("loadMoreItems", startIndex, items);
setItems(items.concat(Array(10).fill({ name: "1", size: startIndex })));
};
const innerET = React.forwardRef((props, ref) => (
<div ref={ref} {...props} />
));
const Row = React.useCallback(
({ index, style }) => {
console.log("Row", items, index);
return items[index] ? (
<ListItem style={style} key={index}>
<Button variant="contained" color="primary" onClick={openMenu}>
Row {index}: {items[index].size}
</Button>
</ListItem>
) : null;
},
[items, openMenu]
);
const innerListType = React.forwardRef((props, ref) => (
<List ref={ref} {...props} />
));
return (
<div className="App">
<div className="ceiling">Something at top</div>
<div className="interest">
<Menu anchorEl={anchorEl} onClose={handleClose} />
<Measure bounds offset>
{({ measureRef, contentRect }) => {
const height = Math.min(
contentRect && contentRect.offset
? document.getElementById("root").getBoundingClientRect()
.height - contentRect.offset.top
: itemSize * items.length,
itemSize * items.length
);
console.log(
"bounds",
height,
contentRect.bounds,
contentRect.offset
);
return (
<div>
<div />
<div ref={measureRef} className="measurement">
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={itemCount}
loadMoreItems={loadMoreItems}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={height}
width={
contentRect.bounds !== undefined &&
contentRect.bounds.width !== undefined
? contentRect.bounds.width
: -1
}
itemCount={itemCount}
itemSize={itemSize}
onItemsRendered={onItemsRendered}
ref={ref}
innerElementType={innerET}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
</div>
</div>
);
}}
</Measure>
</div>
</div>
);
}
As far as I understand, the ripple effect would trigger a re-render in the box with the first click. Moreover, the second click after the re-render upon clicking would not trigger a re-render. That feels even more peculiar to me.
EDIT: I fixed the first sandbox. And by using Material UI's list, this issue is reproducible. https://codesandbox.io/s/blissful-butterfly-qn3g7
So the problem lies in using innerElementType property.
It turns out that a hook is needed.
const innerListType = React.useMemo(() => {
return React.forwardRef((props, ref) => (
<List component="div" ref={ref} {...props} />
));
}, []);
To fix my problems, hooks for handling events are needed to be handled more carefully.
I have a nav menu built with material-ui/core in Navbar.
I use useRef to track the position of clicked button on toggle menu close.
anchorRef.current.contains(event.target)
And I am getting 'Uncaught TypeError: anchorRef.current.contains is not a function' .
I tried 'Object.values(anchorRef.current).includes(event.target)' instead, it always returns false.
-- update --
anchorRef.current.props Object.
withStyles {
props:{
aria-haspopup: "true"
aria-owns: undefined
children: "계정"
className: "nav-menu--btn"
onClic: f onClick()
get ref: f()
isReactWarning: true
arguments: (...)
caller: (...)
length: 0
name: "warnAboutAccessingRef"
...
}, context{...}, refs{...}, ...}
ToggleMenuList
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<Button
ref={anchorRef}
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
ToggleMenuItems
const ToggleMenuItems = ({
listId,
activeId,
handleClose,
anchorRef,
items,
}) => {
const isOpen = activeId === listId;
const leftSideMenu = activeId === 3 || activeId === 4 ? 'leftSideMenu' : '';
return (
<Popper
open={isOpen}
anchorEl={anchorRef.current}
keepMounted
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === 'bottom' ? 'center top' : 'center bottom',
}}
className={`toggle-menu ${leftSideMenu}`}
>
<Paper id="menu-list-grow">
<ClickAwayListener
onClickAway={handleClose}
>
<MenuList className="toggle-menu--list">
{items.map(e => (
<MenuItem
key={e.id}
className="toggle-menu--item"
onClick={handleClose}
>
<Link
to={e.to}
className="anchor td-none c-text1 toggle-menu--link"
>
{e.name}
</Link>
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
);
};
export default ToggleMenuItems;
react: ^16.8.6
react-dom: ^16.8.6
react-router-dom: ^4.3.1
#material-ui/core: ^3.1.2
I assume your ToggleMenuItems sets up global(document-level?) event listener on click to collapse Menu on clicking somewhere outside.
And you have a sibling button element. Clicking on that you want to keep menu expanded, right? So that was the point to use .contains in onClick to check if we are clicked outside of ToggleMenuItems but in scope of specific Button. The reason why it does not work: <Button> is custom class-based React component so it returns React component instance in ref. And it does not have any DOM-specific methods like .contains
You can rework you current approach: just stop bubbling event in case Button has been clicked. It would stop global event handler set by ToggleMenuItems to react.
const stopPropagation = (event) => event.stopPropagation();
const ToggleMenuList = ({ navAdminList, navAdminItems, classes }) => {
const [activeId, setActiveId] = useState(null);
const anchorRef = useRef(null);
const handleToggle = id => {
setActiveId(id);
};
const handleClose = event => {
setActiveId(null);
};
return (
<React.Fragment>
<div className={`nav-menu--admin ${classes.root}`}>
{navAdminList.map(e => (
<div key={e.id}>
<div onClick={stopPropagation}>
<Button
aria-owns={activeId === e.id ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={() => handleToggle(e.id)}
>
{e.name}
</Button>
</div>
{activeId === e.id && (
<ToggleMenuItems
id={e.id}
activeId={activeId}
handleClose={handleClose}
anchorRef={anchorRef}
items={navAdminItems[e.id]}
/>
)}
</div>
))}
</div>
</React.Fragment>
);
};
export default withStyles(styles)(ToggleMenuList);
I've put stopPropagation handler outside since it does not depend on any internal variable.