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'