Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/components/HelpMenu/HelpMenu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* HelpMenu trigger button */
.help-menu-button {
color: var(--help-menu-button-color, var(--ox-color-gray));
font-size: 1.4rem;
}

/* HelpMenu menu item */
.help-menu-item {
color: var(--help-menu-item-color, var(--ox-color-neutral-darker));
text-decoration: none;
}

/* The compound selector keeps the override of .navbar-menu-item:focus-visible
independent of stylesheet order */
.navbar-menu-item.help-menu-item:focus-visible {
outline: 0;
background: var(--help-menu-item-focus-bg, var(--ox-color-neutral-lighter));
}

.help-menu-item:hover {
color: var(--help-menu-item-color, var(--ox-color-neutral-darker));
text-decoration: none;
}

/* Contact form iframe, portaled to the body */
.help-menu-iframe-wrapper {
background-color: var(--help-menu-iframe-wrapper-bg, var(--ox-color-neutral-bright));
position: absolute;
width: 100%;
top: 4rem;
left: 0;
bottom: 0;
z-index: 20;
}

.help-menu-iframe {
border: 0;
width: 100%;
height: calc(100% - 5rem);
}

/* Bar with the "Back" button that puts the contact form away */
.help-menu-put-away {
border-top: 0.1rem solid var(--help-menu-put-away-border-color, var(--ox-color-pale));
width: 100%;
height: 5.6rem;
display: flex;
align-items: center;
background-color: var(--help-menu-put-away-bg, var(--ox-color-neutral-bright));
padding-left: 1.5rem;
position: fixed;
bottom: 0;
left: 0;
z-index: 20;
}

@media (min-width: 56em) {
.help-menu-put-away {
padding: 0 calc(50vw - 43rem);
}
}

.help-menu-put-away button {
height: 3rem;
background-color: var(--help-menu-put-away-button-bg, var(--ox-color-white));
border: 1px solid var(--help-menu-put-away-button-border-color, var(--ox-color-pale));
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
width: 9rem;
border-radius: 0.5rem;
}
119 changes: 118 additions & 1 deletion src/components/HelpMenu/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { Menu } from 'react-aria-components';
import { BodyPortalSlotsContext } from '../BodyPortalSlotsContext';
import { HelpMenu, HelpMenuItem, HelpMenuProps, NewTabIcon } from '.';
import { HelpMenu, HelpMenuButton, HelpMenuItem, HelpMenuProps, NewTabIcon } from '.';
import { NavBar } from '../NavBar';
import { ChatConfiguration } from './hooks';
import type { CSSPropertiesWithVariables } from '../../types';

type HelpMenuButtonProps = React.ComponentProps<typeof HelpMenuButton>;
type HelpMenuItemProps = React.ComponentProps<typeof HelpMenuItem>;

describe('HelpMenu', () => {
let root: HTMLElement;
Expand Down Expand Up @@ -327,3 +332,115 @@ describe('HelpMenu', () => {
await screen.findByRole('menuitem', { name: /report an issue/i });
});
});

