@furystack/shades-common-components 12.4.0 → 12.6.0

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.
Files changed (297) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/esm/components/app-bar-link.spec.js +16 -19
  3. package/esm/components/app-bar-link.spec.js.map +1 -1
  4. package/esm/components/app-bar.spec.js +6 -4
  5. package/esm/components/app-bar.spec.js.map +1 -1
  6. package/esm/components/avatar.spec.js +9 -9
  7. package/esm/components/avatar.spec.js.map +1 -1
  8. package/esm/components/breadcrumb.spec.js +2 -2
  9. package/esm/components/breadcrumb.spec.js.map +1 -1
  10. package/esm/components/button-group.d.ts +32 -0
  11. package/esm/components/button-group.d.ts.map +1 -1
  12. package/esm/components/button-group.js +26 -2
  13. package/esm/components/button-group.js.map +1 -1
  14. package/esm/components/button-group.spec.js +127 -11
  15. package/esm/components/button-group.spec.js.map +1 -1
  16. package/esm/components/button.spec.js +4 -4
  17. package/esm/components/button.spec.js.map +1 -1
  18. package/esm/components/cache-view.spec.js +2 -3
  19. package/esm/components/cache-view.spec.js.map +1 -1
  20. package/esm/components/carousel.spec.js +47 -47
  21. package/esm/components/carousel.spec.js.map +1 -1
  22. package/esm/components/circular-progress.spec.js +2 -2
  23. package/esm/components/command-palette/command-palette-input.spec.js +23 -19
  24. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -1
  25. package/esm/components/command-palette/command-palette-suggestion-list.spec.js +27 -27
  26. package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -1
  27. package/esm/components/command-palette/index.spec.js +64 -51
  28. package/esm/components/command-palette/index.spec.js.map +1 -1
  29. package/esm/components/context-menu/context-menu.spec.js +33 -33
  30. package/esm/components/context-menu/context-menu.spec.js.map +1 -1
  31. package/esm/components/data-grid/body.spec.js +13 -13
  32. package/esm/components/data-grid/body.spec.js.map +1 -1
  33. package/esm/components/data-grid/data-grid-row.spec.js +8 -8
  34. package/esm/components/data-grid/data-grid-row.spec.js.map +1 -1
  35. package/esm/components/data-grid/data-grid.d.ts +47 -3
  36. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  37. package/esm/components/data-grid/data-grid.js +8 -11
  38. package/esm/components/data-grid/data-grid.js.map +1 -1
  39. package/esm/components/data-grid/data-grid.spec.js +71 -28
  40. package/esm/components/data-grid/data-grid.spec.js.map +1 -1
  41. package/esm/components/data-grid/filters/boolean-filter.d.ts +12 -0
  42. package/esm/components/data-grid/filters/boolean-filter.d.ts.map +1 -0
  43. package/esm/components/data-grid/filters/boolean-filter.js +27 -0
  44. package/esm/components/data-grid/filters/boolean-filter.js.map +1 -0
  45. package/esm/components/data-grid/filters/boolean-filter.spec.d.ts +2 -0
  46. package/esm/components/data-grid/filters/boolean-filter.spec.d.ts.map +1 -0
  47. package/esm/components/data-grid/filters/boolean-filter.spec.js +114 -0
  48. package/esm/components/data-grid/filters/boolean-filter.spec.js.map +1 -0
  49. package/esm/components/data-grid/filters/date-filter.d.ts +12 -0
  50. package/esm/components/data-grid/filters/date-filter.d.ts.map +1 -0
  51. package/esm/components/data-grid/filters/date-filter.js +109 -0
  52. package/esm/components/data-grid/filters/date-filter.js.map +1 -0
  53. package/esm/components/data-grid/filters/date-filter.spec.d.ts +2 -0
  54. package/esm/components/data-grid/filters/date-filter.spec.d.ts.map +1 -0
  55. package/esm/components/data-grid/filters/date-filter.spec.js +145 -0
  56. package/esm/components/data-grid/filters/date-filter.spec.js.map +1 -0
  57. package/esm/components/data-grid/filters/enum-filter.d.ts +16 -0
  58. package/esm/components/data-grid/filters/enum-filter.d.ts.map +1 -0
  59. package/esm/components/data-grid/filters/enum-filter.js +72 -0
  60. package/esm/components/data-grid/filters/enum-filter.js.map +1 -0
  61. package/esm/components/data-grid/filters/enum-filter.spec.d.ts +2 -0
  62. package/esm/components/data-grid/filters/enum-filter.spec.d.ts.map +1 -0
  63. package/esm/components/data-grid/filters/enum-filter.spec.js +136 -0
  64. package/esm/components/data-grid/filters/enum-filter.spec.js.map +1 -0
  65. package/esm/components/data-grid/filters/filter-dropdown.d.ts +6 -0
  66. package/esm/components/data-grid/filters/filter-dropdown.d.ts.map +1 -0
  67. package/esm/components/data-grid/filters/filter-dropdown.js +41 -0
  68. package/esm/components/data-grid/filters/filter-dropdown.js.map +1 -0
  69. package/esm/components/data-grid/filters/filter-dropdown.spec.d.ts +2 -0
  70. package/esm/components/data-grid/filters/filter-dropdown.spec.d.ts.map +1 -0
  71. package/esm/components/data-grid/filters/filter-dropdown.spec.js +69 -0
  72. package/esm/components/data-grid/filters/filter-dropdown.spec.js.map +1 -0
  73. package/esm/components/data-grid/filters/filter-styles.d.ts +24 -0
  74. package/esm/components/data-grid/filters/filter-styles.d.ts.map +1 -0
  75. package/esm/components/data-grid/filters/filter-styles.js +25 -0
  76. package/esm/components/data-grid/filters/filter-styles.js.map +1 -0
  77. package/esm/components/data-grid/filters/index.d.ts +7 -0
  78. package/esm/components/data-grid/filters/index.d.ts.map +1 -0
  79. package/esm/components/data-grid/filters/index.js +7 -0
  80. package/esm/components/data-grid/filters/index.js.map +1 -0
  81. package/esm/components/data-grid/filters/number-filter.d.ts +12 -0
  82. package/esm/components/data-grid/filters/number-filter.d.ts.map +1 -0
  83. package/esm/components/data-grid/filters/number-filter.js +65 -0
  84. package/esm/components/data-grid/filters/number-filter.js.map +1 -0
  85. package/esm/components/data-grid/filters/number-filter.spec.d.ts +2 -0
  86. package/esm/components/data-grid/filters/number-filter.spec.d.ts.map +1 -0
  87. package/esm/components/data-grid/filters/number-filter.spec.js +142 -0
  88. package/esm/components/data-grid/filters/number-filter.spec.js.map +1 -0
  89. package/esm/components/data-grid/filters/string-filter.d.ts +12 -0
  90. package/esm/components/data-grid/filters/string-filter.d.ts.map +1 -0
  91. package/esm/components/data-grid/filters/string-filter.js +63 -0
  92. package/esm/components/data-grid/filters/string-filter.js.map +1 -0
  93. package/esm/components/data-grid/filters/string-filter.spec.d.ts +2 -0
  94. package/esm/components/data-grid/filters/string-filter.spec.d.ts.map +1 -0
  95. package/esm/components/data-grid/filters/string-filter.spec.js +128 -0
  96. package/esm/components/data-grid/filters/string-filter.spec.js.map +1 -0
  97. package/esm/components/data-grid/footer.d.ts +1 -0
  98. package/esm/components/data-grid/footer.d.ts.map +1 -1
  99. package/esm/components/data-grid/footer.js +24 -16
  100. package/esm/components/data-grid/footer.js.map +1 -1
  101. package/esm/components/data-grid/footer.spec.js +111 -71
  102. package/esm/components/data-grid/footer.spec.js.map +1 -1
  103. package/esm/components/data-grid/header.d.ts +6 -9
  104. package/esm/components/data-grid/header.d.ts.map +1 -1
  105. package/esm/components/data-grid/header.js +51 -117
  106. package/esm/components/data-grid/header.js.map +1 -1
  107. package/esm/components/data-grid/header.spec.js +116 -187
  108. package/esm/components/data-grid/header.spec.js.map +1 -1
  109. package/esm/components/data-grid/index.d.ts +1 -0
  110. package/esm/components/data-grid/index.d.ts.map +1 -1
  111. package/esm/components/data-grid/index.js +1 -0
  112. package/esm/components/data-grid/index.js.map +1 -1
  113. package/esm/components/data-grid/selection-cell.spec.js +8 -8
  114. package/esm/components/data-grid/selection-cell.spec.js.map +1 -1
  115. package/esm/components/drawer/drawer-toggle-button.spec.js +22 -22
  116. package/esm/components/drawer/drawer-toggle-button.spec.js.map +1 -1
  117. package/esm/components/drawer/index.spec.js +36 -36
  118. package/esm/components/drawer/index.spec.js.map +1 -1
  119. package/esm/components/dropdown.spec.js +38 -30
  120. package/esm/components/dropdown.spec.js.map +1 -1
  121. package/esm/components/fab.spec.js +4 -4
  122. package/esm/components/fab.spec.js.map +1 -1
  123. package/esm/components/form.spec.js +37 -37
  124. package/esm/components/form.spec.js.map +1 -1
  125. package/esm/components/grid.d.ts +3 -0
  126. package/esm/components/grid.d.ts.map +1 -1
  127. package/esm/components/grid.js +3 -0
  128. package/esm/components/grid.js.map +1 -1
  129. package/esm/components/grid.spec.js +3 -3
  130. package/esm/components/grid.spec.js.map +1 -1
  131. package/esm/components/image.spec.js +55 -52
  132. package/esm/components/image.spec.js.map +1 -1
  133. package/esm/components/inputs/autocomplete.d.ts +3 -0
  134. package/esm/components/inputs/autocomplete.d.ts.map +1 -1
  135. package/esm/components/inputs/autocomplete.js +3 -0
  136. package/esm/components/inputs/autocomplete.js.map +1 -1
  137. package/esm/components/inputs/autocomplete.spec.js +7 -14
  138. package/esm/components/inputs/autocomplete.spec.js.map +1 -1
  139. package/esm/components/inputs/checkbox.spec.js +22 -22
  140. package/esm/components/inputs/checkbox.spec.js.map +1 -1
  141. package/esm/components/inputs/input-number.spec.js +47 -47
  142. package/esm/components/inputs/input-number.spec.js.map +1 -1
  143. package/esm/components/inputs/input.spec.js +53 -53
  144. package/esm/components/inputs/input.spec.js.map +1 -1
  145. package/esm/components/inputs/radio-group.spec.js +14 -14
  146. package/esm/components/inputs/radio-group.spec.js.map +1 -1
  147. package/esm/components/inputs/radio.spec.js +16 -16
  148. package/esm/components/inputs/radio.spec.js.map +1 -1
  149. package/esm/components/inputs/select.spec.js +74 -74
  150. package/esm/components/inputs/select.spec.js.map +1 -1
  151. package/esm/components/inputs/slider.spec.js +16 -16
  152. package/esm/components/inputs/slider.spec.js.map +1 -1
  153. package/esm/components/inputs/switch.spec.js +24 -24
  154. package/esm/components/inputs/switch.spec.js.map +1 -1
  155. package/esm/components/inputs/text-area.spec.js +17 -17
  156. package/esm/components/inputs/text-area.spec.js.map +1 -1
  157. package/esm/components/linear-progress.spec.js +2 -2
  158. package/esm/components/list/list.d.ts +10 -0
  159. package/esm/components/list/list.d.ts.map +1 -1
  160. package/esm/components/list/list.js +23 -2
  161. package/esm/components/list/list.js.map +1 -1
  162. package/esm/components/list/list.spec.js +137 -36
  163. package/esm/components/list/list.spec.js.map +1 -1
  164. package/esm/components/markdown/markdown-display.spec.js +15 -15
  165. package/esm/components/markdown/markdown-display.spec.js.map +1 -1
  166. package/esm/components/markdown/markdown-editor.spec.js +8 -8
  167. package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
  168. package/esm/components/markdown/markdown-input.d.ts +14 -0
  169. package/esm/components/markdown/markdown-input.d.ts.map +1 -1
  170. package/esm/components/markdown/markdown-input.js +48 -2
  171. package/esm/components/markdown/markdown-input.js.map +1 -1
  172. package/esm/components/markdown/markdown-input.spec.js +114 -17
  173. package/esm/components/markdown/markdown-input.spec.js.map +1 -1
  174. package/esm/components/menu/menu.spec.js +28 -28
  175. package/esm/components/menu/menu.spec.js.map +1 -1
  176. package/esm/components/modal.spec.js +15 -18
  177. package/esm/components/modal.spec.js.map +1 -1
  178. package/esm/components/noty-list.spec.js +25 -23
  179. package/esm/components/noty-list.spec.js.map +1 -1
  180. package/esm/components/page-container/index.spec.js +16 -16
  181. package/esm/components/page-container/index.spec.js.map +1 -1
  182. package/esm/components/page-container/page-header.spec.js +16 -16
  183. package/esm/components/page-container/page-header.spec.js.map +1 -1
  184. package/esm/components/page-layout/index.spec.js +29 -29
  185. package/esm/components/page-layout/index.spec.js.map +1 -1
  186. package/esm/components/paper.spec.js +3 -3
  187. package/esm/components/paper.spec.js.map +1 -1
  188. package/esm/components/rating.spec.js +61 -61
  189. package/esm/components/rating.spec.js.map +1 -1
  190. package/esm/components/skeleton.spec.js +10 -6
  191. package/esm/components/skeleton.spec.js.map +1 -1
  192. package/esm/components/suggest/index.d.ts +10 -2
  193. package/esm/components/suggest/index.d.ts.map +1 -1
  194. package/esm/components/suggest/index.js +21 -1
  195. package/esm/components/suggest/index.js.map +1 -1
  196. package/esm/components/suggest/index.spec.js +50 -0
  197. package/esm/components/suggest/index.spec.js.map +1 -1
  198. package/esm/components/suggest/suggest-input.spec.js +4 -10
  199. package/esm/components/suggest/suggest-input.spec.js.map +1 -1
  200. package/esm/components/tabs.spec.js +30 -30
  201. package/esm/components/tabs.spec.js.map +1 -1
  202. package/esm/components/tree/tree.spec.js +27 -27
  203. package/esm/components/tree/tree.spec.js.map +1 -1
  204. package/esm/components/typography.spec.js +3 -3
  205. package/esm/components/typography.spec.js.map +1 -1
  206. package/esm/components/wizard/index.d.ts +8 -0
  207. package/esm/components/wizard/index.d.ts.map +1 -1
  208. package/esm/components/wizard/index.js +90 -0
  209. package/esm/components/wizard/index.js.map +1 -1
  210. package/esm/components/wizard/index.spec.js +84 -7
  211. package/esm/components/wizard/index.spec.js.map +1 -1
  212. package/esm/utils/promisify-animation.d.ts.map +1 -1
  213. package/esm/utils/promisify-animation.js +3 -0
  214. package/esm/utils/promisify-animation.js.map +1 -1
  215. package/package.json +3 -3
  216. package/src/components/app-bar-link.spec.tsx +16 -19
  217. package/src/components/app-bar.spec.tsx +6 -4
  218. package/src/components/avatar.spec.tsx +9 -9
  219. package/src/components/breadcrumb.spec.tsx +2 -2
  220. package/src/components/button-group.spec.tsx +155 -11
  221. package/src/components/button-group.tsx +49 -2
  222. package/src/components/button.spec.tsx +4 -4
  223. package/src/components/cache-view.spec.tsx +3 -3
  224. package/src/components/carousel.spec.tsx +47 -47
  225. package/src/components/circular-progress.spec.tsx +2 -2
  226. package/src/components/command-palette/command-palette-input.spec.tsx +23 -19
  227. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +27 -27
  228. package/src/components/command-palette/index.spec.tsx +64 -51
  229. package/src/components/context-menu/context-menu.spec.tsx +33 -33
  230. package/src/components/data-grid/body.spec.tsx +13 -13
  231. package/src/components/data-grid/data-grid-row.spec.tsx +8 -8
  232. package/src/components/data-grid/data-grid.spec.tsx +106 -28
  233. package/src/components/data-grid/data-grid.tsx +57 -13
  234. package/src/components/data-grid/filters/boolean-filter.spec.tsx +142 -0
  235. package/src/components/data-grid/filters/boolean-filter.tsx +45 -0
  236. package/src/components/data-grid/filters/date-filter.spec.tsx +181 -0
  237. package/src/components/data-grid/filters/date-filter.tsx +162 -0
  238. package/src/components/data-grid/filters/enum-filter.spec.tsx +168 -0
  239. package/src/components/data-grid/filters/enum-filter.tsx +119 -0
  240. package/src/components/data-grid/filters/filter-dropdown.spec.tsx +89 -0
  241. package/src/components/data-grid/filters/filter-dropdown.tsx +60 -0
  242. package/src/components/data-grid/filters/filter-styles.ts +26 -0
  243. package/src/components/data-grid/filters/index.ts +6 -0
  244. package/src/components/data-grid/filters/number-filter.spec.tsx +174 -0
  245. package/src/components/data-grid/filters/number-filter.tsx +115 -0
  246. package/src/components/data-grid/filters/string-filter.spec.tsx +157 -0
  247. package/src/components/data-grid/filters/string-filter.tsx +112 -0
  248. package/src/components/data-grid/footer.spec.tsx +130 -74
  249. package/src/components/data-grid/footer.tsx +41 -34
  250. package/src/components/data-grid/header.spec.tsx +128 -212
  251. package/src/components/data-grid/header.tsx +95 -183
  252. package/src/components/data-grid/index.tsx +1 -0
  253. package/src/components/data-grid/selection-cell.spec.tsx +8 -8
  254. package/src/components/drawer/drawer-toggle-button.spec.tsx +22 -22
  255. package/src/components/drawer/index.spec.tsx +36 -36
  256. package/src/components/dropdown.spec.tsx +38 -30
  257. package/src/components/fab.spec.tsx +4 -4
  258. package/src/components/form.spec.tsx +37 -37
  259. package/src/components/grid.spec.tsx +3 -3
  260. package/src/components/grid.tsx +3 -0
  261. package/src/components/image.spec.tsx +55 -52
  262. package/src/components/inputs/autocomplete.spec.tsx +7 -14
  263. package/src/components/inputs/autocomplete.tsx +3 -0
  264. package/src/components/inputs/checkbox.spec.tsx +22 -22
  265. package/src/components/inputs/input-number.spec.tsx +47 -47
  266. package/src/components/inputs/input.spec.tsx +53 -53
  267. package/src/components/inputs/radio-group.spec.tsx +14 -14
  268. package/src/components/inputs/radio.spec.tsx +16 -16
  269. package/src/components/inputs/select.spec.tsx +74 -74
  270. package/src/components/inputs/slider.spec.tsx +16 -16
  271. package/src/components/inputs/switch.spec.tsx +24 -24
  272. package/src/components/inputs/text-area.spec.tsx +17 -17
  273. package/src/components/linear-progress.spec.tsx +2 -2
  274. package/src/components/list/list.spec.tsx +209 -36
  275. package/src/components/list/list.tsx +56 -19
  276. package/src/components/markdown/markdown-display.spec.tsx +15 -15
  277. package/src/components/markdown/markdown-editor.spec.tsx +8 -8
  278. package/src/components/markdown/markdown-input.spec.tsx +159 -17
  279. package/src/components/markdown/markdown-input.tsx +65 -1
  280. package/src/components/menu/menu.spec.tsx +28 -28
  281. package/src/components/modal.spec.tsx +15 -18
  282. package/src/components/noty-list.spec.tsx +25 -23
  283. package/src/components/page-container/index.spec.tsx +16 -16
  284. package/src/components/page-container/page-header.spec.tsx +16 -16
  285. package/src/components/page-layout/index.spec.tsx +29 -29
  286. package/src/components/paper.spec.tsx +3 -3
  287. package/src/components/rating.spec.tsx +61 -61
  288. package/src/components/skeleton.spec.tsx +10 -6
  289. package/src/components/suggest/index.spec.tsx +83 -0
  290. package/src/components/suggest/index.tsx +36 -3
  291. package/src/components/suggest/suggest-input.spec.tsx +4 -10
  292. package/src/components/tabs.spec.tsx +30 -30
  293. package/src/components/tree/tree.spec.tsx +27 -27
  294. package/src/components/typography.spec.tsx +3 -3
  295. package/src/components/wizard/index.spec.tsx +123 -6
  296. package/src/components/wizard/index.tsx +125 -0
  297. package/src/utils/promisify-animation.ts +3 -0
