From c2eb480b679ae57954d83f4ce7c40347a412eecb Mon Sep 17 00:00:00 2001 From: GeoffCoxMSFT Date: Fri, 4 Nov 2022 13:17:44 -0700 Subject: [PATCH 1/5] Add support to keep tabs from changing size when selected --- .../src/components/Tab/Tab.types.ts | 5 ++++ .../src/components/Tab/renderTab.tsx | 3 +++ .../react-tabs/src/components/Tab/useTab.ts | 2 ++ .../src/components/Tab/useTabStyles.ts | 27 +++++++++++++++++++ .../src/components/TabList/TabList.types.ts | 12 ++++++++- .../src/components/TabList/TabListContext.ts | 1 + .../src/components/TabList/useTabList.ts | 10 ++++++- .../TabList/useTabListContextValues.tsx | 2 ++ 8 files changed, 60 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-tabs/src/components/Tab/Tab.types.ts b/packages/react-components/react-tabs/src/components/Tab/Tab.types.ts index 1adf14d42b0a7..8eead270a03c4 100644 --- a/packages/react-components/react-tabs/src/components/Tab/Tab.types.ts +++ b/packages/react-components/react-tabs/src/components/Tab/Tab.types.ts @@ -56,6 +56,11 @@ export type TabState = ComponentState & * 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. + */ + selectedTabContentClassName?: string; /** * A tab can be either 'small' or 'medium' size. */ diff --git a/packages/react-components/react-tabs/src/components/Tab/renderTab.tsx b/packages/react-components/react-tabs/src/components/Tab/renderTab.tsx index 79e17c399a8bd..c14148fd57d8d 100644 --- a/packages/react-components/react-tabs/src/components/Tab/renderTab.tsx +++ b/packages/react-components/react-tabs/src/components/Tab/renderTab.tsx @@ -12,6 +12,9 @@ export const renderTab_unstable = (state: TabState) => { {slots.icon && } {!state.iconOnly && } + {!state.selected && !state.iconOnly && state.selectedTabContentClassName !== undefined && ( + + )} ); }; diff --git a/packages/react-components/react-tabs/src/components/Tab/useTab.ts b/packages/react-components/react-tabs/src/components/Tab/useTab.ts index 05c5fc4ad92fe..1b693970706b0 100644 --- a/packages/react-components/react-tabs/src/components/Tab/useTab.ts +++ b/packages/react-components/react-tabs/src/components/Tab/useTab.ts @@ -18,6 +18,7 @@ export const useTab_unstable = (props: TabProps, ref: React.Ref): T const { content, disabled: tabDisabled = false, icon, value } = props; const appearance = useContextSelector(TabListContext, ctx => ctx.appearance); + const consistentTabSize = useContextSelector(TabListContext, ctx => ctx.keepTabSizeConsistent); const listDisabled = useContextSelector(TabListContext, ctx => ctx.disabled); const selected = useContextSelector(TabListContext, ctx => ctx.selectedValue === value); const onRegister = useContextSelector(TabListContext, ctx => ctx.onRegister); @@ -64,6 +65,7 @@ export const useTab_unstable = (props: TabProps, ref: React.Ref): T iconOnly: Boolean(iconShorthand?.children && !contentShorthand.children), content: contentShorthand, appearance, + selectedTabContentClassName: consistentTabSize ? '' : undefined, disabled, selected, size, diff --git a/packages/react-components/react-tabs/src/components/Tab/useTabStyles.ts b/packages/react-components/react-tabs/src/components/Tab/useTabStyles.ts index 1da0c08b7683b..cf3cc1a863333 100644 --- a/packages/react-components/react-tabs/src/components/Tab/useTabStyles.ts +++ b/packages/react-components/react-tabs/src/components/Tab/useTabStyles.ts @@ -309,6 +309,8 @@ const useActiveIndicatorStyles = makeStyles({ */ const useIconStyles = makeStyles({ base: { + gridColumnStart: 1, + gridRowStart: 1, alignItems: 'center', display: 'inline-flex', justifyContent: 'center', @@ -341,6 +343,17 @@ const useContentStyles = makeStyles({ selected: { ...typographyStyles.body1Strong, }, + noIconBefore: { + gridColumnStart: 1, + gridRowStart: 1, + }, + iconBefore: { + gridColumnStart: 2, + gridRowStart: 1, + }, + placeholder: { + visibility: 'hidden', + }, }); /** @@ -384,6 +397,7 @@ export const useTabStyles_unstable = (state: TabState): TabState => { size === 'small' && (vertical ? activeIndicatorStyles.smallVertical : activeIndicatorStyles.smallHorizontal), selected && disabled && activeIndicatorStyles.disabled, + state.root.className, ); @@ -391,10 +405,23 @@ export const useTabStyles_unstable = (state: TabState): TabState => { 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.selectedTabContentClass !== undefined) { + state.selectedTabContentClass = mergeClasses( + tabClassNames.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, ); diff --git a/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts b/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts index 5e6173cc28fe8..ba31f73574b2b 100644 --- a/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts +++ b/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts @@ -47,6 +47,16 @@ export type TabListProps = ComponentProps & { */ appearance?: 'transparent' | 'subtle'; + /** + * Tab size may change between unselected and selected states. + * The default scenario is the selected tab has bold text. + * + * When true, this property causes unselected tabs to be the same size as when selected. + * This is done by rendering the content again with the selected styles and visibility:hidden. + * @default 'true' + */ + keepTabSizeConsistent?: boolean; + /** * The value of the tab to be selected by default. * Typically useful when the selectedValue is uncontrolled. @@ -83,7 +93,7 @@ export type TabListProps = ComponentProps & { }; export type TabListContextValue = Pick & - Required> & { + Required> & { /** A callback to allow a tab to register itself with the tab list. */ onRegister: RegisterTabEventHandler; diff --git a/packages/react-components/react-tabs/src/components/TabList/TabListContext.ts b/packages/react-components/react-tabs/src/components/TabList/TabListContext.ts index ded6bdbf36aca..17c921b179db6 100644 --- a/packages/react-components/react-tabs/src/components/TabList/TabListContext.ts +++ b/packages/react-components/react-tabs/src/components/TabList/TabListContext.ts @@ -5,6 +5,7 @@ import { TabListContextValue } from './TabList.types'; // eslint-disable-next-line @fluentui/no-context-default-value export const TabListContext: Context = createContext({ appearance: 'transparent', + keepTabSizeConsistent: true, disabled: false, selectedValue: undefined, onRegister: () => { diff --git a/packages/react-components/react-tabs/src/components/TabList/useTabList.ts b/packages/react-components/react-tabs/src/components/TabList/useTabList.ts index 102f1f425d5be..d872b66cfb0d2 100644 --- a/packages/react-components/react-tabs/src/components/TabList/useTabList.ts +++ b/packages/react-components/react-tabs/src/components/TabList/useTabList.ts @@ -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): TabListState => { - const { appearance = 'transparent', disabled = false, onTabSelect, size = 'medium', vertical = false } = props; + const { + appearance = 'transparent', + keepTabSizeConsistent = true, + disabled = false, + onTabSelect, + size = 'medium', + vertical = false, + } = props; const innerRef = React.useRef(null); @@ -81,6 +88,7 @@ export const useTabList_unstable = (props: TabListProps, ref: React.Ref Date: Fri, 4 Nov 2022 15:15:50 -0700 Subject: [PATCH 2/5] Update for naming and tests --- .../react-tabs/etc/react-tabs.api.md | 4 ++- .../src/components/Tab/Tab.types.ts | 2 +- .../src/components/Tab/renderTab.tsx | 4 +-- .../react-tabs/src/components/Tab/useTab.ts | 4 +-- .../src/components/Tab/useTabStyles.ts | 10 ++++-- .../src/components/TabList/TabList.types.ts | 11 +++--- .../src/components/TabList/TabListContext.ts | 2 +- .../__snapshots__/TabList.test.tsx.snap | 35 +++++++++++++++++++ .../src/components/TabList/useTabList.ts | 4 +-- .../TabList/useTabListContextValues.tsx | 4 +-- 10 files changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/react-components/react-tabs/etc/react-tabs.api.md b/packages/react-components/react-tabs/etc/react-tabs.api.md index f0fbcd191e0bf..bd79b980a82f8 100644 --- a/packages/react-components/react-tabs/etc/react-tabs.api.md +++ b/packages/react-components/react-tabs/etc/react-tabs.api.md @@ -44,7 +44,7 @@ export const TabList: ForwardRefComponent; export const tabListClassNames: SlotClassNames; // @public (undocumented) -export type TabListContextValue = Pick & Required> & { +export type TabListContextValue = Pick & Required> & { onRegister: RegisterTabEventHandler; onUnregister: RegisterTabEventHandler; onSelect: SelectTabEventHandler; @@ -63,6 +63,7 @@ export type TabListContextValues = { // @public export type TabListProps = ComponentProps & { appearance?: 'transparent' | 'subtle'; + reserveSelectedTabSpace?: boolean; defaultSelectedValue?: TabValue; disabled?: boolean; onTabSelect?: SelectTabEventHandler; @@ -103,6 +104,7 @@ export type TabState = ComponentState & Pick & Requ appearance?: 'transparent' | 'subtle'; iconOnly: boolean; selected: boolean; + contentReservedSpaceClassName?: string; size: 'small' | 'medium'; vertical: boolean; }; diff --git a/packages/react-components/react-tabs/src/components/Tab/Tab.types.ts b/packages/react-components/react-tabs/src/components/Tab/Tab.types.ts index 8eead270a03c4..903d12b268942 100644 --- a/packages/react-components/react-tabs/src/components/Tab/Tab.types.ts +++ b/packages/react-components/react-tabs/src/components/Tab/Tab.types.ts @@ -60,7 +60,7 @@ export type TabState = ComponentState & * When defined, tab content with selected style is rendered hidden to reserve space. * This keeps consistent content size between unselected and selected states. */ - selectedTabContentClassName?: string; + contentReservedSpaceClassName?: string; /** * A tab can be either 'small' or 'medium' size. */ diff --git a/packages/react-components/react-tabs/src/components/Tab/renderTab.tsx b/packages/react-components/react-tabs/src/components/Tab/renderTab.tsx index c14148fd57d8d..3d8b88ad1f0c4 100644 --- a/packages/react-components/react-tabs/src/components/Tab/renderTab.tsx +++ b/packages/react-components/react-tabs/src/components/Tab/renderTab.tsx @@ -12,8 +12,8 @@ export const renderTab_unstable = (state: TabState) => { {slots.icon && } {!state.iconOnly && } - {!state.selected && !state.iconOnly && state.selectedTabContentClassName !== undefined && ( - + {!state.selected && !state.iconOnly && state.contentReservedSpaceClassName !== undefined && ( + )} ); diff --git a/packages/react-components/react-tabs/src/components/Tab/useTab.ts b/packages/react-components/react-tabs/src/components/Tab/useTab.ts index 1b693970706b0..3393132388adc 100644 --- a/packages/react-components/react-tabs/src/components/Tab/useTab.ts +++ b/packages/react-components/react-tabs/src/components/Tab/useTab.ts @@ -18,7 +18,7 @@ export const useTab_unstable = (props: TabProps, ref: React.Ref): T const { content, disabled: tabDisabled = false, icon, value } = props; const appearance = useContextSelector(TabListContext, ctx => ctx.appearance); - const consistentTabSize = useContextSelector(TabListContext, ctx => ctx.keepTabSizeConsistent); + 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); @@ -65,7 +65,7 @@ export const useTab_unstable = (props: TabProps, ref: React.Ref): T iconOnly: Boolean(iconShorthand?.children && !contentShorthand.children), content: contentShorthand, appearance, - selectedTabContentClassName: consistentTabSize ? '' : undefined, + contentReservedSpaceClassName: reserveSelectedTabSpace ? '' : undefined, disabled, selected, size, diff --git a/packages/react-components/react-tabs/src/components/Tab/useTabStyles.ts b/packages/react-components/react-tabs/src/components/Tab/useTabStyles.ts index cf3cc1a863333..a1ef4f2b1c2aa 100644 --- a/packages/react-components/react-tabs/src/components/Tab/useTabStyles.ts +++ b/packages/react-components/react-tabs/src/components/Tab/useTabStyles.ts @@ -12,6 +12,10 @@ export const tabClassNames: SlotClassNames = { content: 'fui-Tab__content', }; +const reservedSpaceClassNames = { + content: 'fui-Tab__content--reserved-space', +}; + /** * Styles for the root slot */ @@ -406,9 +410,9 @@ export const useTabStyles_unstable = (state: TabState): TabState => { } // This needs to be before state.content.className is updated - if (state.selectedTabContentClass !== undefined) { - state.selectedTabContentClass = mergeClasses( - tabClassNames.content, + if (state.contentReservedSpaceClassName !== undefined) { + state.contentReservedSpaceClassName = mergeClasses( + reservedSpaceClassNames.content, contentStyles.base, contentStyles.selected, state.icon ? contentStyles.iconBefore : contentStyles.noIconBefore, diff --git a/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts b/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts index ba31f73574b2b..bc817c390b358 100644 --- a/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts +++ b/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts @@ -49,13 +49,12 @@ export type TabListProps = ComponentProps & { /** * Tab size may change between unselected and selected states. - * The default scenario is the selected tab has bold text. + * The default scenario is a selected tab has bold text. * - * When true, this property causes unselected tabs to be the same size as when selected. - * This is done by rendering the content again with the selected styles and visibility:hidden. + * When true, this property requests tabs be the same size whether unselected or selected. * @default 'true' */ - keepTabSizeConsistent?: boolean; + reserveSelectedTabSpace?: boolean; /** * The value of the tab to be selected by default. @@ -92,8 +91,8 @@ export type TabListProps = ComponentProps & { vertical?: boolean; }; -export type TabListContextValue = Pick & - Required> & { +export type TabListContextValue = Pick & + Required> & { /** A callback to allow a tab to register itself with the tab list. */ onRegister: RegisterTabEventHandler; diff --git a/packages/react-components/react-tabs/src/components/TabList/TabListContext.ts b/packages/react-components/react-tabs/src/components/TabList/TabListContext.ts index 17c921b179db6..f9973753c2b0e 100644 --- a/packages/react-components/react-tabs/src/components/TabList/TabListContext.ts +++ b/packages/react-components/react-tabs/src/components/TabList/TabListContext.ts @@ -5,7 +5,7 @@ import { TabListContextValue } from './TabList.types'; // eslint-disable-next-line @fluentui/no-context-default-value export const TabListContext: Context = createContext({ appearance: 'transparent', - keepTabSizeConsistent: true, + reserveSelectedTabSpace: true, disabled: false, selectedValue: undefined, onRegister: () => { diff --git a/packages/react-components/react-tabs/src/components/TabList/__snapshots__/TabList.test.tsx.snap b/packages/react-components/react-tabs/src/components/TabList/__snapshots__/TabList.test.tsx.snap index 8c2fb54ef9744..23713405d80eb 100644 --- a/packages/react-components/react-tabs/src/components/TabList/__snapshots__/TabList.test.tsx.snap +++ b/packages/react-components/react-tabs/src/components/TabList/__snapshots__/TabList.test.tsx.snap @@ -19,6 +19,11 @@ exports[`TabList renders tabs when disabled 1`] = ` > First + + First + @@ -70,6 +80,11 @@ exports[`TabList renders tabs with default selected tab 1`] = ` > First + + First + @@ -133,6 +153,11 @@ exports[`TabList renders with tabs 1`] = ` > First + + First + diff --git a/packages/react-components/react-tabs/src/components/TabList/useTabList.ts b/packages/react-components/react-tabs/src/components/TabList/useTabList.ts index d872b66cfb0d2..4feebdc563d48 100644 --- a/packages/react-components/react-tabs/src/components/TabList/useTabList.ts +++ b/packages/react-components/react-tabs/src/components/TabList/useTabList.ts @@ -21,7 +21,7 @@ import { TabValue } from '../Tab/Tab.types'; export const useTabList_unstable = (props: TabListProps, ref: React.Ref): TabListState => { const { appearance = 'transparent', - keepTabSizeConsistent = true, + reserveSelectedTabSpace = true, disabled = false, onTabSelect, size = 'medium', @@ -88,7 +88,7 @@ export const useTabList_unstable = (props: TabListProps, ref: React.Ref Date: Fri, 4 Nov 2022 15:18:25 -0700 Subject: [PATCH 3/5] Rename update --- .../src/components/TabList/useTabListContextValues.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-components/react-tabs/src/components/TabList/useTabListContextValues.tsx b/packages/react-components/react-tabs/src/components/TabList/useTabListContextValues.tsx index ad0ec37d4f3b8..cc170c46f6f24 100644 --- a/packages/react-components/react-tabs/src/components/TabList/useTabListContextValues.tsx +++ b/packages/react-components/react-tabs/src/components/TabList/useTabListContextValues.tsx @@ -3,7 +3,7 @@ import { TabListContextValue, TabListContextValues, TabListState } from './TabLi export function useTabListContextValues(state: TabListState): TabListContextValues { const { appearance, - reserveSelectedTabSpace: keepTabSizeConsistent, + reserveSelectedTabSpace, disabled, selectedValue: selectedKey, onRegister, @@ -16,7 +16,7 @@ export function useTabListContextValues(state: TabListState): TabListContextValu const tabList: TabListContextValue = { appearance, - reserveSelectedTabSpace: keepTabSizeConsistent, + reserveSelectedTabSpace, disabled, selectedValue: selectedKey, onSelect, From ec84f2044412d2c1af165bc9b16014f293c1bf54 Mon Sep 17 00:00:00 2001 From: "Geoff Cox (Microsoft)" Date: Sat, 5 Nov 2022 10:42:47 -0700 Subject: [PATCH 4/5] Update packages/react-components/react-tabs/src/components/TabList/TabList.types.ts Co-authored-by: Sean Monahan --- .../react-tabs/src/components/TabList/TabList.types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts b/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts index bc817c390b358..3df9c08a436aa 100644 --- a/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts +++ b/packages/react-components/react-tabs/src/components/TabList/TabList.types.ts @@ -52,7 +52,7 @@ export type TabListProps = ComponentProps & { * 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' + * @default true */ reserveSelectedTabSpace?: boolean; From 068303069090ed6c3e7adf61898924bce0efa05d Mon Sep 17 00:00:00 2001 From: GeoffCoxMSFT Date: Mon, 7 Nov 2022 11:16:59 -0800 Subject: [PATCH 5/5] yarn change --- ...ui-react-tabs-5857fdb2-8ae6-4796-9d11-757f662e5046.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@fluentui-react-tabs-5857fdb2-8ae6-4796-9d11-757f662e5046.json diff --git a/change/@fluentui-react-tabs-5857fdb2-8ae6-4796-9d11-757f662e5046.json b/change/@fluentui-react-tabs-5857fdb2-8ae6-4796-9d11-757f662e5046.json new file mode 100644 index 0000000000000..44215836ff105 --- /dev/null +++ b/change/@fluentui-react-tabs-5857fdb2-8ae6-4796-9d11-757f662e5046.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "Added support for reserving space for selected state", + "packageName": "@fluentui/react-tabs", + "email": "gcox@microsoft.com", + "dependentChangeType": "patch" +}