Skip to content

Commit

Permalink
feat(header): L3-4289 support disabling top row of header (#476)
Browse files Browse the repository at this point in the history
  • Loading branch information
scottdickerson authored Jan 16, 2025
1 parent aecc2bd commit 8ce936b
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 21 deletions.
1 change: 1 addition & 0 deletions src/components/Dropdown/Dropdown.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const Playground = ({ value, ...props }: DropdownProps) => {
Playground.args = {
options: languages,
value: 'en',
disabled: false,
};

Playground.argTypes = {
Expand Down
20 changes: 20 additions & 0 deletions src/components/Dropdown/Dropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,24 @@ describe('Dropdown', () => {
await userEvent.click(option);
expect(onValueChangeMock).toHaveBeenCalled();
});
it('disabled dropdown should not pop selection', async () => {
render(
<Dropdown
options={languages}
value={SupportedLanguages.zh}
label="Select a language"
disabled
id="test"
onValueChange={vitest.fn()}
/>,
);

const trigger = screen.getByRole('combobox', {
name: 'Select a language',
});

expect(trigger).toBeDisabled();
await userEvent.click(trigger);
expect(screen.queryByRole('option', { name: '中文' })).not.toBeInTheDocument();
});
});
13 changes: 11 additions & 2 deletions src/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export interface DropdownProps
* Aria-label for specific dropdown use, e.g. Select your language
*/
label: string;
/**
* Is the dropdown disabled
*/
disabled?: boolean;
}

