Skip to content

Commit 88ff275

Browse files
committed
refactor(theme): split parseHex out to a reusable function and adds tests
1 parent 768cfa8 commit 88ff275

File tree

2 files changed

+113
-40
lines changed

2 files changed

+113
-40
lines changed

core/src/utils/test/theme.spec.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import {
1010
getClassList,
1111
getClassMap,
1212
getCustomTheme,
13+
hexToRgb,
1314
injectCSS,
15+
mix,
1416
} from '../theme';
1517

1618
describe('getClassList()', () => {
@@ -649,3 +651,78 @@ describe('generateColorClasses', () => {
649651
expect(css).toBe('');
650652
});
651653
});
654+
655+
describe('hexToRgb()', () => {
656+
it('should convert 6-digit hex colors to RGB strings', () => {
657+
expect(hexToRgb('#ffffff')).toBe('255, 255, 255');
658+
expect(hexToRgb('#000000')).toBe('0, 0, 0');
659+
expect(hexToRgb('#ff0000')).toBe('255, 0, 0');
660+
expect(hexToRgb('#00ff00')).toBe('0, 255, 0');
661+
expect(hexToRgb('#0000ff')).toBe('0, 0, 255');
662+
expect(hexToRgb('#3880ff')).toBe('56, 128, 255');
663+
});
664+
665+
it('should convert 3-digit hex colors to RGB strings', () => {
666+
expect(hexToRgb('#fff')).toBe('255, 255, 255');
667+
expect(hexToRgb('#000')).toBe('0, 0, 0');
668+
expect(hexToRgb('#f00')).toBe('255, 0, 0');
669+
expect(hexToRgb('#0f0')).toBe('0, 255, 0');
670+
expect(hexToRgb('#00f')).toBe('0, 0, 255');
671+
expect(hexToRgb('#abc')).toBe('170, 187, 204');
672+
});
673+
674+
it('should handle hex colors without hash prefix', () => {
675+
expect(hexToRgb('ffffff')).toBe('255, 255, 255');
676+
expect(hexToRgb('fff')).toBe('255, 255, 255');
677+
expect(hexToRgb('3880ff')).toBe('56, 128, 255');
678+
});
679+
});
680+
681+
describe('mix()', () => {
682+
it('should mix two hex colors by weight percentage', () => {
683+
// Mix white into black
684+
expect(mix('#000000', '#ffffff', '0%')).toBe('#000000');
685+
expect(mix('#000000', '#ffffff', '50%')).toBe('#808080');
686+
expect(mix('#000000', '#ffffff', '100%')).toBe('#ffffff');
687+
});
688+
689+
it('should mix colors with different percentages', () => {
690+
// Mix red into blue
691+
expect(mix('#0000ff', '#ff0000', '25%')).toBe('#4000bf');
692+
expect(mix('#0000ff', '#ff0000', '75%')).toBe('#bf0040');
693+
});
694+
695+
it('should handle 3-digit hex colors', () => {
696+
expect(mix('#000', '#fff', '50%')).toBe('#808080');
697+
expect(mix('#f00', '#0f0', '50%')).toBe('#808000');
698+
});
699+
700+
it('should handle hex colors without hash prefix', () => {
701+
expect(mix('000000', 'ffffff', '50%')).toBe('#808080');
702+
expect(mix('f00', '0f0', '50%')).toBe('#808000');
703+
});
704+
705+
it('should handle fractional percentages', () => {
706+
expect(mix('#000000', '#ffffff', '12.5%')).toBe('#202020');
707+
expect(mix('#ffffff', '#000000', '87.5%')).toBe('#202020');
708+
});
709+
710+
it('should work with real-world color examples', () => {
711+
// Mix primary Ionic blue with white
712+
expect(mix('#3880ff', '#ffffff', '10%')).toBe('#4c8dff');
713+
714+
// Mix primary Ionic blue with black for shade
715+
expect(mix('#3880ff', '#000000', '12%')).toBe('#3171e0');
716+
});
717+
718+
it('should handle edge cases', () => {
719+
// Same colors should return base color regardless of weight
720+
expect(mix('#ff0000', '#ff0000', '50%')).toBe('#ff0000');
721+
722+
// Zero weight should return base color
723+
expect(mix('#123456', '#abcdef', '0%')).toBe('#123456');
724+
725+
// 100% weight should return mix color
726+
expect(mix('#123456', '#abcdef', '100%')).toBe('#abcdef');
727+
});
728+
});

