autocomplete.stories.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import { Meta, StoryObj } from '@storybook/react-webpack5'
  2. import { useState } from 'react'
  3. import OLAutocomplete from '@/shared/components/ol/ol-autocomplete'
  4. type Args = React.ComponentProps<typeof OLAutocomplete>
  5. const sampleItems = [
  6. { value: 'react', label: 'React' },
  7. { value: 'vue', label: 'Vue' },
  8. { value: 'angular', label: 'Angular' },
  9. { value: 'svelte', label: 'Svelte' },
  10. { value: 'ember', label: 'Ember' },
  11. { value: 'backbone', label: 'Backbone' },
  12. { value: 'preact', label: 'Preact' },
  13. { value: 'nextjs', label: 'Next.js' },
  14. { value: 'nuxtjs', label: 'Nuxt.js' },
  15. { value: 'gatsby', label: 'Gatsby' },
  16. ]
  17. const programmingLanguages = [
  18. { value: 'javascript', label: 'JavaScript' },
  19. { value: 'typescript', label: 'TypeScript' },
  20. { value: 'python', label: 'Python' },
  21. { value: 'java', label: 'Java' },
  22. { value: 'cpp', label: 'C++' },
  23. { value: 'csharp', label: 'C#' },
  24. { value: 'go', label: 'Go' },
  25. { value: 'rust', label: 'Rust' },
  26. { value: 'ruby', label: 'Ruby' },
  27. { value: 'php', label: 'PHP' },
  28. { value: 'swift', label: 'Swift' },
  29. { value: 'kotlin', label: 'Kotlin' },
  30. { value: 'dart', label: 'Dart' },
  31. ]
  32. const countries = [
  33. { value: 'us', label: 'United States' },
  34. { value: 'uk', label: 'United Kingdom' },
  35. { value: 'ca', label: 'Canada' },
  36. { value: 'au', label: 'Australia' },
  37. { value: 'de', label: 'Germany' },
  38. { value: 'fr', label: 'France' },
  39. { value: 'es', label: 'Spain' },
  40. { value: 'it', label: 'Italy' },
  41. { value: 'nl', label: 'Netherlands' },
  42. { value: 'be', label: 'Belgium' },
  43. ]
  44. const entryTypeItems = [
  45. { value: 'article', label: 'Article', group: 'Most common' },
  46. { value: 'review', label: 'Review', group: 'Most common' },
  47. { value: 'book', label: 'Book', group: 'Most common' },
  48. { value: 'conference', label: 'Conference paper', group: 'Most common' },
  49. { value: 'thesis', label: 'Thesis', group: 'Most common' },
  50. { value: 'mastersthesis', label: "Master's thesis", group: 'Most common' },
  51. { value: 'phdthesis', label: 'PhD thesis', group: 'Most common' },
  52. { value: 'bookchapter', label: 'Book chapter', group: 'Book types' },
  53. { value: 'booksection', label: 'Book section', group: 'Book types' },
  54. { value: 'bookpart', label: 'Book part', group: 'Book types' },
  55. { value: 'editedbook', label: 'Edited book', group: 'Book types' },
  56. { value: 'referencebook', label: 'Reference book', group: 'Book types' },
  57. { value: 'proceedings', label: 'Conference proceedings', group: 'Other' },
  58. { value: 'journalarticle', label: 'Journal article', group: 'Other' },
  59. { value: 'techreport', label: 'Technical report', group: 'Other' },
  60. { value: 'preprint', label: 'Preprint', group: 'Other' },
  61. { value: 'patent', label: 'Patent', group: 'Other' },
  62. { value: 'manuscript', label: 'Manuscript', group: 'Other' },
  63. { value: 'unpublished', label: 'Unpublished', group: 'Other' },
  64. ]
  65. function InteractiveAutocomplete(args: Args) {
  66. const [value, setValue] = useState('')
  67. return (
  68. <div style={{ maxWidth: '400px' }}>
  69. <OLAutocomplete {...args} onChange={setValue} />
  70. <div style={{ marginTop: '1rem' }}>
  71. <strong>Current value:</strong> {value || '(empty)'}
  72. </div>
  73. </div>
  74. )
  75. }
  76. export const Default: StoryObj<Args> = {
  77. render: args => <InteractiveAutocomplete {...args} />,
  78. args: {
  79. items: sampleItems,
  80. label: 'Select a framework',
  81. placeholder: 'Type to search...',
  82. showLabel: true,
  83. allowCreate: true,
  84. },
  85. }
  86. export const WithoutCreateOption: StoryObj<Args> = {
  87. render: args => <InteractiveAutocomplete {...args} />,
  88. args: {
  89. items: programmingLanguages,
  90. label: 'Programming language',
  91. placeholder: 'Choose a language',
  92. showLabel: true,
  93. allowCreate: false,
  94. },
  95. }
  96. export const AllowCreatePredicate: StoryObj<Args> = {
  97. render: args => <InteractiveAutocomplete {...args} />,
  98. args: {
  99. items: programmingLanguages,
  100. label: 'Programming language (custom rule)',
  101. placeholder: 'Try typing java or script',
  102. showLabel: true,
  103. allowCreate: value => {
  104. const normalized = value.trim().toLowerCase()
  105. return (
  106. normalized.length >= 4 &&
  107. !normalized.includes('script') &&
  108. normalized !== 'java'
  109. )
  110. },
  111. },
  112. }
  113. export const HiddenLabel: StoryObj<Args> = {
  114. render: args => <InteractiveAutocomplete {...args} />,
  115. args: {
  116. items: countries,
  117. label: 'Country',
  118. placeholder: 'Select your country',
  119. showLabel: false,
  120. allowCreate: true,
  121. },
  122. }
  123. export const CustomCreatePrefix: StoryObj<Args> = {
  124. render: args => <InteractiveAutocomplete {...args} />,
  125. args: {
  126. items: [
  127. { value: 'alpha', label: 'Project Alpha' },
  128. { value: 'beta', label: 'Project Beta' },
  129. { value: 'gamma', label: 'Project Gamma' },
  130. ],
  131. label: 'Project name',
  132. placeholder: 'Select or create a project',
  133. showLabel: true,
  134. allowCreate: true,
  135. createOptionPrefix: '➕ Create new project:',
  136. },
  137. }
  138. export const Disabled: StoryObj<Args> = {
  139. render: args => <InteractiveAutocomplete {...args} />,
  140. args: {
  141. items: sampleItems,
  142. label: 'Framework (disabled)',
  143. placeholder: 'Cannot interact',
  144. showLabel: true,
  145. disabled: true,
  146. },
  147. }
  148. export const SmallList: StoryObj<Args> = {
  149. render: args => <InteractiveAutocomplete {...args} />,
  150. args: {
  151. items: [
  152. { value: 'option1', label: 'Option 1' },
  153. { value: 'option2', label: 'Option 2' },
  154. { value: 'option3', label: 'Option 3' },
  155. ],
  156. label: 'Choose an option',
  157. placeholder: 'Type or select...',
  158. showLabel: true,
  159. allowCreate: true,
  160. },
  161. }
  162. export const GroupedItems: StoryObj<Args> = {
  163. render: args => <InteractiveAutocomplete {...args} />,
  164. args: {
  165. items: entryTypeItems,
  166. label: 'Entry type',
  167. placeholder: 'Enter entry type',
  168. showLabel: true,
  169. allowCreate: true,
  170. },
  171. }
  172. export const GroupedWithoutCreate: StoryObj<Args> = {
  173. render: args => <InteractiveAutocomplete {...args} />,
  174. args: {
  175. items: [
  176. { value: 'react', label: 'React', group: 'Frontend' },
  177. { value: 'vue', label: 'Vue', group: 'Frontend' },
  178. { value: 'angular', label: 'Angular', group: 'Frontend' },
  179. { value: 'svelte', label: 'Svelte', group: 'Frontend' },
  180. { value: 'nodejs', label: 'Node.js', group: 'Backend' },
  181. { value: 'django', label: 'Django', group: 'Backend' },
  182. { value: 'rails', label: 'Ruby on Rails', group: 'Backend' },
  183. { value: 'spring', label: 'Spring Boot', group: 'Backend' },
  184. { value: 'reactnative', label: 'React Native', group: 'Mobile' },
  185. { value: 'flutter', label: 'Flutter', group: 'Mobile' },
  186. { value: 'swift', label: 'Swift', group: 'Mobile' },
  187. { value: 'kotlin', label: 'Kotlin', group: 'Mobile' },
  188. ],
  189. label: 'Technology',
  190. placeholder: 'Search technologies...',
  191. showLabel: true,
  192. allowCreate: false,
  193. },
  194. }
  195. export const FuzzySearch: StoryObj<Args> = {
  196. render: args => <InteractiveAutocomplete {...args} />,
  197. args: {
  198. items: [
  199. { value: 'javascript', label: 'JavaScript' },
  200. { value: 'typescript', label: 'TypeScript' },
  201. { value: 'python', label: 'Python' },
  202. { value: 'rust', label: 'Rust' },
  203. { value: 'golang', label: 'Go' },
  204. ],
  205. label: 'Language (fuzzy search)',
  206. placeholder: 'Try typo searches, e.g. javasript or pyton',
  207. showLabel: true,
  208. allowCreate: true,
  209. useFuzzySearch: true,
  210. },
  211. }
  212. export const GroupedFuzzySearch: StoryObj<Args> = {
  213. render: args => <InteractiveAutocomplete {...args} />,
  214. args: {
  215. items: entryTypeItems,
  216. label: 'Grouped entry type (fuzzy search)',
  217. placeholder: 'Try typo searches, e.g. confernce or theis',
  218. showLabel: true,
  219. allowCreate: true,
  220. useFuzzySearch: true,
  221. },
  222. }
  223. export const ExpandUp: StoryObj<Args> = {
  224. render: args => <InteractiveAutocomplete {...args} />,
  225. args: {
  226. items: sampleItems,
  227. label: 'Framework (expand up)',
  228. placeholder: 'Dropdown expands upward',
  229. showLabel: true,
  230. allowCreate: true,
  231. expandUp: true,
  232. },
  233. }
  234. export const ExpandUpGrouped: StoryObj<Args> = {
  235. render: args => <InteractiveAutocomplete {...args} />,
  236. args: {
  237. items: entryTypeItems,
  238. label: 'Entry type (expand up)',
  239. placeholder: 'Grouped items expanding upward',
  240. showLabel: true,
  241. allowCreate: true,
  242. expandUp: true,
  243. },
  244. }
  245. const meta: Meta<typeof OLAutocomplete> = {
  246. title: 'Shared / Components / Autocomplete',
  247. component: OLAutocomplete,
  248. tags: ['autodocs'],
  249. parameters: {
  250. controls: {
  251. include: [
  252. 'items',
  253. 'label',
  254. 'placeholder',
  255. 'showLabel',
  256. 'allowCreate',
  257. 'disabled',
  258. 'createOptionPrefix',
  259. 'useFuzzySearch',
  260. 'expandUp',
  261. ],
  262. },
  263. },
  264. argTypes: {
  265. items: {
  266. control: 'object',
  267. description:
  268. 'Array of available options; use the optional group property for grouped sections',
  269. },
  270. onChange: {
  271. description: 'Callback when value changes',
  272. },
  273. label: {
  274. control: 'text',
  275. description: 'Label for the input',
  276. },
  277. placeholder: {
  278. control: 'text',
  279. description: 'Placeholder text',
  280. },
  281. showLabel: {
  282. control: 'boolean',
  283. description: 'Show or hide the label visually',
  284. },
  285. allowCreate: {
  286. control: 'boolean',
  287. description:
  288. 'Allow creating custom values, or provide a predicate function `(value) => boolean` to conditionally allow create per input value',
  289. table: {
  290. type: {
  291. summary: 'boolean | ((value: string) => boolean)',
  292. },
  293. },
  294. },
  295. disabled: {
  296. control: 'boolean',
  297. description: 'Disable the input',
  298. },
  299. createOptionPrefix: {
  300. control: 'text',
  301. description: 'Text prefix for the create option',
  302. },
  303. useFuzzySearch: {
  304. control: 'boolean',
  305. description: 'Enable fuzzy search matching for suggestions',
  306. },
  307. expandUp: {
  308. control: 'boolean',
  309. description:
  310. 'When true, the dropdown expands upward and the search bar appears below the results list',
  311. },
  312. },
  313. }
  314. export default meta