@furystack/shades-common-components 12.3.0 → 12.5.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 (268) hide show
  1. package/CHANGELOG.md +86 -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 +40 -2
  36. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  37. package/esm/components/data-grid/data-grid.js +7 -10
  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.map +1 -1
  98. package/esm/components/data-grid/footer.js +24 -9
  99. package/esm/components/data-grid/footer.js.map +1 -1
  100. package/esm/components/data-grid/footer.spec.js +38 -36
  101. package/esm/components/data-grid/footer.spec.js.map +1 -1
  102. package/esm/components/data-grid/header.d.ts +6 -9
  103. package/esm/components/data-grid/header.d.ts.map +1 -1
  104. package/esm/components/data-grid/header.js +51 -117
  105. package/esm/components/data-grid/header.js.map +1 -1
  106. package/esm/components/data-grid/header.spec.js +116 -187
  107. package/esm/components/data-grid/header.spec.js.map +1 -1
  108. package/esm/components/data-grid/index.d.ts +1 -0
  109. package/esm/components/data-grid/index.d.ts.map +1 -1
  110. package/esm/components/data-grid/index.js +1 -0
  111. package/esm/components/data-grid/index.js.map +1 -1
  112. package/esm/components/data-grid/selection-cell.spec.js +8 -8
  113. package/esm/components/data-grid/selection-cell.spec.js.map +1 -1
  114. package/esm/components/drawer/drawer-toggle-button.spec.js +22 -22
  115. package/esm/components/drawer/drawer-toggle-button.spec.js.map +1 -1
  116. package/esm/components/drawer/index.spec.js +36 -36
  117. package/esm/components/drawer/index.spec.js.map +1 -1
  118. package/esm/components/dropdown.spec.js +38 -30
  119. package/esm/components/dropdown.spec.js.map +1 -1
  120. package/esm/components/fab.spec.js +4 -4
  121. package/esm/components/fab.spec.js.map +1 -1
  122. package/esm/components/form.d.ts +5 -2
  123. package/esm/components/form.d.ts.map +1 -1
  124. package/esm/components/form.js +28 -6
  125. package/esm/components/form.js.map +1 -1
  126. package/esm/components/form.spec.js +227 -20
  127. package/esm/components/form.spec.js.map +1 -1
  128. package/esm/components/grid.spec.js +3 -3
  129. package/esm/components/grid.spec.js.map +1 -1
  130. package/esm/components/image.spec.js +55 -52
  131. package/esm/components/image.spec.js.map +1 -1
  132. package/esm/components/inputs/autocomplete.spec.js +7 -14
  133. package/esm/components/inputs/autocomplete.spec.js.map +1 -1
  134. package/esm/components/inputs/checkbox.spec.js +22 -22
  135. package/esm/components/inputs/checkbox.spec.js.map +1 -1
  136. package/esm/components/inputs/input-number.spec.js +47 -47
  137. package/esm/components/inputs/input-number.spec.js.map +1 -1
  138. package/esm/components/inputs/input.spec.js +53 -53
  139. package/esm/components/inputs/input.spec.js.map +1 -1
  140. package/esm/components/inputs/radio-group.spec.js +14 -14
  141. package/esm/components/inputs/radio-group.spec.js.map +1 -1
  142. package/esm/components/inputs/radio.spec.js +16 -16
  143. package/esm/components/inputs/radio.spec.js.map +1 -1
  144. package/esm/components/inputs/select.spec.js +74 -74
  145. package/esm/components/inputs/select.spec.js.map +1 -1
  146. package/esm/components/inputs/slider.spec.js +16 -16
  147. package/esm/components/inputs/slider.spec.js.map +1 -1
  148. package/esm/components/inputs/switch.spec.js +24 -24
  149. package/esm/components/inputs/switch.spec.js.map +1 -1
  150. package/esm/components/inputs/text-area.spec.js +17 -17
  151. package/esm/components/inputs/text-area.spec.js.map +1 -1
  152. package/esm/components/linear-progress.spec.js +2 -2
  153. package/esm/components/list/list.spec.js +36 -36
  154. package/esm/components/list/list.spec.js.map +1 -1
  155. package/esm/components/markdown/markdown-display.spec.js +15 -15
  156. package/esm/components/markdown/markdown-display.spec.js.map +1 -1
  157. package/esm/components/markdown/markdown-editor.spec.js +8 -8
  158. package/esm/components/markdown/markdown-editor.spec.js.map +1 -1
  159. package/esm/components/markdown/markdown-input.spec.js +17 -17
  160. package/esm/components/markdown/markdown-input.spec.js.map +1 -1
  161. package/esm/components/menu/menu.spec.js +28 -28
  162. package/esm/components/menu/menu.spec.js.map +1 -1
  163. package/esm/components/modal.spec.js +15 -18
  164. package/esm/components/modal.spec.js.map +1 -1
  165. package/esm/components/noty-list.spec.js +25 -23
  166. package/esm/components/noty-list.spec.js.map +1 -1
  167. package/esm/components/page-container/index.spec.js +16 -16
  168. package/esm/components/page-container/index.spec.js.map +1 -1
  169. package/esm/components/page-container/page-header.spec.js +16 -16
  170. package/esm/components/page-container/page-header.spec.js.map +1 -1
  171. package/esm/components/page-layout/index.spec.js +29 -29
  172. package/esm/components/page-layout/index.spec.js.map +1 -1
  173. package/esm/components/paper.spec.js +3 -3
  174. package/esm/components/paper.spec.js.map +1 -1
  175. package/esm/components/rating.spec.js +61 -61
  176. package/esm/components/rating.spec.js.map +1 -1
  177. package/esm/components/skeleton.spec.js +10 -6
  178. package/esm/components/skeleton.spec.js.map +1 -1
  179. package/esm/components/suggest/suggest-input.spec.js +4 -10
  180. package/esm/components/suggest/suggest-input.spec.js.map +1 -1
  181. package/esm/components/tabs.spec.js +30 -30
  182. package/esm/components/tabs.spec.js.map +1 -1
  183. package/esm/components/tree/tree.spec.js +27 -27
  184. package/esm/components/tree/tree.spec.js.map +1 -1
  185. package/esm/components/typography.spec.js +3 -3
  186. package/esm/components/typography.spec.js.map +1 -1
  187. package/esm/components/wizard/index.spec.js +5 -5
  188. package/esm/components/wizard/index.spec.js.map +1 -1
  189. package/esm/utils/promisify-animation.d.ts.map +1 -1
  190. package/esm/utils/promisify-animation.js +3 -0
  191. package/esm/utils/promisify-animation.js.map +1 -1
  192. package/package.json +2 -2
  193. package/src/components/app-bar-link.spec.tsx +16 -19
  194. package/src/components/app-bar.spec.tsx +6 -4
  195. package/src/components/avatar.spec.tsx +9 -9
  196. package/src/components/breadcrumb.spec.tsx +2 -2
  197. package/src/components/button-group.spec.tsx +155 -11
  198. package/src/components/button-group.tsx +49 -2
  199. package/src/components/button.spec.tsx +4 -4
  200. package/src/components/cache-view.spec.tsx +3 -3
  201. package/src/components/carousel.spec.tsx +47 -47
  202. package/src/components/circular-progress.spec.tsx +2 -2
  203. package/src/components/command-palette/command-palette-input.spec.tsx +23 -19
  204. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +27 -27
  205. package/src/components/command-palette/index.spec.tsx +64 -51
  206. package/src/components/context-menu/context-menu.spec.tsx +33 -33
  207. package/src/components/data-grid/body.spec.tsx +13 -13
  208. package/src/components/data-grid/data-grid-row.spec.tsx +8 -8
  209. package/src/components/data-grid/data-grid.spec.tsx +106 -28
  210. package/src/components/data-grid/data-grid.tsx +44 -11
  211. package/src/components/data-grid/filters/boolean-filter.spec.tsx +142 -0
  212. package/src/components/data-grid/filters/boolean-filter.tsx +45 -0
  213. package/src/components/data-grid/filters/date-filter.spec.tsx +181 -0
  214. package/src/components/data-grid/filters/date-filter.tsx +162 -0
  215. package/src/components/data-grid/filters/enum-filter.spec.tsx +168 -0
  216. package/src/components/data-grid/filters/enum-filter.tsx +119 -0
  217. package/src/components/data-grid/filters/filter-dropdown.spec.tsx +89 -0
  218. package/src/components/data-grid/filters/filter-dropdown.tsx +60 -0
  219. package/src/components/data-grid/filters/filter-styles.ts +26 -0
  220. package/src/components/data-grid/filters/index.ts +6 -0
  221. package/src/components/data-grid/filters/number-filter.spec.tsx +174 -0
  222. package/src/components/data-grid/filters/number-filter.tsx +115 -0
  223. package/src/components/data-grid/filters/string-filter.spec.tsx +157 -0
  224. package/src/components/data-grid/filters/string-filter.tsx +112 -0
  225. package/src/components/data-grid/footer.spec.tsx +38 -36
  226. package/src/components/data-grid/footer.tsx +21 -8
  227. package/src/components/data-grid/header.spec.tsx +128 -212
  228. package/src/components/data-grid/header.tsx +95 -183
  229. package/src/components/data-grid/index.tsx +1 -0
  230. package/src/components/data-grid/selection-cell.spec.tsx +8 -8
  231. package/src/components/drawer/drawer-toggle-button.spec.tsx +22 -22
  232. package/src/components/drawer/index.spec.tsx +36 -36
  233. package/src/components/dropdown.spec.tsx +38 -30
  234. package/src/components/fab.spec.tsx +4 -4
  235. package/src/components/form.spec.tsx +329 -20
  236. package/src/components/form.tsx +31 -8
  237. package/src/components/grid.spec.tsx +3 -3
  238. package/src/components/image.spec.tsx +55 -52
  239. package/src/components/inputs/autocomplete.spec.tsx +7 -14
  240. package/src/components/inputs/checkbox.spec.tsx +22 -22
  241. package/src/components/inputs/input-number.spec.tsx +47 -47
  242. package/src/components/inputs/input.spec.tsx +53 -53
  243. package/src/components/inputs/radio-group.spec.tsx +14 -14
  244. package/src/components/inputs/radio.spec.tsx +16 -16
  245. package/src/components/inputs/select.spec.tsx +74 -74
  246. package/src/components/inputs/slider.spec.tsx +16 -16
  247. package/src/components/inputs/switch.spec.tsx +24 -24
  248. package/src/components/inputs/text-area.spec.tsx +17 -17
  249. package/src/components/linear-progress.spec.tsx +2 -2
  250. package/src/components/list/list.spec.tsx +36 -36
  251. package/src/components/markdown/markdown-display.spec.tsx +15 -15
  252. package/src/components/markdown/markdown-editor.spec.tsx +8 -8
  253. package/src/components/markdown/markdown-input.spec.tsx +17 -17
  254. package/src/components/menu/menu.spec.tsx +28 -28
  255. package/src/components/modal.spec.tsx +15 -18
  256. package/src/components/noty-list.spec.tsx +25 -23
  257. package/src/components/page-container/index.spec.tsx +16 -16
  258. package/src/components/page-container/page-header.spec.tsx +16 -16
  259. package/src/components/page-layout/index.spec.tsx +29 -29
  260. package/src/components/paper.spec.tsx +3 -3
  261. package/src/components/rating.spec.tsx +61 -61
  262. package/src/components/skeleton.spec.tsx +10 -6
  263. package/src/components/suggest/suggest-input.spec.tsx +4 -10
  264. package/src/components/tabs.spec.tsx +30 -30
  265. package/src/components/tree/tree.spec.tsx +27 -27
  266. package/src/components/typography.spec.tsx +3 -3
  267. package/src/components/wizard/index.spec.tsx +5 -5
  268. package/src/utils/promisify-animation.ts +3 -0
