mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-07-25 15:36:55 +01:00
Update AxoBadge component
This commit is contained in:
@@ -31,6 +31,10 @@
|
||||
"messageformat": "Clear",
|
||||
"description": "Axo Design System > Text Field Component > Clear Button > Generic accessibility label"
|
||||
},
|
||||
"icu:AxoBadge.MaxOverflow": {
|
||||
"messageformat": "{max, number}+",
|
||||
"description": "Axo Design System > Badge Component > When numeric value is greater than max (Ex: 99 -> '99', 100 -> '99+')"
|
||||
},
|
||||
"icu:AddUserToAnotherGroupModal__title": {
|
||||
"messageformat": "Add to a group",
|
||||
"description": "Shown as the title of the dialog that allows you to add a contact to an group"
|
||||
@@ -435,6 +439,10 @@
|
||||
"messageformat": "{maxCount, number}+",
|
||||
"description": "Left Pane > Inbox > Chat List > Chat Folders > Item > Badge Count > When over the max count (Example: 1000 or more would be 999+)"
|
||||
},
|
||||
"icu:LeftPaneChatFolders__ItemUnreadBadge__AccessibleLabel": {
|
||||
"messageformat": "{count, plural, one {# Unread Message} other {# Unread Messages}}",
|
||||
"description": "Left Pane > Inbox > Chat List > Chat Folders > Item > Badge Count > Accessible Label"
|
||||
},
|
||||
"icu:LeftPaneChatFolders__Item__ContextMenu__MarkAllRead": {
|
||||
"messageformat": "Mark all read",
|
||||
"description": "Left Pane > Inbox > Chat List > Chat Folders > Item > Right-Click Context Menu > Mark all unread chats in chat folder as read"
|
||||
|
||||
@@ -3,56 +3,95 @@
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { ExperimentalAxoBadge } from './AxoBadge.dom.tsx';
|
||||
import { AxoBadge } from './AxoBadge.dom.tsx';
|
||||
import { tw } from './tw.dom.tsx';
|
||||
|
||||
export default {
|
||||
title: 'Axo/AriaBadge (Experimental)',
|
||||
title: 'Axo/AxoBadge',
|
||||
} satisfies Meta;
|
||||
|
||||
export function All(): JSX.Element {
|
||||
const values: ReadonlyArray<ExperimentalAxoBadge.Value> = [
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
10,
|
||||
123,
|
||||
1234,
|
||||
12345,
|
||||
'mention',
|
||||
'unread',
|
||||
];
|
||||
|
||||
function Template(props: Partial<AxoBadge.RootProps>) {
|
||||
return (
|
||||
<table className={tw('border-separate border-spacing-2 text-center')}>
|
||||
<thead>
|
||||
<th>size</th>
|
||||
{values.map(value => {
|
||||
return <th key={value}>{value}</th>;
|
||||
})}
|
||||
</thead>
|
||||
<tbody>
|
||||
{ExperimentalAxoBadge._getAllSizes().map(size => {
|
||||
return (
|
||||
<tr key={size}>
|
||||
<th>{size}</th>
|
||||
{values.map(value => {
|
||||
return (
|
||||
<td key={value} className={tw('')}>
|
||||
<ExperimentalAxoBadge.Root
|
||||
size={size}
|
||||
value={value}
|
||||
max={99}
|
||||
maxDisplay="99+"
|
||||
label={null}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<AxoBadge.Root
|
||||
variant="primary"
|
||||
size="lg"
|
||||
value={1}
|
||||
max={99}
|
||||
label={null}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Basic(): JSX.Element {
|
||||
return <Template />;
|
||||
}
|
||||
|
||||
export function Digits(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('flex gap-4')}>
|
||||
<Template value={10} max={Infinity} />
|
||||
<Template value={99} max={Infinity} />
|
||||
<Template value={999} max={Infinity} />
|
||||
<Template value={9999} max={Infinity} />
|
||||
<Template value={999999999} max={Infinity} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Max(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('flex gap-4')}>
|
||||
<Template value={9} max={9} />
|
||||
<Template value={10} max={9} />
|
||||
<Template value={99} max={99} />
|
||||
<Template value={100} max={99} />
|
||||
<Template value={999} max={999} />
|
||||
<Template value={1000} max={999} />
|
||||
<Template value={9999} max={9999} />
|
||||
<Template value={10000} max={9999} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Mention(): JSX.Element {
|
||||
return <Template value="mention" />;
|
||||
}
|
||||
|
||||
export function Unread(): JSX.Element {
|
||||
return <Template value="unread" />;
|
||||
}
|
||||
|
||||
export function Error(): JSX.Element {
|
||||
return <Template variant="destructive" value="error" />;
|
||||
}
|
||||
|
||||
export function Variants(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('flex gap-4')}>
|
||||
<Template variant="primary" />
|
||||
<Template variant="destructive" />
|
||||
<Template variant="secondary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sizes(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('flex gap-4')}>
|
||||
<Template size="dot" />
|
||||
<Template size="sm" />
|
||||
<Template size="md" />
|
||||
<Template size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Dot(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('flex gap-4')}>
|
||||
<Template size="dot" variant="primary" value="unread" />
|
||||
<Template size="dot" variant="destructive" value="unread" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+93
-26
@@ -6,6 +6,7 @@ import { AxoSymbol } from './AxoSymbol.dom.tsx';
|
||||
import { tw } from './tw.dom.tsx';
|
||||
import { unreachable } from './_internal/assert.std.tsx';
|
||||
import { variants } from './_internal/variants.dom.tsx';
|
||||
import { type AxoIntl, useAxoIntl } from './_internal/AxoIntl.dom.tsx';
|
||||
|
||||
/**
|
||||
* @example Anatomy
|
||||
@@ -21,14 +22,19 @@ import { variants } from './_internal/variants.dom.tsx';
|
||||
* </AxoBadge.Root>
|
||||
* ````
|
||||
*/
|
||||
export namespace ExperimentalAxoBadge {
|
||||
export namespace AxoBadge {
|
||||
/**
|
||||
* Visual style of the badge.
|
||||
*/
|
||||
export type Variant = 'primary' | 'secondary' | 'destructive';
|
||||
|
||||
/**
|
||||
* Visual size of the badge.
|
||||
* - `sm`: 14px height
|
||||
* - `md`: 16px height
|
||||
* - `lg`: 18px height
|
||||
*/
|
||||
export type Size = 'sm' | 'md' | 'lg';
|
||||
export type Size = 'dot' | 'sm' | 'md' | 'lg';
|
||||
|
||||
/**
|
||||
* What the badge represents.
|
||||
@@ -36,25 +42,39 @@ export namespace ExperimentalAxoBadge {
|
||||
* - `'mention'`: Shows an `@`-sign icon.
|
||||
* - `'unread'`: A dot with no text content.
|
||||
*/
|
||||
export type Value = number | 'mention' | 'unread';
|
||||
export type Value = number | 'mention' | 'unread' | 'error';
|
||||
|
||||
const baseStyles = tw(
|
||||
'flex size-fit items-center justify-center-safe overflow-clip',
|
||||
'flex items-center justify-center-safe overflow-clip',
|
||||
'rounded-full font-semibold',
|
||||
'bg-accent text-primary-oncolor',
|
||||
'forced-color-adjust-none forced-colors:bg-[Mark] forced-colors:text-[MarkText]'
|
||||
);
|
||||
|
||||
const Variants = variants<Variant>('AxoBadge.Variant', {
|
||||
primary: tw('bg-accent text-primary-oncolor'),
|
||||
secondary: tw('bg-primary text-secondary'),
|
||||
destructive: tw('bg-destructive text-primary-oncolor'),
|
||||
});
|
||||
|
||||
const Sizes = variants<Size>('AxoBadge.Size', {
|
||||
sm: tw(baseStyles, 'min-h-3.5 min-w-3.5 text-[8px] leading-3.5'),
|
||||
md: tw(baseStyles, 'min-h-4 min-w-4 text-[11px] leading-4'),
|
||||
lg: tw(baseStyles, 'min-h-4.5 min-w-4.5 text-[11px] leading-4.5'),
|
||||
dot: tw('size-1.5'),
|
||||
sm: tw('size-fit min-h-3.5 min-w-3.5'),
|
||||
md: tw('size-fit min-h-4 min-w-4'),
|
||||
lg: tw('size-fit min-h-4.5 min-w-4.5'),
|
||||
});
|
||||
|
||||
const TextSizes = variants<Size>('AxoBadge.Size', {
|
||||
dot: tw('sr-only'),
|
||||
sm: tw('text-[8px] leading-3.5'),
|
||||
md: tw('text-[11px] leading-4'),
|
||||
lg: tw('text-[11px] leading-4.5'),
|
||||
});
|
||||
|
||||
const CountSizes = variants<Size>('AxoBadge.Size', {
|
||||
sm: tw('px-[3px]'),
|
||||
md: tw('px-[4px]'),
|
||||
lg: tw('px-[5px]'),
|
||||
dot: tw(),
|
||||
sm: tw('px-0.75'),
|
||||
md: tw('px-1'),
|
||||
lg: tw('px-1.25'),
|
||||
});
|
||||
|
||||
/** @testexport */
|
||||
@@ -67,10 +87,10 @@ export namespace ExperimentalAxoBadge {
|
||||
function formatBadgeCount(
|
||||
value: number,
|
||||
max: number,
|
||||
maxDisplay: string
|
||||
intl: AxoIntl.ContextType
|
||||
): string {
|
||||
if (value > max) {
|
||||
return maxDisplay;
|
||||
return intl.get('AxoBadge.MaxOverflow')(max);
|
||||
}
|
||||
cachedNumberFormat ??= new Intl.NumberFormat();
|
||||
return cachedNumberFormat.format(value);
|
||||
@@ -82,14 +102,14 @@ export namespace ExperimentalAxoBadge {
|
||||
*/
|
||||
|
||||
export type RootProps = Readonly<{
|
||||
/** Visual style of the badge. */
|
||||
variant: Variant;
|
||||
/** Visual size of the badge. */
|
||||
size: Size;
|
||||
/** What the badge represents. */
|
||||
value: Value;
|
||||
/** When `value` is a number, values above this are replaced with `maxDisplay`. */
|
||||
/** When `value` is a number, values above this are formatted `{max}+`. */
|
||||
max: number;
|
||||
/** The string shown when the numeric `value` exceeds `max` (e.g. `"999+"`). */
|
||||
maxDisplay: string;
|
||||
/** Accessible label for screen readers. Pass `null` if the badge is purely decorative. */
|
||||
label: string | null;
|
||||
}>;
|
||||
@@ -99,41 +119,88 @@ export namespace ExperimentalAxoBadge {
|
||||
*
|
||||
* @example Count with overflow
|
||||
* ```tsx
|
||||
* <ExperimentalAxoBadge.Root size="md" value={42} max={99} maxDisplay="99+" label="42 unread messages" />
|
||||
* <AxoBadge.Root
|
||||
* variant="primary"
|
||||
* size="md"
|
||||
* value={42}
|
||||
* max={99}
|
||||
* label="42 unread messages"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @example Mention
|
||||
* ```tsx
|
||||
* <ExperimentalAxoBadge.Root size="md" value="mention" max={0} maxDisplay="" label="You were mentioned" />
|
||||
* <AxoBadge.Root
|
||||
* variant="primary"
|
||||
* size="md"
|
||||
* value="mention"
|
||||
* max={99}
|
||||
* label="You were mentioned"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* @example Unread dot
|
||||
* @example Unread
|
||||
* ```tsx
|
||||
* <ExperimentalAxoBadge.Root size="md" value="unread" max={0} maxDisplay="" label="Marked unread" />
|
||||
* <AxoBadge.Root
|
||||
* variant="primary"
|
||||
* size="md"
|
||||
* value="unread"
|
||||
* max={99}
|
||||
* label="Marked unread"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* * @example Update Dot
|
||||
* ```tsx
|
||||
* <AxoBadge.Root
|
||||
* variant="primary"
|
||||
* size="dot"
|
||||
* value={0}
|
||||
* max={0}
|
||||
* label="Update Available"
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const Root: FC<RootProps> = memo(props => {
|
||||
const { size, value, max, maxDisplay } = props;
|
||||
const { variant, size, value, max } = props;
|
||||
const intl = useAxoIntl();
|
||||
|
||||
const children = useMemo(() => {
|
||||
if (value === 'unread') {
|
||||
return null;
|
||||
}
|
||||
if (value === 'error') {
|
||||
return (
|
||||
<span className={TextSizes.get(size)}>
|
||||
<AxoSymbol.InlineGlyph symbol="error-fill" label={null} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (value === 'mention') {
|
||||
return <AxoSymbol.InlineGlyph symbol="at" label={null} />;
|
||||
return (
|
||||
<span className={TextSizes.get(size)}>
|
||||
<AxoSymbol.InlineGlyph symbol="at" label={null} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return (
|
||||
<span aria-hidden className={CountSizes.get(size)}>
|
||||
{formatBadgeCount(value, max, maxDisplay)}
|
||||
<span
|
||||
aria-hidden
|
||||
className={tw(TextSizes.get(size), CountSizes.get(size))}
|
||||
>
|
||||
{formatBadgeCount(value, max, intl)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
unreachable(value);
|
||||
}, [size, value, max, maxDisplay]);
|
||||
}, [size, value, max, intl]);
|
||||
|
||||
return (
|
||||
<span aria-label={props.label ?? undefined} className={Sizes.get(size)}>
|
||||
<span
|
||||
aria-label={props.label ?? undefined}
|
||||
className={tw(baseStyles, Variants.get(variant), Sizes.get(size))}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -42,11 +42,11 @@ function Template(props: {
|
||||
Inbox
|
||||
</ExperimentalAxoSegmentedControl.ItemText>
|
||||
{props.includeBadges && (
|
||||
<ExperimentalAxoSegmentedControl.ExperimentalItemBadge
|
||||
<ExperimentalAxoSegmentedControl.ItemBadge
|
||||
variant="primary"
|
||||
value={42}
|
||||
max={99}
|
||||
maxDisplay="99+"
|
||||
label={null}
|
||||
label="42 unread messages"
|
||||
/>
|
||||
)}
|
||||
</ExperimentalAxoSegmentedControl.Item>
|
||||
@@ -56,11 +56,11 @@ function Template(props: {
|
||||
Drafts
|
||||
</ExperimentalAxoSegmentedControl.ItemText>
|
||||
{props.includeBadges && (
|
||||
<ExperimentalAxoSegmentedControl.ExperimentalItemBadge
|
||||
<ExperimentalAxoSegmentedControl.ItemBadge
|
||||
variant="primary"
|
||||
value="mention"
|
||||
max={99}
|
||||
maxDisplay="99+"
|
||||
label={null}
|
||||
label="You were mentioned"
|
||||
/>
|
||||
)}
|
||||
</ExperimentalAxoSegmentedControl.Item>
|
||||
@@ -69,11 +69,11 @@ function Template(props: {
|
||||
Sent
|
||||
</ExperimentalAxoSegmentedControl.ItemText>
|
||||
{props.includeBadges && (
|
||||
<ExperimentalAxoSegmentedControl.ExperimentalItemBadge
|
||||
<ExperimentalAxoSegmentedControl.ItemBadge
|
||||
variant="primary"
|
||||
value="unread"
|
||||
max={99}
|
||||
maxDisplay="99+"
|
||||
label={null}
|
||||
label="Marked unread"
|
||||
/>
|
||||
)}
|
||||
</ExperimentalAxoSegmentedControl.Item>
|
||||
|
||||
@@ -147,7 +147,7 @@ export namespace ExperimentalAxoSegmentedControl {
|
||||
*/
|
||||
value: string;
|
||||
/**
|
||||
* Should be an `ItemText`, optionally followed by an `ExperimentalItemBadge`.
|
||||
* Should be an `ItemText`, optionally followed by an `ItemBadge`.
|
||||
*/
|
||||
children: ReactNode;
|
||||
}>;
|
||||
@@ -156,9 +156,9 @@ export namespace ExperimentalAxoSegmentedControl {
|
||||
* An item in the group.
|
||||
*/
|
||||
export const Item: FC<ItemProps> = memo(props => {
|
||||
const { value, children, ...rest } = props;
|
||||
const { ref, value, children, ...rest } = props;
|
||||
return (
|
||||
<ToggleGroup.Item asChild ref={props.ref} value={value} {...rest}>
|
||||
<ToggleGroup.Item asChild ref={ref} value={value} {...rest}>
|
||||
<ExperimentalAxoBaseSegmentedControl.Item value={value}>
|
||||
{children}
|
||||
</ExperimentalAxoBaseSegmentedControl.Item>
|
||||
@@ -203,19 +203,15 @@ export namespace ExperimentalAxoSegmentedControl {
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type ExperimentalItemBadgeProps =
|
||||
ExperimentalAxoBaseSegmentedControl.ExperimentalItemBadgeProps;
|
||||
export type ItemBadgeProps =
|
||||
ExperimentalAxoBaseSegmentedControl.ItemBadgeProps;
|
||||
|
||||
/**
|
||||
* A badge shown on an item, typically for unread counts.
|
||||
*/
|
||||
export const ExperimentalItemBadge: FC<ExperimentalItemBadgeProps> = memo(
|
||||
props => {
|
||||
return (
|
||||
<ExperimentalAxoBaseSegmentedControl.ExperimentalItemBadge {...props} />
|
||||
);
|
||||
}
|
||||
);
|
||||
export const ItemBadge: FC<ItemBadgeProps> = memo(props => {
|
||||
return <ExperimentalAxoBaseSegmentedControl.ItemBadge {...props} />;
|
||||
});
|
||||
|
||||
ExperimentalItemBadge.displayName = 'AxoSegmentedControl.ItemBadge';
|
||||
ItemBadge.displayName = 'AxoSegmentedControl.ItemBadge';
|
||||
}
|
||||
|
||||
+16
-21
@@ -6,7 +6,7 @@ import { Select } from 'radix-ui';
|
||||
import { AxoBaseMenu } from './_internal/AxoBaseMenu.dom.tsx';
|
||||
import { AxoSymbol } from './AxoSymbol.dom.tsx';
|
||||
import { tw } from './tw.dom.tsx';
|
||||
import { ExperimentalAxoBadge } from './AxoBadge.dom.tsx';
|
||||
import { AxoBadge } from './AxoBadge.dom.tsx';
|
||||
import { AxoTheme } from './AxoTheme.dom.tsx';
|
||||
import { variants } from './_internal/variants.dom.tsx';
|
||||
|
||||
@@ -462,31 +462,26 @@ export namespace AxoSelect {
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type ExperimentalItemBadgeProps = Omit<
|
||||
ExperimentalAxoBadge.RootProps,
|
||||
'size'
|
||||
>;
|
||||
export type ItemBadgeProps = Omit<AxoBadge.RootProps, 'size'>;
|
||||
|
||||
/**
|
||||
* A badge shown at the trailing edge of an item, typically for unread counts.
|
||||
*/
|
||||
export const ExperimentalItemBadge = memo(
|
||||
(props: ExperimentalItemBadgeProps) => {
|
||||
return (
|
||||
<span className={tw('ms-[5px]')}>
|
||||
<ExperimentalAxoBadge.Root
|
||||
size="sm"
|
||||
value={props.value}
|
||||
max={props.max}
|
||||
maxDisplay={props.maxDisplay}
|
||||
label={props.label}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
export const ItemBadge: FC<ItemBadgeProps> = memo((props: ItemBadgeProps) => {
|
||||
return (
|
||||
<span className={tw('ms-[5px]')}>
|
||||
<AxoBadge.Root
|
||||
variant={props.variant}
|
||||
size="sm"
|
||||
value={props.value}
|
||||
max={props.max}
|
||||
label={props.label}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
||||
ExperimentalItemBadge.displayName = 'AxoSelect.ItemBadge';
|
||||
ItemBadge.displayName = 'AxoSelect.ItemBadge';
|
||||
|
||||
/**
|
||||
* <AxoSelect.Group>
|
||||
|
||||
@@ -5,9 +5,14 @@ import { memo, useId, useMemo } from 'react';
|
||||
import type { Transition } from 'motion/react';
|
||||
import { motion } from 'motion/react';
|
||||
import { tw } from '../tw.dom.tsx';
|
||||
import { ExperimentalAxoBadge } from '../AxoBadge.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';
|
||||
|
||||
/**
|
||||
* Used to share styles/animations for SegmentedControls, Toolbar ToggleGroups,
|
||||
@@ -165,8 +170,13 @@ export namespace ExperimentalAxoBaseSegmentedControl {
|
||||
}>;
|
||||
|
||||
export const Item: FC<ItemProps> = memo(props => {
|
||||
const { value, children, ...rest } = props;
|
||||
const { ref, value, children, ...rest } = props;
|
||||
const context = useStrictContext(RootContext);
|
||||
const {
|
||||
context: labellingContext,
|
||||
labelId,
|
||||
descriptionId,
|
||||
} = useCreateAriaLabellingContext();
|
||||
|
||||
const isSelected = useMemo(() => {
|
||||
if (context.value == null) {
|
||||
@@ -181,35 +191,39 @@ export namespace ExperimentalAxoBaseSegmentedControl {
|
||||
}, [value, context.value]);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={props.ref}
|
||||
type="button"
|
||||
className={tw(
|
||||
'relative flex min-w-0 items-center justify-center px-3 py-[5px]',
|
||||
'cursor-pointer rounded-full type-body-medium font-medium text-primary',
|
||||
'outline-focused-inner not-forced-colors:outline-none not-forced-colors:keyboard-mode:focus:axo-focus-ring',
|
||||
'forced-colors:bg-[ButtonFace] forced-colors:text-[ButtonText]',
|
||||
'forced-colors:data-[axo-contextmenu-state=open]:text-[HighlightText]',
|
||||
ItemWidths.get(context.itemWidth),
|
||||
isSelected && tw('forced-colors:text-[SelectedItemText]'),
|
||||
!isSelected &&
|
||||
tw(
|
||||
'data-[axo-contextmenu-state=open]:bg-primary',
|
||||
'forced-colors:data-[axo-contextmenu-state=open]:bg-[Highlight]'
|
||||
)
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{isSelected && (
|
||||
<motion.span
|
||||
layoutId={`${context.id}.Indicator`}
|
||||
className={IndicatorStyles.get(context.variant)}
|
||||
transition={IndicatorTransition}
|
||||
style={{ borderRadius: 14 }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<AriaLabellingProvider value={labellingContext}>
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
className={tw(
|
||||
'relative flex min-w-0 items-center justify-center px-3 py-[5px]',
|
||||
'cursor-pointer rounded-full type-body-medium font-medium text-primary',
|
||||
'outline-focused-inner not-forced-colors:outline-none not-forced-colors:keyboard-mode:focus:axo-focus-ring',
|
||||
'forced-colors:bg-[ButtonFace] forced-colors:text-[ButtonText]',
|
||||
'forced-colors:data-[axo-contextmenu-state=open]:text-[HighlightText]',
|
||||
ItemWidths.get(context.itemWidth),
|
||||
isSelected && tw('forced-colors:text-[SelectedItemText]'),
|
||||
!isSelected &&
|
||||
tw(
|
||||
'data-[axo-contextmenu-state=open]:bg-primary',
|
||||
'forced-colors:data-[axo-contextmenu-state=open]:bg-[Highlight]'
|
||||
)
|
||||
)}
|
||||
aria-labelledby={labelId}
|
||||
aria-describedby={descriptionId}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{isSelected && (
|
||||
<motion.span
|
||||
layoutId={`${context.id}.Indicator`}
|
||||
className={IndicatorStyles.get(context.variant)}
|
||||
transition={IndicatorTransition}
|
||||
style={{ borderRadius: 14 }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</AriaLabellingProvider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -231,8 +245,12 @@ 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 }}
|
||||
>
|
||||
@@ -248,27 +266,30 @@ export namespace ExperimentalAxoBaseSegmentedControl {
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type ExperimentalItemBadgeProps = Omit<
|
||||
ExperimentalAxoBadge.RootProps,
|
||||
'size'
|
||||
>;
|
||||
export type ItemBadgeProps = Omit<AxoBadge.RootProps, 'size'>;
|
||||
|
||||
/** A badge rendered to the right of the item label. */
|
||||
export const ExperimentalItemBadge = memo(
|
||||
(props: ExperimentalItemBadgeProps) => {
|
||||
return (
|
||||
<span className={tw('relative z-20 ms-[5px]')}>
|
||||
<ExperimentalAxoBadge.Root
|
||||
size="md"
|
||||
value={props.value}
|
||||
max={props.max}
|
||||
maxDisplay={props.maxDisplay}
|
||||
label={props.label}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
ExperimentalItemBadge.displayName = 'AxoBaseSegmentedControl.ItemBadge';
|
||||
ItemBadge.displayName = 'AxoBaseSegmentedControl.ItemBadge';
|
||||
}
|
||||
|
||||
@@ -16,20 +16,21 @@ export namespace AxoIntl {
|
||||
'AxoDialog.Back': 'Back',
|
||||
'AxoDialog.Close': 'Close',
|
||||
'AxoTextField.Clear': 'Clear',
|
||||
'AxoBadge.MaxOverflow': (max: number) => `${max}+`,
|
||||
};
|
||||
|
||||
/** A key for a built-in Axo UI string. */
|
||||
export type MessageKey = keyof typeof DefaultMessages;
|
||||
|
||||
/** Map of all message keys to their translated strings. */
|
||||
export type Messages = Record<MessageKey, string>;
|
||||
export type Messages = typeof DefaultMessages;
|
||||
|
||||
/** A key for a built-in Axo UI string. */
|
||||
export type MessageKey = keyof Messages;
|
||||
|
||||
/** Directionality of text */
|
||||
export type Direction = 'ltr' | 'rtl';
|
||||
|
||||
/** The intl API available via `useAxoIntl`. */
|
||||
export type ContextType = Readonly<{
|
||||
get: (key: MessageKey) => string;
|
||||
get: <const K extends MessageKey>(key: K) => Messages[K];
|
||||
}>;
|
||||
|
||||
function createIntlContext(messages: Messages): ContextType {
|
||||
|
||||
@@ -44,9 +44,7 @@ export type LeftPaneChatFoldersProps = Readonly<{
|
||||
onChatFolderOpenSettings: (chatFolderId: ChatFolderId) => void;
|
||||
}>;
|
||||
|
||||
function getBadgeValue(
|
||||
unreadStats: UnreadStats | null
|
||||
): ExperimentalAxoSegmentedControl.ExperimentalItemBadgeProps['value'] | null {
|
||||
function getBadgeValue(unreadStats: UnreadStats | null): number | null {
|
||||
if (unreadStats == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -210,16 +208,14 @@ function ChatFolderSelectItem(props: {
|
||||
{getChatFolderLabel(i18n, props.chatFolder, true)}
|
||||
</AxoSelect.ItemText>
|
||||
{badgeValue != null && (
|
||||
<AxoSelect.ExperimentalItemBadge
|
||||
<AxoSelect.ItemBadge
|
||||
variant="primary"
|
||||
value={badgeValue}
|
||||
max={UNREAD_BADGE_MAX_COUNT}
|
||||
maxDisplay={i18n(
|
||||
'icu:LeftPaneChatFolders__ItemUnreadBadge__MaxCount',
|
||||
{
|
||||
maxCount: UNREAD_BADGE_MAX_COUNT,
|
||||
}
|
||||
label={i18n(
|
||||
'icu:LeftPaneChatFolders__ItemUnreadBadge__AccessibleLabel',
|
||||
{ count: badgeValue }
|
||||
)}
|
||||
label={null}
|
||||
/>
|
||||
)}
|
||||
</AxoSelect.Item>
|
||||
@@ -256,14 +252,14 @@ function ChatFolderSegmentedControlItem(props: {
|
||||
{getChatFolderLabel(i18n, props.chatFolder, false)}
|
||||
</ExperimentalAxoSegmentedControl.ItemText>
|
||||
{badgeValue != null && (
|
||||
<ExperimentalAxoSegmentedControl.ExperimentalItemBadge
|
||||
<ExperimentalAxoSegmentedControl.ItemBadge
|
||||
variant="primary"
|
||||
value={badgeValue}
|
||||
max={UNREAD_BADGE_MAX_COUNT}
|
||||
maxDisplay={i18n(
|
||||
'icu:LeftPaneChatFolders__ItemUnreadBadge__MaxCount',
|
||||
{ maxCount: UNREAD_BADGE_MAX_COUNT }
|
||||
label={i18n(
|
||||
'icu:LeftPaneChatFolders__ItemUnreadBadge__AccessibleLabel',
|
||||
{ count: badgeValue }
|
||||
)}
|
||||
label={null}
|
||||
/>
|
||||
)}
|
||||
</ExperimentalAxoSegmentedControl.Item>
|
||||
|
||||
@@ -20,6 +20,7 @@ export const AppProvider: FC<AppProviderProps> = memo(
|
||||
'AxoDialog.Back': i18n('icu:AxoDialog.Back'),
|
||||
'AxoDialog.Close': i18n('icu:AxoDialog.Close'),
|
||||
'AxoTextField.Clear': i18n('icu:AxoTextField.Clear'),
|
||||
'AxoBadge.MaxOverflow': max => i18n('icu:AxoBadge.MaxOverflow', { max }),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user