Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions code/core/src/docs-tools/argTypes/convert/flow/convert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';

import { convert } from './convert.ts';

describe('Flow convert', () => {
describe('union', () => {
it('converts a union of literals to an enum', () => {
expect(
convert({
name: 'union',
elements: [
{ name: 'literal', value: "'sm'" },
{ name: 'literal', value: "'md'" },
{ name: 'literal', value: "'lg'" },
],
})
).toEqual({ name: 'enum', value: ['sm', 'md', 'lg'] });
});

it('converts an optional literal union (?type) to an enum by filtering void', () => {
// Flow represents `?('sm' | 'md' | 'lg')` as a union containing void and null
expect(
convert({
name: 'union',
elements: [
{ name: 'literal', value: "'sm'" },
{ name: 'literal', value: "'md'" },
{ name: 'literal', value: "'lg'" },
{ name: 'void' },
{ name: 'null' },
],
})
).toEqual({ name: 'enum', value: ['sm', 'md', 'lg'] });
});

it('falls back to union when elements are not all literals after filtering', () => {
expect(
convert({
name: 'union',
elements: [
{ name: 'literal', value: "'sm'" },
{ name: 'string' },
],
})
).toMatchObject({ name: 'union' });
});

it('falls back to union when only void/null remain after filtering', () => {
expect(
convert({
name: 'union',
elements: [{ name: 'void' }, { name: 'null' }],
})
).toMatchObject({ name: 'union' });
});
});
});
12 changes: 9 additions & 3 deletions code/core/src/docs-tools/argTypes/convert/flow/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,17 @@ export const convert = (type: FlowType): SBType | void => {
}
case 'signature':
return { ...base, ...convertSig(type) };
case 'union':
if (type.elements?.every(isLiteral)) {
return { ...base, name: 'enum', value: type.elements?.map(toEnumOption) };
case 'union': {
// Filter out Flow's nullable markers (void, null) so optional unions like
// ?('sm' | 'md' | 'lg') still resolve to an enum control.
const meaningfulElements = type.elements?.filter(
(element) => element.name !== 'void' && element.name !== 'null'
);
if (meaningfulElements && meaningfulElements.length > 0 && meaningfulElements.every(isLiteral)) {
return { ...base, name: 'enum', value: meaningfulElements.map(toEnumOption) };
}
return { ...base, name, value: type.elements?.map(convert) };
}

case 'intersection':
return { ...base, name, value: type.elements?.map(convert) };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,37 @@ describe('normalizeInputType', () => {
defaultValue: 'defaultValue',
});
});

it('hoists options nested inside control object to top level', () => {
expect(
normalizeInputType(
{
control: { type: 'select', options: ['sm', 'md', 'lg'] } as any,
},
'arg'
)
).toEqual({
name: 'arg',
options: ['sm', 'md', 'lg'],
control: { type: 'select', disable: false },
});
});

it('preserves top-level options when control also has options', () => {
expect(
normalizeInputType(
{
options: ['xs', 'sm'],
control: { type: 'select', options: ['sm', 'md', 'lg'] } as any,
},
'arg'
)
).toEqual({
name: 'arg',
options: ['xs', 'sm'],
control: { type: 'select', options: ['sm', 'md', 'lg'], disable: false },
});
});
});

describe('normalizeInputTypes', () => {
Expand Down
26 changes: 22 additions & 4 deletions code/core/src/preview-api/modules/store/csf/normalizeInputTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,35 @@ const normalizeControl = (control: InputType['control']): StrictInputType['contr

export const normalizeInputType = (inputType: InputType, key: string): StrictInputType => {
const { type, control, ...rest } = inputType;

// Hoist options nested inside the control object to the top level when the
// user writes { control: { type: 'select', options: [...] } } instead of the
// documented { control: { type: 'select' }, options: [...] } form.
let effectiveControl = control;
const effectiveRest = { ...rest };
if (
!effectiveRest.options &&
control &&
typeof control === 'object' &&
!Array.isArray(control) &&
'options' in control
) {
const { options, ...controlWithoutOptions } = control as Record<string, unknown>;
effectiveRest.options = options as InputType['options'];
effectiveControl = controlWithoutOptions as InputType['control'];
}

const normalized: StrictInputType = {
name: key,
...rest,
...effectiveRest,
};

if (type) {
normalized.type = normalizeType(type);
}
if (control) {
normalized.control = normalizeControl(control);
} else if (control === false) {
if (effectiveControl) {
normalized.control = normalizeControl(effectiveControl);
} else if (effectiveControl === false) {
normalized.control = { disable: true };
}
return normalized;
Expand Down
Loading