Disclosure Panel closes when component re-renders - javascript

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>
)
}

Related

Make div with react components as children disappear if I click outside of it

I want to make a form that disappears if I click outside of it.
Form Component:
const CreateTaskPopup = (props) => {
const ref = query(collection(db, "tasks"));
const mutation = useFirestoreCollectionMutation(ref);
useEffect(() => {
const closeTaskPopup = (event) => {
if (event.target.id != "addForm") {
props.setTrigger(false)
}
}
document.addEventListener('click', closeTaskPopup);
return () => document.removeEventListener('click', closeTaskPopup)
}, [])
return (props.trigger) ? (
<>
<div className="bg-darkishGrey my-4 p-1 mx-3 ml-16 cursor-pointer block"
id="addForm">
<div className="flex justify-between">
<div className="flex break-all items-center ">
<Image src={fileIcon} className="w-6 mx-2"/>
<div>
<Formik
initialValues={{
taskName: "",
taskIsDone: false,
parentId: props.parentId ? props.parentId : "",
hasChildren: false,
}}
onSubmit={(values) => {
mutation.mutate(values);
props.setTrigger(false);
}}
>
<Form>
<div className="">
<TextInput
placeholder="Type a name"
name="taskName"
type="text"
/>
</div>
</Form>
</Formik>
</div>
</div>
<div className="flex items-center">
</div>
</div>
</div>
</>
) : null
}
export default CreateTaskPopup
Text Input Component:
import { useField } from "formik";
const TextInput = ({ label, ...props}) => {
const [field, meta] = useField(props);
return (
<div>
<label id="addForm" className="text-lightestGrey text-xl block"
htmlFor={props.id || props.name}>
{label}
</label>
<input id="addForm" className="bg-darkishGrey text-xl text-almostWhite my-2
outline-none w-10/12 rounded-sm p-1 mx-3" {...field} {...props} />
{meta.touched && meta.error ? <div>{meta.error}</div>: null}
</div>
);
};
export default TextInput;
I tried giving an id to the elements inside it but it's not the best solution as it has components from the Formik library to which I can't assign an id. I don't know what would be the best solution for this problem.

React function executes continuously and state keeps refreshing

