Merge pull request #31 from AEmotionStudio/palette/editable-color-picker-6542475088779009480
🎨 Palette: Editable Hex Code in Color Picker
This commit is contained in:
@@ -381,4 +381,19 @@
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
color: var(--descrip-text, #999);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
width: 60px;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.magnify-color-preview:hover,
|
||||
.magnify-color-preview:focus {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-color: var(--border-color, #444);
|
||||
color: var(--fg-color, #ddd);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -237,12 +237,44 @@ export function createColorPicker(
|
||||
colorInput.className = 'magnify-color-input';
|
||||
colorInput.value = value;
|
||||
|
||||
const colorPreview = document.createElement('span');
|
||||
const colorPreview = document.createElement('input');
|
||||
colorPreview.type = 'text';
|
||||
colorPreview.className = 'magnify-color-preview';
|
||||
colorPreview.textContent = value;
|
||||
colorPreview.value = value;
|
||||
colorPreview.maxLength = 7;
|
||||
colorPreview.setAttribute('aria-label', `Hex code for ${label}`);
|
||||
|
||||
// Stop propagation of keys to prevent global hotkeys while typing
|
||||
colorPreview.addEventListener('keydown', (e) => e.stopPropagation());
|
||||
|
||||
// Select all on focus for easy copy/paste
|
||||
colorPreview.addEventListener('focus', () => colorPreview.select());
|
||||
|
||||
// Sync from text input to color picker
|
||||
const updateFromText = () => {
|
||||
let val = colorPreview.value;
|
||||
if (!val.startsWith('#') && /^[0-9A-Fa-f]{6}$/.test(val)) {
|
||||
val = '#' + val;
|
||||
}
|
||||
|
||||
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
|
||||
colorPreview.value = val; // Ensure normalized format
|
||||
colorInput.value = val;
|
||||
if (onInput) onInput(val);
|
||||
onChange(val);
|
||||
} else {
|
||||
// Revert to last valid value if invalid
|
||||
colorPreview.value = colorInput.value;
|
||||
}
|
||||
};
|
||||
|
||||
colorPreview.addEventListener('change', updateFromText);
|
||||
// Blur is handled by change for most cases, but keeping it as a fallback isn't harmful
|
||||
// However, since change covers commit actions (Enter/Blur), the explicit blur handler is redundant if updateFromText handles reversion.
|
||||
// Removing explicit blur handler to avoid double-handling, as change event suffices for committing value.
|
||||
|
||||
colorInput.addEventListener('input', () => {
|
||||
colorPreview.textContent = colorInput.value;
|
||||
colorPreview.value = colorInput.value;
|
||||
if (onInput) onInput(colorInput.value);
|
||||
});
|
||||
|
||||
|
||||
@@ -141,5 +141,53 @@ describe('Sidebar Controls', () => {
|
||||
expect(labelEl.getAttribute('for')).toBe(input.id);
|
||||
expect(input.id).toMatch(/^magnify-color-/);
|
||||
});
|
||||
|
||||
it('should sync text input with color picker', () => {
|
||||
const onChange = vi.fn();
|
||||
const colorRow = createColorPicker('Test Color', '#000000', onChange);
|
||||
|
||||
const colorInput = colorRow.querySelector('input[type="color"]') as HTMLInputElement;
|
||||
const textInput = colorRow.querySelector('input.magnify-color-preview') as HTMLInputElement;
|
||||
|
||||
expect(textInput).toBeTruthy();
|
||||
expect(textInput.value).toBe('#000000');
|
||||
|
||||
// Simulate typing a valid hex code
|
||||
textInput.value = '#ffffff';
|
||||
textInput.dispatchEvent(new Event('change'));
|
||||
|
||||
expect(colorInput.value).toBe('#ffffff');
|
||||
expect(onChange).toHaveBeenCalledWith('#ffffff');
|
||||
|
||||
// Simulate typing a hex code without hash
|
||||
textInput.value = 'ff0000';
|
||||
textInput.dispatchEvent(new Event('change'));
|
||||
|
||||
expect(textInput.value).toBe('#ff0000'); // Should normalize
|
||||
expect(colorInput.value).toBe('#ff0000');
|
||||
expect(onChange).toHaveBeenCalledWith('#ff0000');
|
||||
|
||||
// Simulate typing invalid hex code
|
||||
textInput.value = 'invalid';
|
||||
textInput.dispatchEvent(new Event('change'));
|
||||
|
||||
// Should not update color input (keeps previous value)
|
||||
expect(colorInput.value).toBe('#ff0000');
|
||||
// Should revert text input to last valid value immediately on change
|
||||
expect(textInput.value).toBe('#ff0000');
|
||||
});
|
||||
|
||||
it('should stop keydown propagation on text input', () => {
|
||||
const onChange = vi.fn();
|
||||
const colorRow = createColorPicker('Test Color', '#000000', onChange);
|
||||
const textInput = colorRow.querySelector('input.magnify-color-preview') as HTMLInputElement;
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: 'a' });
|
||||
const stopPropagationSpy = vi.spyOn(event, 'stopPropagation');
|
||||
|
||||
textInput.dispatchEvent(event);
|
||||
|
||||
expect(stopPropagationSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -381,4 +381,19 @@
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
color: var(--descrip-text, #999);
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
padding: 2px 4px;
|
||||
width: 60px;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.magnify-color-preview:hover,
|
||||
.magnify-color-preview:focus {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-color: var(--border-color, #444);
|
||||
color: var(--fg-color, #ddd);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -127,11 +127,31 @@ function createColorPicker(label, value, onChange, tooltip, onInput) {
|
||||
colorInput.id = colorId;
|
||||
colorInput.className = "magnify-color-input";
|
||||
colorInput.value = value;
|
||||
const colorPreview = document.createElement("span");
|
||||
const colorPreview = document.createElement("input");
|
||||
colorPreview.type = "text";
|
||||
colorPreview.className = "magnify-color-preview";
|
||||
colorPreview.textContent = value;
|
||||
colorPreview.value = value;
|
||||
colorPreview.maxLength = 7;
|
||||
colorPreview.setAttribute("aria-label", `Hex code for ${label}`);
|
||||
colorPreview.addEventListener("keydown", (e) => e.stopPropagation());
|
||||
colorPreview.addEventListener("focus", () => colorPreview.select());
|
||||
const updateFromText = () => {
|
||||
let val = colorPreview.value;
|
||||
if (!val.startsWith("#") && /^[0-9A-Fa-f]{6}$/.test(val)) {
|
||||
val = "#" + val;
|
||||
}
|
||||
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
|
||||
colorPreview.value = val;
|
||||
colorInput.value = val;
|
||||
if (onInput) onInput(val);
|
||||
onChange(val);
|
||||
} else {
|
||||
colorPreview.value = colorInput.value;
|
||||
}
|
||||
};
|
||||
colorPreview.addEventListener("change", updateFromText);
|
||||
colorInput.addEventListener("input", () => {
|
||||
colorPreview.textContent = colorInput.value;
|
||||
colorPreview.value = colorInput.value;
|
||||
if (onInput) onInput(colorInput.value);
|
||||
});
|
||||
colorInput.addEventListener("change", () => {
|
||||
|
||||
Reference in New Issue
Block a user