InputSelectPermission component (#9585)

This commit is contained in:
Hemachandar
2025-07-10 18:16:19 +05:30
committed by GitHub
parent 7dd0616b8c
commit d4cdf4202f
8 changed files with 288 additions and 234 deletions

View File

@@ -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."
)}
/>

View File

@@ -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,9 +70,12 @@ 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) {
export const InputSelectNew = React.forwardRef<HTMLButtonElement, Props>(
(props, ref) => {
const {
options,
value,
@@ -76,16 +83,14 @@ export function InputSelectNew(props: Props) {
ariaLabel,
label,
hideLabel,
disabled,
short,
help,
...triggerProps
} = props;
const [localValue, setLocalValue] = React.useState(value);
const [open, setOpen] = React.useState(false);
const triggerRef =
React.useRef<React.ElementRef<typeof InputSelectTrigger>>(null);
const contentRef =
React.useRef<React.ElementRef<typeof InputSelectContent>>(null);
@@ -138,6 +143,7 @@ export function InputSelectNew(props: Props) {
if (isMobile) {
return (
<MobileSelect
ref={ref}
{...props}
value={localValue}
onChange={onValueChange}
@@ -149,15 +155,15 @@ export function InputSelectNew(props: Props) {
return (
<Wrapper short={short}>
<Label text={label} hidden={hideLabel ?? false} />
<Label text={label} hidden={hideLabel ?? false} help={help} />
<InputSelectRoot
open={open}
onOpenChange={setOpen}
value={localValue}
value={localValue ?? undefined}
onValueChange={onValueChange}
>
<InputSelectTrigger
ref={triggerRef}
ref={ref}
placeholder={placeholder}
{...triggerProps}
/>
@@ -173,13 +179,16 @@ export function InputSelectNew(props: Props) {
</Wrapper>
);
}
);
InputSelectNew.displayName = "InputSelect";
type MobileSelectProps = Props & {
placeholder: string;
optionsHaveIcon: boolean;
};
function MobileSelect(props: MobileSelectProps) {
const MobileSelect = React.forwardRef<HTMLButtonElement, MobileSelectProps>(
(props, ref) => {
const {
options,
value,
@@ -195,7 +204,8 @@ function MobileSelect(props: MobileSelectProps) {
} = props;
const [open, setOpen] = React.useState(false);
const contentRef = React.useRef<React.ElementRef<typeof DrawerContent>>(null);
const contentRef =
React.useRef<React.ElementRef<typeof DrawerContent>>(null);
const selectedOption = React.useMemo(
() =>
@@ -253,6 +263,7 @@ function MobileSelect(props: MobileSelectProps) {
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<SelectButton
ref={ref}
{...triggerProps}
neutral
disclosure
@@ -283,14 +294,35 @@ function MobileSelect(props: MobileSelectProps) {
</Wrapper>
);
}
);
MobileSelect.displayName = "InputSelect";
function Label({ text, hidden }: { text: string; hidden: boolean }) {
const labelText = <LabelText>{text}</LabelText>;
function Label({
text,
hidden,
help,
}: {
text: string;
hidden: boolean;
help?: string;
}) {
const content = (
<Flex align="center" gap={2} style={{ marginBottom: "4px" }}>
<LabelText style={{ paddingBottom: 0 }}>{text}</LabelText>
{help ? (
<Tooltip content={help}>
<TooltipButton size={18}>
<QuestionMarkIcon size={18} />
</TooltipButton>
</Tooltip>
) : null}
</Flex>
);
return hidden ? (
<VisuallyHidden.Root>{labelText}</VisuallyHidden.Root>
<VisuallyHidden.Root>{content}</VisuallyHidden.Root>
) : (
labelText
content
);
}
@@ -352,3 +384,12 @@ const IconSpacer = styled.div`
const StyledScrollable = styled(Scrollable)`
max-height: 75vh;
`;
const TooltipButton = styled(NudeButton)`
color: ${s("textSecondary")};
&:hover,
&[aria-expanded="true"] {
background: none !important;
}
`;

View File

@@ -1,54 +1,67 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { $Diff } from "utility-types";
import { s } from "@shared/styles";
import { CollectionPermission } from "@shared/types";
import {
InputSelectNew,
Option as OptionNew,
} from "~/components/InputSelectNew";
import { EmptySelectValue } from "~/types";
import InputSelect, { Props, Option, InputSelectRef } from "./InputSelect";
function InputSelectPermission(
props: $Diff<
Props,
{
options: Array<Option>;
ariaLabel: string;
}
>,
ref: React.RefObject<InputSelectRef>
) {
const { value, onChange, ...rest } = props;
type Props = {
shrink?: boolean;
} & Pick<
React.ComponentProps<typeof InputSelectNew>,
"value" | "onChange" | "disabled" | "hideLabel" | "nude" | "help"
>;
export const InputSelectPermission = React.forwardRef<HTMLButtonElement, Props>(
(props, ref) => {
const { value, onChange, shrink, ...rest } = props;
const { t } = useTranslation();
return (
<Select
ref={ref}
label={t("Permission")}
options={[
const options = React.useMemo<OptionNew[]>(
() => [
{
type: "item",
label: t("View only"),
value: CollectionPermission.Read,
},
{
type: "item",
label: t("Can edit"),
value: CollectionPermission.ReadWrite,
},
{
divider: true,
type: "separator",
},
{
type: "item",
label: t("No access"),
value: EmptySelectValue,
},
]}
ariaLabel={t("Default access")}
],
[t]
);
return (
<Select
ref={ref}
options={options}
value={value || EmptySelectValue}
onChange={onChange}
ariaLabel={t("Default access")}
label={t("Permission")}
$shrink={shrink}
{...rest}
/>
);
}
);
InputSelectPermission.displayName = "InputSelectPermission";
const Select = styled(InputSelect)`
const Select = styled(InputSelectNew)<{ $shrink?: boolean }>`
color: ${s("textSecondary")};
${({ $shrink }) => !$shrink && "margin-bottom: 16px;"}
`;
export default React.forwardRef(InputSelectPermission);

View File

@@ -9,7 +9,7 @@ import { CollectionPermission } from "@shared/types";
import Collection from "~/models/Collection";
import { Avatar, GroupAvatar, AvatarSize } from "~/components/Avatar";
import InputMemberPermissionSelect from "~/components/InputMemberPermissionSelect";
import InputSelectPermission from "~/components/InputSelectPermission";
import { InputSelectPermission } from "~/components/InputSelectPermission";
import Scrollable from "~/components/Scrollable";
import useMaxHeight from "~/hooks/useMaxHeight";
import usePolicy from "~/hooks/usePolicy";
@@ -121,7 +121,6 @@ export const AccessControlList = observer(
actions={
<div style={{ marginRight: -8 }}>
<InputSelectPermission
style={{ margin: 0 }}
onChange={(
value: CollectionPermission | typeof EmptySelectValue
) => {
@@ -131,8 +130,9 @@ export const AccessControlList = observer(
}}
disabled={!can.update}
value={collection?.permission}
labelHidden
hideLabel
nude
shrink
/>
</div>
}

View File

@@ -30,11 +30,11 @@ const InputSelectTrigger = React.forwardRef<
React.ElementRef<typeof InputSelectPrimitive.Trigger>,
InputSelectTriggerProps
>((props, ref) => {
const { placeholder, children, ...buttonProps } = props;
const { placeholder, children, nude, ...buttonProps } = props;
return (
<InputSelectPrimitive.Trigger ref={ref} asChild>
<SelectButton neutral disclosure {...buttonProps}>
<SelectButton neutral disclosure $nude={nude} {...buttonProps}>
<InputSelectPrimitive.Value placeholder={placeholder} />
</SelectButton>
</InputSelectPrimitive.Trigger>

View File

@@ -10,7 +10,7 @@ import { AttachmentPreset, CollectionPermission } from "@shared/types";
import { bytesToHumanReadable } from "@shared/utils/files";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import InputSelectPermission from "~/components/InputSelectPermission";
import { InputSelectPermission } from "~/components/InputSelectPermission";
import LoadingIndicator from "~/components/LoadingIndicator";
import Text from "~/components/Text";
import useStores from "~/hooks/useStores";

View File

@@ -5,7 +5,7 @@ import { ImportInput } from "@shared/schema";
import { CollectionPermission, IntegrationService } from "@shared/types";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import InputSelectPermission from "~/components/InputSelectPermission";
import { InputSelectPermission } from "~/components/InputSelectPermission";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";

View File

@@ -289,11 +289,11 @@
"Flags": "Flags",
"Select a color": "Select a color",
"Loading": "Loading",
"Permission": "Permission",
"View only": "View only",
"Can edit": "Can edit",
"No access": "No access",
"Default access": "Default access",
"Permission": "Permission",
"Change Language": "Change Language",
"Dismiss": "Dismiss",
"Youre offline.": "Youre offline.",