Skip to content
Merged
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 @@
{
"type": "minor",
"comment": "Added support for reserving space for selected state",
"packageName": "@fluentui/react-tabs",
"email": "[email protected]",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const TabList: ForwardRefComponent<TabListProps>;
export const tabListClassNames: SlotClassNames<TabListSlots>;

// @public (undocumented)
export type TabListContextValue = Pick<TabListProps, 'onTabSelect' | 'selectedValue'> & Required<Pick<TabListProps, 'appearance' | 'disabled' | 'size' | 'vertical'>> & {
export type TabListContextValue = Pick<TabListProps, 'onTabSelect' | 'selectedValue' | 'reserveSelectedTabSpace'> & Required<Pick<TabListProps, 'appearance' | 'disabled' | 'size' | 'vertical'>> & {
onRegister: RegisterTabEventHandler;
onUnregister: RegisterTabEventHandler;
onSelect: SelectTabEventHandler;
Expand All @@ -63,6 +63,7 @@ export type TabListContextValues = {
// @public
export type TabListProps = ComponentProps<TabListSlots> & {
appearance?: 'transparent' | 'subtle';
reserveSelectedTabSpace?: boolean;
defaultSelectedValue?: TabValue;
disabled?: boolean;
onTabSelect?: SelectTabEventHandler;
Expand Down Expand Up @@ -103,6 +104,7 @@ export type TabState = ComponentState<TabSlots> & Pick<TabProps, 'value'> & Requ
appearance?: 'transparent' | 'subtle';
iconOnly: boolean;
selected: boolean;
contentReservedSpaceClassName?: string;
size: 'small' | 'medium';
vertical: boolean;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export type TabState = ComponentState<TabSlots> &
* If this tab is selected
*/
selected: boolean;
/**
* When defined, tab content with selected style is rendered hidden to reserve space.
* This keeps consistent content size between unselected and selected states.
*/
contentReservedSpaceClassName?: string;
/**
* A tab can be either 'small' or 'medium' size.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export const renderTab_unstable = (state: TabState) => {
<slots.root {...slotProps.root}>
{slots.icon && <slots.icon {...slotProps.icon} />}
{!state.iconOnly && <slots.content {...slotProps.content} />}
{!state.selected && !state.iconOnly && state.contentReservedSpaceClassName !== undefined && (
<slots.content {...slotProps.content} className={state.contentReservedSpaceClassName} />
)}
</slots.root>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const useTab_unstable = (props: TabProps, ref: React.Ref<HTMLElement>): T
const { content, disabled: tabDisabled = false, icon, value } = props;

const appearance = useContextSelector(TabListContext, ctx => ctx.appearance);
const reserveSelectedTabSpace = useContextSelector(TabListContext, ctx => ctx.reserveSelectedTabSpace);
const listDisabled = useContextSelector(TabListContext, ctx => ctx.disabled);
const selected = useContextSelector(TabListContext, ctx => ctx.selectedValue === value);
const onRegister = useContextSelector(TabListContext, ctx => ctx.onRegister);
Expand Down Expand Up @@ -64,6 +65,7 @@ export const useTab_unstable = (props: TabProps, ref: React.Ref<HTMLElement>): T
iconOnly: Boolean(iconShorthand?.children && !contentShorthand.children),
content: contentShorthand,
appearance,
contentReservedSpaceClassName: reserveSelectedTabSpace ? '' : undefined,
disabled,
selected,
size,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const tabClassNames: SlotClassNames<TabSlots> = {
content: 'fui-Tab__content',
};

const reservedSpaceClassNames = {
content: 'fui-Tab__content--reserved-space',
};

/**
* Styles for the root slot
*/
Expand Down Expand Up @@ -309,6 +313,8 @@ const useActiveIndicatorStyles = makeStyles({
*/
const useIconStyles = makeStyles({
base: {
gridColumnStart: 1,
gridRowStart: 1,
alignItems: 'center',
display: 'inline-flex',
justifyContent: 'center',
Expand Down Expand Up @@ -341,6 +347,17 @@ const useContentStyles = makeStyles({
selected: {
...typographyStyles.body1Strong,
},
noIconBefore: {
gridColumnStart: 1,
gridRowStart: 1,
},
iconBefore: {
gridColumnStart: 2,
gridRowStart: 1,
},
placeholder: {
visibility: 'hidden',
},
});

/**
Expand Down Expand Up @@ -384,17 +401,31 @@ export const useTabStyles_unstable = (state: TabState): TabState => {
size === 'small' &&
(vertical ? activeIndicatorStyles.smallVertical : activeIndicatorStyles.smallHorizontal),
selected && disabled && activeIndicatorStyles.disabled,

state.root.className,
);

if (state.icon) {
state.icon.className = mergeClasses(tabClassNames.icon, iconStyles.base, iconStyles[size], state.icon.className);
}

// This needs to be before state.content.className is updated
if (state.contentReservedSpaceClassName !== undefined) {
state.contentReservedSpaceClassName = mergeClasses(
reservedSpaceClassNames.content,
contentStyles.base,
contentStyles.selected,
state.icon ? contentStyles.iconBefore : contentStyles.noIconBefore,
contentStyles.placeholder,
state.content.className,
);
}

state.content.className = mergeClasses(
tabClassNames.content,
contentStyles.base,
selected && contentStyles.selected,
state.icon ? contentStyles.iconBefore : contentStyles.noIconBefore,
state.content.className,
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ export type TabListProps = ComponentProps<TabListSlots> & {
*/
appearance?: 'transparent' | 'subtle';

/**
* Tab size may change between unselected and selected states.
* The default scenario is a selected tab has bold text.
*
* When true, this property requests tabs be the same size whether unselected or selected.
* @default true
*/
reserveSelectedTabSpace?: boolean;

/**
* The value of the tab to be selected by default.
* Typically useful when the selectedValue is uncontrolled.
Expand Down Expand Up @@ -82,7 +91,7 @@ export type TabListProps = ComponentProps<TabListSlots> & {
vertical?: boolean;
};

export type TabListContextValue = Pick<TabListProps, 'onTabSelect' | 'selectedValue'> &
export type TabListContextValue = Pick<TabListProps, 'onTabSelect' | 'selectedValue' | 'reserveSelectedTabSpace'> &
Required<Pick<TabListProps, 'appearance' | 'disabled' | 'size' | 'vertical'>> & {
/** A callback to allow a tab to register itself with the tab list. */
onRegister: RegisterTabEventHandler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TabListContextValue } from './TabList.types';
// eslint-disable-next-line @fluentui/no-context-default-value
export const TabListContext: Context<TabListContextValue> = createContext<TabListContextValue>({
appearance: 'transparent',
reserveSelectedTabSpace: true,
disabled: false,
selectedValue: undefined,
onRegister: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ exports[`TabList renders tabs when disabled 1`] = `
>
First
</span>
<span
class="fui-Tab__content--reserved-space"
>
First
</span>
</button>
<button
class="fui-Tab"
Expand All @@ -45,6 +50,11 @@ exports[`TabList renders tabs when disabled 1`] = `
>
Third
</span>
<span
class="fui-Tab__content--reserved-space"
>
Third
</span>
</button>
</div>
</div>
Expand All @@ -70,6 +80,11 @@ exports[`TabList renders tabs with default selected tab 1`] = `
>
First
</span>
<span
class="fui-Tab__content--reserved-space"
>
First
</span>
</button>
<button
aria-selected="true"
Expand Down Expand Up @@ -98,6 +113,11 @@ exports[`TabList renders tabs with default selected tab 1`] = `
>
Third
</span>
<span
class="fui-Tab__content--reserved-space"
>
Third
</span>
</button>
</div>
</div>
Expand Down Expand Up @@ -133,6 +153,11 @@ exports[`TabList renders with tabs 1`] = `
>
First
</span>
<span
class="fui-Tab__content--reserved-space"
>
First
</span>
</button>
<button
aria-selected="false"
Expand All @@ -147,6 +172,11 @@ exports[`TabList renders with tabs 1`] = `
>
Second
</span>
<span
class="fui-Tab__content--reserved-space"
>
Second
</span>
</button>
<button
aria-selected="false"
Expand All @@ -161,6 +191,11 @@ exports[`TabList renders with tabs 1`] = `
>
Third
</span>
<span
class="fui-Tab__content--reserved-space"
>
Third
</span>
</button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ import { TabValue } from '../Tab/Tab.types';
* @param ref - reference to root HTMLElement of TabList
*/
export const useTabList_unstable = (props: TabListProps, ref: React.Ref<HTMLElement>): TabListState => {
const { appearance = 'transparent', disabled = false, onTabSelect, size = 'medium', vertical = false } = props;
const {
appearance = 'transparent',
reserveSelectedTabSpace = true,
disabled = false,
onTabSelect,
size = 'medium',
vertical = false,
} = props;

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

Expand Down Expand Up @@ -81,6 +88,7 @@ export const useTabList_unstable = (props: TabListProps, ref: React.Ref<HTMLElem
...props,
}),
appearance,
reserveSelectedTabSpace,
disabled,
selectedValue,
size,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TabListContextValue, TabListContextValues, TabListState } from './TabLi
export function useTabListContextValues(state: TabListState): TabListContextValues {
const {
appearance,
reserveSelectedTabSpace,
disabled,
selectedValue: selectedKey,
onRegister,
Expand All @@ -15,6 +16,7 @@ export function useTabListContextValues(state: TabListState): TabListContextValu

const tabList: TabListContextValue = {
appearance,
reserveSelectedTabSpace,
disabled,
selectedValue: selectedKey,
onSelect,
Expand Down