Allow AxoItem labels to wrap before switching to stacked layout

Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-09-10 21:35:56 +00:00
committed by GitHub
co-authored by Jamie
parent 1aeebfbbc6
commit 059eb85a4c
5 changed files with 80 additions and 68 deletions
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { TailwindStyles } from '../tw.dom.tsx';
/** @internal */
export function concatClassNames(
classNames: ReadonlyArray<string | boolean | null | undefined>
): TailwindStyles {
const { length } = classNames;
let result = '';
let first = true;
for (let index = 0; index < length; index += 1) {
const className = classNames[index];
if (typeof className === 'string') {
if (first) {
first = false;
} else {
result += ' ';
}
result += className;
}
}
return result as TailwindStyles;
}
/** @internal */
export function css(
...classnames: Array<
TailwindStyles | `axo-${string}` | boolean | null | undefined
>
): TailwindStyles {
return concatClassNames(classnames);
}
+1 -1
View File
@@ -639,7 +639,7 @@ export function StressTests(): ReactNode {
<Story.Legend label="Long label">
<Story.Callout>
These should all break into a stacked layout.
Long labels should <em>not</em> break into a stacked layout.
</Story.Callout>
<StressTest label={LONG_LABEL} />
+13 -8
View File
@@ -4,7 +4,7 @@
*/
@utility axo-item-centering-to-max-height {
@apply flex h-full min-h-fit items-center;
@apply flex h-[stretch] min-h-fit items-center;
/* --axo-leading-body-medium + --axo-leading-body-small + (description margin top) */
max-height: 38px;
}
@@ -50,6 +50,15 @@
@apply min-w-0;
/* Leave room for focus rings */
@apply -m-1;
/* Provider scroll state container */
@apply @container-[scroll-state] overflow-x-hidden;
}
:where(.axo-item-content) .axo-item-content-force-scroll-state {
/* We need this fix or the item will get stuck in a layout cycle */
/* Force scroll-state to stay stuck as scrollable once it becomes scrollable */
@apply relative container-scrollable:w-[calc(100%+1px)];
}
:where(.axo-item-content) .axo-item-content-inner {
@@ -64,6 +73,7 @@
}
:where(.axo-item-body) .axo-item-label {
@apply min-w-50;
@apply type-body-medium text-primary;
}
@@ -72,9 +82,9 @@
}
:where(.axo-item-body) .axo-item-description {
@apply min-w-50;
@apply mt-0.5;
@apply type-body-small text-secondary forced-colors:text-[GrayText];
@apply min-w-[min(max-content,120px)];
@apply wrap-break-word [word-break:auto-phrase];
}
@@ -102,7 +112,7 @@
.axo-item-label {
grid-area: axo-item-label;
@apply self-center;
@apply text-nowrap;
@apply contain-inline-size contain-layout;
}
.axo-item-value {
@@ -125,11 +135,6 @@
/* When overflowing container */
@container scroll-state(scrollable: inline) {
.axo-item-content-inner {
width: calc(100% + 1px);
padding-inline-end: 1px;
}
.axo-item-accessory {
@apply mt-0.5;
@apply flex-col items-stretch;
+26 -41
View File
@@ -6,6 +6,7 @@ import {
createStrictContext,
useStrictContext,
} from '../_internal/StrictContext.dom.tsx';
import { css } from '../_internal/css.dom.tsx';
import { tw } from '../tw.dom.tsx';
import { AriaClickable } from '../AriaClickable.dom.tsx';
import { forwardExtraPropsForRadix } from '../_internal/props.dom.tsx';
@@ -15,21 +16,6 @@ import { AxoButton } from '../AxoButton.dom.tsx';
import { AxoCheckbox } from '../AxoCheckbox.dom.tsx';
import { AxoAvatar } from '../AxoAvatar.dom.tsx';
import { variants } from '../_internal/variants.dom.tsx';
import { FlexWrapDetector } from '../_internal/FlexWrapDetector.dom.tsx';
const AXO_ITEM_GROUP_CLASS = 'axo-item-group';
const AXO_ITEM_ROOT_CLASS = 'axo-item-root';
const AXO_ITEM_ROOT_INNER_CLASS = 'axo-item-root-inner';
const AXO_ITEM_LEADING_CLASS = 'axo-item-leading';
const AXO_ITEM_CONTENT_CLASS = 'axo-item-content';
const AXO_ITEM_CONTENT_INNER_CLASS = 'axo-item-content-inner';
const AXO_ITEM_BODY_CLASS = 'axo-item-body';
const AXO_ITEM_LABEL_CLASS = 'axo-item-label';
const AXO_ITEM_ACCESSORY_CLASS = 'axo-item-accessory';
const AXO_ITEM_VALUE_CLASS = 'axo-item-value';
const AXO_ITEM_DESCRIPTION_CLASS = 'axo-item-description';
const AXO_ITEM_TRAILING_CLASS = 'axo-item-trailing';
const AXO_ITEM_ARROW_CLASS = 'axo-item-arrow';
/**
* @example Anatomy
@@ -92,7 +78,7 @@ export namespace AxoBaseItem {
<GroupContext value={context}>
<div
ref={ref}
className={AXO_ITEM_GROUP_CLASS}
className="axo-item-group"
{...forwardExtraPropsForRadix(rest)}
>
{children}
@@ -161,14 +147,14 @@ export namespace AxoBaseItem {
<RootContext value={context}>
<AriaClickable.Root asChild>
<div
className={tw(AXO_ITEM_ROOT_CLASS, 'group')}
className={css('axo-item-root', tw('group'))}
{...forwardExtraPropsForRadix(rest)}
>
<div
className={tw(
AXO_ITEM_ROOT_INNER_CLASS,
className={css(
'axo-item-root-inner',
RootSpacing.get(groupContext.spacing),
disabled && 'text-disabled'
disabled && tw('text-disabled')
)}
>
{children}
@@ -191,7 +177,7 @@ export namespace AxoBaseItem {
}>;
export const Leading: FC<LeadingProps> = memo(props => {
return <div className={AXO_ITEM_LEADING_CLASS}>{props.children}</div>;
return <div className="axo-item-leading">{props.children}</div>;
});
Leading.displayName = 'AxoBaseItem.Leading';
@@ -281,10 +267,9 @@ export namespace AxoBaseItem {
export const Content: FC<ContentProps> = memo(props => {
return (
<div className={AXO_ITEM_CONTENT_CLASS}>
<FlexWrapDetector>
<div className={AXO_ITEM_CONTENT_INNER_CLASS}>{props.children}</div>
</FlexWrapDetector>
<div className="axo-item-content">
<div className="axo-item-content-inner">{props.children}</div>
<div className="axo-item-content-force-scroll-state" />
</div>
);
});
@@ -301,7 +286,7 @@ export namespace AxoBaseItem {
}>;
export const Body: FC<BodyProps> = memo(props => {
return <div className={AXO_ITEM_BODY_CLASS}>{props.children}</div>;
return <div className="axo-item-body">{props.children}</div>;
});
Body.displayName = 'AxoBaseItem.Body';
@@ -323,9 +308,9 @@ export namespace AxoBaseItem {
return (
<div
ref={ref}
className={tw(
AXO_ITEM_LABEL_CLASS,
truncate && 'truncate',
className={css(
'axo-item-label',
truncate && tw('truncate'),
disabled ? DisabledVariants.get(variant) : Variants.get(variant)
)}
{...forwardExtraPropsForRadix(rest)}
@@ -353,9 +338,9 @@ export namespace AxoBaseItem {
return (
<div
ref={ref}
className={tw(
AXO_ITEM_VALUE_CLASS,
disabled && 'text-disabled forced-colors:text-[GrayText]'
className={css(
'axo-item-value',
disabled && tw('text-disabled forced-colors:text-[GrayText]')
)}
{...forwardExtraPropsForRadix(rest)}
>
@@ -383,10 +368,10 @@ export namespace AxoBaseItem {
return (
<div
ref={ref}
className={tw(
AXO_ITEM_DESCRIPTION_CLASS,
truncate && 'truncate',
disabled && 'text-disabled'
className={css(
'axo-item-description',
truncate && tw('truncate'),
disabled && tw('text-disabled')
)}
{...forwardExtraPropsForRadix(rest)}
>
@@ -436,7 +421,7 @@ export namespace AxoBaseItem {
}>;
export const Accessory: FC<AccessoryProps> = memo(props => {
return <div className={AXO_ITEM_ACCESSORY_CLASS}>{props.children}</div>;
return <div className="axo-item-accessory">{props.children}</div>;
});
Accessory.displayName = 'AxoBaseItem.Accessory';
@@ -523,7 +508,7 @@ export namespace AxoBaseItem {
}>;
export const Trailing: FC<TrailingProps> = memo(props => {
return <div className={AXO_ITEM_TRAILING_CLASS}>{props.children}</div>;
return <div className="axo-item-trailing">{props.children}</div>;
});
Trailing.displayName = 'AxoBaseItem.Trailing';
@@ -552,9 +537,9 @@ export namespace AxoBaseItem {
const { disabled } = useStrictContext(RootContext);
return (
<div
className={tw(
AXO_ITEM_ARROW_CLASS,
disabled && 'text-disabled forced-colors:text-[GrayText]'
className={css(
'axo-item-arrow',
disabled && tw('text-disabled forced-colors:text-[GrayText]')
)}
>
<AxoSymbol.InlineGlyph label={null} symbol={ArrowKinds.get(kind)} />
+3 -18
View File
@@ -1,6 +1,8 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { concatClassNames } from './_internal/css.dom.tsx';
/** Opaque type for styles returned by tw() */
export type TailwindStyles = string & { __Styles: never };
@@ -21,22 +23,5 @@ export function tw(
TailwindStyles | string | boolean | null | undefined
>
): TailwindStyles {
const { length } = classNames;
let result = '';
let first = true;
for (let index = 0; index < length; index += 1) {
const className = classNames[index];
if (typeof className === 'string') {
if (first) {
first = false;
} else {
result += ' ';
}
result += className;
}
}
return result as TailwindStyles;
return concatClassNames(classNames);
}