feat(sidebar): add editable hex code input to color picker

- Replaced read-only hex code span with an editable text input in `createColorPicker`
- Implemented bidirectional sync between text input and color picker
- Added validation for hex codes and auto-correction on blur
- Ensured keydown propagation is stopped to prevent triggering global shortcuts
- Added aria-label for accessibility
- Updated unit tests to verify sync logic and event handling
- Added CSS styles for `magnify-color-preview` input to match UI design

Co-authored-by: AEmotionStudio <163354043+AEmotionStudio@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot]
2026-02-09 16:09:24 +00:00
co-authored by AEmotionStudio
parent 7ac655da1c
commit 6bd509bdf7
3 changed files with 100 additions and 3 deletions
+15
View File
@@ -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;
}
+35 -3
View File
@@ -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);
}
};
colorPreview.addEventListener('change', updateFromText);
colorPreview.addEventListener('blur', () => {
// Revert to valid value on blur if invalid
if (!/^#[0-9A-Fa-f]{6}$/.test(colorPreview.value)) {
colorPreview.value = colorInput.value;
}
});
colorInput.addEventListener('input', () => {
colorPreview.textContent = colorInput.value;
colorPreview.value = colorInput.value;
if (onInput) onInput(colorInput.value);
});
+50
View File
@@ -141,5 +141,55 @@ 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');
// Simulate blur with invalid code -> should revert
textInput.dispatchEvent(new Event('blur'));
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();
});
});
});