diff --git a/app/components/Collection/CollectionForm.tsx b/app/components/Collection/CollectionForm.tsx index 17c029a56f..feba61daf8 100644 --- a/app/components/Collection/CollectionForm.tsx +++ b/app/components/Collection/CollectionForm.tsx @@ -14,7 +14,7 @@ import Collection from "~/models/Collection"; import Button from "~/components/Button"; import Flex from "~/components/Flex"; import Input from "~/components/Input"; -import InputSelectPermission from "~/components/InputSelectPermission"; +import { InputSelectPermission } from "~/components/InputSelectPermission"; import { createLazyComponent } from "~/components/LazyLoad"; import Switch from "~/components/Switch"; import Text from "~/components/Text"; @@ -172,7 +172,7 @@ export const CollectionForm = observer(function CollectionForm_({ ) => { field.onChange(value === EmptySelectValue ? null : value); }} - note={t( + help={t( "The default access for workspace members, you can share with more users or groups later." )} /> diff --git a/app/components/InputSelectNew.tsx b/app/components/InputSelectNew.tsx index e0ba401d35..e0e52840ac 100644 --- a/app/components/InputSelectNew.tsx +++ b/app/components/InputSelectNew.tsx @@ -1,14 +1,18 @@ import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; +import { QuestionMarkIcon } from "outline-icons"; import { transparentize } from "polished"; import * as React from "react"; import styled from "styled-components"; +import { s } from "@shared/styles"; import Text from "~/components/Text"; import useMobile from "~/hooks/useMobile"; import Separator from "./ContextMenu/Separator"; import Flex from "./Flex"; import { LabelText } from "./Input"; +import NudeButton from "./NudeButton"; import Scrollable from "./Scrollable"; import { IconWrapper } from "./Sidebar/components/SidebarLink"; +import Tooltip from "./Tooltip"; import { Drawer, DrawerContent, @@ -53,7 +57,7 @@ type Props = { /* Options to display in the select menu. */ options: Option[]; /* Current chosen value. */ - value?: string; + value?: string | null; /* Callback when an option is selected. */ onChange: (value: string) => void; /* ARIA label for accessibility. */ @@ -66,231 +70,259 @@ type Props = { disabled?: boolean; /* When true, width of the menu trigger is restricted. Otherwise, takes up the full width of parent. */ short?: boolean; + /** Display a tooltip with the descriptive help text about the select menu. */ + help?: string; } & TriggerButtonProps; -export function InputSelectNew(props: Props) { - const { - options, - value, - onChange, - ariaLabel, - label, - hideLabel, - disabled, - short, - ...triggerProps - } = props; +export const InputSelectNew = React.forwardRef( + (props, ref) => { + const { + options, + value, + onChange, + ariaLabel, + label, + hideLabel, + short, + help, + ...triggerProps + } = props; - const [localValue, setLocalValue] = React.useState(value); - const [open, setOpen] = React.useState(false); + const [localValue, setLocalValue] = React.useState(value); + const [open, setOpen] = React.useState(false); - const triggerRef = - React.useRef>(null); - const contentRef = - React.useRef>(null); + const contentRef = + React.useRef>(null); - const isMobile = useMobile(); + const isMobile = useMobile(); - const placeholder = `Select a ${ariaLabel.toLowerCase()}`; - const optionsHaveIcon = options.some( - (opt) => opt.type === "item" && !!opt.icon - ); + const placeholder = `Select a ${ariaLabel.toLowerCase()}`; + const optionsHaveIcon = options.some( + (opt) => opt.type === "item" && !!opt.icon + ); - const renderOption = React.useCallback( - (option: Option) => { - if (option.type === "separator") { - return ; + const renderOption = React.useCallback( + (option: Option) => { + if (option.type === "separator") { + return ; + } + + return ( + + + ); + }, + [optionsHaveIcon] + ); + + const onValueChange = React.useCallback( + async (val: string) => { + setLocalValue(val); + onChange(val); + }, + [onChange, setLocalValue] + ); + + const enablePointerEvents = React.useCallback(() => { + if (contentRef.current) { + contentRef.current.style.pointerEvents = "auto"; } + }, []); + const disablePointerEvents = React.useCallback(() => { + if (contentRef.current) { + contentRef.current.style.pointerEvents = "none"; + } + }, []); + + React.useEffect(() => { + setLocalValue(value); + }, [value]); + + if (isMobile) { return ( - - + ); - }, - [optionsHaveIcon] - ); - - const onValueChange = React.useCallback( - async (val: string) => { - setLocalValue(val); - onChange(val); - }, - [onChange, setLocalValue] - ); - - const enablePointerEvents = React.useCallback(() => { - if (contentRef.current) { - contentRef.current.style.pointerEvents = "auto"; } - }, []); - const disablePointerEvents = React.useCallback(() => { - if (contentRef.current) { - contentRef.current.style.pointerEvents = "none"; - } - }, []); - - React.useEffect(() => { - setLocalValue(value); - }, [value]); - - if (isMobile) { return ( - + + ); } - - return ( - - - ); -} +); +InputSelectNew.displayName = "InputSelect"; type MobileSelectProps = Props & { placeholder: string; optionsHaveIcon: boolean; }; -function MobileSelect(props: MobileSelectProps) { - const { - options, - value, - onChange, - ariaLabel, - label, - hideLabel, - disabled, - short, - placeholder, - optionsHaveIcon, - ...triggerProps - } = props; +const MobileSelect = React.forwardRef( + (props, ref) => { + const { + options, + value, + onChange, + ariaLabel, + label, + hideLabel, + disabled, + short, + placeholder, + optionsHaveIcon, + ...triggerProps + } = props; - const [open, setOpen] = React.useState(false); - const contentRef = React.useRef>(null); + const [open, setOpen] = React.useState(false); + const contentRef = + React.useRef>(null); - const selectedOption = React.useMemo( - () => - value - ? options.find((opt) => opt.type === "item" && opt.value === value) - : undefined, - [value, options] - ); + const selectedOption = React.useMemo( + () => + value + ? options.find((opt) => opt.type === "item" && opt.value === value) + : undefined, + [value, options] + ); - const handleSelect = React.useCallback( - async (val: string) => { - setOpen(false); - onChange(val); - }, - [onChange] - ); + const handleSelect = React.useCallback( + async (val: string) => { + setOpen(false); + onChange(val); + }, + [onChange] + ); - const renderOption = React.useCallback( - (option: Option) => { - if (option.type === "separator") { - return ; - } + const renderOption = React.useCallback( + (option: Option) => { + if (option.type === "separator") { + return ; + } - const isSelected = option === selectedOption; + const isSelected = option === selectedOption; - return ( - handleSelect(option.value)} - data-state={isSelected ? "checked" : "unchecked"} - > - - ); - }, - [handleSelect, selectedOption, optionsHaveIcon] - ); - - const enablePointerEvents = React.useCallback(() => { - if (contentRef.current) { - contentRef.current.style.pointerEvents = "auto"; - } - }, []); - - const disablePointerEvents = React.useCallback(() => { - if (contentRef.current) { - contentRef.current.style.pointerEvents = "none"; - } - }, []); - - return ( - - - ); -} +