@@ -0,0 +1,112 @@
1
+ import { createComponent, Shade } from '@furystack/shades'
2
+ import type { ObservableValue } from '@furystack/utils'
3
+ import { Button } from '../../button.js'
4
+ import { SegmentedControl } from '../../button-group.js'
5
+ import { Icon } from '../../icons/icon.js'
6
+ import { close as closeIcon, search as searchIcon } from '../../icons/icon-definitions.js'
7
+ import type { FilterableFindOptions } from '../data-grid.js'
8
+ import { filterBaseCss, filterInputCss } from './filter-styles.js'
9
+
10
+ type StringOperator = '$regex' | '$startsWith' | '$endsWith' | '$eq'
11
+
12
+ const operatorLabels: Record<StringOperator, string> = {
13
+ $regex: 'Contains',
14
+ $startsWith: 'Starts with',
15
+ $endsWith: 'Ends with',
16
+ $eq: 'Equals',
17
+ }
18
+
19
+ export const StringFilter = Shade<{
20
+ field: string
21
+ findOptions: ObservableValue<FilterableFindOptions>
22
+ onClose: () => void
23
+ }>({
24
+ shadowDomName: 'data-grid-string-filter',
25
+ css: {
26
+ ...filterBaseCss,
27
+ '& input': filterInputCss,
28
+ },
29
+ render: ({ props, useObservable, useState }) => {
30
+ const [findOptions, setFindOptions] = useObservable('findOptions', props.findOptions)
31
+
32
+ const currentFilter = findOptions.filter?.[props.field] as Record<string, string> | undefined
33
+ const currentOperator: StringOperator = currentFilter
34
+ ? ((Object.keys(currentFilter).find((k) => k in operatorLabels) as StringOperator) ?? '$regex')
35
+ : '$regex'
36
+ const currentValue = currentFilter?.[currentOperator] ?? ''
37
+
38
+ const applyFilter = (operator: StringOperator, value: string) => {
39
+ const filter = { ...findOptions.filter }
40
+ if (!value) {
41
+ delete filter[props.field]
42
+ } else {
43
+ filter[props.field] = { [operator]: value }
44
+ }
45
+ setFindOptions({ ...findOptions, filter, skip: 0 })
46
+ props.onClose()
47
+ }
48
+
49
+ const clearFilter = () => {
50
+ const filter = { ...findOptions.filter }
51
+ delete filter[props.field]
52
+ setFindOptions({ ...findOptions, filter, skip: 0 })
53
+ props.onClose()
54
+ }
55
+
56
+ const [selectedOperator, setSelectedOperator] = useState<StringOperator>('selectedOperator', currentOperator)
57
+ let inputValue = currentValue
58
+
59
+ return (
60
+ <form
61
+ onsubmit={(ev: Event) => {
62
+ ev.preventDefault()
63
+ applyFilter(selectedOperator, inputValue)
64
+ }}
65
+ >
66
+ <div className="filter-row">
67
+ <SegmentedControl
68
+ size="small"
69
+ value={selectedOperator}
70
+ onValueChange={(v) => setSelectedOperator(v as StringOperator)}
71
+ options={(Object.keys(operatorLabels) as StringOperator[]).map((op) => ({
72
+ value: op,
73
+ label: operatorLabels[op],
74
+ }))}
75
+ />
76
+ </div>
77
+ <div className="filter-row">
78
+ <input
79
+ data-testid="string-filter-value"
80
+ type="text"
81
+ placeholder="Filter value..."
82
+ value={currentValue}
83
+ autofocus
84
+ oninput={(ev: Event) => {
85
+ inputValue = (ev.target as HTMLInputElement).value
86
+ }}
87
+ />
88
+ </div>
89
+ <div className="filter-actions">
90
+ <Button
91
+ type="button"
92
+ variant="outlined"
93
+ size="small"
94
+ onclick={clearFilter}
95
+ startIcon={<Icon icon={closeIcon} size={14} />}
96
+ >
97
+ Clear
98
+ </Button>
99
+ <Button
100
+ type="submit"
101
+ variant="outlined"
102
+ size="small"
103
+ color="primary"
104
+ startIcon={<Icon icon={searchIcon} size={14} />}
105
+ >
106
+ Apply
107
+ </Button>
108
+ </div>
109
+ </form>
110
+ )
111
+ },
112
+ })
@@ -1,7 +1,7 @@
1
1
  import type { FindOptions } from '@furystack/core'
