How to collapse Siblings with Headless UI - javascript

To make the accordion component with Headless UI, I have used Disclosure component. But I have a problem to control the collapse/expand state for it's siblings.
So, I want to close other siblings when I open one, but Disclosure component is only supporting internal render props, open and close. So, I can't control it outside of the component and can't close others when I open one.
import { Disclosure } from '#headlessui/react'
import { ChevronUpIcon } from '#heroicons/react/solid'
export default function Example() {
return (
<div className="w-full px-4 pt-16">
<div className="mx-auto w-full max-w-md rounded-2xl bg-white p-2">
<Disclosure>
{({ open }) => (
<>
<Disclosure.Button className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75">
<span>What is your refund policy?</span>
<ChevronUpIcon
className={`${
open ? 'rotate-180 transform' : ''
} h-5 w-5 text-purple-500`}
/>
</Disclosure.Button>
<Disclosure.Panel className="px-4 pt-4 pb-2 text-sm text-gray-500">
If you're unhappy with your purchase for any reason, email us
within 90 days and we'll refund you in full, no questions asked.
</Disclosure.Panel>
</>
)}
</Disclosure>
<Disclosure as="div" className="mt-2">
{({ open }) => (
<>
<Disclosure.Button className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75">
<span>Do you offer technical support?</span>
<ChevronUpIcon
className={`${
open ? 'rotate-180 transform' : ''
} h-5 w-5 text-purple-500`}
/>
</Disclosure.Button>
<Disclosure.Panel className="px-4 pt-4 pb-2 text-sm text-gray-500">
No.
</Disclosure.Panel>
</>
)}
</Disclosure>
</div>
</div>
)
}
How do we control the close/open state outside of the component?

I don't think so it's possible using HeadlessUI, although you can create your own Disclosure like component.
Lift the state up to the parent component by creating a disclosures state that stores all the information about the disclosures.
Loop over the disclosures using map and render them.
Render a button that toggles the isClose property of the disclosures and also handles the aria attributes.
On button click, toggle the isOpen value of the clicked disclosure and close all the other disclosures.
Checkout the snippet below:
import React, { useState } from "react";
import { ChevronUpIcon } from "#heroicons/react/solid";
export default function Example() {
const [disclosures, setDisclosures] = useState([
{
id: "disclosure-panel-1",
isOpen: false,
buttonText: "What is your refund policy?",
panelText:
"If you're unhappy with your purchase for any reason, email us within 90 days and we'll refund you in full, no questions asked."
},
{
id: "disclosure-panel-2",
isOpen: false,
buttonText: "Do you offer technical support?",
panelText: "No."
}
]);
const handleClick = (id) => {
setDisclosures(
disclosures.map((d) =>
d.id === id ? { ...d, isOpen: !d.isOpen } : { ...d, isOpen: false }
)
);
};
return (
<div className="w-full px-4 pt-16">
<div className="mx-auto w-full max-w-md rounded-2xl bg-white p-2 space-y-2">
{disclosures.map(({ id, isOpen, buttonText, panelText }) => (
<React.Fragment key={id}>
<button
className="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75"
onClick={() => handleClick(id)}
aria-expanded={isOpen}
{...(isOpen && { "aria-controls": id })}
>
{buttonText}
<ChevronUpIcon
className={`${
isOpen ? "rotate-180 transform" : ""
} h-5 w-5 text-purple-500`}
/>
</button>
{isOpen && (
<div className="px-4 pt-4 pb-2 text-sm text-gray-500">
{panelText}
</div>
)}
</React.Fragment>
))}
</div>
</div>
);
}

it is possible just you need to add some extra props selectors to the Disclosure.Button Component, in this case, I am adding aria-label='panel' like so...
import { Disclosure } from '#headlessui/react'
function MyDisclosure() {
return (
<Disclosure>
<Disclosure.Button aria-label="panel" className="py-2">
Is team pricing available?
</Disclosure.Button>
<Disclosure.Panel className="text-gray-500">
Yes! You can purchase a license that you can share with your entire
team.
</Disclosure.Panel>
</Disclosure>
)
}
next you need to select the following with "querySelectorAll" like...
<button
type='button'
onClick={() => {
const panels = [...document.querySelectorAll('[aria-expanded=true][aria-label=panel]')]
panels.map((panel) => panel.click())
}}
>
</button>
with this, you just need to change 'aria-expanded' to either 'true' or 'false' to expand or collapse

