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
@@ -1,8 +1,7 @@
export { Tab } from './Tab';

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/Card Converged - Disabled 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Card Converged - Disabled.appearance disabled + interactive - Dark Mode.normal.chromium.png 3330 Changed
vr-tests-react-components/Card Converged - Disabled.appearance disabled + selectable.normal.chromium.png 12549 Changed
vr-tests-react-components/Card Converged - Disabled.appearance disabled + interactive - High Contrast.normal.chromium.png 5610 Changed
vr-tests-react-components/Charts-DonutChart 2 screenshots
Image Name Diff(in Pixels) Image Type
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.updated 2 times.chromium.png 57 Changed
vr-tests-react-components/Positioning.Positioning end.chromium.png 892 Changed

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

export type { TabInternalSlots, TabBaseProps, TabProps, TabSlots, TabBaseState, TabState, TabValue } from './Tab.types';
export { renderTab_unstable } from './renderTab';
export { useTab_unstable } from './useTab';
export { useTabBase_unstable } from './useTabBase';
export { useTab_unstable, useTabBase_unstable, useTabFocusAttributes_unstable } from './useTab';
export {
tabClassNames,
tabReservedSpaceClassNames,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
'use client';

import * as React from 'react';
import { omit, slot } from '@fluentui/react-utilities';
import type { TabProps, TabState } from './Tab.types';
import { type TabsterDOMAttribute, useTabsterAttributes } from '@fluentui/react-tabster';
import { mergeCallbacks, useEventCallback, useMergedRefs, slot, omit } from '@fluentui/react-utilities';
import type { TabProps, TabState, TabBaseProps, TabBaseState } from './Tab.types';
import { useTabListContext_unstable } from '../TabList';
import { useTabBase_unstable } from './useTabBase';
import type { SelectTabEvent } from '../TabList';

/**
* Create the state required to render Tab.
*
* The returned state can be modified with hooks such as useTabStyles_unstable,
* before being passed to renderTab_unstable.
*
* @internal
* @param props - props from this instance of Tab
* @param ref - reference to root HTMLElement of Tab
*/
export const useTab_unstable = (props: TabProps, ref: React.Ref<HTMLElement>): TabState => {
const { content } = props;

const state = useTabBase_unstable(props, ref);
const focusAttributes = useTabFocusAttributes_unstable(state);

const appearance = useTabListContext_unstable(ctx => ctx.appearance);
const reserveSelectedTabSpace = useTabListContext_unstable(ctx => ctx.reserveSelectedTabSpace);
Expand All @@ -31,6 +34,10 @@ export const useTab_unstable = (props: TabProps, ref: React.Ref<HTMLElement>): T
...state,
// eslint-disable-next-line @typescript-eslint/no-deprecated
components: { ...state.components, contentReservedSpace: 'span' },
root: {
...state.root,
...focusAttributes,
},
contentReservedSpace: slot.optional(contentReservedSpace, {
renderByDefault: !state.selected && !state.iconOnly && reserveSelectedTabSpace,
defaultProps: { children: props.children },
Expand All @@ -40,3 +47,83 @@ export const useTab_unstable = (props: TabProps, ref: React.Ref<HTMLElement>): T
size,
};
};

/**
* Create the based state required to render Tab without design specifics and focus attributes.
*
* @param props - props from this instance of Tab
* @param ref - reference to root HTMLElement of Tab
*/
export const useTabBase_unstable = (props: TabBaseProps, ref: React.Ref<HTMLElement>): TabBaseState => {
const { content, disabled: tabDisabled = false, icon, onClick, onFocus, value, ...rest } = props;

const selectTabOnFocus = useTabListContext_unstable(ctx => ctx.selectTabOnFocus);
const listDisabled = useTabListContext_unstable(ctx => ctx.disabled);
const selected = useTabListContext_unstable(ctx => ctx.selectedValue === value);
const onRegister = useTabListContext_unstable(ctx => ctx.onRegister);
const onUnregister = useTabListContext_unstable(ctx => ctx.onUnregister);
const onSelect = useTabListContext_unstable(ctx => ctx.onSelect);
const vertical = useTabListContext_unstable(ctx => !!ctx.vertical);
const disabled = listDisabled || tabDisabled;

const innerRef = React.useRef<HTMLElement>(null);
const onSelectCallback = (event: SelectTabEvent) => onSelect(event, { value });
const onTabClick = useEventCallback(mergeCallbacks(onClick, onSelectCallback));
const onTabFocus = useEventCallback(mergeCallbacks(onFocus, onSelectCallback));

React.useEffect(() => {
onRegister({
value,
ref: innerRef,
});

return () => {
onUnregister({ value, ref: innerRef });
};
}, [onRegister, onUnregister, innerRef, value]);

const iconSlot = slot.optional(icon, { elementType: 'span' });
const contentSlot = slot.always(content, {
defaultProps: { children: props.children },
elementType: 'span',
});
const iconOnly = Boolean(iconSlot?.children && !contentSlot.children);
return {
components: { root: 'button', icon: 'span', content: 'span', contentReservedSpace: 'span' },
root: slot.always(
{
ref: useMergedRefs(ref, innerRef),
role: 'tab',
type: 'button',
// aria-selected undefined indicates it is not selectable
// according to https://www.w3.org/TR/wai-aria-1.1/#aria-selected
'aria-selected': disabled ? undefined : (`${selected}` as 'true' | 'false'),
value,
...rest,
disabled,
onClick: onTabClick,
onFocus: selectTabOnFocus ? onTabFocus : onFocus,
},
{ elementType: 'button' },
) as TabBaseState['root'],
icon: iconSlot,
iconOnly,
content: contentSlot,
disabled,
selected,
value,
vertical,
};
};

/**
* Hook to return focus attributes to a Tab based on selected state.
* Should be applied on the button with role="tab".
*
* @internal
*/
export const useTabFocusAttributes_unstable = ({ selected }: Pick<TabBaseState, 'selected'>): TabsterDOMAttribute => {
return useTabsterAttributes({
focusable: { isDefault: selected },
});
};

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ export type {
} from './TabList.types';
export { TabListContext, TabListProvider, useTabListContext_unstable } from './TabListContext';
export { renderTabList_unstable } from './renderTabList';
export { useTabList_unstable } from './useTabList';
export { useTabListBase_unstable } from './useTabListBase';
export {
useTabList_unstable,
useTabListBase_unstable,
useTabListFocusAttributes_unstable as useTabListFocusAttributes,
} from './useTabList';
export { useTabListContextValues_unstable } from './useTabListContextValues';
export { tabListClassNames, useTabListStyles_unstable } from './useTabListStyles.styles';
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
'use client';

import * as React from 'react';
import type { TabListProps, TabListState } from './TabList.types';
import { useTabListBase_unstable } from './useTabListBase';
import { type TabsterDOMAttribute, useArrowNavigationGroup } from '@fluentui/react-tabster';
import { useControllableState, useEventCallback, useMergedRefs, slot } from '@fluentui/react-utilities';
import type {
TabRegisterData,
SelectTabData,
SelectTabEvent,
TabListBaseProps,
TabListBaseState,
TabListProps,
TabListState,
} from './TabList.types';
import type { TabValue } from '../Tab';

/**
* Create the state required to render TabList.
Expand All @@ -16,11 +26,136 @@ import { useTabListBase_unstable } from './useTabListBase';
export const useTabList_unstable = (props: TabListProps, ref: React.Ref<HTMLElement>): TabListState => {
const { appearance = 'transparent', reserveSelectedTabSpace = true, size = 'medium' } = props;
const state = useTabListBase_unstable(props, ref);
const focusAttributes = useTabListFocusAttributes_unstable({ vertical: state.vertical });

return {
...state,
root: {
...state.root,
...focusAttributes,
},
appearance,
reserveSelectedTabSpace,
size,
};
};

/**
* Create the state required to render TabList.
*
* The returned state can be modified with hooks such as useTabListStyles_unstable,
* before being passed to renderTabList_unstable.
*
* @param props - props from this instance of TabList
* @param ref - reference to root HTMLElement of TabList
*/
export const useTabListBase_unstable = (props: TabListBaseProps, ref: React.Ref<HTMLElement>): TabListBaseState => {
const {
disabled = false,
onTabSelect,
selectTabOnFocus = false,
vertical = false,
selectedValue: controlledSelectedValue,
defaultSelectedValue,
...rest
} = props;

const innerRef = React.useRef<HTMLElement>(null);

const [selectedValue, setSelectedValue] = useControllableState({
state: controlledSelectedValue,
defaultState: defaultSelectedValue,
initialState: undefined,
});

// considered usePrevious, but it is sensitive to re-renders
// this could cause the previous to move to current in the case where the tab list re-renders.
// these refs avoid getRegisteredTabs changing when selectedValue changes and causing
// renders for tabs that have not changed.
const currentSelectedValue = React.useRef<TabValue | undefined>(undefined);
const previousSelectedValue = React.useRef<TabValue | undefined>(undefined);

React.useEffect(() => {
previousSelectedValue.current = currentSelectedValue.current;
currentSelectedValue.current = selectedValue;
}, [selectedValue]);

const onSelect = useEventCallback((event: SelectTabEvent, data: SelectTabData) => {
setSelectedValue(data.value);
onTabSelect?.(event, data);
});

const registeredTabs = React.useRef<Record<string, TabRegisterData>>({});

const onRegister = useEventCallback((data: TabRegisterData) => {
const key = JSON.stringify(data.value);

if (!key && process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.error(
[
`[@fluentui/react-tabs] The value "${data.value}" cannot be serialized to JSON string.`,
'Tab component requires serializable values.',
'Please provide a primitive value (string, number, boolean),',
`or a plain object/array that doesn't contain functions, symbols, or circular references.`,
].join(' '),
);
}

registeredTabs.current[key] = data;
});

const onUnregister = useEventCallback((data: TabRegisterData) => {
delete registeredTabs.current[JSON.stringify(data.value)];
});

const getRegisteredTabs = React.useCallback(() => {
return {
selectedValue: currentSelectedValue.current,
previousSelectedValue: previousSelectedValue.current,
registeredTabs: registeredTabs.current,
};
}, []);

return {
components: {
root: 'div',
},
root: slot.always(
{
ref: useMergedRefs(ref, innerRef) as React.Ref<HTMLDivElement>,
role: 'tablist',
'aria-orientation': vertical ? 'vertical' : 'horizontal',
...rest,
},
{ elementType: 'div' },
),
disabled,
selectTabOnFocus,
selectedValue,
onRegister,
onUnregister,
onSelect,
getRegisteredTabs,
vertical,
};
};

/**
* Hook to get Tabster DOM attributes for TabList focus handling
*
* @internal
* @param vertical - whether the TabList is vertical
* @returns Tabster DOM attributes
*/
export const useTabListFocusAttributes_unstable = ({
vertical,
}: Pick<TabListBaseState, 'vertical'>): TabsterDOMAttribute => {
return useArrowNavigationGroup({
circular: true,
axis: vertical ? 'vertical' : 'horizontal',
memorizeCurrent: false,
// eslint-disable-next-line @typescript-eslint/naming-convention
unstable_hasDefault: true,
});
};
Loading
Loading