neldelesndblogtagscontactresume
neldelesndblogtagscontactresume

2021 June 28thHeadlessUI render props in Twintwin 

Adapting the Dropdown TailwindUI component for Twin.macro


Finally got it working for the Listbox. Was trying to use one of TailwindUI's Dropdown components which is built upon HeadlessUI's Listbox.

UPDATE as of 2021-06-29: There's a way to make it not a workaround and the example's right in the docs (https://headlessui.dev/react/listbox#styling-the-active-and-selected-option).

Listbox.Option by default renders as a <li> element. But if you pass in the as={Fragment} prop value, it becomes..a fragment. Then in the render prop func you can just explicitly wrap everything in a <li> and style via tw css prop normally. No extra nested div's needed.

<Listbox.Option key={person.id} value={person} + as={Fragment} > {({ active, selected }) => ( <li - className={`${ - active ? 'bg-blue-500 text-white' : 'bg-white text-black' - }`} + css={[ + active && tw`bg-blue-500 text-white`, + !active && tw`bg-white text-black` + ]} > {selected && <CheckIcon />} {person.name} </li> )} </Listbox.Option>

The relevant TailwindUI code that was giving me issues:

// Original TailwindUI Code <Transition show={open} as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0" > <Listbox.Options static className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" > {people.map((person) => ( <Listbox.Option key={person.id} className={({ active }) => classNames( active ? 'text-white bg-indigo-600' : 'text-gray-900', 'cursor-default select-none relative py-2 pl-3 pr-9' ) } value={person} > {({ selected, active }) => ( <> <span className={classNames(selected ? 'font-semibold' : 'font-normal', 'block truncate')}> {person.name} </span> {selected ? ( <span className={classNames( active ? 'text-white' : 'text-indigo-600', 'absolute inset-y-0 right-0 flex items-center pr-4' )} > <CheckIcon className="h-5 w-5" aria-hidden="true" /> </span> ) : null} </> )} </Listbox.Option> ))} </Listbox.Options> </Transition>

This is how my final working code looks like:

import { ClassNames } from "@emotion/react" // ...more code + <ClassNames> + {({ css }) => ( <Transition show={open} as={Fragment} leave={css(tw`transition ease-in duration-100`)} leaveFrom={css(tw`opacity-100`)} leaveTo={css(tw`opacity-0`)} > <Listbox.Options static tw="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" > {people.map(person => ( <Listbox.Option key={person.id} - className={({ active }) => - classNames( - active ? 'text-white bg-indigo-600' : 'text-gray-900', - 'cursor-default select-none relative py-2 pl-3 pr-9' - ) - } value={person} > {({ selected, active }) => ( - <> + <div + css={[ + tw`cursor-default select-none relative py-2 pl-3 pr-9`, + active && tw`text-white bg-indigo-600`, + !active && tw`text-gray-900`, + ]} + > <span css={[ tw`block truncate`, selected && tw`font-semibold italic`, !selected && tw`font-normal`, ]} > {person.name} </span> {selected ? ( <span css={[ tw`absolute inset-y-0 right-0 flex items-center pr-4`, active && tw`text-white`, !active && tw`text-indigo-600`, ]} > <CheckIcon tw="h-5 w-5" aria-hidden="true" /> </span> ) : null} - </> + </div> )} </Listbox.Option> ))} </Listbox.Options> </Transition> )} + </ClassNames>

Import of ClassNames is to get animations working.

To get the active styles working, what I did was to change the Fragment to a div and move the relevant css to that div, instead of writing it inside <Listbox.Option>.

What made me think of trying this out is this article on render props, specifically the parts Implementing render props and Implementing other props.

Lines 15-20 in Original TailwindUI Code are equivalent to using the render prop:

const Dismiss = (props) => { const dismiss = () => { ...code to implement dismissal animations etc } return props.render(dismiss) } const DismissableContent = () => { return ( <Dismiss render={ dismiss => <Content dismiss={dismiss} /> } /> ) }

whereas line 24 in Original TailwindUI Code is similar to the example given in Implementing other props:

const Dismiss = ({ children }) => { const dismiss = () => { ...code to implement dismissal animations etc } return children(dismiss) } const DismissableContent = () => { return ( <Dismiss> {(dismiss) => ( <Content dismiss={dismiss} /> )} </Dismiss> ) }

Both are functionally the same, but just with different implementations. Thus applying it to this problem of mine, I assume they are refering to the same active render prop argument, and I could just implement them under the child function.

Caveat: So far it's working, but I do not actually know if implementing it this way will have adverse effects.

Footer


Related: