Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/CalendarCompat 4 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/CalendarCompat.multiDayView.default.chromium_1.png 654 Changed
vr-tests-react-components/CalendarCompat.multiDayView - Dark Mode.default.chromium.png 2172 Changed
vr-tests-react-components/CalendarCompat.multiDayView - RTL.default.chromium.png 654 Changed
vr-tests-react-components/CalendarCompat.multiDayView - High Contrast.default.chromium.png 2243 Changed
vr-tests-react-components/Charts-DonutChart 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Charts-DonutChart.Dynamic - Dark Mode.default.chromium.png 7530 Changed
vr-tests-react-components/Charts-DonutChart.Dynamic - RTL.default.chromium.png 5570 Changed
vr-tests-react-components/Charts-DonutChart.Dynamic.default.chromium.png 5581 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 899 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 753 Changed
vr-tests-react-components/ProgressBar converged 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png 59 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 40 Changed
vr-tests-react-components/Skeleton converged 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Skeleton converged.Opaque Skeleton with circle - Dark Mode.default.chromium.png 1 Changed
vr-tests-react-components/TagPicker 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.disabled input hover.chromium.png 658 Changed
vr-tests-react-components/TagPicker.disabled - RTL.chromium.png 635 Changed

There were 3 duplicate changes discarded. Check the build logs for more information.

"type": "patch",
"comment": "fix: fix useList and useListItem as prop handling",
"packageName": "@fluentui/react-list",
"email": "dmytrokirpa@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export const renderListItem_unstable: (state: ListItemState) => JSXElement;
// @public
export const useList_unstable: (props: ListProps, ref: React_2.Ref<HTMLDivElement | HTMLUListElement | HTMLOListElement>) => ListState;

// @public
export function useListContextValues_unstable(state: ListState): ListContextValues;

// @public
export const useListItem_unstable: (props: ListItemProps, ref: React_2.Ref<HTMLLIElement | HTMLDivElement>) => ListItemState;

Expand Down
3 changes: 2 additions & 1 deletion packages/react-components/react-list/library/src/List.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
List,
listClassNames,
renderList_unstable,
useListStyles_unstable,
useList_unstable,
useListContextValues_unstable,
useListStyles_unstable,
} from './components/List/index';
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export type {
} from './List.types';
export { renderList_unstable } from './renderList';
export { useList_unstable } from './useList';
export { useListContextValues_unstable } from './useListContextValues';
export { listClassNames, useListStyles_unstable } from './useListStyles.styles';
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';

import { useList_unstable } from './useList';

describe('useList_unstable', () => {
let ref: React.RefObject<HTMLDivElement | HTMLUListElement | HTMLOListElement | null>;

beforeEach(() => {
ref = React.createRef<HTMLDivElement | HTMLUListElement | HTMLOListElement>();
});

it('returns default state for non-selectable list', () => {
const { result } = renderHook(() => useList_unstable({}, ref));

expect(result.current).toMatchObject({
components: {
root: 'ul',
},
root: expect.objectContaining({
ref,
role: 'list',
}),
listItemRole: 'listitem',
navigationMode: undefined,
selection: undefined,
});
});

it('returns selection state and listbox role when selectionMode is enabled', () => {
const onSelectionChange = jest.fn();
const { result } = renderHook(() =>
useList_unstable(
{
selectionMode: 'single',
onSelectionChange,
},
ref,
),
);

expect(result.current.root).toMatchObject({
role: 'listbox',
});
expect(result.current.selection).toBeDefined();

act(() => {
result.current.selection?.toggleItem({} as React.SyntheticEvent, 'item-1');
});

expect(result.current.selection?.selectedItems).toEqual(['item-1']);
expect(onSelectionChange).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
type: 'change',
selectedItems: ['item-1'],
}),
);
});

it('uses div root and grid roles in composite navigation mode', () => {
const { result } = renderHook(() => useList_unstable({ navigationMode: 'composite' }, ref));

expect(result.current).toMatchObject({
components: {
root: 'div',
},
root: expect.objectContaining({
role: 'grid',
}),
listItemRole: 'row',
navigationMode: 'composite',
});
});

it('respects an explicit as prop in returned root component', () => {
const { result } = renderHook(() => useList_unstable({ as: 'ol' }, ref));

// eslint-disable-next-line @typescript-eslint/no-deprecated
expect(result.current.components.root).toBe('ol');
expect(result.current.root.role).toBe('list');
});
it('respects explicit as prop even in composite navigation mode', () => {
const { result } = renderHook(() => useList_unstable({ as: 'ol', navigationMode: 'composite' }, ref));

// eslint-disable-next-line @typescript-eslint/no-deprecated
expect(result.current.components.root).toBe('ol');
});

