Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: creates ARIAButtonComponent notion",
"packageName": "@fluentui/react-aria",
"email": "[email protected]",
"dependentChangeType": "patch"
}
22 changes: 15 additions & 7 deletions packages/react-components/react-aria/etc/react-aria.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,37 @@ 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<AlternateAs extends 'a' | 'div' = 'a' | 'div'> = HTMLButtonElement | (AlternateAs extends 'a' ? HTMLAnchorElement : never) | (AlternateAs extends 'div' ? HTMLDivElement : never);

// @internal (undocumented)
// @internal
export type ARIAButtonElementIntersection<AlternateAs extends 'a' | 'div' = 'a' | 'div'> = UnionToIntersection<ARIAButtonElement<AlternateAs>>;

// @public
export type ARIAButtonProps<Type extends ARIAButtonType = ARIAButtonType> = React_2.PropsWithRef<JSX.IntrinsicElements[Type]> & {
export type ARIAButtonProps<Type extends ARIAButtonType = ARIAButtonType> = React_2.PropsWithRef<JSX.IntrinsicElements[Extract<Type, string>]> & {
disabled?: boolean;
disabledFocusable?: boolean;
};

// @public
export type ARIAButtonResultProps<Type extends ARIAButtonType, Props> = Props & UnionToIntersection<ARIAButtonAlteredProps<Type>>;

// @public (undocumented)
// @public
export type ARIAButtonSlotProps<AlternateAs extends 'a' | 'div' = 'a' | 'div'> = ExtractSlotProps<Slot<'button', AlternateAs>> & Pick<ARIAButtonProps<ARIAButtonType>, '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 extends ARIAButtonType, Props extends ARIAButtonProps<Type>>(type?: Type, props?: Props): ARIAButtonResultProps<Type, Props>;
export function useARIAButtonProps<Type extends ARIAButtonType, Props extends ARIAButtonProps<Type>>(elementType: Type, props?: Props): ARIAButtonResultProps<Type, Props>;

// @internal
export const useARIAButtonShorthand: ResolveShorthandFunction<ARIAButtonSlotProps>;
Expand Down
1 change: 1 addition & 0 deletions packages/react-components/react-aria/src/button/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './useARIAButtonProps';
export * from './useARIAButtonShorthand';
export * from './types';
export * from './isARIAButtonComponent';
Original file line number Diff line number Diff line change
@@ -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);
}
36 changes: 34 additions & 2 deletions packages/react-components/react-aria/src/button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ import * as React from 'react';

type UnionToIntersection<U> = (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<AlternateAs extends 'a' | 'div' = 'a' | 'div'> =
| HTMLButtonElement
Expand All @@ -15,6 +24,8 @@ export type ARIAButtonElement<AlternateAs extends 'a' | 'div' = 'a' | 'div'> =

/**
* @internal
*
* An intersection of possible HTMLElement types found internally on `ARIAButton` hooks
*/
export type ARIAButtonElementIntersection<AlternateAs extends 'a' | 'div' = 'a' | 'div'> = UnionToIntersection<
ARIAButtonElement<AlternateAs>
Expand All @@ -24,7 +35,7 @@ export type ARIAButtonElementIntersection<AlternateAs extends 'a' | 'div' = 'a'
* Props expected by `useARIAButtonProps` hooks
*/
export type ARIAButtonProps<Type extends ARIAButtonType = ARIAButtonType> = React.PropsWithRef<
JSX.IntrinsicElements[Type]
JSX.IntrinsicElements[Extract<Type, string>]
> & {
disabled?: boolean;
/**
Expand All @@ -38,6 +49,9 @@ export type ARIAButtonProps<Type extends ARIAButtonType = ARIAButtonType> = Reac
disabledFocusable?: boolean;
};

/**
* Props expected by `useARIAButtonShorthands` hooks
*/
export type ARIAButtonSlotProps<AlternateAs extends 'a' | 'div' = 'a' | 'div'> = ExtractSlotProps<
Slot<'button', AlternateAs>
> &
Expand Down Expand Up @@ -70,3 +84,21 @@ export type ARIAButtonAlteredProps<Type extends ARIAButtonType> =
*/
export type ARIAButtonResultProps<Type extends ARIAButtonType, Props> = Props &
UnionToIntersection<ARIAButtonAlteredProps<Type>>;

/**
* 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<MyComponentProps> & ARIAButtonComponent = ...;
*
* MyComponent.isARIAButtonComponent = true; // MUST also set this to true
* ```
*/
export type ARIAButtonComponent = {
isARIAButtonComponent?: boolean;
};
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -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,.
*
Expand All @@ -31,7 +34,7 @@ import type { ARIAButtonElementIntersection, ARIAButtonProps, ARIAButtonResultPr
* ```
*/
export function useARIAButtonProps<Type extends ARIAButtonType, Props extends ARIAButtonProps<Type>>(
type?: Type,
elementType: Type,
props?: Props,
): ARIAButtonResultProps<Type, Props> {
const {
Expand Down Expand Up @@ -107,7 +110,7 @@ export function useARIAButtonProps<Type extends ARIAButtonType, Props extends AR
});

// If a <button> 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,
Expand All @@ -120,7 +123,10 @@ export function useARIAButtonProps<Type extends ARIAButtonType, Props extends AR
onKeyDown: disabledFocusable ? undefined : onKeyDown,
} as ARIAButtonResultProps<Type, Props>;
}

// If a ARIAButtonComponent element is being passed, then there's nothing to be done
else if (isARIAButtonComponent(elementType)) {
return props as ARIAButtonResultProps<Type, Props>;
}
// If an <a> or <div> tag is to be rendered we have to remove disabled and type,
// and set aria-disabled, role and tabIndex.
else {
Expand All @@ -137,7 +143,7 @@ export function useARIAButtonProps<Type extends ARIAButtonType, Props extends AR
tabIndex: disabled && !disabledFocusable ? undefined : tabIndex ?? 0,
} as ARIAButtonResultProps<Type, Props>;

if (type === 'a' && isDisabled) {
if (elementType === 'a' && isDisabled) {
(resultProps as ARIAButtonResultProps<'a', Props>).href = undefined;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/react-components/react-aria/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export { useARIAButtonShorthand, useARIAButtonProps } from './button/index';
export { useARIAButtonShorthand, useARIAButtonProps, isARIAButtonComponent } from './button/index';
export type {
ARIAButtonSlotProps,
ARIAButtonProps,
ARIAButtonResultProps,
ARIAButtonType,
ARIAButtonElement,
ARIAButtonElementIntersection,
ARIAButtonComponent,
} from './button/index';