-
Notifications
You must be signed in to change notification settings - Fork 186
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Refactor] Extract color selector as component (#2620)
- Loading branch information
Showing
3 changed files
with
228 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
src/components/common/__tests__/ColorCustomizationSelector.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: '' | ||
}) | ||
}) | ||
}) |