Skip to content

Commit

Permalink
Merge pull request #26 from xmimiex/feat/dropdown
Browse files Browse the repository at this point in the history
Feat/dropdown
  • Loading branch information
jcfauchet authored May 28, 2024
2 parents 2911569 + c9196d6 commit 8ffdc5f
Show file tree
Hide file tree
Showing 11 changed files with 633 additions and 18 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ module.exports = {
- Badges
- Breadcrumbs
- Buttons
- CodeBlock
- Drawer
- Dropdown
- Inputs
- Modal
- Ratings
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flowbite-qwik",
"version": "0.11.0",
"version": "0.12.0",
"description": "Official Qwik components built for Flowbite and Tailwind CSS",
"keywords": [
"design-system",
Expand Down
Binary file added public/profile-picture.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import clsx from 'clsx'
import { twMerge } from 'tailwind-merge'
import { useDrawerClasses } from './composables/use-drawer-classes'
import { DrawerPosition } from './drawer-types'
import { IconProps } from '@qwikest/icons/*'
import { IconProps } from '@qwikest/icons'
import { IconCloseOutline } from '../Icon'
import { isServer } from '@builder.io/qwik/build'

Expand Down
286 changes: 286 additions & 0 deletions src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import {
$,
ClassList,
Component,
component$,
FunctionComponent,
JSXChildren,
JSXOutput,
PropsOf,
Slot,
useComputed$,
useSignal,
useStore,
} from '@builder.io/qwik'
import { getChild } from '~/utils/getChild'
import { Button } from '~/components/Button/Button'
import { useDocumentOuterClick } from '~/composables/use-outer-click'
import { ButtonSize } from '~/components/Button/button-types'
import { IconProps } from '@qwikest/icons'
import { IconAngleDownOutline } from '~/components/Icon'
import { DropdownSize } from '~/components/Dropdown/dropdown-types'
import { useDropdownClasses } from '~/components/Dropdown/composables/use-dropdown-classes'

interface ComponentType {
id: number
value?: string
header: boolean
divider: boolean
icon?: Component<IconProps>
content: JSXChildren
onClick$?: () => void
}

type DropdownProps = PropsOf<'div'> & {
label: string
as?: JSXOutput
closeWhenSelect?: boolean
inline?: boolean
size?: DropdownSize
iconRotate?: string | number
}
export const Dropdown: FunctionComponent<DropdownProps> = ({
children,
label,
as,
closeWhenSelect = true,
inline = false,
size = 'm',
iconRotate,
...attrs
}) => {
const childrenToProcess = Array.isArray(children) ? [...children] : [children]

const components: ComponentType[] = []

getChild(childrenToProcess, [
{
component: DropdownItem,
foundComponentCallback: (child, index) => {
components.push({
id: index,
value: child.props.value as string | undefined,
header: Boolean(child.props.header),
divider: Boolean(child.props.divider),
icon: child.props.icon as Component | undefined,
content: child.children,
onClick$: child.props.onClick$ as () => void | undefined,
})
},
},
])

return (
<div>
<InnerDropdown
components={components}
label={label}
as={as}
closeWhenSelect={closeWhenSelect}
inline={inline}
size={size}
iconRotate={iconRotate}
class={attrs.class}
/>
</div>
)
}

type DropdownItemProps = {
value?: string
header?: boolean
divider?: boolean
icon?: Component<IconProps>
onClick$?: () => void
}
export const DropdownItem = component$<DropdownItemProps>(() => {
return <Slot />
})

/**
* InnerDropdown
*/

type InnerDropdownProps = {
label: string
as?: JSXOutput
closeWhenSelect: boolean
components: ComponentType[]
inline: boolean
size: DropdownSize
iconRotate?: string | number
class?: ClassList
}

const InnerDropdown = component$<InnerDropdownProps>(({ label, as, closeWhenSelect, components, inline, size, iconRotate, class: classNames }) => {
const { dropdownModalClasses } = useDropdownClasses(useComputed$(() => size))

const visible = useSignal(false)
const dropdownRef = useSignal<HTMLDivElement>()
const componentsAsSignals = useStore(() => components, { deep: true })

const toggleVisible = $(() => {
visible.value = !visible.value
})
const TriggerButton = inline ? InnerTriggerInline : InnerTriggerButton
const TriggerButtonAs = as ? InnerTriggerAs : undefined

useDocumentOuterClick([dropdownRef], toggleVisible, visible) // FIXME first double-click

return (
<div class={['inline-flex relative justify-center']}>
<div ref={dropdownRef}>
{TriggerButtonAs ? (
<TriggerButtonAs onClick$={toggleVisible} size={size} as={as} />
) : (
<TriggerButton onClick$={toggleVisible} label={label} size={size} iconRotate={iconRotate} />
)}

{visible.value && (
<div role="menu" class={[classNames, dropdownModalClasses.value]}>
<ul tabIndex={0} class="py-1 focus:outline-none">
{componentsAsSignals.map((comp) => (
<li role="menuitem" key={comp.id}>
{comp.header ? (
<InnerDropdownHeader size={size}>{comp.content}</InnerDropdownHeader>
) : comp.divider ? (
<InnerDropdownDivider size={size} />
) : (
<InnerDropdownItem
size={size}
icon={comp.icon}
onClick$={$(() => {
comp.onClick$?.()

if (closeWhenSelect) {
toggleVisible()
}
})}
>
{comp.content}
</InnerDropdownItem>
)}
</li>
))}
</ul>
</div>
)}
</div>
</div>
)
})

/**
* InnerDropdownHeader
*/
type InnerDropdownHeaderProps = {
size: DropdownSize
}
const InnerDropdownHeader = component$<InnerDropdownHeaderProps>(({ size }) => {
const { dropdownHeaderContainerClasses, dropdownHeaderSeparatorClasses } = useDropdownClasses(useComputed$(() => size))

return (
<>
<div class={dropdownHeaderContainerClasses.value}>
<Slot />
</div>
<div class={dropdownHeaderSeparatorClasses.value} />
</>
)
})

/**
* InnerDropdownDivider
*/
type InnerDropdownDividerProps = {
size: DropdownSize
}
const InnerDropdownDivider = component$<InnerDropdownDividerProps>(({ size }) => {
const { dropdownDividerClasses } = useDropdownClasses(useComputed$(() => size))

return <div class={dropdownDividerClasses.value} />
})

/**
* InnerDropdownItem
*/
type InnerDropdownItemProps = PropsOf<'button'> & {
icon?: Component<IconProps>
onClick$?: () => void
size: DropdownSize
}
const InnerDropdownItem = component$<InnerDropdownItemProps>(({ icon: Icon, size, onClick$, ...props }) => {
const { dropdownItemClasses } = useDropdownClasses(useComputed$(() => size))

return (
<button type="button" class={dropdownItemClasses.value} onClick$={onClick$} disabled={props.disabled}>
{Icon && <Icon class="h-4 w-4 mr-2" />}
<Slot />
</button>
)
})

/**
* InnerTriggerInline
*/
type InnerTriggerInlineProps = {
label: string
onClick$: () => void
size: DropdownSize
iconRotate?: string | number
}
const InnerTriggerInline = component$<InnerTriggerInlineProps>(({ label, size, iconRotate, onClick$ }) => {
const { triggerInlineClasses } = useDropdownClasses(useComputed$(() => size))

return (
<button onClick$={onClick$} class={triggerInlineClasses.value}>
{label}
<IconAngleDownOutline style={iconRotate ? { transform: `rotate(${iconRotate}deg)` } : undefined} />
</button>
)
})

/**
* InnerTriggerAs
*/
type InnerTriggerAsProps = {
onClick$: () => void
size: DropdownSize
as: JSXOutput
}
const InnerTriggerAs = component$<InnerTriggerAsProps>(({ as, size, onClick$ }) => {
const { triggerInlineClasses } = useDropdownClasses(useComputed$(() => size))

return (
<button onClick$={onClick$} class={triggerInlineClasses.value}>
{as}
</button>
)
})

/**
* InnerTriggerButton
*/
type InnerTriggerButtonProps = {
label: string
onClick$: () => void
size: DropdownSize
iconRotate?: string | number
}
const InnerTriggerButton = component$<InnerTriggerButtonProps>(({ label, size, iconRotate, onClick$ }) => {
const buttonSize: Record<string, ButtonSize> = {
s: 'sm',
m: 'md',
l: 'lg',
}

return (
<Button
onClick$={onClick$}
size={buttonSize[size]}
suffix={<IconAngleDownOutline rotate={iconRotate} style={iconRotate ? { transform: `rotate(${iconRotate}deg)` } : undefined} />}
>
{label}
</Button>
)
})
43 changes: 43 additions & 0 deletions src/components/Dropdown/composables/use-dropdown-classes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Signal, useComputed$ } from '@builder.io/qwik'
import { twMerge } from 'tailwind-merge'
import { DropdownSize } from '~/components/Dropdown/dropdown-types'

export function useDropdownClasses(size: Signal<DropdownSize>) {
const dropdownModalClasses = useComputed$(() => {
return twMerge(
'w-full z-10 absolute left-0 divide-y divide-gray-100 rounded shadow focus:outline-none transition-opacity duration-100 border border-gray-200 bg-white text-gray-900 dark:border-none dark:bg-gray-700 dark:text-white',
size.value === 's' ? 'top-10' : '',
size.value === 'm' ? 'top-11' : '',
size.value === 'l' ? 'top-[3.25rem]' : '',
)
})

const triggerInlineClasses = useComputed$(() => {
return twMerge(
'inline-flex gap-2 items-center',
size.value === 's' ? 'text-sm px-3 py-1.5' : '',
size.value === 'm' ? 'text-sm px-4 py-2' : '',
size.value === 'l' ? 'text-base px-5 py-2.5' : '',
)
})

const dropdownItemClasses = useComputed$(
() =>
'flex w-full cursor-pointer items-center justify-start px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:text-gray-200 dark:hover:bg-gray-600 dark:hover:text-white dark:focus:bg-gray-600 dark:focus:text-white',
)

const dropdownDividerClasses = useComputed$(() => 'my-1 h-px bg-gray-100 dark:bg-gray-600')

const dropdownHeaderContainerClasses = useComputed$(() => 'px-4 py-2 text-sm text-gray-700 dark:text-gray-200')

const dropdownHeaderSeparatorClasses = useComputed$(() => 'my-1 h-px bg-gray-100 dark:bg-gray-600')

return {
dropdownModalClasses,
dropdownHeaderContainerClasses,
dropdownHeaderSeparatorClasses,
dropdownItemClasses,
triggerInlineClasses,
dropdownDividerClasses,
}
}
1 change: 1 addition & 0 deletions src/components/Dropdown/dropdown-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type DropdownSize = 's' | 'm' | 'l'
4 changes: 2 additions & 2 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { $, component$, JSXOutput, PropsOf, Signal, Slot, useComputed$, useSignal, useTask$ } from '@builder.io/qwik'
import { ModalSize } from '~/components/Modal/modal-types'
import { useOuterClick } from '~/composables/use-outer-click'
import { useComponentOuterClick } from '~/composables/use-outer-click'
import { isServer } from '@builder.io/qwik/build'
import { useModalClasses } from '~/components/Modal/composables/use-modal-classes'

Expand Down Expand Up @@ -44,7 +44,7 @@ export const Modal = component$<ModalProps>(
}
})

useOuterClick(
useComponentOuterClick(
[modalRef],
$(() => {
if (!persistent) {
Expand Down
Loading

0 comments on commit 8ffdc5f

Please sign in to comment.