Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/custom dropdown classes #339

Merged
merged 13 commits into from
Jan 21, 2025
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ jspm_packages/
.vuepress/dist
docs/.vitepress/cache

# VSCode custom configs
.vscode

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

Expand Down
2 changes: 1 addition & 1 deletion src/components/FwbButton/FwbButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ import { useButtonSpinner } from './composables/useButtonSpinner'
import type { ButtonGradient, ButtonMonochromeGradient, ButtonSize, ButtonVariant } from './types'

interface IButtonProps {
class?: string
class?: string | object
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is required to allow dynamic binding of user classes

color?: ButtonVariant
gradient?: ButtonGradient | null
size?: ButtonSize
Expand Down
4 changes: 2 additions & 2 deletions src/components/FwbButton/composables/useButtonClasses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ const buttonShadowClasses: Record<ButtonMonochromeGradient, string> = {
teal: 'shadow-lg shadow-teal-500/50 dark:shadow-lg dark:shadow-teal-800/80',
}

export type UseButtonClassesProps = {
class: Ref<string>
interface UseButtonClassesProps {
class: Ref<string|object>
pill: Ref<boolean>
disabled: Ref<boolean>
loading: Ref<boolean>
Expand Down
229 changes: 120 additions & 109 deletions src/components/FwbDropdown/FwbDropdown.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<template>
<div
ref="wrapper"
class="fwb-dropdown inline-flex relative"
ref="dropdownWrapper"
:class="wrapperClasses"
>
<div class="inline-flex items-center">
<div :class="triggerWrapperClasses">
<fwb-slot-listener @click="onToggle">
<slot name="trigger">
<fwb-button
:disabled="disabled"
:class="{'flex-row-reverse': placement === 'left', 'pl-2': placement === 'left', triggerClass}"
:color="color"
:disabled="disabled"
>
Sqrcz marked this conversation as resolved.
Show resolved Hide resolved
{{ text }}
<template #suffix>
<svg
class="w-4 h-4 ml-2"
:class="triggerSuffixClass"
class="w-4 h-4"
Comment on lines +20 to +21
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is another part for the "left placement"

fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
Expand All @@ -33,9 +35,9 @@
</div>
<transition :name="transitionName">
<div
v-if="visible"
ref="content"
:class="[contentClasses]"
v-if="isContentVisible"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

variable renamed to better match it's use

ref="contentWrapper"
Copy link
Collaborator Author

@Sqrcz Sqrcz Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref renamed to better match what it is referring to

:class="contentWrapperClasses"
:style="contentStyles"
>
<fwb-slot-listener @click="onHide">
Expand All @@ -47,54 +49,66 @@
</template>

<script lang="ts" setup>
import { computed, ref, toRef, watch } from 'vue'

import { computed, ref, watch } from 'vue'
import { onClickOutside } from '@vueuse/core'
import type { DropdownPlacement } from './types'
import { useDropdownClasses } from './composables/useDropdownClasses'
import FwbButton from '@/components/FwbButton/FwbButton.vue'
import FwbSlotListener from '@/components/utils/FwbSlotListener/FwbSlotListener.vue'
import { useDropdownClasses } from './composables/useDropdownClasses'
import type { ButtonVariant } from '@/components/FwbButton/types'
import type { DropdownPlacement } from './types'

const visible = ref(false)
const onHide = () => {
if (props.closeInside) visible.value = false
}
const onToggle = () => {
if (props.disabled) return
visible.value = !visible.value
export interface DropdownProps {
alignToEnd?: boolean
class?: string
closeInside?: boolean
color?: ButtonVariant
contentWrapperClass?: string
disabled?: boolean
placement?: DropdownPlacement
text?: string
transition?: string
triggerClass?: string
triggerWrapperClass?: string
Comment on lines +74 to +75
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

The triggerClass prop is defined but not utilized

The triggerClass prop is declared in DropdownProps and provided with a default value; however, it's not applied in the template. This omission prevents users from customizing the trigger button's classes as intended.

Apply this diff to include triggerClass in the <fwb-button> component:

             <fwb-button
-                :class="{'flex-row-reverse': placement === 'left', 'pl-2': placement === 'left'}"
+                :class="[placement === 'left' ? ['flex-row-reverse', 'pl-2'] : '', props.triggerClass]"
                 :color="color"
                 :disabled="disabled"
             >

Committable suggestion skipped: line range outside the PR's diff.

}

const props = withDefaults(
defineProps<{
placement?: DropdownPlacement
text?: string
color?: ButtonVariant
transition?: string
closeInside?: boolean
alignToEnd?: boolean
disabled?: boolean
}>(),
defineProps<DropdownProps>(),
{
placement: 'bottom',
text: '',
alignToEnd: false,
class: '',
closeInside: false,
color: 'default',
contentWrapperClass: '',
disabled: false,
placement: 'bottom',
text: 'Dropdown',
transition: '',
closeInside: false,
alignToEnd: false,
triggerClass: '',
triggerWrapperClass: '',
},
)

const dropdownWrapper = ref<HTMLDivElement>()
const contentWrapper = ref<HTMLDivElement>()
const isContentVisible = ref(false)

const onToggle = () => (isContentVisible.value = !isContentVisible.value)
const onHide = () => props.closeInside && (isContentVisible.value = false)

onClickOutside(dropdownWrapper, () =>
isContentVisible.value && (isContentVisible.value = false),
)

const emit = defineEmits<{
show: []
hide: []
}>()

watch(visible, (isVisible: boolean) => {
if (isVisible) {
emit('show')
} else {
emit('hide')
}
watch(isContentVisible, () => {
isContentVisible.value
? emit('show')
: emit('hide')
})

const placementTransitionMap: Record<DropdownPlacement, string> = {
Expand All @@ -104,89 +118,86 @@ const placementTransitionMap: Record<DropdownPlacement, string> = {
top: 'to-top',
}

const transitionName = computed(() => {
if (props.transition === null) return placementTransitionMap[props.placement]
return props.transition
})

const content = ref<HTMLDivElement>()
const wrapper = ref<HTMLDivElement>()
const transitionName = computed(() =>
(!props.transition)
? placementTransitionMap[props.placement]
: props.transition,
)

const { contentClasses, contentStyles } = useDropdownClasses({
placement: toRef(props, 'placement'),
alignToEnd: toRef(props, 'alignToEnd'),
visible,
contentRef: content,
})
const {
contentStyles,
contentWrapperClasses,
triggerSuffixClass,
triggerWrapperClasses,
wrapperClasses,
} = useDropdownClasses({ contentWrapper, isContentVisible, props })

onClickOutside(wrapper, () => {
if (!visible.value) return
visible.value = false
})
</script>

<style scoped>
/* transitions */
.to-bottom-enter-active,
.to-bottom-leave-active,
.to-left-enter-active,
.to-left-leave-active,
.to-right-enter-active,
.to-right-leave-active,
.to-top-enter-active,
.to-top-leave-active {
transition: all 250ms;
}
<style>
.fwb-dropdown {
/* transitions */
.to-bottom-enter-active,
.to-bottom-leave-active,
.to-left-enter-active,
.to-left-leave-active,
.to-right-enter-active,
.to-right-leave-active,
.to-top-enter-active,
.to-top-leave-active {
transition: all 250ms;
}

/* to top */
.to-top-enter-active,
.to-top-leave-to {
opacity: 0;
transform: translateY(10px);
}
/* to top */
.to-top-enter-active,
.to-top-leave-to {
opacity: 0;
transform: translateY(10px);
}

.to-top-leave,
.to-top-enter-to {
opacity: 1;
transform: translateY(0);
}
.to-top-leave,
.to-top-enter-to {
opacity: 1;
transform: translateY(0);
}

/* to right */
.to-right-enter-active,
.to-right-leave-to {
opacity: 0;
transform: translateX(-10px);
}
/* to right */
.to-right-enter-active,
.to-right-leave-to {
opacity: 0;
transform: translateX(-10px);
}

.to-right-leave,
.to-right-enter-to {
opacity: 1;
transform: translateX(0);
}
.to-right-leave,
.to-right-enter-to {
opacity: 1;
transform: translateX(0);
}

/* to bottom */
.to-bottom-enter-active,
.to-bottom-leave-to {
opacity: 0;
transform: translateY(-10px);
}
/* to bottom */
.to-bottom-enter-active,
.to-bottom-leave-to {
opacity: 0;
transform: translateY(-10px);
}

.to-bottom-leave,
.to-bottom-enter-to {
opacity: 1;
transform: translateY(0);
}
.to-bottom-leave,
.to-bottom-enter-to {
opacity: 1;
transform: translateY(0);
}

/* to left */
.to-left-enter-active,
.to-left-leave-to {
opacity: 0;
transform: translateX(10px);
}
/* to left */
.to-left-enter-active,
.to-left-leave-to {
opacity: 0;
transform: translateX(10px);
}

.to-left-leave,
.to-left-enter-to {
opacity: 1;
transform: translateX(0);
.to-left-leave,
.to-left-enter-to {
opacity: 1;
transform: translateX(0);
}
}
</style>
Loading
Loading