describe('HelpMenu style passthrough', () => {
beforeAll(() => {
global.CSS = {
supports: () => true,
escape: jest.fn(),
} as any;
});

// The components no longer set --help-menu-* inline; those are defaults in HelpMenu.css,
// guarded by src/theme/tokens.spec.ts. See the note in ProfileMenu's spec.
const renderButton = (style: HelpMenuButtonProps['style']) => {
render(<HelpMenuButton label='Help' style={style} />);
return document.querySelector('.help-menu-button') as HTMLElement;
};

const renderMenuItem = (style: HelpMenuItemProps['style']) => {
render(
<Menu aria-label='Test menu'>
<HelpMenuItem style={style}>Report an issue</HelpMenuItem>
</Menu>
);
return document.querySelector('.help-menu-item') as HTMLElement;
};

describe('HelpMenuButton', () => {
it('passes a render-callback style through', () => {
const button = renderButton(() => ({ color: 'rgb(255, 0, 0)' }));

expect(button.style.color).toBe('rgb(255, 0, 0)');
});

it('passes an object style through', () => {
const button = renderButton({ color: 'rgb(0, 0, 255)' });

expect(button.style.color).toBe('rgb(0, 0, 255)');
});

it('lets the caller override the CSS variables', () => {
const button = renderButton({
'--help-menu-button-color': 'rebeccapurple'
} as CSSPropertiesWithVariables);

expect(button.style.getPropertyValue('--help-menu-button-color')).toBe('rebeccapurple');
});
});

describe('HelpMenuItem', () => {
it('passes a render-callback style through', () => {
const item = renderMenuItem(() => ({ color: 'rgb(255, 0, 0)' }));

expect(item.style.color).toBe('rgb(255, 0, 0)');
});

it('passes an object style through', () => {
const item = renderMenuItem({ color: 'rgb(0, 0, 255)' });

expect(item.style.color).toBe('rgb(0, 0, 255)');
});

it('lets the caller override the CSS variables', () => {
const item = renderMenuItem({
'--help-menu-item-focus-bg': 'rebeccapurple'
} as CSSPropertiesWithVariables);

expect(item.style.getPropertyValue('--help-menu-item-focus-bg')).toBe('rebeccapurple');
});
});
});

describe('HelpMenu className composition', () => {
beforeAll(() => {
global.CSS = {
supports: () => true,
escape: jest.fn(),
} as any;
});

it('composes a render-callback className on each wrapper', () => {
render(
<HelpMenuButton label='Help' className={() => 'caller-button'}>
<HelpMenuItem className={() => 'caller-item'}>Report an issue</HelpMenuItem>
</HelpMenuButton>
);

const button = document.querySelector('.help-menu-button');
expect(button?.className).toContain('navbar-button');
expect(button?.className).toContain('caller-button');

render(
<Menu aria-label='Test menu'>
<HelpMenuItem className={() => 'caller-item'}>Report an issue</HelpMenuItem>
</Menu>
);

const item = document.querySelector('.help-menu-item');
expect(item?.className).toContain('navbar-menu-item');
expect(item?.className).toContain('caller-item');
});

it('keeps composing a string className', () => {
render(<HelpMenuButton label='Help' className='caller-button' />);
expect(document.querySelector('.help-menu-button')?.className).toContain('caller-button');

render(
<Menu aria-label='Test menu'>
<HelpMenuItem className='caller-item'>Report an issue</HelpMenuItem>
</Menu>
);
expect(document.querySelector('.help-menu-item')?.className).toContain('caller-item');
});
});
100 changes: 32 additions & 68 deletions src/components/HelpMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,46 @@
import React from 'react';
import { NavBarMenuButton, NavBarMenuItem } from '../NavBarMenuButtons';
import classNames from 'classnames';
import { composeRenderProps } from 'react-aria-components';
import { NavBarBaseButtonProps, NavBarMenuButton, NavBarMenuItem } from '../NavBarMenuButtons';
import { colors } from '../../theme';
import styled from 'styled-components';
import { BodyPortal } from '../BodyPortal';
import { ChatConfiguration, getPreChatFields, useChatController, useHoursRange } from './hooks';
import './HelpMenu.css';
import '../../theme/theme.css';

export const HelpMenuButton = ({ className, ...props }: NavBarBaseButtonProps) => (
// style is deliberately not destructured: with the theme defaults moved into
// HelpMenu.css there is nothing left to merge it with, so it passes straight through in
// ...props and react-aria handles both the object and render-callback forms.
<NavBarMenuButton
className={composeRenderProps(className, (resolved) => classNames('help-menu-button', resolved))}
{...props}
/>
);

export const HelpMenuButton = styled(NavBarMenuButton)`
color: ${colors.palette.gray};
font-size: 1.4rem;
`;

