Skip to content

Commit ff2a7b8

Browse files
feat(combinator): DLT-3470 render DtToggle mixed state as a segmented control (#1309)
1 parent e0af275 commit ff2a7b8

4 files changed

Lines changed: 88 additions & 9 deletions

File tree

apps/dialtone-documentation/docs/components/toggle.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ The Toggle component acts as a way to allow the User to switch between two mutua
4646
<dt-toggle :model-value="true" class="d-g-200">Checked Toggle</dt-toggle>
4747
<dt-toggle disabled class="d-g-200">Unchecked Disabled</dt-toggle>
4848
<dt-toggle :model-value="true" disabled class="d-g-200">Checked Disabled</dt-toggle>
49-
<dt-toggle :model-value="mixed" class="d-g-200">Indeterminate Toggle</dt-toggle>
50-
<dt-toggle :model-value="mixed" class="d-g-200" disabled>Indeterminate Disabled</dt-toggle>
49+
<dt-toggle :model-value="'mixed'" class="d-g-200">Indeterminate Toggle</dt-toggle>
50+
<dt-toggle :model-value="'mixed'" class="d-g-200" disabled>Indeterminate Disabled</dt-toggle>
5151
<dt-toggle class="d-g-200" :show-icon="false">Without icon</dt-toggle>
5252
</dt-stack>
5353
```

packages/combinator/src/lib/control.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,24 @@ export function getControlByValue (value) {
108108
}
109109

110110
export function getControlByMemberType (type, args) {
111+
const values = args?.values ?? [];
112+
113+
if (isMixedBooleanEnum(values)) return 'segmented';
111114
if (type === 'boolean') return 'boolean';
112-
if (args?.values?.length > 0) {
113-
if (shouldUseSegmented(args.values)) return 'segmented';
114-
return 'selection';
115-
}
116-
switch (type) {
117-
case 'string': return 'string';
118-
default: return type;
115+
116+
if (values.length > 0) {
117+
return shouldUseSegmented(values) ? 'segmented' : 'selection';
119118
}
119+
120+
return type === 'string' ? 'string' : type;
120121
}
121122

123+
function isMixedBooleanEnum (values) {
124+
return values.length === 3 &&
125+
values.includes(true) &&
126+
values.includes(false) &&
127+
values.includes('mixed');
128+
}
122129
function shouldUseSegmented (values) {
123130
return values.length <= MAX_SEGMENTED_COUNT &&
124131
values.every(v => String(v).length <= MAX_SEGMENTED_LABEL_LENGTH);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { getControlByMemberType } from './control';
3+
import { extendMember } from './info_extend';
4+
5+
describe('control.js test', function () {
6+
describe('getControlByMemberType', function () {
7+
it('uses the boolean control for a plain boolean', function () {
8+
expect(getControlByMemberType('boolean')).toBe('boolean');
9+
});
10+
11+
it('uses the boolean control for a nullable boolean enum', function () {
12+
expect(getControlByMemberType('boolean', { values: ['null', true, false] })).toBe('boolean');
13+
});
14+
15+
it('uses the segmented control for the mixed boolean enum', function () {
16+
expect(getControlByMemberType('boolean', { values: [true, false, 'mixed'] })).toBe('segmented');
17+
});
18+
19+
it('uses the segmented control for short string values', function () {
20+
expect(getControlByMemberType('string', { values: ['sm', 'md', 'lg'] })).toBe('segmented');
21+
});
22+
23+
it('uses the selection control for longer string values', function () {
24+
expect(getControlByMemberType('string', { values: ['label', 'count'] })).toBe('selection');
25+
});
26+
});
27+
28+
describe('docgen value normalization', function () {
29+
it('normalizes a mixed boolean enum to runtime values', function () {
30+
const member = { type: { name: 'boolean|string' }, values: ['true', 'false', '\'mixed\''] };
31+
32+
extendMember(member);
33+
34+
expect(member.values).toEqual([true, false, 'mixed']);
35+
});
36+
37+
it('leaves non-boolean string enums untouched', function () {
38+
const member = { type: { name: 'string' }, values: ['\'body\'', '\'parent\''] };
39+
40+
extendMember(member);
41+
42+
expect(member.values).toEqual(['\'body\'', '\'parent\'']);
43+
});
44+
});
45+
});

packages/combinator/src/lib/info_extend.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function extendMember (member) {
1818
member.types = extractMemberTypes(typeString);
1919
}
2020
}
21+
normalizeDocValues(member);
2122
if (member.name) {
2223
member.label = paramCase(member.name);
2324
}
@@ -54,6 +55,32 @@ function extractMemberTypes (typeString) {
5455
return typeString.split('|').map(type => type.trim().toLowerCase());
5556
}
5657

58+
function normalizeDocValues (member) {
59+
if (!member.values) return;
60+
member.values = member.values.map(value => normalizeDocValue(value, member.types));
61+
}
62+
63+
// Only boolean-typed enums are normalized — 'true'/'false' become booleans and the
64+
// quoted 'mixed' sentinel is unwrapped so DtToggle routes to a segmented control.
65+
// Skipping everything else leaves string enums and numeric token strings ('200') intact.
66+
// (parseDocValue/JSON5 isn't reused here precisely because it would coerce '200' to a number.)
67+
function normalizeDocValue (value, types = []) {
68+
if (typeof value !== 'string' || !types.includes('boolean')) return value;
69+
70+
if (value === 'true') return true;
71+
if (value === 'false') return false;
72+
if (isQuotedString(value)) return value.slice(1, -1);
73+
74+
return value;
75+
}
76+
77+
function isQuotedString (value) {
78+
return (
79+
(value.startsWith('\'') && value.endsWith('\'')) ||
80+
(value.startsWith('"') && value.endsWith('"'))
81+
);
82+
}
83+
5784
/**
5885
* Extends an event member.
5986
*

0 commit comments

Comments
 (0)