mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Axo dialog design updates & aria checks
This commit is contained in:
@@ -78,11 +78,7 @@ function CardButton(props: {
|
||||
}) {
|
||||
return (
|
||||
<AriaClickable.SubWidget>
|
||||
<AxoButton.Root
|
||||
variant={props.variant}
|
||||
size="medium"
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<AxoButton.Root variant={props.variant} size="md" onClick={props.onClick}>
|
||||
{props.children}
|
||||
</AxoButton.Root>
|
||||
</AriaClickable.SubWidget>
|
||||
|
||||
@@ -20,7 +20,12 @@ const EXAMPLE_TITLE_LONG = (
|
||||
</>
|
||||
);
|
||||
|
||||
const EXAMPLE_DESCRIPTION = <>Exporting chat</>;
|
||||
const EXAMPLE_DESCRIPTION = (
|
||||
<>
|
||||
Your chat will be downloaded in the background. You can continue to use
|
||||
Signal during this process.
|
||||
</>
|
||||
);
|
||||
const EXAMPLE_DESCRIPTION_LONG = (
|
||||
<>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Nobis, amet aut
|
||||
@@ -72,12 +77,11 @@ function Template(props: {
|
||||
return (
|
||||
<AxoAlertDialog.Root open={open} onOpenChange={setOpen}>
|
||||
<AxoAlertDialog.Trigger>
|
||||
<AxoButton.Root variant="subtle-primary" size="medium">
|
||||
<AxoButton.Root variant="subtle-primary" size="md">
|
||||
Open
|
||||
</AxoButton.Root>
|
||||
</AxoAlertDialog.Trigger>
|
||||
<AxoAlertDialog.Content
|
||||
size="md"
|
||||
escape={
|
||||
props.requireExplicitChoice
|
||||
? 'cancel-is-destructive'
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { AxoSymbol } from './AxoSymbol.dom.js';
|
||||
|
||||
const Namespace = 'AxoAlertDialog';
|
||||
|
||||
const { useContentEscapeBehavior, useContentSize } = AxoBaseDialog;
|
||||
const { useContentEscapeBehavior } = AxoBaseDialog;
|
||||
|
||||
/**
|
||||
* Displays a menu located at the pointer, triggered by a right click or a long press.
|
||||
@@ -74,30 +74,29 @@ export namespace AxoAlertDialog {
|
||||
* --------------------------------
|
||||
*/
|
||||
|
||||
export type ContentSize = AxoBaseDialog.ContentSize;
|
||||
export type ContentEscape = AxoBaseDialog.ContentEscape;
|
||||
export type ContentProps = AxoBaseDialog.ContentProps;
|
||||
export type ContentProps = Readonly<{
|
||||
escape: ContentEscape;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const Content: FC<ContentProps> = memo(props => {
|
||||
const sizeConfig = AxoBaseDialog.ContentSizes[props.size];
|
||||
const handleContentEscapeEvent = useContentEscapeBehavior(props.escape);
|
||||
return (
|
||||
<AxoBaseDialog.ContentSizeProvider value={props.size}>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay className={AxoBaseDialog.overlayStyles}>
|
||||
<AlertDialog.Content
|
||||
onEscapeKeyDown={handleContentEscapeEvent}
|
||||
className={AxoBaseDialog.contentStyles}
|
||||
style={{
|
||||
minWidth: sizeConfig.minWidth,
|
||||
width: sizeConfig.width,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Overlay>
|
||||
</AlertDialog.Portal>
|
||||
</AxoBaseDialog.ContentSizeProvider>
|
||||
<AlertDialog.Portal>
|
||||
<AlertDialog.Overlay className={AxoBaseDialog.overlayStyles}>
|
||||
<AlertDialog.Content
|
||||
onEscapeKeyDown={handleContentEscapeEvent}
|
||||
className={AxoBaseDialog.contentStyles}
|
||||
style={{
|
||||
minWidth: 300,
|
||||
width: 300,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Overlay>
|
||||
</AlertDialog.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -113,14 +112,8 @@ export namespace AxoAlertDialog {
|
||||
}>;
|
||||
|
||||
export const Body: FC<BodyProps> = memo(props => {
|
||||
const contentSize = useContentSize();
|
||||
const contentSizeConfig = AxoBaseDialog.ContentSizes[contentSize];
|
||||
|
||||
return (
|
||||
<AxoScrollArea.Root
|
||||
maxHeight={contentSizeConfig.maxBodyHeight}
|
||||
scrollbarWidth="none"
|
||||
>
|
||||
<AxoScrollArea.Root maxHeight={440} scrollbarWidth="none">
|
||||
<AxoScrollArea.Hint edge="bottom" />
|
||||
<AxoScrollArea.Viewport>
|
||||
<AxoScrollArea.Content>
|
||||
@@ -208,7 +201,7 @@ export namespace AxoAlertDialog {
|
||||
export const Cancel: FC<CancelProps> = memo(props => {
|
||||
return (
|
||||
<AlertDialog.Cancel asChild>
|
||||
<AxoButton.Root variant="secondary" size="medium" width="fill">
|
||||
<AxoButton.Root variant="secondary" size="md" width="full">
|
||||
{props.children}
|
||||
</AxoButton.Root>
|
||||
</AlertDialog.Cancel>
|
||||
@@ -239,8 +232,8 @@ export namespace AxoAlertDialog {
|
||||
variant={props.variant}
|
||||
symbol={props.symbol}
|
||||
arrow={props.arrow}
|
||||
size="medium"
|
||||
width="fill"
|
||||
size="md"
|
||||
width="full"
|
||||
>
|
||||
{props.children}
|
||||
</AxoButton.Root>
|
||||
|
||||
@@ -145,7 +145,7 @@ const LONG_TEXT = (
|
||||
|
||||
function Fit(props: { longText?: boolean }) {
|
||||
return (
|
||||
<AxoButton.Root variant="primary" size="medium" width="fit">
|
||||
<AxoButton.Root variant="primary" size="md" width="fit">
|
||||
Fit {props.longText && LONG_TEXT}
|
||||
</AxoButton.Root>
|
||||
);
|
||||
@@ -153,15 +153,15 @@ function Fit(props: { longText?: boolean }) {
|
||||
|
||||
function Grow(props: { longText?: boolean }) {
|
||||
return (
|
||||
<AxoButton.Root variant="affirmative" size="medium" width="grow">
|
||||
<AxoButton.Root variant="affirmative" size="md" width="grow">
|
||||
Grow {props.longText && LONG_TEXT}
|
||||
</AxoButton.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function Fill(props: { longText?: boolean }) {
|
||||
function Full(props: { longText?: boolean }) {
|
||||
return (
|
||||
<AxoButton.Root variant="destructive" size="medium" width="fill">
|
||||
<AxoButton.Root variant="destructive" size="md" width="full">
|
||||
Fill {props.longText && LONG_TEXT}
|
||||
</AxoButton.Root>
|
||||
);
|
||||
@@ -180,7 +180,7 @@ function WidthTestTemplate(props: {
|
||||
<>
|
||||
<Fit />
|
||||
<Grow />
|
||||
<Fill />
|
||||
<Full />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -239,26 +239,26 @@ function WidthTestTemplate(props: {
|
||||
<p>Fill</p>
|
||||
{props.children(
|
||||
<>
|
||||
<Fill />
|
||||
<Fill />
|
||||
<Fill />
|
||||
<Full />
|
||||
<Full />
|
||||
<Full />
|
||||
</>
|
||||
)}
|
||||
<p>Fill: With long text</p>
|
||||
<p>Full: With long text</p>
|
||||
{props.children(
|
||||
<>
|
||||
<Fill longText />
|
||||
<Fill longText />
|
||||
<Fill longText />
|
||||
<Full longText />
|
||||
<Full longText />
|
||||
<Full longText />
|
||||
</>
|
||||
)}
|
||||
|
||||
<p>Fill: With mixed length texts</p>
|
||||
<p>Full: With mixed length texts</p>
|
||||
{props.children(
|
||||
<>
|
||||
<Fill />
|
||||
<Fill />
|
||||
<Fill longText />
|
||||
<Full />
|
||||
<Full />
|
||||
<Full longText />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -128,9 +128,9 @@ const AxoButtonVariants = {
|
||||
};
|
||||
|
||||
const AxoButtonSizes = {
|
||||
large: tw('min-w-16 px-4 py-2 type-body-medium font-medium'),
|
||||
medium: tw('min-w-14 px-3 py-1.5 type-body-medium font-medium'),
|
||||
small: tw('min-w-12 px-2 py-1 type-body-small font-medium'),
|
||||
lg: tw('min-w-16 px-4 py-2 type-body-medium font-medium'),
|
||||
md: tw('min-w-14 px-3 py-1.5 type-body-medium font-medium'),
|
||||
sm: tw('min-w-12 px-2 py-1 type-body-small font-medium'),
|
||||
} as const satisfies Record<string, TailwindStyles>;
|
||||
|
||||
type BaseButtonAttrs = Omit<
|
||||
@@ -171,9 +171,9 @@ const AxoButtonSpinnerSizes: Record<
|
||||
AxoButtonSize,
|
||||
{ size: number; strokeWidth: number }
|
||||
> = {
|
||||
large: { size: 20, strokeWidth: 2 },
|
||||
medium: { size: 20, strokeWidth: 2 },
|
||||
small: { size: 16, strokeWidth: 1.5 },
|
||||
lg: { size: 20, strokeWidth: 2 },
|
||||
md: { size: 20, strokeWidth: 2 },
|
||||
sm: { size: 16, strokeWidth: 1.5 },
|
||||
};
|
||||
|
||||
type ExperimentalButtonSpinnerProps = Readonly<{
|
||||
@@ -204,7 +204,7 @@ export namespace AxoButton {
|
||||
export type Variant = AxoButtonVariant;
|
||||
export type Size = AxoButtonSize;
|
||||
|
||||
export type Width = 'fit' | 'grow' | 'fill';
|
||||
export type Width = 'fit' | 'grow' | 'full';
|
||||
|
||||
const Widths: Record<Width, TailwindStyles> = {
|
||||
/* Always try to fit to the content of the button */
|
||||
@@ -212,7 +212,7 @@ export namespace AxoButton {
|
||||
/* Allow the button to grow within a flex container */
|
||||
grow: tw('grow'),
|
||||
/* Always try to fill the available space */
|
||||
fill: tw('w-full'),
|
||||
full: tw('w-full'),
|
||||
};
|
||||
|
||||
export type RootProps = BaseButtonAttrs &
|
||||
|
||||
@@ -25,7 +25,7 @@ export namespace AxoCheckbox {
|
||||
disabled={props.disabled}
|
||||
required={props.required}
|
||||
className={tw(
|
||||
'flex size-5 items-center justify-center rounded-full',
|
||||
'flex size-5 items-center justify-center rounded-full leading-none',
|
||||
'border border-border-primary inset-shadow-on-color',
|
||||
'data-[state=unchecked]:bg-fill-primary',
|
||||
'data-[state=unchecked]:pressed:bg-fill-primary-pressed',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import React, { useId, useMemo, useState } from 'react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { AxoDialog } from './AxoDialog.dom.js';
|
||||
import { AxoButton } from './AxoButton.dom.js';
|
||||
import { tw } from './tw.dom.js';
|
||||
import { AxoCheckbox } from './AxoCheckbox.dom.js';
|
||||
import { getScrollbarGutters } from './_internal/scrollbars.dom.js';
|
||||
|
||||
export default {
|
||||
title: 'Axo/AxoDialog',
|
||||
@@ -46,7 +48,7 @@ function Template(props: {
|
||||
return (
|
||||
<AxoDialog.Root open={open} onOpenChange={setOpen}>
|
||||
<AxoDialog.Trigger>
|
||||
<AxoButton.Root variant="secondary" size="medium">
|
||||
<AxoButton.Root variant="secondary" size="md">
|
||||
Open Dialog
|
||||
</AxoButton.Root>
|
||||
</AxoDialog.Trigger>
|
||||
@@ -143,3 +145,219 @@ export function FooterContentLongAndTight(): JSX.Element {
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
function Spacer(props: { height: 8 | 12 }) {
|
||||
return <div style={{ height: props.height }} />;
|
||||
}
|
||||
|
||||
function TextInputField(props: { placeholder: string }) {
|
||||
const style = useMemo(() => {
|
||||
const bodyPadding = 24;
|
||||
const inputPadding = 16;
|
||||
|
||||
return { marginInline: inputPadding - bodyPadding };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={tw('py-1.5')} style={style}>
|
||||
<input
|
||||
placeholder={props.placeholder}
|
||||
className={tw(
|
||||
'w-full px-3 py-1.5',
|
||||
'border-[0.5px] border-border-primary shadow-elevation-0',
|
||||
'rounded-lg bg-fill-primary',
|
||||
'placeholder:text-label-placeholder'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExampleNicknameAndNoteDialog(): JSX.Element {
|
||||
const [open, setOpen] = useState(true);
|
||||
return (
|
||||
<AxoDialog.Root open={open} onOpenChange={setOpen}>
|
||||
<AxoDialog.Trigger>
|
||||
<AxoButton.Root variant="secondary" size="md">
|
||||
Open Dialog
|
||||
</AxoButton.Root>
|
||||
</AxoDialog.Trigger>
|
||||
<AxoDialog.Content size="sm" escape="cancel-is-destructive">
|
||||
<AxoDialog.Header>
|
||||
<AxoDialog.Title>Nickname</AxoDialog.Title>
|
||||
<AxoDialog.Close aria-label="Close" />
|
||||
</AxoDialog.Header>
|
||||
<AxoDialog.Body>
|
||||
<p className={tw('mb-4 type-body-small text-label-secondary')}>
|
||||
Nicknames & notes are stored with Signal and end-to-end
|
||||
encrypted. They are only visible to you.
|
||||
</p>
|
||||
<div
|
||||
className={tw('mx-auto size-20 rounded-full bg-color-fill-primary')}
|
||||
/>
|
||||
<Spacer height={12} />
|
||||
<TextInputField placeholder="First name" />
|
||||
<TextInputField placeholder="Last name" />
|
||||
<TextInputField placeholder="Note" />
|
||||
<Spacer height={12} />
|
||||
</AxoDialog.Body>
|
||||
<AxoDialog.Footer>
|
||||
<AxoDialog.Actions>
|
||||
<AxoDialog.Action variant="secondary" onClick={action('onCancel')}>
|
||||
Cancel
|
||||
</AxoDialog.Action>
|
||||
<AxoDialog.Action variant="primary" onClick={action('onSave')}>
|
||||
Save
|
||||
</AxoDialog.Action>
|
||||
</AxoDialog.Actions>
|
||||
</AxoDialog.Footer>
|
||||
</AxoDialog.Content>
|
||||
</AxoDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckboxField(props: { label: string }) {
|
||||
const id = useId();
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={tw('flex gap-3 py-2.5')}>
|
||||
<AxoCheckbox.Root
|
||||
id={id}
|
||||
checked={checked}
|
||||
onCheckedChange={setChecked}
|
||||
/>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={tw('truncate type-body-large text-label-primary')}
|
||||
>
|
||||
{props.label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExampleMuteNotificationsDialog(): JSX.Element {
|
||||
const [open, setOpen] = useState(true);
|
||||
return (
|
||||
<AxoDialog.Root open={open} onOpenChange={setOpen}>
|
||||
<AxoDialog.Trigger>
|
||||
<AxoButton.Root variant="secondary" size="md">
|
||||
Open Dialog
|
||||
</AxoButton.Root>
|
||||
</AxoDialog.Trigger>
|
||||
<AxoDialog.Content size="sm" escape="cancel-is-noop">
|
||||
<AxoDialog.Header>
|
||||
<AxoDialog.Title>Mute notifications</AxoDialog.Title>
|
||||
<AxoDialog.Close aria-label="Close" />
|
||||
</AxoDialog.Header>
|
||||
<AxoDialog.Body>
|
||||
<Spacer height={8} />
|
||||
<CheckboxField label="Mute for 1 hour" />
|
||||
<CheckboxField label="Mute for 8 hours" />
|
||||
<CheckboxField label="Mute for 1 day" />
|
||||
<CheckboxField label="Mute for 1 week" />
|
||||
<CheckboxField label="Mute always" />
|
||||
<Spacer height={8} />
|
||||
</AxoDialog.Body>
|
||||
<AxoDialog.Footer>
|
||||
<AxoDialog.Actions>
|
||||
<AxoDialog.Action variant="secondary" onClick={action('onCancel')}>
|
||||
Cancel
|
||||
</AxoDialog.Action>
|
||||
<AxoDialog.Action variant="primary" onClick={action('onSave')}>
|
||||
Save
|
||||
</AxoDialog.Action>
|
||||
</AxoDialog.Actions>
|
||||
</AxoDialog.Footer>
|
||||
</AxoDialog.Content>
|
||||
</AxoDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ExampleItem(props: { label: string; description: string }) {
|
||||
const labelId = useId();
|
||||
const descriptionId = useId();
|
||||
|
||||
const style = useMemo((): CSSProperties => {
|
||||
return {
|
||||
paddingInline: 24 - getScrollbarGutters('thin', 'custom').vertical,
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="option"
|
||||
aria-selected={false}
|
||||
aria-labelledby={labelId}
|
||||
aria-describedby={descriptionId}
|
||||
tabIndex={0}
|
||||
className={tw('rounded-lg py-2.5 hover:bg-fill-secondary')}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
id={labelId}
|
||||
className={tw('truncate type-body-large text-label-primary')}
|
||||
>
|
||||
{props.label}
|
||||
</div>
|
||||
<div
|
||||
id={descriptionId}
|
||||
className={tw('truncate type-body-small text-label-secondary')}
|
||||
>
|
||||
{props.description}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExampleLanguageDialog(): JSX.Element {
|
||||
const [open, setOpen] = useState(true);
|
||||
return (
|
||||
<AxoDialog.Root open={open} onOpenChange={setOpen}>
|
||||
<AxoDialog.Trigger>
|
||||
<AxoButton.Root variant="secondary" size="md">
|
||||
Open Dialog
|
||||
</AxoButton.Root>
|
||||
</AxoDialog.Trigger>
|
||||
<AxoDialog.Content size="sm" escape="cancel-is-noop">
|
||||
<AxoDialog.Header>
|
||||
<AxoDialog.Title>Language</AxoDialog.Title>
|
||||
<AxoDialog.Close aria-label="Close" />
|
||||
</AxoDialog.Header>
|
||||
<AxoDialog.ExperimentalSearch>
|
||||
<input
|
||||
type="search"
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus
|
||||
placeholder="Search languages"
|
||||
className={tw('w-full rounded-lg bg-fill-secondary px-3 py-[5px]')}
|
||||
/>
|
||||
</AxoDialog.ExperimentalSearch>
|
||||
<AxoDialog.Body padding="only-scrollbar-gutter">
|
||||
<div role="listbox">
|
||||
<ExampleItem label="System Language" description="English" />
|
||||
<ExampleItem label="Afrikaans" description="Afrikaans" />
|
||||
<ExampleItem label="Arabic" description="العربية" />
|
||||
<ExampleItem label="Azerbaijani" description="Azərbaycan dili" />
|
||||
<ExampleItem label="Bulgarian" description="Български" />
|
||||
<ExampleItem label="Bangla" description="বাংলা" />
|
||||
<ExampleItem label="Bosnian" description="bosanski" />
|
||||
<ExampleItem label="Catalan" description="català" />
|
||||
<ExampleItem label="Czech" description="Čeština" />
|
||||
</div>
|
||||
</AxoDialog.Body>
|
||||
<AxoDialog.Footer>
|
||||
<AxoDialog.Actions>
|
||||
<AxoDialog.Action variant="secondary" onClick={action('onCancel')}>
|
||||
Cancel
|
||||
</AxoDialog.Action>
|
||||
<AxoDialog.Action variant="primary" onClick={action('onSet')}>
|
||||
Set
|
||||
</AxoDialog.Action>
|
||||
</AxoDialog.Actions>
|
||||
</AxoDialog.Footer>
|
||||
</AxoDialog.Content>
|
||||
</AxoDialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
+90
-59
@@ -19,30 +19,30 @@ import { AxoButton } from './AxoButton.dom.js';
|
||||
|
||||
const Namespace = 'AxoDialog';
|
||||
|
||||
const { useContentEscapeBehavior, useContentSize } = AxoBaseDialog;
|
||||
const { useContentEscapeBehavior } = AxoBaseDialog;
|
||||
|
||||
// We want to have 25px of padding on either side of header/body/footer, but
|
||||
// We want to have 24px of padding on either side of header/body/footer, but
|
||||
// it's import that we remain aligned with the vertical scrollbar gutters that
|
||||
// we need to measure in the browser to know the value of.
|
||||
//
|
||||
// Chrome currently renders vertical scrollbars as 11px with
|
||||
// `scrollbar-width: thin` but that could change someday or based on some OS
|
||||
// settings. So we'll target 24px but we'll tolerate different values.
|
||||
const SCROLLBAR_WIDTH_EXPECTED = 11; /* (keep in sync with chromium) */
|
||||
const SCROLLBAR_WIDTH_ACTUAL = getScrollbarGutters('thin', 'custom').vertical;
|
||||
function getPadding(target: number, scrollbars: boolean): number {
|
||||
const scrollbarWidthExpected = 11;
|
||||
const paddingBeforeScrollbarWidth = target - scrollbarWidthExpected;
|
||||
|
||||
const DIALOG_PADDING_TARGET = 20;
|
||||
if (scrollbars) {
|
||||
// If this element has scrollbars we should just rely on the rendered gutter
|
||||
return paddingBeforeScrollbarWidth;
|
||||
}
|
||||
|
||||
const DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH =
|
||||
DIALOG_PADDING_TARGET - SCROLLBAR_WIDTH_EXPECTED;
|
||||
const scrollbarWidthActual = getScrollbarGutters('thin', 'custom').vertical;
|
||||
|
||||
const DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH =
|
||||
SCROLLBAR_WIDTH_ACTUAL + DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH;
|
||||
|
||||
const DIALOG_HEADER_PADDING_BLOCK = 10;
|
||||
|
||||
const DIALOG_HEADER_ICON_BUTTON_MARGIN =
|
||||
DIALOG_HEADER_PADDING_BLOCK - DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH;
|
||||
// If this element doesn't have scrollbars, we need to add the exact value of
|
||||
// the actual scrollbar gutter
|
||||
return scrollbarWidthActual + paddingBeforeScrollbarWidth;
|
||||
}
|
||||
|
||||
export namespace AxoDialog {
|
||||
/**
|
||||
@@ -86,31 +86,44 @@ export namespace AxoDialog {
|
||||
* ------------------------------
|
||||
*/
|
||||
|
||||
export type ContentSize = AxoBaseDialog.ContentSize;
|
||||
type ContentSizeConfig = Readonly<{
|
||||
width: number;
|
||||
minWidth: number;
|
||||
}>;
|
||||
|
||||
const ContentSizes: Record<ContentSize, ContentSizeConfig> = {
|
||||
sm: { width: 360, minWidth: 360 },
|
||||
md: { width: 420, minWidth: 360 },
|
||||
lg: { width: 720, minWidth: 360 },
|
||||
};
|
||||
|
||||
export type ContentSize = 'sm' | 'md' | 'lg';
|
||||
export type ContentEscape = AxoBaseDialog.ContentEscape;
|
||||
export type ContentProps = AxoBaseDialog.ContentProps;
|
||||
export type ContentProps = Readonly<{
|
||||
size: ContentSize;
|
||||
escape: ContentEscape;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const Content: FC<ContentProps> = memo(props => {
|
||||
const sizeConfig = AxoBaseDialog.ContentSizes[props.size];
|
||||
const sizeConfig = ContentSizes[props.size];
|
||||
const handleContentEscapeEvent = useContentEscapeBehavior(props.escape);
|
||||
return (
|
||||
<AxoBaseDialog.ContentSizeProvider value={props.size}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className={AxoBaseDialog.overlayStyles}>
|
||||
<Dialog.Content
|
||||
className={AxoBaseDialog.contentStyles}
|
||||
onEscapeKeyDown={handleContentEscapeEvent}
|
||||
onInteractOutside={handleContentEscapeEvent}
|
||||
style={{
|
||||
width: sizeConfig.width,
|
||||
minWidth: sizeConfig.minWidth,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Dialog.Content>
|
||||
</Dialog.Overlay>
|
||||
</Dialog.Portal>
|
||||
</AxoBaseDialog.ContentSizeProvider>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className={AxoBaseDialog.overlayStyles}>
|
||||
<Dialog.Content
|
||||
className={AxoBaseDialog.contentStyles}
|
||||
onEscapeKeyDown={handleContentEscapeEvent}
|
||||
onInteractOutside={handleContentEscapeEvent}
|
||||
style={{
|
||||
width: sizeConfig.width,
|
||||
minWidth: 320,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Dialog.Content>
|
||||
</Dialog.Overlay>
|
||||
</Dialog.Portal>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -128,14 +141,13 @@ export namespace AxoDialog {
|
||||
export const Header: FC<HeaderProps> = memo(props => {
|
||||
const style = useMemo(() => {
|
||||
return {
|
||||
paddingBlock: DIALOG_HEADER_PADDING_BLOCK,
|
||||
paddingInline: DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH,
|
||||
paddingInline: getPadding(10, false),
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
className={tw(
|
||||
'grid items-center',
|
||||
'grid items-center py-2.5',
|
||||
'grid-cols-[[back-slot]_1fr_[title-slot]_auto_[close-slot]_1fr]'
|
||||
)}
|
||||
style={style}
|
||||
@@ -167,12 +179,12 @@ export namespace AxoDialog {
|
||||
type="button"
|
||||
aria-label={label}
|
||||
className={tw(
|
||||
'rounded-full p-1.5',
|
||||
'rounded-full p-[5px] leading-none',
|
||||
'hovered:bg-fill-secondary pressed:bg-fill-secondary-pressed',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]'
|
||||
)}
|
||||
>
|
||||
<AxoSymbol.Icon symbol={symbol} size={20} label={null} />
|
||||
<AxoSymbol.Icon symbol={symbol} size={18} label={null} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -186,17 +198,25 @@ export namespace AxoDialog {
|
||||
*/
|
||||
|
||||
export type TitleProps = Readonly<{
|
||||
screenReaderOnly?: boolean;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const Title: FC<TitleProps> = memo(props => {
|
||||
const style = useMemo(() => {
|
||||
return {
|
||||
paddingInline: 24 - getPadding(10, false),
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<Dialog.Title
|
||||
className={tw(
|
||||
'col-[title-slot]',
|
||||
'col-[title-slot] py-0.5',
|
||||
'truncate text-center',
|
||||
'type-title-small text-label-primary'
|
||||
'type-body-medium font-semibold text-label-primary',
|
||||
props.screenReaderOnly && 'sr-only'
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{props.children}
|
||||
</Dialog.Title>
|
||||
@@ -215,11 +235,8 @@ export namespace AxoDialog {
|
||||
}>;
|
||||
|
||||
export const Back: FC<BackProps> = memo(props => {
|
||||
const style = useMemo((): CSSProperties => {
|
||||
return { marginInlineStart: DIALOG_HEADER_ICON_BUTTON_MARGIN };
|
||||
}, []);
|
||||
return (
|
||||
<div className={tw('col-[back-slot] text-start')} style={style}>
|
||||
<div className={tw('col-[back-slot] text-start')}>
|
||||
<HeaderIconButton
|
||||
label={props['aria-label']}
|
||||
symbol="chevron-[start]"
|
||||
@@ -240,11 +257,8 @@ export namespace AxoDialog {
|
||||
}>;
|
||||
|
||||
export const Close: FC<CloseProps> = memo(props => {
|
||||
const style = useMemo((): CSSProperties => {
|
||||
return { marginInlineEnd: DIALOG_HEADER_ICON_BUTTON_MARGIN };
|
||||
}, []);
|
||||
return (
|
||||
<div className={tw('col-[close-slot] text-end')} style={style}>
|
||||
<div className={tw('col-[close-slot] text-end')}>
|
||||
<Dialog.Close asChild>
|
||||
<HeaderIconButton label={props['aria-label']} symbol="x" />
|
||||
</Dialog.Close>
|
||||
@@ -254,6 +268,23 @@ export namespace AxoDialog {
|
||||
|
||||
Close.displayName = `${Namespace}.Close`;
|
||||
|
||||
export type ExperimentalSearchProps = Readonly<{
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
export const ExperimentalSearch: FC<ExperimentalSearchProps> = memo(props => {
|
||||
const style = useMemo(() => {
|
||||
return { paddingInline: getPadding(16, false) };
|
||||
}, []);
|
||||
return (
|
||||
<div style={style} className={tw('pb-2')}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ExperimentalSearch.displayName = `${Namespace}.ExperimentalSearch`;
|
||||
|
||||
/**
|
||||
* Component: <AxoDialog.Body>
|
||||
* ---------------------------
|
||||
@@ -268,22 +299,18 @@ export namespace AxoDialog {
|
||||
|
||||
export const Body: FC<BodyProps> = memo(props => {
|
||||
const { padding = 'normal' } = props;
|
||||
const contentSize = useContentSize();
|
||||
const contentSizeConfig = AxoBaseDialog.ContentSizes[contentSize];
|
||||
|
||||
const style = useMemo((): CSSProperties => {
|
||||
return {
|
||||
paddingInline:
|
||||
padding === 'normal'
|
||||
? DIALOG_PADDING_BEFORE_SCROLLBAR_WIDTH
|
||||
: undefined,
|
||||
paddingInline: padding === 'normal' ? getPadding(24, true) : undefined,
|
||||
};
|
||||
}, [padding]);
|
||||
|
||||
return (
|
||||
<AxoScrollArea.Root
|
||||
maxHeight={contentSizeConfig.maxBodyHeight}
|
||||
maxHeight={440}
|
||||
scrollbarWidth="thin"
|
||||
scrollbarVisibility="as-needed"
|
||||
>
|
||||
<AxoScrollArea.Hint edge="top" />
|
||||
<AxoScrollArea.Hint edge="bottom" />
|
||||
@@ -325,13 +352,13 @@ export namespace AxoDialog {
|
||||
export const Footer: FC<FooterProps> = memo(props => {
|
||||
const style = useMemo((): CSSProperties => {
|
||||
return {
|
||||
paddingInline: DIALOG_PADDING_PLUS_SCROLLBAR_WIDTH,
|
||||
paddingInline: getPadding(12, false),
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tw('flex flex-wrap items-center gap-3 py-3')}
|
||||
className={tw('flex flex-wrap items-center gap-3 py-2.5')}
|
||||
style={style}
|
||||
>
|
||||
{props.children}
|
||||
@@ -351,6 +378,9 @@ export namespace AxoDialog {
|
||||
}>;
|
||||
|
||||
export const FooterContent: FC<FooterContentProps> = memo(props => {
|
||||
const style = useMemo(() => {
|
||||
return { paddingInlineStart: 24 - getPadding(12, false) };
|
||||
}, []);
|
||||
return (
|
||||
<div
|
||||
className={tw(
|
||||
@@ -364,6 +394,7 @@ export namespace AxoDialog {
|
||||
'flex-grow',
|
||||
'type-body-large text-label-primary'
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
@@ -422,7 +453,7 @@ export namespace AxoDialog {
|
||||
variant={props.variant}
|
||||
symbol={props.symbol}
|
||||
arrow={props.arrow}
|
||||
size="medium"
|
||||
size="md"
|
||||
width="grow"
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function Basic(): JSX.Element {
|
||||
<Container>
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoButton.Root variant="secondary" size="medium">
|
||||
<AxoButton.Root variant="secondary" size="md">
|
||||
Open Dropdown Menu
|
||||
</AxoButton.Root>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
@@ -114,7 +114,7 @@ export function WithHeader(): JSX.Element {
|
||||
<Container>
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoButton.Root variant="secondary" size="medium">
|
||||
<AxoButton.Root variant="secondary" size="md">
|
||||
Open Dropdown Menu
|
||||
</AxoButton.Root>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
@@ -222,7 +222,7 @@ export function StressTestLongText(): JSX.Element {
|
||||
<Container>
|
||||
<AxoDropdownMenu.Root>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoButton.Root variant="secondary" size="medium">
|
||||
<AxoButton.Root variant="secondary" size="md">
|
||||
Open Dropdown Menu
|
||||
</AxoButton.Root>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useId } from 'react';
|
||||
import React, { memo, useEffect, useId, useRef } from 'react';
|
||||
import { DropdownMenu } from 'radix-ui';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { getRole, computeAccessibleName } from 'dom-accessibility-api';
|
||||
import { AxoSymbol } from './AxoSymbol.dom.js';
|
||||
import { AxoBaseMenu } from './_internal/AxoBaseMenu.dom.js';
|
||||
import { tw } from './tw.dom.js';
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
useAriaLabellingContext,
|
||||
useCreateAriaLabellingContext,
|
||||
} from './_internal/AriaLabellingContext.dom.js';
|
||||
import { assert } from './_internal/assert.dom.js';
|
||||
|
||||
const Namespace = 'AxoDropdownMenu';
|
||||
|
||||
@@ -88,18 +90,41 @@ export namespace AxoDropdownMenu {
|
||||
|
||||
export type TriggerProps = AxoBaseMenu.MenuTriggerProps;
|
||||
|
||||
const triggerDisplayName = `${Namespace}.Trigger`;
|
||||
|
||||
/**
|
||||
* The button that toggles the dropdown menu.
|
||||
* By default, the {@link AxoDropdownMenu.Content} will position itself
|
||||
* against the trigger.
|
||||
*/
|
||||
export const Trigger: FC<TriggerProps> = memo(props => {
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
assert(
|
||||
ref.current instanceof HTMLElement,
|
||||
`${triggerDisplayName} child must forward ref`
|
||||
);
|
||||
assert(
|
||||
getRole(ref.current) === 'button',
|
||||
`${triggerDisplayName} child must be a <button> or role=button`
|
||||
);
|
||||
assert(
|
||||
computeAccessibleName(ref.current) !== '',
|
||||
`${triggerDisplayName} child must have an accessible name`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownMenu.Trigger asChild>{props.children}</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Trigger ref={ref} asChild>
|
||||
{props.children}
|
||||
</DropdownMenu.Trigger>
|
||||
);
|
||||
});
|
||||
|
||||
Trigger.displayName = `${Namespace}.Trigger`;
|
||||
Trigger.displayName = triggerDisplayName;
|
||||
|
||||
/**
|
||||
* Component: <AxoDropdownMenu.Content>
|
||||
|
||||
@@ -100,7 +100,7 @@ export function Basic(): JSX.Element {
|
||||
return (
|
||||
<div className={tw('flex h-96 w-full items-center justify-center gap-8')}>
|
||||
<Template renderer="AxoDropdownMenu">
|
||||
<AxoButton.Root variant="secondary" size="medium">
|
||||
<AxoButton.Root variant="secondary" size="md">
|
||||
Open Dropdown Menu
|
||||
</AxoButton.Root>
|
||||
</Template>
|
||||
|
||||
@@ -66,9 +66,12 @@ export namespace AxoScrollArea {
|
||||
|
||||
export type ScrollBehavior = 'auto' | 'smooth';
|
||||
|
||||
export type ScrollbarVisibility = 'auto' | 'as-needed';
|
||||
|
||||
type ScrollAreaConfig = Readonly<{
|
||||
scrollbarWidth: ScrollbarWidth;
|
||||
scrollbarGutter: ScrollbarGutter;
|
||||
scrollbarVisibility: ScrollbarVisibility;
|
||||
scrollBehavior: ScrollBehavior;
|
||||
}>;
|
||||
|
||||
@@ -93,6 +96,7 @@ export namespace AxoScrollArea {
|
||||
maxHeight?: number;
|
||||
scrollbarWidth: ScrollbarWidth;
|
||||
scrollbarGutter?: ScrollbarGutter;
|
||||
scrollbarVisibility?: ScrollbarVisibility;
|
||||
scrollBehavior?: ScrollBehavior;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
@@ -104,12 +108,18 @@ export namespace AxoScrollArea {
|
||||
maxHeight,
|
||||
scrollbarWidth = 'thin',
|
||||
scrollbarGutter = 'stable-both-edges',
|
||||
scrollbarVisibility = 'auto',
|
||||
scrollBehavior = 'auto',
|
||||
} = props;
|
||||
|
||||
const config = useMemo((): ScrollAreaConfig => {
|
||||
return { scrollbarWidth, scrollbarGutter, scrollBehavior };
|
||||
}, [scrollbarWidth, scrollbarGutter, scrollBehavior]);
|
||||
return {
|
||||
scrollbarWidth,
|
||||
scrollbarGutter,
|
||||
scrollbarVisibility,
|
||||
scrollBehavior,
|
||||
};
|
||||
}, [scrollbarWidth, scrollbarGutter, scrollbarVisibility, scrollBehavior]);
|
||||
|
||||
const style = useMemo((): CSSProperties => {
|
||||
return {
|
||||
@@ -158,13 +168,6 @@ export namespace AxoScrollArea {
|
||||
'outline-0'
|
||||
);
|
||||
|
||||
// Note: Use "scroll" for `overflow-x` because scrollbar-gutter doesnt fix the space
|
||||
const ViewportOrientations: Record<Orientation, TailwindStyles> = {
|
||||
vertical: tw('overflow-x-hidden overflow-y-auto'),
|
||||
horizontal: tw('overflow-x-scroll overflow-y-hidden'),
|
||||
both: tw('overflow-x-scroll overflow-y-auto'),
|
||||
};
|
||||
|
||||
const ViewportScrollbarWidths: Record<ScrollbarWidth, TailwindStyles> = {
|
||||
wide: tw('scrollbar-width-auto'),
|
||||
thin: tw('scrollbar-width-thin'),
|
||||
@@ -177,6 +180,16 @@ export namespace AxoScrollArea {
|
||||
'stable-both-edges': tw('scrollbar-gutter-stable'),
|
||||
};
|
||||
|
||||
const ViewportScrollbarVisibilities: Record<
|
||||
ScrollbarVisibility,
|
||||
TailwindStyles
|
||||
> = {
|
||||
auto: tw(),
|
||||
'as-needed': tw(
|
||||
'transition-[scrollbar-color] duration-150 not-hover:not-focus-within:scrollbar-thumb-transparent'
|
||||
),
|
||||
};
|
||||
|
||||
const ViewportScrollBehaviors: Record<ScrollBehavior, TailwindStyles> = {
|
||||
auto: tw('scroll-auto'),
|
||||
smooth: tw('scroll-smooth'),
|
||||
@@ -188,8 +201,12 @@ export namespace AxoScrollArea {
|
||||
|
||||
export const Viewport: FC<ViewportProps> = memo(props => {
|
||||
const orientation = useAxoScrollAreaOrientation();
|
||||
const { scrollbarWidth, scrollbarGutter, scrollBehavior } =
|
||||
useAxoScrollAreaConfig();
|
||||
const {
|
||||
scrollbarWidth,
|
||||
scrollbarGutter,
|
||||
scrollbarVisibility,
|
||||
scrollBehavior,
|
||||
} = useAxoScrollAreaConfig();
|
||||
|
||||
const style = useMemo((): CSSProperties => {
|
||||
const hasVerticalScrollbar = orientation !== 'horizontal';
|
||||
@@ -239,9 +256,9 @@ export namespace AxoScrollArea {
|
||||
data-axo-scroll-area-viewport
|
||||
className={tw(
|
||||
baseViewportStyles,
|
||||
ViewportOrientations[orientation],
|
||||
ViewportScrollbarWidths[scrollbarWidth],
|
||||
ViewportScrollbarGutters[scrollbarGutter],
|
||||
ViewportScrollbarVisibilities[scrollbarVisibility],
|
||||
ViewportScrollBehaviors[scrollBehavior]
|
||||
)}
|
||||
style={style}
|
||||
@@ -452,7 +469,10 @@ export namespace AxoScrollArea {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tw('flex size-full flex-col', AXO_MASK_CLASS_NAME)}
|
||||
className={tw(
|
||||
'flex size-full flex-col overflow-hidden',
|
||||
AXO_MASK_CLASS_NAME
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -84,7 +84,7 @@ export namespace AxoSelect {
|
||||
*/
|
||||
|
||||
export type TriggerVariant = 'default' | 'floating' | 'borderless';
|
||||
export type TriggerWidth = 'hug' | 'full';
|
||||
export type TriggerWidth = 'fit' | 'full';
|
||||
export type TriggerChevron = 'always' | 'on-hover';
|
||||
|
||||
const baseTriggerStyles = tw(
|
||||
@@ -116,7 +116,7 @@ export namespace AxoSelect {
|
||||
};
|
||||
|
||||
const TriggerWidths: Record<TriggerWidth, TailwindStyles> = {
|
||||
hug: tw(),
|
||||
fit: tw(),
|
||||
full: tw('w-full'),
|
||||
};
|
||||
|
||||
@@ -170,7 +170,7 @@ export namespace AxoSelect {
|
||||
*/
|
||||
export const Trigger: FC<TriggerProps> = memo(props => {
|
||||
const variant = props.variant ?? 'default';
|
||||
const width = props.width ?? 'hug';
|
||||
const width = props.width ?? 'fit';
|
||||
const chevron = props.chevron ?? 'always';
|
||||
const variantStyles = TriggerVariants[variant];
|
||||
const widthStyles = TriggerWidths[width];
|
||||
|
||||
@@ -65,14 +65,15 @@ export namespace AxoSymbol {
|
||||
*/
|
||||
|
||||
export type IconName = AxoSymbolIconName;
|
||||
export type IconSize = 12 | 14 | 16 | 20 | 24 | 48;
|
||||
export type IconSize = 12 | 14 | 16 | 18 | 20 | 24 | 48;
|
||||
|
||||
type IconSizeConfig = { size: number; fontSize: number };
|
||||
|
||||
const IconSizes: Record<IconSize, IconSizeConfig> = {
|
||||
12: { size: 12, fontSize: 11 },
|
||||
12: { size: 12, fontSize: 10 },
|
||||
14: { size: 14, fontSize: 12 },
|
||||
16: { size: 16, fontSize: 14 },
|
||||
18: { size: 18, fontSize: 16 },
|
||||
20: { size: 20, fontSize: 18 },
|
||||
24: { size: 24, fontSize: 22 },
|
||||
48: { size: 48, fontSize: 44 },
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createContext, useCallback, useContext } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { tw } from '../tw.dom.js';
|
||||
import { assert } from './assert.dom.js';
|
||||
|
||||
export namespace AxoBaseDialog {
|
||||
/**
|
||||
@@ -46,6 +45,7 @@ export namespace AxoBaseDialog {
|
||||
*/
|
||||
|
||||
export const contentStyles = tw(
|
||||
'relative',
|
||||
'max-h-full min-h-fit max-w-full min-w-fit',
|
||||
'rounded-3xl bg-elevated-background-primary shadow-elevation-3 select-none',
|
||||
'outline-0 outline-border-focused focused:outline-[2.5px]',
|
||||
@@ -53,21 +53,6 @@ export namespace AxoBaseDialog {
|
||||
'animate-scale-98 animate-translate-y-1'
|
||||
);
|
||||
|
||||
export type ContentSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
export type ContentSizeConfig = Readonly<{
|
||||
width: number;
|
||||
minWidth: number;
|
||||
maxBodyHeight: number;
|
||||
}>;
|
||||
|
||||
// TODO: These sizes are not finalized
|
||||
export const ContentSizes: Record<ContentSize, ContentSizeConfig> = {
|
||||
sm: { width: 320, minWidth: 320, maxBodyHeight: 440 },
|
||||
md: { width: 440, minWidth: 320, maxBodyHeight: 440 },
|
||||
lg: { width: 560, minWidth: 440, maxBodyHeight: 440 },
|
||||
};
|
||||
|
||||
export type ContentEscape = 'cancel-is-noop' | 'cancel-is-destructive';
|
||||
|
||||
export function useContentEscapeBehavior(
|
||||
@@ -82,21 +67,4 @@ export namespace AxoBaseDialog {
|
||||
[escape]
|
||||
);
|
||||
}
|
||||
|
||||
export type ContentProps = Readonly<{
|
||||
escape: ContentEscape;
|
||||
size: ContentSize;
|
||||
children: ReactNode;
|
||||
}>;
|
||||
|
||||
const ContentSizeContext = createContext<ContentSize | null>(null);
|
||||
|
||||
export const ContentSizeProvider = ContentSizeContext.Provider;
|
||||
|
||||
export function useContentSize(): ContentSize {
|
||||
return assert(
|
||||
useContext(ContentSizeContext),
|
||||
'Must be wrapped with dialog <Content> component'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user