Skip to content

Commit

Permalink
UXIT-1428 - Headless UI v2 component refactor (#701)
Browse files Browse the repository at this point in the history
* refactor headless ui component v2

* replace headless with headlessUI and fix active to focus

* remove aria label and implement data selectors

* upgrade headless version and remove aria lables

* fixup! refactor headless ui component v2

* Remove Transition component from Popover

---------

Co-authored-by: Mirha Masala <[email protected]>
Co-authored-by: Charly MARTIN <[email protected]>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent 91daf65 commit c620db6
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 124 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"prepare": "husky"
},
"dependencies": {
"@headlessui/react": "^2.1.2",
"@headlessui/react": "^2.1.10",
"@headlessui/tailwindcss": "^0.2.1",
"@hookform/resolvers": "^3.9.0",
"@octokit/rest": "^21.0.1",
Expand Down
34 changes: 15 additions & 19 deletions src/app/_components/CategoryListbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,21 @@ export function CategoryListbox({

return (
<Listbox value={selected} onChange={onChange}>
{({ open }) => (
<>
<ListboxButton ariaLabel="Category options" open={open}>
<span>Category</span>
<Icon component={CaretDown} size={16} weight="bold" />
</ListboxButton>
<ListboxOptions>
{totalCategoryCount && (
<ListboxOption
option={{ id: DEFAULT_CATEGORY, name: DEFAULT_CATEGORY }}
counts={totalCategoryCount}
/>
)}
{options.map((option) => (
<ListboxOption key={option.id} option={option} counts={counts} />
))}
</ListboxOptions>
</>
)}
<ListboxButton>
<span>Category</span>
<Icon component={CaretDown} size={16} weight="bold" />
</ListboxButton>
<ListboxOptions>
{totalCategoryCount && (
<ListboxOption
option={{ id: DEFAULT_CATEGORY, name: DEFAULT_CATEGORY }}
counts={totalCategoryCount}
/>
)}
{options.map((option) => (
<ListboxOption key={option.id} option={option} counts={counts} />
))}
</ListboxOptions>
</Listbox>
)
}
19 changes: 4 additions & 15 deletions src/app/_components/ListboxButton.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
import React from 'react'

import { Listbox } from '@headlessui/react'
import { ListboxButton as HeadlessUIListboxButton } from '@headlessui/react'

type ListboxButtonProps = {
ariaLabel: string
open: boolean
children: React.ReactNode
}

export function ListboxButton({
ariaLabel,
open,
children,
}: ListboxButtonProps) {
export function ListboxButton({ children }: ListboxButtonProps) {
return (
<Listbox.Button
aria-haspopup="listbox"
aria-expanded={open}
aria-label={ariaLabel}
className="focus:brand-outline inline-flex w-full items-center justify-between gap-2 rounded-lg border border-brand-300 p-3 text-brand-300 hover:border-current hover:text-brand-400 md:min-w-40"
>
<HeadlessUIListboxButton className="inline-flex w-full items-center justify-between gap-2 rounded-lg border border-brand-300 p-3 text-brand-300 focus:brand-outline hover:border-current hover:text-brand-400 md:min-w-40">
{children}
</Listbox.Button>
</HeadlessUIListboxButton>
)
}
28 changes: 9 additions & 19 deletions src/app/_components/ListboxOption.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Fragment } from 'react'

import { Listbox } from '@headlessui/react'
import { ListboxOption as HeadlessUIListboxOption } from '@headlessui/react'
import { Check } from '@phosphor-icons/react'
import { clsx } from 'clsx'

import { type CategoryCounts } from '@/types/categoryTypes'

Expand Down Expand Up @@ -31,22 +30,13 @@ function OptionContent({ option, counts }: ListboxOptionProps) {

export function ListboxOption({ option, counts }: ListboxOptionProps) {
return (
<Listbox.Option value={option.id} as={Fragment}>
{({ active, selected }) => (
<li
className={clsx(
'flex cursor-default items-center justify-between gap-12 px-5 py-2',
{ 'bg-brand-500': active, 'bg-transparent': !active },
)}
>
<OptionContent option={option} counts={counts} />
{selected && (
<span className="mb-px">
<Icon component={Check} size={20} />
</span>
)}
</li>
)}
</Listbox.Option>
<HeadlessUIListboxOption value={option.id} as={Fragment}>
<li className="group flex cursor-default items-center justify-between gap-12 bg-transparent px-5 py-2 data-[focus]:bg-brand-500">
<OptionContent option={option} counts={counts} />
<span className="mb-px group-data-[selected]:visible">
<Icon component={Check} size={20} />
</span>
</li>
</HeadlessUIListboxOption>
)
}
7 changes: 3 additions & 4 deletions src/app/_components/ListboxOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Listbox } from '@headlessui/react'
import { ListboxOptions as HeadlessUIListboxOptions } from '@headlessui/react'
import { clsx } from 'clsx'

type ListboxOptionsProps = {
Expand All @@ -13,14 +13,13 @@ export function ListboxOptions({
const positionClass = position === 'right' ? 'right-6 md:right-auto' : ''

return (
<Listbox.Options
aria-labelledby="listbox-button"
<HeadlessUIListboxOptions
className={clsx(
'absolute z-10 mt-2 overflow-hidden rounded-lg border border-brand-100 bg-brand-800 py-2 text-brand-100 focus:brand-outline focus-within:outline-2',
positionClass,
)}
>
{children}
</Listbox.Options>
</HeadlessUIListboxOptions>
)
}
60 changes: 30 additions & 30 deletions src/app/_components/NavigationPopover.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client'

import { cloneElement, Fragment } from 'react'
import { cloneElement } from 'react'

import { Popover, Transition } from '@headlessui/react'
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'
import { CaretDown } from '@phosphor-icons/react'

import { Icon } from '@/components/Icon'
Expand All @@ -14,14 +14,8 @@ type PopOverProps = {
children: React.ReactElement
}

const TransitionProps = {
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',
}
const SPACE_BETWEEN_PANEL_AND_BUTTON = 24
const SPACE_BETWEEN_PANEL_AND_VIEWPORT = 8

export function NavigationPopover({
label,
Expand All @@ -31,33 +25,39 @@ export function NavigationPopover({
}: PopOverProps) {
return (
<Popover as={as}>
<Popover.Button
<PopoverButton
aria-label={`${label} (opens a navigation menu)`}
className={mainNavItemStyles}
>
<span>{label}</span>
<span className="transition-transform ui-open:rotate-180">
<Icon component={CaretDown} size={20} color="brand-400" />
</span>
</Popover.Button>
<Popover.Overlay className="fixed inset-0 -z-10" />
<Transition as={Fragment} {...TransitionProps}>
<Popover.Panel className="absolute right-0 z-10 mt-6 xl:-right-6">
{(props) => {
const clonedChildren = cloneElement(children, {
onClick: function closeOnClickWithin() {
props.close()
},
})

return (
<div className="overflow-hidden rounded-2xl border border-brand-500 bg-brand-800 p-4">
{clonedChildren}
</div>
)
}}
</Popover.Panel>
</Transition>
</PopoverButton>

<PopoverPanel
transition
className="z-10 transition duration-200 ease-out data-[closed]:translate-y-1 data-[open]:translate-y-0 data-[closed]:opacity-0 data-[open]:opacity-100"
anchor={{
to: 'bottom',
gap: SPACE_BETWEEN_PANEL_AND_BUTTON,
padding: SPACE_BETWEEN_PANEL_AND_VIEWPORT,
}}
>
{(props) => {
const clonedChildren = cloneElement(children, {
onClick: function closeOnClickWithin() {
props.close()
},
})

return (
<div className="overflow-hidden rounded-2xl border border-brand-500 bg-brand-800 p-4">
{clonedChildren}
</div>
)
}}
</PopoverPanel>
</Popover>
)
}
31 changes: 19 additions & 12 deletions src/app/_components/SlideOver.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { Fragment } from 'react'

import { Dialog, Transition } from '@headlessui/react'
import {
Dialog,
DialogPanel,
DialogBackdrop,
Transition,
TransitionChild,
type DialogProps,
} from '@headlessui/react'

type SlideOverProps = {
open: boolean
setOpen: (open: boolean) => void
open: DialogProps['open']
setOpen: DialogProps['onClose']
children: React.ReactNode
}

export function SlideOver({ open, setOpen, children }: SlideOverProps) {
return (
<Transition.Root show={open} as={Fragment}>
<Transition show={open} as={Fragment}>
<Dialog className="relative z-10" onClose={setOpen}>
<Transition.Child
<TransitionChild
as={Fragment}
enter="ease-in-out duration-500 sm:duration-700"
enterFrom="opacity-0"
Expand All @@ -21,13 +28,13 @@ export function SlideOver({ open, setOpen, children }: SlideOverProps) {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 backdrop-blur-lg" />
</Transition.Child>
<DialogBackdrop className="fixed inset-0 backdrop-blur-lg" />
</TransitionChild>

<div className="fixed inset-0 overflow-hidden">
<div className="absolute inset-0 overflow-hidden">
<div className="pointer-events-none fixed inset-y-0 right-0 flex w-full max-w-[480px]">
<Transition.Child
<TransitionChild
as={Fragment}
enter="transform transition ease-in-out duration-500 sm:duration-700"
enterFrom="translate-x-full"
Expand All @@ -36,16 +43,16 @@ export function SlideOver({ open, setOpen, children }: SlideOverProps) {
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<Dialog.Panel className="pointer-events-auto w-full">
<DialogPanel className="pointer-events-auto w-full">
<div className="flex h-full flex-col overflow-y-scroll bg-brand-800">
{children}
</div>
</Dialog.Panel>
</Transition.Child>
</DialogPanel>
</TransitionChild>
</div>
</div>
</div>
</Dialog>
</Transition.Root>
</Transition>
)
}
36 changes: 16 additions & 20 deletions src/app/_components/SortListbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,22 @@ export function SortListbox({ sortId, onChange, options }: SortListboxProps) {

return (
<Listbox value={sortId} onChange={onChange}>
{({ open }) => (
<>
<ListboxButton ariaLabel="Sort options" open={open}>
<div className="inline-flex items-center gap-2">
<Icon component={ArrowsDownUp} />
<span className="hidden text-nowrap md:block">
{selectedOption.name}
</span>
</div>
<span className="hidden md:block">
<Icon component={CaretDown} size={16} weight="bold" />
</span>
</ListboxButton>
<ListboxOptions position="right">
{options.map((option) => (
<ListboxOption key={option.id} option={option} />
))}
</ListboxOptions>
</>
)}
<ListboxButton>
<div className="inline-flex items-center gap-2">
<Icon component={ArrowsDownUp} />
<span className="hidden text-nowrap md:block">
{selectedOption.name}
</span>
</div>
<span className="hidden md:block">
<Icon component={CaretDown} size={16} weight="bold" />
</span>
</ListboxButton>
<ListboxOptions position="right">
{options.map((option) => (
<ListboxOption key={option.id} option={option} />
))}
</ListboxOptions>
</Listbox>
)
}

0 comments on commit c620db6

Please sign in to comment.