it('determines default root component based on composite navigation mode', () => {
const { result } = renderHook(() => useList_unstable({ navigationMode: 'composite' }, ref));

// eslint-disable-next-line @typescript-eslint/no-deprecated
expect(result.current.components.root).toBe('div');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const useList_unstable = (
): ListState => {
const { navigationMode, selectionMode, selectedItems, defaultSelectedItems, onSelectionChange } = props;

const as = props.as || navigationMode === 'composite' ? 'div' : DEFAULT_ROOT_EL_TYPE;
const as = props.as || (navigationMode === 'composite' ? 'div' : DEFAULT_ROOT_EL_TYPE);

const arrowNavigationAttributes = useArrowNavigationGroup({
axis: 'vertical',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as React from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { Space } from '@fluentui/keyboard-keys';

import { useListItem_unstable } from './useListItem';
import { ListContextProvider, ListSynchronousContextProvider } from '../List/listContext';
import type { ListContextValue, ListSynchronousContextValue } from '../List/List.types';
import type { ListSelectionState } from '../../hooks/types';

describe('useListItem_unstable', () => {
let ref: React.RefObject<HTMLLIElement | HTMLDivElement | null>;

beforeEach(() => {
ref = React.createRef<HTMLLIElement | HTMLDivElement>();
});

it('returns default state when list context is not provided', () => {
const { result } = renderHook(() => useListItem_unstable({}, ref));

expect(result.current).toMatchObject({
components: {
root: 'li',
},
root: expect.objectContaining({
role: 'listitem',
}),
selectable: false,
navigable: false,
checkmark: undefined,
});
});

it('uses div root and row role in composite navigation mode', () => {
const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
const listContextValue: ListContextValue = {
selection: undefined,
validateListItem: jest.fn(),
};
const syncContextValue: ListSynchronousContextValue = {
navigationMode: 'composite',
listItemRole: 'row',
};

return (
<ListContextProvider value={listContextValue}>
<ListSynchronousContextProvider value={syncContextValue}>{children}</ListSynchronousContextProvider>
</ListContextProvider>
);
};

const { result } = renderHook(() => useListItem_unstable({}, ref), { wrapper });

expect(result.current).toMatchObject({
components: {
root: 'div',
},
root: expect.objectContaining({
role: 'row',
}),
navigable: true,
});
});

it('respects an explicit as prop in returned root component', () => {
const { result } = renderHook(() => useListItem_unstable({ as: 'li' }, ref));

// eslint-disable-next-line @typescript-eslint/no-deprecated
expect(result.current.components.root).toBe('li');
});

it('returns selectable state and toggles selection with keyboard action', () => {
const toggleItem = jest.fn();
const selection: ListSelectionState = {
isSelected: () => true,
toggleItem,
deselectItem: jest.fn(),
selectItem: jest.fn(),
clearSelection: jest.fn(),
toggleAllItems: jest.fn(),
setSelectedItems: jest.fn(),
selectedItems: ['item-1'],
};

const wrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
const listContextValue: ListContextValue = {
selection,
validateListItem: jest.fn(),
};
const syncContextValue: ListSynchronousContextValue = {
navigationMode: 'items',
listItemRole: 'option',
};

return (
<ListContextProvider value={listContextValue}>
<ListSynchronousContextProvider value={syncContextValue}>{children}</ListSynchronousContextProvider>
</ListContextProvider>
);
};

const { result } = renderHook(() => useListItem_unstable({ value: 'item-1' }, ref), { wrapper });

expect(result.current).toMatchObject({
selectable: true,
navigable: true,
root: expect.objectContaining({
role: 'option',
'aria-selected': true,
}),
checkmark: expect.objectContaining({
checked: true,
}),
});

const target = document.createElement('div');
const event = {
key: Space,
target,
currentTarget: target,
preventDefault: jest.fn(),
defaultPrevented: false,
} as unknown as React.KeyboardEvent<HTMLLIElement> & React.KeyboardEvent<HTMLDivElement>;

act(() => {
result.current.root.onKeyDown?.(event);
});

expect(event.preventDefault).toHaveBeenCalled();
expect(toggleItem).toHaveBeenCalledWith(event, 'item-1');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const useListItem_unstable = (
const isSelected = useListContext_unstable(ctx => ctx.selection?.isSelected(value)) ?? false;
const validateListItem = useListContext_unstable(ctx => ctx.validateListItem);

const as = props.as || navigationMode === 'composite' ? 'div' : DEFAULT_ROOT_EL_TYPE;
const as = props.as || (navigationMode === 'composite' ? 'div' : DEFAULT_ROOT_EL_TYPE);

const finalListItemRole = role || listItemRole;

Expand Down
11 changes: 9 additions & 2 deletions packages/react-components/react-list/library/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export { List, listClassNames, renderList_unstable, useListStyles_unstable, useList_unstable } from './List';
export {
List,
listClassNames,
renderList_unstable,
useList_unstable,
useListContextValues_unstable,
useListStyles_unstable,
} from './List';

export type { ListProps, ListSlots, ListState } from './List';
export type { ListContextValues, ListProps, ListSlots, ListState } from './List';
export {
ListItem,
listItemClassNames,
Expand Down
Loading