core/src/utils/theme.ts

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -401,62 +401,58 @@ export const applyComponentTheme = (element: HTMLElement): void => {
401401
};
402402

403403
/**
404-
* Converts a hex color to RGB comma-separated values
405-
* @param hex Hex color (e.g., '#ffffff' or '#fff')
406-
* @returns RGB string (e.g., '255, 255, 255')
404+
* Parses a hex color string and returns RGB values as an array.
405+
*
406+
* @param hex Hex color (e.g. `'#ffffff'` or `'#fff'`)
407+
*
408+
* @returns RGB values as `[r, g, b]` array
407409
*/
408-
export const hexToRgb = (hex: string): string => {
410+
const parseHex = (hex: string): [number, number, number] => {
409411
const cleanHex = hex.replace('#', '');
410412

411-
let r: number, g: number, b: number;
412-
413+
// Short hex format like 'fff' → expand to 'ffffff'
413414
if (cleanHex.length === 3) {
414-
// Short hex format like 'fff' → expand to 'ffffff'
415-
r = parseInt(cleanHex[0] + cleanHex[0], 16);
416-
g = parseInt(cleanHex[1] + cleanHex[1], 16);
417-
b = parseInt(cleanHex[2] + cleanHex[2], 16);
418-
} else {
415+
return [
416+
parseInt(cleanHex[0] + cleanHex[0], 16),
417+
parseInt(cleanHex[1] + cleanHex[1], 16),
418+
parseInt(cleanHex[2] + cleanHex[2], 16),
419+
];
419420
// Full hex format like 'ffffff'
420-
r = parseInt(cleanHex.substr(0, 2), 16);
421-
g = parseInt(cleanHex.substr(2, 2), 16);
422-
b = parseInt(cleanHex.substr(4, 2), 16);
421+
} else {
422+
return [
423+
parseInt(cleanHex.substring(0, 2), 16),
424+
parseInt(cleanHex.substring(2, 4), 16),
425+
parseInt(cleanHex.substring(4, 6), 16),
426+
];
423427
}
428+
};
424429

430+
/**
431+
* Converts a hex color to a string of RGB comma-separated values.
432+
*
433+
* @param hex Hex color (e.g. `'#ffffff'` or `'#fff'`)
434+
*
435+
* @returns RGB string (e.g. `'255, 255, 255'`)
436+
*/
437+
export const hexToRgb = (hex: string): string => {
438+
const [r, g, b] = parseHex(hex);
425439
return `${r}, ${g}, ${b}`;
426440
};
427441

428442
/**
429-
* Mixes two hex colors by a given weight percentage
430-
* @param baseColor Base color (e.g., '#0054e9')
431-
* @param mixColor Color to mix in (e.g., '#000000' or '#fff')
432-
* @param weight Weight percentage as string - how much of mixColor to mix into baseColor (e.g., '12%')
433-
* @returns Mixed hex color
443+
* Mixes two hex colors by a given weight percentage and returns
444+
* it as a hex color.
445+
*
446+
* @param baseColor Base color (e.g. `'#0054e9'`)
447+
* @param mixColor Color to mix in (e.g. `'#000000'` or `'#fff'`)
448+
* @param weight Weight percentage as string - how much of mixColor to mix into baseColor (e.g. `'12%'`)
449+
*
450+
* @returns Mixed hex color (e.g. `'#004acd'`)
434451
*/
435452
export const mix = (baseColor: string, mixColor: string, weight: string): string => {
436453
// Parse weight percentage
437454
const w = parseFloat(weight.replace('%', '')) / 100;
438455

439-
// Parse hex colors
440-
const parseHex = (hex: string): [number, number, number] => {
441-
const cleanHex = hex.replace('#', '');
442-
443-
// Short hex format like 'fff' → expand to 'ffffff'
444-
if (cleanHex.length === 3) {
445-
return [
446-
parseInt(cleanHex[0] + cleanHex[0], 16),
447-
parseInt(cleanHex[1] + cleanHex[1], 16),
448-
parseInt(cleanHex[2] + cleanHex[2], 16),
449-
];
450-
// Full hex format like 'ffffff'
451-
} else {
452-
return [
453-
parseInt(cleanHex.substr(0, 2), 16),
454-
parseInt(cleanHex.substr(2, 2), 16),
455-
parseInt(cleanHex.substr(4, 2), 16),
456-
];
457-
}
458-
};
459-
460456
// Parse both colors
461457
const [baseR, baseG, baseB] = parseHex(baseColor);
462458
const [mixR, mixG, mixB] = parseHex(mixColor);

0 commit comments

Comments
 (0)