Skip to content

Commit

Permalink
[Refactor] Extract color selector as component (#2620)
Browse files Browse the repository at this point in the history
  • Loading branch information
huchenlei authored Feb 18, 2025
1 parent 01ffc9e commit d1a682b
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 67 deletions.
83 changes: 83 additions & 0 deletions src/components/common/ColorCustomizationSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<template>
<div
class="color-customization-selector-container flex flex-row items-center gap-2"
>
<SelectButton
v-model="selectedColorOption"
:options="colorOptionsWithCustom"
optionLabel="name"
dataKey="value"
:allow-empty="false"
>
<template #option="slotProps">
<div
v-if="slotProps.option.name !== '_custom'"
:style="{
width: '20px',
height: '20px',
backgroundColor: slotProps.option.value,
borderRadius: '50%'
}"
></div>
<i v-else class="pi pi-palette text-lg"></i>
</template>
</SelectButton>
<ColorPicker
v-if="selectedColorOption.name === '_custom'"
v-model="customColorValue"
/>
</div>
</template>

<script setup lang="ts">
import ColorPicker from 'primevue/colorpicker'
import SelectButton from 'primevue/selectbutton'
import { computed, onMounted, ref, watch } from 'vue'
const { modelValue, colorOptions } = defineProps<{
modelValue: string | null
colorOptions: { name: Exclude<string, '_custom'>; value: string }[]
}>()
const customColorOption = { name: '_custom', value: '' }
const colorOptionsWithCustom = computed(() => [
...colorOptions,
customColorOption
])
const emit = defineEmits<{
'update:modelValue': [value: string | null]
}>()
const selectedColorOption = ref(customColorOption)
const customColorValue = ref('')
// Initialize the component with the provided modelValue
onMounted(() => {
if (modelValue) {
const predefinedColor = colorOptions.find((opt) => opt.value === modelValue)
if (predefinedColor) {
selectedColorOption.value = predefinedColor
} else {
selectedColorOption.value = customColorOption
customColorValue.value = modelValue.replace('#', '')
}
}
})
// Watch for changes in selection and emit updates
watch(selectedColorOption, (newOption, oldOption) => {
if (newOption.name === '_custom') {
// Inherit the color from previous selection
customColorValue.value = oldOption.value.replace('#', '')
} else {
emit('update:modelValue', newOption.value)
}
})
watch(customColorValue, (newValue) => {
if (selectedColorOption.value.name === '_custom') {
emit('update:modelValue', newValue ? `#${newValue}` : null)
}
})
</script>
83 changes: 16 additions & 67 deletions src/components/common/CustomizationDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,10 @@
<Divider />
<div class="field color-field">
<label for="color">{{ $t('g.color') }}</label>
<div class="color-picker-container">
<SelectButton
v-model="selectedColor"
:options="colorOptions"
optionLabel="name"
dataKey="value"
:allow-empty="false"
>
<template #option="slotProps">
<div
v-if="slotProps.option.value !== 'custom'"
:style="{
width: '20px',
height: '20px',
backgroundColor: slotProps.option.value,
borderRadius: '50%'
}"
></div>
<i
v-else
class="pi pi-palette"
:style="{ fontSize: '1.2rem' }"
v-tooltip="$t('color.custom')"
></i>
</template>
</SelectButton>
<ColorPicker
v-if="selectedColor.value === 'custom'"
v-model="customColor"
/>
</div>
<ColorCustomizationSelector
v-model="finalColor"
:color-options="colorOptions"
/>
</div>
</div>
<template #footer>
Expand All @@ -72,13 +45,13 @@

