@astryxdesign/lab 0.7.4-canary.3f6ea7d → 0.7.4-canary.88ec018
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/blocks/ListInputBudgetAllocation.doc.mjs +14 -0
- package/blocks/ListInputBudgetAllocation.tsx +132 -0
- package/blocks/ListInputFamilyMembers.doc.mjs +14 -0
- package/blocks/ListInputFamilyMembers.tsx +164 -0
- package/blocks/ListInputMailingList.doc.mjs +14 -0
- package/blocks/ListInputMailingList.tsx +115 -0
- package/blocks/ListInputShowcase.doc.mjs +15 -0
- package/blocks/ListInputShowcase.tsx +139 -0
- package/dist/ListInput/ListInput.d.ts +1 -1
- package/dist/ListInput/ListInput.d.ts.map +1 -1
- package/dist/ListInput/ListInput.js +161 -129
- package/dist/lab.css +6 -3
- package/package.json +4 -4
- package/src/ListInput/ListInput.doc.mjs +252 -7
- package/src/ListInput/ListInput.test.tsx +183 -3
- package/src/ListInput/ListInput.tsx +231 -166
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export default {
|
|
5
|
+
type: 'block',
|
|
6
|
+
name: 'Budget allocation',
|
|
7
|
+
displayName: 'Budget allocation',
|
|
8
|
+
description:
|
|
9
|
+
'Mixed control types in one record: a team Selector in a proportional column beside a percentage NumberInput in a fixed 132px column. Each team can appear once (a record error), the rows must total 100% (a list error), and Add stops at four teams.',
|
|
10
|
+
exampleFor: 'ListInput',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['ListInput', 'Selector', 'NumberInput', 'VStack'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @input Controlled team allocation records
|
|
7
|
+
* @output A ListInput pairing a team Selector with a fixed-width percentage
|
|
8
|
+
* NumberInput, checking that teams are unique and the total is 100%
|
|
9
|
+
* @position Copyable Lab ListInput example
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useState} from 'react';
|
|
13
|
+
import {VStack} from '@astryxdesign/core/Layout';
|
|
14
|
+
import {NumberInput} from '@astryxdesign/core/NumberInput';
|
|
15
|
+
import {Selector} from '@astryxdesign/core/Selector';
|
|
16
|
+
import {pixel, proportional} from '@astryxdesign/core/Table';
|
|
17
|
+
import {ListInput, type ListInputColumn} from '@astryxdesign/lab';
|
|
18
|
+
|
|
19
|
+
type Allocation = {id: string; team: string; percent: number | null};
|
|
20
|
+
|
|
21
|
+
const TEAM_OPTIONS = [
|
|
22
|
+
{value: 'design', label: 'Design'},
|
|
23
|
+
{value: 'engineering', label: 'Engineering'},
|
|
24
|
+
{value: 'marketing', label: 'Marketing'},
|
|
25
|
+
{value: 'operations', label: 'Operations'},
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const COLUMNS: ListInputColumn<Allocation>[] = [
|
|
29
|
+
{
|
|
30
|
+
key: 'team',
|
|
31
|
+
header: 'Team',
|
|
32
|
+
width: proportional(2, {minWidth: 180}),
|
|
33
|
+
renderInput: ({
|
|
34
|
+
item,
|
|
35
|
+
label,
|
|
36
|
+
isLabelHidden,
|
|
37
|
+
status,
|
|
38
|
+
statusVariant,
|
|
39
|
+
isDisabled,
|
|
40
|
+
isLoading,
|
|
41
|
+
updateItem,
|
|
42
|
+
}) => (
|
|
43
|
+
<Selector
|
|
44
|
+
label={label}
|
|
45
|
+
isLabelHidden={isLabelHidden}
|
|
46
|
+
options={TEAM_OPTIONS}
|
|
47
|
+
value={item.team}
|
|
48
|
+
placeholder="Choose a team"
|
|
49
|
+
onChange={team => updateItem({...item, team}, 'team')}
|
|
50
|
+
status={status}
|
|
51
|
+
statusVariant={statusVariant}
|
|
52
|
+
isDisabled={isDisabled || isLoading}
|
|
53
|
+
/>
|
|
54
|
+
),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
key: 'percent',
|
|
58
|
+
header: 'Allocation',
|
|
59
|
+
width: pixel(132),
|
|
60
|
+
renderInput: ({
|
|
61
|
+
item,
|
|
62
|
+
label,
|
|
63
|
+
isLabelHidden,
|
|
64
|
+
status,
|
|
65
|
+
statusVariant,
|
|
66
|
+
isDisabled,
|
|
67
|
+
isLoading,
|
|
68
|
+
updateItem,
|
|
69
|
+
}) => (
|
|
70
|
+
<NumberInput
|
|
71
|
+
label={label}
|
|
72
|
+
isLabelHidden={isLabelHidden}
|
|
73
|
+
value={item.percent}
|
|
74
|
+
onChange={percent => updateItem({...item, percent}, 'percent')}
|
|
75
|
+
status={status}
|
|
76
|
+
statusVariant={statusVariant}
|
|
77
|
+
isDisabled={isDisabled || isLoading}
|
|
78
|
+
min={0}
|
|
79
|
+
max={100}
|
|
80
|
+
units="%"
|
|
81
|
+
isIntegerOnly
|
|
82
|
+
/>
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
export default function ListInputBudgetAllocation() {
|
|
88
|
+
const [allocations, setAllocations] = useState<Allocation[]>([
|
|
89
|
+
{id: 'design', team: 'design', percent: 35},
|
|
90
|
+
{id: 'engineering', team: 'engineering', percent: 40},
|
|
91
|
+
{id: 'marketing', team: 'marketing', percent: 15},
|
|
92
|
+
]);
|
|
93
|
+
const total = allocations.reduce(
|
|
94
|
+
(sum, allocation) => sum + (allocation.percent ?? 0),
|
|
95
|
+
0,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<VStack width={680} maxWidth="100%">
|
|
100
|
+
<ListInput<Allocation>
|
|
101
|
+
label="Quarterly budget allocation"
|
|
102
|
+
description="Assign each team once. The percentages must total 100%."
|
|
103
|
+
value={allocations}
|
|
104
|
+
onChange={setAllocations}
|
|
105
|
+
getItemKey={allocation => allocation.id}
|
|
106
|
+
createItem={() => ({id: crypto.randomUUID(), team: '', percent: null})}
|
|
107
|
+
columns={COLUMNS}
|
|
108
|
+
itemName="allocation"
|
|
109
|
+
isRequired
|
|
110
|
+
maxItems={4}
|
|
111
|
+
status={
|
|
112
|
+
total === 100
|
|
113
|
+
? undefined
|
|
114
|
+
: {
|
|
115
|
+
type: 'error',
|
|
116
|
+
message: `Allocations must total 100%. Current total: ${total}%.`,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
getItemStatus={allocation =>
|
|
120
|
+
allocation.team !== '' &&
|
|
121
|
+
allocations.some(
|
|
122
|
+
candidate =>
|
|
123
|
+
candidate.id !== allocation.id &&
|
|
124
|
+
candidate.team === allocation.team,
|
|
125
|
+
)
|
|
126
|
+
? {type: 'error', message: 'Each team can only appear once.'}
|
|
127
|
+
: undefined
|
|
128
|
+
}
|
|
129
|
+
/>
|
|
130
|
+
</VStack>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export default {
|
|
5
|
+
type: 'block',
|
|
6
|
+
name: 'Family member declaration',
|
|
7
|
+
displayName: 'Family member declaration',
|
|
8
|
+
description:
|
|
9
|
+
'Three simple fields per record, the recommended ceiling: a full legal name, a relationship Selector, and a DateInput in a fixed 176px column. Order is immaterial, so the list is not reorderable. At 640px or narrower each record stacks its fields with repeated labels.',
|
|
10
|
+
exampleFor: 'ListInput',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['ListInput', 'TextInput', 'Selector', 'DateInput', 'VStack'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @input Controlled family member records
|
|
7
|
+
* @output A three-field ListInput (name, relationship, date of birth) whose
|
|
8
|
+
* records stack their fields when the list is narrow
|
|
9
|
+
* @position Copyable Lab ListInput example
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useState} from 'react';
|
|
13
|
+
import type {ISODateString} from '@astryxdesign/core/Calendar';
|
|
14
|
+
import {DateInput} from '@astryxdesign/core/DateInput';
|
|
15
|
+
import {VStack} from '@astryxdesign/core/Layout';
|
|
16
|
+
import {Selector} from '@astryxdesign/core/Selector';
|
|
17
|
+
import {pixel, proportional} from '@astryxdesign/core/Table';
|
|
18
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
19
|
+
import {ListInput, type ListInputColumn} from '@astryxdesign/lab';
|
|
20
|
+
|
|
21
|
+
type FamilyMember = {
|
|
22
|
+
id: string;
|
|
23
|
+
fullName: string;
|
|
24
|
+
relationship: string;
|
|
25
|
+
dateOfBirth: ISODateString | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const RELATIONSHIP_OPTIONS = [
|
|
29
|
+
{value: 'spouse-or-partner', label: 'Spouse or partner'},
|
|
30
|
+
{value: 'child', label: 'Child'},
|
|
31
|
+
{value: 'parent', label: 'Parent'},
|
|
32
|
+
{value: 'sibling', label: 'Sibling'},
|
|
33
|
+
{value: 'other-dependent', label: 'Other dependent'},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const COLUMNS: ListInputColumn<FamilyMember>[] = [
|
|
37
|
+
{
|
|
38
|
+
key: 'fullName',
|
|
39
|
+
header: 'Full legal name',
|
|
40
|
+
width: proportional(2, {minWidth: 200}),
|
|
41
|
+
renderInput: ({
|
|
42
|
+
item,
|
|
43
|
+
label,
|
|
44
|
+
isLabelHidden,
|
|
45
|
+
status,
|
|
46
|
+
statusVariant,
|
|
47
|
+
isDisabled,
|
|
48
|
+
isLoading,
|
|
49
|
+
updateItem,
|
|
50
|
+
}) => (
|
|
51
|
+
<TextInput
|
|
52
|
+
label={label}
|
|
53
|
+
isLabelHidden={isLabelHidden}
|
|
54
|
+
value={item.fullName}
|
|
55
|
+
onChange={fullName => updateItem({...item, fullName}, 'fullName')}
|
|
56
|
+
status={status}
|
|
57
|
+
statusVariant={statusVariant}
|
|
58
|
+
isDisabled={isDisabled}
|
|
59
|
+
isLoading={isLoading}
|
|
60
|
+
/>
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: 'relationship',
|
|
65
|
+
header: 'Relationship',
|
|
66
|
+
width: proportional(1.25, {minWidth: 160}),
|
|
67
|
+
renderInput: ({
|
|
68
|
+
item,
|
|
69
|
+
label,
|
|
70
|
+
isLabelHidden,
|
|
71
|
+
status,
|
|
72
|
+
statusVariant,
|
|
73
|
+
isDisabled,
|
|
74
|
+
isLoading,
|
|
75
|
+
updateItem,
|
|
76
|
+
}) => (
|
|
77
|
+
<Selector
|
|
78
|
+
label={label}
|
|
79
|
+
isLabelHidden={isLabelHidden}
|
|
80
|
+
options={RELATIONSHIP_OPTIONS}
|
|
81
|
+
value={item.relationship}
|
|
82
|
+
placeholder="Choose relationship"
|
|
83
|
+
onChange={relationship =>
|
|
84
|
+
updateItem({...item, relationship}, 'relationship')
|
|
85
|
+
}
|
|
86
|
+
status={status}
|
|
87
|
+
statusVariant={statusVariant}
|
|
88
|
+
isDisabled={isDisabled || isLoading}
|
|
89
|
+
/>
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
key: 'dateOfBirth',
|
|
94
|
+
header: 'Date of birth',
|
|
95
|
+
width: pixel(176),
|
|
96
|
+
renderInput: ({
|
|
97
|
+
item,
|
|
98
|
+
label,
|
|
99
|
+
isLabelHidden,
|
|
100
|
+
status,
|
|
101
|
+
statusVariant,
|
|
102
|
+
isDisabled,
|
|
103
|
+
isLoading,
|
|
104
|
+
updateItem,
|
|
105
|
+
}) => (
|
|
106
|
+
<DateInput
|
|
107
|
+
label={label}
|
|
108
|
+
isLabelHidden={isLabelHidden}
|
|
109
|
+
value={item.dateOfBirth}
|
|
110
|
+
onChange={dateOfBirth =>
|
|
111
|
+
updateItem({...item, dateOfBirth}, 'dateOfBirth')
|
|
112
|
+
}
|
|
113
|
+
status={status}
|
|
114
|
+
statusVariant={statusVariant}
|
|
115
|
+
isDisabled={isDisabled}
|
|
116
|
+
isLoading={isLoading}
|
|
117
|
+
/>
|
|
118
|
+
),
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
export default function ListInputFamilyMembers() {
|
|
123
|
+
const [members, setMembers] = useState<FamilyMember[]>([
|
|
124
|
+
{
|
|
125
|
+
id: 'jordan',
|
|
126
|
+
fullName: 'Jordan Lee',
|
|
127
|
+
relationship: 'spouse-or-partner',
|
|
128
|
+
dateOfBirth: '1988-11-02',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'alexandria',
|
|
132
|
+
fullName: 'Alexandria María de la Cruz',
|
|
133
|
+
relationship: 'child',
|
|
134
|
+
dateOfBirth: '2012-06-24',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'grace',
|
|
138
|
+
fullName: 'Grace Lee',
|
|
139
|
+
relationship: 'parent',
|
|
140
|
+
dateOfBirth: '1958-04-17',
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<VStack width={680} maxWidth="100%">
|
|
146
|
+
<ListInput<FamilyMember>
|
|
147
|
+
label="Family member declaration"
|
|
148
|
+
description="Declare each immediate family member, their relationship to you, and their date of birth."
|
|
149
|
+
value={members}
|
|
150
|
+
onChange={setMembers}
|
|
151
|
+
getItemKey={member => member.id}
|
|
152
|
+
createItem={() => ({
|
|
153
|
+
id: crypto.randomUUID(),
|
|
154
|
+
fullName: '',
|
|
155
|
+
relationship: '',
|
|
156
|
+
dateOfBirth: undefined,
|
|
157
|
+
})}
|
|
158
|
+
columns={COLUMNS}
|
|
159
|
+
itemName="family member"
|
|
160
|
+
isRequired
|
|
161
|
+
/>
|
|
162
|
+
</VStack>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export default {
|
|
5
|
+
type: 'block',
|
|
6
|
+
name: 'Mailing list',
|
|
7
|
+
displayName: 'Mailing list',
|
|
8
|
+
description:
|
|
9
|
+
'A one-column list that starts empty, shows the empty state with Add still available, and stops adding at six addresses. A repeated address is a record error, a malformed address is a field error after blur, and removing the last address shows the required-list error.',
|
|
10
|
+
exampleFor: 'ListInput',
|
|
11
|
+
isReady: true,
|
|
12
|
+
aspectRatio: 4 / 3,
|
|
13
|
+
componentsUsed: ['ListInput', 'TextInput', 'VStack'],
|
|
14
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @input Controlled subscriber records, starting empty
|
|
7
|
+
* @output A one-column ListInput of email addresses with a six-record limit,
|
|
8
|
+
* duplicate detection, and blur validation
|
|
9
|
+
* @position Copyable Lab ListInput example
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useMemo, useState} from 'react';
|
|
13
|
+
import {VStack} from '@astryxdesign/core/Layout';
|
|
14
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
15
|
+
import {ListInput, type ListInputColumn} from '@astryxdesign/lab';
|
|
16
|
+
|
|
17
|
+
type Subscriber = {id: string; email: string};
|
|
18
|
+
|
|
19
|
+
function isValidEmail(email: string): boolean {
|
|
20
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function ListInputMailingList() {
|
|
24
|
+
const [subscribers, setSubscribers] = useState<Subscriber[]>([]);
|
|
25
|
+
const [hasChanged, setHasChanged] = useState(false);
|
|
26
|
+
// Field errors wait until the field has been left once.
|
|
27
|
+
const [touched, setTouched] = useState<ReadonlySet<string>>(() => new Set());
|
|
28
|
+
|
|
29
|
+
const columns = useMemo<ListInputColumn<Subscriber>[]>(
|
|
30
|
+
() => [
|
|
31
|
+
{
|
|
32
|
+
key: 'email',
|
|
33
|
+
header: 'Email address',
|
|
34
|
+
renderInput: ({
|
|
35
|
+
item,
|
|
36
|
+
label,
|
|
37
|
+
isLabelHidden,
|
|
38
|
+
status,
|
|
39
|
+
statusVariant,
|
|
40
|
+
isDisabled,
|
|
41
|
+
isLoading,
|
|
42
|
+
updateItem,
|
|
43
|
+
}) => (
|
|
44
|
+
<TextInput
|
|
45
|
+
type="email"
|
|
46
|
+
label={label}
|
|
47
|
+
isLabelHidden={isLabelHidden}
|
|
48
|
+
value={item.email}
|
|
49
|
+
placeholder="name@example.com"
|
|
50
|
+
onChange={email => updateItem({...item, email}, 'email')}
|
|
51
|
+
onBlur={() =>
|
|
52
|
+
setTouched(current =>
|
|
53
|
+
current.has(item.id) ? current : new Set(current).add(item.id),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
status={status}
|
|
57
|
+
statusVariant={statusVariant}
|
|
58
|
+
isDisabled={isDisabled}
|
|
59
|
+
isLoading={isLoading}
|
|
60
|
+
/>
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
[],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<VStack width={680} maxWidth="100%">
|
|
69
|
+
<ListInput<Subscriber>
|
|
70
|
+
label="Mailing list"
|
|
71
|
+
description="Add up to six addresses."
|
|
72
|
+
value={subscribers}
|
|
73
|
+
onChange={nextSubscribers => {
|
|
74
|
+
setSubscribers(nextSubscribers);
|
|
75
|
+
setHasChanged(true);
|
|
76
|
+
}}
|
|
77
|
+
getItemKey={subscriber => subscriber.id}
|
|
78
|
+
createItem={() => ({id: crypto.randomUUID(), email: ''})}
|
|
79
|
+
columns={columns}
|
|
80
|
+
itemName="subscriber"
|
|
81
|
+
isRequired
|
|
82
|
+
maxItems={6}
|
|
83
|
+
status={
|
|
84
|
+
hasChanged && subscribers.length === 0
|
|
85
|
+
? {type: 'error', message: 'Add at least one subscriber.'}
|
|
86
|
+
: undefined
|
|
87
|
+
}
|
|
88
|
+
getItemStatus={subscriber => {
|
|
89
|
+
const email = subscriber.email.trim().toLowerCase();
|
|
90
|
+
const isDuplicate =
|
|
91
|
+
email !== '' &&
|
|
92
|
+
subscribers.some(
|
|
93
|
+
candidate =>
|
|
94
|
+
candidate.id !== subscriber.id &&
|
|
95
|
+
candidate.email.trim().toLowerCase() === email,
|
|
96
|
+
);
|
|
97
|
+
return isDuplicate
|
|
98
|
+
? {type: 'error', message: 'This address is already in the list.'}
|
|
99
|
+
: undefined;
|
|
100
|
+
}}
|
|
101
|
+
getFieldStatus={subscriber => {
|
|
102
|
+
if (!touched.has(subscriber.id)) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (subscriber.email.trim() === '') {
|
|
106
|
+
return {type: 'error', message: 'Enter an email address.'};
|
|
107
|
+
}
|
|
108
|
+
return isValidEmail(subscriber.email)
|
|
109
|
+
? undefined
|
|
110
|
+
: {type: 'error', message: 'Enter a valid email address.'};
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
</VStack>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
|
|
4
|
+
export default {
|
|
5
|
+
type: 'block',
|
|
6
|
+
name: 'Tag options',
|
|
7
|
+
displayName: 'Tag options',
|
|
8
|
+
description:
|
|
9
|
+
'A reorderable tag editor. Each record pairs a color Selector with a label TextInput; drag a grip or use the keyboard to reorder. Validation sits at the narrowest useful level: an empty label after blur is a field error, a disallowed color and label pair is a record error, and fewer than five tags is a list error.',
|
|
10
|
+
exampleFor: 'ListInput',
|
|
11
|
+
isReady: true,
|
|
12
|
+
isShowcase: true,
|
|
13
|
+
aspectRatio: 4 / 3,
|
|
14
|
+
componentsUsed: ['ListInput', 'Selector', 'TextInput', 'VStack'],
|
|
15
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @input Controlled tag records and blur-tracked fields
|
|
7
|
+
* @output A reorderable ListInput of tag options with field, record, and list
|
|
8
|
+
* validation
|
|
9
|
+
* @position Lab ListInput's docsite showcase and copyable CLI block
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {useMemo, useState} from 'react';
|
|
13
|
+
import {VStack} from '@astryxdesign/core/Layout';
|
|
14
|
+
import {Selector} from '@astryxdesign/core/Selector';
|
|
15
|
+
import {proportional} from '@astryxdesign/core/Table';
|
|
16
|
+
import {TextInput} from '@astryxdesign/core/TextInput';
|
|
17
|
+
import {ListInput, type ListInputColumn} from '@astryxdesign/lab';
|
|
18
|
+
|
|
19
|
+
type TagOption = {id: string; color: string; label: string};
|
|
20
|
+
|
|
21
|
+
const COLOR_OPTIONS = [
|
|
22
|
+
{value: 'blue', label: 'Blue'},
|
|
23
|
+
{value: 'green', label: 'Green'},
|
|
24
|
+
{value: 'amber', label: 'Amber'},
|
|
25
|
+
{value: 'red', label: 'Red'},
|
|
26
|
+
{value: 'purple', label: 'Purple'},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const INITIAL_TAGS: TagOption[] = [
|
|
30
|
+
{id: 'tag-new', color: 'blue', label: 'New'},
|
|
31
|
+
{id: 'tag-blocked', color: 'green', label: 'Blocked'},
|
|
32
|
+
{id: 'tag-draft', color: 'amber', label: ''},
|
|
33
|
+
{id: 'tag-review', color: 'purple', label: 'In review'},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export default function ListInputShowcase() {
|
|
37
|
+
const [tags, setTags] = useState(INITIAL_TAGS);
|
|
38
|
+
// Field errors wait until the field has been left once.
|
|
39
|
+
const [touchedLabels, setTouchedLabels] = useState<ReadonlySet<string>>(
|
|
40
|
+
() => new Set(),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const columns = useMemo<ListInputColumn<TagOption>[]>(
|
|
44
|
+
() => [
|
|
45
|
+
{
|
|
46
|
+
key: 'color',
|
|
47
|
+
header: 'Color',
|
|
48
|
+
width: proportional(1, {minWidth: 120}),
|
|
49
|
+
renderInput: ({
|
|
50
|
+
item,
|
|
51
|
+
label,
|
|
52
|
+
isLabelHidden,
|
|
53
|
+
status,
|
|
54
|
+
statusVariant,
|
|
55
|
+
isDisabled,
|
|
56
|
+
updateItem,
|
|
57
|
+
}) => (
|
|
58
|
+
<Selector
|
|
59
|
+
label={label}
|
|
60
|
+
isLabelHidden={isLabelHidden}
|
|
61
|
+
options={COLOR_OPTIONS}
|
|
62
|
+
value={item.color}
|
|
63
|
+
onChange={color => updateItem({...item, color}, 'color')}
|
|
64
|
+
status={status}
|
|
65
|
+
statusVariant={statusVariant}
|
|
66
|
+
isDisabled={isDisabled}
|
|
67
|
+
/>
|
|
68
|
+
),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'label',
|
|
72
|
+
header: 'Label',
|
|
73
|
+
width: proportional(2, {minWidth: 200}),
|
|
74
|
+
renderInput: ({
|
|
75
|
+
item,
|
|
76
|
+
label,
|
|
77
|
+
isLabelHidden,
|
|
78
|
+
status,
|
|
79
|
+
statusVariant,
|
|
80
|
+
isDisabled,
|
|
81
|
+
isLoading,
|
|
82
|
+
updateItem,
|
|
83
|
+
}) => (
|
|
84
|
+
<TextInput
|
|
85
|
+
label={label}
|
|
86
|
+
isLabelHidden={isLabelHidden}
|
|
87
|
+
value={item.label}
|
|
88
|
+
onChange={nextLabel =>
|
|
89
|
+
updateItem({...item, label: nextLabel}, 'label')
|
|
90
|
+
}
|
|
91
|
+
onBlur={() =>
|
|
92
|
+
setTouchedLabels(current =>
|
|
93
|
+
current.has(item.id) ? current : new Set(current).add(item.id),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
status={status}
|
|
97
|
+
statusVariant={statusVariant}
|
|
98
|
+
isDisabled={isDisabled}
|
|
99
|
+
isLoading={isLoading}
|
|
100
|
+
/>
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
[],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<VStack width={680} maxWidth="100%">
|
|
109
|
+
<ListInput<TagOption>
|
|
110
|
+
label="Tag options"
|
|
111
|
+
description="Add at least five tags and drag rows to set their order."
|
|
112
|
+
value={tags}
|
|
113
|
+
onChange={setTags}
|
|
114
|
+
getItemKey={tag => tag.id}
|
|
115
|
+
createItem={() => ({id: crypto.randomUUID(), color: 'blue', label: ''})}
|
|
116
|
+
columns={columns}
|
|
117
|
+
itemName="tag"
|
|
118
|
+
isReorderable
|
|
119
|
+
status={
|
|
120
|
+
tags.length < 5
|
|
121
|
+
? {type: 'error', message: 'Add at least five tag options.'}
|
|
122
|
+
: undefined
|
|
123
|
+
}
|
|
124
|
+
getItemStatus={tag =>
|
|
125
|
+
tag.color === 'green' && tag.label.trim().toLowerCase() === 'blocked'
|
|
126
|
+
? {type: 'error', message: `“${tag.label}” cannot use green.`}
|
|
127
|
+
: undefined
|
|
128
|
+
}
|
|
129
|
+
getFieldStatus={(tag, columnKey) =>
|
|
130
|
+
columnKey === 'label' &&
|
|
131
|
+
touchedLabels.has(tag.id) &&
|
|
132
|
+
tag.label.trim() === ''
|
|
133
|
+
? {type: 'error', message: 'Enter a label.'}
|
|
134
|
+
: undefined
|
|
135
|
+
}
|
|
136
|
+
/>
|
|
137
|
+
</VStack>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file ListInput.tsx
|
|
3
|
-
* @input React, StyleX, Astryx field/list/button/icon/layer/empty-state primitives, compact size context, browser scroll geometry, programmatic theme tokens, and shared Lab reorder styles
|
|
3
|
+
* @input React, StyleX, Astryx field/list/button/icon/layer/empty-state primitives, the container-reveal hook, compact size context, browser scroll geometry, programmatic theme tokens, and shared Lab reorder styles
|
|
4
4
|
* @output Exports ListInput and its controlled data, column, renderer, and change types
|
|
5
5
|
* @position Lab experiment (RFC facebook/astryx#4531) for editing compact repeated records
|
|
6
6
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ListInput.d.ts","sourceRoot":"","sources":["../../src/ListInput/ListInput.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,EAOL,KAAK,GAAG,EACR,KAAK,SAAS,EAEf,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAGlD,OAAO,EAAQ,KAAK,WAAW,EAAC,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"ListInput.d.ts","sourceRoot":"","sources":["../../src/ListInput/ListInput.tsx"],"names":[],"mappings":"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"}
|