Italian cuisine is a celebration of life's simplest pleasures, brought to life through a harmony of rich flavors and fresh ingredients. Known for its regional diversity, from the creamy risottos of the North to the zesty citrus-infused dishes of the South, Italian food is a testament to the country's rich cultural heritage. At its heart lies pasta, in all its glorious forms, each dish telling a story of tradition and family. Whether it's the comforting embrace of a lasagna or the elegant simplicity of a Caprese salad, Italian cuisine invites you into a world where food is not just nourishment but an art form, meant to be savored with every bite.
Japanese Cuisine: A World of Delicate Elegance
+
Japanese cuisine is a poetic journey through taste and tradition, where the aesthetic presentation complements the delicate balance of flavors. It's a culinary tradition that emphasizes seasonal ingredients, purity, and minimalism, inviting diners to appreciate the intrinsic flavors of each component. From the precision of sushi, a testament to the harmony between rice, fish, and wasabi, to the comforting warmth of a bowl of ramen, Japanese food is an adventurous exploration of textures and tastes. Beyond just food, it's a philosophy of balance and respect for ingredients, presented to you in dishes that are as visually stunning as they are delectably satisfying.
Mexican Cuisine: A Vibrant Fiesta of Flavors
+
Mexican cuisine is a colorful tapestry of indigenous and Spanish influences, bursting with bold flavors and hearty ingredients. It's a culinary fiesta where each dish resonates with the vibrant soul of its people, from the smoky depth of a mole sauce to the fresh zing of a ceviche. Staples such as corn, beans, and chili peppers are elevated through time-honored techniques into comfort food that feels like a warm embrace. Whether it's the communal joy of sharing a plate of tacos or savoring the intricate layers of a tamale, Mexican food is an invitation to celebrate life with every meal, offering a taste of warmth and conviviality.
Introduction
Your employer, the vibrant, dynamic recipe portal Fooderia that allows users to explore, save, and share thousands of mouth-watering recipes from around the globe. The recipes are neatly categorized into various cuisines such as Italian, Japanese, Mexican, Indian, and many others. In order to help users explore the characteristics and nuances between the cuisines the application faces the challenge of presenting them in a user-friendly manner that doesn't overwhelm the users with information while ensuring they can easily navigate through different cuisines.
Accordion is a component that allows you to show and hide content. It can be used to list the cuisines in a smaller space and let the user expand a cuisine for more information. You are given the task of implementing an Accordion component and showcase it with some mock-up data in order for stakeholders to evaluate if it could be a good fit for the job. There is some concern that a user might get lost in the information space by having multiple cuisines expanded at the same time so a requirement is that at most one cuisine may be expanded at any time.
Tutorial
Since you are well prepared and already have a neat fresh Next.js project set up, you start by considering what files and components you need, and how to quickly view and test those components. You decide for now to have a dedicated route where you can display your Accordion, and of course the Accordion component itself. After a quick chat with the backend folks you learn that the cuisine data format has been decided, although they have not yet implemented any services for it yet, so you will mock it. You start by thinking about the file structure:
app
components
Accordion
Accordion.jsx
example-data.js
index.js
Naturally, there is going to be an Accordion component, it will get some kind of list and it's probably going to display the items as such:
components/Accordion/Accordion.jsx
export default function Accordion ({ items = [] }) {
return (<ul><li>items here...</li></ul>)
}
In your discussion with the backend folks you learned that the data will be structured as an array of pairs. Each pair contains a summary or title and a body or description. From that you swing together a simple mock data file:
components/Accordion/exampleData.js
const exampleData = [
['Italian cuisine: A Symphony of flavors', 'Italian cuisine is a celebration of life\’s simplest pleasures, brought to life through a harmony of rich flavors and fresh ingredients. Known for its regional diversity, from the creamy risottos of the North to the zesty citrus-infused dishes of the South, Italian food is a testament to the country\'s rich cultural heritage. At its heart lies pasta, in all its glorious forms, each dish telling a story of tradition and family. Whether it\'s the comforting embrace of a lasagna or the elegant simplicity of a Caprese salad, Italian cuisine invites you into a world where food is not just nourishment but an art form, meant to be savored with every bite.'],
['Japanese cuisine', 'Japanese cuisine is a poetic journey through taste and tradition, where the aesthetic presentation complements the delicate balance of flavors. It\'s a culinary tradition that emphasizes seasonal ingredients, purity, and minimalism, inviting diners to appreciate the intrinsic flavors of each component. From the precision of sushi, a testament to the harmony between rice, fish, and wasabi, to the comforting warmth of a bowl of ramen, Japanese food is an adventurous exploration of textures and tastes. Beyond just food, it\'s a philosophy of balance and respect for ingredients, presented to you in dishes that are as visually stunning as they are delectably satisfying.'],
['Mexican cuisine', 'Mexican cuisine is a colorful tapestry of indigenous and Spanish influences, bursting with bold flavors and hearty ingredients. It\'s a culinary fiesta where each dish resonates with the vibrant soul of its people, from the smoky depth of a mole sauce to the fresh zing of a ceviche. Staples such as corn, beans, and chili peppers are elevated through time-honored techniques into comfort food that feels like a warm embrace. Whether it\'s the communal joy of sharing a plate of tacos or savoring the intricate layers of a tamale, Mexican food is an invitation to celebrate life with every meal, offering a taste of warmth and conviviality.']
]
export default exampleData
You also decide to have an index file in the component´s folder to make it easier to import and to control what the module is exporting:
components/Accordion/index.js
export { default } from './Accordion'
To actually view the Accordion component you create a page in the app folder for it:
app/accordion/page.jsx
import Accordion from '@/components/Accordion'
export default function AccordionPage () {
const exampleData = []
return (
<main>
<h1>Accordion</h1>
<Accordion items={exampleData} />
</main>
)
}
It is time to verify if everything is working so far, if you do not have the dev server running, start it (npm run dev) and visit http://localhost:3000/accordion to check it out.
Nice! There is your component. It is very unstyled, let's make it more representable. Since you have opted in for Tailwind css and Tailwind basically strips away all default browser styles in the css, you add a few classes to your page:
app/accordion/page.jsx
import Accordion from '@/components/Accordion'
// 'flex flex-col gap-8' will display the element as a flex box
// where the children are stacked vertically with a gap of 2rem
// 'p-4 sm:p-24' will add padding to the main element, smaller
// screens will have a padding of 1rem, while larger screens will
// have a padding of 6rem
// 'text-xl font-bold' will set the font size to 1.25rem and the
There, a little bit better. Before starting to implement the actual accordion functionality it would be nice to get hold of the mock data and provide it to the Accordion, you import the data and pass it to the Accordion and display the data:
components/Accordion/Accordion.jsx
import Accordion from '@/components/Accordion'
import exampleData from '@/components/Accordion/example-data' // <- add this line
In the Accordion component, instead of the placeholder item, map over the items and display each item to verify data has been received. When using map, remember that react requires a key for every item in the list, since no cuisine will have the same summary, you use that as a key:
components/Accordion/Accordion.jsx
export default function Accordion ({ items = [] }) {
Finally, you can start to implement the actual accordion functionality. Given that food is for everyone you realise that accessibility for your components is important. And when it comes to accessibility, using existing html elements is particularily effective in that respect both for browsers and also for any assistive tool one might use when visiting a site. As it turns out there exists such an element in this case, the <details> element seems to be something we can utilize. No time to waste, you decide to use the details element for every item in the list:
components/Accordion/Accordion.jsx
// replace the content of the <li> elements with <details> elements
export default function Accordion ({ items = [] }) {
return (
<ul>
{items.map (([summary, body]) => (
<li key={summary}>
<details>
<summary>{summary}</summary>
<p>{body}</p>
</details>
</li>)
)}
</ul>
)
}
Eh, WOW, that was easy! You have an accordion! You can now expand and collapse the items. However, one requirement was that only one item at a time should be expanded, you have to add that functionality.
The <details> element has an open attribute attached to it controlling the expansion/contraction of the element. You can set this attribute to true or false to expand or collapse the element. It does also fire a toggle event when it is expanding/contracting. Can you catch that browser event and figure out which item is expanded and which is not and close every other currently expanded elements? You give it a shot:
components/Accordion/Accordion.jsx
import { useRef } from 'react'
export default function Accordion ({ items = [] }) {
// reference the <ul> element and capture onToggle events
// fired by its children
<ul ref={ref} onToggle={handleExpansion}>
{items.map (([summary, body]) => (
<li key={summary}>
<details>
<summary>{summary}</summary>
<p>{body}</p>
</details>
</li>))
}
</ul>
)
}
Oh noo, what? That did not work as expected. The whole app just blew up in your face?
Turns out that Next.js app routes are importing and using components as React Server Components by default, but such components cannot use react hooks like useRef... The solution is to make Accordion a regular react component, often referred to client components in the realms of Next.js. Simple enough:
components/Accordion/index.jsx
'use client' // <- add this string to the top of the file
export { default } from './Accordion'
Great! That did the trick. After verifying that your handleExpansion event handler is being called by examining the browser console, you start implementing the actual behavior:
components/Accordion/Accordion.jsx
// ...
const handleExpansion = (event) => {
// Find all open details element, ref.current is the <ul> element
const opened = ref?.current.querySelectorAll ('details[open]') || []
// The details element that fired the event
const details = event.target
// Iterate all opened details elements and close them
for (const elem of opened) {
if (elem !== details) {
elem.open = false
}
}
}
// ...
Hm, when you test the component, you notice it works fine to expand an item the first time, but the next time all items closes no matter which item you click. It also seems like the clicked item first opens but in a flash it closes. What gives now? Just work already. After knocking your head for a while you realise that since the toggle event fires every time an item is expanded or contracted what happens is, if we break it down:
All items are closed
Click on an item, item expands and fires toggle event
handleExpansion is called, no elements but current element is open so the algorithm works fine.
Click on another item, item expands and fires toggle event
handleExpansion is called, now there are two elements expanded, so the algorithm keeps the current item open but closes the other one, and here comes the thing, when closing it, it fires the toggle event again with itself as the target element, causing the function to close the just opened element and keep itself closed.
Your mind is twisting, how to solve this catch 22? Could you use another event perhaps. The click event will also fire when an item is expanded, but not when it is closed in thehandleExpansion function? You need to make some small changes to the handler trying out capturing the click event instead:
components/Accordion/Accordion.jsx
import { useRef } from 'react'
export default function Accordion ({ items = [] }) {
const ref = useRef ()
const handleExpansion = (event) => {
const opened = ref?.current.querySelectorAll ('details[open]') || []
// Before we could rely on event.target being the details element
// but now we need to find the closest details element
const details = event.target.closest ('details')
if (details) {
for (const elem of opened) {
if (elem !== details) {
elem.open = false
}
}
}
}
return (
// capture the click event instead
<ul ref={ref} onClick={handleExpansion}>
{items.map (([summary, body]) => (
<li key={summary}>
<details>
<summary>{summary}</summary>
<p>{body}</p>
</details>
</li>))
}
</ul>
)
}
Now it works! You can expand and collapse the items and only one item at a time is expanded. There is a slight penalty though using the click event since it fires more often than the toggle event. But you can live with that, you suddenly have other things to worry about; the UI/UX department called and they do not like the default look of the Accordion. They also had concerns about the single select requirement, could you make it also support multi select?
Fair enough, you start by adding multi select support, because it is a no brainer:
components/Accordion/Accordion.jsx
import { useRef } from 'react'
// Add a property to the Accordion, isMultiSelect, which, if true, will
// allow the user to select multiple items at once
Sweet! Now it is time to style the Accordion. This introduces a new challenge, you need to know if an item of the Accordion is expanded or not, so you can render a different indicator (+ or -) for expanded and contracted items. For starters, you style the component the way you can and add some comments for want you want to do.
You've successfully created a styled Accordion component, but now you need to show whether each section is expanded or collapsed clearly. After some experimentation, you discover an effective approach: monitor the open state of each details element by attaching a toggle event listener. By doing this, you can maintain a list in the state that keeps track of which items are open. Then, you can use this list to dynamically display an indicator for each section, showing whether it is expanded or collapsed.