<script setup lang="ts">
import Button from 'primevue/button'
import ColorPicker from 'primevue/colorpicker'
import Dialog from 'primevue/dialog'
import Divider from 'primevue/divider'
import SelectButton from 'primevue/selectbutton'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ColorCustomizationSelector from '@/components/common/ColorCustomizationSelector.vue'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
const { t } = useI18n()
Expand Down Expand Up @@ -118,51 +91,33 @@ const colorOptions = [
{ name: t('color.green'), value: '#28a745' },
{ name: t('color.red'), value: '#dc3545' },
{ name: t('color.pink'), value: '#e83e8c' },
{ name: t('color.yellow'), value: '#ffc107' },
{ name: t('color.custom'), value: 'custom' }
{ name: t('color.yellow'), value: '#ffc107' }
]
const defaultIcon = iconOptions.find(
(option) => option.value === nodeBookmarkStore.defaultBookmarkIcon
)
const defaultColor = colorOptions.find(
(option) => option.value === nodeBookmarkStore.defaultBookmarkColor
)
const selectedIcon = ref<{ name: string; value: string }>(defaultIcon)
const selectedColor = ref<{ name: string; value: string }>(defaultColor)
const finalColor = computed(() =>
selectedColor.value.value === 'custom'
? `#${customColor.value}`
: selectedColor.value.value
const finalColor = ref(
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
)
const customColor = ref('000000')
const closeDialog = () => {
visible.value = false
const resetCustomization = () => {
selectedIcon.value =
iconOptions.find((option) => option.value === props.initialIcon) ||
defaultIcon
finalColor.value =
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
}
const confirmCustomization = () => {
emit('confirm', selectedIcon.value.value, finalColor.value)
closeDialog()
}
const resetCustomization = () => {
selectedIcon.value =
iconOptions.find((option) => option.value === props.initialIcon) ||
defaultIcon
const colorOption = colorOptions.find(
(option) => option.value === props.initialColor
)
if (!props.initialColor) {
selectedColor.value = defaultColor
} else if (!colorOption) {
customColor.value = props.initialColor.replace('#', '')
selectedColor.value = { name: t('color.custom'), value: 'custom' }
} else {
selectedColor.value = colorOption
}
const closeDialog = () => {
visible.value = false
}
watch(
Expand Down Expand Up @@ -190,10 +145,4 @@ watch(
flex-direction: column;
gap: 0.5rem;
}
.color-picker-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>
129 changes: 129 additions & 0 deletions src/components/common/__tests__/ColorCustomizationSelector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { mount } from '@vue/test-utils'
import ColorPicker from 'primevue/colorpicker'
import PrimeVue from 'primevue/config'
import SelectButton from 'primevue/selectbutton'
import { beforeEach, describe, expect, it } from 'vitest'
import { createApp, nextTick } from 'vue'

import ColorCustomizationSelector from '../ColorCustomizationSelector.vue'

describe('ColorCustomizationSelector', () => {
const colorOptions = [
{ name: 'Blue', value: '#0d6efd' },
{ name: 'Green', value: '#28a745' }
]

beforeEach(() => {
// Setup PrimeVue
const app = createApp({})
app.use(PrimeVue)
})

const mountComponent = (props = {}) => {
return mount(ColorCustomizationSelector, {
global: {
plugins: [PrimeVue],
components: { SelectButton, ColorPicker }
},
props: {
modelValue: null,
colorOptions,
...props
}
})
}

it('renders predefined color options and custom option', () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)

expect(selectButton.props('options')).toHaveLength(colorOptions.length + 1)
expect(selectButton.props('options')?.at(-1)?.name).toBe('_custom')
})

it('initializes with predefined color when provided', async () => {
const wrapper = mountComponent({
modelValue: '#0d6efd'
})

await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: 'Blue',
value: '#0d6efd'
})
})

it('initializes with custom color when non-predefined color provided', async () => {
const wrapper = mountComponent({
modelValue: '#123456'
})

await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
const colorPicker = wrapper.findComponent(ColorPicker)

expect(selectButton.props('modelValue').name).toBe('_custom')
expect(colorPicker.props('modelValue')).toBe('123456')
})

it('shows color picker when custom option is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)

// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })

expect(wrapper.findComponent(ColorPicker).exists()).toBe(true)
})

it('emits update when predefined color is selected', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)

await selectButton.setValue(colorOptions[0])

expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#0d6efd'])
})

it('emits update when custom color is changed', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)

// Select custom option
await selectButton.setValue({ name: '_custom', value: '' })

// Change custom color
const colorPicker = wrapper.findComponent(ColorPicker)
await colorPicker.setValue('ff0000')

expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['#ff0000'])
})

it('inherits color from previous selection when switching to custom', async () => {
const wrapper = mountComponent()
const selectButton = wrapper.findComponent(SelectButton)

// First select a predefined color
await selectButton.setValue(colorOptions[0])

// Then switch to custom
await selectButton.setValue({ name: '_custom', value: '' })

const colorPicker = wrapper.findComponent(ColorPicker)
expect(colorPicker.props('modelValue')).toBe('0d6efd')
})

it('handles null modelValue correctly', async () => {
const wrapper = mountComponent({
modelValue: null
})

await nextTick()
const selectButton = wrapper.findComponent(SelectButton)
expect(selectButton.props('modelValue')).toEqual({
name: '_custom',
value: ''
})
})
})

0 comments on commit d1a682b

Please sign in to comment.