Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Refreshing GlobalHeader Storybook Example #2891

Merged
Merged
Show file tree
Hide file tree
Changes from 16 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
30 changes: 27 additions & 3 deletions modules/react/_examples/stories/GlobalHeader.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,36 @@ import {Basic} from './examples/GlobalHeader';

<Meta title="Examples/GlobalHeader" />

# Canvas Kit Examples

## GlobalHeader
# GlobalHeader Example

Developers building internal Workday applications will likely not need to create this component.
However, if you're building components to be used outside of Workday, this is a helpful reference
for building a global navigation header that looks like our internal `GlobalHeader`.

<ExampleCodeBlock code={Basic} />

## Tooltip usage

- The `default` variant Tooltip is used on all of the icon buttons, which will automatically set the
Tooltip's text to the accessible name. (`aria-label`)
- The `describe` variant Tooltip is used instead on the "MENU" button because this is a text button.
The Tooltip's text "Global Navigation" will instead be assigned to the accessible description to
ensure that the visible button text "MENU" is not overriden.

## Count badge usage

When `<CountBadge>` is used as a sibling component for button, the `aria-describedby` property is
set on the button referencing the `id` value of the `<CountBadge>`. This practice helps support
users depending on screen readers to describe both the name of the button and the value of the
`<CountBadge>`.

When a web app dynamically updates count badges in real-time, consider the following accessibility
enhancements to support live, real-time announcements for screen readers:

- The `<CountBadge>` component is rendered as a child of the `<AriaLiveRegion>` container.
- The `<AriaLiveRegion>` container is assigned a name by using `aria-labelledby` to reference the
name of the icon button `"Notifications"`.
- The `<AccessibleHide>` component is used following the `<CountBadge>` to render a hidden word
"new" that only screen reader users can access.
- When the `<CountBadge>` is updated, then screen readers can automatically describe (in real-time)
the name of the live region, "Notifications" and the text updated inside of it, "1 new".
261 changes: 224 additions & 37 deletions modules/react/_examples/stories/examples/GlobalHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,252 @@
import * as React from 'react';
import {styled, createComponent, dubLogoBlue} from '@workday/canvas-kit-react/common';
import {colors, depth, space, type} from '@workday/canvas-kit-react/tokens';

import {
AccessibleHide,
AriaLiveRegion,
composeHooks,
createComponent,
createElemPropsHook,
createSubcomponent,
ExtractProps,
useUniqueId,
} from '@workday/canvas-kit-react/common';
import {base, system} from '@workday/canvas-tokens-web';
import {calc, createStyles, px2rem} from '@workday/canvas-kit-styling';
import {
notificationsIcon,
inboxIcon,
justifyIcon,
assistantIcon,
searchIcon,
} from '@workday/canvas-system-icons-web';

import {TertiaryButton, Hyperlink} from '@workday/canvas-kit-react/button';
import {SecondaryButton, TertiaryButton} from '@workday/canvas-kit-react/button';
import {Avatar} from '@workday/canvas-kit-react/avatar';
import {Flex, FlexProps} from '@workday/canvas-kit-react/layout';
import {SearchForm} from '@workday/canvas-kit-labs-react/search-form';
import {Tooltip} from '@workday/canvas-kit-react/tooltip';
import {Combobox, useComboboxModel, useComboboxInput} from '@workday/canvas-kit-react/combobox';
import {InputGroup, TextInput} from '@workday/canvas-kit-react/text-input';
import {StyledMenuItem} from '@workday/canvas-kit-react/menu';
import {SystemIcon} from '@workday/canvas-kit-react/icon';
import {CountBadge} from '@workday/canvas-kit-react/badge';

interface HeaderItemProps extends FlexProps {}
interface LiveCountBadgeProps extends FlexProps {
cnt: number;
}

const tasks = ['Request Time Off', 'Create Expense Report', 'Change Benefits'];

