Skip to content

Commit

Permalink
Fix: Update tabs to remain the same size between unselected and selec…
Browse files Browse the repository at this point in the history
…ted states (#25542)

* Add support to keep tabs from changing size when selected

* Update for naming and tests

* Rename update

* Update packages/react-components/react-tabs/src/components/TabList/TabList.types.ts

Co-authored-by: Sean Monahan <[email protected]>

Co-authored-by: Sean Monahan <[email protected]>
  • Loading branch information
GeoffCoxMSFT and spmonahan authored Nov 7, 2022
1 parent b3213e6 commit b0f2a9a
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 3 deletions.
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"
}
4 changes: 3 additions & 1 deletion packages/react-components/react-tabs/etc/react-tabs.api.md
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

0 comments on commit b0f2a9a

Please sign in to comment.