Skip to content

Commit cbb9d09

Browse files
committed
task(settings): Support additional layout customization
Because: * We want to allow setting a gradient as button colour * We want to allow customization of headline text size and color This commit: * Update the CmsButtonWithFallback component to support setting a gradient as buttonColor * Update calculate-contrast to provide better contrast on light colour gradient * Update cta styling to prevent a border artifact when background is gradient * Support headlineFontSize and headlineTextColor attributes from cmsInfo Closes #FXA-12791
1 parent 810c57a commit cbb9d09

30 files changed

Lines changed: 1012 additions & 255 deletions

File tree

libs/shared/cms/src/__generated__/gql.ts

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/shared/cms/src/__generated__/graphql.ts

Lines changed: 14 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/shared/cms/src/lib/queries/relying-party/factories.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
import { faker } from '@faker-js/faker';
66

7-
import { RelyingPartiesQuery } from '../../../__generated__/graphql';
7+
import {
8+
RelyingPartiesQuery,
9+
Enum_Componentaccountsshared_Headlinefontsize,
10+
} from '../../../__generated__/graphql';
811
import { RelyingPartyResult } from '@fxa/shared/cms';
912

1013
export const RelyingPartyQueryFactory = (
@@ -83,6 +86,12 @@ export const RelyingPartyResultFactory = (
8386
syncHidePromoAfterLogin: faker.datatype.boolean(),
8487
},
8588
favicon: faker.internet.url(),
89+
headlineFontSize: faker.helpers.arrayElement([
90+
Enum_Componentaccountsshared_Headlinefontsize.Default,
91+
Enum_Componentaccountsshared_Headlinefontsize.Medium,
92+
Enum_Componentaccountsshared_Headlinefontsize.Large,
93+
]),
94+
headlineTextColor: faker.color.rgb(),
8695
},
8796
NewDeviceLoginEmail: {
8897
logoUrl: faker.internet.url(),

libs/shared/cms/src/lib/queries/relying-party/query.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ export const relyingPartyQuery = graphql(`
3535
splitLayoutAltText
3636
}
3737
favicon
38+
headlineFontSize
39+
headlineTextColor
3840
}
3941
EmailFirstPage {
4042
logoUrl

libs/shared/cms/src/lib/queries/relying-party/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
import { Enum_Componentaccountsshared_Headlinefontsize } from '../../../__generated__/graphql';
6+
57
export interface Page {
68
headline: string;
79
description: string | null;
@@ -38,6 +40,8 @@ export interface Shared {
3840
headerLogoAltText: string | null;
3941
featureFlags: FeatureFlags | null;
4042
favicon: string | null;
43+
headlineFontSize: Enum_Componentaccountsshared_Headlinefontsize | null;
44+
headlineTextColor: string | null;
4145
}
4246

4347
export interface FeatureFlags {

packages/fxa-react/lib/calculate-contrast.test.ts

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313

1414
describe('calculate-contrast', () => {
1515
describe('getTextColorClassName', () => {
16-
describe('default color (text-grey-400)', () => {
16+
describe('minimum contrast preference (default)', () => {
1717
it('should return text-grey-400 for light solid colors', () => {
1818
expect(getTextColorClassName('#ffffff')).toBe('text-grey-400');
1919
expect(getTextColorClassName('#FAFAFD')).toBe('text-grey-400');
@@ -24,6 +24,7 @@ describe('calculate-contrast', () => {
2424
'text-grey-400'
2525
);
2626
});
27+
2728
it('should return text-grey-400 for light gradients', () => {
2829
expect(
2930
getTextColorClassName(
@@ -37,6 +38,19 @@ describe('calculate-contrast', () => {
3738
).toBe('text-grey-400');
3839
});
3940

41+
it('should return text-white for dark solid colors', () => {
42+
expect(getTextColorClassName('#8B4000')).toBe('text-white');
43+
expect(getTextColorClassName('#000080')).toBe('text-white');
44+
expect(getTextColorClassName('#000000')).toBe('text-white');
45+
expect(getTextColorClassName('#240005')).toBe('text-white');
46+
expect(getTextColorClassName('#4B0082')).toBe('text-white');
47+
});
48+
49+
it('should return text-grey-400 for bright colors where grey-400 passes', () => {
50+
// Very light yellow #FFFF99: grey-400 has sufficient contrast (~6.7:1)
51+
expect(getTextColorClassName('#FFFF99')).toBe('text-grey-400');
52+
});
53+
4054
it('should handle semi-transparent colors with alpha blending', () => {
4155
expect(getTextColorClassName('rgba(0, 0, 0, 0.7)')).toBe('text-white');
4256
expect(getTextColorClassName('rgba(255, 255, 255, 0.1)')).toBe(
@@ -51,69 +65,61 @@ describe('calculate-contrast', () => {
5165
});
5266
});
5367

54-
describe('white text (text-white)', () => {
55-
it('should return text-white for dark solid colors that have poor contrast with grey', () => {
56-
expect(getTextColorClassName('#8B4000')).toBe('text-white');
57-
expect(getTextColorClassName('#000080')).toBe('text-white');
58-
expect(getTextColorClassName('#000000')).toBe('text-white');
59-
expect(getTextColorClassName('#240005')).toBe('text-white');
60-
expect(getTextColorClassName('#4B0082')).toBe('text-white');
61-
expect(getTextColorClassName('rgb(0, 0, 0)')).toBe('text-white');
62-
expect(getTextColorClassName('rgba(0, 0, 0, 1)')).toBe('text-white');
68+
describe('maximum contrast preference', () => {
69+
it('should return text-grey-900 for white background (maximum contrast)', () => {
70+
expect(getTextColorClassName('#ffffff', 'maximum')).toBe(
71+
'text-grey-900'
72+
);
73+
expect(getTextColorClassName('#FAFAFD', 'maximum')).toBe(
74+
'text-grey-900'
75+
);
6376
});
6477

65-
it('should return text-white when all 3 fail contrast', () => {
66-
expect(getTextColorClassName('#808000')).toBe('text-white');
78+
it('should return text-white for dark backgrounds (maximum contrast)', () => {
79+
expect(getTextColorClassName('#000000', 'maximum')).toBe('text-white');
80+
expect(getTextColorClassName('#0060DF', 'maximum')).toBe('text-white');
81+
expect(getTextColorClassName('#4B0082', 'maximum')).toBe('text-white');
6782
});
6883

69-
it('should return text-white for medium colors that have poor contrast with grey', () => {
70-
expect(getTextColorClassName('#7037d4')).toBe('text-white');
71-
// Using a test background color instead of MOCK_CMS_INFO
72-
expect(getTextColorClassName('#6D37D1')).toBe('text-white');
84+
it('should return text-grey-900 for very light backgrounds (maximum contrast)', () => {
85+
// For very light backgrounds, black should have the most contrast
86+
expect(getTextColorClassName('#F0F0F0', 'maximum')).toBe(
87+
'text-grey-900'
88+
);
7389
});
7490

75-
it('should return text-white for dark gradients', () => {
76-
expect(
77-
getTextColorClassName(
78-
'linear-gradient(135deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 100%)'
79-
)
80-
).toBe('text-white');
81-
expect(
82-
getTextColorClassName(
83-
'radial-gradient(circle at center, rgba(139, 64, 0, 1) 0%, rgba(101, 67, 33, 1) 100%)'
84-
)
85-
).toBe('text-white');
86-
expect(
87-
getTextColorClassName(
88-
'linear-gradient(135deg, rgba(75, 0, 130, 1) 0%, rgba(72, 61, 139, 1) 100%)'
89-
)
90-
).toBe('text-white');
91+
it('should return text-white for dark gradients (maximum contrast)', () => {
9192
expect(
9293
getTextColorClassName(
93-
'radial-gradient(circle, rgba(0, 0, 128, 1) 0%, rgba(25, 25, 112, 1) 100%)'
94+
'linear-gradient(135deg, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 1) 100%)',
95+
'maximum'
9496
)
9597
).toBe('text-white');
98+
});
99+
100+
it('should return text-grey-900 for light gradients (maximum contrast)', () => {
96101
expect(
97102
getTextColorClassName(
98-
'linear-gradient(135deg, rgba(128, 128, 0, 1) 0%, rgba(85, 107, 47, 1) 100%)'
103+
'radial-gradient(circle, rgba(255, 245, 235, 0.8) 0%, rgba(255, 250, 240, 0.6) 100%)',
104+
'maximum'
99105
)
100-
).toBe('text-white');
106+
).toBe('text-grey-900');
101107
});
102108
});
103109

104-
describe('dark grey text (text-grey-600)', () => {
105-
it('should return text-grey-600 when grey-400 and white fail but grey-600 passes', () => {
106-
expect(getTextColorClassName('#37d44c')).toBe('text-grey-600');
107-
expect(
108-
getTextColorClassName(
109-
'linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%)'
110-
)
111-
).toBe('text-grey-600');
112-
expect(
113-
getTextColorClassName(
114-
'radial-gradient(ellipse at top, rgba(250, 100, 210, 1) 0%, rgba(220, 200, 240, 1) 100%)'
115-
)
116-
).toBe('text-grey-600');
110+
describe('preference comparison', () => {
111+
it('minimum preference prefers grey-400 on white, maximum prefers black', () => {
112+
expect(getTextColorClassName('#ffffff', 'minimum')).toBe(
113+
'text-grey-400'
114+
);
115+
expect(getTextColorClassName('#ffffff', 'maximum')).toBe(
116+
'text-grey-900'
117+
);
118+
});
119+
120+
it('both preferences return white for dark backgrounds', () => {
121+
expect(getTextColorClassName('#000000', 'minimum')).toBe('text-white');
122+
expect(getTextColorClassName('#000000', 'maximum')).toBe('text-white');
117123
});
118124
});
119125
});
@@ -294,10 +300,15 @@ describe('calculate-contrast', () => {
294300
});
295301

296302
describe('edge cases', () => {
297-
it('should handle empty string', () => {
303+
it('should handle empty string with minimum preference', () => {
298304
expect(getTextColorClassName('')).toBe('text-grey-400');
299305
});
300306

307+
it('should handle empty string with maximum preference', () => {
308+
// Empty string is parsed as white, so maximum contrast should be black
309+
expect(getTextColorClassName('', 'maximum')).toBe('text-grey-900');
310+
});
311+
301312
it('should handle malformed gradient', () => {
302313
expect(getTextColorClassName('linear-gradient(malformed)')).toBe(
303314
'text-grey-400'

0 commit comments

Comments
 (0)