diff --git a/change/@fluentui-react-badge-eff39565-e2dd-45e9-93f5-ed2afdd31651.json b/change/@fluentui-react-badge-eff39565-e2dd-45e9-93f5-ed2afdd31651.json new file mode 100644 index 00000000000000..0af378b18fa963 --- /dev/null +++ b/change/@fluentui-react-badge-eff39565-e2dd-45e9-93f5-ed2afdd31651.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: add base hooks for Badge", + "packageName": "@fluentui/react-badge", + "email": "dmytrokirpa@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-badge/library/etc/react-badge.api.md b/packages/react-components/react-badge/library/etc/react-badge.api.md index cc6b4b8cc33216..a10cbf9a2d43bf 100644 --- a/packages/react-components/react-badge/library/etc/react-badge.api.md +++ b/packages/react-components/react-badge/library/etc/react-badge.api.md @@ -15,6 +15,12 @@ import type { SlotClassNames } from '@fluentui/react-utilities'; // @public export const Badge: ForwardRefComponent; +// @public (undocumented) +export type BadgeBaseProps = Omit; + +// @public (undocumented) +export type BadgeBaseState = Omit; + // @public (undocumented) export const badgeClassNames: SlotClassNames; @@ -39,6 +45,12 @@ export type BadgeState = ComponentState & Required; +// @public (undocumented) +export type CounterBadgeBaseProps = Omit; + +// @public (undocumented) +export type CounterBadgeBaseState = Omit; + // @public (undocumented) export const counterBadgeClassNames: SlotClassNames; @@ -71,6 +83,12 @@ export const presenceAwayRegular: Record; +// @public (undocumented) +export type PresenceBadgeBaseProps = Omit; + +// @public (undocumented) +export type PresenceBadgeBaseState = Omit; + // @public (undocumented) export const presenceBadgeClassNames: SlotClassNames; @@ -108,23 +126,32 @@ export const presenceOofRegular: Record; // @public (undocumented) -export const renderBadge_unstable: (state: BadgeState) => JSXElement; +export const renderBadge_unstable: (state: BadgeBaseState) => JSXElement; // @public export const useBadge_unstable: (props: BadgeProps, ref: React_2.Ref) => BadgeState; +// @public +export const useBadgeBase_unstable: (props: BadgeBaseProps, ref: React_2.Ref) => BadgeBaseState; + // @public export const useBadgeStyles_unstable: (state: BadgeState) => BadgeState; // @public export const useCounterBadge_unstable: (props: CounterBadgeProps, ref: React_2.Ref) => CounterBadgeState; +// @public +export const useCounterBadgeBase_unstable: (props: CounterBadgeBaseProps, ref: React_2.Ref) => CounterBadgeBaseState; + // @public export const useCounterBadgeStyles_unstable: (state: CounterBadgeState) => CounterBadgeState; // @public export const usePresenceBadge_unstable: (props: PresenceBadgeProps, ref: React_2.Ref) => PresenceBadgeState; +// @public +export const usePresenceBadgeBase_unstable: (props: PresenceBadgeBaseProps, ref: React_2.Ref) => PresenceBadgeBaseState; + // @public export const usePresenceBadgeStyles_unstable: (state: PresenceBadgeState) => PresenceBadgeState; diff --git a/packages/react-components/react-badge/library/src/Badge.ts b/packages/react-components/react-badge/library/src/Badge.ts index 56de48b1a2b87e..9e98c073461461 100644 --- a/packages/react-components/react-badge/library/src/Badge.ts +++ b/packages/react-components/react-badge/library/src/Badge.ts @@ -1,8 +1,9 @@ -export type { BadgeProps, BadgeSlots, BadgeState } from './components/Badge/index'; +export type { BadgeBaseProps, BadgeProps, BadgeSlots, BadgeBaseState, BadgeState } from './components/Badge/index'; export { Badge, badgeClassNames, renderBadge_unstable, useBadgeStyles_unstable, useBadge_unstable, + useBadgeBase_unstable, } from './components/Badge/index'; diff --git a/packages/react-components/react-badge/library/src/CounterBadge.ts b/packages/react-components/react-badge/library/src/CounterBadge.ts index 7fad9f1447d63c..612489b253c0ec 100644 --- a/packages/react-components/react-badge/library/src/CounterBadge.ts +++ b/packages/react-components/react-badge/library/src/CounterBadge.ts @@ -1,7 +1,13 @@ -export type { CounterBadgeProps, CounterBadgeState } from './components/CounterBadge/index'; +export type { + CounterBadgeProps, + CounterBadgeState, + CounterBadgeBaseProps, + CounterBadgeBaseState, +} from './components/CounterBadge/index'; export { CounterBadge, counterBadgeClassNames, useCounterBadgeStyles_unstable, useCounterBadge_unstable, + useCounterBadgeBase_unstable, } from './components/CounterBadge/index'; diff --git a/packages/react-components/react-badge/library/src/PresenceBadge.ts b/packages/react-components/react-badge/library/src/PresenceBadge.ts index 84d192cfde4958..1a35068413ace7 100644 --- a/packages/react-components/react-badge/library/src/PresenceBadge.ts +++ b/packages/react-components/react-badge/library/src/PresenceBadge.ts @@ -1,4 +1,10 @@ -export type { PresenceBadgeProps, PresenceBadgeState, PresenceBadgeStatus } from './components/PresenceBadge/index'; +export type { + PresenceBadgeProps, + PresenceBadgeState, + PresenceBadgeStatus, + PresenceBadgeBaseProps, + PresenceBadgeBaseState, +} from './components/PresenceBadge/index'; export { PresenceBadge, presenceAvailableFilled, @@ -15,4 +21,5 @@ export { presenceUnknownRegular, usePresenceBadgeStyles_unstable, usePresenceBadge_unstable, + usePresenceBadgeBase_unstable, } from './components/PresenceBadge/index'; diff --git a/packages/react-components/react-badge/library/src/components/Badge/Badge.types.ts b/packages/react-components/react-badge/library/src/components/Badge/Badge.types.ts index d9e325415df1e6..6cb555497ad5ed 100644 --- a/packages/react-components/react-badge/library/src/components/Badge/Badge.types.ts +++ b/packages/react-components/react-badge/library/src/components/Badge/Badge.types.ts @@ -41,3 +41,7 @@ export type BadgeProps = Omit, 'color'> & { export type BadgeState = ComponentState & Required>; + +export type BadgeBaseProps = Omit; + +export type BadgeBaseState = Omit; diff --git a/packages/react-components/react-badge/library/src/components/Badge/index.ts b/packages/react-components/react-badge/library/src/components/Badge/index.ts index 94552cb73b051f..03545bbfacb0ae 100644 --- a/packages/react-components/react-badge/library/src/components/Badge/index.ts +++ b/packages/react-components/react-badge/library/src/components/Badge/index.ts @@ -1,6 +1,6 @@ export { Badge } from './Badge'; // Explicit exports to omit BadgeCommons -export type { BadgeProps, BadgeSlots, BadgeState } from './Badge.types'; +export type { BadgeBaseProps, BadgeBaseState, BadgeProps, BadgeSlots, BadgeState } from './Badge.types'; export { renderBadge_unstable } from './renderBadge'; -export { useBadge_unstable } from './useBadge'; +export { useBadge_unstable, useBadgeBase_unstable } from './useBadge'; export { badgeClassNames, useBadgeStyles_unstable } from './useBadgeStyles.styles'; diff --git a/packages/react-components/react-badge/library/src/components/Badge/renderBadge.tsx b/packages/react-components/react-badge/library/src/components/Badge/renderBadge.tsx index f19feddefa98f7..9f4a5b077b8929 100644 --- a/packages/react-components/react-badge/library/src/components/Badge/renderBadge.tsx +++ b/packages/react-components/react-badge/library/src/components/Badge/renderBadge.tsx @@ -4,9 +4,9 @@ import { assertSlots } from '@fluentui/react-utilities'; import type { JSXElement } from '@fluentui/react-utilities'; -import type { BadgeState, BadgeSlots } from './Badge.types'; +import type { BadgeBaseState, BadgeSlots } from './Badge.types'; -export const renderBadge_unstable = (state: BadgeState): JSXElement => { +export const renderBadge_unstable = (state: BadgeBaseState): JSXElement => { assertSlots(state); return ( diff --git a/packages/react-components/react-badge/library/src/components/Badge/useBadge.ts b/packages/react-components/react-badge/library/src/components/Badge/useBadge.ts index 2159c9679333d8..53727729a233b8 100644 --- a/packages/react-components/react-badge/library/src/components/Badge/useBadge.ts +++ b/packages/react-components/react-badge/library/src/components/Badge/useBadge.ts @@ -1,41 +1,48 @@ +'use client'; + import * as React from 'react'; import { getIntrinsicElementProps, slot } from '@fluentui/react-utilities'; -import type { BadgeProps, BadgeState } from './Badge.types'; +import type { BadgeBaseProps, BadgeBaseState, BadgeProps, BadgeState } from './Badge.types'; /** * Returns the props and state required to render the component */ export const useBadge_unstable = (props: BadgeProps, ref: React.Ref): BadgeState => { - const { - shape = 'circular', - size = 'medium', - iconPosition = 'before', - appearance = 'filled', - color = 'brand', - } = props; + const { shape = 'circular', size = 'medium', appearance = 'filled', color = 'brand', ...badgeProps } = props; + + const state = useBadgeBase_unstable(badgeProps, ref as React.Ref); - const state: BadgeState = { + return { + ...state, shape, size, - iconPosition, appearance, color, + }; +}; + +/** + * Base hook for Badge component, which manages state related to slots structure and ARIA attributes. + * + * @param props - User provided props to the Badge component. + * @param ref - User provided ref to be passed to the Badge component. + */ +export const useBadgeBase_unstable = (props: BadgeBaseProps, ref: React.Ref): BadgeBaseState => { + const { iconPosition = 'before' } = props; + + return { + iconPosition, components: { root: 'div', icon: 'span', }, root: slot.always( getIntrinsicElementProps('div', { - // FIXME: - // `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement` - // but since it would be a breaking change to fix it, we are casting ref to it's proper type - ref: ref as React.Ref, + ref, ...props, }), { elementType: 'div' }, ), icon: slot.optional(props.icon, { elementType: 'span' }), }; - - return state; }; diff --git a/packages/react-components/react-badge/library/src/components/CounterBadge/CounterBadge.types.ts b/packages/react-components/react-badge/library/src/components/CounterBadge/CounterBadge.types.ts index 03b0bf09670835..f3b64187b30bd0 100644 --- a/packages/react-components/react-badge/library/src/components/CounterBadge/CounterBadge.types.ts +++ b/packages/react-components/react-badge/library/src/components/CounterBadge/CounterBadge.types.ts @@ -49,3 +49,7 @@ export type CounterBadgeProps = Omit & Required>; + +export type CounterBadgeBaseProps = Omit; + +export type CounterBadgeBaseState = Omit; diff --git a/packages/react-components/react-badge/library/src/components/CounterBadge/index.ts b/packages/react-components/react-badge/library/src/components/CounterBadge/index.ts index 2c149ed8d8fbd7..16620efaf677f5 100644 --- a/packages/react-components/react-badge/library/src/components/CounterBadge/index.ts +++ b/packages/react-components/react-badge/library/src/components/CounterBadge/index.ts @@ -1,4 +1,9 @@ export { CounterBadge } from './CounterBadge'; -export type { CounterBadgeProps, CounterBadgeState } from './CounterBadge.types'; -export { useCounterBadge_unstable } from './useCounterBadge'; +export type { + CounterBadgeBaseProps, + CounterBadgeBaseState, + CounterBadgeProps, + CounterBadgeState, +} from './CounterBadge.types'; +export { useCounterBadge_unstable, useCounterBadgeBase_unstable } from './useCounterBadge'; export { counterBadgeClassNames, useCounterBadgeStyles_unstable } from './useCounterBadgeStyles.styles'; diff --git a/packages/react-components/react-badge/library/src/components/CounterBadge/useCounterBadge.ts b/packages/react-components/react-badge/library/src/components/CounterBadge/useCounterBadge.ts index a4132f5031134c..bd2dc2180ed14b 100644 --- a/packages/react-components/react-badge/library/src/components/CounterBadge/useCounterBadge.ts +++ b/packages/react-components/react-badge/library/src/components/CounterBadge/useCounterBadge.ts @@ -1,27 +1,45 @@ 'use client'; import * as React from 'react'; -import type { BadgeState } from '../Badge/index'; -import { useBadge_unstable } from '../Badge/index'; -import type { CounterBadgeProps, CounterBadgeState } from './CounterBadge.types'; +import { useBadgeBase_unstable } from '../Badge/index'; +import type { + CounterBadgeBaseProps, + CounterBadgeBaseState, + CounterBadgeProps, + CounterBadgeState, +} from './CounterBadge.types'; /** * Returns the props and state required to render the component */ export const useCounterBadge_unstable = (props: CounterBadgeProps, ref: React.Ref): CounterBadgeState => { - const { - shape = 'circular', - appearance = 'filled', - showZero = false, - overflowCount = 99, - count = 0, - dot = false, - } = props; - - const state: CounterBadgeState = { - ...(useBadge_unstable(props, ref) as Pick), + const { shape = 'circular', appearance = 'filled', color = 'brand', size = 'medium', ...counterBadgeProps } = props; + + const state = useCounterBadgeBase_unstable(counterBadgeProps, ref); + + return { + ...state, shape, appearance, + color, + size, + }; +}; + +/** + * Base hook for CounterBadge component, which manages state related to slots structure and counter logic. + * + * @param props - User provided props to the CounterBadge component. + * @param ref - User provided ref to be passed to the CounterBadge component. + */ +export const useCounterBadgeBase_unstable = ( + props: CounterBadgeBaseProps, + ref: React.Ref, +): CounterBadgeBaseState => { + const { showZero = false, overflowCount = 99, count = 0, dot = false, ...badgeProps } = props; + + const state: CounterBadgeBaseState = { + ...useBadgeBase_unstable(badgeProps, ref as React.Ref), showZero, count, dot, diff --git a/packages/react-components/react-badge/library/src/components/PresenceBadge/PresenceBadge.types.ts b/packages/react-components/react-badge/library/src/components/PresenceBadge/PresenceBadge.types.ts index 176e3e7e8ce89d..501d0a9064d007 100644 --- a/packages/react-components/react-badge/library/src/components/PresenceBadge/PresenceBadge.types.ts +++ b/packages/react-components/react-badge/library/src/components/PresenceBadge/PresenceBadge.types.ts @@ -30,3 +30,7 @@ export type PresenceBadgeProps = Omit & BadgeState & Required>; + +export type PresenceBadgeBaseProps = Omit; + +export type PresenceBadgeBaseState = Omit; diff --git a/packages/react-components/react-badge/library/src/components/PresenceBadge/index.ts b/packages/react-components/react-badge/library/src/components/PresenceBadge/index.ts index ca264ac9288d3c..b4776ce4fc7588 100644 --- a/packages/react-components/react-badge/library/src/components/PresenceBadge/index.ts +++ b/packages/react-components/react-badge/library/src/components/PresenceBadge/index.ts @@ -1,6 +1,12 @@ export { PresenceBadge } from './PresenceBadge'; -export type { PresenceBadgeProps, PresenceBadgeState, PresenceBadgeStatus } from './PresenceBadge.types'; -export { usePresenceBadge_unstable } from './usePresenceBadge'; +export type { + PresenceBadgeBaseProps, + PresenceBadgeBaseState, + PresenceBadgeProps, + PresenceBadgeState, + PresenceBadgeStatus, +} from './PresenceBadge.types'; +export { usePresenceBadge_unstable, usePresenceBadgeBase_unstable } from './usePresenceBadge'; export { presenceBadgeClassNames, usePresenceBadgeStyles_unstable } from './usePresenceBadgeStyles.styles'; export { presenceAvailableFilled, diff --git a/packages/react-components/react-badge/library/src/components/PresenceBadge/usePresenceBadge.tsx b/packages/react-components/react-badge/library/src/components/PresenceBadge/usePresenceBadge.tsx index c23befefd56b38..60e27cef7b3b3c 100644 --- a/packages/react-components/react-badge/library/src/components/PresenceBadge/usePresenceBadge.tsx +++ b/packages/react-components/react-badge/library/src/components/PresenceBadge/usePresenceBadge.tsx @@ -14,8 +14,13 @@ import { presenceOofRegular, presenceUnknownRegular, } from './presenceIcons'; -import { useBadge_unstable } from '../Badge/index'; -import type { PresenceBadgeProps, PresenceBadgeState } from './PresenceBadge.types'; +import { useBadgeBase_unstable } from '../Badge/index'; +import type { + PresenceBadgeBaseProps, + PresenceBadgeBaseState, + PresenceBadgeProps, + PresenceBadgeState, +} from './PresenceBadge.types'; const iconMap = (status: PresenceBadgeState['status'], outOfOffice: boolean, size: PresenceBadgeState['size']) => { switch (status) { @@ -56,29 +61,56 @@ export const usePresenceBadge_unstable = ( props: PresenceBadgeProps, ref: React.Ref, ): PresenceBadgeState => { - const { size = 'medium', status = 'available', outOfOffice = false } = props; - - const statusText = DEFAULT_STRINGS[status]; - const oofText = props.outOfOffice && props.status !== 'out-of-office' ? ` ${DEFAULT_STRINGS['out-of-office']}` : ''; + const { size = 'medium', status = 'available', outOfOffice = false, ...baseProps } = props; const IconElement = iconMap(status, outOfOffice, size); const state: PresenceBadgeState = { - ...useBadge_unstable( + ...usePresenceBadgeBase_unstable(baseProps, ref), + appearance: 'filled', + color: 'brand', + shape: 'circular', + size, + status, + outOfOffice, + }; + + if (state.icon) { + state.icon.children ??= ; + } + + return state; +}; + +/** + * Base hook for PresenceBadge component, which manages state related to presence status and ARIA attributes. + * Note: size is excluded from BaseProps as it is a design prop; icon selection uses the 'medium' size default. + * To render size-specific icons, use the full usePresenceBadge_unstable hook. + * + * @param props - User provided props to the PresenceBadge component. + * @param ref - User provided ref to be passed to the PresenceBadge component. + */ +export const usePresenceBadgeBase_unstable = ( + props: PresenceBadgeBaseProps, + ref: React.Ref, +): PresenceBadgeBaseState => { + const { status = 'available', outOfOffice = false } = props; + + const statusText = DEFAULT_STRINGS[status]; + const oofText = props.outOfOffice && props.status !== 'out-of-office' ? ` ${DEFAULT_STRINGS['out-of-office']}` : ''; + + const state: PresenceBadgeBaseState = { + ...useBadgeBase_unstable( { 'aria-label': statusText + oofText, role: 'img', ...props, - size, icon: slot.optional(props.icon, { - defaultProps: { - children: IconElement ? : null, - }, renderByDefault: true, elementType: 'span', }), }, - ref, + ref as React.Ref, ), status, outOfOffice, diff --git a/packages/react-components/react-badge/library/src/index.ts b/packages/react-components/react-badge/library/src/index.ts index a7ebb34ef0c591..58c118a9f8d7fe 100644 --- a/packages/react-components/react-badge/library/src/index.ts +++ b/packages/react-components/react-badge/library/src/index.ts @@ -1,10 +1,18 @@ -export { Badge, badgeClassNames, renderBadge_unstable, useBadgeStyles_unstable, useBadge_unstable } from './Badge'; -export type { BadgeProps, BadgeSlots, BadgeState } from './Badge'; +export { + Badge, + badgeClassNames, + renderBadge_unstable, + useBadgeStyles_unstable, + useBadge_unstable, + useBadgeBase_unstable, +} from './Badge'; +export type { BadgeProps, BadgeSlots, BadgeState, BadgeBaseProps, BadgeBaseState } from './Badge'; export { PresenceBadge, presenceBadgeClassNames, usePresenceBadgeStyles_unstable, usePresenceBadge_unstable, + usePresenceBadgeBase_unstable, presenceAwayRegular, presenceAwayFilled, presenceAvailableRegular, @@ -17,11 +25,23 @@ export { presenceOfflineRegular, presenceUnknownRegular, } from './PresenceBadge'; -export type { PresenceBadgeProps, PresenceBadgeState, PresenceBadgeStatus } from './PresenceBadge'; +export type { + PresenceBadgeProps, + PresenceBadgeState, + PresenceBadgeStatus, + PresenceBadgeBaseProps, + PresenceBadgeBaseState, +} from './PresenceBadge'; export { CounterBadge, counterBadgeClassNames, useCounterBadgeStyles_unstable, useCounterBadge_unstable, + useCounterBadgeBase_unstable, +} from './CounterBadge'; +export type { + CounterBadgeProps, + CounterBadgeState, + CounterBadgeBaseProps, + CounterBadgeBaseState, } from './CounterBadge'; -export type { CounterBadgeProps, CounterBadgeState } from './CounterBadge';