I have kind of a weird issue in React. I am using the Blitz JS framework along side with Prisma for database stuff.
I have a function that queries the database for all entries from a date the user selects forward. It's used for a reservation system that I am trying to build.
After I get the data I use it to create a <select> element and set as <option> every space that does not appear in the database. Everything works fine, the <select> and <option> display what they should, but as soon as I click the drop-down to see all available option, I think the state refreshes and the menu closes down.
If I console.log() inside the function, it will go on forever in the console menu. Also in the terminal I can see the function being called about every second or so.
terminal log
javascript console log
I also tried querying the database from a useEffect() but useEffect() and useQuery (from Blitz.js) don't work together
I will attach the code along side with comments for easier reading.
Thank you for your time!
Main page:
import { BlitzPage, invoke, useQuery } from "blitz"
import { useState, useEffect, Suspense } from "react"
import { UserInfo } from "app/pages"
import DatePicker from "react-datepicker"
import "react-datepicker/dist/react-datepicker.css"
import addDays from "date-fns/addDays"
import format from "date-fns/format"
import insertBooking from "app/bookings/mutations/insertBooking"
import getAllBookings from "app/bookings/queries/getAllBookings"
import { useCurrentBookings } from "app/bookings/hooks/useCurrentBookings"
import { useCurrentUser } from "app/core/hooks/useCurrentUser"
const Add: BlitzPage = () => {
//State for all options that will be added for the booking
const [state, setState] = useState({
intrare: 1,
locParcare: 0,
locPescuit: 0,
casuta: 0,
sezlong: 0,
sedintaFoto: false,
petrecerePrivata: false,
totalPrice: 20,
})
//Date state added separately
const [startDate, setStartDate] = useState(addDays(new Date(), 1))
const [availableSpots, setAvailableSpots] = useState({
pescuit: [0],
casute: {},
sezlonguri: {},
})
// The function that reads the DB, manipulates the data so I can have
// an array of open spots and then renders those values in a select
const PescuitSelect = () => {
const totalFishingSpots = Array.from(Array(114).keys())
const bookings = useCurrentBookings(startDate) //useCurrentBookings is a hook I created
const availableFishingSpots = totalFishingSpots.filter(
(o1) => !bookings.some((o2) => o1 === o2.loc_pescuit)
)
console.log(availableFishingSpots)
setAvailableSpots({ ...availableSpots, pescuit: availableFishingSpots })
return (
<select>
{availableSpots.pescuit.map((value) => {
return (
<option value={value} key={value}>
{value}
</option>
)
})}
</select>
)
}
// Date state handler
const handleDate = (date) => {
setStartDate(date)
}
// Update the price as soon as any of the options changed
useEffect(() => {
const totalPrice =
state.intrare * 20 +
state.locParcare * 5 +
(state.casuta ? 100 : 0) +
(state.locPescuit ? 50 : 0) +
(state.sedintaFoto ? 100 : 0) +
state.sezlong * 15
setState({ ...state, totalPrice: totalPrice })
}, [state])
type booking = {
starts_at: Date
ends_at: Date
intrare_complex: number
loc_parcare: number
loc_pescuit: number
casuta: number
sezlong: number
sedinta_foto: boolean
petrecere_privata: boolean
total_price: number
}
// Here I handle the submit. "petrecerePrivata" means a private party. If that is checked
// it does something, if not, something else
function handleSubmit(event) {
event.preventDefault()
if (state.petrecerePrivata === true) {
setState({
...state,
intrare: 0,
locParcare: 0,
locPescuit: 0,
casuta: 0,
sezlong: 0,
sedintaFoto: false,
totalPrice: 100,
})
} else {
const booking: booking = {
starts_at: startDate,
ends_at: addDays(startDate, 1),
intrare_complex: state.intrare,
loc_parcare: state.locParcare,
loc_pescuit: state.locPescuit,
casuta: state.casuta,
sezlong: state.sezlong,
sedinta_foto: state.sedintaFoto,
petrecere_privata: state.petrecerePrivata,
total_price: state.totalPrice,
}
invoke(insertBooking, booking) // Insert the new created booking into the database
}
}
// State handler for everything but the price, that updates in the useEffect
const handleChange = (evt) => {
const name = evt.target.name
const value = evt.target.type === "checkbox" ? evt.target.checked : evt.target.value
setState({
...state,
[name]: value,
})
}
return (
<>
<Suspense fallback="Loading...">
<UserInfo />
</Suspense>
{
// Here starts the actual page itself
}
<div className="mx-auto max-w-xs ">
<div className="my-10 p-4 max-w-sm bg-white rounded-lg border border-gray-200 shadow-md sm:p-6 lg:p-8 dark:bg-gray-800 dark:border-gray-700">
<form className="space-y-6" action="#" onSubmit={handleSubmit}>
<h5 className="text-xl font-medium text-gray-900 dark:text-white">
Fa o rezervare noua
</h5>
{state.petrecerePrivata ? (
<>
<div>
<label
htmlFor="date"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Data
</label>
<div className="border-2 rounded">
<DatePicker
selected={startDate}
onChange={(date) => handleDate(date)}
dateFormat="dd/MM/yyyy"
includeDateIntervals={[{ start: new Date(), end: addDays(new Date(), 30) }]}
className="cursor-pointer p-2"
/>
</div>
</div>
<label
htmlFor="checked-toggle"
className="relative inline-flex items-center mb-4 cursor-pointer"
>
<input
type="checkbox"
name="petrecerePrivata"
id="checked-toggle"
className="sr-only peer"
checked={state.petrecerePrivata}
onChange={handleChange}
/>
<div className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
Petrecere Privata
</span>
</label>
</>
) : (
<>
<div>
<label
htmlFor="date"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Data
</label>
<div className="border-2 rounded">
<DatePicker
selected={startDate}
onChange={(date) => setStartDate(date)}
dateFormat="dd/MM/yyyy"
includeDateIntervals={[{ start: new Date(), end: addDays(new Date(), 30) }]}
className="cursor-pointer p-2"
/>
</div>
</div>
<div>
<label
htmlFor="intrare"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Bilete Intrare Complex
</label>
<input
type="number"
name="intrare"
id="intrare"
placeholder="1"
value={state.intrare}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
required
/>
</div>
<div>
<label
htmlFor="loParcare"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Numar Locuri de Parcare
</label>
<input
type="number"
name="locParcare"
id="locParcare"
placeholder="0"
min="0"
value={state.locParcare}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
/>
</div>
<div>
<label
htmlFor="locPescuit"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Locul de Pescuit
</label>
{
// Here I call that function inside a Suspense and things go south
}
<Suspense fallback="Cautam locurile de pescuit">
<PescuitSelect />
</Suspense>
</div>
<div>
<label
htmlFor="casuta"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Casuta
</label>
<input
type="number"
name="casuta"
id="casuta"
placeholder="0"
min="0"
max="18"
value={state.casuta}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
/>
</div>
<div>
<label
htmlFor="sezlong"
className="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300"
>
Alege Sezlong
</label>
<input
type="number"
name="sezlong"
id="sezlong"
placeholder="0"
min="0"
max="21"
value={state.sezlong}
onChange={handleChange}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
/>
</div>
<label
htmlFor="sedintaFoto"
className="relative inline-flex items-center mb-4 cursor-pointer"
>
<input
type="checkbox"
name="sedintaFoto"
id="sedintaFoto"
className="sr-only peer"
checked={state.sedintaFoto}
onChange={handleChange}
/>
<div className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
Sedinta foto
</span>
</label>
<label
htmlFor="petrecerePrivata"
className="relative inline-flex items-center mb-4 cursor-pointer"
>
<input
type="checkbox"
name="petrecerePrivata"
id="petrecerePrivata"
className="sr-only peer"
checked={state.petrecerePrivata}
onChange={handleChange}
/>
<div className="w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
<span className="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
Petrecere Privata
</span>
</label>
</>
)}
<button
type="submit"
className="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Subimt
</button>
</form>
</div>
</div>
</>
)
}
export default Add
useCurrentBookings hook:
import { useQuery } from "blitz"
import getAllBookings from "../queries/getAllBookings"
import format from "date-fns/format"
export const useCurrentBookings = (startDate) => {
const [booking] = useQuery(getAllBookings, format(startDate, "yyyy-MM-dd")) // Here I query the database
return booking
}
Actual call to the database:
import db from "db"
//And this is the actual call to the database
export default async function getAllBookings(startsAt: string) {
return await db.booking.findMany({
where: { starts_at: { gte: new Date(startsAt) } },
})
}
useEffect() runs everytime the dependencies change, inside the useEffect your updated the state and called useEffect again. Resulting in an infinite loop.
Resolution:
const [totalPrice, setTotalPrice] = useState(0);
useEffect(() => {
const totalPrice =
state.intrare * 20 +
state.locParcare * 5 +
(state.casuta ? 100 : 0) +
(state.locPescuit ? 50 : 0) +
(state.sedintaFoto ? 100 : 0) +
state.sezlong * 15
setTotalPrice(totalPrice);
}, [state])
I had this problem before, keep refreshing a react component it's because of the Lifecycle in React.
If you do not know about it, be sure to research it deeply.
https://www.w3schools.com/react/react_lifecycle.asp#:~:text=Each%20component%20in%20React%20has,Mounting%2C%20Updating%2C%20and%20Unmounting.
when you render your component it calls PescuitSelect() function
and in this function
setAvailableSpots({ ...availableSpots, pescuit: availableFishingSpots })
one of your state will be updated.
in React when a state updated, the component will refresh again for showing new data of that state
Issue
The PescuitSelect component is unconditionally updating state, which is an unintentional side-effect and triggers a rerender.
const PescuitSelect = () => {
const totalFishingSpots = Array.from(Array(114).keys())
const bookings = useCurrentBookings(startDate) //useCurrentBookings is a hook I created
const availableFishingSpots = totalFishingSpots.filter(
(o1) => !bookings.some((o2) => o1 === o2.loc_pescuit)
)
console.log(availableFishingSpots)
setAvailableSpots({ // <-- unconditional state update
...availableSpots,
pescuit: availableFishingSpots
})
return (
<select>
{availableSpots.pescuit.map((value) => {
return (
<option value={value} key={value}>
{value}
</option>
)
})}
</select>
)
}
On top of this, PescuitSelect is redeclared each render cycle since it is defined inside another React component. It's an anti-pattern to declare React components within React components. They should all be declared at the top-level. Pass any callbacks in as props if necessary instead of trying to use values/callbacks closed over from the outer scope.
There is also a useEffect hook that is updating the state it's using as its dependency.
// Update the price as soon as any of the options changed
useEffect(() => {
const totalPrice =
state.intrare * 20 +
state.locParcare * 5 +
(state.casuta ? 100 : 0) +
(state.locPescuit ? 50 : 0) +
(state.sedintaFoto ? 100 : 0) +
state.sezlong * 15
setState({ ...state, totalPrice: totalPrice })
}, [state]);
Updating state.totalPrice updates the state value and also triggers a rerender which will cause the effect to run again and enqueue another state update. This totalPrice state is easily derived from the other existing state, and as such isn't necessary to also be stored in state.
Solution
Move the PescuitSelect component declaration outside this Add component.
Since it seems that PescuitSelect doesn't have any state and the logic to compute the available fishing spots only exists to update the state in the parent, this logic should be moved to the parent and an availableSpots array passed as a prop to PescuitSelect.
Example:
const PescuitSelect = ({ options }) => (
<select>
{options.map((value) => (
<option value={value} key={value}>
{value}
</option>
))}
</select>
);
The logic that was moved should be placed in an useEffect hook. Add any necessary dependencies.
Remove the useEffect hook that is only computing a totalPrice and just compute this each render. If computations like this are expensive then use the useMemo hook to memoize the result.
Example:
type booking = {
starts_at: Date
ends_at: Date
intrare_complex: number
loc_parcare: number
loc_pescuit: number
casuta: number
sezlong: number
sedinta_foto: boolean
petrecere_privata: boolean
total_price: number
}
const Add: BlitzPage = () => {
//State for all options that will be added for the booking
const [state, setState] = useState({
intrare: 1,
locParcare: 0,
locPescuit: 0,
casuta: 0,
sezlong: 0,
sedintaFoto: false,
petrecerePrivata: false,
});
...
const [availableSpots, setAvailableSpots] = useState({
pescuit: [0],
casute: {},
sezlonguri: {},
});
const bookings = useCurrentBookings(startDate);
useEffect(() => {
const availableFishingSpots = Array.from(Array(114).keys())
.filter(o1 => !bookings.some((o2) => o1 === o2.loc_pescuit));
console.log(availableFishingSpots);
setAvailableSpots(availableSpots => ({
...availableSpots,
pescuit: availableFishingSpots,
}));
}, [bookings]);
...
// Update the price as soon as any of the options changed
const totalPrice = useMemo(() => {
return state.intrare * 20 +
state.locParcare * 5 +
(state.casuta ? 100 : 0) +
(state.locPescuit ? 50 : 0) +
(state.sedintaFoto ? 100 : 0) +
state.sezlong * 15;
}, [state]);
...
return (
<>
...
<div className="mx-auto max-w-xs ">
<div className="....">
<form className="space-y-6" action="#" onSubmit={handleSubmit}>
...
{state.petrecerePrivata ? (
...
) : (
<>
...
<div>
...
<Suspense fallback="Cautam locurile de pescuit">
<PescuitSelect options={availableSpots.pescuit} />
</Suspense>
</div>
...
</>
)}
...
</form>
</div>
</div>
</>
)
}

