Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Paper] Adding a custom PaperComponent property makes the Autocomplete scroll to top on change #38049

Closed
2 tasks done
osv93 opened this issue Jul 19, 2023 · 10 comments
Closed
2 tasks done
Labels
component: autocomplete This is the name of the generic UI component, not the React module! duplicate This issue or pull request already exists

Comments

@osv93
Copy link

osv93 commented Jul 19, 2023

Duplicates

  • I have searched the existing issues

Latest version

  • I have tested the latest version

Steps to reproduce 🕹

Link to live example: https://stackblitz.com/edit/stackblitz-starters-egy3ey?file=src%2FApp.tsx

Steps:

  1. Go to demo: https://stackblitz.com/edit/stackblitz-starters-egy3ey?file=src%2FApp.tsx
  2. Scroll down to (2002) The pianist.
  3. The state get updated.
    Result: You get scrolled to the top of the list.

Current behavior 😯

If I set a custom PaperComponent and I scroll down the list and select an item, I am scrolled back to top automatically.

Expected behavior 🤔

If I set a custom PaperComponent and I scroll down the list and select an item, I should stay scrolled down to that item.

Context 🔦

No response

Your environment 🌎

No response

@osv93 osv93 added the status: waiting for maintainer These issues haven't been looked at yet by a maintainer label Jul 19, 2023
@zannager zannager added the component: Paper This is the name of the generic UI component, not the React module! label Jul 20, 2023
@ZeeshanTamboli
Copy link
Member

Duplicate of #31073.

See the explanation in #31073 (comment). After defining the custom Paper component outside the main component, it works - https://stackblitz.com/edit/stackblitz-starters-erthoc?file=src%2FApp.tsx.

@ZeeshanTamboli ZeeshanTamboli added duplicate This issue or pull request already exists component: autocomplete This is the name of the generic UI component, not the React module! and removed component: Paper This is the name of the generic UI component, not the React module! status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Jul 20, 2023
@osv93
Copy link
Author

osv93 commented Jul 29, 2023

@ZeeshanTamboli

Is it possible to add a button inside the paper component and enable/Disable it based on changes that happen in the main component?

In the next example I have a button and I want to enable/disable it when there is at least one option selected.

const CustomPaper = ({ children, ...paperProps }) => (
<Paper elevation={8} {...paperProps} className="customAutoComplete">
{children}
<Button sx={{ width: '90px' }} variant="contained"
disabled={values.length == 0} >
Next


);

@ZeeshanTamboli
Copy link
Member

Yes, you can pass custom props to your custom Paper component using slotProps.paper prop. See https://stackblitz.com/edit/stackblitz-starters-szpsur?file=src%2FApp.tsx

@shashank-campx
Copy link

shashank-campx commented Jun 3, 2024

@ZeeshanTamboli