2
2
  import { Injector } from '@furystack/inject'
3
- import { createComponent, initializeShadeRoot } from '@furystack/shades'
4
- import { ObservableValue, sleepAsync, usingAsync } from '@furystack/utils'
3
+ import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
4
+ import { ObservableValue, usingAsync } from '@furystack/utils'
5
5
  import { afterEach, beforeEach, describe, expect, it } from 'vitest'
6
6
  import { CollectionService } from '../../services/collection-service.js'
7
7
  import { DataGridFooter, dataGridItemsPerPage } from './footer.js'
@@ -42,7 +42,7 @@ describe('DataGridFooter', () => {
42
42
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
43
43
  })
44
44
 
45
- await sleepAsync(50)
45
+ await flushUpdates()
46
46
 
47
47
  const footer = document.querySelector('shade-data-grid-footer')
48
48
  expect(footer).not.toBeNull()
@@ -61,12 +61,12 @@ describe('DataGridFooter', () => {
61
61
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
62
62
  })
63
63
 
64
- await sleepAsync(50)
64
+ await flushUpdates()
65
65
 
66
66
  const footer = document.querySelector('shade-data-grid-footer')
67
- const selects = footer?.querySelectorAll('select')
67
+ const rowsPerPageSelect = footer?.querySelector('.pager-section select')
68
68
 
