diff --git a/change/@fluentui-react-aria-8f9fdde9-52bf-45bc-a42f-b7354bcd9abb.json b/change/@fluentui-react-aria-8f9fdde9-52bf-45bc-a42f-b7354bcd9abb.json new file mode 100644 index 00000000000000..4af491e460f787 --- /dev/null +++ b/change/@fluentui-react-aria-8f9fdde9-52bf-45bc-a42f-b7354bcd9abb.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: creates ARIAButtonComponent notion", + "packageName": "@fluentui/react-aria", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "patch" +} diff --git a/packages/react-components/react-aria/etc/react-aria.api.md b/packages/react-components/react-aria/etc/react-aria.api.md index 9450ba46c1e89e..6a932823537e8e 100644 --- a/packages/react-components/react-aria/etc/react-aria.api.md +++ b/packages/react-components/react-aria/etc/react-aria.api.md @@ -9,14 +9,19 @@ import * as React_2 from 'react'; import type { ResolveShorthandFunction } from '@fluentui/react-utilities'; import type { Slot } from '@fluentui/react-utilities'; -// @internal (undocumented) +// @public +export type ARIAButtonComponent = { + isARIAButtonComponent?: boolean; +}; + +// @internal export type ARIAButtonElement = HTMLButtonElement | (AlternateAs extends 'a' ? HTMLAnchorElement : never) | (AlternateAs extends 'div' ? HTMLDivElement : never); -// @internal (undocumented) +// @internal export type ARIAButtonElementIntersection = UnionToIntersection>; // @public -export type ARIAButtonProps = React_2.PropsWithRef & { +export type ARIAButtonProps = React_2.PropsWithRef]> & { disabled?: boolean; disabledFocusable?: boolean; }; @@ -24,14 +29,17 @@ export type ARIAButtonProps = Reac // @public export type ARIAButtonResultProps = Props & UnionToIntersection>; -// @public (undocumented) +// @public export type ARIAButtonSlotProps = ExtractSlotProps> & Pick, 'disabled' | 'disabledFocusable'>; -// @public (undocumented) -export type ARIAButtonType = 'button' | 'a' | 'div'; +// @public +export type ARIAButtonType = 'button' | 'a' | 'div' | ARIAButtonComponent; + +// @internal +export function isARIAButtonComponent(value: unknown): value is ARIAButtonComponent; // @internal -export function useARIAButtonProps>(type?: Type, props?: Props): ARIAButtonResultProps; +export function useARIAButtonProps>(elementType: Type, props?: Props): ARIAButtonResultProps; // @internal export const useARIAButtonShorthand: ResolveShorthandFunction; diff --git a/packages/react-components/react-aria/src/button/index.ts b/packages/react-components/react-aria/src/button/index.ts index caa15e61d0ed13..45624baab69d5d 100644 --- a/packages/react-components/react-aria/src/button/index.ts +++ b/packages/react-components/react-aria/src/button/index.ts @@ -1,3 +1,4 @@ export * from './useARIAButtonProps'; export * from './useARIAButtonShorthand'; export * from './types'; +export * from './isARIAButtonComponent'; diff --git a/packages/react-components/react-aria/src/button/isARIAButtonComponent.ts b/packages/react-components/react-aria/src/button/isARIAButtonComponent.ts new file mode 100644 index 00000000000000..b039e76ad4c7e5 --- /dev/null +++ b/packages/react-components/react-aria/src/button/isARIAButtonComponent.ts @@ -0,0 +1,10 @@ +import { ARIAButtonComponent } from './types'; + +/** + * @internal + * Checks if an unknown value is a ARIAButtonComponent + * @param value - an unknown value + */ +export function isARIAButtonComponent(value: unknown): value is ARIAButtonComponent { + return Boolean(value && (value as ARIAButtonComponent).isARIAButtonComponent); +} diff --git a/packages/react-components/react-aria/src/button/types.ts b/packages/react-components/react-aria/src/button/types.ts index 541be29d845f9c..25cf178c776e45 100644 --- a/packages/react-components/react-aria/src/button/types.ts +++ b/packages/react-components/react-aria/src/button/types.ts @@ -3,10 +3,19 @@ import * as React from 'react'; type UnionToIntersection = (U extends unknown ? (x: U) => U : never) extends (x: infer I) => U ? I : never; -export type ARIAButtonType = 'button' | 'a' | 'div'; +/** + * Possible element types supported by `ARIAButton` + * 1. `button` - Minimal interference from ARIAButton hooks, as semantic button already supports most of the states + * 2. `a` or `div` - Proper keyboard/mouse handling plus other support to ensure behavior + * 3. `ARIAButtonComponent` - No interface from ARIAButton hooks, as the given element already implements + * uses `ARIAButton` hooks under the hood. + */ +export type ARIAButtonType = 'button' | 'a' | 'div' | ARIAButtonComponent; /** * @internal + * + * An union of possible HTMLElement types found internally on `ARIAButton` hooks */ export type ARIAButtonElement = | HTMLButtonElement @@ -15,6 +24,8 @@ export type ARIAButtonElement = /** * @internal + * + * An intersection of possible HTMLElement types found internally on `ARIAButton` hooks */ export type ARIAButtonElementIntersection = UnionToIntersection< ARIAButtonElement @@ -24,7 +35,7 @@ export type ARIAButtonElementIntersection = React.PropsWithRef< - JSX.IntrinsicElements[Type] + JSX.IntrinsicElements[Extract] > & { disabled?: boolean; /** @@ -38,6 +49,9 @@ export type ARIAButtonProps = Reac disabledFocusable?: boolean; }; +/** + * Props expected by `useARIAButtonShorthands` hooks + */ export type ARIAButtonSlotProps = ExtractSlotProps< Slot<'button', AlternateAs> > & @@ -70,3 +84,21 @@ export type ARIAButtonAlteredProps = */ export type ARIAButtonResultProps = Props & UnionToIntersection>; + +/** + * Allows a component to be tagged as a FluentUI ARIAButton component. + * + * ARIAButton are special-case components: they implement ARIA specific button functionality. + * Having a component declared as being an ARIAButton component will ensure that ARIAButtonProps will consider + * that specific component as being a ARIA compliant button element + * + * A component can be tagged as a ARIAButton as follows: + * ```ts + * const MyComponent: React.FC & ARIAButtonComponent = ...; + * + * MyComponent.isARIAButtonComponent = true; // MUST also set this to true + * ``` + */ +export type ARIAButtonComponent = { + isARIAButtonComponent?: boolean; +}; diff --git a/packages/react-components/react-aria/src/button/useARIAButtonProps.ts b/packages/react-components/react-aria/src/button/useARIAButtonProps.ts index 5a01425e4a4f59..11af4f3c091de6 100644 --- a/packages/react-components/react-aria/src/button/useARIAButtonProps.ts +++ b/packages/react-components/react-aria/src/button/useARIAButtonProps.ts @@ -1,6 +1,7 @@ import { Enter, Space } from '@fluentui/keyboard-keys'; import { useEventCallback } from '@fluentui/react-utilities'; import * as React from 'react'; +import { isARIAButtonComponent } from './isARIAButtonComponent'; import type { ARIAButtonElementIntersection, ARIAButtonProps, ARIAButtonResultProps, ARIAButtonType } from './types'; /** @@ -10,9 +11,11 @@ import type { ARIAButtonElementIntersection, ARIAButtonProps, ARIAButtonResultPr * for multiple scenarios of non native button elements. Ensuring 1st rule of ARIA for cases * where no attribute addition is required. * - * @param type - the proper scenario to be interpreted by the hook. + * @param elementType - the proper scenario to be interpreted by the hook. * 1. `button` - Minimal interference from the hook, as semantic button already supports most of the states * 2. `a` or `div` - Proper keyboard/mouse handling plus other support to ensure ARIA behavior + * 3. `ARIAButtonComponent` - Minimal interference from the hook, + * a `ARIAButtonComponent` - is treated as a ARIA compliant element * @param props - the props to be passed down the line to the desired element. * This hook will encapsulate proper properties, such as `onClick`, `onKeyDown`, `onKeyUp`, etc,. * @@ -31,7 +34,7 @@ import type { ARIAButtonElementIntersection, ARIAButtonProps, ARIAButtonResultPr * ``` */ export function useARIAButtonProps>( - type?: Type, + elementType: Type, props?: Props, ): ARIAButtonResultProps { const { @@ -107,7 +110,7 @@ export function useARIAButtonProps tag is to be rendered we just need to set disabled and aria-disabled correctly - if (type === 'button' || type === undefined) { + if (elementType === 'button') { return { ...rest, tabIndex, @@ -120,7 +123,10 @@ export function useARIAButtonProps; } - + // If a ARIAButtonComponent element is being passed, then there's nothing to be done + else if (isARIAButtonComponent(elementType)) { + return props as ARIAButtonResultProps; + } // If an or
tag is to be rendered we have to remove disabled and type, // and set aria-disabled, role and tabIndex. else { @@ -137,7 +143,7 @@ export function useARIAButtonProps; - if (type === 'a' && isDisabled) { + if (elementType === 'a' && isDisabled) { (resultProps as ARIAButtonResultProps<'a', Props>).href = undefined; } diff --git a/packages/react-components/react-aria/src/index.ts b/packages/react-components/react-aria/src/index.ts index 86f3713ac66f82..3bd187ff19bcfd 100644 --- a/packages/react-components/react-aria/src/index.ts +++ b/packages/react-components/react-aria/src/index.ts @@ -1,4 +1,4 @@ -export { useARIAButtonShorthand, useARIAButtonProps } from './button/index'; +export { useARIAButtonShorthand, useARIAButtonProps, isARIAButtonComponent } from './button/index'; export type { ARIAButtonSlotProps, ARIAButtonProps, @@ -6,4 +6,5 @@ export type { ARIAButtonType, ARIAButtonElement, ARIAButtonElementIntersection, + ARIAButtonComponent, } from './button/index';