diff --git a/src/components/HelpMenu/HelpMenu.css b/src/components/HelpMenu/HelpMenu.css new file mode 100644 index 000000000..236ca4578 --- /dev/null +++ b/src/components/HelpMenu/HelpMenu.css @@ -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; +} diff --git a/src/components/HelpMenu/index.spec.tsx b/src/components/HelpMenu/index.spec.tsx index 1691ab68e..22d2d23b1 100644 --- a/src/components/HelpMenu/index.spec.tsx +++ b/src/components/HelpMenu/index.spec.tsx @@ -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; +type HelpMenuItemProps = React.ComponentProps; describe('HelpMenu', () => { let root: HTMLElement; @@ -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(); + return document.querySelector('.help-menu-button') as HTMLElement; + }; + + const renderMenuItem = (style: HelpMenuItemProps['style']) => { + render( + + Report an issue + + ); + 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( + 'caller-button'}> + 'caller-item'}>Report an issue + + ); + + const button = document.querySelector('.help-menu-button'); + expect(button?.className).toContain('navbar-button'); + expect(button?.className).toContain('caller-button'); + + render( + + 'caller-item'}>Report an issue + + ); + + 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(); + expect(document.querySelector('.help-menu-button')?.className).toContain('caller-button'); + + render( + + Report an issue + + ); + expect(document.querySelector('.help-menu-item')?.className).toContain('caller-item'); + }); +}); diff --git a/src/components/HelpMenu/index.tsx b/src/components/HelpMenu/index.tsx index c4ad9c382..972bb22c1 100644 --- a/src/components/HelpMenu/index.tsx +++ b/src/components/HelpMenu/index.tsx @@ -1,48 +1,39 @@ 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. + 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 +>(({ className, ...props }, ref) => ( + // style passes through in ...props — see the note on HelpMenuButton above. + classNames('help-menu-item', resolved))} + {...props} + /> +)); +HelpMenuItem.displayName = 'HelpMenuItem'; function PutAway({onClick, className}: {onClick: () => void; className?: string}) { return ( -
+
@@ -50,33 +41,6 @@ function PutAway({onClick, className}: {onClick: () => void; className?: string} ); } -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 @@ -162,10 +126,10 @@ export const HelpMenu: React.FC = ({ contactFormParams, chatConfi {showIframe && ( - -