@@ -30,10 +30,11 @@ import {
30
30
Grid ,
31
31
Form ,
32
32
FormGroup ,
33
- SimpleList ,
34
- SimpleListItem ,
33
+ Select ,
34
+ SelectGroup ,
35
+ SelectOption ,
36
+ SelectOptionObject ,
35
37
TextInput ,
36
- Tile ,
37
38
} from "@patternfly/react-core"
38
39
39
40
import HomeIcon from "@patternfly/react-icons/dist/esm/icons/home-icon"
@@ -129,36 +130,11 @@ export default class AskUI extends React.PureComponent<Props, State> {
129
130
< CardActions hasNoOffset > { this . actions ( ) } </ CardActions >
130
131
</ CardHeader >
131
132
132
- < CardBody className = "scrollable scrollable-auto" style = { { paddingTop : "1em" } } >
133
- { body }
134
- </ CardBody >
133
+ < CardBody > { body } </ CardBody >
135
134
</ Card >
136
135
)
137
136
}
138
137
139
- /** Render a simplified set of choices where the message is the same as the title */
140
- private select ( ask : Ask < Prompts . Select > ) {
141
- return this . card (
142
- ask . title ,
143
- < SimpleList >
144
- { ask . prompt . choices . map ( ( _ ) => {
145
- const isSuggested = this . state ?. userSelection === _ . name
146
- return (
147
- < SimpleListItem
148
- key = { _ . name }
149
- itemId = { _ . name }
150
- data-name = { _ . name }
151
- isActive = { isSuggested }
152
- onClick = { this . _onSimpleListClick }
153
- >
154
- { _ . name }
155
- </ SimpleListItem >
156
- )
157
- } ) }
158
- </ SimpleList >
159
- )
160
- }
161
-
162
138
private justTheMessage ( choice : Ask [ "prompt" ] [ "choices" ] [ number ] ) {
163
139
return stripAnsi ( choice . message ) . replace ( " ◄ you selected this last time" , "" )
164
140
}
@@ -183,60 +159,124 @@ export default class AskUI extends React.PureComponent<Props, State> {
183
159
return false
184
160
}
185
161
186
- /** User has clicked on a tile */
187
- private readonly _onTileClick = ( evt : React . MouseEvent ) => {
188
- const name = evt . currentTarget . getAttribute ( "data-name" )
189
- if ( name && this . props . ask ) {
162
+ /** User has clicked on a SelectOption */
163
+ private readonly _onSelect = (
164
+ evt : React . MouseEvent | React . ChangeEvent ,
165
+ selection : string | SelectOptionObject ,
166
+ isPlaceholder ?: boolean
167
+ ) => {
168
+ if ( ! isPlaceholder && selection && this . props . ask ) {
169
+ const name = selection . toString ( )
190
170
this . setState ( { userSelection : name } )
191
171
this . props . ask . onChoose ( Promise . resolve ( name ) )
192
172
}
193
173
}
194
174
195
- private tiles ( ask : Ask < Prompts . Select > ) {
196
- // is every message the same as the title?
197
- const isSimplistic = ask . prompt . choices . every (
198
- ( _ ) => _ . name === stripAnsi ( _ . message ) . replace ( " ◄ you selected this last time" , "" )
175
+ private readonly _selectOptionForChoice = (
176
+ _ : Ask < Prompts . Select > [ "prompt" ] [ "choices" ] [ number ] ,
177
+ isFocused = false
178
+ ) => {
179
+ const message = this . justTheMessage ( _ )
180
+ const isSuggested = this . state ?. userSelection === _ . name
181
+ const description = (
182
+ < React . Fragment >
183
+ { " " }
184
+ { message !== _ . name && (
185
+ < div >
186
+ < Ansi noWrap = "normal" className = "sans-serif" >
187
+ { _ . message . split ( / \n / ) . slice ( - 2 ) [ 0 ] }
188
+ </ Ansi >
189
+ </ div >
190
+ ) }
191
+ { isSuggested && (
192
+ < div className = "top-pad color-base0D" >
193
+ < InfoIcon /> You selected this last time
194
+ </ div >
195
+ ) }
196
+ </ React . Fragment >
199
197
)
200
- if ( isSimplistic ) {
201
- return this . select ( ask )
198
+
199
+ // re: className: pf-m-focus; patternfly seems to have a bug with isGrouped Select and isFocused SelectOptions
200
+ return (
201
+ < SelectOption
202
+ key = { _ . name }
203
+ id = { _ . name }
204
+ value = { _ . name }
205
+ description = { description }
206
+ isFocused = { isFocused }
207
+ className = { isFocused ? "pf-m-focus" : undefined }
208
+ >
209
+ { _ . name }
210
+ </ SelectOption >
211
+ )
212
+ }
213
+
214
+ /** PatternFly's <Select> requires an onToggle, but we want the Select to remain ever-open */
215
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
216
+ private readonly _doNothing = ( ) => {
217
+ // Intentionally empty
218
+ }
219
+
220
+ /** Render a UI for making a selection */
221
+ private select ( ask : Ask < Prompts . Select > ) {
222
+ const suggested = ask . prompt . choices . find ( ( _ ) => _ . name === this . state ?. userSelection )
223
+
224
+ // present a filtered list of options; note that we filter based
225
+ // on the full "message" field, which includes both the title
226
+ // (_.name) and the description (which is buried inside of
227
+ // _.message
228
+ const mkOptions = ( filter = "" ) => {
229
+ const pattern = new RegExp ( filter , "i" )
230
+
231
+ // options other than the "suggested" (i.e. prior choice)
232
+ const others = ask . prompt . choices
233
+ . filter ( ( _ ) => _ . name !== this . state ?. userSelection && ( ! filter || pattern . test ( _ . message ) ) )
234
+ . map ( ( _ ) => this . _selectOptionForChoice ( _ ) )
235
+
236
+ // ugh, a bit of syntactic garbage here to make typescript
237
+ // happy. this is just creating a filtered list of two groups:
238
+ // Prior choice and Other choices, but either one may be empty
239
+ // due to the filtering, or the lack of a prior choice
240
+ return [
241
+ ...( suggested && ( ! filter || pattern . test ( suggested . message ) )
242
+ ? [ < SelectGroup label = "Prior choice" > { this . _selectOptionForChoice ( suggested , true ) } </ SelectGroup > ]
243
+ : [ ] ) ,
244
+ ...( others . length > 0
245
+ ? [ < SelectGroup label = { suggested ? "Other choices" : "Choices" } > { others } </ SelectGroup > ]
246
+ : [ ] ) ,
247
+ ]
202
248
}
203
249
204
- return this . card (
205
- ask . title ,
206
- < Grid hasGutter md = { 3 } >
207
- { ask . prompt . choices . map ( ( _ ) => {
208
- const message = this . justTheMessage ( _ )
209
- const isSuggested = this . state ?. userSelection === _ . name
210
-
211
- //icon={isSuggested && <Icons icon="Info"/>}
212
-
213
- return (
214
- < Tile
215
- className = "kui--guide-tile"
216
- isSelected = { isSuggested }
217
- key = { _ . name }
218
- title = { _ . name }
219
- data-name = { _ . name }
220
- data-large = { ! isSimplistic || undefined }
221
- isStacked
222
- onClick = { this . _onTileClick }
223
- >
224
- { message !== _ . name && (
225
- < div >
226
- < Ansi noWrap = "normal" className = "sans-serif" >
227
- { _ . message . split ( / \n / ) . slice ( - 2 ) [ 0 ] }
228
- </ Ansi >
229
- </ div >
230
- ) }
231
- { isSuggested && (
232
- < div className = "top-pad color-base0D" >
233
- < InfoIcon /> You selected this last time
234
- </ div >
235
- ) }
236
- </ Tile >
237
- )
238
- } ) }
239
- </ Grid >
250
+ const onFilter = ( evt : React . ChangeEvent | null , filter : string ) => mkOptions ( filter )
251
+
252
+ const placeholderText = "Select an option"
253
+ const titleId = "kui--madwizard-ask-ui-title"
254
+
255
+ const props = {
256
+ isOpen : true ,
257
+ isGrouped : true ,
258
+ hasInlineFilter : true ,
259
+ isInputValuePersisted : true ,
260
+ isInputFilterPersisted : true ,
261
+ // variant: "typeahead" as const,
262
+ // typeAheadAriaLabel: "Select an option",
263
+ onFilter,
264
+ "aria-labelledby" : titleId ,
265
+ noResultsFoundText : "No matching choices" ,
266
+ placeholderText,
267
+ onSelect : this . _onSelect ,
268
+ onToggle : this . _doNothing ,
269
+ toggleIndicator : < React . Fragment /> ,
270
+ children : mkOptions ( ) ,
271
+ }
272
+
273
+ return (
274
+ < React . Fragment >
275
+ < span id = { titleId } hidden >
276
+ { placeholderText }
277
+ </ span >
278
+ { this . card ( ask . title , < Select { ...props } /> ) }
279
+ </ React . Fragment >
240
280
)
241
281
}
242
282
@@ -290,7 +330,7 @@ export default class AskUI extends React.PureComponent<Props, State> {
290
330
291
331
private ask ( ask : Ask ) {
292
332
if ( this . isSelect ( ask ) ) {
293
- return this . tiles ( ask )
333
+ return this . select ( ask )
294
334
} else if ( this . isMultiSelect ( ask ) ) {
295
335
return this . checkboxes ( ask )
296
336
} else {
0 commit comments