export const Basic = () => (
<GlobalHeader>
<GlobalHeader.Item>
<TertiaryButton aria-label="menu" icon={justifyIcon} />
<Hyperlink>
<WorkdayLogo dangerouslySetInnerHTML={{__html: dubLogoBlue}} />
</Hyperlink>
</GlobalHeader.Item>
<GlobalHeader.Item margin="auto" width="100%" maxWidth={`calc(${space.xxxl} * 6)`}>
<SearchForm onSubmit={() => 1} />
</GlobalHeader.Item>
<GlobalHeader.Item>
<TertiaryButton aria-label="messages" icon={assistantIcon} />
<TertiaryButton aria-label="notifications" icon={notificationsIcon} />
<TertiaryButton aria-label="inbox" icon={inboxIcon} />
<Avatar size={Avatar.Size.m} variant={Avatar.Variant.Light} />
</GlobalHeader.Item>
</GlobalHeader>
const styleOverrides = {
headerWrapper: createStyles({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxSizing: 'border-box',
...system.type.subtext.large,
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
backgroundColor: system.color.bg.default,
padding: system.space.x1,
}),
inputGroupInner: createStyles({
marginLeft: '1rem',
width: px2rem(20),
transition: 'opacity 100ms ease',
}),
comboboxContainer: createStyles({
margin: 'auto',
width: '100%',
maxWidth: calc.multiply(system.space.x20, 6),
}),
comboboxInput: createStyles({
borderRadius: px2rem(1000),
width: '20rem',
}),
comboboxMenuList: createStyles({
maxHeight: px2rem(200),
}),
menuButtonStyles: createStyles({
textDecoration: 'none',
color: base.blackPepper500,
}),
notificationContainerStyles: createStyles({
boxSizing: 'border-box',
position: 'relative',
}),
countBadgeStyles: createStyles({
boxSizing: 'border-box',
position: 'absolute',
top: calc.negate(system.space.x1),
insetInlineEnd: calc.negate(system.space.x1),
}),
actionButtonStyles: createStyles({
gap: system.space.x4,
margin: system.space.x4,
}),
};

const useAutocompleteInput = composeHooks(
createElemPropsHook(useComboboxModel)(model => {
return {
onKeyPress(event: React.KeyboardEvent) {
model.events.show(event);
},
};
}),
useComboboxInput
);

const AutoCompleteInput = createSubcomponent(TextInput)({
modelHook: useComboboxModel,
elemPropsHook: useAutocompleteInput,
})<ExtractProps<typeof Combobox.Input, never>>((elemProps, Element) => {
return <Combobox.Input as={Element} {...elemProps} />;
});

export const Basic = () => {
const [notifications, setNotifications] = React.useState(0);

function handleAdd() {
setNotifications(prev => prev + 1);
}

function handleClear() {
setNotifications(0);
}

return (
<>
<GlobalHeader>
<GlobalHeader.Item>
<Tooltip title="Global Navigation" type="describe">
<TertiaryButton icon={justifyIcon} cs={styleOverrides.menuButtonStyles}>
MENU
</TertiaryButton>
</Tooltip>
<Tooltip title="Workday Home">
<TertiaryButton>
<img src="https://design.workday.com/images/ck-dub-logo-blue.svg" alt="" />
</TertiaryButton>
</Tooltip>
</GlobalHeader.Item>
<GlobalHeader.Item cs={styleOverrides.comboboxContainer}>
<Autocomplete aria-label="Search Workday" />
</GlobalHeader.Item>
<GlobalHeader.Item>
<Tooltip title="Assistant">
<TertiaryButton icon={assistantIcon} />
</Tooltip>

<NotificationLiveBadge cnt={notifications} />

<Tooltip title="My Tasks">
<TertiaryButton icon={inboxIcon} />
</Tooltip>
<Tooltip title="Profile">
<Avatar />
</Tooltip>
</GlobalHeader.Item>
</GlobalHeader>
<Flex cs={styleOverrides.actionButtonStyles}>
<SecondaryButton onClick={handleAdd}>Add notification</SecondaryButton>
<TertiaryButton onClick={handleClear}>Clear</TertiaryButton>
</Flex>
</>
);
};

const GlobalHeaderItem = createComponent('div')({
displayName: 'GlobalHeader.Item',
Component: ({gap = 's', ...props}: HeaderItemProps, ref) => (
<Flex gap={gap} alignItems="center" marginX={space.xs} ref={ref} {...props} />
<Flex gap={gap} alignItems="center" marginX={system.space.x3} ref={ref} {...props} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does passing a token here work?

),
});

const GlobalHeader = createComponent('header')({
displayName: 'GlobalHeader',
Component: (props, ref, Element) => <HeaderWrapper ref={ref} as={Element} {...props} />,
Component: (props, ref) => (
<header className={styleOverrides.headerWrapper} ref={ref} {...props} />
),
subComponents: {Item: GlobalHeaderItem},
});

const HeaderWrapper = styled('header')({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxSizing: 'border-box',
...type.levels.subtext.large,
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
backgroundColor: colors.frenchVanilla100,
...depth[1],
padding: space.xxs,
const Autocomplete = createComponent('div')({
displayName: 'Autocomplete',
Component: props => {
const [searchText, setSearchText] = React.useState('');
const filteredTasks = tasks.filter(i => {
if (searchText.trim() === '' || typeof searchText !== 'string') {
return true;
}
return i.toLowerCase().includes(searchText.trim().toLowerCase());
});

function handleChange(e) {
setSearchText(e.target.value);
}

return (
<Combobox>
<InputGroup>
<InputGroup.InnerStart cs={styleOverrides.inputGroupInner}>
<SystemIcon icon={searchIcon} />
</InputGroup.InnerStart>
<InputGroup.Input
as={AutoCompleteInput}
cs={styleOverrides.comboboxInput}
onChange={handleChange}
value={searchText}
{...props}
/>
</InputGroup>
<Combobox.Menu.Popper>
<Combobox.Menu.Card>
{filteredTasks.length === 0 ? (
<StyledMenuItem as="span">No Results Found</StyledMenuItem>
) : (
filteredTasks.map(i => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although mapping works, we should probably use a render function like in the auto complete example:

{model.state.items.length > 0 && (
                <Combobox.Menu.List cs={styleOverrides.comboboxMenuList}>
                  {item => <Combobox.Menu.Item>{item}</Combobox.Menu.Item>}
                </Combobox.Menu.List>
              )}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll need some help with using this model!

<Combobox.Menu.List cs={styleOverrides.comboboxMenuList}>
<Combobox.Menu.Item key={i}>{i}</Combobox.Menu.Item>
</Combobox.Menu.List>
))
)}
</Combobox.Menu.Card>
</Combobox.Menu.Popper>
</Combobox>
);
},
});

const WorkdayLogo = styled('span')({lineHeight: 0});
const NotificationLiveBadge = createComponent('span')({
displayName: 'NotificationLiveBadge',
Component: ({cnt = 0, ...props}: LiveCountBadgeProps) => {
const btnId = useUniqueId();
const badgeId = useUniqueId();

return (
<Flex cs={styleOverrides.notificationContainerStyles}>
<Tooltip title="Notifications">
<TertiaryButton
id={btnId}
icon={notificationsIcon}
aria-describedby={cnt > 0 ? badgeId : undefined}
{...props}
/>
</Tooltip>
<AriaLiveRegion aria-labelledby={btnId}>
{cnt > 0 && (
<>
<CountBadge
id={badgeId}
count={cnt}
limit={100}
cs={styleOverrides.countBadgeStyles}
/>
<AccessibleHide>New</AccessibleHide>
</>
)}
</AriaLiveRegion>
</Flex>
);
},
});
Loading