@ahrowe/ui 0.49.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +4 -1
- package/dist/cjs-types/common/chatComposer/chatComposer.d.ts +4 -0
- package/dist/cjs-types/common/chatComposer/chatComposer.types.d.ts +40 -0
- package/dist/cjs-types/common/chatComposer/index.d.ts +2 -0
- package/dist/cjs-types/common/chatMessage/chatMessage.d.ts +4 -0
- package/dist/cjs-types/common/chatMessage/chatMessage.types.d.ts +60 -0
- package/dist/cjs-types/common/chatMessage/index.d.ts +2 -0
- package/dist/cjs-types/common/chatMessageList/chatMessageList.d.ts +4 -0
- package/dist/cjs-types/common/chatMessageList/chatMessageList.types.d.ts +27 -0
- package/dist/cjs-types/common/chatMessageList/index.d.ts +2 -0
- package/dist/cjs-types/common/configProvider/configProvider.types.d.ts +6 -0
- package/dist/cjs-types/common/dropdown/dropdown.types.d.ts +3 -2
- package/dist/cjs-types/common/hooks/useAutoGrow.d.ts +12 -0
- package/dist/cjs-types/common/infiniteBlock/useInfiniteScroll.d.ts +1 -1
- package/dist/cjs-types/common/tabHeader/tabHeader.types.d.ts +5 -3
- package/dist/cjs-types/index.d.ts +6 -0
- package/dist/esm/common/chatComposer/chatComposer.mjs +2 -0
- package/dist/esm/common/chatComposer/chatComposer.mjs.map +1 -0
- package/dist/esm/common/chatComposer/chatComposer.module.mjs +2 -0
- package/dist/esm/common/chatComposer/chatComposer.module.mjs.map +1 -0
- package/dist/esm/common/chatMessage/chatMessage.mjs +2 -0
- package/dist/esm/common/chatMessage/chatMessage.mjs.map +1 -0
- package/dist/esm/common/chatMessage/chatMessage.module.mjs +2 -0
- package/dist/esm/common/chatMessage/chatMessage.module.mjs.map +1 -0
- package/dist/esm/common/chatMessage/chatMessage.types.mjs +2 -0
- package/dist/esm/common/chatMessage/chatMessage.types.mjs.map +1 -0
- package/dist/esm/common/chatMessageList/chatMessageList.mjs +2 -0
- package/dist/esm/common/chatMessageList/chatMessageList.mjs.map +1 -0
- package/dist/esm/common/chatMessageList/chatMessageList.module.mjs +2 -0
- package/dist/esm/common/chatMessageList/chatMessageList.module.mjs.map +1 -0
- package/dist/esm/common/dropdown/dropdown.mjs +1 -1
- package/dist/esm/common/dropdown/dropdown.mjs.map +1 -1
- package/dist/esm/common/dropdown/dropdown.module.mjs +1 -1
- package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
- package/dist/esm/common/hooks/useAutoGrow.mjs +2 -0
- package/dist/esm/common/hooks/useAutoGrow.mjs.map +1 -0
- package/dist/esm/common/infiniteBlock/useInfiniteScroll.mjs +1 -1
- package/dist/esm/common/infiniteBlock/useInfiniteScroll.mjs.map +1 -1
- package/dist/esm/common/input/input.mjs +1 -1
- package/dist/esm/common/input/input.mjs.map +1 -1
- package/dist/esm/common/loader/loader.module.mjs.map +1 -1
- package/dist/esm/common/tabHeader/tabHeader.mjs +1 -1
- package/dist/esm/common/tabHeader/tabHeader.mjs.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/common/chatComposer/chatComposer.d.ts +4 -0
- package/dist/types/common/chatComposer/chatComposer.types.d.ts +40 -0
- package/dist/types/common/chatComposer/index.d.ts +2 -0
- package/dist/types/common/chatMessage/chatMessage.d.ts +4 -0
- package/dist/types/common/chatMessage/chatMessage.types.d.ts +60 -0
- package/dist/types/common/chatMessage/index.d.ts +2 -0
- package/dist/types/common/chatMessageList/chatMessageList.d.ts +4 -0
- package/dist/types/common/chatMessageList/chatMessageList.types.d.ts +27 -0
- package/dist/types/common/chatMessageList/index.d.ts +2 -0
- package/dist/types/common/configProvider/configProvider.types.d.ts +6 -0
- package/dist/types/common/dropdown/dropdown.types.d.ts +3 -2
- package/dist/types/common/hooks/useAutoGrow.d.ts +12 -0
- package/dist/types/common/infiniteBlock/useInfiniteScroll.d.ts +1 -1
- package/dist/types/common/tabHeader/tabHeader.types.d.ts +5 -3
- package/dist/types/index.d.ts +6 -0
- package/docs/CLAUDE.md +6 -3
- package/docs/ChatComposer.md +100 -0
- package/docs/ChatMessage.md +88 -0
- package/docs/ChatMessageList.md +113 -0
- package/docs/ConfigProvider.md +1 -0
- package/docs/Dropdown.md +17 -3
- package/docs/TabHeader.md +19 -8
- package/package.json +1 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode, Ref, TextareaHTMLAttributes } from 'react';
|
|
2
|
+
import { SlotClassNames, SlotStyles, DataProps } from '../types/slots.types.js';
|
|
3
|
+
export type ChatComposerSlots = 'input' | 'button';
|
|
4
|
+
/** Text ChatComposer renders of its own, for translating it. */
|
|
5
|
+
export interface ChatComposerLabels {
|
|
6
|
+
/** Shown in the empty field, and its accessible name. */
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
/** Send button, read out by screen readers. */
|
|
9
|
+
send?: string;
|
|
10
|
+
/** Stop button shown while `onStop` is set, read out by screen readers. */
|
|
11
|
+
stop?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Native attributes go to the `<textarea>`; `className` and `style` to the root. */
|
|
14
|
+
export interface ChatComposerProps extends DataProps, Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange' | 'value' | 'defaultValue' | 'placeholder'> {
|
|
15
|
+
/** Called with the trimmed text. Not called while the field is empty or only whitespace. */
|
|
16
|
+
onSend: (text: string) => void;
|
|
17
|
+
/** Controlled value. Sending clears it through `onChange('')`. */
|
|
18
|
+
value?: string;
|
|
19
|
+
defaultValue?: string;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
/** Runs before the composer's own handling; `preventDefault()` on an Enter keeps it from sending. */
|
|
22
|
+
onKeyDown?: TextareaHTMLAttributes<HTMLTextAreaElement>['onKeyDown'];
|
|
23
|
+
/** Swaps the send button for a stop button while set, e.g. while an AI answer streams. Enter sends nothing meanwhile. */
|
|
24
|
+
onStop?: () => void;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
/** The height of the empty field, in lines. It grows from there to seven. Default 1. */
|
|
27
|
+
rows?: number;
|
|
28
|
+
/** Before the field: an attach or emoji button. Moves to a row below it once the text wraps. */
|
|
29
|
+
startContent?: ReactNode;
|
|
30
|
+
/** Between the field and the send button. Moves to a row below it, with the send button, once the text wraps. */
|
|
31
|
+
endContent?: ReactNode;
|
|
32
|
+
/** Ref to the inner `<textarea>`. */
|
|
33
|
+
ref?: Ref<HTMLTextAreaElement>;
|
|
34
|
+
/** Overrides the text ChatComposer renders of its own. Merged per key. */
|
|
35
|
+
labels?: ChatComposerLabels;
|
|
36
|
+
className?: string;
|
|
37
|
+
style?: CSSProperties;
|
|
38
|
+
classNames?: SlotClassNames<ChatComposerSlots>;
|
|
39
|
+
styles?: SlotStyles<ChatComposerSlots>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types.js';
|
|
3
|
+
import { ColorValue } from '../themeProvider/color.types.js';
|
|
4
|
+
/** How much the message stands out. Independent of its colour. */
|
|
5
|
+
export declare enum ChatMessageStyleType {
|
|
6
|
+
/** Solid bubble, text in a contrasting colour. The default for `isOwn`. */
|
|
7
|
+
Filled = "filled",
|
|
8
|
+
/** A light tint of the colour. The default for everyone else. */
|
|
9
|
+
Light = "light",
|
|
10
|
+
/** No bubble, full width: an AI assistant's answer. */
|
|
11
|
+
Plain = "plain"
|
|
12
|
+
}
|
|
13
|
+
/** Delivery state of an own message: an icon next to `time`, and `Failed` a notice under the bubble. */
|
|
14
|
+
export declare enum ChatMessageStatus {
|
|
15
|
+
Sending = "sending",
|
|
16
|
+
Sent = "sent",
|
|
17
|
+
Delivered = "delivered",
|
|
18
|
+
Read = "read",
|
|
19
|
+
Failed = "failed"
|
|
20
|
+
}
|
|
21
|
+
export type ChatMessageSlots = 'avatar' | 'body' | 'author' | 'bubble' | 'meta' | 'actions';
|
|
22
|
+
/** Text ChatMessage renders of its own, for translating it. */
|
|
23
|
+
export interface ChatMessageLabels {
|
|
24
|
+
/** Read out for the status icon. */
|
|
25
|
+
sending?: string;
|
|
26
|
+
sent?: string;
|
|
27
|
+
delivered?: string;
|
|
28
|
+
read?: string;
|
|
29
|
+
/** Shown under the bubble. */
|
|
30
|
+
failed?: string;
|
|
31
|
+
/** Names an own message without `author` for screen readers. */
|
|
32
|
+
you?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface ChatMessageProps extends HtmlProps<HTMLDivElement> {
|
|
35
|
+
/** The message. A string keeps its line breaks; anything else (rendered Markdown, an image) renders as given. */
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
/** Sent by the current user: aligned to the end, `Filled` in `Primary` by default. */
|
|
38
|
+
isOwn?: boolean;
|
|
39
|
+
/** Follows a message from the same author: hides `author` from view, keeps the space of `avatar` and tightens the gap above. */
|
|
40
|
+
isContinued?: boolean;
|
|
41
|
+
/** Shown beside the bubble, typically an `Avatar`. */
|
|
42
|
+
avatar?: ReactNode;
|
|
43
|
+
/** Name above the bubble. Screen readers get it on every message of a run, not only the first. */
|
|
44
|
+
author?: ReactNode;
|
|
45
|
+
/** Shown in the bubble's corner. Format it yourself, e.g. with `Intl.DateTimeFormat`. */
|
|
46
|
+
time?: ReactNode;
|
|
47
|
+
status?: ChatMessageStatus;
|
|
48
|
+
/** Shown below the bubble, after the `Failed` notice: copy, retry, rate. */
|
|
49
|
+
actions?: ReactNode;
|
|
50
|
+
/** Default `Filled` when `isOwn`, otherwise `Light`. */
|
|
51
|
+
styleType?: ChatMessageStyleType;
|
|
52
|
+
/** A `ThemeColor` or any CSS colour. Default `Primary` when `isOwn`, otherwise `Neutral`. */
|
|
53
|
+
color?: ColorValue;
|
|
54
|
+
/** Overrides the text ChatMessage renders of its own. Merged per key. */
|
|
55
|
+
labels?: ChatMessageLabels;
|
|
56
|
+
className?: string;
|
|
57
|
+
style?: CSSProperties;
|
|
58
|
+
classNames?: SlotClassNames<ChatMessageSlots>;
|
|
59
|
+
styles?: SlotStyles<ChatMessageSlots>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode, Ref } from 'react';
|
|
2
|
+
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types.js';
|
|
3
|
+
export type ChatMessageListSlots = 'viewport' | 'content' | 'scrollButton';
|
|
4
|
+
/** Text ChatMessageList renders of its own, for translating it. */
|
|
5
|
+
export interface ChatMessageListLabels {
|
|
6
|
+
/** Accessible name of the message log. */
|
|
7
|
+
messages?: string;
|
|
8
|
+
/** Button that jumps back to the newest message, read out by screen readers. */
|
|
9
|
+
scrollToBottom?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ChatMessageListHandle {
|
|
12
|
+
/** Scrolls to the newest message and follows new ones again. Call it after the user sends one. */
|
|
13
|
+
scrollToBottom: () => void;
|
|
14
|
+
}
|
|
15
|
+
export interface ChatMessageListProps extends HtmlProps<HTMLDivElement> {
|
|
16
|
+
/** `ChatMessage`s, and anything between them: a `Divider` with a date, a typing indicator. Oldest first. */
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
/** Loads older messages as the user nears the top. Prepend them; the view stays where it was. Leave it out once there are no more. */
|
|
19
|
+
onLoadMore?: () => Promise<void>;
|
|
20
|
+
ref?: Ref<ChatMessageListHandle>;
|
|
21
|
+
/** Overrides the text ChatMessageList renders of its own. Merged per key. */
|
|
22
|
+
labels?: ChatMessageListLabels;
|
|
23
|
+
className?: string;
|
|
24
|
+
style?: CSSProperties;
|
|
25
|
+
classNames?: SlotClassNames<ChatMessageListSlots>;
|
|
26
|
+
styles?: SlotStyles<ChatMessageListSlots>;
|
|
27
|
+
}
|
|
@@ -49,6 +49,9 @@ import { RoomViewerProps } from '../roomViewer/roomViewer.types.js';
|
|
|
49
49
|
import { PopoverProps } from '../popover/popover.types.js';
|
|
50
50
|
import { ModalProps } from '../modal/modal.types.js';
|
|
51
51
|
import { StepperProps } from '../stepper/stepper.types.js';
|
|
52
|
+
import { ChatMessageProps } from '../chatMessage/chatMessage.types.js';
|
|
53
|
+
import { ChatMessageListProps } from '../chatMessageList/chatMessageList.types.js';
|
|
54
|
+
import { ChatComposerProps } from '../chatComposer/chatComposer.types.js';
|
|
52
55
|
import { Presentation } from '../configProvider/presentation.types.js';
|
|
53
56
|
/**
|
|
54
57
|
* Global default props keyed by component name.
|
|
@@ -133,6 +136,9 @@ export interface ComponentDefaults {
|
|
|
133
136
|
CommandPalette?: Partial<CommandPaletteProps>;
|
|
134
137
|
WeatherIcon?: Partial<WeatherIconProps>;
|
|
135
138
|
Stepper?: Partial<StepperProps>;
|
|
139
|
+
ChatMessage?: Partial<ChatMessageProps>;
|
|
140
|
+
ChatMessageList?: Partial<ChatMessageListProps>;
|
|
141
|
+
ChatComposer?: Partial<ChatComposerProps>;
|
|
136
142
|
}
|
|
137
143
|
export interface ConfigProviderProps {
|
|
138
144
|
children: React.ReactNode;
|
|
@@ -8,7 +8,7 @@ export interface DropdownItem {
|
|
|
8
8
|
disabled?: boolean;
|
|
9
9
|
}
|
|
10
10
|
export type DropdownSlots = 'root' | 'label' | 'field' | 'control' | 'icon' | 'placeholder' | 'list' | 'option';
|
|
11
|
-
export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'onMouseEnter' | 'onMouseLeave'> {
|
|
11
|
+
export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'onMouseEnter' | 'onMouseLeave' | 'onScroll'> {
|
|
12
12
|
formValidator?: FormValidator | null;
|
|
13
13
|
onMouseEnter?: ((event: React.MouseEvent) => void) | null;
|
|
14
14
|
onMouseLeave?: ((event: React.MouseEvent) => void) | null;
|
|
@@ -27,7 +27,8 @@ export interface DropdownProps extends Omit<HtmlProps, 'onFocus' | 'onBlur' | 'o
|
|
|
27
27
|
isRow?: boolean;
|
|
28
28
|
disabled?: boolean;
|
|
29
29
|
readOnly?: boolean;
|
|
30
|
-
|
|
30
|
+
/** Loads more items as the end of the open list comes near. Pass `undefined` once there are no more. */
|
|
31
|
+
onLoadMore?: () => Promise<void>;
|
|
31
32
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
32
33
|
dropdownLabel?: string;
|
|
33
34
|
isDropdownListWider?: boolean;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
interface AutoGrowOptions {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
/** Whether the text needs more than the field's `rows`, after each change of `value` or width. */
|
|
5
|
+
onGrow?: (isGrown: boolean) => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Keeps a textarea as tall as its text, up to the `max-height` its stylesheet sets. It measures
|
|
9
|
+
* again when `value` changes, so the field must be controlled, and when its width changes.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useAutoGrow(ref: RefObject<HTMLElement | null>, value: unknown, { enabled, onGrow }?: AutoGrowOptions): void;
|
|
12
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
|
-
export declare function useInfiniteScroll(containerRef: React.RefObject<HTMLDivElement | null>,
|
|
2
|
+
export declare function useInfiniteScroll(containerRef: React.RefObject<HTMLDivElement | null>, onLoadMore: (() => Promise<void>) | undefined): {
|
|
3
3
|
isLoading: boolean;
|
|
4
4
|
sentinelRef: React.RefObject<HTMLDivElement | null>;
|
|
5
5
|
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
import { IconContent } from '../types/icon.types.js';
|
|
3
3
|
import { SlotClassNames, SlotStyles, HtmlProps } from '../types/slots.types.js';
|
|
4
|
-
export type TabHeaderSlots = 'root' | 'tab' | 'icon' | 'labels' | 'title' | 'subtitle';
|
|
4
|
+
export type TabHeaderSlots = 'root' | 'tab' | 'icon' | 'labels' | 'title' | 'subtitle' | 'endContent';
|
|
5
5
|
export interface TabHeaderTab {
|
|
6
6
|
id: string;
|
|
7
7
|
icon: IconContent;
|
|
8
|
-
title?:
|
|
9
|
-
subtitle?:
|
|
8
|
+
title?: React.ReactNode;
|
|
9
|
+
subtitle?: React.ReactNode;
|
|
10
|
+
/** After the labels. Stays visible when a narrow screen collapses the tab, so keep it small: a `Badge`, a dot. */
|
|
11
|
+
endContent?: React.ReactNode;
|
|
10
12
|
disabled?: boolean;
|
|
11
13
|
}
|
|
12
14
|
export interface TabHeaderProps extends Omit<HtmlProps, 'onChange'> {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export { default as Card } from './common/card/index.js';
|
|
|
29
29
|
export * from './common/card/index.js';
|
|
30
30
|
export { default as Carousel } from './common/carousel/index.js';
|
|
31
31
|
export * from './common/carousel/index.js';
|
|
32
|
+
export { default as ChatComposer } from './common/chatComposer/index.js';
|
|
33
|
+
export * from './common/chatComposer/index.js';
|
|
34
|
+
export { default as ChatMessage } from './common/chatMessage/index.js';
|
|
35
|
+
export * from './common/chatMessage/index.js';
|
|
36
|
+
export { default as ChatMessageList } from './common/chatMessageList/index.js';
|
|
37
|
+
export * from './common/chatMessageList/index.js';
|
|
32
38
|
export { default as Checkbox } from './common/checkbox/index.js';
|
|
33
39
|
export * from './common/checkbox/index.js';
|
|
34
40
|
export { default as Chip } from './common/chip/index.js';
|
package/docs/CLAUDE.md
CHANGED
|
@@ -41,9 +41,9 @@ enums and slot names. Do not infer props from a component's name.
|
|
|
41
41
|
`Light` for status and selection, `Outlined` for secondary actions, `Plain` for tertiary ones.
|
|
42
42
|
- **`icon` props** take a FontAwesome `IconDefinition` or any React element.
|
|
43
43
|
- **`ref`** is a plain prop, there is no `forwardRef`. It lands on the element you would reach for:
|
|
44
|
-
Input, Textarea, NumberInput and SearchInput hand out their inner `<input
|
|
45
|
-
Ripple, VirtualList, RoomDrawer and RoomViewer
|
|
46
|
-
exported next to the props.
|
|
44
|
+
Input, Textarea, NumberInput and SearchInput hand out their inner `<input>`, ChatComposer its
|
|
45
|
+
`<textarea>`. Timer, Flip, Tilt, Ripple, VirtualList, ChatMessageList, RoomDrawer and RoomViewer
|
|
46
|
+
expose an imperative handle instead; its type is exported next to the props.
|
|
47
47
|
- Every module is `'use client'`; nothing renders on the server. The exception is
|
|
48
48
|
`@ahrowe/ui/floor-plan`, the plan model (FloorPlan.md) without React, for server code that reads
|
|
49
49
|
or validates a plan. No right-to-left support.
|
|
@@ -76,6 +76,9 @@ enums and slot names. Do not infer props from a component's name.
|
|
|
76
76
|
| Calendar.md | Calendar, CalendarView | Showing and editing your own events over time, in month, week, day and agenda views: bookings, shifts, appointments |
|
|
77
77
|
| Card.md | Card, CardStyleType, CardHeader, CardMedia, CardContent, CardActions, ThemeColor | Surface container for grouping related content — list items, profile cards, dashboard tiles, product listings |
|
|
78
78
|
| Carousel.md | Carousel | A slide track for stepping through a set of items one at a time, with arrows, dots, and drag/swipe support, plus optional autoplay |
|
|
79
|
+
| ChatComposer.md | ChatComposer | The field a chat message is written in, with its send button: under a ChatMessageList, which shows a whole chat put together |
|
|
80
|
+
| ChatMessage.md | ChatMessage, ChatMessageStyleType, ChatMessageStatus | One message in a conversation: a messenger bubble, a support agent's reply, an AI assistant's answer |
|
|
81
|
+
| ChatMessageList.md | ChatMessageList | The scrolling conversation of a chat: a messenger, a support widget, an AI assistant |
|
|
79
82
|
| Checkbox.md | Checkbox | Boolean toggle input — settings, multi-select lists, accept terms, feature flags |
|
|
80
83
|
| Chip.md | Chip, ChipContainer, ChipStyleType, ThemeColor | Compact tag, label, or filter token — selected items, applied filters, category badges, user avatars with remove |
|
|
81
84
|
| ColorPicker.md | ColorPicker | Hex colour selection — theme editors, annotation tools, customisation panels |
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ChatComposer
|
|
2
|
+
|
|
3
|
+
**When to use:** The field a chat message is written in, with its send button: under a [ChatMessageList](ChatMessageList.md), which shows a whole chat put together. For multiline text in a form, with a label and validation, use [Textarea](Textarea.md).
|
|
4
|
+
|
|
5
|
+
**Keywords:** message input, chat input, prompt, compose, reply box, enter to send, submit, attachment
|
|
6
|
+
|
|
7
|
+
**Import:** `import { ChatComposer } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { ChatComposerProps, ChatComposerLabels } from '@ahrowe/ui'`
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { ActionIcon, ChatComposer } from '@ahrowe/ui';
|
|
12
|
+
import { faFaceSmile, faPaperclip } from '@fortawesome/free-solid-svg-icons';
|
|
13
|
+
|
|
14
|
+
// Uncontrolled: it clears itself after sending
|
|
15
|
+
<ChatComposer onSend={(text) => send(text)} />
|
|
16
|
+
|
|
17
|
+
// AI chat: a stop button replaces send while the answer streams
|
|
18
|
+
<ChatComposer
|
|
19
|
+
onSend={ask}
|
|
20
|
+
onStop={isStreaming ? stop : undefined}
|
|
21
|
+
labels={{ placeholder: 'Ask anything' }}
|
|
22
|
+
/>
|
|
23
|
+
|
|
24
|
+
// Buttons of your own around the field
|
|
25
|
+
<ChatComposer
|
|
26
|
+
onSend={send}
|
|
27
|
+
startContent={<ActionIcon icon={faPaperclip} aria-label='Attach a file' onClick={openFilePicker} />}
|
|
28
|
+
endContent={<ActionIcon icon={faFaceSmile} aria-label='Emoji' onClick={openEmojiPicker} />}
|
|
29
|
+
/>
|
|
30
|
+
|
|
31
|
+
// Controlled, e.g. to keep a draft per conversation: sending clears it through onChange('')
|
|
32
|
+
<ChatComposer value={draft} onChange={setDraft} onSend={send} />
|
|
33
|
+
|
|
34
|
+
// Native attributes go to the textarea; an Enter your onKeyDown prevents is not sent
|
|
35
|
+
<ChatComposer
|
|
36
|
+
onSend={send}
|
|
37
|
+
maxLength={2000}
|
|
38
|
+
autoFocus
|
|
39
|
+
onKeyDown={(event) => {
|
|
40
|
+
if (event.key === 'Enter' && isMentionListOpen) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
pickMention();
|
|
43
|
+
}
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Keys:**
|
|
49
|
+
|
|
50
|
+
| Key | Does |
|
|
51
|
+
|-----|------|
|
|
52
|
+
| `Enter` | Sends. On a touch screen it starts a new line instead, since that keyboard has no other way to, and only the button sends |
|
|
53
|
+
| `Shift+Enter` | A new line |
|
|
54
|
+
| `Ctrl+Enter` / `⌘+Enter` | Sends, on a touch screen too |
|
|
55
|
+
|
|
56
|
+
The `Enter` that confirms a word in a Japanese, Chinese or Korean input method never sends,
|
|
57
|
+
including in Safari, which reports it as an ordinary `Enter`.
|
|
58
|
+
|
|
59
|
+
`onSend` gets the text trimmed, and is not called while the field is empty or only whitespace;
|
|
60
|
+
the send button is disabled then too. The focus stays in the field after sending, so a phone
|
|
61
|
+
keyboard stays open. The field grows with the text up to seven lines, then scrolls, and measures
|
|
62
|
+
again when its width changes.
|
|
63
|
+
|
|
64
|
+
**Buttons of your own.** `startContent` and `endContent` sit beside the field while the text fits
|
|
65
|
+
in it. Once it needs more lines than `rows` (a second one, by default), the field takes the whole
|
|
66
|
+
width and the buttons move to a row below it: `startContent` at the start, `endContent` and the
|
|
67
|
+
send button at the end, the usual toolbar of an AI chat. So several of them never leave the field
|
|
68
|
+
too narrow to write in. They move back once the field is empty, usually after sending, rather
|
|
69
|
+
than as soon as the wider field fits the text on one line, which would wrap it again. With only
|
|
70
|
+
the send button there is nothing to move, and it stays beside the field.
|
|
71
|
+
|
|
72
|
+
While `onStop` is set the send button is a stop button that calls it, and `Enter` sends nothing,
|
|
73
|
+
so the user can type the next message while an answer streams in.
|
|
74
|
+
|
|
75
|
+
**Key props:**
|
|
76
|
+
|
|
77
|
+
| Prop | Type | Description |
|
|
78
|
+
|------|------|-------------|
|
|
79
|
+
| `onSend` | `(text: string) => void` | Called with the trimmed text (required) |
|
|
80
|
+
| `value` | `string` | Controlled value. Sending clears it through `onChange('')` |
|
|
81
|
+
| `defaultValue` | `string` | Initial value, uncontrolled |
|
|
82
|
+
| `onChange` | `(value: string) => void` | Every change |
|
|
83
|
+
| `onStop` | `() => void` | Swaps send for a stop button while set |
|
|
84
|
+
| `onKeyDown` | `KeyboardEventHandler` | Runs before the composer's own keys; `preventDefault()` on an Enter keeps it from sending |
|
|
85
|
+
| `disabled` | `boolean` | Disables the field and both buttons |
|
|
86
|
+
| `rows` | `number` | The empty field's height in lines, default 1. It grows from there to seven |
|
|
87
|
+
| `startContent` | `ReactNode` | Before the field: attach, emoji. Below it once the text wraps, see above |
|
|
88
|
+
| `endContent` | `ReactNode` | Between the field and the send button. Below it once the text wraps |
|
|
89
|
+
| `ref` | `Ref<HTMLTextAreaElement>` | Ref to the inner `<textarea>`, e.g. to focus it when a conversation opens |
|
|
90
|
+
|
|
91
|
+
Every other native attribute (`maxLength`, `autoFocus`, `name`, `spellCheck`, `onPaste`, `aria-*`,
|
|
92
|
+
`data-*`) goes to the `<textarea>`, as on [Input](Input.md); `className` and `style` go to the
|
|
93
|
+
root. An `aria-label` replaces the placeholder as the field's name, and `readOnly` also keeps it
|
|
94
|
+
from sending.
|
|
95
|
+
|
|
96
|
+
**Global defaults:** adopts `ConfigProvider`. See [ConfigProvider.md](ConfigProvider.md).
|
|
97
|
+
|
|
98
|
+
**Slots:** `input` (the `<textarea>`) `button` (send or stop)
|
|
99
|
+
|
|
100
|
+
**Labels:** `placeholder` (also the field's accessible name) `send` `stop`: the text this component renders of its own. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `ChatComposerLabels`.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# ChatMessage
|
|
2
|
+
|
|
3
|
+
**When to use:** One message in a conversation: a messenger bubble, a support agent's reply, an AI assistant's answer. Put them in a [ChatMessageList](ChatMessageList.md), which also shows a whole chat put together. For a note between messages ("Lisa joined", a date), use a [Divider](Divider.md) with `text`.
|
|
4
|
+
|
|
5
|
+
**Keywords:** speech bubble, messenger, whatsapp, chatbot, assistant, LLM, reply, read receipt, seen, ticks, typing indicator
|
|
6
|
+
|
|
7
|
+
**Import:** `import { ChatMessage, ChatMessageStyleType, ChatMessageStatus } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { ChatMessageProps, ChatMessageLabels } from '@ahrowe/ui'`
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { Avatar, ChatMessage, ChatMessageStatus, ChatMessageStyleType, CopyButton, Loader, LoaderType, ThemeColor } from '@ahrowe/ui';
|
|
12
|
+
|
|
13
|
+
// Messenger: two from Ava in a run, then your own with its delivery state
|
|
14
|
+
<ChatMessage author='Ava' avatar={<Avatar name='Ava' />} time='10:41'>Are we still on for tonight?</ChatMessage>
|
|
15
|
+
<ChatMessage author='Ava' avatar={<Avatar name='Ava' />} time='10:41' isContinued>8 o'clock?</ChatMessage>
|
|
16
|
+
<ChatMessage isOwn time='10:42' status={ChatMessageStatus.Read}>Yes, see you there</ChatMessage>
|
|
17
|
+
|
|
18
|
+
// AI assistant: no bubble, full width, actions underneath
|
|
19
|
+
<ChatMessage styleType={ChatMessageStyleType.Plain} actions={<CopyButton value={answer} />}>
|
|
20
|
+
<Markdown>{answer}</Markdown>
|
|
21
|
+
</ChatMessage>
|
|
22
|
+
|
|
23
|
+
// Someone typing, or an answer on its way: a Loader is the content
|
|
24
|
+
<ChatMessage avatar={<Avatar name='Ava' />}>
|
|
25
|
+
<Loader type={LoaderType.Dots} size='1em' color={ThemeColor.Neutral} labels={{ loading: 'Ava is typing' }} />
|
|
26
|
+
</ChatMessage>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Enums:**
|
|
30
|
+
|
|
31
|
+
- `ChatMessageStyleType`: `Filled` (default when `isOwn`) | `Light` (default otherwise) | `Plain`, see [ThemeColor.md](ThemeColor.md). A `Light` bubble keeps the page's text colour, as a `Light` Card does. `Plain` has no bubble and takes the full width, for an assistant's answer
|
|
32
|
+
- `ChatMessageStatus`: `Sending` (a clock) | `Sent` (a check) | `Delivered` (two checks) | `Read` (two checks at full strength, in `--info-color` except on a `Filled` bubble, where the rest are dimmed) | `Failed` (no icon in the bubble: "Not sent" under it, ahead of `actions`, where a retry button belongs, with the icon in `--error-color` and the text in the page's own)
|
|
33
|
+
|
|
34
|
+
**A run of messages.** Set `isContinued` on every message after the first from the same author:
|
|
35
|
+
it hides `author` from view, tightens the gap above, rounds off the corner that points at the
|
|
36
|
+
author, and keeps the space of `avatar` so the run stays aligned. Pass the same `author` and
|
|
37
|
+
`avatar` to all of them: screen readers still read `author` on each one. Start a new run after a
|
|
38
|
+
date `Divider`, or the first message under it shows no name.
|
|
39
|
+
|
|
40
|
+
**Who sent it, for screen readers.** Alignment and colour are not read out, so each message names
|
|
41
|
+
its sender: `author`, or "You" (`labels.you`, never shown) on an own message without one. In a
|
|
42
|
+
one-to-one chat that is enough to leave `author` out; in a group, pass it to everyone else's.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
{messages.map((message, index) => (
|
|
46
|
+
<ChatMessage
|
|
47
|
+
key={message.id}
|
|
48
|
+
isOwn={message.authorId === me.id}
|
|
49
|
+
isContinued={messages[index - 1]?.authorId === message.authorId}
|
|
50
|
+
author={message.authorId === me.id ? undefined : message.authorName}
|
|
51
|
+
avatar={message.authorId === me.id ? undefined : <Avatar name={message.authorName} src={message.avatarUrl} />}
|
|
52
|
+
time={timeFormat.format(message.sentAt)}
|
|
53
|
+
status={message.status}
|
|
54
|
+
>
|
|
55
|
+
{message.text}
|
|
56
|
+
</ChatMessage>
|
|
57
|
+
))}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Content.** A string keeps its line breaks, so text typed with Shift+Enter shows as typed. Any
|
|
61
|
+
other content renders as given: the library renders no Markdown, so pass your renderer's output
|
|
62
|
+
(`react-markdown`, `marked`). A long word or URL wraps rather than widening the bubble. `time` and
|
|
63
|
+
`status` sit at the bubble's end: on the same line as a short message, on a line of their own
|
|
64
|
+
under a long one.
|
|
65
|
+
|
|
66
|
+
**Key props:**
|
|
67
|
+
|
|
68
|
+
| Prop | Type | Description |
|
|
69
|
+
|------|------|-------------|
|
|
70
|
+
| `children` | `ReactNode` | The message |
|
|
71
|
+
| `isOwn` | `boolean` | Sent by the current user: aligned to the end, `Filled` in `Primary` by default |
|
|
72
|
+
| `isContinued` | `boolean` | Follows a message from the same author, see above |
|
|
73
|
+
| `avatar` | `ReactNode` | Beside the bubble, typically an [Avatar](Avatar.md) |
|
|
74
|
+
| `author` | `ReactNode` | Name above the bubble, on the first message of a run. Screen readers get it on every one |
|
|
75
|
+
| `time` | `ReactNode` | In the bubble's corner. Format it yourself, e.g. with `Intl.DateTimeFormat` |
|
|
76
|
+
| `status` | `ChatMessageStatus` | Delivery state, an icon after `time` that screen readers read out. `Failed` is a notice under the bubble instead |
|
|
77
|
+
| `actions` | `ReactNode` | Under the bubble, after the `Failed` notice: a [CopyButton](CopyButton.md), retry, thumbs up and down |
|
|
78
|
+
| `styleType` | `ChatMessageStyleType` | See above |
|
|
79
|
+
| `color` | `ColorValue` | A `ThemeColor` or any CSS colour. Default `Primary` when `isOwn`, otherwise `Neutral` |
|
|
80
|
+
|
|
81
|
+
The bubble is at most 80% of the list's width (40em at most); `styles={{ body: { maxWidth } }}`
|
|
82
|
+
changes that.
|
|
83
|
+
|
|
84
|
+
**Global defaults:** adopts `ConfigProvider`. See [ConfigProvider.md](ConfigProvider.md).
|
|
85
|
+
|
|
86
|
+
**Slots:** `avatar` `body` (the column holding author, bubble and actions) `author` `bubble` `meta` (time and status) `actions` (the row under the bubble, with the `Failed` notice)
|
|
87
|
+
|
|
88
|
+
**Labels:** `sending` `sent` `delivered` `read` (the names screen readers give the status icon), `failed` (the notice under a failed message) and `you` (an own message's sender, for screen readers only). Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `ChatMessageLabels`.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# ChatMessageList
|
|
2
|
+
|
|
3
|
+
**When to use:** The scrolling conversation of a chat: a messenger, a support widget, an AI assistant. It opens at the newest message, follows new ones while the user is at the bottom, and loads older ones at the top without moving the view. Fill it with [ChatMessage](ChatMessage.md)s and put a [ChatComposer](ChatComposer.md) under it. For a long list that is not a conversation, use [VirtualList](VirtualList.md).
|
|
4
|
+
|
|
5
|
+
**Keywords:** messenger, whatsapp, support chat, chatbot, assistant, LLM, history, thread, stick to bottom, autoscroll, infinite scroll, load older, jump to latest, unread
|
|
6
|
+
|
|
7
|
+
**Import:** `import { ChatMessageList } from '@ahrowe/ui'`
|
|
8
|
+
**Types:** `import type { ChatMessageListProps, ChatMessageListHandle, ChatMessageListLabels } from '@ahrowe/ui'`
|
|
9
|
+
|
|
10
|
+
## A whole chat
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { useRef, useState } from 'react';
|
|
14
|
+
import { Avatar, ChatComposer, ChatMessage, ChatMessageList, ChatMessageStatus, Divider } from '@ahrowe/ui';
|
|
15
|
+
import type { ChatMessageListHandle } from '@ahrowe/ui';
|
|
16
|
+
|
|
17
|
+
function Chat({ me, initial }: ChatProps) {
|
|
18
|
+
const [messages, setMessages] = useState<Message[]>(initial);
|
|
19
|
+
const [hasOlder, setHasOlder] = useState(true);
|
|
20
|
+
const listRef = useRef<ChatMessageListHandle>(null);
|
|
21
|
+
|
|
22
|
+
const loadOlder = async () => {
|
|
23
|
+
const page = await api.messagesBefore(messages[0].id);
|
|
24
|
+
setMessages((current) => [...page.messages, ...current]);
|
|
25
|
+
setHasOlder(page.hasMore);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const send = (text: string) => {
|
|
29
|
+
setMessages((current) => [...current, { id: crypto.randomUUID(), authorId: me.id, text, status: ChatMessageStatus.Sending }]);
|
|
30
|
+
// Sending from further up the history takes the user down to their message.
|
|
31
|
+
listRef.current?.scrollToBottom();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div style={{ display: 'flex', flexDirection: 'column', height: '100dvh' }}>
|
|
36
|
+
<ChatMessageList ref={listRef} style={{ flex: 1 }} onLoadMore={hasOlder ? loadOlder : undefined}>
|
|
37
|
+
<Divider text='Today' />
|
|
38
|
+
{messages.map((message, index) => (
|
|
39
|
+
<ChatMessage
|
|
40
|
+
key={message.id}
|
|
41
|
+
isOwn={message.authorId === me.id}
|
|
42
|
+
isContinued={messages[index - 1]?.authorId === message.authorId}
|
|
43
|
+
avatar={message.authorId === me.id ? undefined : <Avatar name={message.authorName} />}
|
|
44
|
+
author={message.authorId === me.id ? undefined : message.authorName}
|
|
45
|
+
status={message.status}
|
|
46
|
+
>
|
|
47
|
+
{message.text}
|
|
48
|
+
</ChatMessage>
|
|
49
|
+
))}
|
|
50
|
+
</ChatMessageList>
|
|
51
|
+
<ChatComposer onSend={send} />
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
An AI chat is the same with a `Plain` answer that grows as it streams, and the composer's stop
|
|
58
|
+
button while it does:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<ChatMessageList ref={listRef} style={{ flex: 1 }}>
|
|
62
|
+
{turns.map((turn) =>
|
|
63
|
+
turn.role === 'user' ? (
|
|
64
|
+
<ChatMessage key={turn.id} isOwn>{turn.text}</ChatMessage>
|
|
65
|
+
) : (
|
|
66
|
+
<ChatMessage key={turn.id} styleType={ChatMessageStyleType.Plain} actions={<CopyButton value={turn.text} />}>
|
|
67
|
+
<Markdown>{turn.text}</Markdown>
|
|
68
|
+
</ChatMessage>
|
|
69
|
+
),
|
|
70
|
+
)}
|
|
71
|
+
</ChatMessageList>
|
|
72
|
+
<ChatComposer onSend={ask} onStop={isStreaming ? stop : undefined} />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Scrolling
|
|
76
|
+
|
|
77
|
+
- **Following.** While the user is within 32px of the bottom, anything that makes the content
|
|
78
|
+
taller keeps the newest message in view: a new message, an answer streaming in, an image
|
|
79
|
+
loading, the list getting shorter because the composer grew or a phone keyboard opened.
|
|
80
|
+
- **Reading back.** As soon as the user scrolls up, however little, nothing moves under them: new
|
|
81
|
+
messages arrive below, and a round button appears in the corner that jumps back down and starts
|
|
82
|
+
following again. It turns `Primary` once a new message has arrived below. Scrolling back to the
|
|
83
|
+
bottom by hand does the same.
|
|
84
|
+
- **After sending**, call `scrollToBottom()` on the handle, or a user who sent from further up
|
|
85
|
+
never sees their own message arrive.
|
|
86
|
+
- **Older messages.** With `onLoadMore`, nearing the top calls it and shows a loader there.
|
|
87
|
+
Prepend what it loads; the message that was at the top stays exactly where it was, at the very
|
|
88
|
+
top too, where the browser's own scroll anchoring does nothing and Safari has none at all. It is
|
|
89
|
+
called again while the loaded page still leaves the top near, but not after one that added
|
|
90
|
+
nothing. If it rejects, it is called again after three seconds if the top is still near, or
|
|
91
|
+
whenever the user next scrolls there. Pass `undefined` once there is nothing older.
|
|
92
|
+
|
|
93
|
+
**Sizing.** The list scrolls itself and fills the height it is given: `flex: 1` in a column, as
|
|
94
|
+
above, or a `height`. A conversation shorter than the list sits at the bottom, by the composer. To
|
|
95
|
+
start at the top instead, as an AI chat often does: `styles={{ content: { marginTop: 0 } }}`.
|
|
96
|
+
|
|
97
|
+
**Key props:**
|
|
98
|
+
|
|
99
|
+
| Prop | Type | Description |
|
|
100
|
+
|------|------|-------------|
|
|
101
|
+
| `children` | `ReactNode` | Messages, oldest first, and anything between them: a `Divider` with a date, a typing indicator |
|
|
102
|
+
| `onLoadMore` | `() => Promise<void>` | Loads older messages as the top comes near, see above |
|
|
103
|
+
| `ref` | `Ref<ChatMessageListHandle>` | `scrollToBottom()`: back to the newest message, smoothly unless the user prefers reduced motion, and following again |
|
|
104
|
+
|
|
105
|
+
**Accessibility:** the scrolling element is a `role="log"`, which screen readers announce
|
|
106
|
+
politely as messages are added, named "Messages" (`labels.messages`). It is a tab stop of its own,
|
|
107
|
+
so the history can be scrolled from the keyboard, and `aria-busy` while older messages load.
|
|
108
|
+
|
|
109
|
+
**Global defaults:** adopts `ConfigProvider`. See [ConfigProvider.md](ConfigProvider.md).
|
|
110
|
+
|
|
111
|
+
**Slots:** `viewport` (the scrolling log) `content` (the column of messages inside it) `scrollButton`
|
|
112
|
+
|
|
113
|
+
**Labels:** `messages` `scrollToBottom`: the text this component renders of its own. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; they merge per key. Type: `ChatMessageListLabels`.
|
package/docs/ConfigProvider.md
CHANGED
|
@@ -97,6 +97,7 @@ Each entry is a `Partial<...Props>`, so any of that component's props can be def
|
|
|
97
97
|
- **Form inputs:** `Input` · `Textarea` · `NumberInput` · `Dropdown` · `InputDropdown` · `Checkbox` · `Switch` · `RadioGroup` · `DatePicker` · `OptionPicker` · `OtpInput`
|
|
98
98
|
- **Display:** `Button` · `CopyButton` · `ActionButtons` · `Badge` · `Chip` · `Card` · `SectionHeader` · `Skeleton` · `Loader` · `LoadingOverlay` · `Accordion` · `Divider` · `Timer` · `WeatherIcon` · `Stepper`
|
|
99
99
|
- **Overlays:** `ConfirmModal` · `Modal` · `CommandPalette` · `Popover`
|
|
100
|
+
- **Chat:** `ChatMessage` · `ChatMessageList` · `ChatComposer`
|
|
100
101
|
- **Editors:** `RoomDrawer`, `RoomViewer`
|
|
101
102
|
- **Scheduling:** `Calendar`, for `labels`, `locale`, `weekStartsOn`, `slotMinutes`, `views`, `hiddenDays`, `scrollToHour`, `workingHours` and `showWeekNumbers` only (see below)
|
|
102
103
|
|
package/docs/Dropdown.md
CHANGED
|
@@ -36,13 +36,16 @@ const statusValidator = new FormValidator('', [Validators.required()]);
|
|
|
36
36
|
// Bottom sheet on small touch screens, anchored to the input everywhere else
|
|
37
37
|
<Dropdown label="Status" items={items} value={status} onChange={onChange} presentation={Presentation.Auto} />
|
|
38
38
|
|
|
39
|
-
// Infinite scroll
|
|
39
|
+
// Infinite scroll: more items as the end of the open list comes near
|
|
40
40
|
<Dropdown
|
|
41
41
|
items={items}
|
|
42
|
-
|
|
42
|
+
onLoadMore={hasMore ? loadMoreItems : undefined}
|
|
43
43
|
/>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
Loading on scroll suits a list people browse. To find one entry among hundreds, typing beats
|
|
47
|
+
scrolling: InputDropdown filters as the user types.
|
|
48
|
+
|
|
46
49
|
**Key props:**
|
|
47
50
|
|
|
48
51
|
| Prop | Type | Description |
|
|
@@ -57,7 +60,7 @@ const statusValidator = new FormValidator('', [Validators.required()]);
|
|
|
57
60
|
| `disabled` | `boolean` | Greys the field out and prevents interaction |
|
|
58
61
|
| `readOnly` | `boolean` | Locks the selection without the disabled styling — the field keeps its normal appearance and stays focusable, but the menu won't open and the value can't change (the chevron is hidden). Default `false` |
|
|
59
62
|
| `placeholder` | `string` | Placeholder when no selection |
|
|
60
|
-
| `
|
|
63
|
+
| `onLoadMore` | `() => Promise<void>` | Loads more items as the end of the open list comes near, with a spinner meanwhile. Append what it loads. Called again while the end stays near, until a load appends nothing; pass `undefined` once there are no more to save that last call. A rejected load is tried again after three seconds |
|
|
61
64
|
| `useMatLabelStyle` | `boolean` | Material-style floating label |
|
|
62
65
|
| `presentation` | `Presentation` | Where the list opens: anchored to the input (`Default`), always a bottom sheet, or a sheet only on a small touch screen. Imported from the same package: `import { Presentation } from '@ahrowe/ui'` |
|
|
63
66
|
| `alwaysFloatLabel` | `boolean` | Keep the material label floated in the border notch even when nothing is selected, so an empty dropdown reads as blank instead of showing the label as if it were the selected value. Only applies with `useMatLabelStyle` (default) |
|
|
@@ -87,3 +90,14 @@ outside (a wrapper, a grid column, `style={{ maxWidth: 240 }}`) rather than expe
|
|
|
87
90
|
to the label: everything inside is absolutely positioned, so it has no width of its own to fall back on.
|
|
88
91
|
|
|
89
92
|
**Slots:** `root` `label` `field` `control` `icon` `placeholder` `list` `option`
|
|
93
|
+
|
|
94
|
+
## Migration from `onScroll`
|
|
95
|
+
|
|
96
|
+
Before 0.51 the load-more callback was `onScroll`, and it was never called. TypeScript points at
|
|
97
|
+
every use:
|
|
98
|
+
|
|
99
|
+
| Before | After |
|
|
100
|
+
|--------|-------|
|
|
101
|
+
| `onScroll={…}` | `onLoadMore={…}` |
|
|
102
|
+
|
|
103
|
+
Loading now actually happens, so pass `undefined` once there is nothing more to load.
|