From 5aefa49d7d80ce3cd7e79a1f70cb3c5aec66dd7d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:01:00 +1000 Subject: [PATCH] fix(ui): popover ref & wrapping of children (wip) --- invokeai/frontend/web/public/locales/en.json | 18 ++-- .../components/IAIInformationalPopover.tsx | 86 +++++++++++-------- .../components/IAIMantineMultiSelect.tsx | 10 ++- .../components/IAIMantineSearchableSelect.tsx | 10 ++- .../common/components/IAIMantineSelect.tsx | 10 ++- .../src/common/components/IAINumberInput.tsx | 8 +- .../web/src/common/components/IAISlider.tsx | 8 +- .../web/src/common/components/IAISwitch.tsx | 2 + .../Parameters/Core/ParamIterations.tsx | 4 +- .../Core/ParamNegativeConditioning.tsx | 47 +++++----- .../Core/ParamPositiveConditioning.tsx | 23 ++--- .../MainModel/ParamMainModelSelect.tsx | 18 ++-- .../web/src/theme/components/popover.ts | 2 +- 13 files changed, 140 insertions(+), 106 deletions(-) diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index c309e4de50..f48df3368f 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1213,14 +1213,14 @@ }, "dynamicPromptsCombinatorial": { "heading": "Combinatorial Generation", - "paragraph": "Generate an image for every possible combination of Dynamic Prompt until the Max Prompts is reached." + "paragraph": "Generate an image for every possible combination of Dynamic Prompts until the Max Prompts is reached." }, "infillMethod": { "heading": "Infill Method", "paragraph": "Method to infill the selected area." }, "lora": { - "heading": "LoRA", + "heading": "LoRA Weight", "paragraph": "Weight of the LoRA. Higher weight will lead to larger impacts on the final image." }, "noiseEnable": { @@ -1239,21 +1239,21 @@ "heading": "Denoising Strength", "paragraph": "How much noise is added to the input image. 0 will result in an identical image, while 1 will result in a completely new image." }, - "paramImages": { - "heading": "Images", - "paragraph": "Number of images that will be generated." + "paramIterations": { + "heading": "Iterations", + "paragraph": "The number of images to generate. If Dynamic Prompts is enabled, each of the prompts will be generated this many times." }, "paramModel": { "heading": "Model", "paragraph": "Model used for the denoising steps. Different models are trained to specialize in producing different aesthetic results and content." }, "paramNegativeConditioning": { - "heading": "Negative Prompts", - "paragraph": "This is where you enter your negative prompts." + "heading": "Negative Prompt", + "paragraph": "The generation process avoids the concepts in the negative prompt. Use this to exclude qualities or objects from the output. Supports Compel syntax and embeddings." }, "paramPositiveConditioning": { - "heading": "Positive Prompts", - "paragraph": "This is where you enter your positive prompts." + "heading": "Positive Prompt", + "paragraph": "Guides the generation process. You may use any words or phrases. Supports Compel and Dynamic Prompts syntaxes and embeddings." }, "paramRatio": { "heading": "Ratio", diff --git a/invokeai/frontend/web/src/common/components/IAIInformationalPopover.tsx b/invokeai/frontend/web/src/common/components/IAIInformationalPopover.tsx index 876ce6488a..4a9dd82665 100644 --- a/invokeai/frontend/web/src/common/components/IAIInformationalPopover.tsx +++ b/invokeai/frontend/web/src/common/components/IAIInformationalPopover.tsx @@ -1,37 +1,43 @@ import { + Box, Button, - Popover, - PopoverTrigger, - PopoverContent, - PopoverArrow, - PopoverCloseButton, - PopoverHeader, - PopoverBody, - PopoverProps, + Divider, Flex, - Text, + Heading, Image, + Popover, + PopoverArrow, + PopoverBody, + PopoverCloseButton, + PopoverContent, + PopoverProps, + PopoverTrigger, + Portal, + Text, } from '@chakra-ui/react'; -import { useAppSelector } from '../../app/store/storeHooks'; +import { ReactNode, memo } from 'react'; import { useTranslation } from 'react-i18next'; +import { useAppSelector } from '../../app/store/storeHooks'; -interface Props extends PopoverProps { +const OPEN_DELAY = 1500; + +type Props = Omit<PopoverProps, 'children'> & { details: string; - children: JSX.Element; + children: ReactNode; image?: string; buttonLabel?: string; buttonHref?: string; placement?: PopoverProps['placement']; -} +}; -function IAIInformationalPopover({ +const IAIInformationalPopover = ({ details, image, buttonLabel, buttonHref, children, placement, -}: Props): JSX.Element { +}: Props) => { const shouldEnableInformationalPopovers = useAppSelector( (state) => state.system.shouldEnableInformationalPopovers ); @@ -41,18 +47,21 @@ function IAIInformationalPopover({ const paragraph = t(`popovers.${details}.paragraph`); if (!shouldEnableInformationalPopovers) { - return children; - } else { - return ( - <Popover - placement={placement || 'top'} - closeOnBlur={false} - trigger="hover" - variant="informational" - > - <PopoverTrigger> - <div>{children}</div> - </PopoverTrigger> + return <>{children}</>; + } + + return ( + <Popover + placement={placement || 'top'} + closeOnBlur={false} + trigger="hover" + variant="informational" + openDelay={OPEN_DELAY} + > + <PopoverTrigger> + <Box w="full">{children}</Box> + </PopoverTrigger> + <Portal> <PopoverContent> <PopoverArrow /> <PopoverCloseButton /> @@ -83,14 +92,17 @@ function IAIInformationalPopover({ gap: 3, flexDirection: 'column', width: '100%', - p: 3, - pt: heading ? 0 : 3, }} > - {heading && <PopoverHeader>{heading}</PopoverHeader>} - <Text sx={{ px: 3 }}>{paragraph}</Text> + {heading && ( + <> + <Heading size="sm">{heading}</Heading> + <Divider /> + </> + )} + <Text>{paragraph}</Text> {buttonLabel && ( - <Flex sx={{ px: 3 }} justifyContent="flex-end"> + <Flex justifyContent="flex-end"> <Button onClick={() => window.open(buttonHref)} size="sm" @@ -104,9 +116,9 @@ function IAIInformationalPopover({ </Flex> </PopoverBody> </PopoverContent> - </Popover> - ); - } -} + </Portal> + </Popover> + ); +}; -export default IAIInformationalPopover; +export default memo(IAIInformationalPopover); diff --git a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx index 28c680b824..07a7ada88d 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx @@ -1,4 +1,4 @@ -import { FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import { FormControl, FormLabel, Tooltip, forwardRef } from '@chakra-ui/react'; import { MultiSelect, MultiSelectProps } from '@mantine/core'; import { useAppDispatch } from 'app/store/storeHooks'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice'; @@ -11,7 +11,7 @@ type IAIMultiSelectProps = Omit<MultiSelectProps, 'label'> & { label?: string; }; -const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { +const IAIMantineMultiSelect = forwardRef((props: IAIMultiSelectProps, ref) => { const { searchable = true, tooltip, @@ -47,7 +47,7 @@ const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { <MultiSelect label={ label ? ( - <FormControl isDisabled={disabled}> + <FormControl ref={ref} isDisabled={disabled}> <FormLabel>{label}</FormLabel> </FormControl> ) : undefined @@ -63,6 +63,8 @@ const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { /> </Tooltip> ); -}; +}); + +IAIMantineMultiSelect.displayName = 'IAIMantineMultiSelect'; export default memo(IAIMantineMultiSelect); diff --git a/invokeai/frontend/web/src/common/components/IAIMantineSearchableSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineSearchableSelect.tsx index 079421d4e5..1ed8490848 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineSearchableSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineSearchableSelect.tsx @@ -1,4 +1,4 @@ -import { FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import { FormControl, FormLabel, Tooltip, forwardRef } from '@chakra-ui/react'; import { Select, SelectProps } from '@mantine/core'; import { useAppDispatch } from 'app/store/storeHooks'; import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice'; @@ -17,7 +17,7 @@ type IAISelectProps = Omit<SelectProps, 'label'> & { inputRef?: RefObject<HTMLInputElement>; }; -const IAIMantineSearchableSelect = (props: IAISelectProps) => { +const IAIMantineSearchableSelect = forwardRef((props: IAISelectProps, ref) => { const { searchable = true, tooltip, @@ -74,7 +74,7 @@ const IAIMantineSearchableSelect = (props: IAISelectProps) => { ref={inputRef} label={ label ? ( - <FormControl isDisabled={disabled}> + <FormControl ref={ref} isDisabled={disabled}> <FormLabel>{label}</FormLabel> </FormControl> ) : undefined @@ -92,6 +92,8 @@ const IAIMantineSearchableSelect = (props: IAISelectProps) => { /> </Tooltip> ); -}; +}); + +IAIMantineSearchableSelect.displayName = 'IAIMantineSearchableSelect'; export default memo(IAIMantineSearchableSelect); diff --git a/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx index 46b5fc95f6..19c116bfd5 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineSelect.tsx @@ -1,4 +1,4 @@ -import { FormControl, FormLabel, Tooltip } from '@chakra-ui/react'; +import { FormControl, FormLabel, Tooltip, forwardRef } from '@chakra-ui/react'; import { Select, SelectProps } from '@mantine/core'; import { useMantineSelectStyles } from 'mantine-theme/hooks/useMantineSelectStyles'; import { RefObject, memo } from 'react'; @@ -15,7 +15,7 @@ export type IAISelectProps = Omit<SelectProps, 'label'> & { label?: string; }; -const IAIMantineSelect = (props: IAISelectProps) => { +const IAIMantineSelect = forwardRef((props: IAISelectProps, ref) => { const { tooltip, inputRef, label, disabled, required, ...rest } = props; const styles = useMantineSelectStyles(); @@ -25,7 +25,7 @@ const IAIMantineSelect = (props: IAISelectProps) => { <Select label={ label ? ( - <FormControl isRequired={required} isDisabled={disabled}> + <FormControl ref={ref} isRequired={required} isDisabled={disabled}> <FormLabel>{label}</FormLabel> </FormControl> ) : undefined @@ -37,6 +37,8 @@ const IAIMantineSelect = (props: IAISelectProps) => { /> </Tooltip> ); -}; +}); + +IAIMantineSelect.displayName = 'IAIMantineSelect'; export default memo(IAIMantineSelect); diff --git a/invokeai/frontend/web/src/common/components/IAINumberInput.tsx b/invokeai/frontend/web/src/common/components/IAINumberInput.tsx index de3b44564a..243dac9d63 100644 --- a/invokeai/frontend/web/src/common/components/IAINumberInput.tsx +++ b/invokeai/frontend/web/src/common/components/IAINumberInput.tsx @@ -13,6 +13,7 @@ import { NumberInputStepperProps, Tooltip, TooltipProps, + forwardRef, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { stopPastePropagation } from 'common/util/stopPastePropagation'; @@ -50,7 +51,7 @@ interface Props extends Omit<NumberInputProps, 'onChange'> { /** * Customized Chakra FormControl + NumberInput multi-part component. */ -const IAINumberInput = (props: Props) => { +const IAINumberInput = forwardRef((props: Props, ref) => { const { label, isDisabled = false, @@ -141,6 +142,7 @@ const IAINumberInput = (props: Props) => { return ( <Tooltip {...tooltipProps}> <FormControl + ref={ref} isDisabled={isDisabled} isInvalid={isInvalid} {...formControlProps} @@ -172,6 +174,8 @@ const IAINumberInput = (props: Props) => { </FormControl> </Tooltip> ); -}; +}); + +IAINumberInput.displayName = 'IAINumberInput'; export default memo(IAINumberInput); diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index fd3eed754f..e879c00977 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -22,6 +22,7 @@ import { SliderTrackProps, Tooltip, TooltipProps, + forwardRef, } from '@chakra-ui/react'; import { useAppDispatch } from 'app/store/storeHooks'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; @@ -71,7 +72,7 @@ export type IAIFullSliderProps = { sliderIAIIconButtonProps?: IAIIconButtonProps; }; -const IAISlider = (props: IAIFullSliderProps) => { +const IAISlider = forwardRef((props: IAIFullSliderProps, ref) => { const [showTooltip, setShowTooltip] = useState(false); const { label, @@ -187,6 +188,7 @@ const IAISlider = (props: IAIFullSliderProps) => { return ( <FormControl + ref={ref} onClick={forceInputBlur} sx={ isCompact @@ -354,6 +356,8 @@ const IAISlider = (props: IAIFullSliderProps) => { </HStack> </FormControl> ); -}; +}); + +IAISlider.displayName = 'IAISlider'; export default memo(IAISlider); diff --git a/invokeai/frontend/web/src/common/components/IAISwitch.tsx b/invokeai/frontend/web/src/common/components/IAISwitch.tsx index da0883d77e..8773be49e5 100644 --- a/invokeai/frontend/web/src/common/components/IAISwitch.tsx +++ b/invokeai/frontend/web/src/common/components/IAISwitch.tsx @@ -72,4 +72,6 @@ const IAISwitch = (props: IAISwitchProps) => { ); }; +IAISwitch.displayName = 'IAISwitch'; + export default memo(IAISwitch); diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamIterations.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamIterations.tsx index f9df43d7ca..429291190b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamIterations.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamIterations.tsx @@ -61,7 +61,7 @@ const ParamIterations = ({ asSlider }: Props) => { }, [dispatch, initial]); return asSlider || shouldUseSliders ? ( - <IAIInformationalPopover details="paramImages"> + <IAIInformationalPopover details="paramIterations"> <IAISlider label={t('parameters.iterations')} step={step} @@ -77,7 +77,7 @@ const ParamIterations = ({ asSlider }: Props) => { /> </IAIInformationalPopover> ) : ( - <IAIInformationalPopover details="paramImages"> + <IAIInformationalPopover details="paramIterations"> <IAINumberInput label={t('parameters.iterations')} step={step} diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx index 1154187eaf..a54b1489e3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx @@ -1,6 +1,7 @@ import { Box, FormControl, useDisclosure } from '@chakra-ui/react'; import type { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; import IAITextarea from 'common/components/IAITextarea'; import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton'; import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover'; @@ -9,7 +10,6 @@ import { ChangeEvent, KeyboardEvent, memo, useCallback, useRef } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { useFeatureStatus } from '../../../../system/hooks/useFeatureStatus'; -import IAIInformationalPopover from 'common/components/IAIInformationalPopover'; const ParamNegativeConditioning = () => { const negativePrompt = useAppSelector( @@ -76,13 +76,16 @@ const ParamNegativeConditioning = () => { const isEmbeddingEnabled = useFeatureStatus('embedding').isFeatureEnabled; return ( - <FormControl> - <ParamEmbeddingPopover - isOpen={isOpen} - onClose={onClose} - onSelect={handleSelectEmbedding} - > - <IAIInformationalPopover details="paramNegativeConditioning"> + <IAIInformationalPopover + placement="right" + details="paramNegativeConditioning" + > + <FormControl> + <ParamEmbeddingPopover + isOpen={isOpen} + onClose={onClose} + onSelect={handleSelectEmbedding} + > <IAITextarea id="negativePrompt" name="negativePrompt" @@ -95,20 +98,20 @@ const ParamNegativeConditioning = () => { minH={16} {...(isEmbeddingEnabled && { onKeyDown: handleKeyDown })} /> - </IAIInformationalPopover> - </ParamEmbeddingPopover> - {!isOpen && isEmbeddingEnabled && ( - <Box - sx={{ - position: 'absolute', - top: 0, - insetInlineEnd: 0, - }} - > - <AddEmbeddingButton onClick={onOpen} /> - </Box> - )} - </FormControl> + </ParamEmbeddingPopover> + {!isOpen && isEmbeddingEnabled && ( + <Box + sx={{ + position: 'absolute', + top: 0, + insetInlineEnd: 0, + }} + > + <AddEmbeddingButton onClick={onOpen} /> + </Box> + )} + </FormControl> + </IAIInformationalPopover> ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index 8a78621463..9ff616084d 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -104,13 +104,16 @@ const ParamPositiveConditioning = () => { return ( <Box position="relative"> - <FormControl> - <ParamEmbeddingPopover - isOpen={isOpen} - onClose={onClose} - onSelect={handleSelectEmbedding} - > - <IAIInformationalPopover details="paramPositiveConditioning"> + <IAIInformationalPopover + placement="right" + details="paramPositiveConditioning" + > + <FormControl> + <ParamEmbeddingPopover + isOpen={isOpen} + onClose={onClose} + onSelect={handleSelectEmbedding} + > <IAITextarea id="prompt" name="prompt" @@ -122,9 +125,9 @@ const ParamPositiveConditioning = () => { resize="vertical" minH={32} /> - </IAIInformationalPopover> - </ParamEmbeddingPopover> - </FormControl> + </ParamEmbeddingPopover> + </FormControl> + </IAIInformationalPopover> {!isOpen && isEmbeddingEnabled && ( <Box sx={{ diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/MainModel/ParamMainModelSelect.tsx index 9c957523bc..4252310fc0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/MainModel/ParamMainModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/MainModel/ParamMainModelSelect.tsx @@ -119,8 +119,8 @@ const ParamMainModelSelect = () => { data={[]} /> ) : ( - <IAIInformationalPopover details="paramModel" placement="bottom"> - <Flex w="100%" alignItems="center" gap={3}> + <Flex w="100%" alignItems="center" gap={3}> + <IAIInformationalPopover details="paramModel" placement="bottom"> <IAIMantineSearchableSelect tooltip={selectedModel?.description} label={t('modelManager.model')} @@ -134,13 +134,13 @@ const ParamMainModelSelect = () => { onChange={handleChangeModel} w="100%" /> - {isSyncModelEnabled && ( - <Box mt={7}> - <SyncModelsButton iconMode /> - </Box> - )} - </Flex> - </IAIInformationalPopover> + </IAIInformationalPopover> + {isSyncModelEnabled && ( + <Box mt={7}> + <SyncModelsButton iconMode /> + </Box> + )} + </Flex> ); }; diff --git a/invokeai/frontend/web/src/theme/components/popover.ts b/invokeai/frontend/web/src/theme/components/popover.ts index 55f69e9036..0facb25849 100644 --- a/invokeai/frontend/web/src/theme/components/popover.ts +++ b/invokeai/frontend/web/src/theme/components/popover.ts @@ -37,7 +37,7 @@ const informationalContent = defineStyle((props) => { 'colors.base.400', 'colors.base.400' )(props), - p: 0, + p: 4, bg: mode('base.100', 'base.600')(props), border: 'none', shadow: 'dark-lg',