/**
Expand All @@ -36,7 +40,7 @@ export interface DropdownProps
* [Storybook Link](https://phillips-seldon.netlify.app/?path=/docs/components-dropdown--overview)
*/
const Dropdown = React.forwardRef<HTMLButtonElement, DropdownProps>(
({ options, value, onValueChange, label, className, id, ...props }, ref) => {
({ options, value, onValueChange, label, className, id, disabled, ...props }, ref) => {
const { className: baseClassName, ...commonProps } = getCommonProps({ id }, 'Dropdown');
const [isOpen, setIsOpen] = useState(false);

Expand All @@ -49,7 +53,12 @@ const Dropdown = React.forwardRef<HTMLButtonElement, DropdownProps>(
}}
onOpenChange={setIsOpen}
>
<DropdownSelect.Trigger className={`${baseClassName}__trigger`} aria-label={label} ref={ref}>
<DropdownSelect.Trigger
className={`${baseClassName}__trigger`}
aria-label={label}
ref={ref}
disabled={disabled}
>
<DropdownSelect.Value placeholder={value} />
<DropdownSelect.Icon>
{<ChevronDownIcon className={classnames({ [`${baseClassName}__trigger-icon-expanded`]: isOpen })} />}
Expand Down
14 changes: 12 additions & 2 deletions src/components/Dropdown/_dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
justify-content: center;
padding: $spacing-xsm;

&:disabled {
color: $medium-gray;
cursor: default;
pointer-events: none;

svg {
stroke: $light-gray;
}
}

svg {
height: 16px;
width: 16px;
Expand All @@ -21,11 +31,11 @@
transform: rotateX(180deg);
}

&:hover {
&:hover:not(:disabled) {
cursor: pointer;
}

&:focus-within {
&:focus-within:not(:disabled) {
box-shadow: 0 0 0 1px $light-gray;
outline: 1px solid $pure-black;
}
Expand Down
18 changes: 10 additions & 8 deletions src/patterns/LanguageSelector/LanguageSelector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ describe('LanguageSelector', () => {
runCommonTests(LanguageSelector, 'LanguageSelector');
});

const languageOptions: LanguageOption[] = [
{ label: 'English', value: SupportedLanguages.en },
{ label: '中文', value: SupportedLanguages.zh },
];

describe('LanguageSelector', () => {
test('renders language options correctly', async () => {
const languageOptions: LanguageOption[] = [
{ label: 'English', value: SupportedLanguages.en },
{ label: '中文', value: SupportedLanguages.zh },
];
const currentLanguage = SupportedLanguages.en;
const onLanguageChange = vitest.fn();

Expand All @@ -35,10 +36,6 @@ describe('LanguageSelector', () => {
expect(onLanguageChange).toHaveBeenCalledWith('zh');
});
test('switch from Chinese to english', async () => {
const languageOptions: LanguageOption[] = [
{ label: 'English', value: SupportedLanguages.en },
{ label: '中文', value: SupportedLanguages.zh },
];
const currentLanguage = SupportedLanguages.zh;
const onLanguageChange = vitest.fn();

Expand All @@ -58,6 +55,11 @@ describe('LanguageSelector', () => {

expect(onLanguageChange).toHaveBeenCalledWith('en');
});

test('disabled Language Selector', () => {
render(<LanguageSelector languageOptions={languageOptions} disabled />);
expect(screen.getByRole('combobox', { name: 'English' })).toBeDisabled();
});
});

test('getLanguageLabel', () => {
Expand Down
9 changes: 8 additions & 1 deletion src/patterns/LanguageSelector/LanguageSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ interface DropdownSelectorProps extends ComponentProps<'div'> {
onValueChange: (value: string) => void;
label: string;
options: { label: string; value: string }[];
// eslint-disable-next-line react/boolean-prop-naming
disabled?: boolean;
}

const MobileLanguageSelector = ({
Expand Down Expand Up @@ -69,6 +71,10 @@ export interface LanguageSelectorProps extends ComponentProps<'div'> {
* hide or show with an opacity transition
*/
isHidden?: boolean;
/**
* Disable the language selector so it can't be interacted with
*/
disabled?: boolean;
}
/**
* ## Overview
Expand All @@ -91,6 +97,7 @@ const LanguageSelector = forwardRef<HTMLElement, LanguageSelectorProps>(
onLanguageChange = noOp,
id,
isHidden,
disabled = false,
...props
},
ref,
Expand All @@ -114,7 +121,7 @@ const LanguageSelector = forwardRef<HTMLElement, LanguageSelectorProps>(
return (
<>
<SSRMediaQuery.Media greaterThanOrEqual="md">
<Dropdown {...selectorProps} ref={ref as ForwardedRef<HTMLButtonElement>} />
<Dropdown {...selectorProps} disabled={disabled} ref={ref as ForwardedRef<HTMLButtonElement>} />
</SSRMediaQuery.Media>
<SSRMediaQuery.Media lessThan="md">
<MobileLanguageSelector {...selectorProps} ref={ref as ForwardedRef<HTMLDivElement>} />
Expand Down
1 change: 1 addition & 0 deletions src/patterns/UserManagement/UserManagement.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const Playground: Story = {
authState: AuthState.LoggedOut,
loginLabel: 'Login',
accountLabel: 'Account',
disabled: false,
},
argTypes: {
authState: { control: { type: 'select' }, options: Object.values(AuthState) },
Expand Down
9 changes: 9 additions & 0 deletions src/patterns/UserManagement/UserManagement.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ describe('UserManagement', () => {
expect(loginLinkElement).toBeInTheDocument();
});

it('disabled if passed', async () => {
const onLoginMock = vitest.fn();
render(<UserManagement disabled onLogin={onLoginMock} />);
const loginLinkElement = screen.getByRole('button', { name: 'Login' });
expect(loginLinkElement).toBeDisabled();
await userEvent.click(loginLinkElement);
expect(onLoginMock).not.toHaveBeenCalled();
});

it('calls onLogin when login link is clicked', async () => {
const onLoginMock = vitest.fn();
render(<UserManagement onLogin={onLoginMock} />);
Expand Down
9 changes: 7 additions & 2 deletions src/patterns/UserManagement/UserManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export interface UserManagementProps extends ComponentProps<'div'> {
* The label for the account details link
*/
accountLabel?: ReactNode;
/**
* is the link disabled
*/
disabled?: boolean;
}

const UserManagement = forwardRef<HTMLDivElement, UserManagementProps>(
Expand All @@ -43,6 +47,7 @@ const UserManagement = forwardRef<HTMLDivElement, UserManagementProps>(
loginLabel = 'Login',
accountLabel = 'Account',
href = '/account',
disabled = false,
...props
},
ref,
Expand All @@ -57,12 +62,12 @@ const UserManagement = forwardRef<HTMLDivElement, UserManagementProps>(
{shouldShowAccountDetails && (
<>
{isLoggedIn ? (
<AccountDetailsComponent className={`${baseClassName}__login`} href={href}>
<AccountDetailsComponent className={`${baseClassName}__login`} href={href} disabled={disabled}>
<AccountCircle className={`${baseClassName}__account-icon`} />
<Text variant={TextVariants.body3}>{accountLabel}</Text>
</AccountDetailsComponent>
) : (
<button className={`${baseClassName}__login`} onClick={onLogin}>
<button className={`${baseClassName}__login`} onClick={onLogin} disabled={disabled}>
<AccountCircle className={`${baseClassName}__account-icon`} />
<Text variant={TextVariants.body3}>{loginLabel}</Text>
</button>
Expand Down
9 changes: 7 additions & 2 deletions src/patterns/UserManagement/_userManagement.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

.#{$px}-user-management {
align-items: center;
cursor: pointer;
display: flex;
gap: 0 $spacing-xsm;
justify-content: flex-end;
Expand Down Expand Up @@ -30,7 +29,13 @@
padding: 0;
padding-bottom: $padding-xsm;

&:hover {
&:disabled {
cursor: default;
opacity: 0.5;
pointer-events: none;
}

&:hover:not(:disabled) {
@include underline;
}

Expand Down
19 changes: 15 additions & 4 deletions src/site-furniture/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React, { PropsWithChildren } from 'react';
import classnames from 'classnames';
import { findChildrenExcludingTypes, findChildrenOfType, px } from '../../utils';
import Logo from '../../assets/PhillipsLogo.svg?react';
import UserManagement from '../../patterns/UserManagement/UserManagement';
import { LanguageSelector } from '../../patterns/LanguageSelector';
import UserManagement, { UserManagementProps } from '../../patterns/UserManagement/UserManagement';
import { LanguageSelector, LanguageSelectorProps } from '../../patterns/LanguageSelector';
import Navigation from '../../components/Navigation/Navigation';
import { Component, ComponentProps, forwardRef, ReactElement, useState, createContext } from 'react';
import { defaultHeaderContext } from './utils';
Expand Down Expand Up @@ -31,6 +31,10 @@ export interface HeaderProps extends ComponentProps<'header'> {
* label for the Logo link
*/
logoText?: string;
/**
* Is the header disabled
*/
disabled?: boolean;
}
export type HeaderContextType = {
/**
Expand Down Expand Up @@ -74,12 +78,19 @@ const Header = forwardRef<HTMLElement, HeaderProps>(
toggleOpenText = 'Open Menu',
toggleCloseText = 'Close Menu',
logoText = 'Home Page',
disabled,
...props
},
ref,
) => {
const userManagementElement = findChildrenOfType(children, UserManagement);
const languageSelectorElement = findChildrenOfType(children, LanguageSelector);
const userManagementChildren = findChildrenOfType(children, UserManagement);
const userManagementElement = React.isValidElement(userManagementChildren?.[0])
? React.cloneElement(userManagementChildren[0], { disabled } as UserManagementProps)
: '';
const languageSelectorChildren = findChildrenOfType(children, LanguageSelector);
const languageSelectorElement = React.isValidElement(languageSelectorChildren?.[0])
? React.cloneElement(languageSelectorChildren[0], { disabled } as LanguageSelectorProps)
: '';
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
const navigationElement = findChildrenOfType(children, Navigation);
const otherChildren = findChildrenExcludingTypes(children, [Navigation, UserManagement, LanguageSelector]);
Expand Down

0 comments on commit 8ce936b

Please sign in to comment.