69
- expect(selects?.length).toBeGreaterThan(0)
69
+ expect(rowsPerPageSelect).not.toBeNull()
70
70
  })
71
71
  })
72
72
 
@@ -82,13 +82,13 @@ describe('DataGridFooter', () => {
82
82
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
83
83
  })
84
84
 
85
- await sleepAsync(50)
85
+ await flushUpdates()
86
86
 
87
87
  const footer = document.querySelector('shade-data-grid-footer')
88
88
  const selects = Array.from(footer?.querySelectorAll('select') ?? [])
89
89
  const itemsPerPageSelect = selects.find((s) => {
90
- const parent = s.parentElement
91
- return parent?.textContent?.includes('items per page')
90
+ const parent = s.closest('.pager-section')
91
+ return parent?.textContent?.includes('Rows per page')
92
92
  })
93
93
 
94
94
  expect(itemsPerPageSelect).toBeDefined()
@@ -97,7 +97,7 @@ describe('DataGridFooter', () => {
97
97
  })
98
98
  })
99
99
 
100
- it('should show page selector when pagination is enabled', async () => {
100
+ it('should show Pagination component when pagination is enabled', async () => {
101
101
  await usingAsync(new Injector(), async (injector) => {
102
102
  const rootElement = document.getElementById('root') as HTMLDivElement
103
103
  const service = createService([], 100)
@@ -109,15 +109,15 @@ describe('DataGridFooter', () => {
109
109
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
110
110
  })
111
111
 
112
- await sleepAsync(50)
112
+ await flushUpdates()
113
113
 
114
114
  const footer = document.querySelector('shade-data-grid-footer')
115
- const pager = footer?.querySelector('.pager')
116
- expect(pager?.textContent).toContain('Goto page')
115
+ const pagination = footer?.querySelector('shade-pagination')
116
+ expect(pagination).not.toBeNull()
117
117
  })
118
118
  })
119
119
 
120
- it('should hide page selector when showing all items (Infinity)', async () => {
120
+ it('should hide Pagination when showing all items (Infinity)', async () => {
121
121
  await usingAsync(new Injector(), async (injector) => {
122
122
  const rootElement = document.getElementById('root') as HTMLDivElement
123
123
  const service = createService([], 50)
@@ -129,15 +129,15 @@ describe('DataGridFooter', () => {
129
129
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
130
130
  })
131
131
 
132
- await sleepAsync(50)
132
+ await flushUpdates()
133
133
 
134
134
  const footer = document.querySelector('shade-data-grid-footer')
135
- const pager = footer?.querySelector('.pager')
136
- expect(pager?.textContent).not.toContain('Goto page')
135
+ const pagination = footer?.querySelector('shade-pagination')
136
+ expect(pagination).toBeNull()
137
137
  })
138
138
  })
139
139
 
140
- it('should render correct number of page options based on data count and items per page', async () => {
140
+ it('should render page buttons in Pagination based on data count and items per page', async () => {
141
141
  await usingAsync(new Injector(), async (injector) => {
142
142
  const rootElement = document.getElementById('root') as HTMLDivElement
143
143
  const service = createService([], 100)
@@ -149,22 +149,20 @@ describe('DataGridFooter', () => {
149
149
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
150
150
  })
151
151
 
152
- await sleepAsync(50)
152
+ await flushUpdates()
153
153
 
154
154
  const footer = document.querySelector('shade-data-grid-footer')
155
- const selects = Array.from(footer?.querySelectorAll('select') ?? [])
156
- const pageSelect = selects.find((s) => {
157
- const parent = s.parentElement
158
- return parent?.textContent?.includes('Goto page')
159
- })
155
+ const pagination = footer?.querySelector('shade-pagination')
156
+ expect(pagination).not.toBeNull()
160
157
 
161
- expect(pageSelect).toBeDefined()
162
- const options = pageSelect?.querySelectorAll('option')
163
- expect(options?.length).toBe(4)
158
+ const pageButtons = Array.from(pagination?.querySelectorAll('.pagination-item') ?? []).filter((btn) =>
159
+ btn.getAttribute('aria-label')?.startsWith('Go to page'),
160
+ )
161
+ expect(pageButtons.length).toBe(4)
164
162
  })
165
163
  })
166
164
 
167
- it('should update findOptions when page is changed', async () => {
165
+ it('should update findOptions when page is changed via Pagination', async () => {
168
166
  await usingAsync(new Injector(), async (injector) => {
169
167
  const rootElement = document.getElementById('root') as HTMLDivElement
170
168
  const service = createService([], 100)
@@ -176,24 +174,19 @@ describe('DataGridFooter', () => {
176
174
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
177
175
  })
178
176
 
179
- await sleepAsync(50)
177
+ await flushUpdates()
180
178
 
181
179
  const footer = document.querySelector('shade-data-grid-footer')
182
- const selects = Array.from(footer?.querySelectorAll('select') ?? [])
183
- const pageSelect = selects.find((s) => {
184
- const parent = s.parentElement
185
- return parent?.textContent?.includes('Goto page')
186
- })
180
+ const pagination = footer?.querySelector('shade-pagination')
181
+ const nextButton = pagination?.querySelector('[aria-label="Go to next page"]') as HTMLButtonElement | null
187
182
 
188
- expect(pageSelect).toBeDefined()
183
+ expect(nextButton).not.toBeNull()
184
+ nextButton!.click()
189
185
 
190
- pageSelect!.value = '2'
191
- pageSelect!.dispatchEvent(new Event('change', { bubbles: true }))
192
-
193
- await sleepAsync(50)
186
+ await flushUpdates()
194
187
 
195
188
  const updatedOptions = findOptions.getValue()
196
- expect(updatedOptions.skip).toBe(20)
189
+ expect(updatedOptions.skip).toBe(10)
197
190
  })
198
191
  })
199
192
 
@@ -209,13 +202,13 @@ describe('DataGridFooter', () => {
209
202
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
210
203
  })
211
204
 
212
- await sleepAsync(50)
205
+ await flushUpdates()
213
206
 
214
207
  const footer = document.querySelector('shade-data-grid-footer')
215
208
  const selects = Array.from(footer?.querySelectorAll('select') ?? [])
216
209
  const itemsPerPageSelect = selects.find((s) => {
217
- const parent = s.parentElement
218
- return parent?.textContent?.includes('items per page')
210
+ const parent = s.closest('.pager-section')
211
+ return parent?.textContent?.includes('Rows per page')
219
212
  })
220
213
 
221
214
  expect(itemsPerPageSelect).toBeDefined()
@@ -223,7 +216,7 @@ describe('DataGridFooter', () => {
223
216
  itemsPerPageSelect!.value = '25'
224
217
  itemsPerPageSelect!.dispatchEvent(new Event('change', { bubbles: true }))
225
218
 
226
- await sleepAsync(50)
219
+ await flushUpdates()
227
220
 
228
221
  const updatedOptions = findOptions.getValue()
229
222
  expect(updatedOptions.top).toBe(25)
@@ -242,13 +235,13 @@ describe('DataGridFooter', () => {
242
235
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
243
236
  })
244
237
 
245
- await sleepAsync(50)
238
+ await flushUpdates()
246
239
 
247
240
  const footer = document.querySelector('shade-data-grid-footer')
248
241
  const selects = Array.from(footer?.querySelectorAll('select') ?? [])
249
242
  const itemsPerPageSelect = selects.find((s) => {
250
- const parent = s.parentElement
251
- return parent?.textContent?.includes('items per page')
243
+ const parent = s.closest('.pager-section')
244
+ return parent?.textContent?.includes('Rows per page')
252
245
  })
253
246
 
254
247
  expect(itemsPerPageSelect).toBeDefined()
@@ -256,7 +249,7 @@ describe('DataGridFooter', () => {
256
249
  itemsPerPageSelect!.value = '25'
257
250
  itemsPerPageSelect!.dispatchEvent(new Event('change', { bubbles: true }))
258
251
 
259
- await sleepAsync(50)
252
+ await flushUpdates()
260
253
 
261
254
  const updatedOptions = findOptions.getValue()
262
255
  expect(updatedOptions.top).toBe(25)
@@ -264,7 +257,7 @@ describe('DataGridFooter', () => {
264
257
  })
265
258
  })
266
259
 
267
- it('should select the correct current page in the page selector', async () => {
260
+ it('should highlight the correct current page in Pagination', async () => {
268
261
  await usingAsync(new Injector(), async (injector) => {
269
262
  const rootElement = document.getElementById('root') as HTMLDivElement
270
263
  const service = createService([], 100)
@@ -276,17 +269,14 @@ describe('DataGridFooter', () => {
276
269
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
277
270
  })
278
271
 
279
- await sleepAsync(50)
272
+ await flushUpdates()
280
273
 
281
274
  const footer = document.querySelector('shade-data-grid-footer')
282
- const selects = Array.from(footer?.querySelectorAll('select') ?? [])
283
- const pageSelect = selects.find((s) => {
284
- const parent = s.parentElement
285
- return parent?.textContent?.includes('Goto page')
286
- })
275
+ const pagination = footer?.querySelector('shade-pagination')
276
+ const selectedButton = pagination?.querySelector('.pagination-item[data-selected]')
287
277
 
288
- expect(pageSelect).toBeDefined()
289
- expect(pageSelect?.value).toBe('3')
278
+ expect(selectedButton).not.toBeNull()
279
+ expect(selectedButton?.textContent?.trim()).toBe('4')
290
280
  })
291
281
  })
292
282
 
@@ -302,13 +292,13 @@ describe('DataGridFooter', () => {
302
292
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
303
293
  })
304
294
 
305
- await sleepAsync(50)
295
+ await flushUpdates()
306
296
 
307
297
  const footer = document.querySelector('shade-data-grid-footer')
308
298
  const selects = Array.from(footer?.querySelectorAll('select') ?? [])
309
299
  const itemsPerPageSelect = selects.find((s) => {
310
- const parent = s.parentElement
311
- return parent?.textContent?.includes('items per page')
300
+ const parent = s.closest('.pager-section')
301
+ return parent?.textContent?.includes('Rows per page')
312
302
  })
313
303
 
314
304
  expect(itemsPerPageSelect).toBeDefined()
@@ -319,7 +309,7 @@ describe('DataGridFooter', () => {
319
309
  it('should react to data count changes', async () => {
320
310
  await usingAsync(new Injector(), async (injector) => {
321
311
  const rootElement = document.getElementById('root') as HTMLDivElement
322
- const service = createService([], 50)
312
+ const service = createService([], 30)
323
313
  const findOptions = createFindOptions(10, 0)
324
314
 
325
315
  initializeShadeRoot({
@@ -328,29 +318,95 @@ describe('DataGridFooter', () => {
328
318
  jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
329
319
  })
330
320
 
331
- await sleepAsync(50)
321
+ await flushUpdates()
332
322
 
333
323
  let footer = document.querySelector('shade-data-grid-footer')
334
- let selects = Array.from(footer?.querySelectorAll('select') ?? [])
335
- let pageSelect = selects.find((s) => s.parentElement?.textContent?.includes('Goto page'))
336
- let pageOptions = pageSelect?.querySelectorAll('option')
324
+ let pagination = footer?.querySelector('shade-pagination')
325
+ let pageButtons = Array.from(pagination?.querySelectorAll('.pagination-item') ?? []).filter((btn) =>
326
+ btn.getAttribute('aria-label')?.startsWith('Go to page'),
327
+ )
328
+ expect(pageButtons.length).toBe(3)
337
329
 
338
- expect(pageOptions?.length).toBe(5)
330
+ service.data.setValue({ entries: [], count: 50 })
339
331
 
340
- service.data.setValue({ entries: [], count: 100 })
341
-
342
- await sleepAsync(50)
332
+ await flushUpdates()
343
333
 
344
334
  footer = document.querySelector('shade-data-grid-footer')
345
- selects = Array.from(footer?.querySelectorAll('select') ?? [])
346
- pageSelect = selects.find((s) => s.parentElement?.textContent?.includes('Goto page'))
347
- pageOptions = pageSelect?.querySelectorAll('option')
348
-
349
- expect(pageOptions?.length).toBe(10)
335
+ pagination = footer?.querySelector('shade-pagination')
336
+ pageButtons = Array.from(pagination?.querySelectorAll('.pagination-item') ?? []).filter((btn) =>
337
+ btn.getAttribute('aria-label')?.startsWith('Go to page'),
338
+ )
339
+ expect(pageButtons.length).toBe(5)
350
340
  })
351
341
  })
352
342
 
353
343
  it('should export dataGridItemsPerPage constant', () => {
354
344
  expect(dataGridItemsPerPage).toEqual([10, 20, 25, 50, 100, Infinity])
355
345
  })
346
+
347
+ it('should render custom paginationOptions', async () => {
348
+ await usingAsync(new Injector(), async (injector) => {
349
+ const rootElement = document.getElementById('root') as HTMLDivElement
350
+ const service = createService()
351
+ const findOptions = createFindOptions()
352
+
353
+ initializeShadeRoot({
354
+ injector,
355
+ rootElement,
356
+ jsxElement: <DataGridFooter service={service} findOptions={findOptions} paginationOptions={[5, 15, 30]} />,
357
+ })
358
+
359
+ await flushUpdates()
360
+
361
+ const footer = document.querySelector('shade-data-grid-footer')
362
+ const itemsPerPageSelect = footer?.querySelector('.pager-section select')
363
+
364
+ expect(itemsPerPageSelect).not.toBeNull()
365
+ const options = itemsPerPageSelect?.querySelectorAll('option')
366
+ expect(options?.length).toBe(3)
367
+ expect(options?.[0]?.textContent).toBe('5')
368
+ expect(options?.[1]?.textContent).toBe('15')
369
+ expect(options?.[2]?.textContent).toBe('30')
370
+ })
371
+ })
372
+
373
+ it('should hide the rows-per-page selector when only one paginationOption is provided', async () => {
374
+ await usingAsync(new Injector(), async (injector) => {
375
+ const rootElement = document.getElementById('root') as HTMLDivElement
376
+ const service = createService([], 50)
377
+ const findOptions = createFindOptions(10, 0)
378
+
379
+ initializeShadeRoot({
380
+ injector,
381
+ rootElement,
382
+ jsxElement: <DataGridFooter service={service} findOptions={findOptions} paginationOptions={[10]} />,
383
+ })
384
+
385
+ await flushUpdates()
386
+
387
+ const footer = document.querySelector('shade-data-grid-footer')
388
+ const pagerSection = footer?.querySelector('.pager-section')
389
+ expect(pagerSection).toBeNull()
390
+ })
391
+ })
392
+
393
+ it('should use default dataGridItemsPerPage when paginationOptions is not provided', async () => {
394
+ await usingAsync(new Injector(), async (injector) => {
395
+ const rootElement = document.getElementById('root') as HTMLDivElement
396
+ const service = createService()
397
+ const findOptions = createFindOptions()
398
+
399
+ initializeShadeRoot({
400
+ injector,
401
+ rootElement,
402
+ jsxElement: <DataGridFooter service={service} findOptions={findOptions} />,
403
+ })
404
+
405
+ await flushUpdates()
406
+
407
+ const footer = document.querySelector('shade-data-grid-footer')
408
+ const options = footer?.querySelectorAll('.pager-section select option')
409
+ expect(options?.length).toBe(dataGridItemsPerPage.length)
410
+ })
411
+ })
356
412
  })
@@ -3,12 +3,14 @@ import { Shade, createComponent } from '@furystack/shades'
3
3
  import type { ObservableValue } from '@furystack/utils'
4
4
  import type { CollectionService } from '../../services/collection-service.js'
5
5
  import { cssVariableTheme } from '../../services/css-variable-theme.js'
6
+ import { Pagination } from '../pagination.js'
6
7
 
7
8
  export const dataGridItemsPerPage = [10, 20, 25, 50, 100, Infinity]
8
9
 
9
10
  export const DataGridFooter: <T>(props: {
10
11
  service: CollectionService<T>
11
12
  findOptions: ObservableValue<FindOptions<T, Array<keyof T>>>
13
+ paginationOptions?: number[]
12
14
  }) => JSX.Element = Shade({
13
15
  shadowDomName: 'shade-data-grid-footer',
14
16
  css: {
@@ -20,15 +22,28 @@ export const DataGridFooter: <T>(props: {
20
22
  bottom: '0',
21
23
  display: 'flex',
22
24
  justifyContent: 'flex-end',
23
- padding: '1em',
25
+ padding: '8px 12px',
24
26
  alignItems: 'center',
27
+ gap: '16px',
28
+ fontSize: cssVariableTheme.typography.fontSize.xs,
29
+ },
30
+ '& .pager-section': {
31
+ display: 'flex',
32
+ alignItems: 'center',
33
+ gap: '6px',
25
34
  },
26
35
  '& select': {
27
- margin: '0 1em',
36
+ padding: '4px 8px',
37
+ borderRadius: cssVariableTheme.shape.borderRadius.sm,
38
+ border: `1px solid ${cssVariableTheme.divider}`,
39
+ background: cssVariableTheme.background.default,
40
+ color: cssVariableTheme.text.primary,
41
+ fontSize: cssVariableTheme.typography.fontSize.xs,
42
+ cursor: 'pointer',
28
43
  },
29
44
  },
30
45
  render: ({ props, useObservable }) => {
31
- const { service, findOptions } = props
46
+ const { service, findOptions, paginationOptions = dataGridItemsPerPage } = props
32
47
  const [currentData] = useObservable('dataUpdater', service.data)
33
48
  const [currentOptions, setCurrentOptions] = useObservable('optionsUpdater', findOptions, {
34
49
  filter: (newValue, oldValue) => {
@@ -41,49 +56,41 @@ export const DataGridFooter: <T>(props: {
41
56
  const currentPage = Math.ceil(skip) / (top || 1)
42
57
  const currentEntriesPerPage = top
43
58
 
44
- const pages = new Array(Math.ceil(currentData.count / (currentOptions.top || Infinity)))
45
- .fill(0)
46
- .map((_, index) => index)
59
+ const pageCount = Math.ceil(currentData.count / (currentOptions.top || Infinity))
47
60
 
48
61
  return (
49
62
  <div className="pager">
50
- {currentEntriesPerPage !== Infinity && (
51
- <div>
52
- Goto page
63
+ {currentEntriesPerPage !== Infinity && pageCount > 1 && (
64
+ <Pagination
65
+ count={pageCount}
66
+ page={currentPage + 1}
67
+ size="small"
68
+ onPageChange={(newPage) => {
69
+ setCurrentOptions({ ...currentOptions, skip: (currentOptions.top || 0) * (newPage - 1) })
70
+ }}
71
+ />
72
+ )}
73
+ {paginationOptions.length > 1 && (
74
+ <div className="pager-section">
75
+ <span>Rows per page</span>
53
76
  <select
54
77
  onchange={(ev) => {
55
- const value = parseInt((ev.target as HTMLInputElement).value, 10)
56
- setCurrentOptions({ ...currentOptions, skip: (currentOptions.top || 0) * value })
78
+ const value = parseInt((ev.currentTarget as HTMLInputElement).value, 10)
79
+ setCurrentOptions({
80
+ ...currentOptions,
81
+ top: value,
82
+ skip: currentPage * value,
83
+ })
57
84
  }}
58
85
  >
59
- {pages.map((index) => (
60
- <option value={index.toString()} selected={currentPage === index}>
61
- {(index + 1).toString()}
86
+ {paginationOptions.map((no) => (
87
+ <option value={no.toString()} selected={no === currentEntriesPerPage}>
88
+ {no === Infinity ? 'All' : no.toString()}
62
89
  </option>
63
90
  ))}
64
91
  </select>
65
92
  </div>
66
93
  )}
67
- <div>
68
- Show
69
- <select
70
- onchange={(ev) => {
71
- const value = parseInt((ev.currentTarget as HTMLInputElement).value, 10)
72
- setCurrentOptions({
73
- ...currentOptions,
74
- top: value,
75
- skip: currentPage * value,
76
- })
77
- }}
78
- >
79
- {dataGridItemsPerPage.map((no) => (
80
- <option value={no.toString()} selected={no === currentEntriesPerPage}>
81
- {no.toString()}
82
- </option>
83
- ))}
84
- </select>
85
- items per page
86
- </div>
87
94
  </div>
88
95
  )
89
96
  },