diff --git a/modules/react/_examples/stories/GlobalHeader.stories.mdx b/modules/react/_examples/stories/GlobalHeader.stories.mdx index e1b063daa6..1fc9f0a840 100644 --- a/modules/react/_examples/stories/GlobalHeader.stories.mdx +++ b/modules/react/_examples/stories/GlobalHeader.stories.mdx @@ -2,12 +2,36 @@ import {Basic} from './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`. + +## 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 `` is used as a sibling component for button, the `aria-describedby` property is +set on the button referencing the `id` value of the ``. This practice helps support +users depending on screen readers to describe both the name of the button and the value of the +``. + +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 `` component is rendered as a child of the `` container. +- The `` container is assigned a name by using `aria-labelledby` to reference the + name of the icon button `"Notifications"`. +- The `` component is used following the `` to render a hidden word + "new" that only screen reader users can access. +- When the `` 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". diff --git a/modules/react/_examples/stories/examples/GlobalHeader.tsx b/modules/react/_examples/stories/examples/GlobalHeader.tsx index 2519a65018..3d5bfa8e9d 100644 --- a/modules/react/_examples/stories/examples/GlobalHeader.tsx +++ b/modules/react/_examples/stories/examples/GlobalHeader.tsx @@ -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 = () => ( - - - - - - - - - 1} /> - - - - - - - - +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, +})>((elemProps, Element) => { + return ; +}); + +export const Basic = () => { + const [notifications, setNotifications] = React.useState(0); + + function handleAdd() { + setNotifications(prev => prev + 1); + } + + function handleClear() { + setNotifications(0); + } + + return ( + <> + + + + + MENU + + + + + + + + + + + + + + + + + + + + + + + + + + + + Add notification + Clear + + + ); +}; + const GlobalHeaderItem = createComponent('div')({ displayName: 'GlobalHeader.Item', Component: ({gap = 's', ...props}: HeaderItemProps, ref) => ( - + ), }); const GlobalHeader = createComponent('header')({ displayName: 'GlobalHeader', - Component: (props, ref, Element) => , + Component: (props, ref) => ( +
+ ), 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 ( + + + + + + + + + + {filteredTasks.length === 0 ? ( + No Results Found + ) : ( + filteredTasks.map(i => ( + + {i} + + )) + )} + + + + ); + }, }); -const WorkdayLogo = styled('span')({lineHeight: 0}); +const NotificationLiveBadge = createComponent('span')({ + displayName: 'NotificationLiveBadge', + Component: ({cnt = 0, ...props}: LiveCountBadgeProps) => { + const btnId = useUniqueId(); + const badgeId = useUniqueId(); + + return ( + + + 0 ? badgeId : undefined} + {...props} + /> + + + {cnt > 0 && ( + <> + + New + + )} + + + ); + }, +});