` import {
Box,
Autocomplete as MuiAutocomplete,
Paper,
PaperProps,
TextField,
} from "@mui/material";
import axios from "axios";
import { useReducer, useRef } from "react";
import { campxAxios } from "../../../utils/campxAxios";
import { Typography } from "../../Typography/Typography";
import { LabelWrapper } from "../LabelWrapper/LabelWrapper";
import { FetchingOptionsLoader } from "../components/FetchingOptionsLoader";
import { OptionContainer } from "../styles";

function sleep(duration: number): Promise {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration);
});
}

export interface SingleSelectProps {
options?: { label: string; subLabel?: string; value: any }[];
optionsApiEndPoint?: string;
useCampxAxios: boolean;
required?: boolean;
label?: string;
}

enum SingleSelectActionsTypes {
OPEN = "open",
CLOSE = "close",
LOAD_INTERNAL_OPTIONS_START = "load_internal_options_start",
LOAD_INTERNAL_OPTIONS_END = "load_internal_options_end",
SET_NETWORK_ERROR = "set_network_error",
SET_INTERNAL_OPTIONS = "set_internal_options",
APPEND_INTERNAL_OPTIONS = "append_internal_options",
}
const singleSelectReducer = (
state: any,
{
actionType,
stateChanges,
}: { actionType: SingleSelectActionsTypes; stateChanges?: any }
) => {
switch (actionType) {
case SingleSelectActionsTypes.OPEN: {
return { ...state, open: true };
}
case SingleSelectActionsTypes.CLOSE: {
return { ...state, open: false, loadingInternalOptions: false };
}
case SingleSelectActionsTypes.LOAD_INTERNAL_OPTIONS_START: {
return { ...state, loadingInternalOptions: true };
}
case SingleSelectActionsTypes.LOAD_INTERNAL_OPTIONS_END: {
return { ...state, loadingInternalOptions: false };
}
case SingleSelectActionsTypes.SET_NETWORK_ERROR: {
return { ...state, ...stateChanges };
}
case SingleSelectActionsTypes.SET_INTERNAL_OPTIONS: {
return {
...state,
internalOptions: stateChanges.internalOptions,
loadingInternalOptions: false,
};
}
case SingleSelectActionsTypes.APPEND_INTERNAL_OPTIONS: {
return {
...state,
internalOptions: [...state.internalOptions, ...stateChanges.newOptions],
loadingInternalOptions: false,
};
}
default:
return { ...state, ...stateChanges };
}
};

const PaperComponent = (props: PaperProps, loadingInternalOptions: boolean) => (
<Paper {...props}>
{props.children}


);

export const SingleSelect = ({
options,
optionsApiEndPoint,
useCampxAxios = true,
required = false,
label,
}: SingleSelectProps) => {
const [state, dispatch] = useReducer(singleSelectReducer, {
open: false,
loadingInternalOptions: false,
internalOptions: options ?? [],
});
const { open, loadingInternalOptions, internalOptions } = state;
const listboxRef = useRef(null);

const internalAxios = useCampxAxios ? campxAxios : axios;

const handleOpen = async () => {
dispatch({
actionType: SingleSelectActionsTypes.OPEN,
});
if (optionsApiEndPoint) {
try {
dispatch({
actionType: SingleSelectActionsTypes.LOAD_INTERNAL_OPTIONS_START,
});
const options = await internalAxios
.get(optionsApiEndPoint, {
params: {
limit: 100,
offset: 0,
},
})
.then((res) => res.data);
dispatch({
actionType: SingleSelectActionsTypes.SET_INTERNAL_OPTIONS,
stateChanges: {
internalOptions: options,
},
});
} catch (e) {
dispatch({
actionType: SingleSelectActionsTypes.SET_NETWORK_ERROR,
stateChanges: {
error: e,
},
});
dispatch({
actionType: SingleSelectActionsTypes.LOAD_INTERNAL_OPTIONS_END,
});
}
}
};

const handleScroll = async (event: any) => {
const listboxNode = event.currentTarget;

console.log(
  listboxNode.scrollTop + listboxNode.clientHeight,
  listboxNode.scrollHeight
);
if (
  listboxNode.scrollTop + listboxNode.clientHeight >=
  listboxNode.scrollHeight - 100
) {
  console.log("dispatched");
  dispatch({
    actionType: SingleSelectActionsTypes.APPEND_INTERNAL_OPTIONS,
    stateChanges: {
      newOptions: internalOptions,
    },
  });
}

};

return (
<MuiAutocomplete
open={open}
autoHighlight={true}
renderInput={(params) => (

<TextField {...params} />

)}
PaperComponent={(props) => PaperComponent(props, loadingInternalOptions)}
renderOption={(props, option: any) => {
return (
<Box component="li" {...props}>

{option.label}
{option?.subLabel}


);
}}
ListboxProps={{
onScroll: handleScroll,
ref: listboxRef,
}}
onOpen={handleOpen}
onClose={() => {
dispatch({
actionType: SingleSelectActionsTypes.CLOSE,
});
}}
options={internalOptions}
/>
);
};

export default SingleSelect;`
I having the scroll issue even though my PaperComponent is outside main component and cannot pass the loading property as you said

@ZeeshanTamboli
Copy link
Member

@shashank-campx Please provide a StackBlitz reproduction or create a new issue with a reproduction.

@HelloIAmBanana
Copy link

HelloIAmBanana commented Sep 3, 2024

Yes, you can pass custom props to your custom Paper component using slotProps.paper prop. See https://stackblitz.com/edit/stackblitz-starters-szpsur?file=src%2FApp.tsx

@ZeeshanTamboli Hey, I tried doing the same thing you did in the stackblitz but it throws me the next error:

 Object literal may only specify known properties, and 'values' does not exist in type 'PaperProps'.ts(2353)

Do you have any idea why it doesnt allow me to add custom props?

@ZeeshanTamboli
Copy link
Member

@HelloIAmBanana PaperProps does not contain the values prop.

@HelloIAmBanana
Copy link

Yeah I got it but why doesn't it throw you an error aswell...?
Arnt we using the same PaperProps?
@ZeeshanTamboli

@ZeeshanTamboli
Copy link
Member

ZeeshanTamboli commented Sep 4, 2024

@HelloIAmBanana Thanks for pointing this out. You're correct—it does throw an error, but only in a local project, not sure why not on StackBlitz. The issue is that we aren't allowing custom props to be added through SlotPropsOverrides using TypeScript module augmentation. This is similar to issue #42601, but that one is for the Alert component. I also noticed an error on slots.paper.

To fix this, we can modify the code as follows:

--- a/packages/mui-material/src/Autocomplete/Autocomplete.d.ts
+++ b/packages/mui-material/src/Autocomplete/Autocomplete.d.ts
@@ -18,6 +18,13 @@ import useAutocomplete, {
 import { AutocompleteClasses } from './autocompleteClasses';
 import { CreateSlotsAndSlotProps, SlotProps } from '../utils/types';

+export interface AutocompleteChipSlotPropsOverrides {}
+export interface AutocompleteClearIndicatorSlotPropsOverrides {}
+export interface AutocompleteListboxSlotPropsOverrides {}
+export interface AutocompletePaperSlotPropsOverrides {}
+export interface AutocompletePopperSlotPropsOverrides {}
+export interface AutocompletePopupIndicatorSlotPropsOverrides {}
+
 export {
   AutocompleteChangeDetails,
   AutocompleteChangeReason,
@@ -89,17 +96,19 @@ export interface AutocompleteSlots {
    * The component used to render the listbox.
    * @default 'ul'
    */
-  listbox?: React.JSXElementConstructor<React.HTMLAttributes<HTMLElement>>;
+  listbox?: React.JSXElementConstructor<
+    React.HTMLAttributes<HTMLElement> & AutocompleteListboxSlotPropsOverrides
+  >;
   /**
    * The component used to render the body of the popup.
    * @default Paper
    */
-  paper?: React.JSXElementConstructor<PaperProps>;
+  paper?: React.JSXElementConstructor<PaperProps & AutocompletePaperSlotPropsOverrides>;
   /**
    * The component used to position the popup.
    * @default Popper
    */
-  popper?: React.JSXElementConstructor<PopperProps>;
+  popper?: React.JSXElementConstructor<PopperProps & AutocompletePopperSlotPropsOverrides>;
 }

 export type AutocompleteSlotsAndSlotProps<
@@ -113,12 +122,12 @@ export type AutocompleteSlotsAndSlotProps<
   {
     chip: SlotProps<
       React.ElementType<Partial<ChipProps<ChipComponent>>>,
-      {},
+      AutocompleteChipSlotPropsOverrides,
       AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
     >;
     clearIndicator: SlotProps<
       React.ElementType<Partial<IconButtonProps>>,
-      {},
+      AutocompleteClearIndicatorSlotPropsOverrides,
       AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
     >;
     /**
@@ -131,23 +140,23 @@ export type AutocompleteSlotsAndSlotProps<
           ref?: React.Ref<Element>;
         }
       >,
-      {},
+      AutocompleteListboxSlotPropsOverrides,
       AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
     >;

     paper: SlotProps<
       React.ElementType<Partial<PaperProps>>,
-      {},
+      AutocompletePaperSlotPropsOverrides,
       AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
     >;
     popper: SlotProps<
       React.ElementType<Partial<PopperProps>>,
-      {},
+      AutocompletePopperSlotPropsOverrides,
       AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
     >;
     popupIndicator: SlotProps<
       React.ElementType<Partial<IconButtonProps>>,
-      {},
+      AutocompletePopupIndicatorSlotPropsOverrides,
       AutocompleteOwnerState<Value, Multiple, DisableClearable, FreeSolo, ChipComponent>
     >;

This would allow custom props to be passed through TypeScript module augmentation, like:

import Autocomplete from '@mui/material/Autocomplete';
import Button from '@mui/material/Button';
import Paper, { PaperProps } from '@mui/material/Paper';
import TextField from '@mui/material/TextField';
import * as React from 'react';

declare module '@mui/material/Autocomplete' {
  interface AutocompletePaperSlotPropsOverrides {
    values: Options[];
  }
}

const CustomPaper = ({ children, values, ...paperProps }: PaperProps & { values: Options[] }) => (
  <Paper {...paperProps} onMouseDown={(event) => event.preventDefault()}>
    {children}
    <Button sx={{ width: '90px' }} variant="contained" disabled={values.length === 0}>
      Next
    </Button>
  </Paper>
);

interface Options {
  title: string;
  year: number;
}

export default function App() {
  const [values, setValues] = React.useState<Options[]>([]);
  return (
    <Autocomplete
      openOnFocus
      multiple
      disableCloseOnSelect
      isOptionEqualToValue={(o, v) => o.title == v.title}
      renderInput={(params) => <TextField {...params} placeholder={'Select'} />}
      onChange={(e, selected) => {
        setValues(selected);
      }}
      getOptionLabel={(option) => `(${option?.year}) ${option?.title}`}
      options={[...top100Films]}
      value={values}
      slots={{ paper: CustomPaper }}
      slotProps={{ paper: { values } }}
    />
  );
}

Would you like to create a new issue for this, and possibly submit a PR to fix it with a test?

@HelloIAmBanana
Copy link

HelloIAmBanana commented Sep 4, 2024

@ZeeshanTamboli Hey, thanks a lot for the quick response and solution! I don't mind creating an issue for it, but as for the PR, I have no clue on how to do it so I would rather not try it and risk messing things up haha 😅

EDIT:
#43609 I created the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: autocomplete This is the name of the generic UI component, not the React module! duplicate This issue or pull request already exists
Projects
None yet
Development

No branches or pull requests

6 participants