diff --git a/change/@fluentui-react-headless-components-preview-25f0894c-b02c-4b76-a05a-39a855967011.json b/change/@fluentui-react-headless-components-preview-25f0894c-b02c-4b76-a05a-39a855967011.json new file mode 100644 index 00000000000000..9db337321c9c8e --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-25f0894c-b02c-4b76-a05a-39a855967011.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: let a consumer-supplied popover attribute win over the default on the PopoverSurface, MenuPopover and Listbox slots", + "packageName": "@fluentui/react-headless-components-preview", + "email": "array.knight@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-30cabdfc-56c7-47ed-9b2a-a9b5824ad6d4.json b/change/@fluentui-react-headless-components-preview-30cabdfc-56c7-47ed-9b2a-a9b5824ad6d4.json new file mode 100644 index 00000000000000..7469d60a1c5e0f --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-30cabdfc-56c7-47ed-9b2a-a9b5824ad6d4.json @@ -0,0 +1,6 @@ +{ + "type": "patch", + "comment": "feat: expose the Avatar and AvatarGroup context readers from the headless entry points", + "packageName": "@fluentui/react-headless-components-preview", + "email": "vgenaev@gmail.com" +} diff --git a/change/@fluentui-react-utilities-2487f27f-0f06-4edf-8046-4f2016681dfd.json b/change/@fluentui-react-utilities-2487f27f-0f06-4edf-8046-4f2016681dfd.json new file mode 100644 index 00000000000000..a95a0b1fe6f02c --- /dev/null +++ b/change/@fluentui-react-utilities-2487f27f-0f06-4edf-8046-4f2016681dfd.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: Add loading to img element property whitelist", + "packageName": "@fluentui/react-utilities", + "email": "84954628+lukiod@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md b/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md index 017fb5ec39b99a..139b09b7202547 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/avatar-group.api.md @@ -29,6 +29,7 @@ import { renderAvatarGroup_unstable as renderAvatarGroup } from '@fluentui/react import { renderAvatarGroupItem_unstable as renderAvatarGroupItem } from '@fluentui/react-avatar'; import type { Slot } from '@fluentui/react-utilities'; import type { TooltipBaseProps } from '@fluentui/react-tooltip'; +import { useAvatarGroupContext_unstable as useAvatarGroupContext } from '@fluentui/react-avatar'; // @public export const AvatarGroup: ForwardRefComponent; @@ -105,6 +106,8 @@ export const renderAvatarGroupPopover: (state: AvatarGroupPopoverState, contextV // @public export const useAvatarGroup: (props: AvatarGroupProps, ref: React_2.Ref) => AvatarGroupState; +export { useAvatarGroupContext } + // @public export const useAvatarGroupContextValues: (state: AvatarGroupState) => AvatarGroupContextValues; diff --git a/packages/react-components/react-headless-components-preview/library/etc/avatar.api.md b/packages/react-components/react-headless-components-preview/library/etc/avatar.api.md index 7df93d4b42c08b..4324a0dde26b39 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/avatar.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/avatar.api.md @@ -10,6 +10,7 @@ import { AvatarBaseState as AvatarState } from '@fluentui/react-avatar'; import type { ForwardRefComponent } from '@fluentui/react-utilities'; import type * as React_2 from 'react'; import { renderAvatar_unstable as renderAvatar } from '@fluentui/react-avatar'; +import { useAvatarContext } from '@fluentui/react-avatar'; // @public export const Avatar: ForwardRefComponent; @@ -25,6 +26,8 @@ export { renderAvatar } // @public export const useAvatar: (props: AvatarProps, ref: React_2.Ref) => AvatarState; +export { useAvatarContext } + // (No @packageDocumentation comment for this package) ``` diff --git a/packages/react-components/react-headless-components-preview/library/src/avatar-group.ts b/packages/react-components/react-headless-components-preview/library/src/avatar-group.ts index 9b7304d61eedef..8b50b843c507ac 100644 --- a/packages/react-components/react-headless-components-preview/library/src/avatar-group.ts +++ b/packages/react-components/react-headless-components-preview/library/src/avatar-group.ts @@ -2,6 +2,7 @@ export { AvatarGroup, renderAvatarGroup, useAvatarGroup, + useAvatarGroupContext, useAvatarGroupContextValues, AvatarGroupItem, renderAvatarGroupItem, diff --git a/packages/react-components/react-headless-components-preview/library/src/avatar.ts b/packages/react-components/react-headless-components-preview/library/src/avatar.ts index b18247893808b1..fa9db547039f73 100644 --- a/packages/react-components/react-headless-components-preview/library/src/avatar.ts +++ b/packages/react-components/react-headless-components-preview/library/src/avatar.ts @@ -1,2 +1,2 @@ -export { Avatar, renderAvatar, useAvatar } from './components/Avatar/index'; +export { Avatar, renderAvatar, useAvatar, useAvatarContext } from './components/Avatar/index'; export type { AvatarSlots, AvatarProps, AvatarState } from './components/Avatar/index'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts index 58da5abff54df7..0e5e2dd8039fec 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/index.ts @@ -1,4 +1,4 @@ export { Avatar } from './Avatar'; export { renderAvatar } from './renderAvatar'; -export { useAvatar } from './useAvatar'; +export { useAvatar, useAvatarContext } from './useAvatar'; export type { AvatarSlots, AvatarProps, AvatarState } from './Avatar.types'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/useAvatar.ts b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/useAvatar.ts index dc195103a6840e..00244b7210ab44 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Avatar/useAvatar.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Avatar/useAvatar.ts @@ -14,3 +14,8 @@ export const useAvatar = (props: AvatarProps, ref: React.Ref): Avat return state; }; + +/** + * Returns the value from the nearest Avatar context. + */ +export { useAvatarContext } from '@fluentui/react-avatar'; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/index.ts b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/index.ts index 8bea653d83bf27..5070709391cb49 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/index.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/index.ts @@ -1,6 +1,6 @@ export { AvatarGroup } from './AvatarGroup'; export { renderAvatarGroup } from './renderAvatarGroup'; -export { useAvatarGroup } from './useAvatarGroup'; +export { useAvatarGroup, useAvatarGroupContext } from './useAvatarGroup'; export { useAvatarGroupContextValues } from './useAvatarGroupContextValues'; export type { AvatarGroupSlots, diff --git a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/useAvatarGroup.ts b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/useAvatarGroup.ts index d392dd0c49e1ad..4d771d9fa2c96b 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/useAvatarGroup.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/AvatarGroup/useAvatarGroup.ts @@ -17,3 +17,8 @@ export const useAvatarGroup = (props: AvatarGroupProps, ref: React.Ref { expect(getAllByRole('option')).toHaveLength(3); }); + it('lets a consumer override the popover attribute', () => { + const { getByRole } = render( + + + + , + ); + + expect(getByRole('listbox')).toHaveAttribute('popover', 'manual'); + }); + it('sets data-selected="true" on the selected option', () => { const { getAllByRole } = render( diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Dropdown/Listbox/useListbox.ts b/packages/react-components/react-headless-components-preview/library/src/components/Dropdown/Listbox/useListbox.ts index 3abe831cf461d8..bfabbf6068c14c 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Dropdown/Listbox/useListbox.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Dropdown/Listbox/useListbox.ts @@ -12,8 +12,8 @@ import type { ListboxProps, ListboxState } from './Listbox.types'; export const useListbox = (props: ListboxProps, ref: React.Ref): ListboxState => { const state = useListbox_unstable(props, ref); - // eslint-disable-next-line react-hooks/immutability - state.root.popover = 'auto'; - - return state; + return { + ...state, + root: { popover: 'auto', ...state.root }, + }; }; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx index d5dc4f9b44fed9..056bd96554ebc9 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/Menu.test.tsx @@ -306,6 +306,40 @@ describe('Menu', () => { expect(container.contains(getByRole('menu'))).toBe(true); }); + it('promotes the MenuPopover with popover="auto" by default', () => { + const { getByRole } = render( + + + + + + + Item 1 + + + , + ); + + expect(getByRole('menu').parentElement).toHaveAttribute('popover', 'auto'); + }); + + it('lets a consumer override the popover attribute', () => { + const { getByRole } = render( + + + + + + + Item 1 + + + , + ); + + expect(getByRole('menu').parentElement).toHaveAttribute('popover', 'manual'); + }); + // Arrow navigation is handled by Tabster (useArrowNavigationGroup). // Tabster's behavior is comprehensively tested in @fluentui/react-tabster. // E2E and integration tests should verify arrow navigation works correctly in the browser. diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuPopover/useMenuPopover.ts b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuPopover/useMenuPopover.ts index df3b8e0238292d..5edbeb927374ae 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuPopover/useMenuPopover.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Menu/MenuPopover/useMenuPopover.ts @@ -15,7 +15,7 @@ export const useMenuPopover = (props: MenuPopoverProps, ref: React.Ref ctx.open); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/PopoverSurface.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/PopoverSurface.test.tsx index 7889e48914a48b..e674729f0dc765 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/PopoverSurface.test.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/PopoverSurface.test.tsx @@ -57,6 +57,19 @@ describe('PopoverSurface', () => { expect(getByRole('group', { hidden: true })).toHaveAttribute('popover', 'auto'); }); + it('lets a consumer override the popover attribute', () => { + const { getByRole } = render( + + + + + Content + , + ); + + expect(getByRole('group', { hidden: true })).toHaveAttribute('popover', 'manual'); + }); + it('mirrors a browser-driven `toggle` event into onOpenChange', () => { const onOpenChange = jest.fn(); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/usePopoverSurface.ts b/packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/usePopoverSurface.ts index 9aed28add187ca..0c85468300011b 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/usePopoverSurface.ts +++ b/packages/react-components/react-headless-components-preview/library/src/components/Popover/PopoverSurface/usePopoverSurface.ts @@ -33,11 +33,15 @@ export const usePopoverSurface = ( role: trapFocus ? 'dialog' : 'group', ...props, id: surfaceId, - popover: trapFocus ? undefined : 'auto', 'data-popover-surface': '', 'data-open': toDataAttributeValue(open), }, - { elementType: 'dialog' }, + { + defaultProps: { + popover: trapFocus ? undefined : 'auto', + }, + elementType: 'dialog', + }, ), 'data-open': open ? 'true' : 'false', }; diff --git a/packages/react-components/react-utilities/src/utils/properties.test.ts b/packages/react-components/react-utilities/src/utils/properties.test.ts index 77b913f40979d0..5895b7ca8f5eac 100644 --- a/packages/react-components/react-utilities/src/utils/properties.test.ts +++ b/packages/react-components/react-utilities/src/utils/properties.test.ts @@ -1,5 +1,12 @@ import type * as React from 'react'; -import { getNativeProps, divProperties, inputProperties, anchorProperties, buttonProperties } from './properties'; +import { + getNativeProps, + divProperties, + inputProperties, + anchorProperties, + buttonProperties, + imgProperties, +} from './properties'; describe('getNativeProps', () => { it('can pass through data tags', () => { @@ -113,6 +120,27 @@ describe('getNativeProps', () => { expect(result).not.toHaveProperty('foobar'); }); + it('can pass through img props including loading', () => { + const result = getNativeProps>( + { + src: 'https://example.com/image.png', + alt: 'An image', + loading: 'lazy', + // Non-img property + foobar: 1, + }, + imgProperties, + ); + + expect(result).toMatchObject({ + src: 'https://example.com/image.png', + alt: 'An image', + loading: 'lazy', + }); + + expect(result).not.toHaveProperty('foobar'); + }); + describe('popover', () => { it('allows the popoverTarget attribute on button', () => { const result = getNativeProps>( diff --git a/packages/react-components/react-utilities/src/utils/properties.ts b/packages/react-components/react-utilities/src/utils/properties.ts index f71a7200a9592c..5be0ca0973bb1a 100644 --- a/packages/react-components/react-utilities/src/utils/properties.ts +++ b/packages/react-components/react-utilities/src/utils/properties.ts @@ -416,6 +416,7 @@ export const imgProperties = toObjectMap(htmlElementProperties, [ 'alt', // area, img, input 'crossOrigin', // img 'height', // canvas, embed, iframe, img, input, object, video + 'loading', // img, iframe 'src', // audio, embed, iframe, img, input, script, source, track, video 'srcSet', // img, source 'useMap', // img, object,