diff --git a/modules/react/_examples/stories/GlobalHeader.stories.mdx b/modules/react/_examples/stories/GlobalHeader.stories.mdx
new file mode 100644
index 0000000000..1fc9f0a840
--- /dev/null
+++ b/modules/react/_examples/stories/GlobalHeader.stories.mdx
@@ -0,0 +1,37 @@
+import {Basic} from './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/mdx/Headers.mdx b/modules/react/_examples/stories/mdx/Headers.mdx
index dd416ce054..5939859d6e 100644
--- a/modules/react/_examples/stories/mdx/Headers.mdx
+++ b/modules/react/_examples/stories/mdx/Headers.mdx
@@ -13,6 +13,32 @@ Developers building internal Workday applications will likely not need to create
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".
+
## Page Header
diff --git a/modules/react/_examples/stories/mdx/examples/GlobalHeader.tsx b/modules/react/_examples/stories/mdx/examples/GlobalHeader.tsx
index 4190644e66..671200fa88 100644
--- a/modules/react/_examples/stories/mdx/examples/GlobalHeader.tsx
+++ b/modules/react/_examples/stories/mdx/examples/GlobalHeader.tsx
@@ -1,65 +1,295 @@
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 {LoadReturn} from '@workday/canvas-kit-react/collection';
+import {Tooltip} from '@workday/canvas-kit-react/tooltip';
+import {
+ Combobox,
+ useComboboxModel,
+ useComboboxInput,
+ useComboboxLoader,
+} 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;
+}
-export const Basic = () => (
-
-
-
-
-
-
-
-
- 1} />
-
-
-
-
-
-
-
-
+const tasks = ['Request Time Off', 'Create Expense Report', 'Change Benefits'];
+
+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('');
+
+ function handleChange(e) {
+ setSearchText(e.target.value);
+ }
+
+ const {model, loader} = useComboboxLoader(
+ {
+ // You can start with any number that makes sense.
+ total: 0,
+
+ // Pick whatever number makes sense for your API
+ pageSize: 20,
+
+ // A load function that will be called by the loader. You must return a promise that returns
+ // an object like `{items: [], total: 0}`. The `items` will be merged into the loader's cache
+ async load({pageNumber, pageSize, filter}) {
+ return new Promise>(resolve => {
+ // simulate a server response by resolving after a period of time
+ setTimeout(() => {
+ // simulate paging and filtering based on pre-computed items
+ const start = (pageNumber - 1) * pageSize;
+ const end = start + pageSize;
+ const filteredTasks = tasks.filter(i => {
+ if (searchText.trim() === '' || typeof searchText !== 'string') {
+ return true;
+ }
+ return i.toLowerCase().includes(searchText.trim().toLowerCase());
+ });
+
+ const total = filteredTasks.length;
+ const items = filteredTasks.slice(start, end);
+
+ resolve({
+ items,
+ total,
+ });
+ }, 300);
+ });
+ },
+ onShow() {
+ // The `shouldLoad` cancels while the combobox menu is hidden, so let's load when it is
+ // visible
+ loader.load();
+ },
+ },
+ useComboboxModel
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+ {model.state.items.length === 0 ? (
+ No Results Found
+ ) : (
+ model.state.items.length > 0 && (
+
+ {item => {item}}
+
+ )
+ )}
+
+
+
+ );
+ },
});
-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
+ >
+ )}
+
+
+ );
+ },
+});
diff --git a/modules/react/collection/lib/useOverflowListModel.tsx b/modules/react/collection/lib/useOverflowListModel.tsx
index b78637ecf0..f5e593a2c4 100644
--- a/modules/react/collection/lib/useOverflowListModel.tsx
+++ b/modules/react/collection/lib/useOverflowListModel.tsx
@@ -195,7 +195,6 @@ export const useOverflowListModel = createModelHook({
[data.id]: model.state.orientation === 'horizontal' ? data.width : data.height,
};
-
setItemSizeCache(itemSizeCacheRef.current);
const ids = getHiddenIds(