From a0f91a08c5fe3ab00d5bece38c1e858a80a07d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Thu, 7 Sep 2023 10:19:56 +0200 Subject: [PATCH] [base-ui] Create hooks contribution guide (#38679) Co-authored-by: Diego Andai Co-authored-by: Sam Sycamore <71297412+samuelsycamore@users.noreply.github.com> --- packages/mui-base/CONTRIBUTING.md | 188 ++++++++++++++++++++++++++++++ packages/mui-base/README.md | 4 + 2 files changed, 192 insertions(+) create mode 100644 packages/mui-base/CONTRIBUTING.md diff --git a/packages/mui-base/CONTRIBUTING.md b/packages/mui-base/CONTRIBUTING.md new file mode 100644 index 00000000000000..b1f78a0c01f710 --- /dev/null +++ b/packages/mui-base/CONTRIBUTING.md @@ -0,0 +1,188 @@ +# Contributing + +## Creating a new hook + +### File structure + +When creating a new hook, follow the file structure found in other hooks' directories: + +Taking the imaginary `useAwesomeControl` as an example: + +- 📂 `useAwesomeControl` + - 📄 `index.ts` - aggregates the public exports from all the other files in the directory + - 📄 `useAwesomeControl.test.tsx` - unit tests + - 📄 `useAwesomeControl.spec.tsx` - (optional) type tests + - 📄 `useAwesomeControl.ts` - the implementation of the hook + - 📄 `useAwesomeControl.types.ts` - type definitions + +### Implementation + +Most Base UI hooks have a similar structure: + +1. [Parameters destructuring](#parameters-destructuring) +2. Hook-specific logic +3. [Event handler factory functions](#event-handler-factory-functions) +4. [Slot props resolvers](#slot-props-resolvers) + +#### Parameters destructuring + +The parameters type must be called `[HookName]Parameters`. +There are docs generation scripts that require this pattern. + +```ts +function useAwesomeControl(parameters: UseAwesomeControlParameters) { + const { disabled, readOnly } = parameters; + + // the rest of the hook's logic... +} +``` + +#### Event handler factory functions + +We don't define event handlers directly as functions because they must be able to access and call other handlers provided by developers in slot props resolvers. + +In other words, instead of defining the `handleClick(event: React.MouseEvent) => void` function, we define the `createHandleClick(otherHandlers: EventHandlers) => (event: React.MouseEvent) => void`. The `otherHandlers` parameter contains external handlers provided by developers. + +By convention, we call them `createHandle[EventName]`. + +If we allow a developer to skip running our logic for a given handler, we check the `event.defaultMuiPrevented` field. It's an equivalent of the native `defaultPrevented` that works just for Base UI code: + +```tsx +const createHandleKeyDown = (otherHandlers: EventHandlers) => (event: React.KeyboardEvent & MuiCancellableEvent) => { + // Run the external handler first. + // It can potentially set the defaultMuiPrevented field. + otherHandlers.onKeyDown?.(event); + + // If the field is set, do not execute the usual handler logic. + if (event.defaultMuiPrevented) { + return; + } + + // handler-specific logic... +``` + +#### Slot props resolvers + +These are functions called `get[SlotName]Props` that accept additional (optional) props and return props to be placed on slots of a component. +Many components have just one slot (that we call "root"), but more complex components can have more. + +The order of merging props for the resulting object is important so that users can override the build-in props when necessary: + +1. built-in props +2. external props +3. ref +4. event handlers + +Refs and event handlers can be placed in any order. +They just have to be after external props. + +Example: + +```tsx +const getRootProps = ( + otherHandlers: OtherHandlers = {} as OtherHandlers, +): UseAwesomeControlRootSlotProps => { + return { + id, + disabled, + role: 'button' as const, + ...otherHandlers, // if `id`, `disabled`, or `role` is provided here, they will override the default values set by us. + ref: handleListboxRef, // refs mustn't be overridden, so they come after `...otherHandlers` + onMouseDown: createHandleMouseDown(otherHandlers), // here we execute the event handler factory supplying it with external props + }; +}; +``` + +It's common that a hook uses other hooks and combines their `get*Props` with its own. +To handle these cases, we have the `combineHooksSlotProps` utility. +It creates a function that merges two other slot resolver functions: + +```tsx +const createHandleClick = (otherHandlers: EventHandlers) => (event: React.KeyboardEvent) => { + /* ... */ +} + +const { getRootProps as getListRootProps } = useList(/* ... */); +const getOwnRootEventHandlers = (otherHandlers: EventHandlers = {}) => ({ + onClick: createHandleClick(otherHandlers), +}); + +const getRootProps = ( + otherHandlers: TOther = {} as TOther, + ): UseAwesomeControlRootSlotProps => { + const getCombinedRootProps = combineHooksSlotProps(getOwnRootEventHandlers, getListRootProps); + return { + ...getCombinedRootProps(otherHandlers), + role: 'combobox' + } + } + +``` + +#### Ref handling + +When a hook needs to access the DOM node it operates on, it should create a ref and return it in the `get*Props` function. +However, since the user of the hook may already have a ref to the element, we accept the refs in parameters and merge them with our refs using the `useForkRef` function, so the callers of the hook don't have to do it. + +Since hooks can operate on many elements (when dealing with multiple slots), we call refs in input parameters `[slotName]Ref`. + +Example: + +```ts + +interface AwesomeControlHookParameters { + rootRef?: React.Ref; + // ... +} + +const useAwesomeControlHook = (parameters: AwesomeControlHookParameters) { + const { rootRef: externalRef } = parameters; + const innerRef = React.useRef(null); + + const handleRef = useForkRef(externalRef, innerRef); + + return { + // parameters omitted for the sake of brevity + getRootProps: () => { + ref: handleRef + }, + rootRef: handleRef + } +} +``` + +Note that the merged ref (`handleRef`) is not only returned as a part of root props but also as a field of the hook's return object. +This is helpful in situations where the ref needs to be merged with yet another ref. + +### Types + +Defining proper types can tremendously help developers use the hooks. +The following types are required for each hook: + +- [HookName]Parameters - input parameters +- [HookName]ReturnValue - the shape of the object returned by the hook +- [HookName][SlotName]SlotProps - return values of slot props resolvers + +The parameters and return value types are usually straightforward. +The definition of slot props, however, is more complex as it must take into consideration the object passed as an argument to the props resolver function: + +```ts +export interface UseMenuReturnValue { + getListboxProps: ( + otherHandlers?: TOther, + ) => UseMenuListboxSlotProps; + // ... +} + +interface UseMenuListboxSlotEventHandlers { + onBlur: React.FocusEventHandler; + onKeyDown: React.KeyboardEventHandler; +} + +export type UseMenuListboxSlotProps = UseListRootSlotProps< + Omit & UseMenuListboxSlotEventHandlers +> & { + ref: React.RefCallback | null; + role: React.AriaRole; +}; +``` diff --git a/packages/mui-base/README.md b/packages/mui-base/README.md index c21095a94f0490..40e493415bb595 100644 --- a/packages/mui-base/README.md +++ b/packages/mui-base/README.md @@ -19,3 +19,7 @@ yarn add @mui/base [The documentation](https://mui.com/base-ui/getting-started/) + +## Contributing + +See [the contributing guide](./CONTRIBUTING.md) if you wish to implement Base UI components or hooks.