diff --git a/.changeset/cozy-areas-ask.md b/.changeset/cozy-areas-ask.md
new file mode 100644
index 0000000000..5567d64356
--- /dev/null
+++ b/.changeset/cozy-areas-ask.md
@@ -0,0 +1,13 @@
+---
+"@ultraviolet/ui": patch
+---
+
+`InfoTable.Cell`:
+- should not override the custom style of its children.
+- when children are an array of strings or numbers, the same style is applied as for a single string.
+
+`Text`: updated tooltip detection logic to handle overflowing content when children are of type `(string | number)[]`. Such arrays are now converted to strings for tooltip display. For example:
+```js
+Ready?{isReady ? "Yes!" : "No..."}
+```
+Previously, no tooltip appeared when overflowing since the computed child was `['Ready?', 'Yes!']` (or `['Ready?', 'No...']`). Now, the tooltip correctly displays `Ready? Yes!`.
diff --git a/packages/ui/src/compositions/InfoTable/components/Cell.tsx b/packages/ui/src/compositions/InfoTable/components/Cell.tsx
index a94c4970bf..e7e94e6ef1 100644
--- a/packages/ui/src/compositions/InfoTable/components/Cell.tsx
+++ b/packages/ui/src/compositions/InfoTable/components/Cell.tsx
@@ -5,6 +5,7 @@ import { useContext } from 'react'
import type { CSSProperties, ReactNode } from 'react'
import { Stack } from '../../../components/Stack'
import { Text } from '../../../components/Text'
+import { isStringOrNumberArray } from '../../../helpers/isStringOrNumberArray'
import { InfoTableContext } from '../Context'
import { infoTableStyle } from '../styles.css'
@@ -31,7 +32,10 @@ export const InfoTableCell = ({ children, title, multiline = false, style }: Cel
*`, {
+globalStyle(`:where(${desc}) > *`, {
marginRight: theme.space[1],
minWidth: 0,
maxWidth: '100%',
diff --git a/packages/ui/src/helpers/__tests__/index.test.ts b/packages/ui/src/helpers/__tests__/index.test.ts
index 8c27bc0a19..74e0a7404d 100644
--- a/packages/ui/src/helpers/__tests__/index.test.ts
+++ b/packages/ui/src/helpers/__tests__/index.test.ts
@@ -1,6 +1,7 @@
import type { KeyboardEvent } from 'react'
// oxlint-disable typescript/no-unsafe-type-assertion
import { describe, expect, it, vi } from 'vitest'
+import { isStringOrNumberArray } from '../isStringOrNumberArray'
import onKeyOnlyNumbers from '../keycode'
import parseIntOr from '../numbers'
import recursivelyGetChildrenString from '../recursivelyGetChildrenString'
@@ -19,10 +20,10 @@ describe(recursivelyGetChildrenString, () => {
it.each`
test | value | expected
${'is bare string'} | ${'hello'} | ${'hello'}
- ${'is array'} | ${['hello', 'world']} | ${''}
+ ${'is array'} | ${['hello', 'world']} | ${'hello world'}
${'is Boolean'} | ${true} | ${''}
${'is complex children with a nested string children'} | ${complexChildrenWithStringNestedChildren} | ${'hello'}
- ${'is complex children with a nested array children'} | ${complexChildrenWithArrayNestedChildren} | ${''}
+ ${'is complex children with a nested array children'} | ${complexChildrenWithArrayNestedChildren} | ${'hello'}
${'is complex children without a nested string children'} | ${complexChildrenWithoutStringNestedChildren} | ${''}
`('returns "$expected" when $test', current => {
expect(recursivelyGetChildrenString(current.value as string)).toBe(current.expected)
@@ -64,4 +65,20 @@ describe(parseIntOr, () => {
`('returns $expected when $test', current => {
expect(parseIntOr(current.value as string, fallback)).toBe(current.expected)
})
+
+ describe(isStringOrNumberArray, () => {
+ it.each`
+ test | value | expected
+ ${'is number[]'} | ${[1, 2]} | ${true}
+ ${'is string[]'} | ${['hello', 'world']} | ${true}
+ ${'is (number | string)[]'} | ${['', 'true', 1, 0]} | ${true}
+ ${'is empty'} | ${[]} | ${true}
+ ${'is (string | undefined)[]'} | ${['test', undefined]} | ${false}
+ ${'is string'} | ${'test'} | ${false}
+ ${'is number'} | ${1} | ${false}
+ ${'is (string[] | string)[]'} | ${['string', ['string']]} | ${false}
+ `('returns "$expected" when $test', current => {
+ expect(isStringOrNumberArray(current.value as string)).toBe(current.expected)
+ })
+ })
})
diff --git a/packages/ui/src/helpers/isStringOrNumberArray.ts b/packages/ui/src/helpers/isStringOrNumberArray.ts
new file mode 100644
index 0000000000..85d0a6eb2a
--- /dev/null
+++ b/packages/ui/src/helpers/isStringOrNumberArray.ts
@@ -0,0 +1,4 @@
+export const isStringOrNumberArray = (value: unknown): value is (string | number)[] =>
+ Array.isArray(value)
+ ? value.every((item): item is string | number => typeof item === 'string' || typeof item === 'number')
+ : false
diff --git a/packages/ui/src/helpers/recursivelyGetChildrenString.ts b/packages/ui/src/helpers/recursivelyGetChildrenString.ts
index 20db87418d..f0d19dfda2 100644
--- a/packages/ui/src/helpers/recursivelyGetChildrenString.ts
+++ b/packages/ui/src/helpers/recursivelyGetChildrenString.ts
@@ -1,12 +1,22 @@
import type { ReactNode } from 'react'
+import { isStringOrNumberArray } from './isStringOrNumberArray'
const recursivelyGetChildrenString = (children: ReactNode): string => {
- if (typeof children === 'string') {
- return children
+ if (typeof children === 'string' || typeof children === 'number') {
+ return String(children)
+ }
+
+ if (isStringOrNumberArray(children)) {
+ return children.join(' ')
}
+
if (Array.isArray(children)) {
- return ''
- } // We can't determine which string to display in tooltip
+ return children
+ .map(child => recursivelyGetChildrenString(child))
+ .filter(Boolean)
+ .join(' ')
+ }
+
if (typeof children === 'object') {
const childProps = ((children as unknown as Record)?.['props'] as Record)?.[
'children'