22
33import * as React from 'react' ;
44import { getIntrinsicElementProps , useMergedRefs , slot } from '@fluentui/react-utilities' ;
5- import { useFocusableGroup , useFocusWithin } from '@fluentui/react-tabster' ;
5+ import { useFocusableGroup , useFocusFinders , useFocusWithin } from '@fluentui/react-tabster' ;
66
7- import type { CardBaseProps , CardBaseState , CardProps , CardState } from './Card.types' ;
7+ import type { CardBaseProps , CardBaseState , CardOnSelectionChangeEvent , CardProps , CardState } from './Card.types' ;
88import { useCardSelectable } from './useCardSelectable' ;
99import { cardContextDefaultValue } from './CardContext' ;
1010
@@ -15,58 +15,30 @@ const focusMap = {
1515 'tab-only' : 'unlimited' ,
1616} as const ;
1717
18+ const interactiveEventProps = [
19+ 'onClick' ,
20+ 'onDoubleClick' ,
21+ 'onMouseUp' ,
22+ 'onMouseDown' ,
23+ 'onPointerUp' ,
24+ 'onPointerDown' ,
25+ 'onTouchStart' ,
26+ 'onTouchEnd' ,
27+ 'onDragStart' ,
28+ 'onDragEnd' ,
29+ ] as ( keyof React . HTMLAttributes < HTMLElement > ) [ ] ;
30+
1831/**
19- * Create the state for interactive cards.
20- *
21- * This internal hook defines if the card is interactive
22- * and control focus properties based on that.
23- *
24- * @param props - props from this instance of Card
32+ * Compute whether a Card is interactive based on the presence of pointer/mouse
33+ * event props and the disabled flag. This intentionally does not depend on
34+ * focus management utilities so it can be used from headless contexts.
2535 */
26- const useCardInteractive = ( { focusMode : initialFocusMode , disabled = false , ...props } : CardProps ) => {
27- const interactive = (
28- [
29- 'onClick' ,
30- 'onDoubleClick' ,
31- 'onMouseUp' ,
32- 'onMouseDown' ,
33- 'onPointerUp' ,
34- 'onPointerDown' ,
35- 'onTouchStart' ,
36- 'onTouchEnd' ,
37- 'onDragStart' ,
38- 'onDragEnd' ,
39- ] as ( keyof React . HTMLAttributes < HTMLElement > ) [ ]
40- ) . some ( prop => props [ prop ] ) ;
41-
42- // default focusMode to tab-only when interactive, and off when not
43- const focusMode = initialFocusMode ?? ( interactive ? 'no-tab' : 'off' ) ;
44-
45- const groupperAttrs = useFocusableGroup ( {
46- tabBehavior : focusMap [ focusMode ] ,
47- } ) ;
48-
49- if ( disabled ) {
50- return {
51- interactive : false ,
52- focusAttributes : null ,
53- } ;
54- }
55-
56- if ( focusMode === 'off' ) {
57- return {
58- interactive,
59- focusAttributes : null ,
60- } ;
36+ const computeInteractive = ( props : CardProps ) : boolean => {
37+ if ( props . disabled ) {
38+ return false ;
6139 }
6240
63- return {
64- interactive,
65- focusAttributes : {
66- ...groupperAttrs ,
67- tabIndex : 0 ,
68- } ,
69- } ;
41+ return interactiveEventProps . some ( prop => props [ prop ] !== undefined ) ;
7042} ;
7143
7244/**
@@ -80,7 +52,50 @@ const useCardInteractive = ({ focusMode: initialFocusMode, disabled = false, ...
8052 */
8153export const useCard_unstable = ( props : CardProps , ref : React . Ref < HTMLDivElement > ) : CardState => {
8254 const { appearance = 'filled' , orientation = 'vertical' , size = 'medium' , ...cardProps } = props ;
83- const state = useCardBase_unstable ( cardProps , ref ) ;
55+ const { disabled = false , focusMode : focusModeProp } = props ;
56+
57+ // Focus-within ref drives the styled focus outline; merged with the user ref
58+ // before being passed down so the base hook does not depend on react-tabster.
59+ const focusWithinRef = useFocusWithin < HTMLDivElement > ( ) ;
60+ const cardRef = useMergedRefs ( focusWithinRef , ref ) ;
61+
62+ // Focus-aware predicate that prevents toggling the selection when the user
63+ // interacts with an inner focusable element.
64+ const { findAllFocusable } = useFocusFinders ( ) ;
65+ const shouldRestrictTriggerAction = React . useCallback (
66+ ( event : CardOnSelectionChangeEvent ) => {
67+ if ( ! focusWithinRef . current ) {
68+ return false ;
69+ }
70+
71+ const focusableElements = findAllFocusable ( focusWithinRef . current ) ;
72+ const target = event . target as HTMLElement ;
73+
74+ return focusableElements . some ( element => element . contains ( target ) ) ;
75+ } ,
76+ [ findAllFocusable , focusWithinRef ] ,
77+ ) ;
78+
79+ const interactive = computeInteractive ( props ) ;
80+ const focusMode = focusModeProp ?? ( interactive ? 'no-tab' : 'off' ) ;
81+ const groupperAttrs = useFocusableGroup ( {
82+ tabBehavior : focusMap [ focusMode ] ,
83+ } ) ;
84+
85+ const state = useCardBase_unstable (
86+ {
87+ shouldRestrictTriggerAction,
88+ ...cardProps ,
89+ } ,
90+ cardRef ,
91+ ) ;
92+
93+ // Apply focusable-group attributes only when the card is not selectable, not
94+ // disabled and the focus mode is enabled.
95+ const shouldApplyFocusAttributes = ! disabled && ! state . selectable && focusMode !== 'off' ;
96+ if ( shouldApplyFocusAttributes ) {
97+ Object . assign ( state . root , groupperAttrs , { tabIndex : 0 } ) ;
98+ }
8499
85100 return {
86101 ...state ,
@@ -92,27 +107,29 @@ export const useCard_unstable = (props: CardProps, ref: React.Ref<HTMLDivElement
92107
93108/**
94109 * Base hook for Card component, which manages state related to interactivity, selection,
95- * focus management, ARIA attributes, and slot structure without design props.
110+ * ARIA attributes, and slot structure without design props or focus management.
111+ *
112+ * This hook is intentionally free of `@fluentui/react-tabster` so that it can be
113+ * consumed by headless component packages. Focus management (focusable group
114+ * attributes, focus-within, focus-restriction predicate) is layered on top in
115+ * `useCard_unstable`.
96116 *
97117 * @param props - props from this instance of Card
98118 * @param ref - reference to the root element of Card
119+ * @param options - optional behavior overrides such as a focus-aware restriction predicate
99120 */
100121export const useCardBase_unstable = ( props : CardBaseProps , ref : React . Ref < HTMLDivElement > ) : CardBaseState => {
101122 const { disabled = false , ...restProps } = props ;
102123
103124 const [ referenceId , setReferenceId ] = React . useState ( cardContextDefaultValue . selectableA11yProps . referenceId ) ;
104125 const [ referenceLabel , setReferenceLabel ] = React . useState ( cardContextDefaultValue . selectableA11yProps . referenceId ) ;
105126
106- const cardBaseRef = useFocusWithin < HTMLDivElement > ( ) ;
107127 const { selectable, selected, selectableCardProps, selectFocused, checkboxSlot, floatingActionSlot } =
108- useCardSelectable ( props , { referenceId, referenceLabel } , cardBaseRef ) ;
109-
110- const cardRef = useMergedRefs ( cardBaseRef , ref ) ;
128+ useCardSelectable ( props , { referenceId, referenceLabel } ) ;
111129
112- const { interactive, focusAttributes } = useCardInteractive ( props ) ;
130+ const interactive = computeInteractive ( props ) ;
113131
114132 let cardRootProps = {
115- ...( ! selectable ? focusAttributes : null ) ,
116133 ...restProps ,
117134 ...selectableCardProps ,
118135 } ;
@@ -146,7 +163,7 @@ export const useCardBase_unstable = (props: CardBaseProps, ref: React.Ref<HTMLDi
146163
147164 root : slot . always (
148165 getIntrinsicElementProps ( 'div' , {
149- ref : cardRef ,
166+ ref,
150167 role : 'group' ,
151168 ...cardRootProps ,
152169 } ) ,
0 commit comments