export const HelpMenuItem = styled(NavBarMenuItem)`
color: ${colors.palette.neutralDarker};
text-decoration: none;

:focus-visible {
outline: 0;
background: ${colors.palette.neutralLighter};
}
:hover {
color: ${colors.palette.neutralDarker};
text-decoration: none;
}
`;

const IframeWrapper = styled(BodyPortal)`
background-color: ${colors.palette.neutralBright};
position: absolute;
width: 100%;
top: 4rem;
left: 0;
bottom: 0;
z-index: 20;
`;

const Iframe = styled.iframe`
border: 0;
width: 100%;
height: calc(100% - 5rem);
`;
export const HelpMenuItem = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof NavBarMenuItem>
>(({ className, ...props }, ref) => (
// style passes through in ...props — see the note on HelpMenuButton above.
<NavBarMenuItem
ref={ref}
className={composeRenderProps(className, (resolved) => classNames('help-menu-item', resolved))}
{...props}
/>
));
HelpMenuItem.displayName = 'HelpMenuItem';

function PutAway({onClick, className}: {onClick: () => void; className?: string}) {
return (
<div className={className}>
<div className={classNames('help-menu-put-away', className)}>
<button type='button' onClick={onClick} aria-label='close form'>
Back
</button>
</div>
);
}

const StyledPutAway = styled(PutAway)`
border-top: 0.1rem solid ${colors.palette.pale};
width: 100%;
height: 5.6rem;
display: flex;
align-items: center;
background-color: ${colors.palette.neutralBright};
padding-left: 1.5rem;
position: fixed;
bottom: 0;
left: 0;
z-index: 20;

@media(min-width: 56em) {
padding: 0 calc(50vw - 43rem);
}

button {
height: 3rem;
background-color: ${colors.palette.white};
border: 1px solid ${colors.palette.pale};
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
width: 9rem;
border-radius: 0.5rem;
}
`;

/**
* SVG icon representing a "new tab" indicator
* Used to visually indicate when a link or action will open in a new tab/window
Expand Down Expand Up @@ -162,10 +126,10 @@ export const HelpMenu: React.FC<HelpMenuProps> = ({ contactFormParams, chatConfi
</HelpMenuButton>

{showIframe && (
<IframeWrapper>
<Iframe title='Contact form' src={showIframe} />
<StyledPutAway onClick={() => setShowIframe(undefined)} />
</IframeWrapper>
<BodyPortal className='help-menu-iframe-wrapper'>
<iframe className='help-menu-iframe' title='Contact form' src={showIframe} />
<PutAway onClick={() => setShowIframe(undefined)} />
</BodyPortal>
)}
</>
);
Expand Down
44 changes: 44 additions & 0 deletions src/components/ProfileMenu/ProfileMenu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* ProfileMenu trigger button */
.profile-menu-button {
display: flex;
justify-content: center;
align-items: center;
width: 3.5rem;
height: 3.5rem;
border-radius: 50%;
border: none;
line-height: 1.6rem;
font-size: 1.4rem;
color: var(--profile-menu-button-color, var(--ox-color-white));
background-color: var(--profile-menu-button-bg, var(--ox-color-dark-teal));
cursor: pointer;
}

.profile-menu-button:hover {
box-shadow: 0 0 0.2rem 0.2rem rgba(0, 0, 0, 0.3);
}

.profile-menu-button[data-focused],
.profile-menu-button[data-focus-visible] {
/* defaultFocusOutline from src/theme.ts */
outline: 0.2rem auto Highlight;
outline: 0.2rem auto -webkit-focus-ring-color;
outline-offset: 0.2rem;
}

.profile-menu-button svg {
width: 1.5rem;
}

/* ProfileMenu popover; the compound selector keeps the margin-top override of
.navbar-popover independent of stylesheet order */
.navbar-popover.profile-menu-popover {
min-width: 10rem;
margin-top: 0.4rem;
}

/* ProfileMenu menu item */
.profile-menu-item {
color: var(--profile-menu-item-color, var(--ox-color-neutral-darker));
text-decoration: none;
}
Loading
Loading