From 28b2f68472570bcbe399837290e2d9c2bc46c9ef Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:03:39 +0100 Subject: [PATCH 1/4] Enable nested tables to render multiple types for the same property --- .../mdx/NestedTable/NestedTableContext.tsx | 6 +- .../NestedTable/NestedTablePropertyRow.tsx | 83 ++++++------ .../Layout/mdx/NestedTable/parseTable.ts | 122 ++++++++++++------ 3 files changed, 131 insertions(+), 80 deletions(-) diff --git a/src/components/Layout/mdx/NestedTable/NestedTableContext.tsx b/src/components/Layout/mdx/NestedTable/NestedTableContext.tsx index fa9be9ddcd..505380f3e3 100644 --- a/src/components/Layout/mdx/NestedTable/NestedTableContext.tsx +++ b/src/components/Layout/mdx/NestedTable/NestedTableContext.tsx @@ -5,7 +5,8 @@ export interface TableProperty { required?: 'required' | 'optional'; // Optional - not present in 2 or 3 column tables description: ReactNode; // ReactNode to preserve markdown elements (links, lists, etc.) type?: ReactNode; // ReactNode to preserve markdown elements (links, etc.) - not present in 2 column tables - typeReference?: string; // ID of referenced table, if type is a table reference + typeReferences: string[]; // IDs of all referenced tables (empty array if none) + typeDisplay?: ReactNode; // Cleaned-up display for the type cell (Table elements replaced with their ID text) } export interface TableData { @@ -61,7 +62,8 @@ export const NestedTableProvider: React.FC = ({ childr (prop, i) => prop.name !== data.properties[i]?.name || prop.required !== data.properties[i]?.required || - prop.typeReference !== data.properties[i]?.typeReference, + prop.typeReferences.length !== data.properties[i]?.typeReferences.length || + prop.typeReferences.some((ref, j) => ref !== data.properties[i]?.typeReferences[j]), ); if (hasChanged) { registryRef.current.set(id, data); diff --git a/src/components/Layout/mdx/NestedTable/NestedTablePropertyRow.tsx b/src/components/Layout/mdx/NestedTable/NestedTablePropertyRow.tsx index 7fc0948394..25c80e9fbe 100644 --- a/src/components/Layout/mdx/NestedTable/NestedTablePropertyRow.tsx +++ b/src/components/Layout/mdx/NestedTable/NestedTablePropertyRow.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import cn from '@ably/ui/core/utils/cn'; -import { TableProperty, useNestedTable } from './NestedTableContext'; +import { TableData, TableProperty, useNestedTable } from './NestedTableContext'; import { NestedTableExpandButton } from './NestedTableExpandButton'; interface NestedTablePropertyRowProps { @@ -12,12 +12,14 @@ interface NestedTablePropertyRowProps { export const NestedTablePropertyRow: React.FC = ({ property, path, depth = 0 }) => { const { lookup, isExpanded, toggleExpanded, registryVersion } = useNestedTable(); const expandPath = `${path}.${property.name}`; - const expanded = isExpanded(expandPath); - // Look up the referenced table, re-computing when registry changes - const referencedTable = useMemo( - () => (property.typeReference ? lookup(property.typeReference) : undefined), - [property.typeReference, lookup, registryVersion], + // Look up all referenced tables, re-computing when registry changes + const referencedTables = useMemo( + () => + property.typeReferences + .map((ref) => ({ id: ref, table: lookup(ref) })) + .filter((entry): entry is { id: string; table: TableData } => entry.table !== undefined), + [property.typeReferences, lookup, registryVersion], ); return ( @@ -42,10 +44,10 @@ export const NestedTablePropertyRow: React.FC = ({ )} - {/* Type name - only shown for 3 or 4-column tables. Use typeReference if available for cleaner display. */} - {(property.typeReference || property.type) && ( + {/* Type name - use typeDisplay for cleaned-up rendering, fall back to raw type */} + {(property.typeReferences.length > 0 || property.type) && ( - {property.typeReference ?? property.type} + {property.typeDisplay ?? property.type} )} @@ -55,39 +57,40 @@ export const NestedTablePropertyRow: React.FC = ({ {property.description} - {/* Expand/collapse button and nested content */} - {referencedTable && property.typeReference && ( - <> - {/* Collapsed: standalone button */} - {!expanded && ( - toggleExpanded(expandPath)} - /> - )} + {/* Expand/collapse buttons and nested content - one per resolved table reference */} + {referencedTables.map(({ id, table }) => { + const refExpandPath = `${expandPath}.${id}`; + const refExpanded = isExpanded(refExpandPath); - {/* Expanded: button attached to nested container */} - {expanded && ( -
- {/* Hide button as header of the container */} - toggleExpanded(expandPath)} - /> - {/* Nested properties */} -
- {referencedTable.properties.map((nestedProperty) => ( -
- -
- ))} + return ( + + {/* Collapsed: standalone button */} + {!refExpanded && ( + toggleExpanded(refExpandPath)} /> + )} + + {/* Expanded: button attached to nested container */} + {refExpanded && ( +
+ {/* Hide button as header of the container */} + toggleExpanded(refExpandPath)} + /> + {/* Nested properties */} +
+ {table.properties.map((nestedProperty) => ( +
+ +
+ ))} +
-
- )} - - )} + )} + + ); + })}
); diff --git a/src/components/Layout/mdx/NestedTable/parseTable.ts b/src/components/Layout/mdx/NestedTable/parseTable.ts index b4d80cae7b..1bd30ccffa 100644 --- a/src/components/Layout/mdx/NestedTable/parseTable.ts +++ b/src/components/Layout/mdx/NestedTable/parseTable.ts @@ -1,4 +1,4 @@ -import { ReactNode, ReactElement, Children, isValidElement } from 'react'; +import { ReactNode, ReactElement, Children, isValidElement, cloneElement } from 'react'; import { TableProperty } from './NestedTableContext'; import { Table as BaseTable } from '../Table'; @@ -9,12 +9,6 @@ import { Table as BaseTable } from '../Table'; */ const TYPE_REFERENCE_PATTERN = /^[A-Z][a-zA-Z0-9]*(Options|Config|Settings|Data|Info|Params|Type|Enum)$/; -/** - * Pattern for explicit table reference syntax in markdown cells. - * Matches: or
- */ -const EXPLICIT_TABLE_REFERENCE_PATTERN = //i; - /** * Extracts text content from React children recursively */ @@ -50,21 +44,20 @@ function getTypeName(element: ReactElement): string { } /** - * Finds a Table element in children and returns its id prop + * Finds all Table elements in children and returns their id props */ -function findTableElementId(children: ReactNode): string | undefined { +function findAllTableElementIds(children: ReactNode): string[] { + const ids: string[] = []; + if (!children) { - return undefined; + return ids; } if (Array.isArray(children)) { for (const child of children) { - const result = findTableElementId(child); - if (result) { - return result; - } + ids.push(...findAllTableElementIds(child)); } - return undefined; + return ids; } if (isValidElement(children)) { @@ -73,48 +66,98 @@ function findTableElementId(children: ReactNode): string | undefined { // Check if this is a Table element with an id if ((typeName === 'Table' || typeName === 'NestedTable') && element.props.id) { - return element.props.id; + ids.push(element.props.id); } // Recurse into children - return findTableElementId(element.props.children); + ids.push(...findAllTableElementIds(element.props.children)); } - return undefined; + return ids; } /** - * Checks if a type cell contains a Table reference. + * Finds all Table references in type text. * Supports two syntaxes: - * 1. Explicit:
or
- * 2. Implicit: PascalCase names ending in recognized suffixes (see TYPE_REFERENCE_PATTERN) + * 1. Explicit:
or
(may appear multiple times) + * 2. Implicit: PascalCase names ending in recognized suffixes (only when entire text is one type name) */ -function extractTableReference(typeText: string): string | undefined { +function extractAllTableReferences(typeText: string): string[] { const trimmed = typeText.trim(); - // Check for explicit
syntax (rendered as text in markdown cells) - const explicitMatch = trimmed.match(EXPLICIT_TABLE_REFERENCE_PATTERN); - if (explicitMatch) { - return explicitMatch[1]; + // Check for explicit
syntax (may appear multiple times) + const matches = [...trimmed.matchAll(//gi)]; + if (matches.length > 0) { + return matches.map((m) => m[1]); } - // Check for implicit type name reference + // Fallback: implicit type name reference (only when the entire text is one type name) if (TYPE_REFERENCE_PATTERN.test(trimmed)) { - return trimmed; + return [trimmed]; + } + + return []; +} + +/** + * Walks React children and replaces Table/NestedTable elements with their ID as plain text. + * Produces a clean display like "PresenceData or String" from mixed React children. + */ +function buildTypeDisplay(children: ReactNode): ReactNode { + if (!children) { + return children; + } + + if (typeof children === 'string' || typeof children === 'number') { + return children; + } + + if (Array.isArray(children)) { + return children.map((child, i) => { + const result = buildTypeDisplay(child); + if (isValidElement(result)) { + return cloneElement(result, { key: i }); + } + return result; + }); + } + + if (isValidElement(children)) { + const element = children as ReactElement<{ id?: string; children?: ReactNode }>; + const typeName = getTypeName(element); + + // Replace Table/NestedTable elements with their ID as plain text + if ((typeName === 'Table' || typeName === 'NestedTable') && element.props.id) { + return element.props.id; + } + + // For other elements, recurse into their children + if (element.props.children) { + const newChildren = buildTypeDisplay(element.props.children); + return cloneElement(element, {}, newChildren); + } } - return undefined; + return children; } /** * Extracts type information from a table cell's children. - * Returns both the ReactNode for display and any detected type reference. + * Returns the ReactNode for display, all detected type references, and a cleaned-up display. */ -function extractTypeInfo(cellChildren: ReactNode): { type: ReactNode; typeReference: string | undefined } { +function extractTypeInfo(cellChildren: ReactNode): { + type: ReactNode; + typeReferences: string[]; + typeDisplay: ReactNode | undefined; +} { const typeText = extractText(cellChildren).trim(); - const tableElementId = findTableElementId(cellChildren); - const typeReference = tableElementId || extractTableReference(typeText); - return { type: cellChildren, typeReference }; + const elementIds = findAllTableElementIds(cellChildren); + const typeReferences = elementIds.length > 0 ? elementIds : extractAllTableReferences(typeText); + + // Build a cleaned-up display if there are references that need replacing + const typeDisplay = typeReferences.length > 0 ? buildTypeDisplay(cellChildren) : undefined; + + return { type: cellChildren, typeReferences, typeDisplay }; } /** @@ -208,14 +251,15 @@ export function parseTableChildren(children: ReactNode): TableProperty[] { const name = extractText(nameCell?.props?.children); const requiredText = extractText(requiredCell?.props?.children).toLowerCase(); const description = descriptionCell?.props?.children; - const { type, typeReference } = extractTypeInfo(typeCell?.props?.children); + const { type, typeReferences, typeDisplay } = extractTypeInfo(typeCell?.props?.children); properties.push({ name: name.trim(), required: requiredText.includes('required') ? 'required' : 'optional', description, type, - typeReference, + typeReferences, + typeDisplay, }); } else if (cells.length === 3) { // 3 columns: Name | Description | Type @@ -225,13 +269,14 @@ export function parseTableChildren(children: ReactNode): TableProperty[] { const name = extractText(nameCell?.props?.children); const description = descriptionCell?.props?.children; - const { type, typeReference } = extractTypeInfo(typeCell?.props?.children); + const { type, typeReferences, typeDisplay } = extractTypeInfo(typeCell?.props?.children); properties.push({ name: name.trim(), description, type, - typeReference, + typeReferences, + typeDisplay, }); } else if (cells.length === 2) { // 2 columns: Name | Description @@ -244,6 +289,7 @@ export function parseTableChildren(children: ReactNode): TableProperty[] { properties.push({ name: name.trim(), description, + typeReferences: [], }); } }); From 8ffe982fe71dd4370c476852a3e83214a52ca39b Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:07:37 +0100 Subject: [PATCH 2/4] Add unit test for nested tables --- .../mdx/NestedTable/parseTable.test.tsx | 343 ++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 src/components/Layout/mdx/NestedTable/parseTable.test.tsx diff --git a/src/components/Layout/mdx/NestedTable/parseTable.test.tsx b/src/components/Layout/mdx/NestedTable/parseTable.test.tsx new file mode 100644 index 0000000000..8f82eda387 --- /dev/null +++ b/src/components/Layout/mdx/NestedTable/parseTable.test.tsx @@ -0,0 +1,343 @@ +import React, { createElement } from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { parseTableChildren } from './parseTable'; +import { NestedTablePropertyRow } from './NestedTablePropertyRow'; +import { NestedTableProvider, TableProperty, useNestedTable } from './NestedTableContext'; + +/** + * Mock component that simulates what MDX produces for
. + * getTypeName() checks displayName, which must be 'Table' or 'NestedTable'. + */ +const TableRef: React.FC<{ id?: string }> = () => null; +TableRef.displayName = 'NestedTable'; + +/** + * Builds a
element tree matching what MDX markdown tables produce. + * Each inner array is a row of cell contents. + */ +function buildTableElement(rows: React.ReactNode[][]) { + const colCount = rows[0]?.length ?? 0; + return createElement( + 'table', + null, + createElement( + 'thead', + null, + createElement( + 'tr', + null, + Array.from({ length: colCount }, (_, i) => createElement('th', { key: i }, `Header ${i}`)), + ), + ), + createElement( + 'tbody', + null, + rows.map((cells, rowIdx) => + createElement( + 'tr', + { key: rowIdx }, + cells.map((cell, cellIdx) => createElement('td', { key: cellIdx }, cell)), + ), + ), + ), + ); +} + +describe('parseTableChildren', () => { + describe('type reference extraction', () => { + it('extracts no references from plain text types', () => { + const table = buildTableElement([['name', 'Required', 'A description', 'String']]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].typeReferences).toEqual([]); + expect(result[0].typeDisplay).toBeUndefined(); + }); + + it('extracts a single Table element reference', () => { + const table = buildTableElement([ + ['name', 'Required', 'A description', createElement(TableRef, { id: 'PresenceData' })], + ]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].typeReferences).toEqual(['PresenceData']); + }); + + it('extracts a single reference alongside plain text', () => { + const typeCell = [createElement(TableRef, { id: 'PresenceData', key: 'ref' }), ' or String']; + const table = buildTableElement([['name', 'Required', 'A description', typeCell]]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].typeReferences).toEqual(['PresenceData']); + }); + + it('extracts two Table element references', () => { + const typeCell = [ + createElement(TableRef, { id: 'TypeA', key: 'a' }), + ' or ', + createElement(TableRef, { id: 'TypeB', key: 'b' }), + ]; + const table = buildTableElement([['name', 'Required', 'A description', typeCell]]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].typeReferences).toEqual(['TypeA', 'TypeB']); + }); + + it('extracts three Table element references', () => { + const typeCell = [ + createElement(TableRef, { id: 'TypeA', key: 'a' }), + ' or ', + createElement(TableRef, { id: 'TypeB', key: 'b' }), + ' or ', + createElement(TableRef, { id: 'TypeC', key: 'c' }), + ]; + const table = buildTableElement([['name', 'Required', 'A description', typeCell]]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].typeReferences).toEqual(['TypeA', 'TypeB', 'TypeC']); + }); + + it('extracts an implicit PascalCase reference when it is the sole type', () => { + const table = buildTableElement([['name', 'A description', 'RoomOptions']]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].typeReferences).toEqual(['RoomOptions']); + }); + + it('does not extract implicit references from non-matching text', () => { + const table = buildTableElement([['name', 'A description', 'string']]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].typeReferences).toEqual([]); + }); + + it('sets empty typeReferences for 2-column tables', () => { + const table = buildTableElement([['name', 'A description']]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].typeReferences).toEqual([]); + expect(result[0].type).toBeUndefined(); + }); + }); + + describe('typeDisplay', () => { + it('is undefined when there are no references', () => { + const table = buildTableElement([['name', 'Required', 'A description', 'String']]); + const result = parseTableChildren(table); + + expect(result[0].typeDisplay).toBeUndefined(); + }); + + it('is the ID string for a single Table reference', () => { + const table = buildTableElement([ + ['name', 'Required', 'A description', createElement(TableRef, { id: 'TypeA' })], + ]); + const result = parseTableChildren(table); + + expect(result[0].typeDisplay).toBe('TypeA'); + }); + + it('preserves text alongside Table references', () => { + const typeCell = [createElement(TableRef, { id: 'TypeA', key: 'ref' }), ' or String']; + const table = buildTableElement([['name', 'Required', 'A description', typeCell]]); + const result = parseTableChildren(table); + + expect(result[0].typeDisplay).toEqual(['TypeA', ' or String']); + }); + + it('replaces multiple Table elements with their IDs', () => { + const typeCell = [ + createElement(TableRef, { id: 'TypeA', key: 'a' }), + ' or ', + createElement(TableRef, { id: 'TypeB', key: 'b' }), + ]; + const table = buildTableElement([['name', 'Required', 'A description', typeCell]]); + const result = parseTableChildren(table); + + expect(result[0].typeDisplay).toEqual(['TypeA', ' or ', 'TypeB']); + }); + }); + + describe('column format handling', () => { + it('parses 4-column tables with required field', () => { + const table = buildTableElement([['myProp', 'Required', 'A description', 'String']]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('myProp'); + expect(result[0].required).toBe('required'); + }); + + it('parses 4-column tables with optional field', () => { + const table = buildTableElement([['myProp', 'Optional', 'A description', 'String']]); + const result = parseTableChildren(table); + + expect(result[0].required).toBe('optional'); + }); + + it('parses 3-column tables without required field', () => { + const table = buildTableElement([['myProp', 'A description', 'Number']]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('myProp'); + expect(result[0].required).toBeUndefined(); + }); + + it('parses multiple rows independently', () => { + const table = buildTableElement([ + ['prop1', 'Required', 'First prop', 'String'], + ['prop2', 'Optional', 'Second prop', createElement(TableRef, { id: 'TypeA' })], + ]); + const result = parseTableChildren(table); + + expect(result).toHaveLength(2); + expect(result[0].name).toBe('prop1'); + expect(result[0].typeReferences).toEqual([]); + expect(result[1].name).toBe('prop2'); + expect(result[1].typeReferences).toEqual(['TypeA']); + }); + }); +}); + +describe('NestedTablePropertyRow', () => { + /** + * Helper component that registers a table in the NestedTable context. + * Uses useEffect to mirror how real NestedTable components register. + */ + const RegisterTable: React.FC<{ id: string; properties: TableProperty[] }> = ({ id, properties }) => { + const { register } = useNestedTable(); + React.useEffect(() => { + register(id, { id, properties }); + }, [id, properties, register]); + return null; + }; + + const typeAProperties: TableProperty[] = [ + { name: 'fieldA', description: 'Field A description', typeReferences: [] }, + ]; + const typeBProperties: TableProperty[] = [ + { name: 'fieldB', description: 'Field B description', typeReferences: [] }, + ]; + + it('renders expand buttons for each resolved type reference', async () => { + const property: TableProperty = { + name: 'myProp', + description: 'A test property', + typeReferences: ['TypeA', 'TypeB'], + typeDisplay: 'TypeA or TypeB', + }; + + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /Show TypeA/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Show TypeB/ })).toBeInTheDocument(); + }); + }); + + it('expands each reference independently', async () => { + const property: TableProperty = { + name: 'myProp', + description: 'A test property', + typeReferences: ['TypeA', 'TypeB'], + typeDisplay: 'TypeA or TypeB', + }; + + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /Show TypeA/ })).toBeInTheDocument(); + }); + + // Expand TypeA + fireEvent.click(screen.getByRole('button', { name: /Show TypeA/ })); + + // TypeA is now expanded (Hide), TypeB is still collapsed (Show) + expect(screen.getByRole('button', { name: /Hide TypeA/ })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Show TypeB/ })).toBeInTheDocument(); + + // The expanded section shows TypeA's nested property + expect(screen.getByText('fieldA')).toBeInTheDocument(); + // TypeB's property is not visible + expect(screen.queryByText('fieldB')).not.toBeInTheDocument(); + }); + + it('only renders buttons for references that resolve to registered tables', async () => { + const property: TableProperty = { + name: 'myProp', + description: 'A test property', + typeReferences: ['TypeA', 'UnregisteredType'], + typeDisplay: 'TypeA or UnregisteredType', + }; + + render( + + + {/* UnregisteredType is intentionally NOT registered */} + + , + ); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /Show TypeA/ })).toBeInTheDocument(); + }); + + // No button for the unregistered type + expect(screen.queryByRole('button', { name: /Show UnregisteredType/ })).not.toBeInTheDocument(); + }); + + it('renders no buttons when there are no type references', () => { + const property: TableProperty = { + name: 'myProp', + description: 'A simple property', + type: 'String', + typeReferences: [], + }; + + render( + + + , + ); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('displays typeDisplay text when available', async () => { + const property: TableProperty = { + name: 'myProp', + description: 'A test property', + typeReferences: ['TypeA'], + typeDisplay: 'TypeA or String', + }; + + render( + + + + , + ); + + expect(screen.getByText('TypeA or String')).toBeInTheDocument(); + }); +}); From b488c28675655fb07796bc8dbbe8e99f8ce4a11f Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:08:07 +0100 Subject: [PATCH 3/4] tmp: chat presence api for testing --- src/pages/docs/chat/api/presence.mdx | 320 +++++++++++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 src/pages/docs/chat/api/presence.mdx diff --git a/src/pages/docs/chat/api/presence.mdx b/src/pages/docs/chat/api/presence.mdx new file mode 100644 index 0000000000..8c38c6aca4 --- /dev/null +++ b/src/pages/docs/chat/api/presence.mdx @@ -0,0 +1,320 @@ +--- +title: Presence +meta_description: "API reference for the Presence interface in the Ably Chat JavaScript SDK." +meta_keywords: "Ably Chat SDK, JavaScript, Presence API, enter, leave, update, get, isUserPresent, subscribe, online users" +--- + +The `Presence` interface provides methods for tracking which users are currently in a chat room. Access it via `room.presence`. + + +```javascript +const presence = room.presence; +``` + + +## Enter presence + +{`presence.enter(data?: PresenceData): Promise`} + +Enters the current user into the chat room presence set. This notifies other users that you have joined the room. + +The room must be attached before calling this method. + + +```javascript +await room.presence.enter({ status: 'online', nickname: 'Alice' }); +``` + + +### Parameters + +The `enter()` method takes the following parameters: + +
+ +| Parameter | Required | Description | Type | +| --- | --- | --- | --- | +| data | Optional | JSON-serializable data to associate with the user's presence. |
or String | +| data | Optional | JSON-serializable data to associate with the user's presence. |
or
or
| + +
+ +### Returns + +`Promise` - A promise that resolves when the user has successfully entered. + +## Update presence data + +{`presence.update(data?: PresenceData): Promise`} + +Updates the presence data for the current user in the chat room. Use this to change your status or other presence information without leaving and re-entering. + +The room must be attached before calling this method. + + +```javascript +await room.presence.update({ status: 'away', nickname: 'Alice' }); +``` + + +### Parameters + +The `update()` method takes the following parameters: + + + +| Parameter | Required | Description | Type | +| --- | --- | --- | --- | +| data | Optional | JSON-serializable data to replace the user's current presence data. |
| + +
+ +### Returns + +`Promise` - A promise that resolves when the presence data has been updated. + +## Leave presence
+ +{`presence.leave(data?: PresenceData): Promise`} + +Removes the current user from the chat room presence set. This notifies other users that you have left the room. + +The room must be attached before calling this method. + + +```javascript +await room.presence.leave({ status: 'offline' }); +``` + + +### Parameters + +The `leave()` method takes the following parameters: + + + +| Parameter | Required | Description | Type | +| --- | --- | --- | --- | +| data | Optional | Final presence data to include with the leave event. |
| + +
+ +### Returns + +`Promise` - A promise that resolves when the user has left the presence set. + +## Get current presence members
+ +{`presence.get(params?: RealtimePresenceParams): Promise`} + +Retrieves the current members present in the chat room. + +The room must be attached before calling this method. + + +```javascript +const members = await room.presence.get(); + +members.forEach(member => { + console.log(`${member.clientId} is ${member.data?.status}`); +}); +``` + + +### Parameters + +The `get()` method takes the following parameters: + + + +| Parameter | Required | Description | Type | +| --- | --- | --- | --- | +| params | Optional | Parameters to filter the presence set. | Ably.RealtimePresenceParams | + +
+ +### Returns + +`Promise` - A promise that resolves with an array of presence members currently in the room. + +## Check if a user is present
+ +{`presence.isUserPresent(clientId: string): Promise`} + +Checks whether a specific user is currently present in the chat room. + +The room must be attached before calling this method. + + +```javascript +const isAliceHere = await room.presence.isUserPresent('alice-123'); +console.log('Alice is present:', isAliceHere); +``` + + +### Parameters + +The `isUserPresent()` method takes the following parameters: + + + +| Parameter | Required | Description | Type | +| --- | --- | --- | --- | +| clientId | Required | The client ID of the user to check. | String | + +
+ +### Returns + +`Promise` - A promise that resolves with `true` if the user is present, `false` otherwise. + +## Subscribe to presence events
+ +{`presence.subscribe(listener: PresenceListener): Subscription`} + +Subscribes to all presence events in the chat room. Receive notifications when users enter, leave, or update their presence data. + +Requires `enableEvents` to be `true` in the room's presence options (this is the default). + + +```javascript +const { unsubscribe } = room.presence.subscribe((event) => { + const member = event.member; + switch (event.type) { + case 'enter': + console.log(`${member.clientId} joined`); + break; + case 'leave': + console.log(`${member.clientId} left`); + break; + case 'update': + console.log(`${member.clientId} updated their status`); + break; + case 'present': + console.log(`${member.clientId} is present`); + break; + } +}); + +// To stop receiving presence events +unsubscribe(); +``` + + +You can also subscribe to specific event types: + + +```javascript +// Subscribe only to enter and leave events +const { unsubscribe } = room.presence.subscribe(['enter', 'leave'], (event) => { + console.log(`${event.member.clientId} ${event.type === 'enter' ? 'joined' : 'left'}`); +}); +``` + + +### Parameters + +The `subscribe()` method takes the following parameters: + + + +| Parameter | Required | Description | Type | +| --- | --- | --- | --- | +| listener | Required | Callback function invoked when any presence event occurs. |
| + +
+ + + +| Property | Description | Type | +| --- | --- | --- | +| type | The type of presence event. | | +| member | The presence member associated with this event. |
| + +
+ + + +| Value | Description | +| --- | --- | +| Enter | A user has entered the presence set. | +| Leave | A user has left the presence set. | +| Update | A user has updated their presence data. | +| Present | Indicates a user is present (received during initial sync). | + + + + + +| Property | Description | Type | +| --- | --- | --- | +| clientId | The client ID of the present user. | String | +| connectionId | The connection ID of the present user. | String | +| data | The presence data associated with the user. | or Undefined | +| extras | Additional data included with the presence message. | JsonObject or Undefined | +| updatedAt | When this presence state was last updated. | Date | + +
+ + + +`PresenceData` is a type alias for `JsonObject`. It represents data that can be entered into presence as an object literal. + + + +Or when subscribing to specific events: + + + +| Parameter | Required | Description | Type | +| --- | --- | --- | --- | +| events | Required | The event type(s) to subscribe to. |
or PresenceEventType[] | +| listener | Required | Callback function invoked when matching presence events occur. | (event:
) =\> void | + +
+ +### Returns + +`{ unsubscribe: () => void }` - A subscription object with an `unsubscribe` method. + +## Example + + +```javascript +const room = await chatClient.rooms.get('my-room', { + presence: { enableEvents: true } +}); + +await room.attach(); + +// Subscribe to presence events +const { unsubscribe } = room.presence.subscribe((event) => { + const member = event.member; + console.log(`${event.type}: ${member.clientId}`); + if (member.data) { + console.log('Data:', member.data); + } +}); + +// Enter the room with custom data +await room.presence.enter({ + status: 'online', + nickname: 'Alice', + avatar: 'https://example.com/alice.png' +}); + +// Get everyone currently in the room +const members = await room.presence.get(); +console.log(`${members.length} users in room`); + +// Update your status +await room.presence.update({ status: 'away' }); + +// Check if a specific user is present +const isBobHere = await room.presence.isUserPresent('bob-456'); + +// Leave when done +await room.presence.leave(); +unsubscribe(); +``` + From 780c9602f2b2616fd0c4462fec570d42d39f2d4b Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:16:18 +0100 Subject: [PATCH 4/4] fixup! Add unit test for nested tables --- src/components/Layout/mdx/NestedTable/parseTable.test.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/Layout/mdx/NestedTable/parseTable.test.tsx b/src/components/Layout/mdx/NestedTable/parseTable.test.tsx index 8f82eda387..4d386c8b6e 100644 --- a/src/components/Layout/mdx/NestedTable/parseTable.test.tsx +++ b/src/components/Layout/mdx/NestedTable/parseTable.test.tsx @@ -220,12 +220,8 @@ describe('NestedTablePropertyRow', () => { return null; }; - const typeAProperties: TableProperty[] = [ - { name: 'fieldA', description: 'Field A description', typeReferences: [] }, - ]; - const typeBProperties: TableProperty[] = [ - { name: 'fieldB', description: 'Field B description', typeReferences: [] }, - ]; + const typeAProperties: TableProperty[] = [{ name: 'fieldA', description: 'Field A description', typeReferences: [] }]; + const typeBProperties: TableProperty[] = [{ name: 'fieldB', description: 'Field B description', typeReferences: [] }]; it('renders expand buttons for each resolved type reference', async () => { const property: TableProperty = {