// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { memo } from 'react'; import type { FC, ReactNode } from 'react'; import { Select } from 'radix-ui'; import { AxoBaseMenu } from './_internal/AxoBaseMenu'; import { AxoSymbol } from './AxoSymbol'; import type { Styles } from './_internal/css'; import { css } from './_internal/css'; const Namespace = 'AxoSelect'; /** * Displays a list of options for the user to pick from—triggered by a button. * * @example Anatomy * ```tsx * export default () => ( * * * * * * * * * * * * ); * ``` */ // eslint-disable-next-line @typescript-eslint/no-namespace export namespace AxoSelect { /** * Component: * --------------------------- */ export type RootProps = Readonly<{ name?: string; form?: string; autoComplete?: string; disabled?: boolean; required?: boolean; open?: boolean; onOpenChange?: (open: boolean) => void; value: string | null; onValueChange: (value: string) => void; children: ReactNode; }>; /** * Contains all the parts of a select. */ export const Root: FC = memo(props => { return ( {props.children} ); }); Root.displayName = `${Namespace}.Root`; /** * Component: * --------------------------- */ const baseTriggerStyles = css( 'flex', 'rounded-full py-[5px] ps-3 pe-2.5 text-label-primary', 'disabled:text-label-disabled', 'outline-0 outline-border-focused focused:outline-[2.5px]' ); const TriggerVariants = { default: css( baseTriggerStyles, 'bg-fill-secondary', 'pressed:bg-fill-secondary-pressed' ), floating: css( baseTriggerStyles, 'bg-fill-floating', 'shadow-elevation-1', 'pressed:bg-fill-floating-pressed' ), borderless: css( baseTriggerStyles, 'bg-transparent', 'hovered:bg-fill-secondary', 'pressed:bg-fill-secondary-pressed' ), } as const satisfies Record; const TriggerWidths = { hug: css(), fixed: css('w-[120px]'), }; export type TriggerVariant = keyof typeof TriggerVariants; export type TriggerWidth = keyof typeof TriggerWidths; export type TriggerProps = Readonly<{ variant?: TriggerVariant; width?: TriggerWidth; placeholder: string; children?: ReactNode; }>; /** * The button that toggles the select. * The {@link AxoSelect.Content} will position itself by aligning over the * trigger. */ export const Trigger: FC = memo(props => { const variant = props.variant ?? 'default'; const width = props.width ?? 'hug'; const variantStyles = TriggerVariants[variant]; const widthStyles = TriggerWidths[width]; return ( {props.children} ); }); Trigger.displayName = `${Namespace}.Trigger`; /** * Component: * ------------------------------ */ export type ContentProps = Readonly<{ children: ReactNode; }>; /** * The component that pops out when the select is open. * Uses a portal to render the content part into the `body`. */ export const Content: FC = memo(props => { return ( {props.children} ); }); Content.displayName = `${Namespace}.Content`; /** * Component: * --------------------------- */ export type ItemProps = Readonly<{ value: string; disabled?: boolean; textValue?: string; children: ReactNode; }>; /** * The component that contains the select items. */ export const Item: FC = memo(props => { return ( {props.children} ); }); Item.displayName = `${Namespace}.Content`; /** * Component: * --------------------------- */ export type GroupProps = Readonly<{ children: ReactNode; }>; /** * Used to group multiple items. * Use in conjunction with {@link AxoSelect.Label to ensure good accessibility * via automatic labelling. */ export const Group: FC = memo(props => { return ( {props.children} ); }); Group.displayName = `${Namespace}.Group`; /** * Component: * --------------------------- */ export type LabelProps = Readonly<{ children: ReactNode; }>; /** * Used to render the label of a group. It won't be focusable using arrow keys. */ export const Label: FC = memo(props => { return ( {props.children} ); }); Label.displayName = `${Namespace}.Label`; /** * Component: * --------------------------- */ export type SeparatorProps = Readonly<{ // N/A }>; /** * Used to visually separate items in the select. */ export const Separator: FC = memo(() => { return ; }); Separator.displayName = `${Namespace}.Separator`; }