Align Google/Microsoft SSO buttons with brand guidelines (#79) #80
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SsoButtons from './SsoButtons';
|
||||
import { startOAuth } from '../../auth/sso';
|
||||
|
||||
jest.mock('../../auth/sso', () => ({
|
||||
startOAuth: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('SsoButtons', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(startOAuth).mockReset();
|
||||
});
|
||||
|
||||
it('renders nothing when no providers enabled', () => {
|
||||
const { container } = render(
|
||||
<SsoButtons intent="login" providers={{ google: false, microsoft: false }} />
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('uses brand-compliant labels and logos for login', () => {
|
||||
render(<SsoButtons intent="login" providers={{ google: true, microsoft: true }} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Continue with Google' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Sign in with Microsoft' })).toBeInTheDocument();
|
||||
expect(document.querySelectorAll('svg').length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('uses Sign up with Google for signup intent', () => {
|
||||
render(<SsoButtons intent="signup" providers={{ google: true }} />);
|
||||
expect(screen.getByRole('button', { name: 'Sign up with Google' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('starts OAuth for the selected provider', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<SsoButtons intent="login" providers={{ google: true, microsoft: true }} />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Continue with Google' }));
|
||||
expect(startOAuth).toHaveBeenCalledWith('google', 'login');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Sign in with Microsoft' }));
|
||||
expect(startOAuth).toHaveBeenCalledWith('microsoft', 'login');
|
||||
});
|
||||
});
|
||||
@@ -20,21 +20,32 @@ const Divider = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const SsoButton = styled.button`
|
||||
const ButtonStack = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
padding: 0.85rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
gap: 0.6rem;
|
||||
margin-top: 0.6rem;
|
||||
`;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
/** Shared layout for IdP buttons — equal size / visual weight. */
|
||||
const SsoButtonBase = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
min-height: 2.75rem;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.65rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
font-family: 'Roboto', 'Google Sans', system-ui, sans-serif;
|
||||
letter-spacing: 0.01em;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -45,6 +56,93 @@ const SsoButton = styled.button`
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Google dark-theme button per
|
||||
* https://developers.google.com/identity/branding-guidelines
|
||||
* Fill #131314, stroke #8E918F, text #E3E3E3; multicolor G on white.
|
||||
*/
|
||||
const GoogleButton = styled(SsoButtonBase)`
|
||||
background: #131314;
|
||||
border: 1px solid #8e918f;
|
||||
color: #e3e3e3;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #1e1f20;
|
||||
border-color: #a8aba9;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Microsoft dark-theme button — logo + "Sign in with Microsoft"
|
||||
* https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps
|
||||
*/
|
||||
const MicrosoftButton = styled(SsoButtonBase)`
|
||||
background: #2f2f2f;
|
||||
border: 1px solid transparent;
|
||||
color: #ffffff;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #3b3b3b;
|
||||
}
|
||||
`;
|
||||
|
||||
const GoogleLogoBadge = styled.span`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
background: #ffffff;
|
||||
border-radius: 0.125rem;
|
||||
`;
|
||||
|
||||
const LogoMark = styled.span`
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
/** Official multicolor Google "G" (standard color Super G). */
|
||||
const GoogleGIcon = (): JSX.Element => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" aria-hidden="true" focusable="false">
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"
|
||||
/>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"
|
||||
/>
|
||||
<path fill="none" d="M0 0h48v48H0z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/** Official Microsoft four-square logo — do not recolor. */
|
||||
const MicrosoftLogoIcon = (): JSX.Element => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21" aria-hidden="true" focusable="false">
|
||||
<rect x="1" y="1" width="9" height="9" fill="#f25022" />
|
||||
<rect x="11" y="1" width="9" height="9" fill="#7fba00" />
|
||||
<rect x="1" y="11" width="9" height="9" fill="#00a4ef" />
|
||||
<rect x="11" y="11" width="9" height="9" fill="#ffb900" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export type OAuthProviderFlags = {
|
||||
google?: boolean;
|
||||
microsoft?: boolean;
|
||||
@@ -56,6 +154,9 @@ type SsoButtonsProps = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const googleLabel = (intent: 'login' | 'signup'): string =>
|
||||
intent === 'signup' ? 'Sign up with Google' : 'Continue with Google';
|
||||
|
||||
const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): JSX.Element | null => {
|
||||
const google = Boolean(providers.google);
|
||||
const microsoft = Boolean(providers.microsoft);
|
||||
@@ -66,24 +167,34 @@ const SsoButtons = ({ intent, providers, disabled = false }: SsoButtonsProps): J
|
||||
return (
|
||||
<>
|
||||
<Divider>or continue with</Divider>
|
||||
{google && (
|
||||
<SsoButton
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => startOAuth('google', intent)}
|
||||
>
|
||||
Continue with Google
|
||||
</SsoButton>
|
||||
)}
|
||||
{microsoft && (
|
||||
<SsoButton
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => startOAuth('microsoft', intent)}
|
||||
>
|
||||
Continue with Microsoft
|
||||
</SsoButton>
|
||||
)}
|
||||
<ButtonStack>
|
||||
{google && (
|
||||
<GoogleButton
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => startOAuth('google', intent)}
|
||||
>
|
||||
<GoogleLogoBadge>
|
||||
<LogoMark>
|
||||
<GoogleGIcon />
|
||||
</LogoMark>
|
||||
</GoogleLogoBadge>
|
||||
{googleLabel(intent)}
|
||||
</GoogleButton>
|
||||
)}
|
||||
{microsoft && (
|
||||
<MicrosoftButton
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => startOAuth('microsoft', intent)}
|
||||
>
|
||||
<LogoMark>
|
||||
<MicrosoftLogoIcon />
|
||||
</LogoMark>
|
||||
Sign in with Microsoft
|
||||
</MicrosoftButton>
|
||||
)}
|
||||
</ButtonStack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('SignIn', () => {
|
||||
renderSignIn();
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Continue with Google' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Continue with Microsoft' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Sign in with Microsoft' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides SSO buttons when oauth not configured', async () => {
|
||||
@@ -99,6 +99,7 @@ describe('SignIn', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: 'Continue with Google' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Sign in with Microsoft' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user