diff --git a/docs/pages/api-docs/click-away-listener.json b/docs/pages/api-docs/click-away-listener.json index 635def8deaccae..7867d5969660d1 100644 --- a/docs/pages/api-docs/click-away-listener.json +++ b/docs/pages/api-docs/click-away-listener.json @@ -21,7 +21,7 @@ "name": "ClickAwayListener", "styles": { "classes": [], "globalClasses": {}, "name": null }, "spread": false, - "filename": "/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js", + "filename": "/packages/material-ui/src/ClickAwayListener/ClickAwayListener.tsx", "inheritance": null, "demos": "", "styledComponent": true, diff --git a/docs/scripts/buildApi.ts b/docs/scripts/buildApi.ts index fee6dc982812cb..bca149010acd47 100644 --- a/docs/scripts/buildApi.ts +++ b/docs/scripts/buildApi.ts @@ -505,7 +505,21 @@ async function annotateComponentDefinition(context: { if (babel.types.isIdentifier(babelPath.node.declaration)) { const bindingId = babelPath.node.declaration.name; const binding = babelPath.scope.bindings[bindingId]; - node = binding.path.parentPath.node; + + // The JSDOC MUST be located at the declaration + if (babel.types.isFunctionDeclaration(binding.path.node)) { + // For function declarations the binding is equal to the declaration + // /** + // */ + // function Component() {} + node = binding.path.node; + } else { + // For variable declarations the binding points to the declarator. + // /** + // */ + // const Component = () => {} + node = binding.path.parentPath.node; + } } } @@ -906,7 +920,7 @@ async function parseComponentSource( // Ignore what we might have generated in `annotateComponentDefinition` const annotatedDescriptionMatch = fullDescription.match(/(Demos|API):\r?\n\r?\n/); if (annotatedDescriptionMatch !== null) { - reactAPI.description = fullDescription.slice(0, annotatedDescriptionMatch.index); + reactAPI.description = fullDescription.slice(0, annotatedDescriptionMatch.index).trim(); } return reactAPI; diff --git a/docs/src/pages/components/button-group/SplitButton.tsx b/docs/src/pages/components/button-group/SplitButton.tsx index fdeadef0293ae0..4a08b07caf0ed2 100644 --- a/docs/src/pages/components/button-group/SplitButton.tsx +++ b/docs/src/pages/components/button-group/SplitButton.tsx @@ -33,7 +33,7 @@ export default function SplitButton() { setOpen((prevOpen) => !prevOpen); }; - const handleClose = (event: React.MouseEvent) => { + const handleClose = (event: Event) => { if ( anchorRef.current && anchorRef.current.contains(event.target as HTMLElement) diff --git a/docs/src/pages/components/menus/MenuListComposition.tsx b/docs/src/pages/components/menus/MenuListComposition.tsx index e820d14a32263c..9070c91154b883 100644 --- a/docs/src/pages/components/menus/MenuListComposition.tsx +++ b/docs/src/pages/components/menus/MenuListComposition.tsx @@ -28,7 +28,7 @@ export default function MenuListComposition() { setOpen((prevOpen) => !prevOpen); }; - const handleClose = (event: React.MouseEvent) => { + const handleClose = (event: Event | React.SyntheticEvent) => { if ( anchorRef.current && anchorRef.current.contains(event.target as HTMLElement) diff --git a/packages/material-ui-utils/src/ownerDocument.ts b/packages/material-ui-utils/src/ownerDocument.ts index eed9b51474ccbf..39b1b60ff95a7c 100644 --- a/packages/material-ui-utils/src/ownerDocument.ts +++ b/packages/material-ui-utils/src/ownerDocument.ts @@ -1,3 +1,3 @@ -export default function ownerDocument(node: Node | undefined): Document { +export default function ownerDocument(node: Node | null | undefined): Document { return (node && node.ownerDocument) || document; } diff --git a/packages/material-ui/src/ClickAwayListener/ClickAwayListener.d.ts b/packages/material-ui/src/ClickAwayListener/ClickAwayListener.d.ts deleted file mode 100644 index 4259dff9c598a9..00000000000000 --- a/packages/material-ui/src/ClickAwayListener/ClickAwayListener.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from 'react'; - -export interface ClickAwayListenerProps { - /** - * The wrapped element. - */ - children?: React.ReactNode; - /** - * If `true`, the React tree is ignored and only the DOM tree is considered. - * This prop changes how portaled elements are handled. - * @default false - */ - disableReactTree?: boolean; - /** - * The mouse event to listen to. You can disable the listener by providing `false`. - * @default 'onClick' - */ - mouseEvent?: 'onClick' | 'onMouseDown' | 'onMouseUp' | false; - /** - * Callback fired when a "click away" event is detected. - */ - onClickAway: (event: React.MouseEvent) => void; - /** - * The touch event to listen to. You can disable the listener by providing `false`. - * @default 'onTouchEnd' - */ - touchEvent?: 'onTouchStart' | 'onTouchEnd' | false; -} - -/** - * Listen for click events that occur somewhere in the document, outside of the element itself. - * For instance, if you need to hide a menu when people click anywhere else on your page. - * - * Demos: - * - * - [Click Away Listener](https://material-ui.com/components/click-away-listener/) - * - [Menus](https://material-ui.com/components/menus/) - * - * API: - * - * - [ClickAwayListener API](https://material-ui.com/api/click-away-listener/) - */ -export default function ClickAwayListener(props: ClickAwayListenerProps): JSX.Element; diff --git a/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js b/packages/material-ui/src/ClickAwayListener/ClickAwayListener.tsx similarity index 64% rename from packages/material-ui/src/ClickAwayListener/ClickAwayListener.js rename to packages/material-ui/src/ClickAwayListener/ClickAwayListener.tsx index 735c990f09d823..3839c2de2ea41c 100644 --- a/packages/material-ui/src/ClickAwayListener/ClickAwayListener.js +++ b/packages/material-ui/src/ClickAwayListener/ClickAwayListener.tsx @@ -5,22 +5,64 @@ import ownerDocument from '../utils/ownerDocument'; import useForkRef from '../utils/useForkRef'; import useEventCallback from '../utils/useEventCallback'; -function mapEventPropToEvent(eventProp) { - return eventProp.substring(2).toLowerCase(); +// TODO: return `EventHandlerName extends `on${infer EventName}` ? Lowercase : never` once generatePropTypes runs with TS 4.1 +function mapEventPropToEvent( + eventProp: ClickAwayMouseEventHandler | ClickAwayTouchEventHandler, +): 'click' | 'mousedown' | 'mouseup' | 'touchstart' | 'touchend' { + return eventProp.substring(2).toLowerCase() as any; } -function clickedRootScrollbar(event, doc) { +function clickedRootScrollbar(event: MouseEvent, doc: Document) { return ( doc.documentElement.clientWidth < event.clientX || doc.documentElement.clientHeight < event.clientY ); } +type ClickAwayMouseEventHandler = 'onClick' | 'onMouseDown' | 'onMouseUp'; +type ClickAwayTouchEventHandler = 'onTouchStart' | 'onTouchEnd'; + +export interface ClickAwayListenerProps { + /** + * The wrapped element. + */ + children: React.ReactElement; + /** + * If `true`, the React tree is ignored and only the DOM tree is considered. + * This prop changes how portaled elements are handled. + * @default false + */ + disableReactTree?: boolean; + /** + * The mouse event to listen to. You can disable the listener by providing `false`. + * @default 'onClick' + */ + mouseEvent?: ClickAwayMouseEventHandler | false; + /** + * Callback fired when a "click away" event is detected. + */ + onClickAway: (event: MouseEvent | TouchEvent) => void; + /** + * The touch event to listen to. You can disable the listener by providing `false`. + * @default 'onTouchEnd' + */ + touchEvent?: ClickAwayTouchEventHandler | false; +} + /** * Listen for click events that occur somewhere in the document, outside of the element itself. * For instance, if you need to hide a menu when people click anywhere else on your page. + * + * Demos: + * + * - [Click Away Listener](https://material-ui.com/components/click-away-listener/) + * - [Menus](https://material-ui.com/components/menus/) + * + * API: + * + * - [ClickAwayListener API](https://material-ui.com/api/click-away-listener/) */ -function ClickAwayListener(props) { +function ClickAwayListener(props: ClickAwayListenerProps): JSX.Element { const { children, disableReactTree = false, @@ -29,7 +71,7 @@ function ClickAwayListener(props) { touchEvent = 'onTouchEnd', } = props; const movedRef = React.useRef(false); - const nodeRef = React.useRef(null); + const nodeRef = React.useRef(null); const activatedRef = React.useRef(false); const syntheticEventRef = React.useRef(false); @@ -44,7 +86,11 @@ function ClickAwayListener(props) { }; }, []); - const handleRef = useForkRef(children.ref, nodeRef); + const handleRef = useForkRef( + // @ts-expect-error TODO upstream fix + children.ref, + nodeRef, + ); // The handler doesn't take event.defaultPrevented into account: // @@ -52,7 +98,7 @@ function ClickAwayListener(props) { // clicking a checkbox to check it, hitting a button to submit a form, // and hitting left arrow to move the cursor in a text input etc. // Only special HTML elements have these default behaviors. - const handleClickAway = useEventCallback((event) => { + const handleClickAway = useEventCallback((event: MouseEvent | TouchEvent) => { // Given developers can stop the propagation of the synthetic event, // we can only be confident with a positive value. const insideReactTree = syntheticEventRef.current; @@ -63,7 +109,11 @@ function ClickAwayListener(props) { // 1. IE11 support, which trigger the handleClickAway even after the unbind // 2. The child might render null. // 3. Behave like a blur listener. - if (!activatedRef.current || !nodeRef.current || clickedRootScrollbar(event, doc)) { + if ( + !activatedRef.current || + !nodeRef.current || + ('clientX' in event && clickedRootScrollbar(event, doc)) + ) { return; } @@ -80,7 +130,14 @@ function ClickAwayListener(props) { insideDOM = event.composedPath().indexOf(nodeRef.current) > -1; } else { insideDOM = - !doc.documentElement.contains(event.target) || nodeRef.current.contains(event.target); + !doc.documentElement.contains( + // @ts-expect-error returns `false` as intended when not dispatched from a Node + event.target, + ) || + nodeRef.current.contains( + // @ts-expect-error returns `false` as intended when not dispatched from a Node + event.target, + ); } if (!insideDOM && (disableReactTree || !insideReactTree)) { @@ -89,7 +146,7 @@ function ClickAwayListener(props) { }); // Keep track of mouse/touch events that bubbled up through the portal. - const createHandleSynthetic = (handlerName) => (event) => { + const createHandleSynthetic = (handlerName: string) => (event: React.SyntheticEvent) => { syntheticEventRef.current = true; const childrenPropsHandler = children.props[handlerName]; @@ -98,7 +155,10 @@ function ClickAwayListener(props) { } }; - const childrenProps = { ref: handleRef }; + const childrenProps: { ref: React.Ref } & Pick< + React.DOMAttributes, + ClickAwayMouseEventHandler | ClickAwayTouchEventHandler + > = { ref: handleRef }; if (touchEvent !== false) { childrenProps[touchEvent] = createHandleSynthetic(touchEvent); @@ -150,7 +210,7 @@ function ClickAwayListener(props) { ClickAwayListener.propTypes = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the d.ts file and run "yarn proptypes" | + // | To update them edit TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- /** * The wrapped element. @@ -180,7 +240,7 @@ ClickAwayListener.propTypes = { if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line - ClickAwayListener['propTypes' + ''] = exactProp(ClickAwayListener.propTypes); + (ClickAwayListener as any)['propTypes' + ''] = exactProp(ClickAwayListener.propTypes); } export default ClickAwayListener; diff --git a/packages/material-ui/src/ClickAwayListener/index.js b/packages/material-ui/src/ClickAwayListener/index.js deleted file mode 100644 index e207d41b085555..00000000000000 --- a/packages/material-ui/src/ClickAwayListener/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ClickAwayListener'; diff --git a/packages/material-ui/src/ClickAwayListener/index.d.ts b/packages/material-ui/src/ClickAwayListener/index.ts similarity index 100% rename from packages/material-ui/src/ClickAwayListener/index.d.ts rename to packages/material-ui/src/ClickAwayListener/index.ts diff --git a/packages/material-ui/tsconfig.build.json b/packages/material-ui/tsconfig.build.json index 32766f15f19783..3a28b86f0de3f7 100644 --- a/packages/material-ui/tsconfig.build.json +++ b/packages/material-ui/tsconfig.build.json @@ -10,7 +10,7 @@ "outDir": "build", "rootDir": "./src" }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["./src/**/*.ts*"], + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"], "references": [{ "path": "../material-ui-unstyled/tsconfig.build.json" }] } diff --git a/scripts/generateProptypes.ts b/scripts/generateProptypes.ts index e0d4bdb6d79a4e..418bb7c6f15bf6 100644 --- a/scripts/generateProptypes.ts +++ b/scripts/generateProptypes.ts @@ -350,6 +350,8 @@ async function run(argv: HandlerArgv) { .filter((filePath) => { return filePattern.test(filePath); }); + // May not be able to understand all files due to mismatch in TS versions. + // Check `programm.getSyntacticDiagnostics()` if referenced files could not be compiled. const program = ttp.createTSProgram(files, tsconfig); const promises = files.map>(async (tsFile) => {