From 0c8610629efb42e642925ab33c62c36afc8aedce Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Tue, 24 Aug 2021 09:56:57 +0200 Subject: [PATCH 01/24] Adds suppot for root as slot --- .../etc/react-utilities.api.md | 55 +++----- .../src/compose/getSlots.test.tsx | 130 +++++++++++------- .../react-utilities/src/compose/getSlots.ts | 112 +++++++-------- .../src/compose/resolveShorthand.ts | 13 +- packages/react-utilities/src/compose/types.ts | 73 +++++----- 5 files changed, 188 insertions(+), 195 deletions(-) diff --git a/packages/react-utilities/etc/react-utilities.api.md b/packages/react-utilities/etc/react-utilities.api.md index 77ed85d2d8f31..7cea5bb61b09c 100644 --- a/packages/react-utilities/etc/react-utilities.api.md +++ b/packages/react-utilities/etc/react-utilities.api.md @@ -41,7 +41,9 @@ export const colGroupProperties: Record; export const colProperties: Record; // @public (undocumented) -export type ComponentProps = DefaultComponentProps & ShorthandPropsRecord; +export type ComponentProps = Omit<{ + [Key in keyof Shorthands]?: ShorthandProps>; +}, Primary> & Shorthands[Primary]; // @public (undocumented) export interface ComponentPropsCompat { @@ -54,24 +56,17 @@ export interface ComponentPropsCompat { } // @public (undocumented) -export type ComponentState = Pick, keyof DefaultComponentProps> & { +export type ComponentState = { components?: { - [K in keyof Record]?: React_2.ElementType; + [Key in keyof Shorthands]?: React_2.ElementType> | keyof JSX.IntrinsicElements; }; -} & { - components?: { - root?: React_2.ElementType; - }; -} & ObjectShorthandPropsRecord; +} & Shorthands; // @public export type ComponentStateCompat = never> = RequiredPropsCompat, DefaultedPropNames>; // @public (undocumented) -export interface DefaultComponentProps { - // (undocumented) - as?: keyof JSX.IntrinsicElements; -} +export type DefaultObjectShorthandProps = ObjectShorthandProps<{}, unknown, keyof JSX.IntrinsicElements>; // Warning: (ae-internal-missing-underscore) The name "defaultSSRContextValue" should be prefixed with an underscore because the declaration is marked as @internal // @@ -91,13 +86,9 @@ export function getNativeElementProps>(props: Record, allowedPropNames: string[] | Record, excludedPropNames?: string[]): T; // @public -export function getSlots(state: ComponentState, slotNames?: string[]): { - readonly slots: { [K in keyof SlotProps]-?: React_2.ElementType; } & { - readonly root: React_2.ElementType; - }; - readonly slotProps: { [Key in keyof SlotProps]-?: UnionToIntersection>; } & { - readonly root: any; - }; +export function getSlots(state: ComponentState, slotNames?: (keyof R)[]): { + slots: Slots; + slotProps: SlotProps; }; // Warning: (ae-forgotten-export) The symbol "GenericDictionary" needs to be exported by the entry point index.d.ts @@ -148,7 +139,8 @@ export const nullRender: () => null; // @public (undocumented) export type ObjectShorthandProps = Props & { +} = {}, Ref = unknown, As extends keyof JSX.IntrinsicElements = never> = Props & React_2.RefAttributes & { + as?: As; children?: Props['children'] | ShorthandRenderFunction; }; @@ -158,9 +150,7 @@ export type ObjectShorthandPropsCompat }; // @public (undocumented) -export type ObjectShorthandPropsRecord = { - [K in keyof Record]: ObjectShorthandProps>; -}; +export type ObjectShorthandPropsRecord = Record; // @public export const olProperties: Record; @@ -191,7 +181,7 @@ export type ResolvedShorthandPropsCompat = Omit & { }; // @public -export function resolveShorthand, Required extends boolean = false>(value: ShorthandProps, options?: ResolveShorthandOptions): Required extends false ? ObjectShorthandProps | undefined : ObjectShorthandProps; +export function resolveShorthand(value: ShorthandProps, options?: ResolveShorthandOptions): Required extends false ? Props | undefined : Props; // @public (undocumented) export interface ResolveShorthandOptions, Required extends boolean = false> { @@ -208,18 +198,13 @@ export const resolveShorthandProps: ; // @public (undocumented) -export type ShorthandProps = React_2.ReactChild | React_2.ReactNodeArray | React_2.ReactPortal | number | null | undefined | ObjectShorthandProps; +export type ShorthandProps = React_2.ReactChild | React_2.ReactNodeArray | React_2.ReactPortal | number | null | undefined | Props; // @public (undocumented) export type ShorthandPropsCompat = React_2.ReactChild | React_2.ReactNodeArray | React_2.ReactPortal | number | null | undefined | ObjectShorthandPropsCompat; // @public (undocumented) -export type ShorthandPropsRecord = { - [K in keyof Record]: ShorthandProps>; -}; - -// @public (undocumented) -export type ShorthandRenderFunction = (Component: React_2.ElementType, props: Props) => React_2.ReactNode; +export type ShorthandRenderFunction = (Component: React_2.ElementType, props: Omit) => React_2.ReactNode; // @public (undocumented) export type ShorthandRenderFunctionCompat = (Component: React_2.ElementType, props: TProps) => React_2.ReactNode; @@ -234,8 +219,10 @@ export type SlotPropsCompat | undefined>; +// @public (undocumented) +export type Slots = { + [K in keyof S]-?: S[K] extends ObjectShorthandProps ? React_2.ElementType> : React_2.ElementType>; +}; // Warning: (ae-incompatible-release-tags) The symbol "SSRContext" is marked as @public, but its signature references "SSRContextValue" which is marked as @internal // @@ -357,7 +344,7 @@ export const videoProperties: Record; // Warnings were encountered during analysis: // -// lib/compose/getSlots.d.ts:27:5 - (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// lib/compose/getSlots.d.ts:27:5 - (ae-forgotten-export) The symbol "SlotProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/packages/react-utilities/src/compose/getSlots.test.tsx b/packages/react-utilities/src/compose/getSlots.test.tsx index e620bc4ecb4b9..411e8d306b335 100644 --- a/packages/react-utilities/src/compose/getSlots.test.tsx +++ b/packages/react-utilities/src/compose/getSlots.test.tsx @@ -1,72 +1,59 @@ import * as React from 'react'; import { getSlots } from './getSlots'; import { nullRender } from './nullRender'; -import type { ComponentState } from './types'; +import { ComponentState, ObjectShorthandProps } from './types'; describe('getSlots', () => { const Foo = (props: { id?: string }) =>
; - const FooForward = React.forwardRef((props: { id?: string }, ref) =>
); it('returns div for root if the as prop is not provided', () => { - expect(getSlots({})).toEqual({ + expect(getSlots({ root: {} })).toEqual({ slots: { root: 'div' }, slotProps: { root: {} }, }); }); it('returns root slot as a span with no props', () => { - expect(getSlots({ as: 'span' } as ComponentState)).toEqual({ + expect(getSlots({ root: { as: 'span' } } as ComponentState<{}>)).toEqual({ slots: { root: 'span' }, slotProps: { root: {} }, }); }); - it('omits invalid props for the rendered element', () => { + it('does not omit invalid props for the rendered element', () => { expect( - getSlots<{}>({ as: 'button', id: 'id', href: 'href' } as ComponentState), + getSlots<{}>({ root: { as: 'button', id: 'id', href: 'href' } } as ComponentState<{}>), ).toEqual({ slots: { root: 'button' }, - slotProps: { root: { id: 'id' } }, + slotProps: { root: { id: 'id', href: 'href' } }, }); }); it('returns root slot as an anchor, leaving the href intact', () => { - expect(getSlots({ as: 'a', id: 'id', href: 'href' } as ComponentState)).toEqual({ + expect(getSlots({ root: { as: 'a', id: 'id', href: 'href' } } as ComponentState<{}>)).toEqual({ slots: { root: 'a' }, slotProps: { root: { id: 'id', href: 'href' } }, }); }); - it('retains all props but components, when root is a component,', () => { - expect( - getSlots({ as: 'div', id: 'id', href: 'href', blah: 1, components: { root: FooForward } } as ComponentState), - ).toEqual({ - slots: { root: FooForward }, - slotProps: { root: { as: 'div', id: 'id', href: 'href', blah: 1 } }, - }); - }); - - it('returns null for undefined slots', () => { - expect( - getSlots( - { - as: 'div', - icon: undefined, - }, - ['icon'], - ), - ).toEqual({ - slots: { root: 'div', icon: nullRender }, - slotProps: { root: {} }, - }); - }); - it('returns a component slot with no children', () => { - type ShorthandProps = { + type Slots = { + root: ObjectShorthandProps< + React.DetailedHTMLProps, HTMLElement>, + HTMLElement, + 'div' + >; icon: React.HTMLAttributes; }; expect( - getSlots({ as: 'div', icon: {}, components: { icon: Foo } }, ['icon']), + getSlots( + { + icon: {}, + components: { icon: Foo }, + root: { as: 'div' }, + }, + ['icon', 'root'], + ), ).toEqual({ slots: { root: 'div', icon: Foo }, slotProps: { root: {}, icon: {} }, @@ -75,16 +62,21 @@ describe('getSlots', () => { it('returns slot as button', () => { type Slots = { - icon: React.ButtonHTMLAttributes; + root: ObjectShorthandProps< + React.DetailedHTMLProps, HTMLElement>, + HTMLElement, + 'span' + >; + icon: React.HTMLAttributes; }; expect( getSlots( { components: { icon: 'button', root: 'div' }, - as: 'span', + root: { as: 'span' }, icon: { id: 'id', children: 'children' }, }, - ['icon'], + ['icon', 'root'], ), ).toEqual({ slots: { root: 'span', icon: 'button' }, @@ -94,16 +86,21 @@ describe('getSlots', () => { it('returns slot as anchor and includes supported props (href)', () => { type Slots = { - icon: React.AnchorHTMLAttributes; + root: ObjectShorthandProps< + React.DetailedHTMLProps, HTMLElement>, + HTMLElement, + 'div' + >; + icon: ObjectShorthandProps>; }; expect( getSlots( { - as: 'div', + root: { as: 'div' }, components: { root: 'div', icon: 'a' }, icon: { id: 'id', href: 'href', children: 'children' }, }, - ['icon'], + ['icon', 'root'], ), ).toEqual({ slots: { root: 'div', icon: 'a' }, @@ -112,13 +109,18 @@ describe('getSlots', () => { }); it('returns a component and includes all props', () => { - type ShorthandProps = { - icon: React.AnchorHTMLAttributes; + type Slots = { + root: ObjectShorthandProps< + React.DetailedHTMLProps, HTMLElement>, + HTMLElement, + 'div' + >; + icon: ObjectShorthandProps>; }; expect( - getSlots( - { components: { icon: Foo }, as: 'div', icon: { id: 'id', href: 'href', children: 'children' } }, - ['icon'], + getSlots( + { components: { icon: Foo }, root: { as: 'div' }, icon: { id: 'id', href: 'href', children: 'children' } }, + ['icon', 'root'], ), ).toEqual({ slots: { root: 'div', icon: Foo }, @@ -127,14 +129,22 @@ describe('getSlots', () => { }); it('can use slot children functions to replace default slot rendering', () => { + type Slots = { + root: ObjectShorthandProps< + React.DetailedHTMLProps, HTMLElement>, + HTMLElement, + 'div' + >; + icon: ObjectShorthandProps>; + }; expect( - getSlots( + getSlots( { components: { icon: Foo }, - as: 'div', + root: { as: 'div' }, icon: { id: 'bar', children: (C: React.ElementType, p: {}) => }, }, - ['icon'], + ['icon', 'root'], ), ).toEqual({ slots: { root: 'div', icon: React.Fragment }, @@ -144,18 +154,32 @@ describe('getSlots', () => { it('can render a primitive input with no children', () => { type Slots = { - input: React.AnchorHTMLAttributes; + root: ObjectShorthandProps< + React.DetailedHTMLProps, HTMLElement>, + HTMLElement, + 'div' + >; + input: ObjectShorthandProps>; + icon?: ObjectShorthandProps>; }; expect( - getSlots({ as: 'div', components: { input: 'input' }, input: { children: null } }, ['input']), + getSlots( + { + root: { as: 'div' }, + components: { input: 'input' }, + input: {}, + icon: undefined, + }, + ['input', 'root', 'icon'], + ), ).toEqual({ - slots: { root: 'div', input: 'input' }, - slotProps: { root: {}, input: { children: null } }, + slots: { root: 'div', input: 'input', icon: nullRender }, + slotProps: { root: {}, input: {}, icon: undefined }, }); }); it('should use `div` as default root element', () => { - expect(getSlots({ icon: { children: 'foo' }, customProp: 'bar' }, ['icon'])).toEqual({ + expect(getSlots({ icon: { children: 'foo' }, root: {} }, ['icon', 'root'])).toEqual({ slots: { root: 'div', icon: 'div' }, slotProps: { root: {}, icon: { children: 'foo' } }, }); diff --git a/packages/react-utilities/src/compose/getSlots.ts b/packages/react-utilities/src/compose/getSlots.ts index c9a2dd5077779..115bfda13d27b 100644 --- a/packages/react-utilities/src/compose/getSlots.ts +++ b/packages/react-utilities/src/compose/getSlots.ts @@ -1,26 +1,18 @@ import * as React from 'react'; + +import { ComponentState, ShorthandRenderFunction, ObjectShorthandPropsRecord, ObjectShorthandProps } from './types'; import { nullRender } from './nullRender'; -import { getNativeElementProps } from '../utils/getNativeElementProps'; import { omit } from '../utils/omit'; -import type { ComponentState, ShorthandRenderFunction, SlotPropsRecord } from './types'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function getRootSlot(state: ComponentState) { - const slot = - state.components?.root === undefined || typeof state.components.root === 'string' - ? state.as || state.components?.root || 'div' - : state.components.root; +export type Slots = { + [K in keyof S]-?: S[K] extends ObjectShorthandProps + ? React.ElementType> + : React.ElementType>; +}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const props: any = typeof slot === 'string' ? getNativeElementProps(slot, state) : omit(state, ['components']); - return [slot, props] as const; -} - -/** - * Hack that converts an Union to an Intersection - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : U; +type SlotProps = { + [K in keyof S]-?: NonNullable extends ObjectShorthandProps ? P : never; +}; /** * Given the state and an array of slot names, will break out `slots` and `slotProps` @@ -38,54 +30,48 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( * @param slotNames - Name of which props are slots * @returns An object containing the `slots` map and `slotProps` map. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getSlots(state: ComponentState, slotNames?: string[]) { - /** - * force typings on state, this should not be added directly in parameters to avoid type inference - */ - const typedState = state as ComponentState; - /** - * force typings on slotNames, this should not be added directly in parameters to avoid type inference - */ - const typedSlotNames = slotNames as Array | undefined; - - type Slots = { [K in keyof SlotProps]-?: React.ElementType }; - - const slots = {} as Slots; +export function getSlots( + state: ComponentState, + slotNames: (keyof R)[] = ['root'], +): { + slots: Slots; + slotProps: SlotProps; +} { + const slots = {} as Slots; + const slotProps = {} as R; - const slotProps = {} as SlotProps; - - if (typedSlotNames) { - for (const name of typedSlotNames) { - if (typedState[name] === undefined) { - slots[name] = nullRender; - continue; - } - const { children, ...rest } = typedState[name]; - - slots[name] = (typedState.components?.[name] || 'div') as Slots[typeof name]; - - if (typeof children === 'function') { - const render = children as ShorthandRenderFunction; - // TODO: converting to unknown might be harmful - slotProps[name] = ({ - children: render(slots[name], rest as ComponentState[keyof SlotProps]), - } as unknown) as SlotProps[keyof SlotProps]; - slots[name] = React.Fragment; - } else { - slotProps[name] = typedState[name]; - } - } + for (const slotName of slotNames) { + const [slot, props] = getSlot(state, slotName); + slots[slotName] = slot as R[typeof slotName] extends ObjectShorthandProps + ? React.ElementType> + : never; + slotProps[slotName] = props; } + return { slots, slotProps: (slotProps as unknown) as SlotProps }; +} - const [root, rootProps] = getRootSlot(state); +function getSlot( + state: ComponentState, + slotName: K, +): readonly [React.ElementType, R[K]] { + if (state[slotName] === undefined) { + return [nullRender, undefined!]; + } + const { children, as: asProp, ...rest } = state[slotName]!; - const typedSlotProps = slotProps as { - [Key in keyof SlotProps]-?: UnionToIntersection>; - }; + const slot = (state.components?.[slotName] === undefined || typeof state.components[slotName] === 'string' + ? asProp || state.components?.[slotName] || 'div' + : state.components[slotName]) as React.ElementType; - return { - slots: { ...slots, root }, - slotProps: { ...typedSlotProps, root: rootProps }, - } as const; + if (typeof children === 'function') { + const render = children as ShorthandRenderFunction; + return [ + React.Fragment, + ({ + children: render(slot, rest), + } as unknown) as R[K], + ]; + } + const props = (typeof slot === 'string' ? omit(state[slotName]!, ['as']) : state[slotName]) as R[K]; + return [slot, props]; } diff --git a/packages/react-utilities/src/compose/resolveShorthand.ts b/packages/react-utilities/src/compose/resolveShorthand.ts index bde91d7c11f04..47c3b218c7fce 100644 --- a/packages/react-utilities/src/compose/resolveShorthand.ts +++ b/packages/react-utilities/src/compose/resolveShorthand.ts @@ -1,5 +1,5 @@ import { isValidElement } from 'react'; -import type { ObjectShorthandProps, ShorthandProps } from './types'; +import { DefaultObjectShorthandProps, ShorthandProps } from './types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface ResolveShorthandOptions, Required extends boolean = false> { @@ -13,17 +13,16 @@ export interface ResolveShorthandOptions, Requ * @param value - the base ShorthandProps * @param defaultProps - base properties to be merged with the end ObjectShorthandProps */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function resolveShorthand, Required extends boolean = false>( +export function resolveShorthand( value: ShorthandProps, options?: ResolveShorthandOptions, -): Required extends false ? ObjectShorthandProps | undefined : ObjectShorthandProps { +): Required extends false ? Props | undefined : Props { const { required = false, defaultProps } = options || {}; if (value === null || (value === undefined && !required)) { - return undefined as Required extends false ? ObjectShorthandProps | undefined : never; + return undefined as Required extends false ? Props | undefined : never; } - let resolvedShorthand = {} as ObjectShorthandProps; + let resolvedShorthand = {} as Props; if (typeof value === 'string' || typeof value === 'number' || Array.isArray(value) || isValidElement(value)) { resolvedShorthand.children = value as Props['children']; @@ -33,5 +32,5 @@ export function resolveShorthand, Required ext return (defaultProps ? { ...defaultProps, ...resolvedShorthand } : resolvedShorthand) as Required extends false ? never - : ObjectShorthandProps; + : Props; } diff --git a/packages/react-utilities/src/compose/types.ts b/packages/react-utilities/src/compose/types.ts index 97cc29ef72a09..cda8a4ff963d9 100644 --- a/packages/react-utilities/src/compose/types.ts +++ b/packages/react-utilities/src/compose/types.ts @@ -1,52 +1,49 @@ import * as React from 'react'; -/** - * Generic record of possible ShorthandProps - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type SlotPropsRecord = Record | undefined>; - -export type ShorthandPropsRecord = { - [K in keyof Record]: ShorthandProps>; -}; - -export type ObjectShorthandPropsRecord = { - [K in keyof Record]: ObjectShorthandProps>; -}; +export type ShorthandRenderFunction = ( + Component: React.ElementType, + props: Omit, +) => React.ReactNode; -export type ShorthandRenderFunction = (Component: React.ElementType, props: Props) => React.ReactNode; +export type ObjectShorthandPropsRecord = Record; -export type ShorthandProps = +export type ShorthandProps = | React.ReactChild | React.ReactNodeArray | React.ReactPortal | number - | null - | undefined - | ObjectShorthandProps; - -export type ObjectShorthandProps = Props & { - children?: Props['children'] | ShorthandRenderFunction; -}; - -export interface DefaultComponentProps { - as?: keyof JSX.IntrinsicElements; -} - -export type ComponentProps = DefaultComponentProps & ShorthandPropsRecord; - -export type ComponentState = Pick< - ComponentProps, - keyof DefaultComponentProps -> & { - components?: { - [K in keyof Record]?: React.ElementType; + | null // force null render + | undefined // default render (or null render if no default provided) + | Props; + +export type DefaultObjectShorthandProps = ObjectShorthandProps<{}, unknown, keyof JSX.IntrinsicElements>; + +export type ObjectShorthandProps< + Props extends { children?: React.ReactNode } = {}, + Ref = unknown, + As extends keyof JSX.IntrinsicElements = never +> = Props & + React.RefAttributes & { + as?: As; + children?: Props['children'] | ShorthandRenderFunction; }; -} & { + +export type ComponentProps< + Shorthands extends ObjectShorthandPropsRecord, + Primary extends keyof Shorthands = 'root' +> = Omit< + { + [Key in keyof Shorthands]?: ShorthandProps>; + }, + Primary +> & + Shorthands[Primary]; + +export type ComponentState = { components?: { - root?: React.ElementType; + [Key in keyof Shorthands]?: React.ElementType> | keyof JSX.IntrinsicElements; }; -} & ObjectShorthandPropsRecord; +} & Shorthands; /////////////////////////// COMPAT ///////////////////////////////////////////////////////////////////// From 762fb402ec2e410093e37e863a5ef9cf33a56ed6 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Tue, 24 Aug 2021 09:57:26 +0200 Subject: [PATCH 02/24] Updates react-aria to use root as slot --- packages/react-aria/etc/react-aria.api.md | 20 ++------------- .../src/hooks/useARIAButton.stories.tsx | 18 +++++++------ .../src/hooks/useARIAButton.test.tsx | 22 ++++++++-------- .../react-aria/src/hooks/useARIAButton.ts | 25 +++++++++---------- 4 files changed, 35 insertions(+), 50 deletions(-) diff --git a/packages/react-aria/etc/react-aria.api.md b/packages/react-aria/etc/react-aria.api.md index 9662615b9cdd7..a2c919d156069 100644 --- a/packages/react-aria/etc/react-aria.api.md +++ b/packages/react-aria/etc/react-aria.api.md @@ -5,30 +5,14 @@ ```ts import { ObjectShorthandProps } from '@fluentui/react-utilities'; -import * as React_2 from 'react'; import { ResolveShorthandOptions } from '@fluentui/react-utilities'; import { ShorthandProps } from '@fluentui/react-utilities'; // @public (undocumented) -export type ARIAButtonAsAnchorProps = React_2.AnchorHTMLAttributes & { - as: 'a'; -}; - -// @public (undocumented) -export type ARIAButtonAsButtonProps = React_2.ButtonHTMLAttributes & { - as?: 'button'; -}; - -// @public (undocumented) -export type ARIAButtonAsElementProps = React_2.HTMLAttributes & { - as: 'div' | 'span'; -}; - -// @public (undocumented) -export type ARIAButtonProps = ARIAButtonAsButtonProps | ARIAButtonAsElementProps | ARIAButtonAsAnchorProps; +export type ARIAButtonShorthandProps = ObjectShorthandProps | ObjectShorthandProps | ObjectShorthandProps | ObjectShorthandProps; // @public -export function useARIAButton(value: ShorthandProps, options?: ResolveShorthandOptions): Required extends false ? ObjectShorthandProps | undefined : ObjectShorthandProps; +export function useARIAButton(value: ShorthandProps, options?: ResolveShorthandOptions): Required extends false ? ARIAButtonShorthandProps | undefined : ARIAButtonShorthandProps; // (No @packageDocumentation comment for this package) diff --git a/packages/react-aria/src/hooks/useARIAButton.stories.tsx b/packages/react-aria/src/hooks/useARIAButton.stories.tsx index da48cc9083a3e..c31e5a3863c67 100644 --- a/packages/react-aria/src/hooks/useARIAButton.stories.tsx +++ b/packages/react-aria/src/hooks/useARIAButton.stories.tsx @@ -1,9 +1,10 @@ -import { ComponentState, getSlots } from '@fluentui/react-utilities'; +import { ComponentState, getSlots, ObjectShorthandProps } from '@fluentui/react-utilities'; import * as React from 'react'; -import { ARIAButtonAsElementProps, ARIAButtonProps, useARIAButton } from './useARIAButton'; +import { ARIAButtonShorthandProps, useARIAButton } from './useARIAButton'; type Slots = { - button: ARIAButtonProps; + root: ObjectShorthandProps, HTMLElement>; + button: ARIAButtonShorthandProps; }; interface State extends ComponentState {} @@ -14,15 +15,16 @@ interface DefaultArgs { export const Default = (args: DefaultArgs) => { const state: State = { + root: {}, button: { ...useARIAButton({ as: 'button', onClick: args.onClick }, { required: true }), children: React.Fragment, }, }; - const { slots, slotProps } = getSlots(state, ['button']); + const { slots, slotProps } = getSlots(state, ['button', 'root']); return ( - this is a button + this is a button ); }; @@ -38,7 +40,7 @@ export const Anchor = (args: DefaultArgs) => { }, { required: true }, ); - const { slots, slotProps } = getSlots(props, []); + const { slots, slotProps } = getSlots({ root: props }, ['root']); return ( this is an anchor @@ -48,13 +50,13 @@ export const Anchor = (args: DefaultArgs) => { export const Span = (args: DefaultArgs) => { const props = useARIAButton({ as: 'span', onClick: args.onClick }, { required: true }); - const { slots, slotProps } = getSlots(props, []); + const { slots, slotProps } = getSlots({ root: props }, ['root']); return this is a span; }; export const Div = (args: DefaultArgs) => { const props = useARIAButton({ as: 'div', onClick: args.onClick }, { required: true }); - const { slots, slotProps } = getSlots(props, []); + const { slots, slotProps } = getSlots({ root: props }, ['root']); return this is a div; }; diff --git a/packages/react-aria/src/hooks/useARIAButton.test.tsx b/packages/react-aria/src/hooks/useARIAButton.test.tsx index 689ca9e13579e..1863bc288082c 100644 --- a/packages/react-aria/src/hooks/useARIAButton.test.tsx +++ b/packages/react-aria/src/hooks/useARIAButton.test.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import { ARIAButtonProps, useARIAButton } from './useARIAButton'; +import { ARIAButtonShorthandProps, useARIAButton } from './useARIAButton'; import { Enter, Space } from '@fluentui/keyboard-keys'; import { renderHook } from '@testing-library/react-hooks'; import { fireEvent, screen, render } from '@testing-library/react'; -import { getSlots, ObjectShorthandProps } from '@fluentui/react-utilities'; +import { getSlots } from '@fluentui/react-utilities'; describe('useARIAButton', () => { it('should return by default shorthand props for a button', () => { - const shorthand: ObjectShorthandProps = {}; + const shorthand: ARIAButtonShorthandProps = { as: 'button' }; renderHook(() => useARIAButton(shorthand)); expect(shorthand.as).toBe(undefined); expect(shorthand.disabled).toBeUndefined(); @@ -19,7 +19,7 @@ describe('useARIAButton', () => { expect(shorthand.onKeyUp).toBeUndefined(); }); it('should return handlers for anchor when anchor element is declared', () => { - const shorthand: ObjectShorthandProps = { as: 'a' }; + const shorthand: ARIAButtonShorthandProps = { as: 'a' }; renderHook(() => useARIAButton(shorthand)); expect(shorthand.as).toBe('a'); expect(shorthand['aria-disabled']).toBe(false); @@ -30,7 +30,7 @@ describe('useARIAButton', () => { expect(shorthand.onKeyUp).toBeInstanceOf(Function); }); it('should return handlers when shorthand props declares another semantic element', () => { - const shorthand: ObjectShorthandProps = { as: 'div' }; + const shorthand: ARIAButtonShorthandProps = { as: 'div' }; renderHook(() => useARIAButton(shorthand)); expect(shorthand.as).toBe('div'); expect(shorthand.role).toBe('button'); @@ -44,7 +44,7 @@ describe('useARIAButton', () => { it('should emit click events on Click', () => { const handleClick = jest.fn(); const { result } = renderHook(() => useARIAButton({ as: 'div', onClick: handleClick }, { required: true })); - const { slots, slotProps } = getSlots(result.current, []); + const { slots, slotProps } = getSlots<{ root: ARIAButtonShorthandProps }>({ root: result.current }, ['root']); render(); fireEvent.click(screen.getByTestId('div')); expect(handleClick).toHaveBeenCalledTimes(1); @@ -53,7 +53,7 @@ describe('useARIAButton', () => { it('should emit click events on SpaceBar', () => { const handleClick = jest.fn(); const { result } = renderHook(() => useARIAButton({ as: 'div', onClick: handleClick }, { required: true })); - const { slots, slotProps } = getSlots(result.current, []); + const { slots, slotProps } = getSlots({ root: result.current }, ['root']); render(); fireEvent.keyUp(screen.getByTestId('div'), { key: Space }); expect(handleClick).toHaveBeenCalledTimes(1); @@ -62,7 +62,7 @@ describe('useARIAButton', () => { it('should emit click events on Enter', () => { const handleClick = jest.fn(); const { result } = renderHook(() => useARIAButton({ as: 'div', onClick: handleClick }, { required: true })); - const { slots, slotProps } = getSlots(result.current, []); + const { slots, slotProps } = getSlots({ root: result.current }, ['root']); render(); fireEvent.keyDown(screen.getByTestId('div'), { key: Enter }); expect(handleClick).toHaveBeenCalledTimes(1); @@ -73,7 +73,7 @@ describe('useARIAButton', () => { const { result } = renderHook(() => useARIAButton({ as: 'div', 'aria-disabled': true, onClick: handleClick }, { required: true }), ); - const { slots, slotProps } = getSlots(result.current, []); + const { slots, slotProps } = getSlots({ root: result.current }, ['root']); render(
@@ -88,7 +88,7 @@ describe('useARIAButton', () => { const { result } = renderHook(() => useARIAButton({ as: 'div', 'aria-disabled': true, onClick: handleClick }, { required: true }), ); - const { slots, slotProps } = getSlots(result.current, []); + const { slots, slotProps } = getSlots({ root: result.current }, ['root']); render(
@@ -103,7 +103,7 @@ describe('useARIAButton', () => { const { result } = renderHook(() => useARIAButton({ as: 'div', 'aria-disabled': true, onClick: handleClick }, { required: true }), ); - const { slots, slotProps } = getSlots(result.current, []); + const { slots, slotProps } = getSlots({ root: result.current }, ['root']); render(
diff --git a/packages/react-aria/src/hooks/useARIAButton.ts b/packages/react-aria/src/hooks/useARIAButton.ts index 6e6256fe2bf63..6853478ba583e 100644 --- a/packages/react-aria/src/hooks/useARIAButton.ts +++ b/packages/react-aria/src/hooks/useARIAButton.ts @@ -1,4 +1,3 @@ -import * as React from 'react'; import { Enter, Space } from '@fluentui/keyboard-keys'; import { ObjectShorthandProps, @@ -15,11 +14,11 @@ function mergeARIADisabled(disabled?: boolean | 'false' | 'true'): boolean { return disabled ?? false; } -export type ARIAButtonAsButtonProps = React.ButtonHTMLAttributes & { as?: 'button' }; -export type ARIAButtonAsElementProps = React.HTMLAttributes & { as: 'div' | 'span' }; -export type ARIAButtonAsAnchorProps = React.AnchorHTMLAttributes & { as: 'a' }; - -export type ARIAButtonProps = ARIAButtonAsButtonProps | ARIAButtonAsElementProps | ARIAButtonAsAnchorProps; +export type ARIAButtonShorthandProps = + | ObjectShorthandProps + | ObjectShorthandProps + | ObjectShorthandProps + | ObjectShorthandProps; /** * button keyboard handling, role, disabled and tabIndex implementation that ensures ARIA spec @@ -27,19 +26,19 @@ export type ARIAButtonProps = ARIAButtonAsButtonProps | ARIAButtonAsElementProps * where no attribute addition is required */ export function useARIAButton( - value: ShorthandProps, - options?: ResolveShorthandOptions, -): Required extends false ? ObjectShorthandProps | undefined : ObjectShorthandProps { + value: ShorthandProps, + options?: ResolveShorthandOptions, +): Required extends false ? ARIAButtonShorthandProps | undefined : ARIAButtonShorthandProps { const shorthand = resolveShorthand(value, options); const { onClick, onKeyDown, onKeyUp, ['aria-disabled']: ariaDisabled } = (shorthand || - {}) as ObjectShorthandProps; + {}) as ARIAButtonShorthandProps; const disabled = mergeARIADisabled( (shorthand && shorthand.as === 'button' ? shorthand.disabled : undefined) ?? ariaDisabled, ); - const onClickHandler: ARIAButtonProps['onClick'] = useEventCallback(ev => { + const onClickHandler: ARIAButtonShorthandProps['onClick'] = useEventCallback(ev => { if (disabled) { ev.preventDefault(); ev.stopPropagation(); @@ -50,7 +49,7 @@ export function useARIAButton( } }); - const onKeyDownHandler: ARIAButtonProps['onKeyDown'] = useEventCallback(ev => { + const onKeyDownHandler: ARIAButtonShorthandProps['onKeyDown'] = useEventCallback(ev => { if (typeof onKeyDown === 'function') { onKeyDown(ev); } @@ -73,7 +72,7 @@ export function useARIAButton( } }); - const onKeyupHandler: ARIAButtonProps['onKeyUp'] = useEventCallback(ev => { + const onKeyupHandler: ARIAButtonShorthandProps['onKeyUp'] = useEventCallback(ev => { if (typeof onKeyUp === 'function') { onKeyUp(ev); } From 32b049be22dd4d848ddf9a38027295655c17a842 Mon Sep 17 00:00:00 2001 From: Bernardo Sunderhus Date: Tue, 24 Aug 2021 09:57:50 +0200 Subject: [PATCH 03/24] Updates react-accordion to use root as slot --- .../etc/react-accordion.api.md | 56 +++++---- .../react-accordion/src/Accordion.stories.tsx | 115 +++++++----------- .../src/common/isConformant.ts | 1 + .../components/Accordion/Accordion.types.ts | 17 ++- .../components/Accordion/renderAccordion.tsx | 7 +- .../src/components/Accordion/useAccordion.ts | 20 ++- .../AccordionHeader/AccordionHeader.types.ts | 24 ++-- .../AccordionHeaderExpandIcon.tsx | 6 +- .../AccordionHeader.test.tsx.snap | 1 - .../AccordionHeader/useAccordionHeader.ts | 45 ++++--- .../useAccordionHeaderStyles.ts | 4 +- .../AccordionItem/AccordionItem.types.ts | 15 +-- .../AccordionItem/renderAccordionItem.tsx | 2 +- .../AccordionItem/useAccordionItem.ts | 17 ++- .../AccordionPanel/AccordionPanel.test.tsx | 2 +- .../AccordionPanel/AccordionPanel.types.ts | 14 +-- .../AccordionPanel/renderAccordionPanel.tsx | 2 +- .../AccordionPanel/useAccordionPanel.ts | 24 ++-- .../AccordionPanel/useAccordionPanelStyles.ts | 2 +- packages/react-accordion/tsconfig.json | 1 + 20 files changed, 194 insertions(+), 181 deletions(-) diff --git a/packages/react-accordion/etc/react-accordion.api.md b/packages/react-accordion/etc/react-accordion.api.md index 162a180e80e16..933b969601af0 100644 --- a/packages/react-accordion/etc/react-accordion.api.md +++ b/packages/react-accordion/etc/react-accordion.api.md @@ -4,17 +4,18 @@ ```ts -import { ARIAButtonProps } from '@fluentui/react-aria'; +import { ARIAButtonShorthandProps } from '@fluentui/react-aria'; import { ComponentProps } from '@fluentui/react-utilities'; import { ComponentState } from '@fluentui/react-utilities'; import { Context } from '@fluentui/react-context-selector'; +import { ObjectShorthandProps } from '@fluentui/react-utilities'; import * as React_2 from 'react'; // @public export const Accordion: React_2.FunctionComponent>; // @public (undocumented) -export interface AccordionCommons extends React_2.HTMLAttributes { +export interface AccordionCommons { collapsible: boolean; multiple: boolean; navigable: boolean; @@ -41,7 +42,7 @@ export interface AccordionContextValues { export const AccordionHeader: React_2.FunctionComponent>; // @public (undocumented) -export interface AccordionHeaderCommons extends Omit, 'children'> { +export interface AccordionHeaderCommons { expandIconPosition: AccordionHeaderExpandIconPosition; inline: boolean; size: AccordionHeaderSize; @@ -66,16 +67,16 @@ export interface AccordionHeaderContextValues { } // @public (undocumented) -export const AccordionHeaderExpandIcon: React_2.ForwardRefExoticComponent>; +export const AccordionHeaderExpandIcon: React_2.ForwardRefExoticComponent, HTMLSpanElement, never>, "color" | "translate" | "aria-label" | "aria-hidden" | "slot" | "style" | "title" | "children" | "as" | "defaultChecked" | "defaultValue" | "suppressContentEditableWarning" | "suppressHydrationWarning" | "accessKey" | "className" | "contentEditable" | "contextMenu" | "dir" | "draggable" | "hidden" | "id" | "lang" | "placeholder" | "spellCheck" | "tabIndex" | "radioGroup" | "role" | "about" | "datatype" | "inlist" | "prefix" | "property" | "resource" | "typeof" | "vocab" | "autoCapitalize" | "autoCorrect" | "autoSave" | "itemProp" | "itemScope" | "itemType" | "itemID" | "itemRef" | "results" | "security" | "unselectable" | "inputMode" | "is" | "aria-activedescendant" | "aria-atomic" | "aria-autocomplete" | "aria-busy" | "aria-checked" | "aria-colcount" | "aria-colindex" | "aria-colspan" | "aria-controls" | "aria-current" | "aria-describedby" | "aria-details" | "aria-disabled" | "aria-dropeffect" | "aria-errormessage" | "aria-expanded" | "aria-flowto" | "aria-grabbed" | "aria-haspopup" | "aria-invalid" | "aria-keyshortcuts" | "aria-labelledby" | "aria-level" | "aria-live" | "aria-modal" | "aria-multiline" | "aria-multiselectable" | "aria-orientation" | "aria-owns" | "aria-placeholder" | "aria-posinset" | "aria-pressed" | "aria-readonly" | "aria-relevant" | "aria-required" | "aria-roledescription" | "aria-rowcount" | "aria-rowindex" | "aria-rowspan" | "aria-selected" | "aria-setsize" | "aria-sort" | "aria-valuemax" | "aria-valuemin" | "aria-valuenow" | "aria-valuetext" | "dangerouslySetInnerHTML" | "onCopy" | "onCopyCapture" | "onCut" | "onCutCapture" | "onPaste" | "onPasteCapture" | "onCompositionEnd" | "onCompositionEndCapture" | "onCompositionStart" | "onCompositionStartCapture" | "onCompositionUpdate" | "onCompositionUpdateCapture" | "onFocus" | "onFocusCapture" | "onBlur" | "onBlurCapture" | "onChange" | "onChangeCapture" | "onBeforeInput" | "onBeforeInputCapture" | "onInput" | "onInputCapture" | "onReset" | "onResetCapture" | "onSubmit" | "onSubmitCapture" | "onInvalid" | "onInvalidCapture" | "onLoad" | "onLoadCapture" | "onError" | "onErrorCapture" | "onKeyDown" | "onKeyDownCapture" | "onKeyPress" | "onKeyPressCapture" | "onKeyUp" | "onKeyUpCapture" | "onAbort" | "onAbortCapture" | "onCanPlay" | "onCanPlayCapture" | "onCanPlayThrough" | "onCanPlayThroughCapture" | "onDurationChange" | "onDurationChangeCapture" | "onEmptied" | "onEmptiedCapture" | "onEncrypted" | "onEncryptedCapture" | "onEnded" | "onEndedCapture" | "onLoadedData" | "onLoadedDataCapture" | "onLoadedMetadata" | "onLoadedMetadataCapture" | "onLoadStart" | "onLoadStartCapture" | "onPause" | "onPauseCapture" | "onPlay" | "onPlayCapture" | "onPlaying" | "onPlayingCapture" | "onProgress" | "onProgressCapture" | "onRateChange" | "onRateChangeCapture" | "onSeeked" | "onSeekedCapture" | "onSeeking" | "onSeekingCapture" | "onStalled" | "onStalledCapture" | "onSuspend" | "onSuspendCapture" | "onTimeUpdate" | "onTimeUpdateCapture" | "onVolumeChange" | "onVolumeChangeCapture" | "onWaiting" | "onWaitingCapture" | "onAuxClick" | "onAuxClickCapture" | "onClick" | "onClickCapture" | "onContextMenu" | "onContextMenuCapture" | "onDoubleClick" | "onDoubleClickCapture" | "onDrag" | "onDragCapture" | "onDragEnd" | "onDragEndCapture" | "onDragEnter" | "onDragEnterCapture" | "onDragExit" | "onDragExitCapture" | "onDragLeave" | "onDragLeaveCapture" | "onDragOver" | "onDragOverCapture" | "onDragStart" | "onDragStartCapture" | "onDrop" | "onDropCapture" | "onMouseDown" | "onMouseDownCapture" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseMoveCapture" | "onMouseOut" | "onMouseOutCapture" | "onMouseOver" | "onMouseOverCapture" | "onMouseUp" | "onMouseUpCapture" | "onSelect" | "onSelectCapture" | "onTouchCancel" | "onTouchCancelCapture" | "onTouchEnd" | "onTouchEndCapture" | "onTouchMove" | "onTouchMoveCapture" | "onTouchStart" | "onTouchStartCapture" | "onPointerDown" | "onPointerDownCapture" | "onPointerMove" | "onPointerMoveCapture" | "onPointerUp" | "onPointerUpCapture" | "onPointerCancel" | "onPointerCancelCapture" | "onPointerEnter" | "onPointerEnterCapture" | "onPointerLeave" | "onPointerLeaveCapture" | "onPointerOver" | "onPointerOverCapture" | "onPointerOut" | "onPointerOutCapture" | "onGotPointerCapture" | "onGotPointerCaptureCapture" | "onLostPointerCapture" | "onLostPointerCaptureCapture" | "onScroll" | "onScrollCapture" | "onWheel" | "onWheelCapture" | "onAnimationStart" | "onAnimationStartCapture" | "onAnimationEnd" | "onAnimationEndCapture" | "onAnimationIteration" | "onAnimationIterationCapture" | "onTransitionEnd" | "onTransitionEndCapture" | "css" | "key"> & React_2.RefAttributes>; // @public (undocumented) export type AccordionHeaderExpandIconPosition = 'start' | 'end'; // @public (undocumented) -export type AccordionHeaderExpandIconProps = React_2.HTMLAttributes; +export type AccordionHeaderExpandIconProps = ObjectShorthandProps, HTMLSpanElement>; // @public (undocumented) -export interface AccordionHeaderProps extends ComponentProps>, Partial { +export interface AccordionHeaderProps extends ComponentProps, Partial { } // @public @@ -86,22 +87,25 @@ export type AccordionHeaderSize = 'small' | 'medium' | 'large' | 'extra-large'; // @public (undocumented) export type AccordionHeaderSlots = { - button: ARIAButtonProps; + root: ObjectShorthandProps, HTMLElement>; + button: ARIAButtonShorthandProps; expandIcon: AccordionHeaderExpandIconProps; - icon?: React_2.HTMLAttributes; - children: React_2.HTMLAttributes; + icon?: ObjectShorthandProps, HTMLElement>; + children: ObjectShorthandProps, HTMLElement>; }; // @public (undocumented) export interface AccordionHeaderState extends ComponentState, AccordionHeaderCommons, AccordionHeaderContextValue { - ref: React_2.Ref; } +// @public (undocumented) +export type AccordionIndex = number | number[]; + // @public -export const AccordionItem: React_2.ForwardRefExoticComponent>; +export const AccordionItem: React_2.ForwardRefExoticComponent & React_2.RefAttributes>; // @public (undocumented) -export interface AccordionItemCommons extends React_2.HTMLAttributes { +export interface AccordionItemCommons { disabled: boolean; } @@ -133,33 +137,35 @@ export interface AccordionItemProps extends ComponentProps, export const accordionItemShorthandProps: Array; // @public (undocumented) -export type AccordionItemSlots = {}; +export type AccordionItemSlots = { + root: ObjectShorthandProps, HTMLElement>; +}; // @public (undocumented) export interface AccordionItemState extends ComponentState, AccordionItemCommons, AccordionItemContextValue { - ref: React_2.Ref; } // @public (undocumented) export type AccordionItemValue = unknown; // @public -export const AccordionPanel: React_2.ForwardRefExoticComponent>; +export const AccordionPanel: React_2.ForwardRefExoticComponent & React_2.RefAttributes>; // @public (undocumented) -export interface AccordionPanelProps extends ComponentProps, React_2.HTMLAttributes { +export interface AccordionPanelProps extends ComponentProps { } // @public export const accordionPanelShorthandProps: Array; // @public (undocumented) -export type AccordionPanelSlots = {}; +export type AccordionPanelSlots = { + root: ObjectShorthandProps, HTMLElement>; +}; // @public (undocumented) -export interface AccordionPanelState extends ComponentState, React_2.HTMLAttributes { +export interface AccordionPanelState extends ComponentState { open: boolean; - ref: React_2.Ref; } // @public (undocumented) @@ -171,11 +177,15 @@ export interface AccordionProps extends ComponentProps, Partial< } // @public (undocumented) -export type AccordionSlots = {}; +export const accordionShorthandProps: Array; + +// @public (undocumented) +export type AccordionSlots = { + root: ObjectShorthandProps, HTMLElement>; +}; // @public (undocumented) export interface AccordionState extends ComponentState, AccordionCommons, AccordionContextValue { - ref: React_2.Ref; } // @public (undocumented) @@ -209,7 +219,7 @@ export const useAccordion: ({ openItems: controlledOpenItems, defaultOpenItems, export function useAccordionContextValues(state: AccordionState): AccordionContextValues; // @public -export const useAccordionHeader: (props: AccordionHeaderProps, ref: React_2.Ref) => AccordionHeaderState; +export const useAccordionHeader: ({ id, icon, button, children, expandIcon, inline, size, expandIconPosition, ...props }: AccordionHeaderProps, ref: React_2.Ref) => AccordionHeaderState; // @public (undocumented) export function useAccordionHeaderContextValues(state: AccordionHeaderState): AccordionHeaderContextValues; @@ -218,7 +228,7 @@ export function useAccordionHeaderContextValues(state: AccordionHeaderState): Ac export const useAccordionHeaderStyles: (state: AccordionHeaderState) => AccordionHeaderState; // @public -export const useAccordionItem: ({ value, ...props }: AccordionItemProps, ref: React_2.Ref) => AccordionItemState; +export const useAccordionItem: ({ value, disabled, ...props }: AccordionItemProps, ref: React_2.Ref) => AccordionItemState; // @public (undocumented) export const useAccordionItemContext: () => AccordionItemContextValue; diff --git a/packages/react-accordion/src/Accordion.stories.tsx b/packages/react-accordion/src/Accordion.stories.tsx index 7bb14c5c7c403..854793cbc9871 100644 --- a/packages/react-accordion/src/Accordion.stories.tsx +++ b/packages/react-accordion/src/Accordion.stories.tsx @@ -9,56 +9,49 @@ interface AccordionExampleProps Pick {} export const AccordionExample = ({ icon, inline, size, expandIconPosition, ...props }: AccordionExampleProps) => { - const items = [ - - : undefined} - > - Accordion Header 1 - - - -
Accordion Panel 1
-
-
, - - : undefined} - > - Accordion Header 2 - - - -
Accordion Panel 2
-
-
, - - : undefined} - > - Accordion Header 3 - - - -
Accordion Panel 3
-
-
, - ]; - - const [shuffledItems, setShuffledItems] = React.useState(items); - return ( <> - {shuffledItems} + + + : undefined} + > + Accordion Header 1 + + +
Accordion Panel 1
+
+
+ + : undefined} + > + Accordion Header 2 + + +
Accordion Panel 2
+
+
+ + : undefined} + > + Accordion Header 3 + + +
Accordion Panel 3
+
+
+
); }; @@ -71,10 +64,10 @@ AccordionExample.argTypes = { defaultValue: false, control: 'boolean', }, - circular: { - defaultValue: false, - control: 'boolean', - }, + // circular: { + // defaultValue: false, + // control: 'boolean', + // }, multiple: { defaultValue: false, control: 'boolean', @@ -116,21 +109,3 @@ export default { title: 'Components/Accordion', component: Accordion, }; - -function shuffle(array: T[]) { - let currentIndex = array.length; - let randomIndex: number; - const nextArray: T[] = Array.from(array); - - // While there remain elements to shuffle... - while (currentIndex !== 0) { - // Pick a remaining element... - randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex--; - - // And swap it with the current element. - [nextArray[currentIndex], nextArray[randomIndex]] = [nextArray[randomIndex], nextArray[currentIndex]]; - } - - return nextArray; -} diff --git a/packages/react-accordion/src/common/isConformant.ts b/packages/react-accordion/src/common/isConformant.ts index 20b3e37e97d44..3fba8ee9032ed 100644 --- a/packages/react-accordion/src/common/isConformant.ts +++ b/packages/react-accordion/src/common/isConformant.ts @@ -6,6 +6,7 @@ export function isConformant( const defaultOptions: Partial> = { asPropHandlesRef: true, componentPath: module!.parent!.filename.replace('.test', ''), + skipAsPropTests: true, }; baseIsConformant(defaultOptions, testInfo); diff --git a/packages/react-accordion/src/components/Accordion/Accordion.types.ts b/packages/react-accordion/src/components/Accordion/Accordion.types.ts index dc0963a3ef141..e0dabddb9df4d 100644 --- a/packages/react-accordion/src/components/Accordion/Accordion.types.ts +++ b/packages/react-accordion/src/components/Accordion/Accordion.types.ts @@ -1,6 +1,8 @@ import * as React from 'react'; -import { ComponentProps, ComponentState } from '@fluentui/react-utilities'; import { AccordionItemValue } from '../AccordionItem/AccordionItem.types'; +import { ComponentProps, ComponentState, ObjectShorthandProps } from '@fluentui/react-utilities'; + +export type AccordionIndex = number | number[]; export type AccordionToggleEvent = React.MouseEvent | React.KeyboardEvent; @@ -22,9 +24,11 @@ export interface AccordionContextValues { accordion: AccordionContextValue; } -export type AccordionSlots = {}; +export type AccordionSlots = { + root: ObjectShorthandProps, HTMLElement>; +}; -export interface AccordionCommons extends React.HTMLAttributes { +export interface AccordionCommons { /** * Indicates if keyboard navigation is available */ @@ -55,9 +59,4 @@ export interface AccordionProps extends ComponentProps, Partial< onToggle?: AccordionToggleEventHandler; } -export interface AccordionState extends ComponentState, AccordionCommons, AccordionContextValue { - /** - * Ref to the root slot - */ - ref: React.Ref; -} +export interface AccordionState extends ComponentState, AccordionCommons, AccordionContextValue {} diff --git a/packages/react-accordion/src/components/Accordion/renderAccordion.tsx b/packages/react-accordion/src/components/Accordion/renderAccordion.tsx index a5935d8c13773..45b3bc112e462 100644 --- a/packages/react-accordion/src/components/Accordion/renderAccordion.tsx +++ b/packages/react-accordion/src/components/Accordion/renderAccordion.tsx @@ -1,7 +1,8 @@ -import { getSlots } from '@fluentui/react-utilities'; import * as React from 'react'; +import { getSlots } from '@fluentui/react-utilities'; -import { AccordionState, AccordionSlots, AccordionContextValues } from './Accordion.types'; +import { AccordionContextValues } from './Accordion.types'; +import { AccordionState, AccordionSlots } from './Accordion.types'; import { AccordionContext } from './AccordionContext'; /** @@ -12,7 +13,7 @@ export const renderAccordion = (state: AccordionState, contextValues: AccordionC return ( - {state.children} + {slotProps.root.children} ); }; diff --git a/packages/react-accordion/src/components/Accordion/useAccordion.ts b/packages/react-accordion/src/components/Accordion/useAccordion.ts index cd9f7de9e229c..187f1356b5b3e 100644 --- a/packages/react-accordion/src/components/Accordion/useAccordion.ts +++ b/packages/react-accordion/src/components/Accordion/useAccordion.ts @@ -1,7 +1,15 @@ import * as React from 'react'; -import { useControllableState, useEventCallback } from '@fluentui/react-utilities'; -import { AccordionProps, AccordionState, AccordionToggleData, AccordionToggleEvent } from './Accordion.types'; import { AccordionItemValue } from '../AccordionItem/AccordionItem.types'; +import { useControllableState, useEventCallback, resolveShorthand } from '@fluentui/react-utilities'; +import { + AccordionProps, + AccordionSlots, + AccordionState, + AccordionToggleData, + AccordionToggleEvent, +} from './Accordion.types'; + +export const accordionShorthandProps: Array = ['root']; export const useAccordion = ( { @@ -32,13 +40,17 @@ export const useAccordion = ( }); return { - ...rest, - ref, multiple, collapsible, navigable, openItems, requestToggle, + root: resolveShorthand(rest, { + required: true, + defaultProps: { + ref, + }, + }), }; }; diff --git a/packages/react-accordion/src/components/AccordionHeader/AccordionHeader.types.ts b/packages/react-accordion/src/components/AccordionHeader/AccordionHeader.types.ts index f421f147f0840..572b479814518 100644 --- a/packages/react-accordion/src/components/AccordionHeader/AccordionHeader.types.ts +++ b/packages/react-accordion/src/components/AccordionHeader/AccordionHeader.types.ts @@ -1,7 +1,7 @@ import * as React from 'react'; -import { ComponentProps, ComponentState } from '@fluentui/react-utilities'; +import { ComponentProps, ComponentState, ObjectShorthandProps } from '@fluentui/react-utilities'; import { AccordionHeaderExpandIconProps } from './AccordionHeaderExpandIcon'; -import { ARIAButtonProps } from '@fluentui/react-aria'; +import { ARIAButtonShorthandProps } from '@fluentui/react-aria'; export type AccordionHeaderSize = 'small' | 'medium' | 'large' | 'extra-large'; export type AccordionHeaderExpandIconPosition = 'start' | 'end'; @@ -18,10 +18,11 @@ export interface AccordionHeaderContextValues { } export type AccordionHeaderSlots = { + root: ObjectShorthandProps, HTMLElement>; /** * The component to be used as button in heading */ - button: ARIAButtonProps; + button: ARIAButtonShorthandProps; /** * Expand icon slot rendered before (or after) children content in heading */ @@ -29,11 +30,11 @@ export type AccordionHeaderSlots = { /** * Expand icon slot rendered before (or after) children content in heading */ - icon?: React.HTMLAttributes; - children: React.HTMLAttributes; + icon?: ObjectShorthandProps, HTMLElement>; + children: ObjectShorthandProps, HTMLElement>; }; -export interface AccordionHeaderCommons extends Omit, 'children'> { +export interface AccordionHeaderCommons { /** * Size of spacing in the heading */ @@ -48,16 +49,9 @@ export interface AccordionHeaderCommons extends Omit>, - Partial {} +export interface AccordionHeaderProps extends ComponentProps, Partial {} export interface AccordionHeaderState extends ComponentState, AccordionHeaderCommons, - AccordionHeaderContextValue { - /** - * Ref to the root slot - */ - ref: React.Ref; -} + AccordionHeaderContextValue {} diff --git a/packages/react-accordion/src/components/AccordionHeader/AccordionHeaderExpandIcon.tsx b/packages/react-accordion/src/components/AccordionHeader/AccordionHeaderExpandIcon.tsx index b57d95c35fca6..5a8a568b643d8 100644 --- a/packages/react-accordion/src/components/AccordionHeader/AccordionHeaderExpandIcon.tsx +++ b/packages/react-accordion/src/components/AccordionHeader/AccordionHeaderExpandIcon.tsx @@ -1,8 +1,12 @@ import * as React from 'react'; import { useAccordionHeaderContext } from './AccordionHeaderContext'; import { AccordionHeaderContextValue } from './AccordionHeader.types'; +import { ObjectShorthandProps } from '@fluentui/react-utilities'; -export type AccordionHeaderExpandIconProps = React.HTMLAttributes; +export type AccordionHeaderExpandIconProps = ObjectShorthandProps< + React.HTMLAttributes, + HTMLSpanElement +>; export const AccordionHeaderExpandIcon = React.forwardRef( ({ children, ...rest }, ref) => { diff --git a/packages/react-accordion/src/components/AccordionHeader/__snapshots__/AccordionHeader.test.tsx.snap b/packages/react-accordion/src/components/AccordionHeader/__snapshots__/AccordionHeader.test.tsx.snap index 6dfd18b7a4bdf..fbab9058ad948 100644 --- a/packages/react-accordion/src/components/AccordionHeader/__snapshots__/AccordionHeader.test.tsx.snap +++ b/packages/react-accordion/src/components/AccordionHeader/__snapshots__/AccordionHeader.test.tsx.snap @@ -8,7 +8,6 @@ exports[`AccordionHeader renders a default state 1`] = `