Skip to content

Commit

Permalink
feat: add support for alpha to hex-input (#91)
Browse files Browse the repository at this point in the history
Co-authored-by: Toby Zerner <toby.zerner@gmail.com>
  • Loading branch information
web-padawan and tobyzerner committed Nov 4, 2022
1 parent b3e11c8 commit f6a5d4c
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 11 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ hex-color-picker::part(hue-pointer) {
`<hex-input>` renders an unstyled `<input>` element inside a slot and exposes it for styling using
`part`. You can also pass your own `<input>` element as a child if you want to fully configure it.

| Property | Default | Description |
| -------- | ------- | -------------------------------------------- |
| `alpha` | `false` | Allows `#rgba` and `#rrggbbaa` color formats |

## Base classes

**vanilla-colorful** provides a set of base classes that can be imported without registering custom
Expand Down
2 changes: 2 additions & 0 deletions src/hex-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { HexInputBase } from './lib/entrypoints/hex-input.js';
*
* @prop {string} color - Color in HEX format.
* @attr {string} color - Selected color in HEX format.
* @prop {boolean} alpha - When true, `#rgba` and `#rrggbbaa` color formats are allowed.
* @attr {boolean} alpha - Allows `#rgba` and `#rrggbbaa` color formats.
*
* @fires color-changed - Event fired when color is changed.
*
Expand Down
52 changes: 44 additions & 8 deletions src/lib/entrypoints/hex-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { tpl } from '../utils/dom.js';
const template = tpl('<slot><input part="input" spellcheck="false"></slot>');

// Escapes all non-hexadecimal characters including "#"
const escape = (hex: string) => hex.replace(/([^0-9A-F]+)/gi, '').substring(0, 6);
const escape = (hex: string, alpha: boolean) =>
hex.replace(/([^0-9A-F]+)/gi, '').substring(0, alpha ? 8 : 6);

const $alpha = Symbol('alpha');
const $color = Symbol('color');
const $saved = Symbol('saved');
const $input = Symbol('saved');
Expand All @@ -28,11 +30,13 @@ export interface HexInputBase {

export class HexInputBase extends HTMLElement {
static get observedAttributes(): string[] {
return ['color'];
return ['alpha', 'color'];
}

private declare [$color]: string;

private declare [$alpha]: boolean;

private declare [$saved]: string;

private declare [$input]: HTMLInputElement;
Expand All @@ -46,6 +50,23 @@ export class HexInputBase extends HTMLElement {
this[$update](hex);
}

get alpha(): boolean {
return this[$alpha];
}

set alpha(alpha: boolean) {
this[$alpha] = alpha;
this.toggleAttribute('alpha', alpha);

// When alpha set to false, update color
const color = this.color;
if (color && !validHex(color, alpha)) {
this.color = color.startsWith('#')
? color.substring(0, color.length === 5 ? 4 : 7)
: color.substring(0, color.length === 4 ? 3 : 6);
}
}

connectedCallback(): void {
const root = this.attachShadow({ mode: 'open' });
root.appendChild(template.content.cloneNode(true));
Expand All @@ -70,6 +91,14 @@ export class HexInputBase extends HTMLElement {
// A user may set a property on an _instance_ of an element,
// before its prototype has been connected to this class.
// If so, we need to run it through the proper class setter.
if (this.hasOwnProperty('alpha')) {
const value = this.alpha;
delete this['alpha' as keyof this];
this.alpha = value;
} else {
this.alpha = this.hasAttribute('alpha');
}

if (this.hasOwnProperty('color')) {
const value = this.color;
delete this['color' as keyof this];
Expand All @@ -86,9 +115,9 @@ export class HexInputBase extends HTMLElement {
const { value } = target;
switch (event.type) {
case 'input':
const hex = escape(value);
const hex = escape(value, this.alpha);
this[$saved] = this.color;
if (validHex(hex) || value === '') {
if (validHex(hex, this.alpha) || value === '') {
this.color = hex;
this.dispatchEvent(
new CustomEvent('color-changed', {
Expand All @@ -99,21 +128,28 @@ export class HexInputBase extends HTMLElement {
}
break;
case 'blur':
if (value && !validHex(value)) {
if (value && !validHex(value, this.alpha)) {
this.color = this[$saved];
}
}
}

attributeChangedCallback(_attr: string, _oldVal: string, newVal: string): void {
if (this.color !== newVal) {
attributeChangedCallback(attr: string, _oldVal: string, newVal: string): void {
if (attr === 'color' && this.color !== newVal) {
this.color = newVal;
}

if (attr === 'alpha') {
const hasAlpha = newVal != null;
if (this.alpha !== hasAlpha) {
this.alpha = hasAlpha;
}
}
}

private [$update](hex: string): void {
if (this[$input]) {
this[$input].value = hex == null || hex == '' ? '' : escape(hex);
this[$input].value = hex == null || hex == '' ? '' : escape(hex, this.alpha);
}
}
}
15 changes: 12 additions & 3 deletions src/lib/utils/validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
const hex3 = /^#?[0-9A-F]{3}$/i;
const hex6 = /^#?[0-9A-F]{6}$/i;
const matcher = /^#?([0-9A-F]{3,8})$/i;

export const validHex = (color: string): boolean => hex6.test(color) || hex3.test(color);
export const validHex = (value: string, alpha?: boolean): boolean => {
const match = matcher.exec(value);
const length = match ? match[1].length : 0;

return (
length === 3 || // '#rgb' format
length === 6 || // '#rrggbb' format
(!!alpha && length === 4) || // '#rgba' format
(!!alpha && length === 8) // '#rrggbbaa' format
);
};
120 changes: 120 additions & 0 deletions src/test/hex-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ describe('hex-input', () => {
it('should work with color property set before upgrade', async () => {
const element = document.createElement('hex-input');
document.body.appendChild(element);
element.alpha = true;
element.color = '#123';
await import('../hex-input');
expect(element.color).to.equal('#123');
expect(element.alpha).to.be.true;
target = getTarget(element);
expect(target.value).to.equal('123');
document.body.removeChild(element);
Expand Down Expand Up @@ -214,4 +216,122 @@ describe('hex-input', () => {
expect(target.value).to.equal('');
});
});

describe('alpha', () => {
describe('property', () => {
beforeEach(async () => {
input = await fixture(html`<hex-input></hex-input>`);
input.alpha = true;
input.color = '#11223344';
target = getTarget(input);
});

it('should allow setting 8 digits HEX when alpha is set with property', () => {
expect(input.color).to.equal('#11223344');
expect(target.value).to.equal('11223344');
});

it('should set alpha attribute when property is set to true', () => {
expect(input.hasAttribute('alpha')).to.be.true;
});

it('should remove alpha attribute when property is set to false', () => {
input.alpha = false;
expect(input.hasAttribute('alpha')).to.be.false;
});

it('should update input value to 6 digits when alpha is set to false', () => {
input.alpha = false;
expect(input.color).to.equal('#112233');
expect(target.value).to.equal('112233');
});

it('should update non-prefixed value to 6 digits when alpha is set to false', () => {
input.color = '11223344';
input.alpha = false;
expect(input.color).to.equal('112233');
expect(target.value).to.equal('112233');
});

it('should update shorthand value to 3 digits when alpha is set to false', () => {
input.color = '#1234';
input.alpha = false;
expect(input.color).to.equal('#123');
expect(target.value).to.equal('123');
});

it('should update non-prefixed shorthand value to 3 digits when alpha is set to false', () => {
input.color = '1234';
input.alpha = false;
expect(input.color).to.equal('123');
expect(target.value).to.equal('123');
});

it('should not allow using 8 digits HEX when alpha is set to false', async () => {
input.alpha = false;
input.focus();

await sendKeys({ press: '3' });
await sendKeys({ press: '6' });
target.dispatchEvent(new Event('blur'));

expect(target.value).to.equal('112233');
});
});

describe('attribute', () => {
beforeEach(async () => {
input = await fixture(html`<hex-input color="#11223344" alpha></hex-input>`);
target = getTarget(input);
});

it('should allow setting 8 digits HEX when alpha is set with attribute', () => {
expect(input.color).to.equal('#11223344');
expect(target.value).to.equal('11223344');
});

it('should set alpha property to false when attribute is removed', () => {
input.removeAttribute('alpha');
expect(input.alpha).to.be.false;
});

it('should update input value to 6 digits when attribute is removed', () => {
input.removeAttribute('alpha');
expect(input.color).to.equal('#112233');
expect(target.value).to.equal('112233');
});

it('should update non-prefixed value to 6 digits when attribute is removed', () => {
input.color = '11223344';
input.removeAttribute('alpha');
expect(input.color).to.equal('112233');
expect(target.value).to.equal('112233');
});

it('should update shorthand value to 3 digits when attribute is removed', () => {
input.color = '#1234';
input.removeAttribute('alpha');
expect(input.color).to.equal('#123');
expect(target.value).to.equal('123');
});

it('should update non-prefixed shorthand value to 3 digits when attribute is removed', () => {
input.color = '1234';
input.removeAttribute('alpha');
expect(input.color).to.equal('123');
expect(target.value).to.equal('123');
});

it('should not allow using 8 digits HEX when attribute is removed', async () => {
input.removeAttribute('alpha');
input.focus();

await sendKeys({ press: '3' });
await sendKeys({ press: '6' });
target.dispatchEvent(new Event('blur'));

expect(target.value).to.equal('112233');
});
});
});
});

0 comments on commit f6a5d4c

Please sign in to comment.