There's a way to do this with React (assuming you're using #headlessui/react) via useState:
const [disclosureState, setDisclosureState] = useState(0);
function handleDisclosureChange(state: number) {
if (state === disclosureState) {
setDisclosureState(0); // close all of them
} else {
setDisclosureState(state); // open the clicked disclosure
}
}
And in each Disclosure component, just pass an onClick callback to the Disclosure.Button:
<Disclosure.Button onClick={() => handleDisclosureChange(N)} />
Where N is the index of the clicked Disclosure (using 1 as the first Disclosure, since 0 handles all disclosures closed).
Finally, conditionally render the Disclosure.Panel based on the disclosureState:
{
disclosureState === N && (<Disclosure.Panel />)
}
Where N is the index of the clicked Disclosure. Using this method you can open just 1 disclosure at a time, and clicking an open disclosure will close all of them.

Related

Disclosure Panel closes when component re-renders

I am creating a nested sortable list in react using the dnd package. Each item inside the draggable contains input fields wrapped inside the Disclosure component from #headlessui. I am updating the item value with onChange on each input field.
The item field is updating on every input but the problem is disclosure panel closes immediately on keystroke. The issue is probably due to rerendering the list I simply tried making a list of the Disclosure panels without dnd and it doesn't close the Disclosure panel on input.
I have reproduced the issue in repl.it sample and I think it's too long to post code here.
export default function App() {
const [items, setItems] = useState([
{id: 1, title: 'My Item 1'},
{id: 2, title: 'My Item 2'},
{id: 3, title: 'My Item 3'},
{id: 4, title: 'My Item 4'},
]);
const handleInputChange = (e, item) => {
const {name, value} = event.target;
setItems(prev =>
[
...prev.map((elm) => {
if (elm.id === item.id) {
elm[name] = value;
}
return elm;
})
]
)
}
return (
<main>
<SortableTree
items={items}
onItemsChanged={setItems}
TreeItemComponent={forwardRef((props, ref) => {
return (
<SimpleTreeItemWrapper
{...props}
showDragHandle={false}
disableCollapseOnItemClick={true}
hideCollapseButton={true}
indentationWidth={20}
ref={ref}
className={"w-[400px]"}
>
<Disclosure as="div" className={"w-full mb-2"}>
{({ open }) => (
<>
<Disclosure.Button
open={true}
className="flex items-center w-full justify-between bg-[#F6F7F7] border border-gray-200 px-3 py-2 text-left text-xs font-medium text-gray-600 shadow-sm hover:border-gray-400 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75"
>
<span>{props.item.title}</span>
<ChevronUpIcon
className={`${
open ? "rotate-180 transform" : ""
} h-5 w-5 text-gray-500`}
/>
</Disclosure.Button>
<Disclosure.Panel className="px-4 pt-4 pb-2 text-sm text-gray-500">
<div className="flex flex-col mb-2">
<label
htmlFor="nav_label"
className="text-xs mb-1"
>
Navigation Label
</label>
<input
onChange={(e) => handleInputChange(e, props.item)}
name='title'
type={"text"}
className="px-2 py-1 border border-gray-400"
value={props.item.title}
/>
</div>
<div className="">
<button
className="text-red-500 text-xs"
>
Remove
</button>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
</SimpleTreeItemWrapper>
);
})}
/>
</main>
)
}

React web app : Mapping <Popover.Button> and <Popover.Panel> separately

I want to render this popover panel
<Popover.Panel className="absolute z-10 inset-x-0 transform shadow-xl pb-2 w-max">
<div>
<Dropdown subCategory={mainCatArray}/>
</div>
</Popover.Panel>
same as this popover button render in this below code. Because I want to render a separate popover panel with the popover button, please help me to do that. (I cannot render in one categoryObj because I want to render this popover button horizontally, but if I use a single map for the entire popover component popover button renders it vertically.
(Simply What I want to do is I want to render one <Popover.Panel> to one <Popover.Button>)
import { Fragment, useEffect, useState } from "react";
import { Popover, Transition } from "#headlessui/react";
import Dropdown from "./dropdown";
import { categoryObj } from "../../components/item/categoriesobj";
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
const CatDropDown = () => {
const [mainCatArray, setMainCatArray] = useState([]);
return (
<>
<Popover className="z-0 relative">
{({ open }) => (
<>
<div>
<div className=" z-10 bg-white">
<div className="w-2/3 flex gap-5 px-4 py-6 sm:px-6 lg:px-8 ">
{categoryObj.map((singleMainCategory) => (
<Popover.Button
className={classNames(
open ? "text-gray-900" : "text-gray-900",
"group bg-white rounded-md inline-flex items-center text-sm font-susty focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-white"
)}
>
<span
key={singleMainCategory.id}
onClick={() => {
setMainCatArray(singleMainCategory.subCategory);
}}
>
<span>{singleMainCategory.name}</span>
</span>
</Popover.Button>
))}
</div>
</div>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 -translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 -translate-y-1"
>
<Popover.Panel className="absolute z-10 inset-x-0 transform shadow-lg pb-2">
<div>
<Dropdown subCategory={mainCatArray}/>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
</>
);
};
export default CatDropDown;

React - List icons with different param

I'm trying to use the map function but I can't get it right.
I have a side-bar which I want to show some icons. Here is an example without the map.
const SideBar = () => {
return (
<div className="fixed top-0 left-0 h-screen w-20 m-0 flex flex-col bg-gray-100 text-white shadow-lg">
<SideBarIcon icon={<FaFire size="30" />} />
<SideBarIcon icon={<FaPoo size="30" />} />
</div>
);
};
const SideBarIcon = ({ icon, text = "tooltip 💡"}) => (
<div className="sidebar-icon group">
{icon}
<span class="sidebar-tooltip group-hover:scale-100">{text}</span>
</div>
);
Here is an example with the map function
const SideBar = () => {
const icons = [FaFire, FaPoo];
return (
<div className="fixed top-0 left-0 h-screen w-20 m-0 flex flex-col bg-gray-100 text-white shadow-lg">
{icons.map(function(icon) {
return <SideBarIcon icon={<icon size="30"/>}/>
})}
</div>
);
};
const SideBarIcon = ({ icon, text = "tooltip 💡"}) => (
<div className="sidebar-icon group">
{icon}
<span class="sidebar-tooltip group-hover:scale-100">{text}</span>
</div>
);
Can you tell me what I'm doing wrong?
Thank you for your time!
By simply putting icon inside the tags, it thinks you're rendering an HTML element called icon, therefore it's not rendering the mapped item. It also wouldn't work if you set it as <{icon}/>, because it would be trying to render an empty element.
Luckily, there's an easy fix -- Just capitalize Icon, and React will render it as a JSX Component.
{icons.map(function(Icon) {
return <SideBarIcon icon={<Icon size="30"/>}/>
})}
{icons.map(function (Icon) {
return <SideBarIcon icon={<Icon size="30"/>}/>
})}
Components start with capital letters, just change icon to Icon.
Please do not forget to use key prop for your mapped items.
https://reactjs.org/docs/jsx-in-depth.html#user-defined-components-must-be-capitalized
const SideBar = () => {
const icons = [<FaFire size="30" />, <FaPoo size="30" />];
return (
<div className="fixed top-0 left-0 h-screen w-20 m-0 flex flex-col bg-gray-100 text-white shadow-lg">
{icons.map(function(icon) {
return <SideBarIcon icon={icon}/>
})}
</div>
);
};
Since you are using icon in lowercase, it is not recognized as a jsx component. To fix this, change it to uppercase.
turn this
{icons.map(function(icon) {
return <SideBarIcon icon={<icon size="30"/>}/>
})}
to this
{icons.map(function(Icon) {
return <SideBarIcon icon={<Icon size="30"/>}/>
})}
Also, if the underlying components allow it, you could use the direct invocation, like so:
return <SideBarIcon icon={icon({size:"30"})/>
Keep in mind, that in most cases that is not what you want, and it might introduce hard-to-fix bugs.

React: How to set up Edit Function in CRUD app to switch out whole components while keeping id

Hey Guys I'm pretty new to React and I'm running into a bit of a pickle here.
I'm making a simple CRUD website creator and I've got the add and delete function created and they work great! but I'm having trouble with the edit function.
I've based this on the this tutorial which works with String data-type
https://www.digitalocean.com/community/tutorials/react-crud-context-hooks
So in my mind this is how it should should work
I've got use state as passing an object with a couple of properties
const [selectedSection, setSelectedSection] = useState({
id: null,
section: {},
});
I've set the id to the current component I'm editing with
const currentSectionId = route.match.params.id;
in my useEffect I'm carrying over the current id with while setting the new compenent skipping the id in the current section
useEffect(() => {
const sectionId = currentSectionId;
const selectedSection = sections.find(
(currentSectionTraversal) => currentSectionTraversal.id === parseInt(sectionId)
);
setSelectedSection(selectedSection);},[currentSectionId, sections]);
const onSubmit = (e) => {
e.preventDefault();
editSection(selectedSection);
history.push("/");
console.log("selectedSection id",selectedSection.section, selectedSection.id)
};
and the button function to spread the selectedSection and change the only the requested value in the button.
const handleOnChange = (userKey, newValue) => setSelectedSection({ ...selectedSection, [userKey]: newValue });
in my render code I've got my button set up like
<button href="/" className="bg-green-400 w-mt hover:bg-green-500 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
value={selectedSection.section}
onChange={() => handleOnChange("section", QuickLinks)}
type="submit"
>Add Section</button>
Now I've tried different things like changing the data-type in the useState Object, having the setSelectedSection in use effect to change the SelectedSection.section
but in the console what I'm noticing is the button is not carrying the data over.
I've imported my compenent as Quicklinks but I'm not sure why it's not passing the component into the selectedSection.section
here is the entire code of the editSection
import React, { useState, useContext, useEffect} from "react";
import { useHistory, Link } from "react-router-dom";
import { GlobalContext } from "./GlobalState";
import QuickLinks from '../components/sections/quick_links/quickLinks';
import QuickLinksPreview from './images/quick-link.jpg';
export const EditSection = (route) => {
let history = useHistory();
const { sections, editSection } = useContext(GlobalContext);
const [selectedSection, setSelectedSection] = useState({
id: null,
section: {},
});
const currentSectionId = route.match.params.id;
useEffect(() => {
const sectionId = currentSectionId;
const selectedSection = sections.find(
(currentSectionTraversal) => currentSectionTraversal.id === parseInt(sectionId)
);
setSelectedSection(selectedSection);
}, [currentSectionId, sections]);
const onSubmit = (e) => {
e.preventDefault();
editSection(selectedSection);
history.push("/");
console.log("selectedSection id",selectedSection.section, selectedSection.id)
};
const handleOnChange = (userKey, newValue) => setSelectedSection({ ...selectedSection, [userKey]: newValue });
if (!selectedSection || !selectedSection.id) {
return <div>Invalid Employee ID.</div>;
}
return (
<React.Fragment>
<div className="w-full container mt-20 mx-auto">
<form onSubmit={onSubmit}>
<table>
<tbody>
{/* ----------------------------Item List Start COPY------------------------ */}
<tr>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="flex-shrink-0 h-40 w-50 shadow">
<img className="h-40 w-full " src={QuickLinksPreview} alt="" />
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">Quick Links</div>
<div className="text-sm text-gray-500">Multi Card Quick Links<br></br>for Description and links</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-white-800"> Beta </span>
{/* Component Development Status
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-black-800"> Constrution </span>
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"> Active </span>
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-black-800"> Testing </span>
*/}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">S03-S5</td>
{/* Pricing Levels as Structure
S = Labels that it is sections
02 = is the Template Id for Developers
S = Standard Price
3 = Price level
*/}
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button href="/" className="bg-green-400 w-mt hover:bg-green-500 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
value={selectedSection.section}
onChange={() => handleOnChange("section", QuickLinks)}
type="submit"
>Add Section</button>
</td>
</tr>
{console.log("Selected section", selectedSection.section)}
{/* ----------------------------Item List END COPY------------------------ */}
</tbody>
</table>
<div className="flex items-center justify-between">
<div className="block mt-5 bg-red-400 w-full hover:bg-red-500 text-white font-bold py-2 px-4 rounded focus:text-gray-600 focus:shadow-outline">
<Link to="/">Cancel</Link>
</div>
</div>
</form>
</div>
</React.Fragment>
);
};
Try using onClick instead of onChange for the button. I think onChange is for <input> tags not buttons. Also href is for links, not buttons. Unless this button is in a form type=submit isn't necessary. Also an arrow function isn't required for onClick. onChange={() => handleOnChange("section", QuickLinks)} -> onClick={handleOnChange("section", QuickLinks)}.

React - Hide parent's child element on blur event

I'm working on a custom input component. so when user clicks on the input element I want to show a dropdown that will contain default ten records and if user types some keyword in the input the dropdown will be updated with the results matching the keyword.
when user clicks on the input I want to show the dropdown and If they click out the container I want to hide the dropdown.
so I've added an onBlur event to the container and now if I click somewhere else it hides the dropdown but If I try to select a record It's also hidding the dropdown which is incorrect.
How can I hide the dropdown when the clicks outside of the input container? any suggestions?
export const ListBoxInput = ({ label, data, keyName }) => {
const [selected, setSelected] = useState({ key: null, value: null });
const [showDropdown, setShowDropdown] = useState(false);
return (
<div
className='flex items-center space-x-3'
onBlur={() => setShowDropdown(false)}
>
<label className='block text-sm font-medium text-gray-700'>{label}</label>
<div className='relative group'>
<input
type='text'
value={selected.key}
onFocus={() => setShowDropdown(true)}
onChange={(e) => setSelected({ key: e.target.value, value: null })}
disabled={!data.length}
placeholder={!data.length ? 'No data...' : ''}
className='w-full py-2 pl-3 pr-10 mt-1 text-left bg-white border border-gray-300 rounded-md shadow-sm disabled:opacity-50 focus:outline-none focus:ring-1 focus:ring-brand focus:border-brand sm:text-sm'
/>
{showDropdown && (
<div className='absolute z-10 w-full py-1 mt-3 overflow-auto text-base bg-white rounded-md shadow-lg max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none '>
{data.map((r) => (
<button
key={r.id}
type='button'
onClick={() => {
setSelected({ key: r[keyName], value: r.id });
setShowDropdown(false);
}}
className='block w-full py-2 pl-3 text-left hover:bg-gray-100 pr-9'
>
{r[keyName]}
</button>
))}
</div>
)}
</div>
</div>
);
};

Categories