diff --git a/Documentation/Chat/index.md b/Documentation/Chat/index.md index a52c3898..0a3c9545 100644 --- a/Documentation/Chat/index.md +++ b/Documentation/Chat/index.md @@ -97,6 +97,8 @@ export const Workspace = () => { | `onRequestTopicName` / `isTopicUnnamed` | callbacks | — | The host-side naming contract. | | `selectedTopicId` / `onTopicSelected` | `ChatIdentifier \| null`, callback | Internal | Owns or observes the open topic. | | `authorOf`, `renderAvatar`, `renderAuthorName`, `buildAvatarUrl` | callbacks | — | Author resolution and rendering. Without `authorOf`, the id is shown as the name. | +| `renderMessageExtra` | `(message) => ReactNode` | — | Content rendered under a message's body, such as reactions or a failed-reply notice. | +| `renderHeaderActions` | `(openTopic) => ReactNode` | — | Content rendered in the header between the title and the close button, such as a rename control. | | `actions`, `quickReply` | `ChatMessageAction[]`, `boolean` | —, `true` | Message actions and quick reply; see [Message actions](./message-actions.md). | | `topicActions` | `ChatTopicAction[]` | — | Actions beside available topics; see [Topic actions](./topic-actions.md). | | `mentionCandidates` / `resolveMentionCandidates` | array or callback | — | See [Mentions and emoji](./mentions-and-emoji.md). Omit both to turn mentions off. | @@ -184,6 +186,25 @@ The built-in avatar shows initials on a color derived from the id. It shows an i /> ``` +## Add your own content to messages and the header + +The chat family stays small, so richer per-message features belong to your application. `renderMessageExtra` renders content under each message's body: reactions, a notice for a reply that failed, or anything else keyed off your own message type. `ChatSidebar` forwards it to the conversation. `renderHeaderActions` renders content in the sidebar header, next to the title; it receives the open topic, or `undefined` while the topic list is shown. Returning `null` renders nothing, and the chat's markup is unchanged when you leave both unset. + +```tsx +type AppMessage = ChatMessage & { failed?: boolean; reactions?: string[] }; + + + renderMessageExtra={(message) => + message.failed ?

The agent could not answer.

: + message.reactions?.length ? : null} + renderHeaderActions={(openTopic) => + openTopic ? : null} + /* ...the rest of your props */ +/> +``` + +`MyReactions` and `MyRenameButton` are your own components. The content you render keeps its own semantics and styling; give interactive controls accessible names. + ## Rendering a message body directly `ChatConversation` uses `ChatMessageBody` internally. Import it directly when an application-owned message list, notification, or transcript needs the same mention rendering without the rest of the conversation UI. diff --git a/Documentation/Common/action-menubar.md b/Documentation/Common/action-menubar.md index fe7ec835..c824c34f 100644 --- a/Documentation/Common/action-menubar.md +++ b/Documentation/Common/action-menubar.md @@ -29,7 +29,7 @@ The root is a `div` with `role='toolbar'` and `data-cratis-part='root'`. Pass `a Each action renders as a native ghost `Button`. By default, `focusMode={ToolbarFocusMode.Arrows}` moves between actions with Left/Right (reversed in RTL), and Home/End move to the first/last available action. Navigation follows DOM order, skips disabled, hidden, and inert actions, and does not wrap. Every action remains its own Tab stop, preserving Tab and Shift+Tab behavior. -Import `ToolbarFocusMode` from `@cratis/components/Common` and set `focusMode={ToolbarFocusMode.None}` to disable toolbar key handling while retaining native Tab stops. Widgets in templates keep their own keys. Child handlers and `pt.root.onKeyDown` can prevent navigation. A single-Tab-stop mode is planned ([#353](https://github.com/Cratis/Components/issues/353)). +Import `ToolbarFocusMode` from `@cratis/components/Common`. Set `focusMode={ToolbarFocusMode.SingleTabStop}` to give the menubar's own actions one Tab stop that follows the arrow keys and returns to the last focused action; template content keeps its own Tab stop. `pt` applies to every action, so setting `pt.root.tabIndex` gives every action that tab index and turns the single Tab stop off. Set `focusMode={ToolbarFocusMode.None}` to disable toolbar key handling while retaining native Tab stops. Widgets in templates keep their own keys. Child handlers and `pt.root.onKeyDown` can prevent navigation. `SingleTabStop` becomes the default in the next major release ([#353](https://github.com/Cratis/Components/issues/353)). An item's visible `label` is also its accessible name, and `ActionMenuItem` has no separate `aria-label`, so give every item a `label` or use `template` for an icon-only action that names itself. @@ -52,7 +52,7 @@ When `template` is present, `ActionMenubar` renders its result directly instead | Prop | Type | Required | Purpose | | ------------ | ------------------ | -------- | ---------------------------------------------------------- | | `model` | `ActionMenuItem[]` | Yes | Actions rendered from left to right. | -| `focusMode` | `ToolbarFocusMode` | No | Keyboard focus mode; defaults to `Arrows`. | +| `focusMode` | `ToolbarFocusMode` | No | `Arrows` (default), `SingleTabStop`, or `None`. | | `className` | `string` | No | Extra class name for the toolbar root. | | `aria-label` | `string` | No | Accessible name for the toolbar. | | `pt` | `ButtonParts` | No | Part attributes applied to every non-template action button. | diff --git a/Documentation/DataTables/index.md b/Documentation/DataTables/index.md index 0520bd22..74c1993a 100644 --- a/Documentation/DataTables/index.md +++ b/Documentation/DataTables/index.md @@ -15,6 +15,10 @@ The DataTables module provides a semantic local-array table plus specialized Arc `DataTableCore` accepts a `status` from `DataTableStatus` (`Ready`, `Loading`, `Failed`, or `Unauthorized`) (import `DataTableStatus` from `@cratis/components/DataTables`); it defaults to `Ready`. For a loading table with no rows, it renders a status row; with rows, it retains them and marks the table busy. Failed or unauthorized states render an alert row. Override the text with `loadingMessage`, `failureMessage`, and `unauthorizedMessage` or configure `CratisComponentsProvider`'s `messages.dataTable`. +`DataTableCore` keeps its own sort, column filters, and search text unless you control them. Pass `sort` with `onSortChange`, `filters` with `onFilter`, and `globalFilter` with `onGlobalFilterChange` to own that state, for example to persist it or to send it to the server. `sort={null}` means not sorted; leaving a prop undefined lets the table keep that piece of state itself, and the change callbacks report every change either way. Import `DataTableSort`, `DataTableSortDirection`, and `DataTableRowProcessing` from `@cratis/components/DataTables`. + +When the rows already reflect that state, because your query filtered and sorted them on the server, set `rowProcessing={DataTableRowProcessing.None}` so the table renders the rows as given instead of filtering and sorting them a second time. The default, `DataTableRowProcessing.Loaded`, filters and sorts the rows the table was given. The bound query tables do not take these props yet; server-side sorting and filtering for them is tracked in [#178](https://github.com/Cratis/Components/issues/178). + ## When to Use Use DataTableCore when: diff --git a/Documentation/Toolbar/index.md b/Documentation/Toolbar/index.md index 2eaaf608..14267dd5 100644 --- a/Documentation/Toolbar/index.md +++ b/Documentation/Toolbar/index.md @@ -21,7 +21,8 @@ import { Toolbar, ToolbarButton } from '@cratis/components/Toolbar'; - The root renders `role='toolbar'`, `aria-orientation`, and an accessible name. The name falls back to the provider's `messages.toolbar.label`, then `Tools`; pass `aria-label` or `aria-labelledby` to name each toolbar. - `focusMode` defaults to `ToolbarFocusMode.Arrows`: Up/Down move between tools in the default vertical orientation; a horizontal toolbar uses Left/Right (reversed in RTL). Home/End move to the first/last available tool in DOM order. Navigation skips disabled, hidden, and inert tools and stops at either end; it does not wrap. Each tool remains its own Tab stop, so Tab and Shift+Tab work as before. -- Set `focusMode={ToolbarFocusMode.None}` to keep native Tab behavior without toolbar key handling. Import `ToolbarFocusMode` from `@cratis/components/Toolbar`. A single-Tab-stop mode is planned ([#353](https://github.com/Cratis/Components/issues/353)). +- Set `focusMode={ToolbarFocusMode.SingleTabStop}` for the WAI-ARIA toolbar pattern: the toolbar's own tools (`ToolbarButton`, folder and fan-out triggers) share one Tab stop, which starts at the first available tool, follows the arrow keys, and returns to the last focused tool. When that tool is disabled, hidden, or removed, the Tab stop moves to the first available tool. A tool rendered outside the toolbar's own markup, such as through a portal or inside a nested element with `role="toolbar"`, keeps its own Tab stop. Tools in an open folder or fan-out panel belong to the same Tab stop, so you reach them with the arrow keys rather than Tab. Controls you place in the toolbar yourself, widgets such as inputs and sliders, a `ToolbarButton` with an explicit `pt.root.tabIndex`, and a folder or fan-out trigger with an explicit `pt.trigger.tabIndex` keep their own Tab stop; with a tab index of 0 or higher the arrow keys still reach them, while `-1` removes the tool from both Tab and arrow-key navigation. Until the toolbar has chosen its active tool (for example, in server-rendered markup before hydration), every tool keeps its own Tab stop. +- Set `focusMode={ToolbarFocusMode.None}` to keep native Tab behavior without toolbar key handling. Import `ToolbarFocusMode` from `@cratis/components/Toolbar`. `SingleTabStop` becomes the default in the next major release ([#353](https://github.com/Cratis/Components/issues/353)). - Arrows and Home/End inside inputs, sliders, selects, and custom widgets retain their own behavior. Nested toolbars handle their own keys. Key handlers on a tool or `pt.root` can cancel navigation with `preventDefault()` or `stopPropagation()`. - `title` is required on `ToolbarButton` and `ToolbarFolder`, and `tooltip` on `ToolbarFanOutItem`. That text becomes the button's `aria-label` and its tooltip, which appears on hover and on keyboard focus. - Passing `active` sets `aria-pressed` to `true` or `false` as well as the visual state (`data-active`, `data-selected`); omitting `active` leaves it unset. See [Active state](active-state.md). diff --git a/Source/Chat/ChatConversation.tsx b/Source/Chat/ChatConversation.tsx index 1bef7a29..97f911bf 100644 --- a/Source/Chat/ChatConversation.tsx +++ b/Source/Chat/ChatConversation.tsx @@ -118,6 +118,14 @@ export interface ChatConversationProps ReactNode; + /** + * Renders host content under a message's body, for example reactions or a failed-reply notice. + * Nothing extra is rendered when it returns null or undefined. + * @param message The message being rendered. + * @returns What to render under the message body. + */ + renderMessageExtra?: (message: TMessage) => ReactNode; + /** The host's own actions, offered as buttons on every message each is available for. */ actions?: ChatMessageAction[]; @@ -228,6 +236,7 @@ export const ChatConversation = ({ authorOf, renderAvatar, renderAuthorName, + renderMessageExtra, actions, mentionCandidates, resolveMentionCandidates, @@ -377,6 +386,7 @@ export const ChatConversation = ({ )} + {renderMessageExtra?.(message)} {showTimestamp && ( {relativeTimestamp( diff --git a/Source/Chat/ChatSidebar.tsx b/Source/Chat/ChatSidebar.tsx index 3f3ae177..c7205208 100644 --- a/Source/Chat/ChatSidebar.tsx +++ b/Source/Chat/ChatSidebar.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { useEffect, useRef, useState } from 'react'; -import type { ButtonHTMLAttributes, HTMLAttributes } from 'react'; +import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react'; import { createPortal } from 'react-dom'; import { Modal, ModalOverlay } from 'react-aria-components'; import type { ChatConversationLabels, ChatConversationProps } from './ChatConversation'; @@ -89,6 +89,14 @@ export interface ChatSidebarProps< ChatConversationProps, 'messages' | 'onSendMessage' | 'labels' | 'className' | 'status' > { + /** + * Renders host content in the header, between the title and the close button, for example a + * rename control for the open topic. Nothing is rendered when it returns null or undefined. + * @param openTopic The open topic, or undefined while the topic list is shown. + * @returns What to render in the header. + */ + renderHeaderActions?: (openTopic: TTopic | undefined) => ReactNode; + /** Whether the sidebar is open. */ open: boolean; @@ -230,6 +238,7 @@ export const ChatSidebar = < topics, messages, topicActions, + renderHeaderActions, topicsStatus, messagesStatus, selectedTopicId, @@ -379,6 +388,7 @@ export const ChatSidebar = < > {title} + {renderHeaderActions?.(openTopic)} + + + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + canvas.getByRole('button', { name: 'Before' }).focus(); + await userEvent.tab(); + await expect(canvas.getByRole('button', { name: 'New' })).toHaveFocus(); + await userEvent.keyboard('{ArrowRight}'); + await expect(canvas.getByRole('button', { name: 'Save' })).toHaveFocus(); + await userEvent.tab(); + await expect(canvas.getByRole('button', { name: 'After' })).toHaveFocus(); + await userEvent.tab({ shift: true }); + await expect(canvas.getByRole('button', { name: 'Save' })).toHaveFocus(); + }, +}; + export const ArrowsWithWidgets: Story = { args: { focusMode: ToolbarFocusMode.Arrows, diff --git a/Source/Common/ActionMenubar.tsx b/Source/Common/ActionMenubar.tsx index aa290f16..c42c34ca 100644 --- a/Source/Common/ActionMenubar.tsx +++ b/Source/Common/ActionMenubar.tsx @@ -5,6 +5,7 @@ import { Fragment, type ReactNode } from 'react'; import { Button, type ButtonParts, type ButtonSeverity, type ButtonTone } from './Button'; import { ToolbarFocusMode } from './ToolbarFocusMode'; import { useToolbarKeyboardNavigation } from './useToolbarKeyboardNavigation'; +import { ToolbarRovingContext, useRovingTool } from './ToolbarRovingContext'; /** A single action in an {@link ActionMenubar}. */ export interface ActionMenuItem { @@ -56,6 +57,28 @@ const buttonToneForSeverity: Record = { contrast: 'neutral', }; +/** One Components-owned action button, which takes part in the menubar's single Tab stop. */ +const ActionMenubarButton = ({ item, pt }: { item: ActionMenuItem; pt: ActionMenubarProps['pt'] }) => { + // The consumer's pt.root tab index and focus handler are composed, because props passed + // directly to Button take precedence over pt.root. + const rovingTool = useRovingTool(pt?.root?.tabIndex, pt?.root?.onFocus); + return ( + + + } title='Draw' /> + } title='Erase' /> + + + + + ), + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + canvas.getByRole('button', { name: 'Before' }).focus(); + await userEvent.tab(); + await expect(canvas.getByRole('button', { name: 'Draw' })).toHaveFocus(); + await userEvent.keyboard('{ArrowRight}'); + await expect(canvas.getByRole('button', { name: 'Erase' })).toHaveFocus(); + await userEvent.tab(); + await expect(canvas.getByRole('textbox', { name: 'Brush size' })).toHaveFocus(); + await userEvent.tab(); + await expect(canvas.getByRole('button', { name: 'After' })).toHaveFocus(); + await userEvent.tab({ shift: true }); + await userEvent.tab({ shift: true }); + await expect(canvas.getByRole('button', { name: 'Erase' })).toHaveFocus(); + }, +}; + /** Demonstrates the active (selected) state of a toolbar button. */ export const WithActiveButton: Story = { render: () => { diff --git a/Source/Toolbar/Toolbar.tsx b/Source/Toolbar/Toolbar.tsx index d91d29ef..73c06c28 100644 --- a/Source/Toolbar/Toolbar.tsx +++ b/Source/Toolbar/Toolbar.tsx @@ -5,6 +5,7 @@ import { type HTMLAttributes, type ReactNode } from 'react'; import { ToolbarDragContext } from './ToolbarDragContext'; import { ToolbarFocusMode } from '../Common/ToolbarFocusMode'; import { useToolbarKeyboardNavigation } from '../Common/useToolbarKeyboardNavigation'; +import { ToolbarRovingContext } from '../Common/ToolbarRovingContext'; import { useCratisComponentsConfig } from '../Common/CratisComponentsProvider'; import type { ExactPartKeys } from '../types/ExactPartKeys'; import type { PartsOf } from '../types/parts'; @@ -63,7 +64,7 @@ export const Toolbar = ({ 'aria-labelledby': ariaLabelledBy, pt, }: ToolbarProps) => { - const { rootRef, onKeyDown } = useToolbarKeyboardNavigation(orientation, focusMode); + const { rootRef, onKeyDown, roving } = useToolbarKeyboardNavigation(orientation, focusMode); const { messages } = useCratisComponentsConfig(); const resolvedAriaLabel = ariaLabel ?? messages?.toolbar?.label ?? 'Tools'; @@ -89,7 +90,9 @@ export const Toolbar = ({ data-cratis-part='root' data-orientation={orientation} > - {children} + + {children} + ); diff --git a/Source/Toolbar/ToolbarButton.tsx b/Source/Toolbar/ToolbarButton.tsx index ab66eec8..72fad111 100644 --- a/Source/Toolbar/ToolbarButton.tsx +++ b/Source/Toolbar/ToolbarButton.tsx @@ -12,6 +12,7 @@ import type { TooltipPosition } from '../Common/Tooltip'; import { useToolbarDragContext } from './ToolbarDragContext'; import { useToolbarFolderMode } from './ToolbarFolderContext'; import { useToolbarItemVisibility } from './ToolbarItemVisibilityContext'; +import { useRovingTool } from '../Common/ToolbarRovingContext'; /** Stable part attributes for {@link ToolbarButton}. */ export interface ToolbarButtonParts { @@ -89,6 +90,7 @@ export const ToolbarButton = ({ const dragContext = useToolbarDragContext(); const folderMode = useToolbarFolderMode(); const isToolbarItemVisible = useToolbarItemVisibility(); + const rovingTool = useRovingTool(pt?.root?.tabIndex, pt?.root?.onFocus); const isListMode = folderMode === 'list'; const isDraggable = draggable ?? dragContext.draggable; @@ -110,6 +112,7 @@ export const ToolbarButton = ({ + + + + + + + + + + ); +}; + +beforeEach(() => { + (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + container = document.createElement('div'); + document.body.append(container); + root = createRoot(container); +}); +afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); +}); + +describe('when a toolbar uses a single Tab stop', () => { + it('should give only the first of its own tools a Tab stop and keep consumer tab indexes', async () => { + await render(); + expect(tabIndexes()).to.deep.equal(['0', '-1', '-1', '-1']); + }); + + it('should enter at the active tool, skip its other tools, and keep the input a separate Tab stop', async () => { + await render(); + const user = userEvent.setup(); + container.querySelector('button')!.focus(); + await user.tab(); + expect(document.activeElement?.getAttribute('aria-label')).to.equal('Draw'); + await user.tab(); + expect(document.activeElement?.getAttribute('aria-label')).to.equal('Size'); + await user.tab(); + expect(document.activeElement?.textContent).to.equal('After'); + }); + + it('should move the Tab stop with arrow keys and return to the last focused tool', async () => { + await render(); + const [draw] = toolButtons(); + draw.focus(); + await key(draw, 'ArrowRight'); + expect(document.activeElement?.getAttribute('aria-label')).to.equal('Erase'); + expect(tabIndexes()).to.deep.equal(['-1', '0', '-1', '-1']); + const user = userEvent.setup(); + container.querySelector('button')!.focus(); + await user.tab(); + expect(document.activeElement?.getAttribute('aria-label')).to.equal('Erase'); + }); + + it('should move the Tab stop to the next available tool when the active tool becomes unavailable', async () => { + await render(); + await act(async () => disableFirst(true)); + expect(tabIndexes()).to.deep.equal(['-1', '0', '-1', '-1']); + }); + + // The tooltip trigger gives each tool tabindex=0 in the default mode, as before this change. + it('should render the default tab indexes in the default mode', async () => { + await render(); + expect(tabIndexes()).to.deep.equal(['0', '0', '0', '-1']); + }); + + it('should restore the default tab indexes when switching back to the default mode', async () => { + await render(); + await render(); + expect(tabIndexes()).to.deep.equal(['0', '0', '0', '-1']); + }); +}); + +// State that changes inside a tool's own wrapper does not re-render the toolbar; the toolbar must +// still notice when its active tool is no longer available. +let changeFirst: (change: 'optOut' | 'disable' | 'remove' | undefined) => void = () => undefined; +const WrappedFirstTool = () => { + const [change, setChange] = useState<'optOut' | 'disable' | 'remove' | undefined>(undefined); + changeFirst = setChange; + if (change === 'remove') return null; + return ( + + ); +}; + +describe('when the active tool changes inside its own wrapper', () => { + for (const change of ['optOut', 'disable', 'remove'] as const) { + it(`should move the Tab stop to the next tool after ${change}`, async () => { + await render( + + + + , + ); + expect(container.querySelector('[aria-label="Draw"]')!.getAttribute('tabindex')).to.equal('0'); + await act(async () => changeFirst(change)); + await act(async () => { await Promise.resolve(); }); + expect(container.querySelector('[aria-label="Erase"]')!.getAttribute('tabindex')).to.equal('0'); + }); + } +}); + +describe('when a toolbar with a folder uses a single Tab stop', () => { + it('should make the folder trigger part of the single Tab stop', async () => { + await render( + + + + + + , + ); + expect(container.querySelector('[aria-label="Shapes"]')!.getAttribute('tabindex')).to.equal('-1'); + const draw = container.querySelector('[aria-label="Draw"]')!; + draw.focus(); + await key(draw, 'ArrowDown'); + expect(document.activeElement?.getAttribute('aria-label')).to.equal('Shapes'); + expect(container.querySelector('[aria-label="Shapes"]')!.getAttribute('tabindex')).to.equal('0'); + }); +}); + +describe('when a consumer handles focus on a tool', () => { + it('should call the consumer handler and still move the Tab stop', async () => { + let focused = 0; + await render( + + + { focused++; } } }} /> + , + ); + await act(async () => container.querySelector('[aria-label="Erase"]')!.focus()); + expect(focused).to.equal(1); + expect(container.querySelector('[aria-label="Erase"]')!.getAttribute('tabindex')).to.equal('0'); + }); +}); + +describe('when a single Tab stop toolbar renders in StrictMode', () => { + it('should still give exactly one tool the Tab stop', async () => { + await render(); + expect(tabIndexes()).to.deep.equal(['0', '-1', '-1', '-1']); + }); +}); + +describe('when an action menubar uses a single Tab stop', () => { + it('should give only its first action a Tab stop and move it with arrow keys', async () => { + await render( + , + ); + const actions = () => Array.from(container.querySelectorAll('button')); + expect(actions().map(action => action.getAttribute('tabindex'))).to.deep.equal(['0', '-1', '-1']); + actions()[0].focus(); + await key(actions()[0], 'End'); + expect(document.activeElement?.textContent).to.contain('Delete'); + expect(actions().map(action => action.getAttribute('tabindex'))).to.deep.equal(['-1', '-1', '0']); + }); +}); diff --git a/Source/api-surface.json b/Source/api-surface.json index 768a6e53..f407eadd 100644 --- a/Source/api-surface.json +++ b/Source/api-surface.json @@ -121,7 +121,7 @@ "ChatComposerLabels": "export interface ChatComposerLabels { placeholder?: string; insertEmoji?: string; send?: string; sendMessage?: string; reactionPicker?: ReactionPickerLabels; }", "ChatConversation": "ChatConversation: (props: ChatConversationProps) => import(\"react\").JSX.Element", "ChatConversationLabels": "export interface ChatConversationLabels { loading?: string; failed?: string; unauthorized?: string; empty?: string; typing?: string; typingTwo?: string; typingSeveral?: string; quickReplyTo?: string; timestamps?: RelativeTimestampLabels; composer?: ChatComposerLabels; }", - "ChatConversationProps": "export interface ChatConversationProps { messages: TMessage[]; status?: ChatStatus; onSendMessage: (body: string, mentions: ChatMention[]) => void; authorOf?: (authorId: ChatIdentifier) => ChatAuthor; renderAvatar?: (authorId: ChatIdentifier, author: ChatAuthor) => ReactNode; renderAuthorName?: (authorId: ChatIdentifier, author: ChatAuthor) => ReactNode; actions?: ChatMessageAction[]; mentionCandidates?: MentionCandidate[]; resolveMentionCandidates?: (query: string) => MentionCandidate[] | Promise; typingAuthors?: ChatTypingAuthor[]; quickReply?: boolean; buildAvatarUrl?: (params: BuildAvatarUrlParams) => string; autoFocus?: boolean; labels?: ChatConversationLabels; className?: string; }", + "ChatConversationProps": "export interface ChatConversationProps { messages: TMessage[]; status?: ChatStatus; onSendMessage: (body: string, mentions: ChatMention[]) => void; authorOf?: (authorId: ChatIdentifier) => ChatAuthor; renderAvatar?: (authorId: ChatIdentifier, author: ChatAuthor) => ReactNode; renderAuthorName?: (authorId: ChatIdentifier, author: ChatAuthor) => ReactNode; renderMessageExtra?: (message: TMessage) => ReactNode; actions?: ChatMessageAction[]; mentionCandidates?: MentionCandidate[]; resolveMentionCandidates?: (query: string) => MentionCandidate[] | Promise; typingAuthors?: ChatTypingAuthor[]; quickReply?: boolean; buildAvatarUrl?: (params: BuildAvatarUrlParams) => string; autoFocus?: boolean; labels?: ChatConversationLabels; className?: string; }", "ChatIdentifier": "export type ChatIdentifier = string | { toString(): string; };", "ChatMention": "export interface ChatMention { id: ChatIdentifier; name: string; kind: ChatAuthorKind; }", "ChatMessage": "export interface ChatMessage { id: ChatIdentifier; topicId: ChatIdentifier; authorId: ChatIdentifier; body: string; timestamp: Date; mentions?: ChatMention[]; metadata?: Record; }", @@ -133,7 +133,7 @@ "ChatSidebarForObservableQueriesProps": "export interface ChatSidebarForObservableQueriesProps, TMessagesQuery extends IObservableQueryFor, TTopicsArguments extends object = object, TMessagesArguments extends object = object> extends Omit, 'topics' | 'messages' | 'selectedTopicId' | 'topicsStatus' | 'messagesStatus'> { topicsQuery: Constructor; topicsArguments?: TTopicsArguments; messagesQuery: Constructor; messagesArguments: (topicId: ChatIdentifier | undefined) => TMessagesArguments | undefined; }", "ChatSidebarLabels": "export interface ChatSidebarLabels { topics?: string; unnamedTopic?: string; back?: string; close?: string; topicList?: ChatTopicListLabels; conversation?: ChatConversationLabels; }", "ChatSidebarParts": "export interface ChatSidebarParts { backdrop?: ChatSidebarPartAttributes; root?: ChatSidebarPartAttributes; header?: ChatSidebarPartAttributes; title?: ChatSidebarPartAttributes; back?: ChatSidebarButtonAttributes; close?: ChatSidebarButtonAttributes; content?: ChatSidebarPartAttributes; }", - "ChatSidebarProps": "export interface ChatSidebarProps extends Omit, 'messages' | 'onSendMessage' | 'labels' | 'className' | 'status'> { open: boolean; onClose: () => void; topics: TTopic[]; messages: TMessage[]; topicActions?: ChatTopicAction[]; topicsStatus?: ChatStatus; messagesStatus?: ChatStatus; selectedTopicId?: ChatIdentifier | null; onTopicSelected?: (topicId: ChatIdentifier | undefined, topic?: TTopic) => void; onStartTopic?: () => ChatIdentifier | undefined | Promise; onSendMessage: (topicId: ChatIdentifier, body: string, mentions: ChatMention[]) => void; onRequestTopicName?: (topic: TTopic, firstMessageBody: string) => void; isTopicUnnamed?: (topic: TTopic) => boolean; position?: 'left' | 'right'; width?: string; modal?: boolean; labels?: ChatSidebarLabels; className?: string; pt?: ChatSidebarParts; }", + "ChatSidebarProps": "export interface ChatSidebarProps extends Omit, 'messages' | 'onSendMessage' | 'labels' | 'className' | 'status'> { renderHeaderActions?: (openTopic: TTopic | undefined) => ReactNode; open: boolean; onClose: () => void; topics: TTopic[]; messages: TMessage[]; topicActions?: ChatTopicAction[]; topicsStatus?: ChatStatus; messagesStatus?: ChatStatus; selectedTopicId?: ChatIdentifier | null; onTopicSelected?: (topicId: ChatIdentifier | undefined, topic?: TTopic) => void; onStartTopic?: () => ChatIdentifier | undefined | Promise; onSendMessage: (topicId: ChatIdentifier, body: string, mentions: ChatMention[]) => void; onRequestTopicName?: (topic: TTopic, firstMessageBody: string) => void; isTopicUnnamed?: (topic: TTopic) => boolean; position?: 'left' | 'right'; width?: string; modal?: boolean; labels?: ChatSidebarLabels; className?: string; pt?: ChatSidebarParts; }", "ChatStatus": "export declare enum ChatStatus { Ready = \"ready\", Loading = \"loading\", Failed = \"failed\", Unauthorized = \"unauthorized\" }", "ChatTopic": "export interface ChatTopic { id: ChatIdentifier; name?: string; startedBy?: ChatIdentifier; started?: Date; lastActivity?: Date; metadata?: Record; }", "ChatTopicAction": "export interface ChatTopicAction { id: string; label: string; icon?: string | ReactNode; isAvailable?: (topic: TTopic) => boolean; onInvoke: (topic: TTopic) => void; }", @@ -361,7 +361,7 @@ "ToggleGroupPartAttributes": "export type ToggleGroupPartAttributes = Pick, 'className' | 'style' | 'title'> & { [dataAttribute: `data-${string}`]: string | number | boolean | undefined; };", "ToggleGroupParts": "export interface ToggleGroupParts { root?: ToggleGroupPartAttributes; option?: ToggleGroupPartAttributes; icon?: ToggleGroupPartAttributes; label?: ToggleGroupPartAttributes; }", "ToggleGroupProps": "export interface ToggleGroupProps { options: ToggleGroupOption[]; value: string; onChange: (value: string) => void; disabled?: boolean; 'aria-labelledby'?: string; 'aria-label'?: string; 'aria-describedby'?: string; invalid?: boolean; className?: string; pt?: ToggleGroupParts; }", - "ToolbarFocusMode": "export declare enum ToolbarFocusMode { Arrows = \"arrows\", None = \"none\" }", + "ToolbarFocusMode": "export declare enum ToolbarFocusMode { Arrows = \"arrows\", SingleTabStop = \"singleTabStop\", None = \"none\" }", "Tooltip": "Tooltip: (props: TooltipProps) => ReactElement>", "TooltipPosition": "export type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';", "TooltipProps": "export interface TooltipProps { content?: ReactNode; position?: TooltipPosition; disabled?: boolean; className?: string; children: ReactElement; }", @@ -395,7 +395,7 @@ "ColumnFilterOption": "export interface ColumnFilterOption { label: string; value: unknown; }", "ColumnProps": "export interface ColumnProps { field?: string; header?: React.ReactNode; body?: (rowData: TData) => React.ReactNode; sortable?: boolean; filter?: boolean; filterField?: string; filterPlaceholder?: string; dataType?: ColumnFilterDataType; showFilterMatchModes?: boolean; filterElement?: ColumnFilterElement; filterOptions?: ColumnFilterOption[]; filterLabels?: Partial; filterPt?: ColumnFilterMenuParts; selectionMode?: 'single' | 'multiple'; style?: React.CSSProperties; className?: string; headerStyle?: React.CSSProperties; headerClassName?: string; bodyStyle?: React.CSSProperties; bodyClassName?: string; }", "DataTableCore": "DataTableCore: (props: DataTableCoreProps) => React.JSX.Element", - "DataTableCoreProps": "export interface DataTableCoreProps { data: TData[]; children?: ReactNode; dataKey?: string; emptyMessage: ReactNode; status?: DataTableStatus; loadingMessage?: ReactNode; failureMessage?: ReactNode; unauthorizedMessage?: ReactNode; selectionMode?: 'single' | 'multiple'; selectionAriaLabel?: string; selectAllAriaLabel?: string; selection?: TData | null; onSelectionChange?: (event: DataTableSelectionChangeEvent) => void; selectedItems?: TData[]; onSelectedItemsChange?: (items: TData[]) => void; onRowClick?: (event: DataTableRowClickEvent) => void; rowClassName?: (rowData: TData) => string; globalFilterFields?: string[]; globalSearchPlaceholder?: string; globalSearchAriaLabel?: string; defaultFilters?: DataTableFilterMeta; onFilter?: (filters: DataTableFilterMeta) => void; scrollable?: boolean; scrollHeight?: string; className?: string; style?: CSSProperties; pt?: DataTableParts; ptOptions?: object; unstyled?: boolean; }", + "DataTableCoreProps": "export interface DataTableCoreProps { data: TData[]; children?: ReactNode; dataKey?: string; emptyMessage: ReactNode; status?: DataTableStatus; loadingMessage?: ReactNode; failureMessage?: ReactNode; unauthorizedMessage?: ReactNode; selectionMode?: 'single' | 'multiple'; selectionAriaLabel?: string; selectAllAriaLabel?: string; selection?: TData | null; onSelectionChange?: (event: DataTableSelectionChangeEvent) => void; selectedItems?: TData[]; onSelectedItemsChange?: (items: TData[]) => void; onRowClick?: (event: DataTableRowClickEvent) => void; rowClassName?: (rowData: TData) => string; globalFilterFields?: string[]; globalSearchPlaceholder?: string; globalSearchAriaLabel?: string; defaultFilters?: DataTableFilterMeta; filters?: DataTableFilterMeta; onFilter?: (filters: DataTableFilterMeta) => void; globalFilter?: string; onGlobalFilterChange?: (globalFilter: string) => void; sort?: DataTableSort | null; onSortChange?: (sort: DataTableSort | null) => void; rowProcessing?: DataTableRowProcessing; scrollable?: boolean; scrollHeight?: string; className?: string; style?: CSSProperties; pt?: DataTableParts; ptOptions?: object; unstyled?: boolean; }", "DataTableCustomFilterMatchMode": "export type DataTableCustomFilterMatchMode = string & { readonly [customFilterMatchMode]: true; };", "DataTableFilterConstraint": "export interface DataTableFilterConstraint { value: unknown; matchMode?: DataTableFilterMatchMode; }", "DataTableFilterEntry": "export type DataTableFilterEntry = DataTableFilterConstraint | DataTableOperatorFilterConstraint;", @@ -410,7 +410,10 @@ "DataTableOperatorFilterConstraint": "export interface DataTableOperatorFilterConstraint { operator?: string; constraints: DataTableFilterConstraint[]; }", "DataTableParts": "export interface DataTableParts { root?: HTMLAttributes; search?: HTMLAttributes; searchInput?: React.InputHTMLAttributes; tableContainer?: HTMLAttributes; table?: TableHTMLAttributes; head?: HTMLAttributes; headerRow?: HTMLAttributes; headerCell?: ThHTMLAttributes; body?: HTMLAttributes; row?: HTMLAttributes; cell?: TdHTMLAttributes; emptyRow?: HTMLAttributes; emptyCell?: TdHTMLAttributes; loadingRow?: HTMLAttributes; loadingCell?: TdHTMLAttributes; failureRow?: HTMLAttributes; failureCell?: TdHTMLAttributes; }", "DataTableRowClickEvent": "export interface DataTableRowClickEvent { data: TData; index: number; }", + "DataTableRowProcessing": "export declare enum DataTableRowProcessing { Loaded = \"loaded\", None = \"none\" }", "DataTableSelectionChangeEvent": "export interface DataTableSelectionChangeEvent { value: TData | null; originalEvent?: SyntheticEvent; }", + "DataTableSort": "export interface DataTableSort { field: string; direction: DataTableSortDirection; }", + "DataTableSortDirection": "export declare enum DataTableSortDirection { Ascending = \"ascending\", Descending = \"descending\" }", "DataTableStatus": "export declare enum DataTableStatus { Ready = \"ready\", Loading = \"loading\", Failed = \"failed\", Unauthorized = \"unauthorized\" }", "TablePaginator": "TablePaginator: (props: TablePaginatorProps) => import(\"react\").ReactElement>", "TablePaginatorParts": "export interface TablePaginatorParts { root?: HTMLAttributes; range?: HTMLAttributes; info?: HTMLAttributes; first?: ButtonParts; previous?: ButtonParts; next?: ButtonParts; last?: ButtonParts; }", @@ -558,7 +561,7 @@ "ToolbarFanOutItem": "ToolbarFanOutItem: (props: ToolbarFanOutItemProps) => React.JSX.Element", "ToolbarFanOutItemProps": "export interface ToolbarFanOutItemProps { icon: Icon; tooltip: string; tooltipPosition?: TooltipPosition; fanOutDirection?: 'right' | 'left' | 'up' | 'down'; className?: string; pt?: ToolbarFanOutParts; children: ReactNode; }", "ToolbarFanOutParts": "export interface ToolbarFanOutParts { root?: HTMLAttributes; trigger?: ButtonHTMLAttributes; panel?: HTMLAttributes; }", - "ToolbarFocusMode": "export declare enum ToolbarFocusMode { Arrows = \"arrows\", None = \"none\" }", + "ToolbarFocusMode": "export declare enum ToolbarFocusMode { Arrows = \"arrows\", SingleTabStop = \"singleTabStop\", None = \"none\" }", "ToolbarFolder": "ToolbarFolder: (props: ToolbarFolderProps) => import(\"react\").JSX.Element", "ToolbarFolderMode": "export type ToolbarFolderMode = 'grid' | 'list';", "ToolbarFolderParts": "export interface ToolbarFolderParts { root?: HTMLAttributes; trigger?: ButtonHTMLAttributes; panel?: HTMLAttributes; }", diff --git a/Storybook/scripts/storybook-inventory.json b/Storybook/scripts/storybook-inventory.json index e3e8a44b..41988fe3 100644 --- a/Storybook/scripts/storybook-inventory.json +++ b/Storybook/scripts/storybook-inventory.json @@ -138,6 +138,7 @@ "common-actionmenubar--custom-parts", "common-actionmenubar--default", "common-actionmenubar--disabled-action", + "common-actionmenubar--single-tab-stop", "common-button--ghost", "common-button--icon-only", "common-button--label", @@ -370,6 +371,7 @@ "common-actionmenubar--custom-parts", "common-actionmenubar--default", "common-actionmenubar--disabled-action", + "common-actionmenubar--single-tab-stop", "common-breadcrumbs--custom-separator", "common-breadcrumbs--default", "common-breadcrumbs--marks-the-current-page", @@ -617,6 +619,7 @@ "toolbar-toolbar--layout-for-editor-modules", "toolbar-toolbar--layout-with-global-and-editor-regions", "toolbar-toolbar--layout-with-smooth-editor-transitions", + "toolbar-toolbar--single-tab-stop", "toolbar-toolbar--with-active-button", "toolbar-toolbar--with-contexts", "toolbar-toolbar--with-editable-tool",