How to collapse Siblings with Headless UI

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.

Creating a subcollection linked to a specific document ID - Firestore

Hi everyone,
I'm trying to link comments to existing posts I have in my firestore database.
It almost works.. :) but for some reason everytime I comment on a given post, instead of using the ID of the document already existing in my collection, it just generates a new one (new document in 'posts' with a new automatically generated ID), containing the comment.
Any idea what I, obviously, do wrong?
Here's my code:
function Post({key, profilePic, image, username, timestamp, message}) {
const [{ user }, dispatch] = useStateValue();
const [comments, setComments] = useState([]);
const [comment, setComment] = useState("");
const sendComment = async (e) => {
e.preventDefault();
const commentToSend = comment;
setComment('');
await db.collection('posts').doc(key)
.collection('comments').add ({
comment: commentToSend,
profilePic: user.photoURL,
username: user.displayName,
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
})
}
return (
<div className="flex flex-col">
<div className="p-5 bg-white mt-5 rounded-t-2xl shadow-sm">
<div className="flex items-center space-x-2">
<Avatar
src={profilePic}
className="rounded-full"
/>
<p className="font-medium">{username}</p>
</div>
<div className="pt-2">
{timestamp ? (
<p className="text-xs text-gray-400">
{new Date(timestamp?.toDate()).toLocaleString()}
</p>
) : (
<p className="text-xs text-gray-400">Loading</p>
)}
</div>
<p className="pt-4 pb-4">{message}</p>
{image && (
<div className="relative h-auto bg-white overflow-hidden">
<img src={image} className="object-cover w-full" />
</div>
)}
</div>
<div className="flex justify-between items-center bg-white shadow-md text-gray-400 border-t">
<div className="inputIcon p-3 rounded-none rounded-bl-2xl">
<ThumbUpIcon className="h-4" />
<p className="text-xs sm:text-base">Like</p>
</div>
<div className="inputIcon p-3 rounded-none">
<ChatBubbleOutlineIcon className="h-4"/>
<p className="text-xs sm:text-base">Comment</p>
</div>
</div>
<div className="flex justify-evenly items-center rounded-b-2xl bg-white shadow-md text-gray-400 border-t">
<form className="flex items-center p-4">
<input
type="text"
onChange={e => setComment(e.target.value)}
value={comment}
placeholder="Add a comment.."
className="border-none flex-1 focus:ring-0 outline-none pr-10"
/>
<button
type="submit"
onClick={sendComment}
className="pl-20 font-semibold text-red-400"
>Post</button>
</form>
</div>
</div>
)
}
export default Post
Not sure if it is of importance but here's my Feed.js, rendering my posts:
function Feed() {
const [{ user }, dispatch] = useStateValue();
const [posts, setPosts] = useState([]);
useEffect(() => {
db.collection('posts').orderBy("timestamp", "desc").onSnapshot((snapshot) =>
setPosts(snapshot.docs.map(doc => ({ id: doc.id, data: doc.data()})))
);
}, []);
return (
<div className="flex-grow pb-44 pt-1 mr-4 xl:mr-30">
<div className="mx-auto max-w-md md:max-w-lg lg:max-w-2xl">
{/* <StoryReel /> */}
<MessageSender />
{posts.map(post => (
<Post
key={post.data.id}
profilePic={post.data.profilePic}
message={post.data.message}
timestamp={post.data.timestamp}
username={post.data.username}
image={post.data.image}
/>
))}
</div>
</div>
)
}
export default Feed
Thank you so much for your help!

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