Init AxoRadioGroupList

This commit is contained in:
Jamie
2026-08-20 17:58:39 +00:00
committed by GitHub
parent f0af69d92c
commit 4d47b9aa3e
18 changed files with 824 additions and 434 deletions
+20 -49
View File
@@ -1,25 +1,12 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import {
memo,
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
} from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DropdownMenu } from 'radix-ui';
import type { FC, ReactNode } from 'react';
import { computeAccessibleName } from 'dom-accessibility-api';
import { AxoSymbol } from './AxoSymbol.dom.tsx';
import { AxoBaseMenu } from './_internal/AxoBaseMenu.dom.tsx';
import { tw } from './tw.dom.tsx';
import {
AriaLabellingProvider,
useAriaLabellingContext,
useCreateAriaLabellingContext,
} from './_internal/AriaLabellingContext.dom.tsx';
import { assert } from './_internal/assert.std.tsx';
import {
getElementAriaRole,
@@ -32,6 +19,7 @@ import {
import { isTestOrMockEnvironment } from '../environment.std.ts';
import { AxoDragRegion } from './AxoDragRegion.dom.tsx';
import { AxoTheme } from './AxoTheme.dom.tsx';
import { AriaLabelled } from './aria/AriaLabelled.dom.tsx';
const { useDisableDragRegions } = AxoDragRegion;
@@ -218,28 +206,25 @@ export namespace AxoDropdownMenu {
* Uses a portal to render the content part into the `body`.
*/
export const Content: FC<ContentProps> = memo(props => {
const { context, labelId, descriptionId } = useCreateAriaLabellingContext();
const { open } = useStrictContext(RootContext);
return (
<AriaLabellingProvider value={context}>
<DropdownMenu.Portal>
<AxoTheme.Inherit>
<DropdownMenu.Portal>
<AxoTheme.Inherit>
<AriaLabelled.Root asChild>
<DropdownMenu.Content
sideOffset={4}
align={props.align}
side={props.side}
collisionPadding={6}
className={AxoBaseMenu.menuContentStyles}
aria-labelledby={labelId}
aria-describedby={descriptionId}
onCloseAutoFocus={props.onCloseAutoFocus}
inert={!open}
>
{props.children}
</DropdownMenu.Content>
</AxoTheme.Inherit>
</DropdownMenu.Portal>
</AriaLabellingProvider>
</AriaLabelled.Root>
</AxoTheme.Inherit>
</DropdownMenu.Portal>
);
});
@@ -396,30 +381,19 @@ export namespace AxoDropdownMenu {
* for the containing menu.
*/
export const Header: FC<HeaderProps> = memo(props => {
const labelId = useId();
const descriptionId = useId();
const { labelRef, descriptionRef } = useAriaLabellingContext(
'AxoDropdownMenu.Content/SubContent'
);
return (
<span aria-hidden="true" className={AxoBaseMenu.menuHeaderStyles}>
<span
ref={labelRef}
id={labelId}
className={AxoBaseMenu.menuHeaderLabelStyles}
>
{props.label}
</span>
{props.description && (
<span
ref={descriptionRef}
id={descriptionId}
className={AxoBaseMenu.menuHeaderDescriptionStyles}
>
{props.description}
<AriaLabelled.Label asChild>
<span className={AxoBaseMenu.menuHeaderLabelStyles}>
{props.label}
</span>
</AriaLabelled.Label>
{props.description && (
<AriaLabelled.Description asChild>
<span className={AxoBaseMenu.menuHeaderDescriptionStyles}>
{props.description}
</span>
</AriaLabelled.Description>
)}
</span>
);
@@ -643,20 +617,17 @@ export namespace AxoDropdownMenu {
* inside {@link AxoDropdownMenu.Sub}.
*/
export const SubContent: FC<SubContentProps> = memo(props => {
const { context, labelId, descriptionId } = useCreateAriaLabellingContext();
return (
<DropdownMenu.Portal>
<AriaLabellingProvider value={context}>
<AriaLabelled.Root asChild>
<DropdownMenu.SubContent
alignOffset={-6}
collisionPadding={6}
className={AxoBaseMenu.menuSubContentStyles}
aria-labelledby={labelId}
aria-describedby={descriptionId}
>
{props.children}
</DropdownMenu.SubContent>
</AriaLabellingProvider>
</AriaLabelled.Root>
</DropdownMenu.Portal>
);
});
@@ -1,67 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { RefCallback } from 'react';
import { useMemo, useState } from 'react';
import { createStrictContext, useStrictContext } from './StrictContext.dom.tsx';
/** @internal */
type AriaLabellingContextType = Readonly<{
labelRef: RefCallback<HTMLElement>;
descriptionRef: RefCallback<HTMLElement>;
}>;
/** @internal */
const AriaLabellingContext = createStrictContext<AriaLabellingContextType>(
'AriaLabellingContext.Provider'
);
export type CreateAriaLabellingContextResult = Readonly<{
/** The labelling context to pass to `AriaLabellingProvider`. */
context: AriaLabellingContextType;
/** The `id` of the label element, once mounted. `undefined` if no label has been registered. */
labelId: string | undefined;
/** The `id` of the description element, once mounted. `undefined` if none has been registered. */
descriptionId: string | undefined;
}>;
/**
* Creates a labelling context that captures element IDs from nested label and
* description refs. Use the returned `labelId`/`descriptionId` to wire up
* `aria-labelledby`/`aria-describedby` on the containing widget.
*/
export function useCreateAriaLabellingContext(): CreateAriaLabellingContextResult {
const [labelId, setLabelId] = useState<string | undefined>();
const [descriptionId, setDescriptionId] = useState<string | undefined>();
const context = useMemo((): AriaLabellingContextType => {
function labelRef(element: HTMLElement | null) {
setLabelId(element?.id);
}
function descriptionRef(element: HTMLElement | null) {
setDescriptionId(element?.id);
}
return { labelRef, descriptionRef };
}, []);
return { context, labelId, descriptionId };
}
/** Context provider. Pass the `context` returned by `useCreateAriaLabellingContext`. */
export const AriaLabellingProvider = AriaLabellingContext.Provider;
/**
* Reads the labelling context and returns `labelRef`/`descriptionRef` callbacks
* for registering label and description elements. Must be called inside an
* `AriaLabellingProvider`.
*/
export function useAriaLabellingContext(
providerName: string
): AriaLabellingContextType {
return useStrictContext(
AriaLabellingContext,
`Must be wrapped with a <${providerName}>`
);
}
@@ -8,12 +8,8 @@ import { tw } from '../tw.dom.tsx';
import { AxoBadge } from '../AxoBadge.dom.tsx';
import { createStrictContext, useStrictContext } from './StrictContext.dom.tsx';
import { variants } from './variants.dom.tsx';
import {
AriaLabellingProvider,
useAriaLabellingContext,
useCreateAriaLabellingContext,
} from './AriaLabellingContext.dom.tsx';
import { forwardExtraPropsForRadix } from './props.dom.tsx';
import { AriaLabelled } from '../aria/AriaLabelled.dom.tsx';
/**
* Used to share styles/animations for SegmentedControls, Toolbar ToggleGroups,
@@ -173,11 +169,6 @@ export namespace ExperimentalAxoBaseSegmentedControl {
export const Item: FC<ItemProps> = memo(props => {
const { ref, value, children, ...rest } = props;
const context = useStrictContext(RootContext);
const {
context: labellingContext,
labelId,
descriptionId,
} = useCreateAriaLabellingContext();
const isSelected = useMemo(() => {
if (context.value == null) {
@@ -192,7 +183,7 @@ export namespace ExperimentalAxoBaseSegmentedControl {
}, [value, context.value]);
return (
<AriaLabellingProvider value={labellingContext}>
<AriaLabelled.Root asChild>
<button
ref={ref}
type="button"
@@ -210,8 +201,6 @@ export namespace ExperimentalAxoBaseSegmentedControl {
'forced-colors:data-[axo-contextmenu-state=open]:bg-[Highlight]'
)
)}
aria-labelledby={labelId}
aria-describedby={descriptionId}
{...forwardExtraPropsForRadix(rest)}
>
{children}
@@ -224,7 +213,7 @@ export namespace ExperimentalAxoBaseSegmentedControl {
/>
)}
</button>
</AriaLabellingProvider>
</AriaLabelled.Root>
);
});
@@ -246,17 +235,17 @@ export namespace ExperimentalAxoBaseSegmentedControl {
/** Truncated label text inside a segmented control item. */
export const ItemText: FC<ItemTextProps> = memo(props => {
const id = useId();
const { labelRef } = useAriaLabellingContext('AxoBaseSegmentControl.Item');
return (
<span
ref={labelRef}
id={id}
className={tw('relative z-20 block truncate forced-color-adjust-none')}
style={{ maxWidth: props.maxWidth }}
>
{props.children}
</span>
<AriaLabelled.Label asChild>
<span
className={tw(
'relative z-20 block truncate forced-color-adjust-none'
)}
style={{ maxWidth: props.maxWidth }}
>
{props.children}
</span>
</AriaLabelled.Label>
);
});
@@ -271,24 +260,18 @@ export namespace ExperimentalAxoBaseSegmentedControl {
/** A badge rendered to the right of the item label. */
export const ItemBadge: FC<ItemBadgeProps> = memo((props: ItemBadgeProps) => {
const id = useId();
const { descriptionRef } = useAriaLabellingContext(
'AxoBaseSegmentControl.Item'
);
return (
<span
ref={descriptionRef}
id={id}
className={tw('relative z-20 ms-[5px]')}
>
<AxoBadge.Root
variant={props.variant}
size="md"
value={props.value}
max={props.max}
label={props.label}
/>
</span>
<AriaLabelled.Description asChild>
<span className={tw('relative z-20 ms-[5px]')}>
<AxoBadge.Root
variant={props.variant}
size="md"
value={props.value}
max={props.max}
label={props.label}
/>
</span>
</AriaLabelled.Description>
);
});
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Meta } from '@storybook/react';
import type { ReactNode } from 'react';
import { AriaLabelled } from './AriaLabelled.dom.tsx';
import { tw } from '../tw.dom.tsx';
export default {
title: 'Axo/Aria/AriaLabelled',
} satisfies Meta;
export function Basic(): ReactNode {
return (
<AriaLabelled.Root asChild>
<div role="figure">
<AriaLabelled.Label>Label</AriaLabelled.Label>
<AriaLabelled.Description>Description</AriaLabelled.Description>
</div>
</AriaLabelled.Root>
);
}
export function AsChild(): ReactNode {
return (
<AriaLabelled.Root asChild>
<div role="figure">
<AriaLabelled.Label asChild>
<div className={tw('type-body-medium font-semibold')}>Label</div>
</AriaLabelled.Label>
<AriaLabelled.Description asChild>
<div className={tw('type-caption text-secondary')}>Description</div>
</AriaLabelled.Description>
</div>
</AriaLabelled.Root>
);
}
+160
View File
@@ -0,0 +1,160 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FC, ReactNode } from 'react';
import { memo, useCallback, useId, useMemo, useState } from 'react';
import { Slot } from 'radix-ui';
import {
createStrictContext,
useStrictContext,
} from '../_internal/StrictContext.dom.tsx';
import { assert } from '../_internal/assert.std.tsx';
export namespace AriaLabelled {
type Ids = ReadonlySet<string>;
type Unregister = () => void;
type Register = (element: Element | null) => Unregister;
function useIds(): [Ids, Register] {
const [ids, setIds] = useState<Ids>(() => new Set());
const register: Register = useCallback(element => {
assert(element != null, 'must be rendered in react 19 or higher');
const id = element.id;
assert(id !== '', 'element is missing id');
setIds(prev => {
const copy = new Set(prev);
copy.add(id);
return copy;
});
return () => {
setIds(prev => {
const copy = new Set(prev);
copy.delete(id);
return copy;
});
};
}, []);
return [ids, register];
}
function concatIds(input: Ids): string | undefined {
if (input.size === 0) {
return undefined;
}
return Array.from(input).join(' ');
}
type RootContextType = Readonly<{
registerLabel: Register;
registerDescription: Register;
}>;
const RootContext = createStrictContext<RootContextType>('AxoLabelled.Root');
/**
* <AriaLabelled.Root>
* --------------------------------------------------------------------------
*/
export type RootProps = Readonly<{
asChild?: boolean;
id?: string;
label?: string;
description?: string;
children: ReactNode;
}>;
export const Root: FC<RootProps> = memo(props => {
const { label, description } = props;
const Comp = props.asChild ? Slot.Root : 'div';
const fallbackId = useId();
const id = props.id ?? fallbackId;
const [labels, registerLabel] = useIds();
const [descriptions, registerDescription] = useIds();
const context = useMemo((): RootContextType => {
return { registerLabel, registerDescription };
}, [registerLabel, registerDescription]);
const labelProps = useMemo(() => {
let labelledBy = concatIds(labels);
let describbedBy = concatIds(descriptions);
if (label != null && labelledBy != null) {
labelledBy = `${id} ${labelledBy}`;
}
if (description != null && describbedBy != null) {
describbedBy = `${id} ${describbedBy}`;
}
return {
id,
'aria-label': label,
'aria-description': description,
'aria-labelledby': labelledBy,
'aria-describedby': describbedBy,
};
}, [id, label, description, labels, descriptions]);
return (
<RootContext value={context}>
<Comp {...labelProps}>{props.children}</Comp>
</RootContext>
);
});
Root.displayName = 'AriaLabelled.Root';
/**
* <AriaLabelled.Label>
* --------------------------------------------------------------------------
*/
export type LabelProps = Readonly<{
asChild?: boolean;
id?: string;
children: ReactNode;
}>;
export const Label: FC<LabelProps> = memo(props => {
const Comp = props.asChild ? Slot.Root : 'div';
const fallbackId = useId();
const { registerLabel } = useStrictContext(RootContext);
return (
<Comp ref={registerLabel} id={props.id ?? fallbackId}>
{props.children}
</Comp>
);
});
Label.displayName = 'AriaLabelled.Label';
/**
* <AriaLabelled.Description>
* --------------------------------------------------------------------------
*/
export type DescriptionProps = Readonly<{
asChild?: boolean;
id?: string;
children: ReactNode;
}>;
export const Description: FC<DescriptionProps> = memo(props => {
const Comp = props.asChild ? Slot.Root : 'div';
const fallbackId = useId();
const { registerDescription } = useStrictContext(RootContext);
return (
<Comp ref={registerDescription} id={props.id ?? fallbackId}>
{props.children}
</Comp>
);
});
Description.displayName = 'AriaLabelled.Description';
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FC, ReactNode } from 'react';
import { memo } from 'react';
import { Slot } from 'radix-ui';
import { AriaLabelled } from './AriaLabelled.dom.tsx';
export namespace AriaList {
/**
* <AriaList.Root>
* --------------------------------------------------------------------------
*/
export type RootProps = Readonly<{
asChild?: boolean;
children: ReactNode;
}>;
export const Root: FC<RootProps> = memo(props => {
const Comp = props.asChild ? Slot.Root : 'ul';
return <Comp role="list">{props.children}</Comp>;
});
Root.displayName = 'AriaList.Root';
/**
* <AriaList.Item>
* --------------------------------------------------------------------------
*/
export type ItemProps = Readonly<{
asChild?: boolean;
id?: string;
children: ReactNode;
}>;
export const Item: FC<ItemProps> = memo(props => {
const Comp = props.asChild ? Slot.Root : 'li';
return (
<AriaLabelled.Root asChild id={props.id}>
<Comp role="listitem">{props.children}</Comp>
</AriaLabelled.Root>
);
});
Item.displayName = 'AriaList.Item';
/**
* <AriaList.Label>
* --------------------------------------------------------------------------
*/
export type LabelProps = Readonly<{
asChild?: boolean;
id?: string;
children: ReactNode;
}>;
export const Label: FC<LabelProps> = memo(props => {
return <AriaLabelled.Label {...props} />;
});
Label.displayName = 'AriaList.Label';
/**
* <AriaList.Description>
* --------------------------------------------------------------------------
*/
export type DescriptionProps = Readonly<{
asChild?: boolean;
id?: string;
children: ReactNode;
}>;
export const Description: FC<DescriptionProps> = memo(props => {
return <AriaLabelled.Description {...props} />;
});
Description.displayName = 'AriaList.Description';
}
@@ -5,7 +5,7 @@ import type { Meta } from '@storybook/react';
import { AxoRadioGroup } from './AxoRadioGroup.dom.tsx';
export default {
title: 'Axo/AxoRadioGroup',
title: 'Axo/Controls/AxoRadioGroup',
} satisfies Meta;
export function Default(): JSX.Element {
@@ -13,15 +13,12 @@ export function Default(): JSX.Element {
return (
<AxoRadioGroup.Root value={value} onValueChange={setValue}>
<AxoRadioGroup.Item value="foo">
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>Foo</AxoRadioGroup.Label>
</AxoRadioGroup.Item>
<AxoRadioGroup.Item value="bar">
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>Bar</AxoRadioGroup.Label>
</AxoRadioGroup.Item>
<AxoRadioGroup.Item value="baz">
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Veniam
accusantium a aperiam quas perferendis error velit ipsam animi natus
@@ -3,12 +3,9 @@
import { RadioGroup } from 'radix-ui';
import type { FC, ReactNode } from 'react';
import { memo, useId, useMemo } from 'react';
import { tw } from './tw.dom.tsx';
import {
createStrictContext,
useStrictContext,
} from './_internal/StrictContext.dom.tsx';
import { memo } from 'react';
import { tw } from '../tw.dom.tsx';
import { AxoBaseRadioGroup } from './_AxoBaseRadioGroup.dom.tsx';
/**
* A set of checkable buttonsknown as radio buttonswhere no more than one of
@@ -18,7 +15,6 @@ import {
* ```tsx
* <AxoRadioGroup.Root value={value} onValueChange={setValue}>
* <AxoRadioGroup.Item value="option-a">
* <AxoRadioGroup.Indicator />
* <AxoRadioGroup.Label>Option A</AxoRadioGroup.Label>
* </AxoRadioGroup.Item>
* </AxoRadioGroup.Root>
@@ -62,15 +58,12 @@ export namespace AxoRadioGroup {
* ```tsx
* <AxoRadioGroup.Root value={notify} onValueChange={setNotify}>
* <AxoRadioGroup.Item value="all">
* <AxoRadioGroup.Indicator />
* <AxoRadioGroup.Label>All messages</AxoRadioGroup.Label>
* </AxoRadioGroup.Item>
* <AxoRadioGroup.Item value="mentions">
* <AxoRadioGroup.Indicator />
* <AxoRadioGroup.Label>Mentions only</AxoRadioGroup.Label>
* </AxoRadioGroup.Item>
* <AxoRadioGroup.Item value="off">
* <AxoRadioGroup.Indicator />
* <AxoRadioGroup.Label>Off</AxoRadioGroup.Label>
* </AxoRadioGroup.Item>
* </AxoRadioGroup.Root>
@@ -96,17 +89,6 @@ export namespace AxoRadioGroup {
* --------------------------------------------------------------------------
*/
/** @internal */
type ItemContextType = Readonly<{
id: string;
value: string;
disabled: boolean;
}>;
/** @internal */
const ItemContext =
createStrictContext<ItemContextType>('AxoRadioGroup.Item');
export type ItemProps = Readonly<{
/**
* The value given as data when submitted with a name.
@@ -117,7 +99,7 @@ export namespace AxoRadioGroup {
*/
disabled?: boolean;
/**
* Should be an `Indicator` and a `Label`.
* Should contain an accessible label
*/
children: ReactNode;
}>;
@@ -126,68 +108,22 @@ export namespace AxoRadioGroup {
* A single radio option.
*/
export const Item: FC<ItemProps> = memo(props => {
const { value, disabled = false } = props;
const id = useId();
const context = useMemo((): ItemContextType => {
return { id, value, disabled };
}, [id, value, disabled]);
return (
<ItemContext.Provider value={context}>
<label htmlFor={id} className={tw('flex gap-3 py-2.5')}>
<AxoBaseRadioGroup.Item
asChild
value={props.value}
disabled={props.disabled}
>
<div className={tw('flex gap-3 py-2.5')}>
<AxoBaseRadioGroup.Indicator />
{props.children}
</label>
</ItemContext.Provider>
</div>
</AxoBaseRadioGroup.Item>
);
});
Item.displayName = 'AxoRadioGroup.Item';
/**
* <AxoRadioGroup.Indicator>
* --------------------------------------------------------------------------
*/
/**
* Renders when the radio item is in a checked state.
*/
export const Indicator: FC = memo(() => {
const context = useStrictContext(ItemContext);
return (
<RadioGroup.Item
id={context.id}
value={context.value}
disabled={context.disabled}
className={tw(
'flex size-5 shrink-0 items-center justify-center rounded-full',
'border border-primary inset-shadow-on-color',
'data-[state=unchecked]:bg-control',
'data-[state=unchecked]:enabled:active:bg-control-pressed',
'data-[state=checked]:bg-accent',
'data-[state=checked]:active:bg-accent-pressed',
'data-disabled:border-secondary',
'outline-none keyboard-mode:focus:axo-focus-ring',
'overflow-hidden',
'forced-colors:data-[state=checked]:bg-[SelectedItem]'
)}
>
<RadioGroup.Indicator asChild>
<span
className={tw(
'size-2.25 rounded-full',
'data-[state=checked]:bg-(--axo-color-label-primary-oncolor)',
'data-[state=checked]:data-disabled:bg-(--axo-color-label-disabled-oncolor)',
'forced-colors:data-[state=checked]:bg-[SelectedItemText]'
)}
/>
</RadioGroup.Indicator>
</RadioGroup.Item>
);
});
Indicator.displayName = 'AxoRadioGroup.Indicator';
/**
* <AxoRadioGroup.Label>
* --------------------------------------------------------------------------
@@ -205,9 +141,11 @@ export namespace AxoRadioGroup {
*/
export const Label: FC<LabelProps> = memo(props => {
return (
<span className={tw('truncate type-body-large text-primary')}>
{props.children}
</span>
<AxoBaseRadioGroup.Label asChild>
<span className={tw('truncate type-body-large text-primary')}>
{props.children}
</span>
</AxoBaseRadioGroup.Label>
);
});
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FC, ReactNode } from 'react';
import { RadioGroup } from 'radix-ui';
import { memo } from 'react';
import { tw } from '../tw.dom.tsx';
import { AriaLabelled } from '../aria/AriaLabelled.dom.tsx';
export namespace AxoBaseRadioGroup {
/**
* <AxoBaseRadioGroup.Item>
* --------------------------------------------------------------------------
*/
export type ItemProps = Readonly<{
asChild?: boolean;
/**
* The value given as data when submitted with a name.
*/
value: string;
/**
* When true, prevents the user from interacting with the radio item.
*/
disabled?: boolean;
/**
* Should be an `Indicator` and a `Label`.
*/
children: ReactNode;
}>;
/**
* A single radio option.
*/
export const Item: FC<ItemProps> = memo(props => {
return (
<AriaLabelled.Root asChild>
<RadioGroup.Item {...props} />
</AriaLabelled.Root>
);
});
Item.displayName = 'AxoRadioGroup.Item';
/**
* <AxoBaseRadioGroup.Indicator>
* --------------------------------------------------------------------------
*/
export const Indicator: FC = memo(() => {
return (
<RadioGroup.Indicator asChild forceMount>
<span
className={tw(
'group',
'flex size-5 shrink-0 items-center justify-center rounded-full',
'border border-primary inset-shadow-on-color',
'data-[state=unchecked]:bg-control',
'data-[state=unchecked]:enabled:active:bg-control-pressed',
'data-[state=checked]:bg-accent',
'data-[state=checked]:active:bg-accent-pressed',
'data-disabled:border-secondary',
'outline-none keyboard-mode:focus:axo-focus-ring',
'overflow-hidden',
'forced-colors:data-[state=checked]:bg-[SelectedItem]'
)}
>
<span
className={tw(
'size-2.25 rounded-full',
'group-data-[state=checked]:bg-(--axo-color-label-primary-oncolor)',
'group-data-[state=checked]:data-disabled:bg-(--axo-color-label-disabled-oncolor)',
'forced-colors:group-data-[state=checked]:bg-[SelectedItemText]'
)}
/>
</span>
</RadioGroup.Indicator>
);
});
Indicator.displayName = 'AxoBaseRadioGroup.Indicator';
/**
* <AxoBaseRadioGroup.Label>
* --------------------------------------------------------------------------
*/
export type LabelProps = Readonly<{
asChild?: boolean;
id?: string;
children: ReactNode;
}>;
export const Label: FC<LabelProps> = memo(props => {
return <AriaLabelled.Label {...props} />;
});
Label.displayName = 'AxoBaseRadioGroup.Label';
/**
* <AxoBaseRadioGroup.Description>
* --------------------------------------------------------------------------
*/
export type DescriptionProps = Readonly<{
asChild?: boolean;
id?: string;
children: ReactNode;
}>;
export const Description: FC<DescriptionProps> = memo(props => {
return <AriaLabelled.Description {...props} />;
});
Description.displayName = 'AxoBaseRadioGroup.Description';
}
+74 -55
View File
@@ -8,6 +8,7 @@ import { AxoList } from './AxoList.dom.tsx';
import type { AxoIconButton } from '../AxoIconButton.dom.tsx';
import { forwardExtraPropsForRadix } from '../_internal/props.dom.tsx';
import { tw } from '../tw.dom.tsx';
import { AriaList } from '../aria/AriaList.dom.tsx';
export namespace AxoContactList {
/**
@@ -23,11 +24,15 @@ export namespace AxoContactList {
export const Root: FC<RootProps> = memo(props => {
return (
<AxoList.Root>
<AxoList.Header>
<AxoList.Title>{props.title}</AxoList.Title>
</AxoList.Header>
{props.title != null && (
<AxoList.Header>
<AxoList.Title>{props.title}</AxoList.Title>
</AxoList.Header>
)}
<AxoList.Body>
<AxoBaseItem.Group spacing="sm">{props.children}</AxoBaseItem.Group>
<AriaList.Root asChild>
<AxoBaseItem.Group spacing="sm">{props.children}</AxoBaseItem.Group>
</AriaList.Root>
</AxoList.Body>
</AxoList.Root>
);
@@ -51,33 +56,39 @@ export namespace AxoContactList {
export const Item: FC<ItemProps> = memo(props => {
const id = useId();
return (
<AxoBaseItem.Root>
<AxoBaseItem.LegacyAvatarSlot>
{props.avatar}
</AxoBaseItem.LegacyAvatarSlot>
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AxoBaseItem.Title id={id}>{props.title}</AxoBaseItem.Title>
{props.value != null && (
<AxoBaseItem.Value>{props.value}</AxoBaseItem.Value>
<AriaList.Item asChild>
<AxoBaseItem.Root>
<AxoBaseItem.LeadingSlot>{props.avatar}</AxoBaseItem.LeadingSlot>
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AriaList.Label asChild id={id}>
<AxoBaseItem.Title>{props.title}</AxoBaseItem.Title>
</AriaList.Label>
{props.value != null && (
<AriaList.Label asChild>
<AxoBaseItem.Value>{props.value}</AxoBaseItem.Value>
</AriaList.Label>
)}
{props.description != null && (
<AriaList.Description asChild>
<AxoBaseItem.Description truncate>
{props.description}
</AxoBaseItem.Description>
</AriaList.Description>
)}
{props.onClick != null && (
<AxoBaseItem.HiddenTrigger
labelledby={id}
onClick={props.onClick}
/>
)}
</AxoBaseItem.Body>
{props.accessory != null && (
<AxoBaseItem.Accessory>{props.accessory}</AxoBaseItem.Accessory>
)}
{props.description != null && (
<AxoBaseItem.Description truncate>
{props.description}
</AxoBaseItem.Description>
)}
{props.onClick != null && (
<AxoBaseItem.HiddenTrigger
labelledby={id}
onClick={props.onClick}
/>
)}
</AxoBaseItem.Body>
{props.accessory != null && (
<AxoBaseItem.Accessory>{props.accessory}</AxoBaseItem.Accessory>
)}
</AxoBaseItem.Content>
</AxoBaseItem.Root>
</AxoBaseItem.Content>
</AxoBaseItem.Root>
</AriaList.Item>
);
});
@@ -91,7 +102,7 @@ export namespace AxoContactList {
export type ItemActionVariant = 'subtle-secondary';
export type ItemActionProps = Readonly<{
ref?: Ref<HTMLButtonElement | null>;
ref?: Ref<HTMLButtonElement>;
variant: ItemActionVariant;
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
children: ReactNode;
@@ -121,7 +132,7 @@ export namespace AxoContactList {
export type ItemIconActionVariant = 'implied-secondary';
export type ItemIconActionProps = Readonly<{
ref?: Ref<HTMLButtonElement | null>;
ref?: Ref<HTMLButtonElement>;
variant: ItemIconActionVariant;
label: string;
symbol: AxoSymbol.Name;
@@ -160,20 +171,24 @@ export namespace AxoContactList {
export const ActionItem: FC<ActionItemProps> = memo(props => {
const id = useId();
return (
<AxoBaseItem.Root>
<AxoBaseItem.IconAvatar size={32} symbol={props.symbol} />
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AxoBaseItem.Title id={id}>{props.title}</AxoBaseItem.Title>
{props.onClick != null && (
<AxoBaseItem.HiddenTrigger
labelledby={id}
onClick={props.onClick}
/>
)}
</AxoBaseItem.Body>
</AxoBaseItem.Content>
</AxoBaseItem.Root>
<AriaList.Item asChild>
<AxoBaseItem.Root>
<AxoBaseItem.IconAvatar size={32} symbol={props.symbol} />
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AriaList.Label asChild id={id}>
<AxoBaseItem.Title>{props.title}</AxoBaseItem.Title>
</AriaList.Label>
{props.onClick != null && (
<AxoBaseItem.HiddenTrigger
labelledby={id}
onClick={props.onClick}
/>
)}
</AxoBaseItem.Body>
</AxoBaseItem.Content>
</AxoBaseItem.Root>
</AriaList.Item>
);
});
@@ -190,15 +205,19 @@ export namespace AxoContactList {
export const EmptyStateItem: FC<EmptyStateItemProps> = memo(props => {
return (
<AxoBaseItem.Root>
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AxoBaseItem.Title truncate>
<span className={tw('text-secondary')}>{props.title}</span>
</AxoBaseItem.Title>
</AxoBaseItem.Body>
</AxoBaseItem.Content>
</AxoBaseItem.Root>
<AriaList.Item asChild>
<AxoBaseItem.Root>
<AxoBaseItem.Content>
<AxoBaseItem.Body>
<AriaList.Label asChild>
<AxoBaseItem.Title truncate>
<span className={tw('text-secondary')}>{props.title}</span>
</AxoBaseItem.Title>
</AriaList.Label>
</AxoBaseItem.Body>
</AxoBaseItem.Content>
</AxoBaseItem.Root>
</AriaList.Item>
);
});
+27 -5
View File
@@ -6,6 +6,7 @@ import type { AxoSymbol } from '../AxoSymbol.dom.tsx';
import type { AxoIconButton } from '../AxoIconButton.dom.tsx';
import { forwardExtraPropsForRadix } from '../_internal/props.dom.tsx';
import { AxoBaseItem } from './_AxoBaseItem.dom.tsx';
import { AriaList } from '../aria/AriaList.dom.tsx';
/**
* @example Anatomy
@@ -43,7 +44,11 @@ export namespace AxoItem {
}>;
export const Group: FC<GroupProps> = memo(props => {
return <AxoBaseItem.Group spacing="md">{props.children}</AxoBaseItem.Group>;
return (
<AriaList.Root asChild>
<AxoBaseItem.Group spacing="md">{props.children}</AxoBaseItem.Group>
</AriaList.Root>
);
});
Group.displayName = 'AxoItem.Group';
@@ -54,11 +59,16 @@ export namespace AxoItem {
*/
export type RootProps = Readonly<{
id?: string;
children: ReactNode;
}>;
export const Root: FC<RootProps> = memo(props => {
return <AxoBaseItem.Root>{props.children}</AxoBaseItem.Root>;
return (
<AriaList.Item asChild id={props.id}>
<AxoBaseItem.Root>{props.children}</AxoBaseItem.Root>
</AriaList.Item>
);
});
Root.displayName = 'AxoItem.Root';
@@ -120,7 +130,9 @@ export namespace AxoItem {
export const Title: FC<TitleProps> = memo(props => {
return (
<AxoBaseItem.Title id={props.id}>{props.children}</AxoBaseItem.Title>
<AriaList.Label asChild id={props.id}>
<AxoBaseItem.Title>{props.children}</AxoBaseItem.Title>
</AriaList.Label>
);
});
@@ -132,11 +144,16 @@ export namespace AxoItem {
*/
export type ValueProps = Readonly<{
id?: string;
children: ReactNode;
}>;
export const Value: FC<ValueProps> = memo(props => {
return <AxoBaseItem.Value>{props.children}</AxoBaseItem.Value>;
return (
<AriaList.Label asChild id={props.id}>
<AxoBaseItem.Value>{props.children}</AxoBaseItem.Value>
</AriaList.Label>
);
});
Value.displayName = 'AxoItem.Value';
@@ -147,11 +164,16 @@ export namespace AxoItem {
*/
export type DescriptionProps = Readonly<{
id?: string;
children: ReactNode;
}>;
export const Description: FC<DescriptionProps> = memo(props => {
return <AxoBaseItem.Description>{props.children}</AxoBaseItem.Description>;
return (
<AriaList.Description asChild id={props.id}>
<AxoBaseItem.Description>{props.children}</AxoBaseItem.Description>
</AriaList.Description>
);
});
Description.displayName = 'AxoItem.Description';
+23 -41
View File
@@ -1,13 +1,9 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FC, ReactNode } from 'react';
import { memo, useId } from 'react';
import { memo } from 'react';
import { tw } from '../tw.dom.tsx';
import {
AriaLabellingProvider,
useAriaLabellingContext,
useCreateAriaLabellingContext,
} from '../_internal/AriaLabellingContext.dom.tsx';
import { AriaLabelled } from '../aria/AriaLabelled.dom.tsx';
export namespace AxoList {
/**
@@ -21,16 +17,10 @@ export namespace AxoList {
}>;
export const Root: FC<RootProps> = memo(props => {
const { context, labelId } = useCreateAriaLabellingContext();
return (
<AriaLabellingProvider value={context}>
<section
aria-label={props.accessibilityLabel}
aria-labelledby={labelId}
>
{props.children}
</section>
</AriaLabellingProvider>
<AriaLabelled.Root asChild label={props.accessibilityLabel}>
<section>{props.children}</section>
</AriaLabelled.Root>
);
});
@@ -61,16 +51,12 @@ export namespace AxoList {
}>;
export const Title: FC<TitleProps> = memo(props => {
const id = useId();
const { labelRef } = useAriaLabellingContext('AxoList.Root');
return (
<h2
ref={labelRef}
id={id}
className={tw('type-body-medium font-semibold')}
>
{props.children}
</h2>
<AriaLabelled.Label asChild>
<h2 className={tw('type-body-medium font-semibold')}>
{props.children}
</h2>
</AriaLabelled.Label>
);
});
@@ -86,16 +72,10 @@ export namespace AxoList {
}>;
export const Description: FC<DescriptionProps> = memo(props => {
const id = useId();
const { descriptionRef } = useAriaLabellingContext('AxoList.Root');
return (
<p
ref={descriptionRef}
id={id}
className={tw('type-body-small text-secondary')}
>
{props.children}
</p>
<AriaLabelled.Description asChild>
<p className={tw('type-body-small text-secondary')}>{props.children}</p>
</AriaLabelled.Description>
);
});
@@ -153,14 +133,16 @@ export namespace AxoList {
export const Help: FC<HelpProps> = memo(props => {
return (
<p
className={tw(
'type-body-small text-secondary',
'forced-colors:text-[GrayText]'
)}
>
{props.children}
</p>
<AriaLabelled.Description asChild>
<p
className={tw(
'type-body-small text-secondary',
'forced-colors:text-[GrayText]'
)}
>
{props.children}
</p>
</AriaLabelled.Description>
);
});
@@ -0,0 +1,54 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Meta } from '@storybook/react';
import type { ReactNode } from 'react';
import { useState } from 'react';
import { AxoRadioGroupList } from './AxoRadioGroupList.dom.tsx';
export default {
title: 'Axo/Items/AxoRadioGroupList',
} satisfies Meta;
export function Basic(): ReactNode {
const [value, setValue] = useState('1');
return (
<AxoRadioGroupList.Root value={value} onValueChange={setValue}>
<AxoRadioGroupList.Item value="1">
<AxoRadioGroupList.Label>One</AxoRadioGroupList.Label>
</AxoRadioGroupList.Item>
<AxoRadioGroupList.Item value="2">
<AxoRadioGroupList.Label>Two</AxoRadioGroupList.Label>
</AxoRadioGroupList.Item>
<AxoRadioGroupList.Item value="3">
<AxoRadioGroupList.Label>Three</AxoRadioGroupList.Label>
</AxoRadioGroupList.Item>
</AxoRadioGroupList.Root>
);
}
export function Descriptions(): ReactNode {
const [value, setValue] = useState('1');
return (
<AxoRadioGroupList.Root value={value} onValueChange={setValue}>
<AxoRadioGroupList.Item value="1">
<AxoRadioGroupList.Label>One</AxoRadioGroupList.Label>
<AxoRadioGroupList.Description>
Lorem ipsum dolor, sit amet consectetur adipisicing elit.
</AxoRadioGroupList.Description>
</AxoRadioGroupList.Item>
<AxoRadioGroupList.Item value="2">
<AxoRadioGroupList.Label>Two</AxoRadioGroupList.Label>
<AxoRadioGroupList.Description>
Lorem ipsum dolor, sit amet consectetur adipisicing elit.
</AxoRadioGroupList.Description>
</AxoRadioGroupList.Item>
<AxoRadioGroupList.Item value="3">
<AxoRadioGroupList.Label>Three</AxoRadioGroupList.Label>
<AxoRadioGroupList.Description>
Lorem ipsum dolor, sit amet consectetur adipisicing elit.
</AxoRadioGroupList.Description>
</AxoRadioGroupList.Item>
</AxoRadioGroupList.Root>
);
}
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FC, ReactNode } from 'react';
import { memo } from 'react';
import { RadioGroup } from 'radix-ui';
import { AxoList } from './AxoList.dom.tsx';
import { AxoBaseItem } from './_AxoBaseItem.dom.tsx';
import { AxoBaseRadioGroup } from '../controls/_AxoBaseRadioGroup.dom.tsx';
export namespace AxoRadioGroupList {
/**
* <AxoRadioGroupList.Root>
* --------------------------------------------------------------------------
*/
export type RootProps = Readonly<{
title?: ReactNode;
description?: ReactNode;
help?: ReactNode;
value: string;
onValueChange: (value: string) => void;
disabled?: boolean;
children: ReactNode;
}>;
export const Root: FC<RootProps> = memo(props => {
return (
<AxoList.Root>
{props.title != null && (
<AxoList.Header>
<AxoList.Title>{props.title}</AxoList.Title>
{/* We probably don't ever want to show a description without a title */}
{props.description != null && (
<AxoList.Description>{props.description}</AxoList.Description>
)}
</AxoList.Header>
)}
<AxoList.Body>
<RadioGroup.Root
asChild
value={props.value}
onValueChange={props.onValueChange}
disabled={props.disabled}
>
<AxoBaseItem.Group spacing="md">{props.children}</AxoBaseItem.Group>
</RadioGroup.Root>
</AxoList.Body>
{props.help != null && (
<AxoList.Footer>
<AxoList.Help>{props.help}</AxoList.Help>
</AxoList.Footer>
)}
</AxoList.Root>
);
});
Root.displayName = 'AxoRadioGroupList.Root';
/**
* <AxoRadioGroupList.Item>
* --------------------------------------------------------------------------
*/
export type ItemProps = Readonly<{
value: string;
disabled?: boolean;
children: ReactNode;
}>;
export const Item: FC<ItemProps> = memo(props => {
return (
<AxoBaseRadioGroup.Item
value={props.value}
disabled={props.disabled}
asChild
>
<AxoBaseItem.Root>
<AxoBaseItem.LeadingSlot>
<AxoBaseRadioGroup.Indicator />
</AxoBaseItem.LeadingSlot>
<AxoBaseItem.Content>
<AxoBaseItem.Body>{props.children}</AxoBaseItem.Body>
</AxoBaseItem.Content>
</AxoBaseItem.Root>
</AxoBaseRadioGroup.Item>
);
});
Item.displayName = 'AxoRadioGroupList.Item';
/**
* <AxoRadioGroupList.Label>
* --------------------------------------------------------------------------
*/
export type LabelProps = Readonly<{
children: ReactNode;
}>;
export const Label: FC<LabelProps> = memo(props => {
return (
<AxoBaseRadioGroup.Label asChild>
<AxoBaseItem.Title>{props.children}</AxoBaseItem.Title>
</AxoBaseRadioGroup.Label>
);
});
Label.displayName = 'AxoRadioGroupList.Label';
/**
* <AxoRadioGroupList.Description>
* --------------------------------------------------------------------------
*/
export type DescriptionProps = Readonly<{
children: ReactNode;
}>;
export const Description: FC<DescriptionProps> = memo(props => {
return (
<AxoBaseRadioGroup.Description asChild>
<AxoBaseItem.Description>{props.children}</AxoBaseItem.Description>
</AxoBaseRadioGroup.Description>
);
});
Description.displayName = 'AxoRadioGroupList.Description';
}
+54 -81
View File
@@ -1,24 +1,18 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FC, MouseEvent, ReactNode, Ref } from 'react';
import { memo, useId, useMemo } from 'react';
import { memo, useMemo } from 'react';
import {
createStrictContext,
useStrictContext,
} from '../_internal/StrictContext.dom.tsx';
import { tw } from '../tw.dom.tsx';
import {
AriaLabellingProvider,
useAriaLabellingContext,
useCreateAriaLabellingContext,
} from '../_internal/AriaLabellingContext.dom.tsx';
import { AriaClickable } from '../AriaClickable.dom.tsx';
import { forwardExtraPropsForRadix } from '../_internal/props.dom.tsx';
import { AxoIconButton } from '../AxoIconButton.dom.tsx';
import { AxoSymbol } from '../AxoSymbol.dom.tsx';
import { AxoButton } from '../AxoButton.dom.tsx';
import { AxoCheckbox } from '../AxoCheckbox.dom.tsx';
import { AxoRadioGroup } from '../AxoRadioGroup.dom.tsx';
import { AxoAvatar } from '../AxoAvatar.dom.tsx';
import { variants } from '../_internal/variants.dom.tsx';
@@ -72,12 +66,13 @@ export namespace AxoBaseItem {
createStrictContext<GroupContextType>('AxoBaseItem.Group');
export type GroupProps = Readonly<{
ref?: Ref<HTMLDivElement>;
spacing: Spacing;
children: ReactNode;
}>;
export const Group: FC<GroupProps> = memo(props => {
const { spacing } = props;
const { ref, spacing, children, ...rest } = props;
const context = useMemo((): GroupContextType => {
return { spacing };
@@ -86,13 +81,16 @@ export namespace AxoBaseItem {
return (
<GroupContext value={context}>
<div
role="list"
ref={ref}
className={tw('grid min-w-90')}
{...forwardExtraPropsForRadix(rest)}
// Style needs to come after forwarded props. Long-term we should
// figure out how to automatically merge props like these
style={{
gridTemplateColumns: GRID_TEMPLATE_COLUMNS,
}}
>
{props.children}
{children}
</div>
</GroupContext>
);
@@ -115,42 +113,40 @@ export namespace AxoBaseItem {
}>;
export const Root: FC<RootProps> = memo(props => {
const { children, ...rest } = props;
const groupContext = useStrictContext(GroupContext);
const { context, labelId, descriptionId } = useCreateAriaLabellingContext();
return (
<AriaLabellingProvider value={context}>
<AriaClickable.Root asChild>
<AriaClickable.Root asChild>
<div
className={tw(
'group',
// forward grid
'col-span-full grid grid-cols-subgrid',
'p-0.5',
'outline-none'
)}
{...forwardExtraPropsForRadix(rest)}
>
<div
role="listitem"
aria-labelledby={labelId}
aria-describedby={descriptionId}
className={tw(
'group',
// forward grid
'col-span-full grid grid-cols-subgrid',
'p-0.5'
'gap-x-3 px-3',
RootSpacing.get(groupContext.spacing),
'items-baseline',
'text-primary',
'curved-14',
'group-data-hovered:bg-secondary',
'group-data-pressed:bg-secondary-pressed',
'outline-none keyboard-mode:group-data-focused:axo-focus-ring',
'keyboard-mode:group-focus:axo-focus-ring'
)}
>
<div
className={tw(
// forward grid
'col-span-full grid grid-cols-subgrid',
'gap-x-3 px-3',
RootSpacing.get(groupContext.spacing),
'items-baseline',
'text-primary',
'curved-14',
'group-data-hovered:bg-secondary',
'group-data-pressed:bg-secondary-pressed',
'outline-none keyboard-mode:group-data-focused:axo-focus-ring'
)}
>
{props.children}
</div>
{children}
</div>
</AriaClickable.Root>
</AriaLabellingProvider>
</div>
</AriaClickable.Root>
);
});
@@ -161,14 +157,12 @@ export namespace AxoBaseItem {
* --------------------------------------------------------------------------
*/
/** @internal */
type LeadingSlotProps = Readonly<{
export type LeadingSlotProps = Readonly<{
className?: string;
children: ReactNode;
}>;
/** @internal */
const LeadingSlot: FC<LeadingSlotProps> = memo(props => {
export const LeadingSlot: FC<LeadingSlotProps> = memo(props => {
return (
<div style={{ gridColumn: LEADING_SLOT }} className={props.className}>
{props.children}
@@ -247,36 +241,6 @@ export namespace AxoBaseItem {
Checkbox.displayName = 'AxoBaseItem.Checkbox';
/**
* <AxoBaseItem.RadioGroupIndicator>
* --------------------------------------------------------------------------
*/
export const RadioGroupIndicator: FC = memo(() => {
return (
<LeadingSlot>
<AxoRadioGroup.Indicator />
</LeadingSlot>
);
});
RadioGroupIndicator.displayName = 'AxoBaseItem.RadioGroupIndicator';
/**
* <AxoBaseItem.LegacyAvatarSlot>
* --------------------------------------------------------------------------
*/
export type LegacyAvatarSlotProps = Readonly<{
children: ReactNode;
}>;
export const LegacyAvatarSlot: FC<LegacyAvatarSlotProps> = memo(props => {
return <LeadingSlot>{props.children}</LeadingSlot>;
});
LegacyAvatarSlot.displayName = 'AxoBaseItem.LegacyAvatarSlot';
/**
* <AxoBaseItem.IconAvatar>
* --------------------------------------------------------------------------
@@ -362,27 +326,26 @@ export namespace AxoBaseItem {
*/
export type TitleProps = Readonly<{
id?: string;
ref?: Ref<HTMLDivElement>;
truncate?: boolean;
children: ReactNode;
}>;
export const Title: FC<TitleProps> = memo(props => {
const fallbackId = useId();
const { labelRef } = useAriaLabellingContext('AxoBaseItem.Root');
const { ref, truncate, children, ...rest } = props;
return (
<div
ref={labelRef}
id={props.id ?? fallbackId}
ref={ref}
className={tw(
'min-w-50', // force value to next line if there's not much space
'grow-[calc(infinity)]',
'type-body-medium text-primary',
props.truncate ? 'truncate' : 'line-clamp-2',
truncate ? 'truncate' : 'line-clamp-2',
'-my-0.75 py-0.75' // extra space for focus rings
)}
{...forwardExtraPropsForRadix(rest)}
>
{props.children}
{children}
</div>
);
});
@@ -395,13 +358,19 @@ export namespace AxoBaseItem {
*/
export type ValueProps = Readonly<{
ref?: Ref<HTMLDivElement>;
children: ReactNode;
}>;
export const Value: FC<ValueProps> = memo(props => {
const { ref, children, ...rest } = props;
return (
<div className={tw('w-fit grow type-body-medium text-secondary')}>
{props.children}
<div
ref={ref}
className={tw('w-fit grow type-body-medium text-secondary')}
{...forwardExtraPropsForRadix(rest)}
>
{children}
</div>
);
});
@@ -414,25 +383,29 @@ export namespace AxoBaseItem {
*/
export type DescriptionProps = Readonly<{
ref?: Ref<HTMLDivElement>;
truncate?: boolean;
children: ReactNode;
}>;
export const Description: FC<DescriptionProps> = memo(props => {
const { ref, truncate, children, ...rest } = props;
return (
<>
{/* Force description to its own line */}
<div className={tw('basis-full')} />
<div
ref={ref}
className={tw(
'min-w-0',
'type-body-small text-secondary',
'forced-colors:text-[GrayText]',
props.truncate && 'truncate',
truncate && 'truncate',
'-my-0.5 py-0.5' // extra space for focus rings
)}
{...forwardExtraPropsForRadix(rest)}
>
{props.children}
{children}
</div>
</>
);
@@ -5,7 +5,7 @@ import { useCallback, useMemo, useState, type JSX } from 'react';
import type { LocalizerType } from '../../../types/Util.std.ts';
import { getMuteOptions } from '../../../util/getMuteOptions.std.ts';
import { AxoDialog } from '../../../axo/AxoDialog.dom.tsx';
import { AxoRadioGroup } from '../../../axo/AxoRadioGroup.dom.tsx';
import { AxoRadioGroup } from '../../../axo/controls/AxoRadioGroup.dom.tsx';
import { safeParseInteger } from '../../../util/numbers.std.ts';
import { strictAssert } from '../../../util/assert.std.ts';
@@ -62,7 +62,6 @@ export function ConversationNotificationsModal({
value={`${option.value}`}
disabled={option.disabled}
>
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>{option.name}</AxoRadioGroup.Label>
</AxoRadioGroup.Item>
);
@@ -4,7 +4,7 @@ import type { MouseEvent } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { AxoDialog } from '../../../axo/AxoDialog.dom.tsx';
import type { LocalizerType } from '../../../types/I18N.std.ts';
import { AxoRadioGroup } from '../../../axo/AxoRadioGroup.dom.tsx';
import { AxoRadioGroup } from '../../../axo/controls/AxoRadioGroup.dom.tsx';
import { strictAssert } from '../../../util/assert.std.ts';
import { AxoAlertDialog } from '../../../axo/AxoAlertDialog.dom.tsx';
import { isInternalFeaturesEnabled } from '../../../util/isInternalFeaturesEnabled.dom.ts';
@@ -239,32 +239,27 @@ function PinMessageSelectDurationDialog(props: {
onValueChange={handleDurationChange}
>
<AxoRadioGroup.Item value={DurationOption.TIME_24_HOURS}>
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>
{i18n('icu:PinMessageDialog__Option--TIME_24_HOURS')}
</AxoRadioGroup.Label>
</AxoRadioGroup.Item>
<AxoRadioGroup.Item value={DurationOption.TIME_7_DAYS}>
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>
{i18n('icu:PinMessageDialog__Option--TIME_7_DAYS')}
</AxoRadioGroup.Label>
</AxoRadioGroup.Item>
<AxoRadioGroup.Item value={DurationOption.TIME_30_DAYS}>
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>
{i18n('icu:PinMessageDialog__Option--TIME_30_DAYS')}
</AxoRadioGroup.Label>
</AxoRadioGroup.Item>
<AxoRadioGroup.Item value={DurationOption.FOREVER}>
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>
{i18n('icu:PinMessageDialog__Option--FOREVER')}
</AxoRadioGroup.Label>
</AxoRadioGroup.Item>
{isInternalFeaturesEnabled() && (
<AxoRadioGroup.Item value={DurationOption.DEBUG_10_SECONDS}>
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>10 seconds (Internal)</AxoRadioGroup.Label>
</AxoRadioGroup.Item>
)}
@@ -14,7 +14,7 @@ import { tw } from '../../../axo/tw.dom.tsx';
import { AxoSymbol } from '../../../axo/AxoSymbol.dom.tsx';
import { AxoButton } from '../../../axo/AxoButton.dom.tsx';
import { AxoDialog } from '../../../axo/AxoDialog.dom.tsx';
import { AxoRadioGroup } from '../../../axo/AxoRadioGroup.dom.tsx';
import { AxoRadioGroup } from '../../../axo/controls/AxoRadioGroup.dom.tsx';
import { AxoTextField } from '../../../axo/fields/AxoTextField.dom.tsx';
import type { LocalizerType } from '../../../types/I18N.std.ts';
@@ -327,7 +327,6 @@ function PhoneNumberDiscoverabilityDialog({
}}
>
<AxoRadioGroup.Item value={PhoneNumberDiscoverability.Discoverable}>
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>
<div className={tw('type-body-medium')}>
{i18n('icu:Preferences__pnp__discoverability__everyone')}
@@ -337,7 +336,6 @@ function PhoneNumberDiscoverabilityDialog({
<AxoRadioGroup.Item
value={PhoneNumberDiscoverability.NotDiscoverable}
>
<AxoRadioGroup.Indicator />
<AxoRadioGroup.Label>
<div className={tw('type-body-medium')}>
{i18n('icu:Preferences__pnp__discoverability__nobody')}