@@ -0,0 +1,89 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
3
+ import { usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { FilterDropdown } from './filter-dropdown.js'
6
+
7
+ describe('FilterDropdown', () => {
8
+ beforeEach(() => {
9
+ document.body.innerHTML = '<div id="root"></div>'
10
+ })
11
+
12
+ afterEach(() => {
13
+ document.body.innerHTML = ''
14
+ })
15
+
16
+ it('should render children inside panel', async () => {
17
+ const onClose = vi.fn()
18
+ await usingAsync(new Injector(), async (injector) => {
19
+ const rootElement = document.getElementById('root')!
20
+ initializeShadeRoot({
21
+ injector,
22
+ rootElement,
23
+ jsxElement: (
24
+ <FilterDropdown onClose={onClose}>
25
+ <span className="test-child">Hello</span>
26
+ </FilterDropdown>
27
+ ),
28
+ })
29
+ await flushUpdates()
30
+
31
+ const dropdown = document.querySelector('data-grid-filter-dropdown')
32
+ expect(dropdown).not.toBeNull()
33
+
34
+ const panel = dropdown?.querySelector('.filter-dropdown-panel')
35
+ expect(panel).not.toBeNull()
36
+
37
+ const child = panel?.querySelector('.test-child')
38
+ expect(child).not.toBeNull()
39
+ expect(child?.textContent).toBe('Hello')
40
+ })
41
+ })
42
+
43
+ it('should add visible class after animation frame', async () => {
44
+ const onClose = vi.fn()
45
+ await usingAsync(new Injector(), async (injector) => {
46
+ const rootElement = document.getElementById('root')!
47
+ initializeShadeRoot({
48
+ injector,
49
+ rootElement,
50
+ jsxElement: (
51
+ <FilterDropdown onClose={onClose}>
52
+ <span>Content</span>
53
+ </FilterDropdown>
54
+ ),
55
+ })
56
+ await flushUpdates()
57
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
58
+
59
+ const panel = document.querySelector('.filter-dropdown-panel')
60
+ expect(panel?.classList.contains('visible')).toBe(true)
61
+ })
62
+ })
63
+
64
+ it('should stop click propagation on panel', async () => {
65
+ const onClose = vi.fn()
66
+ const outerClick = vi.fn()
67
+ await usingAsync(new Injector(), async (injector) => {
68
+ const rootElement = document.getElementById('root')!
69
+ initializeShadeRoot({
70
+ injector,
71
+ rootElement,
72
+ jsxElement: (
73
+ <div onclick={outerClick}>
74
+ <FilterDropdown onClose={onClose}>
75
+ <span className="inner">Content</span>
76
+ </FilterDropdown>
77
+ </div>
78
+ ),
79
+ })
80
+ await flushUpdates()
81
+
82
+ const panel = document.querySelector('.filter-dropdown-panel') as HTMLElement
83
+ panel?.click()
84
+ await flushUpdates()
85
+
86
+ expect(outerClick).not.toHaveBeenCalled()
87
+ })
88
+ })
89
+ })
@@ -0,0 +1,60 @@
1
+ import type { ChildrenList } from '@furystack/shades'
2
+ import { createComponent, Shade } from '@furystack/shades'
3
+ import { ClickAwayService } from '../../../services/click-away-service.js'
4
+ import { buildTransition, cssVariableTheme } from '../../../services/css-variable-theme.js'
5
+
6
+ export type FilterDropdownProps = {
7
+ onClose: () => void
8
+ }
9
+
10
+ export const FilterDropdown: (props: FilterDropdownProps, children: ChildrenList) => JSX.Element = Shade({
11
+ shadowDomName: 'data-grid-filter-dropdown',
12
+ css: {
13
+ display: 'block',
14
+ position: 'absolute',
15
+ top: '100%',
16
+ left: '0',
17
+ zIndex: '10',
18
+ '& .filter-dropdown-panel': {
19
+ background: cssVariableTheme.background.paper,
20
+ borderRadius: cssVariableTheme.shape.borderRadius.md,
21
+ boxShadow: cssVariableTheme.shadows.lg,
22
+ border: `1px solid ${cssVariableTheme.divider}`,
23
+ padding: cssVariableTheme.spacing.md,
24
+ opacity: '0',
25
+ transform: 'scale(0.95) translateY(-4px)',
26
+ transition: buildTransition(
27
+ ['opacity', cssVariableTheme.transitions.duration.fast, 'ease-out'],
28
+ ['transform', cssVariableTheme.transitions.duration.fast, 'ease-out'],
29
+ ),
30
+ },
31
+ '& .filter-dropdown-panel.visible': {
32
+ opacity: '1',
33
+ transform: 'scale(1) translateY(0)',
34
+ },
35
+ },
36
+ render: ({ props, children, useDisposable, useRef }) => {
37
+ const panelRef = useRef<HTMLDivElement>('panel')
38
+
39
+ useDisposable(
40
+ 'clickAway',
41
+ () =>
42
+ new ClickAwayService(panelRef, () => {
43
+ props.onClose()
44
+ }),
45
+ )
46
+
47
+ useDisposable('animateIn', () => {
48
+ const id = requestAnimationFrame(() => {
49
+ panelRef.current?.classList.add('visible')
50
+ })
51
+ return { [Symbol.dispose]: () => cancelAnimationFrame(id) }
52
+ })
53
+
54
+ return (
55
+ <div ref={panelRef} className="filter-dropdown-panel" onclick={(ev: MouseEvent) => ev.stopPropagation()}>
56
+ {children}
57
+ </div>
58
+ )
59
+ },
60
+ })
@@ -0,0 +1,26 @@
1
+ import { cssVariableTheme } from '../../../services/css-variable-theme.js'
2
+
3
+ export const filterBaseCss = {
4
+ display: 'block' as const,
5
+ '& .filter-row': {
6
+ display: 'flex',
7
+ gap: '6px',
8
+ alignItems: 'center',
9
+ marginBottom: '8px',
10
+ },
11
+ '& .filter-actions': {
12
+ display: 'flex',
13
+ justifyContent: 'flex-end',
14
+ gap: '4px',
15
+ },
16
+ }
17
+
18
+ export const filterInputCss = {
19
+ flex: '1',
20
+ padding: '4px 6px',
21
+ borderRadius: cssVariableTheme.shape.borderRadius.sm,
22
+ border: `1px solid ${cssVariableTheme.divider}`,
23
+ background: cssVariableTheme.background.default,
24
+ color: cssVariableTheme.text.primary,
25
+ fontSize: cssVariableTheme.typography.fontSize.xs,
26
+ }
@@ -0,0 +1,6 @@
1
+ export * from './filter-dropdown.js'
2
+ export * from './string-filter.js'
3
+ export * from './number-filter.js'
4
+ export * from './boolean-filter.js'
5
+ export * from './enum-filter.js'
6
+ export * from './date-filter.js'
@@ -0,0 +1,174 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
3
+ import { ObservableValue, usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import type { FilterableFindOptions } from '../data-grid.js'
6
+ import { NumberFilter } from './number-filter.js'
7
+
8
+ describe('NumberFilter', () => {
9
+ beforeEach(() => {
10
+ document.body.innerHTML = '<div id="root"></div>'
11
+ })
12
+
13
+ afterEach(() => {
14
+ document.body.innerHTML = ''
15
+ })
16
+
17
+ const createFindOptions = (options: Partial<FilterableFindOptions> = {}): ObservableValue<FilterableFindOptions> => {
18
+ return new ObservableValue<FilterableFindOptions>(options)
19
+ }
20
+
21
+ const renderNumberFilter = async (
22
+ findOptions: ObservableValue<FilterableFindOptions>,
23
+ field = 'level',
24
+ onClose = vi.fn(),
25
+ ) => {
26
+ const injector = new Injector()
27
+ const rootElement = document.getElementById('root')!
28
+ initializeShadeRoot({
29
+ injector,
30
+ rootElement,
31
+ jsxElement: <NumberFilter field={field} findOptions={findOptions} onClose={onClose} />,
32
+ })
33
+ await flushUpdates()
34
+ return { injector, onClose }
35
+ }
36
+
37
+ it('should render operator segmented control and input', async () => {
38
+ const findOptions = createFindOptions()
39
+ await usingAsync((await renderNumberFilter(findOptions)).injector, async () => {
40
+ const control = document.querySelector('shade-segmented-control')
41
+ expect(control).not.toBeNull()
42
+
43
+ const input = document.querySelector('[data-testid="number-filter-value"]')
44
+ expect(input).not.toBeNull()
45
+ })
46
+ })
47
+
48
+ it('should apply $eq filter on submit', async () => {
49
+ const findOptions = createFindOptions()
50
+ const { injector, onClose } = await renderNumberFilter(findOptions)
51
+ await usingAsync(injector, async () => {
52
+ const input = document.querySelector('[data-testid="number-filter-value"]') as HTMLInputElement
53
+ input.value = '42'
54
+ input.dispatchEvent(new Event('input', { bubbles: true }))
55
+
56
+ const form = document.querySelector('form') as HTMLFormElement
57
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
58
+ await flushUpdates()
59
+
60
+ expect(findOptions.getValue().filter).toEqual({ level: { $eq: 42 } })
61
+ expect(onClose).toHaveBeenCalled()
62
+ })
63
+ })
64
+
65
+ it('should apply $gt operator when selected', async () => {
66
+ const findOptions = createFindOptions()
67
+ const { injector, onClose } = await renderNumberFilter(findOptions)
68
+ await usingAsync(injector, async () => {
69
+ const gtButton = document.querySelector('shade-segmented-control button[data-value="$gt"]') as HTMLButtonElement
70
+ gtButton?.click()
71
+ await flushUpdates()
72
+
73
+ const input = document.querySelector('[data-testid="number-filter-value"]') as HTMLInputElement
74
+ input.value = '10'
75
+ input.dispatchEvent(new Event('input', { bubbles: true }))
76
+
77
+ const form = document.querySelector('form') as HTMLFormElement
78
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
79
+ await flushUpdates()
80
+
81
+ expect(findOptions.getValue().filter).toEqual({ level: { $gt: 10 } })
82
+ expect(onClose).toHaveBeenCalled()
83
+ })
84
+ })
85
+
86
+ it('should apply $lte operator when selected', async () => {
87
+ const findOptions = createFindOptions()
88
+ const { injector, onClose } = await renderNumberFilter(findOptions)
89
+ await usingAsync(injector, async () => {
90
+ const lteButton = document.querySelector('shade-segmented-control button[data-value="$lte"]') as HTMLButtonElement
91
+ lteButton?.click()
92
+ await flushUpdates()
93
+
94
+ const input = document.querySelector('[data-testid="number-filter-value"]') as HTMLInputElement
95
+ input.value = '99.5'
96
+ input.dispatchEvent(new Event('input', { bubbles: true }))
97
+
98
+ const form = document.querySelector('form') as HTMLFormElement
99
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
100
+ await flushUpdates()
101
+
102
+ expect(findOptions.getValue().filter).toEqual({ level: { $lte: 99.5 } })
103
+ expect(onClose).toHaveBeenCalled()
104
+ })
105
+ })
106
+
107
+ it('should clear filter when Clear button is clicked', async () => {
108
+ const findOptions = createFindOptions({ filter: { level: { $eq: 5 } } })
109
+ const { injector, onClose } = await renderNumberFilter(findOptions)
110
+ await usingAsync(injector, async () => {
111
+ const clearButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Clear')
112
+ clearButton?.click()
113
+ await flushUpdates()
114
+
115
+ expect(findOptions.getValue().filter?.level).toBeUndefined()
116
+ expect(onClose).toHaveBeenCalled()
117
+ })
118
+ })
119
+
120
+ it('should remove filter when submitting NaN value', async () => {
121
+ const findOptions = createFindOptions({ filter: { level: { $eq: 5 } } })
122
+ const { injector, onClose } = await renderNumberFilter(findOptions)
123
+ await usingAsync(injector, async () => {
124
+ const input = document.querySelector('[data-testid="number-filter-value"]') as HTMLInputElement
125
+ input.value = ''
126
+ input.dispatchEvent(new Event('input', { bubbles: true }))
127
+
128
+ const form = document.querySelector('form') as HTMLFormElement
129
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
130
+ await flushUpdates()
131
+
132
+ expect(findOptions.getValue().filter?.level).toBeUndefined()
133
+ expect(onClose).toHaveBeenCalled()
134
+ })
135
+ })
136
+
137
+ it('should preserve filters on other fields', async () => {
138
+ const findOptions = createFindOptions({ filter: { level: { $gt: 10 }, name: { $regex: 'keep' } } })
139
+ const { injector } = await renderNumberFilter(findOptions)
140
+ await usingAsync(injector, async () => {
141
+ const clearButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Clear')
142
+ clearButton?.click()
143
+ await flushUpdates()
144
+
145
+ const updatedFilter = findOptions.getValue().filter
146
+ expect(updatedFilter?.level).toBeUndefined()
147
+ expect(updatedFilter?.name).toEqual({ $regex: 'keep' })
148
+ })
149
+ })
150
+
151
+ it('should reset skip to 0 when applying filter', async () => {
152
+ const findOptions = createFindOptions({ skip: 20 })
153
+ const { injector } = await renderNumberFilter(findOptions)
154
+ await usingAsync(injector, async () => {
155
+ const input = document.querySelector('[data-testid="number-filter-value"]') as HTMLInputElement
156
+ input.value = '5'
157
+ input.dispatchEvent(new Event('input', { bubbles: true }))
158
+
159
+ const form = document.querySelector('form') as HTMLFormElement
160
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
161
+ await flushUpdates()
162
+
163
+ expect(findOptions.getValue().skip).toBe(0)
164
+ })
165
+ })
166
+
167
+ it('should show current filter value in input', async () => {
168
+ const findOptions = createFindOptions({ filter: { level: { $eq: 25 } } })
169
+ await usingAsync((await renderNumberFilter(findOptions)).injector, async () => {
170
+ const input = document.querySelector('[data-testid="number-filter-value"]') as HTMLInputElement
171
+ expect(input.value).toBe('25')
172
+ })
173
+ })
174
+ })
@@ -0,0 +1,115 @@
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 NumberOperator = '$eq' | '$gt' | '$gte' | '$lt' | '$lte'
11
+
12
+ const operatorLabels: Record<NumberOperator, string> = {
13
+ $eq: '=',
14
+ $gt: '>',
15
+ $gte: '>=',
16
+ $lt: '<',
17
+ $lte: '<=',
18
+ }
19
+
20
+ export const NumberFilter = Shade<{
21
+ field: string
22
+ findOptions: ObservableValue<FilterableFindOptions>
23
+ onClose: () => void
24
+ }>({
25
+ shadowDomName: 'data-grid-number-filter',
26
+ css: {
27
+ ...filterBaseCss,
28
+ '& input': filterInputCss,
29
+ },
30
+ render: ({ props, useObservable, useState }) => {
31
+ const [findOptions, setFindOptions] = useObservable('findOptions', props.findOptions)
32
+
33
+ const currentFilter = findOptions.filter?.[props.field] as Record<string, number> | undefined
34
+ const currentOperator: NumberOperator = currentFilter
35
+ ? ((Object.keys(currentFilter).find((k) => k in operatorLabels) as NumberOperator) ?? '$eq')
36
+ : '$eq'
37
+ const currentValue = currentFilter?.[currentOperator]
38
+
39
+ const applyFilter = (operator: NumberOperator, value: string) => {
40
+ const num = parseFloat(value)
41
+ const filter = { ...findOptions.filter }
42
+ if (isNaN(num)) {
43
+ delete filter[props.field]
44
+ } else {
45
+ filter[props.field] = { [operator]: num }
46
+ }
47
+ setFindOptions({ ...findOptions, filter, skip: 0 })
48
+ props.onClose()
49
+ }
50
+
51
+ const clearFilter = () => {
52
+ const filter = { ...findOptions.filter }
53
+ delete filter[props.field]
54
+ setFindOptions({ ...findOptions, filter, skip: 0 })
55
+ props.onClose()
56
+ }
57
+
58
+ const [selectedOperator, setSelectedOperator] = useState<NumberOperator>('selectedOperator', currentOperator)
59
+ let inputValue = currentValue !== undefined ? currentValue.toString() : ''
60
+
61
+ return (
62
+ <form
63
+ onsubmit={(ev: Event) => {
64
+ ev.preventDefault()
65
+ applyFilter(selectedOperator, inputValue)
66
+ }}
67
+ >
68
+ <div className="filter-row">
69
+ <SegmentedControl
70
+ size="small"
71
+ value={selectedOperator}
72
+ onValueChange={(v) => setSelectedOperator(v as NumberOperator)}
73
+ options={(Object.keys(operatorLabels) as NumberOperator[]).map((op) => ({
74
+ value: op,
75
+ label: operatorLabels[op],
76
+ }))}
77
+ />
78
+ </div>
79
+ <div className="filter-row">
80
+ <input
81
+ data-testid="number-filter-value"
82
+ type="number"
83
+ step="any"
84
+ placeholder="Value..."
85
+ value={inputValue}
86
+ autofocus
87
+ oninput={(ev: Event) => {
88
+ inputValue = (ev.target as HTMLInputElement).value
89
+ }}
90
+ />
91
+ </div>
92
+ <div className="filter-actions">
93
+ <Button
94
+ type="button"
95
+ variant="outlined"
96
+ size="small"
97
+ onclick={clearFilter}
98
+ startIcon={<Icon icon={closeIcon} size={14} />}
99
+ >
100
+ Clear
101
+ </Button>
102
+ <Button
103
+ type="submit"
104
+ variant="outlined"
105
+ size="small"
106
+ color="primary"
107
+ startIcon={<Icon icon={searchIcon} size={14} />}
108
+ >
109
+ Apply
110
+ </Button>
111
+ </div>
112
+ </form>
113
+ )
114
+ },
115
+ })
@@ -0,0 +1,157 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'
3
+ import { ObservableValue, usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import type { FilterableFindOptions } from '../data-grid.js'
6
+ import { StringFilter } from './string-filter.js'
7
+
8
+ describe('StringFilter', () => {
9
+ beforeEach(() => {
10
+ document.body.innerHTML = '<div id="root"></div>'
11
+ })
12
+
13
+ afterEach(() => {
14
+ document.body.innerHTML = ''
15
+ })
16
+
17
+ const createFindOptions = (options: Partial<FilterableFindOptions> = {}): ObservableValue<FilterableFindOptions> => {
18
+ return new ObservableValue<FilterableFindOptions>(options)
19
+ }
20
+
21
+ const renderStringFilter = async (
22
+ findOptions: ObservableValue<FilterableFindOptions>,
23
+ field = 'name',
24
+ onClose = vi.fn(),
25
+ ) => {
26
+ const injector = new Injector()
27
+ const rootElement = document.getElementById('root')!
28
+ initializeShadeRoot({
29
+ injector,
30
+ rootElement,
31
+ jsxElement: <StringFilter field={field} findOptions={findOptions} onClose={onClose} />,
32
+ })
33
+ await flushUpdates()
34
+ return { injector, onClose }
35
+ }
36
+
37
+ it('should render operator segmented control and input', async () => {
38
+ const findOptions = createFindOptions()
39
+ await usingAsync((await renderStringFilter(findOptions)).injector, async () => {
40
+ const control = document.querySelector('shade-segmented-control')
41
+ expect(control).not.toBeNull()
42
+
43
+ const input = document.querySelector('[data-testid="string-filter-value"]')
44
+ expect(input).not.toBeNull()
45
+ })
46
+ })
47
+
48
+ it('should apply $regex filter on submit', async () => {
49
+ const findOptions = createFindOptions()
50
+ const { injector, onClose } = await renderStringFilter(findOptions)
51
+ await usingAsync(injector, async () => {
52
+ const input = document.querySelector('[data-testid="string-filter-value"]') as HTMLInputElement
53
+ input.value = 'test-value'
54
+ input.dispatchEvent(new Event('input', { bubbles: true }))
55
+
56
+ const form = document.querySelector('form') as HTMLFormElement
57
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
58
+ await flushUpdates()
59
+
60
+ expect(findOptions.getValue().filter).toEqual({ name: { $regex: 'test-value' } })
61
+ expect(onClose).toHaveBeenCalled()
62
+ })
63
+ })
64
+
65
+ it('should clear filter when Clear button is clicked', async () => {
66
+ const findOptions = createFindOptions({ filter: { name: { $regex: 'existing' } } })
67
+ const { injector, onClose } = await renderStringFilter(findOptions)
68
+ await usingAsync(injector, async () => {
69
+ const clearButton = Array.from(document.querySelectorAll('button')).find((b) => b.textContent?.trim() === 'Clear')
70
+ clearButton?.click()
71
+ await flushUpdates()
72
+
73
+ expect(findOptions.getValue().filter?.name).toBeUndefined()
74
+ expect(onClose).toHaveBeenCalled()
75
+ })
76
+ })
77
+
78
+ it('should remove filter when submitting empty value', async () => {
79
+ const findOptions = createFindOptions({ filter: { name: { $regex: 'existing' } } })
80
+ const { injector, onClose } = await renderStringFilter(findOptions)
81
+ await usingAsync(injector, async () => {
82
+ const input = document.querySelector('[data-testid="string-filter-value"]') as HTMLInputElement
83
+ input.value = ''
84
+ input.dispatchEvent(new Event('input', { bubbles: true }))
85
+
86
+ const form = document.querySelector('form') as HTMLFormElement
87
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
88
+ await flushUpdates()
89
+
90
+ expect(findOptions.getValue().filter?.name).toBeUndefined()
91
+ expect(onClose).toHaveBeenCalled()
92
+ })
93
+ })
94
+
95
+ it('should preserve filters on other fields', async () => {
96
+ const findOptions = createFindOptions({ filter: { name: { $regex: 'old' }, email: { $regex: 'keep' } } })
97
+ const { injector } = await renderStringFilter(findOptions)
98
+ await usingAsync(injector, async () => {
99
+ const input = document.querySelector('[data-testid="string-filter-value"]') as HTMLInputElement
100
+ input.value = 'new-value'
101
+ input.dispatchEvent(new Event('input', { bubbles: true }))
102
+
103
+ const form = document.querySelector('form') as HTMLFormElement
104
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
105
+ await flushUpdates()
106
+
107
+ const updatedFilter = findOptions.getValue().filter
108
+ expect(updatedFilter?.name).toEqual({ $regex: 'new-value' })
109
+ expect(updatedFilter?.email).toEqual({ $regex: 'keep' })
110
+ })
111
+ })
112
+
113
+ it('should apply selected operator', async () => {
114
+ const findOptions = createFindOptions()
115
+ const { injector, onClose } = await renderStringFilter(findOptions)
116
+ await usingAsync(injector, async () => {
117
+ const eqButton = document.querySelector('shade-segmented-control button[data-value="$eq"]') as HTMLButtonElement
118
+ eqButton?.click()
119
+ await flushUpdates()
120
+
121
+ const input = document.querySelector('[data-testid="string-filter-value"]') as HTMLInputElement
122
+ input.value = 'exact'
123
+ input.dispatchEvent(new Event('input', { bubbles: true }))
124
+
125
+ const form = document.querySelector('form') as HTMLFormElement
126
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
127
+ await flushUpdates()
128
+
129
+ expect(findOptions.getValue().filter).toEqual({ name: { $eq: 'exact' } })
130
+ expect(onClose).toHaveBeenCalled()
131
+ })
132
+ })
133
+
134
+ it('should reset skip to 0 when applying filter', async () => {
135
+ const findOptions = createFindOptions({ skip: 20 })
136
+ const { injector } = await renderStringFilter(findOptions)
137
+ await usingAsync(injector, async () => {
138
+ const input = document.querySelector('[data-testid="string-filter-value"]') as HTMLInputElement
139
+ input.value = 'test'
140
+ input.dispatchEvent(new Event('input', { bubbles: true }))
141
+
142
+ const form = document.querySelector('form') as HTMLFormElement
143
+ form.dispatchEvent(new Event('submit', { bubbles: true }))
144
+ await flushUpdates()
145
+
146
+ expect(findOptions.getValue().skip).toBe(0)
147
+ })
148
+ })
149
+
150
+ it('should show current filter value in input', async () => {
151
+ const findOptions = createFindOptions({ filter: { name: { $regex: 'current-value' } } })
152
+ await usingAsync((await renderStringFilter(findOptions)).injector, async () => {
153
+ const input = document.querySelector('[data-testid="string-filter-value"]') as HTMLInputElement
154
+ expect(input.value).toBe('current-value')
155
+ })
156
+ })
157
+ })