ngwr 11.0.0 → 11.2.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 (237) hide show
  1. package/action-sheet/styles/_index.scss +4 -1
  2. package/anchor/styles/_index.scss +2 -1
  3. package/calendar/styles/_index.scss +11 -2
  4. package/cascader/styles/_index.scss +3 -1
  5. package/circular-text/styles/_index.scss +11 -0
  6. package/command-palette/styles/_index.scss +14 -1
  7. package/context-menu/styles/_index.scss +2 -1
  8. package/falling-text/styles/_index.scss +2 -1
  9. package/fesm2022/ngwr-action-sheet-testing.mjs +313 -0
  10. package/fesm2022/ngwr-action-sheet-testing.mjs.map +1 -0
  11. package/fesm2022/ngwr-bar-chart-testing.mjs +89 -0
  12. package/fesm2022/ngwr-bar-chart-testing.mjs.map +1 -0
  13. package/fesm2022/ngwr-blur-text-testing.mjs +203 -0
  14. package/fesm2022/ngwr-blur-text-testing.mjs.map +1 -0
  15. package/fesm2022/ngwr-blur-text.mjs +15 -1
  16. package/fesm2022/ngwr-blur-text.mjs.map +1 -1
  17. package/fesm2022/ngwr-border-glow-testing.mjs +325 -0
  18. package/fesm2022/ngwr-border-glow-testing.mjs.map +1 -0
  19. package/fesm2022/ngwr-border-glow.mjs +2 -2
  20. package/fesm2022/ngwr-breadcrumbs.mjs +2 -2
  21. package/fesm2022/ngwr-calendar-heatmap-testing.mjs +95 -0
  22. package/fesm2022/ngwr-calendar-heatmap-testing.mjs.map +1 -0
  23. package/fesm2022/ngwr-calendar-testing.mjs +368 -0
  24. package/fesm2022/ngwr-calendar-testing.mjs.map +1 -0
  25. package/fesm2022/ngwr-calendar.mjs +43 -3
  26. package/fesm2022/ngwr-calendar.mjs.map +1 -1
  27. package/fesm2022/ngwr-card.mjs +2 -2
  28. package/fesm2022/ngwr-cascader.mjs +14 -2
  29. package/fesm2022/ngwr-cascader.mjs.map +1 -1
  30. package/fesm2022/ngwr-checkbox.mjs +8 -0
  31. package/fesm2022/ngwr-checkbox.mjs.map +1 -1
  32. package/fesm2022/ngwr-circular-text-testing.mjs +230 -0
  33. package/fesm2022/ngwr-circular-text-testing.mjs.map +1 -0
  34. package/fesm2022/ngwr-circular-text.mjs +2 -2
  35. package/fesm2022/ngwr-circular-text.mjs.map +1 -1
  36. package/fesm2022/ngwr-color-picker-testing.mjs +432 -0
  37. package/fesm2022/ngwr-color-picker-testing.mjs.map +1 -0
  38. package/fesm2022/ngwr-color-picker.mjs +29 -3
  39. package/fesm2022/ngwr-color-picker.mjs.map +1 -1
  40. package/fesm2022/ngwr-context-menu.mjs +25 -5
  41. package/fesm2022/ngwr-context-menu.mjs.map +1 -1
  42. package/fesm2022/ngwr-date-picker-testing.mjs +10 -79
  43. package/fesm2022/ngwr-date-picker-testing.mjs.map +1 -1
  44. package/fesm2022/ngwr-decrypt-text-testing.mjs +261 -0
  45. package/fesm2022/ngwr-decrypt-text-testing.mjs.map +1 -0
  46. package/fesm2022/ngwr-decrypt-text.mjs +6 -1
  47. package/fesm2022/ngwr-decrypt-text.mjs.map +1 -1
  48. package/fesm2022/ngwr-donut-chart-testing.mjs +93 -0
  49. package/fesm2022/ngwr-donut-chart-testing.mjs.map +1 -0
  50. package/fesm2022/ngwr-drag-drop.mjs +2 -2
  51. package/fesm2022/ngwr-drawer.mjs +36 -7
  52. package/fesm2022/ngwr-drawer.mjs.map +1 -1
  53. package/fesm2022/ngwr-dropdown.mjs +32 -2
  54. package/fesm2022/ngwr-dropdown.mjs.map +1 -1
  55. package/fesm2022/ngwr-event-calendar-testing.mjs +446 -0
  56. package/fesm2022/ngwr-event-calendar-testing.mjs.map +1 -0
  57. package/fesm2022/ngwr-event-calendar.mjs +43 -18
  58. package/fesm2022/ngwr-event-calendar.mjs.map +1 -1
  59. package/fesm2022/ngwr-falling-text-testing.mjs +218 -0
  60. package/fesm2022/ngwr-falling-text-testing.mjs.map +1 -0
  61. package/fesm2022/ngwr-falling-text.mjs +2 -2
  62. package/fesm2022/ngwr-falling-text.mjs.map +1 -1
  63. package/fesm2022/ngwr-fuzzy-text-testing.mjs +134 -0
  64. package/fesm2022/ngwr-fuzzy-text-testing.mjs.map +1 -0
  65. package/fesm2022/ngwr-gauge-testing.mjs +86 -0
  66. package/fesm2022/ngwr-gauge-testing.mjs.map +1 -0
  67. package/fesm2022/ngwr-glitch-text-testing.mjs +181 -0
  68. package/fesm2022/ngwr-glitch-text-testing.mjs.map +1 -0
  69. package/fesm2022/ngwr-glitch-text.mjs +2 -2
  70. package/fesm2022/ngwr-gradient-text-testing.mjs +223 -0
  71. package/fesm2022/ngwr-gradient-text-testing.mjs.map +1 -0
  72. package/fesm2022/ngwr-gradient-text.mjs +2 -2
  73. package/fesm2022/ngwr-i18n-en.mjs +14 -0
  74. package/fesm2022/ngwr-i18n-en.mjs.map +1 -1
  75. package/fesm2022/ngwr-i18n-ru.mjs +14 -0
  76. package/fesm2022/ngwr-i18n-ru.mjs.map +1 -1
  77. package/fesm2022/ngwr-image-cropper-testing.mjs +156 -0
  78. package/fesm2022/ngwr-image-cropper-testing.mjs.map +1 -0
  79. package/fesm2022/ngwr-image-cropper.mjs +10 -3
  80. package/fesm2022/ngwr-image-cropper.mjs.map +1 -1
  81. package/fesm2022/ngwr-input-number.mjs +73 -6
  82. package/fesm2022/ngwr-input-number.mjs.map +1 -1
  83. package/fesm2022/ngwr-knob-testing.mjs +220 -0
  84. package/fesm2022/ngwr-knob-testing.mjs.map +1 -0
  85. package/fesm2022/ngwr-lightbox-testing.mjs +223 -0
  86. package/fesm2022/ngwr-lightbox-testing.mjs.map +1 -0
  87. package/fesm2022/ngwr-lightbox.mjs +9 -2
  88. package/fesm2022/ngwr-lightbox.mjs.map +1 -1
  89. package/fesm2022/ngwr-line-chart-testing.mjs +119 -0
  90. package/fesm2022/ngwr-line-chart-testing.mjs.map +1 -0
  91. package/fesm2022/ngwr-list.mjs +2 -2
  92. package/fesm2022/ngwr-loading-bar.mjs +2 -2
  93. package/fesm2022/ngwr-marquee-testing.mjs +336 -0
  94. package/fesm2022/ngwr-marquee-testing.mjs.map +1 -0
  95. package/fesm2022/ngwr-marquee.mjs +2 -2
  96. package/fesm2022/ngwr-meter-group-testing.mjs +80 -0
  97. package/fesm2022/ngwr-meter-group-testing.mjs.map +1 -0
  98. package/fesm2022/ngwr-overlay.mjs +28 -1
  99. package/fesm2022/ngwr-overlay.mjs.map +1 -1
  100. package/fesm2022/ngwr-popover.mjs +14 -0
  101. package/fesm2022/ngwr-popover.mjs.map +1 -1
  102. package/fesm2022/ngwr-rotating-text-testing.mjs +220 -0
  103. package/fesm2022/ngwr-rotating-text-testing.mjs.map +1 -0
  104. package/fesm2022/ngwr-rotating-text.mjs +2 -2
  105. package/fesm2022/ngwr-rotating-text.mjs.map +1 -1
  106. package/fesm2022/ngwr-select.mjs +70 -11
  107. package/fesm2022/ngwr-select.mjs.map +1 -1
  108. package/fesm2022/ngwr-shiny-text-testing.mjs +237 -0
  109. package/fesm2022/ngwr-shiny-text-testing.mjs.map +1 -0
  110. package/fesm2022/ngwr-shiny-text.mjs +2 -2
  111. package/fesm2022/ngwr-slider.mjs +44 -6
  112. package/fesm2022/ngwr-slider.mjs.map +1 -1
  113. package/fesm2022/ngwr-sparkline-testing.mjs +76 -0
  114. package/fesm2022/ngwr-sparkline-testing.mjs.map +1 -0
  115. package/fesm2022/ngwr-speed-dial-testing.mjs +279 -0
  116. package/fesm2022/ngwr-speed-dial-testing.mjs.map +1 -0
  117. package/fesm2022/ngwr-splash-cursor-testing.mjs +89 -0
  118. package/fesm2022/ngwr-splash-cursor-testing.mjs.map +1 -0
  119. package/fesm2022/ngwr-split-text-testing.mjs +224 -0
  120. package/fesm2022/ngwr-split-text-testing.mjs.map +1 -0
  121. package/fesm2022/ngwr-split-text.mjs +15 -1
  122. package/fesm2022/ngwr-split-text.mjs.map +1 -1
  123. package/fesm2022/ngwr-splitter-testing.mjs +245 -0
  124. package/fesm2022/ngwr-splitter-testing.mjs.map +1 -0
  125. package/fesm2022/ngwr-spotlight-card-testing.mjs +368 -0
  126. package/fesm2022/ngwr-spotlight-card-testing.mjs.map +1 -0
  127. package/fesm2022/ngwr-spotlight-card.mjs +2 -2
  128. package/fesm2022/ngwr-star-border-testing.mjs +172 -0
  129. package/fesm2022/ngwr-star-border-testing.mjs.map +1 -0
  130. package/fesm2022/ngwr-star-border.mjs +2 -2
  131. package/fesm2022/ngwr-table.mjs +13 -3
  132. package/fesm2022/ngwr-table.mjs.map +1 -1
  133. package/fesm2022/ngwr-tabs.mjs +15 -1
  134. package/fesm2022/ngwr-tabs.mjs.map +1 -1
  135. package/fesm2022/ngwr-tilt-card-testing.mjs +190 -0
  136. package/fesm2022/ngwr-tilt-card-testing.mjs.map +1 -0
  137. package/fesm2022/ngwr-tilt-card.mjs +21 -3
  138. package/fesm2022/ngwr-tilt-card.mjs.map +1 -1
  139. package/fesm2022/ngwr-toast.mjs +36 -7
  140. package/fesm2022/ngwr-toast.mjs.map +1 -1
  141. package/fesm2022/ngwr-tour-testing.mjs +173 -0
  142. package/fesm2022/ngwr-tour-testing.mjs.map +1 -0
  143. package/fesm2022/ngwr-tour.mjs +2 -2
  144. package/fesm2022/ngwr-tour.mjs.map +1 -1
  145. package/fesm2022/ngwr-tree.mjs +10 -3
  146. package/fesm2022/ngwr-tree.mjs.map +1 -1
  147. package/fesm2022/ngwr-typewriter-testing.mjs +228 -0
  148. package/fesm2022/ngwr-typewriter-testing.mjs.map +1 -0
  149. package/fesm2022/ngwr-typewriter.mjs +2 -2
  150. package/fesm2022/ngwr-version.mjs +1 -1
  151. package/fesm2022/ngwr-version.mjs.map +1 -1
  152. package/fesm2022/ngwr-virtual-scroll.mjs +2 -2
  153. package/fesm2022/ngwr-waves-testing.mjs +128 -0
  154. package/fesm2022/ngwr-waves-testing.mjs.map +1 -0
  155. package/fesm2022/ngwr-waves.mjs +2 -2
  156. package/fesm2022/ngwr-window-testing.mjs +361 -0
  157. package/fesm2022/ngwr-window-testing.mjs.map +1 -0
  158. package/fesm2022/ngwr-window.mjs +84 -10
  159. package/fesm2022/ngwr-window.mjs.map +1 -1
  160. package/file-upload/styles/_index.scss +7 -2
  161. package/i18n/en.json +16 -2
  162. package/i18n/ru.json +16 -2
  163. package/llms-full.txt +147 -0
  164. package/llms.txt +27 -8
  165. package/mcp/providers.js +29 -0
  166. package/mcp/tools.js +1 -29
  167. package/package.json +145 -1
  168. package/result/styles/_index.scss +3 -1
  169. package/segmented/styles/_index.scss +5 -1
  170. package/select/styles/_index.scss +3 -1
  171. package/skills/ngwr/SKILL.md +115 -0
  172. package/skills/ngwr/references/catalog.md +212 -0
  173. package/skills/ngwr/references/setup.md +73 -0
  174. package/stepper/styles/_index.scss +18 -2
  175. package/table/styles/_index.scss +3 -1
  176. package/theme/styles/_colors.scss +15 -2
  177. package/theme/styles/_dark.scss +24 -12
  178. package/tree/styles/_index.scss +4 -2
  179. package/types/ngwr-action-sheet-testing.d.ts +260 -0
  180. package/types/ngwr-bar-chart-testing.d.ts +77 -0
  181. package/types/ngwr-blur-text-testing.d.ts +180 -0
  182. package/types/ngwr-blur-text.d.ts +12 -0
  183. package/types/ngwr-border-glow-testing.d.ts +219 -0
  184. package/types/ngwr-calendar-heatmap-testing.d.ts +79 -0
  185. package/types/ngwr-calendar-testing.d.ts +277 -0
  186. package/types/ngwr-calendar.d.ts +10 -0
  187. package/types/ngwr-checkbox.d.ts +13 -0
  188. package/types/ngwr-circular-text-testing.d.ts +196 -0
  189. package/types/ngwr-color-picker-testing.d.ts +291 -0
  190. package/types/ngwr-color-picker.d.ts +13 -1
  191. package/types/ngwr-context-menu.d.ts +15 -1
  192. package/types/ngwr-date-picker-testing.d.ts +6 -66
  193. package/types/ngwr-decrypt-text-testing.d.ts +222 -0
  194. package/types/ngwr-donut-chart-testing.d.ts +79 -0
  195. package/types/ngwr-drawer.d.ts +12 -0
  196. package/types/ngwr-dropdown.d.ts +9 -0
  197. package/types/ngwr-event-calendar-testing.d.ts +301 -0
  198. package/types/ngwr-event-calendar.d.ts +9 -6
  199. package/types/ngwr-falling-text-testing.d.ts +186 -0
  200. package/types/ngwr-fuzzy-text-testing.d.ts +130 -0
  201. package/types/ngwr-gauge-testing.d.ts +71 -0
  202. package/types/ngwr-glitch-text-testing.d.ts +172 -0
  203. package/types/ngwr-gradient-text-testing.d.ts +173 -0
  204. package/types/ngwr-image-cropper-testing.d.ts +134 -0
  205. package/types/ngwr-image-cropper.d.ts +1 -0
  206. package/types/ngwr-input-number.d.ts +33 -1
  207. package/types/ngwr-knob-testing.d.ts +150 -0
  208. package/types/ngwr-lightbox-testing.d.ts +154 -0
  209. package/types/ngwr-lightbox.d.ts +6 -0
  210. package/types/ngwr-line-chart-testing.d.ts +93 -0
  211. package/types/ngwr-marquee-testing.d.ts +280 -0
  212. package/types/ngwr-meter-group-testing.d.ts +67 -0
  213. package/types/ngwr-overlay.d.ts +19 -1
  214. package/types/ngwr-rotating-text-testing.d.ts +191 -0
  215. package/types/ngwr-select.d.ts +32 -0
  216. package/types/ngwr-shiny-text-testing.d.ts +165 -0
  217. package/types/ngwr-slider.d.ts +15 -0
  218. package/types/ngwr-sparkline-testing.d.ts +69 -0
  219. package/types/ngwr-speed-dial-testing.d.ts +195 -0
  220. package/types/ngwr-splash-cursor-testing.d.ts +95 -0
  221. package/types/ngwr-split-text-testing.d.ts +209 -0
  222. package/types/ngwr-split-text.d.ts +12 -0
  223. package/types/ngwr-splitter-testing.d.ts +174 -0
  224. package/types/ngwr-spotlight-card-testing.d.ts +303 -0
  225. package/types/ngwr-star-border-testing.d.ts +135 -0
  226. package/types/ngwr-table.d.ts +10 -1
  227. package/types/ngwr-tilt-card-testing.d.ts +158 -0
  228. package/types/ngwr-tilt-card.d.ts +2 -0
  229. package/types/ngwr-toast.d.ts +14 -5
  230. package/types/ngwr-tour-testing.d.ts +143 -0
  231. package/types/ngwr-tree.d.ts +7 -0
  232. package/types/ngwr-typewriter-testing.d.ts +201 -0
  233. package/types/ngwr-version.d.ts +1 -1
  234. package/types/ngwr-waves-testing.d.ts +121 -0
  235. package/types/ngwr-window-testing.d.ts +263 -0
  236. package/types/ngwr-window.d.ts +27 -10
  237. package/window/styles/_index.scss +44 -16
@@ -1 +1 @@
1
- {"version":3,"file":"ngwr-slider.mjs","sources":["../../../projects/lib/slider/slider.ts","../../../projects/lib/slider/slider.html","../../../projects/lib/slider/ngwr-slider.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Directionality } from '@angular/cdk/bidi';\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport type { ElementRef } from '@angular/core';\nimport {\n Component,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n model,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport { clamp, round } from 'ngwr/utils';\n\nimport type { WrSliderValue } from './interfaces';\n\n/** Trim float drift from step calculations. */\n\n/**\n * Numeric slider with optional dual-thumb range mode.\n *\n * A signal-forms native control: it implements `FormValueControl<WrSliderValue>`,\n * so `[formField]` binds straight to its `value` model — no\n * `ControlValueAccessor` in between. `[(value)]` works standalone. The value\n * shape depends on `range`: `number` (default) or `[number, number]` when\n * `range=\"true\"`.\n *\n * Keyboard: ← / → adjust by `step`; ↑ / ↓ same; Shift+arrow by `step × 10`;\n * Home / End jump to min / max; PageUp / PageDown by `step × 10`.\n *\n * Reading direction: the arrows follow the VISUAL track, so under `dir=\"rtl\"`\n * → moves toward the lower end and ← toward the higher one, and a drag reads\n * its ratio from the right edge. ↑ / ↓, PageUp / PageDown and Home / End are\n * unchanged — they name a value, not a side of the screen.\n *\n * @example\n * ```html\n * <!-- signal forms -->\n * <wr-slider [formField]=\"form.volume\" min=\"0\" max=\"100\" step=\"5\" />\n *\n * <!-- standalone two-way binding -->\n * <wr-slider [(value)]=\"range\" range min=\"0\" max=\"1000\" step=\"10\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/slider\n */\n@Component({\n selector: 'wr-slider',\n templateUrl: './slider.html',\n encapsulation: ViewEncapsulation.None,\n host: { '[class]': 'classes()' },\n})\nexport class WrSlider implements FormValueControl<WrSliderValue> {\n // Typed `WrSliderValue | undefined` (not `number`) to satisfy the reserved\n // `FormUiControl` min/max slots, which are keyed to the control's value type.\n // The transform still coerces to a plain number, so the bounds stay numeric\n // at runtime; `minValue`/`maxValue` narrow them back for arithmetic.\n /** Lower bound. @default 0 */\n readonly min = input<WrSliderValue | undefined>(0, { transform: (v: unknown): number => coerceNumberProperty(v, 0) });\n /** Upper bound. @default 100 */\n readonly max = input<WrSliderValue | undefined>(100, {\n transform: (v: unknown): number => coerceNumberProperty(v, 100),\n });\n\n /** Resolved numeric lower bound for internal arithmetic. */\n private readonly minValue = computed(() => {\n const m = this.min();\n return typeof m === 'number' ? m : 0;\n });\n /** Resolved numeric upper bound for internal arithmetic. */\n private readonly maxValue = computed(() => {\n const m = this.max();\n return typeof m === 'number' ? m : 100;\n });\n\n /** Step size for keyboard and drag. @default 1 */\n readonly step = input(1, { transform: (v: unknown): number => Math.max(0.0001, coerceNumberProperty(v, 1)) });\n /** Render two thumbs and emit `[low, high]`. @default false */\n readonly range = input(false, { transform: coerceBooleanProperty });\n /**\n * Disable interaction. Bound automatically from the field's disabled state\n * when used with `[formField]`.\n *\n * @default false\n */\n readonly disabled = input(false, { transform: coerceBooleanProperty });\n /** Render the current value below the track. @default true */\n readonly showLabel = input(true, { transform: coerceBooleanProperty });\n\n /**\n * Current value. Bound by `[formField]`, or two-way via `[(value)]`. Shape\n * follows `range`: a plain `number`, or `[low, high]` in range mode.\n */\n readonly value = model<WrSliderValue>(0);\n\n /** Emitted on blur so a bound field can mark itself touched. */\n readonly touch = output<void>();\n\n protected readonly low = signal(0);\n protected readonly high = signal(100);\n\n protected readonly track = viewChild.required<ElementRef<HTMLElement>>('track');\n\n /**\n * Ambient reading direction. Optional so a bare `TestBed` — or any consumer\n * that never set a direction — needs no provider; `Directionality` is\n * root-provided, so `null` only ever means \"nobody asked\", which is LTR.\n *\n * Nothing caches a direction-derived value: the thumbs are painted with\n * `inset-inline-start`, so the track mirrors from CSS alone, and `isRtl()` is\n * read inside the event handlers. A runtime flip therefore needs no\n * subscription to `Directionality.change`.\n */\n private readonly dir = inject(Directionality, { optional: true });\n\n protected readonly classes = computed(() => {\n const parts = ['wr-slider'];\n if (this.range()) parts.push('wr-slider--range');\n if (this.disabled()) parts.push('wr-slider--disabled');\n return parts.join(' ');\n });\n\n protected readonly lowPercent = computed(() => this.percentOf(this.low()));\n protected readonly highPercent = computed(() => this.percentOf(this.high()));\n\n /** Offset of the fill from the track's START edge — the left one in LTR, the right one in RTL. */\n protected readonly fillStart = computed(() => (this.range() ? this.lowPercent() : 0));\n protected readonly fillWidth = computed(() =>\n this.range() ? this.highPercent() - this.lowPercent() : this.lowPercent()\n );\n\n protected readonly label = computed(() => (this.range() ? `${this.low()} – ${this.high()}` : `${this.low()}`));\n\n constructor() {\n // Keep the internal thumbs in sync with external writes to `value`\n // (the old `writeValue`): split the tuple / clamp into the low & high cells.\n effect(() => {\n const v = this.value();\n if (Array.isArray(v)) {\n const tuple = v as readonly [number, number];\n this.low.set(this.clampToBounds(tuple[0]));\n this.high.set(this.clampToBounds(tuple[1]));\n } else if (typeof v === 'number') {\n this.low.set(this.clampToBounds(v));\n }\n });\n }\n\n // Interaction\n\n protected onPointerDown(event: PointerEvent, thumb: 'low' | 'high'): void {\n if (this.disabled()) return;\n event.preventDefault();\n const target = event.currentTarget as HTMLElement;\n target.setPointerCapture(event.pointerId);\n\n const move = (e: PointerEvent): void => this.updateFromEvent(e, thumb);\n const up = (e: PointerEvent): void => {\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n this.touch.emit();\n };\n\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n this.updateFromEvent(event, thumb);\n }\n\n protected onTrackPointerDown(event: PointerEvent): void {\n if (this.disabled()) return;\n if ((event.target as HTMLElement).closest('.wr-slider__thumb')) return;\n const thumb = this.nearestThumb(event);\n this.updateFromEvent(event, thumb);\n this.focusThumb(thumb);\n }\n\n protected onKey(event: KeyboardEvent, thumb: 'low' | 'high'): void {\n if (this.disabled()) return;\n const big = event.shiftKey || event.key === 'PageUp' || event.key === 'PageDown' ? 10 : 1;\n const delta = this.step() * big;\n const current = thumb === 'low' ? this.low() : this.high();\n // The horizontal arrows name a SIDE of the track, so they follow the\n // reading direction; every other key names a value and does not.\n const inline = this.isRtl() ? -delta : delta;\n let next: number | null = null;\n\n switch (event.key) {\n case 'ArrowRight':\n next = current + inline;\n break;\n case 'ArrowLeft':\n next = current - inline;\n break;\n case 'ArrowUp':\n case 'PageUp':\n next = current + delta;\n break;\n case 'ArrowDown':\n case 'PageDown':\n next = current - delta;\n break;\n case 'Home':\n next = this.minValue();\n break;\n case 'End':\n next = this.maxValue();\n break;\n }\n\n if (next === null) return;\n event.preventDefault();\n this.setThumb(thumb, next);\n this.emitChange();\n this.touch.emit();\n }\n\n // Internals\n\n private isRtl(): boolean {\n return this.dir?.value === 'rtl';\n }\n\n /**\n * The value under a pointer, measured along the track's INLINE axis.\n *\n * The offset is taken from whichever edge the track starts at — the left one\n * in LTR, the right one in RTL — so the whole component needs the direction\n * in exactly one place. Measuring from `rect.left` regardless would read 0 at\n * the visual maximum of a mirrored track.\n */\n private valueAt(event: PointerEvent): number {\n const rect = this.track().nativeElement.getBoundingClientRect();\n const offset = this.isRtl() ? rect.right - event.clientX : event.clientX - rect.left;\n const ratio = clamp(offset / rect.width, 0, 1);\n return this.minValue() + ratio * (this.maxValue() - this.minValue());\n }\n\n private updateFromEvent(event: PointerEvent, thumb: 'low' | 'high'): void {\n this.setThumb(thumb, this.valueAt(event));\n this.emitChange();\n }\n\n /**\n * Which thumb a track click grabs: the one nearest the pointer ON SCREEN.\n *\n * Value distance is the same thing as visual distance once {@link valueAt}\n * has mirrored the ratio — in RTL the low thumb is the RIGHT one, and a click\n * near the right edge lands close to `low` in value space too.\n */\n private nearestThumb(event: PointerEvent): 'low' | 'high' {\n if (!this.range()) return 'low';\n const raw = this.valueAt(event);\n return Math.abs(raw - this.low()) <= Math.abs(raw - this.high()) ? 'low' : 'high';\n }\n\n private focusThumb(thumb: 'low' | 'high'): void {\n queueMicrotask(() => {\n const root = this.track().nativeElement;\n const el = root.querySelector<HTMLElement>(`.wr-slider__thumb--${thumb}`);\n el?.focus();\n });\n }\n\n private setThumb(thumb: 'low' | 'high', raw: number): void {\n const stepped = this.snap(raw);\n if (thumb === 'low') {\n const next = this.range() ? Math.min(stepped, this.high()) : stepped;\n this.low.set(next);\n } else {\n this.high.set(Math.max(stepped, this.low()));\n }\n }\n\n private snap(value: number): number {\n const stepped = Math.round((value - this.minValue()) / this.step()) * this.step() + this.minValue();\n return this.clampToBounds(round(stepped, 6));\n }\n\n private clampToBounds(v: number): number {\n return clamp(v, this.minValue(), this.maxValue());\n }\n\n private percentOf(v: number): number {\n const span = this.maxValue() - this.minValue();\n if (span <= 0) return 0;\n return ((v - this.minValue()) / span) * 100;\n }\n\n private emitChange(): void {\n const value: WrSliderValue = this.range() ? [this.low(), this.high()] : this.low();\n this.value.set(value);\n }\n}\n","<div #track class=\"wr-slider__track\" (pointerdown)=\"onTrackPointerDown($event)\">\n <!--\n Positioned on the INLINE axis, not with `left`: `inset-inline-start` is the\n same edge in LTR and mirrors under `dir=\"rtl\"`, so the track paints itself\n the right way round without the component reading a direction to draw.\n -->\n <span class=\"wr-slider__fill\" [style.inset-inline-start.%]=\"fillStart()\" [style.width.%]=\"fillWidth()\"></span>\n\n <button\n type=\"button\"\n class=\"wr-slider__thumb wr-slider__thumb--low\"\n role=\"slider\"\n [style.inset-inline-start.%]=\"lowPercent()\"\n [disabled]=\"disabled()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"range() ? high() : max()\"\n [attr.aria-valuenow]=\"low()\"\n [attr.aria-label]=\"range() ? 'Lower value' : 'Value'\"\n (pointerdown)=\"onPointerDown($event, 'low')\"\n (keydown)=\"onKey($event, 'low')\"\n ></button>\n\n @if (range()) {\n <button\n type=\"button\"\n class=\"wr-slider__thumb wr-slider__thumb--high\"\n role=\"slider\"\n [style.inset-inline-start.%]=\"highPercent()\"\n [disabled]=\"disabled()\"\n [attr.aria-valuemin]=\"low()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"high()\"\n aria-label=\"Upper value\"\n (pointerdown)=\"onPointerDown($event, 'high')\"\n (keydown)=\"onKey($event, 'high')\"\n ></button>\n }\n</div>\n\n@if (showLabel()) {\n <div class=\"wr-slider__label\">{{ label() }}</div>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ngwr-slider.mjs","sources":["../../../projects/lib/slider/slider.ts","../../../projects/lib/slider/slider.html","../../../projects/lib/slider/ngwr-slider.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { Directionality } from '@angular/cdk/bidi';\nimport { coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';\nimport type { ElementRef } from '@angular/core';\nimport {\n Component,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n model,\n output,\n signal,\n untracked,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport { clamp, round } from 'ngwr/utils';\n\nimport type { WrSliderValue } from './interfaces';\n\n/** Trim float drift from step calculations. */\n\n/**\n * Numeric slider with optional dual-thumb range mode.\n *\n * A signal-forms native control: it implements `FormValueControl<WrSliderValue>`,\n * so `[formField]` binds straight to its `value` model — no\n * `ControlValueAccessor` in between. `[(value)]` works standalone. The value\n * shape depends on `range`: `number` (default) or `[number, number]` when\n * `range=\"true\"`.\n *\n * Keyboard: ← / → adjust by `step`; ↑ / ↓ same; Shift+arrow by `step × 10`;\n * Home / End jump to min / max; PageUp / PageDown by `step × 10`.\n *\n * Reading direction: the arrows follow the VISUAL track, so under `dir=\"rtl\"`\n * → moves toward the lower end and ← toward the higher one, and a drag reads\n * its ratio from the right edge. ↑ / ↓, PageUp / PageDown and Home / End are\n * unchanged — they name a value, not a side of the screen.\n *\n * @example\n * ```html\n * <!-- signal forms -->\n * <wr-slider [formField]=\"form.volume\" min=\"0\" max=\"100\" step=\"5\" />\n *\n * <!-- standalone two-way binding -->\n * <wr-slider [(value)]=\"range\" range min=\"0\" max=\"1000\" step=\"10\" />\n * ```\n *\n * @see https://ngwr.dev/reference/components/slider\n */\n@Component({\n selector: 'wr-slider',\n templateUrl: './slider.html',\n encapsulation: ViewEncapsulation.None,\n host: { '[class]': 'classes()' },\n})\nexport class WrSlider implements FormValueControl<WrSliderValue> {\n // Typed `WrSliderValue | undefined` (not `number`) to satisfy the reserved\n // `FormUiControl` min/max slots, which are keyed to the control's value type.\n // The transform still coerces to a plain number, so the bounds stay numeric\n // at runtime; `minValue`/`maxValue` narrow them back for arithmetic.\n /** Lower bound. @default 0 */\n readonly min = input<WrSliderValue | undefined>(0, { transform: (v: unknown): number => coerceNumberProperty(v, 0) });\n /** Upper bound. @default 100 */\n readonly max = input<WrSliderValue | undefined>(100, {\n transform: (v: unknown): number => coerceNumberProperty(v, 100),\n });\n\n /** Resolved numeric lower bound for internal arithmetic. */\n private readonly minValue = computed(() => {\n const m = this.min();\n return typeof m === 'number' ? m : 0;\n });\n /** Resolved numeric upper bound for internal arithmetic. */\n private readonly maxValue = computed(() => {\n const m = this.max();\n return typeof m === 'number' ? m : 100;\n });\n\n /** Step size for keyboard and drag. @default 1 */\n readonly step = input(1, { transform: (v: unknown): number => Math.max(0.0001, coerceNumberProperty(v, 1)) });\n /** Render two thumbs and emit `[low, high]`. @default false */\n readonly range = input(false, { transform: coerceBooleanProperty });\n /**\n * Disable interaction. Bound automatically from the field's disabled state\n * when used with `[formField]`.\n *\n * @default false\n */\n readonly disabled = input(false, { transform: coerceBooleanProperty });\n /** Render the current value below the track. @default true */\n readonly showLabel = input(true, { transform: coerceBooleanProperty });\n\n /**\n * Current value. Bound by `[formField]`, or two-way via `[(value)]`. Shape\n * follows `range`: a plain `number`, or `[low, high]` in range mode.\n */\n readonly value = model<WrSliderValue>(0);\n\n /** Emitted on blur so a bound field can mark itself touched. */\n readonly touch = output<void>();\n\n protected readonly low = signal(0);\n /**\n * The high thumb's cell. Its literal default stands in for `maxValue()`,\n * which cannot be read at field-initialisation time — see `highSeeded` and\n * the constructor effect.\n */\n protected readonly high = signal(100);\n\n /**\n * Whether `high` has ever been told a real value by the model.\n *\n * `<wr-slider range [min]=\"200\" [max]=\"300\" />` binds no value, so the model\n * holds its own scalar default and the tuple branch below never runs: `high`\n * kept the literal 100, which is not inside every `[min, max]`. The thumb\n * rendered off the track and the first interaction committed `[low, 100]` —\n * a value the slider itself calls out of range.\n */\n private highSeeded = false;\n\n protected readonly track = viewChild.required<ElementRef<HTMLElement>>('track');\n\n /**\n * Ambient reading direction. Optional so a bare `TestBed` — or any consumer\n * that never set a direction — needs no provider; `Directionality` is\n * root-provided, so `null` only ever means \"nobody asked\", which is LTR.\n *\n * Nothing caches a direction-derived value: the thumbs are painted with\n * `inset-inline-start`, so the track mirrors from CSS alone, and `isRtl()` is\n * read inside the event handlers. A runtime flip therefore needs no\n * subscription to `Directionality.change`.\n */\n private readonly dir = inject(Directionality, { optional: true });\n\n protected readonly classes = computed(() => {\n const parts = ['wr-slider'];\n if (this.range()) parts.push('wr-slider--range');\n if (this.disabled()) parts.push('wr-slider--disabled');\n return parts.join(' ');\n });\n\n protected readonly lowPercent = computed(() => this.percentOf(this.low()));\n protected readonly highPercent = computed(() => this.percentOf(this.high()));\n\n /** Offset of the fill from the track's START edge — the left one in LTR, the right one in RTL. */\n protected readonly fillStart = computed(() => (this.range() ? this.lowPercent() : 0));\n protected readonly fillWidth = computed(() =>\n this.range() ? this.highPercent() - this.lowPercent() : this.lowPercent()\n );\n\n protected readonly label = computed(() => (this.range() ? `${this.low()} – ${this.high()}` : `${this.low()}`));\n\n constructor() {\n // Keep the internal thumbs in sync with external writes to `value`\n // (the old `writeValue`): split the tuple / clamp into the low & high cells.\n // The cells are clamped and the MODEL IS NOT TOUCHED, and the asymmetry is\n // deliberate. A thumb cannot render outside its own track, so the display\n // has no choice — but writing that clamp back into `value` would erase the\n // error it represents: under `[formField]` the bounds reach this component\n // only through the schema's `min()` / `max()` rules (binding `[min]` beside\n // `[formField]` is an NG8022 compile error), so the write-back deleted the\n // out-of-range value those rules exist to report, and marked a pristine\n // form dirty on first paint besides. The first real interaction emits an\n // in-range number through `emitChange` and the two agree from then on.\n effect(() => {\n const v = this.value();\n\n if (Array.isArray(v)) {\n const tuple = v as readonly [number, number];\n const lo = this.clampToBounds(tuple[0]);\n const hi = this.clampToBounds(tuple[1]);\n this.low.set(lo);\n this.high.set(hi);\n this.highSeeded = true;\n return;\n }\n\n if (typeof v !== 'number') return;\n const lo = this.clampToBounds(v);\n this.low.set(lo);\n\n if (!this.range()) return;\n\n // Range mode holding a scalar: the high thumb has never been told\n // anything, and its literal default is not inside every `[min, max]`.\n // Seed it at the top of the range — which is what the 100 meant back when\n // the bounds were 0–100. The MODEL is left alone here too; the shape it\n // holds is the consumer's business until they move a thumb.\n this.high.set(this.highSeeded ? this.clampToBounds(untracked(this.high)) : this.maxValue());\n this.highSeeded = true;\n });\n }\n\n // Interaction\n\n protected onPointerDown(event: PointerEvent, thumb: 'low' | 'high'): void {\n if (this.disabled()) return;\n event.preventDefault();\n const target = event.currentTarget as HTMLElement;\n target.setPointerCapture(event.pointerId);\n\n const move = (e: PointerEvent): void => this.updateFromEvent(e, thumb);\n const up = (e: PointerEvent): void => {\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener('pointermove', move);\n target.removeEventListener('pointerup', up);\n this.touch.emit();\n };\n\n target.addEventListener('pointermove', move);\n target.addEventListener('pointerup', up);\n this.updateFromEvent(event, thumb);\n }\n\n protected onTrackPointerDown(event: PointerEvent): void {\n if (this.disabled()) return;\n if ((event.target as HTMLElement).closest('.wr-slider__thumb')) return;\n const thumb = this.nearestThumb(event);\n this.updateFromEvent(event, thumb);\n this.focusThumb(thumb);\n }\n\n protected onKey(event: KeyboardEvent, thumb: 'low' | 'high'): void {\n if (this.disabled()) return;\n const big = event.shiftKey || event.key === 'PageUp' || event.key === 'PageDown' ? 10 : 1;\n const delta = this.step() * big;\n const current = thumb === 'low' ? this.low() : this.high();\n // The horizontal arrows name a SIDE of the track, so they follow the\n // reading direction; every other key names a value and does not.\n const inline = this.isRtl() ? -delta : delta;\n let next: number | null = null;\n\n switch (event.key) {\n case 'ArrowRight':\n next = current + inline;\n break;\n case 'ArrowLeft':\n next = current - inline;\n break;\n case 'ArrowUp':\n case 'PageUp':\n next = current + delta;\n break;\n case 'ArrowDown':\n case 'PageDown':\n next = current - delta;\n break;\n case 'Home':\n next = this.minValue();\n break;\n case 'End':\n next = this.maxValue();\n break;\n }\n\n if (next === null) return;\n event.preventDefault();\n this.setThumb(thumb, next);\n this.emitChange();\n this.touch.emit();\n }\n\n // Internals\n\n private isRtl(): boolean {\n return this.dir?.value === 'rtl';\n }\n\n /**\n * The value under a pointer, measured along the track's INLINE axis.\n *\n * The offset is taken from whichever edge the track starts at — the left one\n * in LTR, the right one in RTL — so the whole component needs the direction\n * in exactly one place. Measuring from `rect.left` regardless would read 0 at\n * the visual maximum of a mirrored track.\n */\n private valueAt(event: PointerEvent): number {\n const rect = this.track().nativeElement.getBoundingClientRect();\n const offset = this.isRtl() ? rect.right - event.clientX : event.clientX - rect.left;\n const ratio = clamp(offset / rect.width, 0, 1);\n return this.minValue() + ratio * (this.maxValue() - this.minValue());\n }\n\n private updateFromEvent(event: PointerEvent, thumb: 'low' | 'high'): void {\n this.setThumb(thumb, this.valueAt(event));\n this.emitChange();\n }\n\n /**\n * Which thumb a track click grabs: the one nearest the pointer ON SCREEN.\n *\n * Value distance is the same thing as visual distance once {@link valueAt}\n * has mirrored the ratio — in RTL the low thumb is the RIGHT one, and a click\n * near the right edge lands close to `low` in value space too.\n */\n private nearestThumb(event: PointerEvent): 'low' | 'high' {\n if (!this.range()) return 'low';\n const raw = this.valueAt(event);\n return Math.abs(raw - this.low()) <= Math.abs(raw - this.high()) ? 'low' : 'high';\n }\n\n private focusThumb(thumb: 'low' | 'high'): void {\n queueMicrotask(() => {\n const root = this.track().nativeElement;\n const el = root.querySelector<HTMLElement>(`.wr-slider__thumb--${thumb}`);\n el?.focus();\n });\n }\n\n private setThumb(thumb: 'low' | 'high', raw: number): void {\n const stepped = this.snap(raw);\n if (thumb === 'low') {\n const next = this.range() ? Math.min(stepped, this.high()) : stepped;\n this.low.set(next);\n } else {\n this.high.set(Math.max(stepped, this.low()));\n }\n }\n\n private snap(value: number): number {\n const stepped = Math.round((value - this.minValue()) / this.step()) * this.step() + this.minValue();\n return this.clampToBounds(round(stepped, 6));\n }\n\n private clampToBounds(v: number): number {\n return clamp(v, this.minValue(), this.maxValue());\n }\n\n private percentOf(v: number): number {\n const span = this.maxValue() - this.minValue();\n if (span <= 0) return 0;\n return ((v - this.minValue()) / span) * 100;\n }\n\n private emitChange(): void {\n const value: WrSliderValue = this.range() ? [this.low(), this.high()] : this.low();\n this.value.set(value);\n }\n}\n","<div #track class=\"wr-slider__track\" (pointerdown)=\"onTrackPointerDown($event)\">\n <!--\n Positioned on the INLINE axis, not with `left`: `inset-inline-start` is the\n same edge in LTR and mirrors under `dir=\"rtl\"`, so the track paints itself\n the right way round without the component reading a direction to draw.\n -->\n <span class=\"wr-slider__fill\" [style.inset-inline-start.%]=\"fillStart()\" [style.width.%]=\"fillWidth()\"></span>\n\n <button\n type=\"button\"\n class=\"wr-slider__thumb wr-slider__thumb--low\"\n role=\"slider\"\n [style.inset-inline-start.%]=\"lowPercent()\"\n [disabled]=\"disabled()\"\n [attr.aria-valuemin]=\"min()\"\n [attr.aria-valuemax]=\"range() ? high() : max()\"\n [attr.aria-valuenow]=\"low()\"\n [attr.aria-label]=\"range() ? 'Lower value' : 'Value'\"\n (pointerdown)=\"onPointerDown($event, 'low')\"\n (keydown)=\"onKey($event, 'low')\"\n ></button>\n\n @if (range()) {\n <button\n type=\"button\"\n class=\"wr-slider__thumb wr-slider__thumb--high\"\n role=\"slider\"\n [style.inset-inline-start.%]=\"highPercent()\"\n [disabled]=\"disabled()\"\n [attr.aria-valuemin]=\"low()\"\n [attr.aria-valuemax]=\"max()\"\n [attr.aria-valuenow]=\"high()\"\n aria-label=\"Upper value\"\n (pointerdown)=\"onPointerDown($event, 'high')\"\n (keydown)=\"onKey($event, 'high')\"\n ></button>\n }\n</div>\n\n@if (showLabel()) {\n <div class=\"wr-slider__label\">{{ label() }}</div>\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,76 @@
1
+ import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
2
+
3
+ /**
4
+ * @license
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
8
+ */
9
+ /**
10
+ * Test harness for `<wr-sparkline>`.
11
+ *
12
+ * **The interesting assertion here is the ARIA one.** A sparkline is a trend line
13
+ * with no axes and no labels, so it is either given an `ariaLabel` — in which case it
14
+ * announces itself as an image — or it is decoration, and must be `aria-hidden` so a
15
+ * screen reader does not stop on a nameless graphic. The component switches between
16
+ * those two states on one input, and {@link isDecorative} is how a spec pins which
17
+ * one it is in.
18
+ *
19
+ * The line itself is an SVG path, and a path's `d` is not something a spec should
20
+ * assert: it is a rendering detail that changes with the viewBox, the stroke width
21
+ * and the smoothing. What IS worth asserting is whether the parts are there —
22
+ * {@link hasLine}, {@link hasArea}, {@link hasTip} — since each is an input the
23
+ * consumer set, and an empty dataset draws none of them.
24
+ *
25
+ * @see https://ngwr.dev/guides/testing
26
+ */
27
+ class WrSparklineHarness extends ComponentHarness {
28
+ static hostSelector = 'wr-sparkline';
29
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
30
+ static with(options = {}) {
31
+ return new HarnessPredicate(WrSparklineHarness, options).addOption('ariaLabel', options.ariaLabel, (harness, label) => HarnessPredicate.stringMatches(harness.getAccessibleName(), label));
32
+ }
33
+ svg = this.locatorFor('svg');
34
+ /** The name the chart announces, or `null` when it is decoration. */
35
+ async getAccessibleName() {
36
+ return (await this.svg()).getAttribute('aria-label');
37
+ }
38
+ /** The role the chart claims — `img` when it is named, `null` when it is not. */
39
+ async getRole() {
40
+ return (await this.svg()).getAttribute('role');
41
+ }
42
+ /**
43
+ * Whether the chart is hidden from assistive tech.
44
+ *
45
+ * The other half of {@link getAccessibleName}, and the pairing worth asserting
46
+ * together: a named sparkline must NOT be hidden, and an unnamed one must be, or a
47
+ * screen reader announces a graphic with nothing to say about it.
48
+ */
49
+ async isDecorative() {
50
+ return (await (await this.svg()).getAttribute('aria-hidden')) === 'true';
51
+ }
52
+ /** Whether a trend line was drawn — `false` for an empty or single-point dataset. */
53
+ async hasLine() {
54
+ return (await this.locatorForOptional('.wr-sparkline__line')()) !== null;
55
+ }
56
+ /** Whether the area under the line is filled (`showArea`). */
57
+ async hasArea() {
58
+ return (await this.locatorForOptional('.wr-sparkline__area')()) !== null;
59
+ }
60
+ /** Whether the last point is marked with a dot (`showTip`). */
61
+ async hasTip() {
62
+ return (await this.locatorForOptional('.wr-sparkline__tip')()) !== null;
63
+ }
64
+ /** The stroke colour as the component passed it through — a CSS value, not a resolved one. */
65
+ async getColor() {
66
+ const line = await this.locatorForOptional('.wr-sparkline__line')();
67
+ return line ? line.getAttribute('stroke') : null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Generated bundle index. Do not edit.
73
+ */
74
+
75
+ export { WrSparklineHarness };
76
+ //# sourceMappingURL=ngwr-sparkline-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-sparkline-testing.mjs","sources":["../../../projects/lib/sparkline/testing/wr-sparkline-harness.ts","../../../projects/lib/sparkline/testing/ngwr-sparkline-testing.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrSparklineHarnessFilters } from './interfaces';\n\n/**\n * Test harness for `<wr-sparkline>`.\n *\n * **The interesting assertion here is the ARIA one.** A sparkline is a trend line\n * with no axes and no labels, so it is either given an `ariaLabel` — in which case it\n * announces itself as an image — or it is decoration, and must be `aria-hidden` so a\n * screen reader does not stop on a nameless graphic. The component switches between\n * those two states on one input, and {@link isDecorative} is how a spec pins which\n * one it is in.\n *\n * The line itself is an SVG path, and a path's `d` is not something a spec should\n * assert: it is a rendering detail that changes with the viewBox, the stroke width\n * and the smoothing. What IS worth asserting is whether the parts are there —\n * {@link hasLine}, {@link hasArea}, {@link hasTip} — since each is an input the\n * consumer set, and an empty dataset draws none of them.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSparklineHarness extends ComponentHarness {\n static hostSelector = 'wr-sparkline';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSparklineHarnessFilters = {}): HarnessPredicate<WrSparklineHarness> {\n return new HarnessPredicate(WrSparklineHarness, options).addOption(\n 'ariaLabel',\n options.ariaLabel,\n (harness, label) => HarnessPredicate.stringMatches(harness.getAccessibleName(), label)\n );\n }\n\n private readonly svg = this.locatorFor('svg');\n\n /** The name the chart announces, or `null` when it is decoration. */\n async getAccessibleName(): Promise<string | null> {\n return (await this.svg()).getAttribute('aria-label');\n }\n\n /** The role the chart claims — `img` when it is named, `null` when it is not. */\n async getRole(): Promise<string | null> {\n return (await this.svg()).getAttribute('role');\n }\n\n /**\n * Whether the chart is hidden from assistive tech.\n *\n * The other half of {@link getAccessibleName}, and the pairing worth asserting\n * together: a named sparkline must NOT be hidden, and an unnamed one must be, or a\n * screen reader announces a graphic with nothing to say about it.\n */\n async isDecorative(): Promise<boolean> {\n return (await (await this.svg()).getAttribute('aria-hidden')) === 'true';\n }\n\n /** Whether a trend line was drawn — `false` for an empty or single-point dataset. */\n async hasLine(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-sparkline__line')()) !== null;\n }\n\n /** Whether the area under the line is filled (`showArea`). */\n async hasArea(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-sparkline__area')()) !== null;\n }\n\n /** Whether the last point is marked with a dot (`showTip`). */\n async hasTip(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-sparkline__tip')()) !== null;\n }\n\n /** The stroke colour as the component passed it through — a CSS value, not a resolved one. */\n async getColor(): Promise<string | null> {\n const line = await this.locatorForOptional('.wr-sparkline__line')();\n return line ? line.getAttribute('stroke') : null;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;;;;;;;;AAiBG;AACG,MAAO,kBAAmB,SAAQ,gBAAgB,CAAA;AACtD,IAAA,OAAO,YAAY,GAAG,cAAc;;AAGpC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAqC,EAAE,EAAA;AACjD,QAAA,OAAO,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC,SAAS,CAChE,WAAW,EACX,OAAO,CAAC,SAAS,EACjB,CAAC,OAAO,EAAE,KAAK,KAAK,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,KAAK,CAAC,CACvF;IACH;AAEiB,IAAA,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;;AAG7C,IAAA,MAAM,iBAAiB,GAAA;AACrB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,YAAY,CAAC;IACtD;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,MAAM,CAAC;IAChD;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,aAAa,CAAC,MAAM,MAAM;IAC1E;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,EAAE,MAAM,IAAI;IAC1E;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,EAAE,MAAM,IAAI;IAC1E;;AAGA,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,oBAAoB,CAAC,EAAE,MAAM,IAAI;IACzE;;AAGA,IAAA,MAAM,QAAQ,GAAA;QACZ,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,qBAAqB,CAAC,EAAE;AACnE,QAAA,OAAO,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,IAAI;IAClD;;;ACnFF;;AAEG;;;;"}
@@ -0,0 +1,279 @@
1
+ import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
2
+
3
+ /**
4
+ * @license
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
8
+ */
9
+ /**
10
+ * Test harness for one action inside a {@link WrSpeedDialHarness}.
11
+ *
12
+ * The label is read from `aria-label` rather than from the button's text, and that
13
+ * is the whole point of the control: an action renders an ICON, or — with no icon —
14
+ * the first glyph of its label. Its visible text is therefore `"S"` for "Share", or
15
+ * nothing at all. {@link getInitial} answers what is drawn; {@link getLabel} answers
16
+ * what it is.
17
+ *
18
+ * @see https://ngwr.dev/guides/testing
19
+ */
20
+ class WrSpeedDialActionHarness extends ComponentHarness {
21
+ /** The action button. `role="menuitem"` sits on this element too. */
22
+ static hostSelector = '.wr-speed-dial__action';
23
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
24
+ static with(options = {}) {
25
+ return new HarnessPredicate(WrSpeedDialActionHarness, options)
26
+ .addOption('label', options.label, (harness, label) => HarnessPredicate.stringMatches(harness.getLabel(), label))
27
+ .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);
28
+ }
29
+ /** The action's accessible name — what a screen reader reads, and what `title` shows. */
30
+ async getLabel() {
31
+ return (await this.host()).getAttribute('aria-label');
32
+ }
33
+ /** The role the action announces — `menuitem`. */
34
+ async getRole() {
35
+ return (await this.host()).getAttribute('role');
36
+ }
37
+ /** Whether the action draws an icon rather than a letter. */
38
+ async hasIcon() {
39
+ return (await this.locatorForOptional('wr-icon')()) !== null;
40
+ }
41
+ /** The icon's registered name, from `wr-icon`'s reflected `data-icon`, or `null`. */
42
+ async getIconName() {
43
+ const icon = await this.locatorForOptional('wr-icon')();
44
+ return icon ? icon.getAttribute('data-icon') : null;
45
+ }
46
+ /**
47
+ * The single glyph drawn in place of a missing icon, or `null` when there is one.
48
+ *
49
+ * A GLYPH, not a character: the component splits the label by code point, so an
50
+ * emoji label draws the whole emoji rather than the broken half of a surrogate pair.
51
+ */
52
+ async getInitial() {
53
+ if (await this.hasIcon())
54
+ return null;
55
+ return (await this.host()).text();
56
+ }
57
+ /**
58
+ * Whether the action refuses interaction.
59
+ *
60
+ * Every action follows the DIAL's `disabled` — there is no per-action flag — so
61
+ * this is really "is the dial disabled", asked at the action. Worth having anyway:
62
+ * it is what the DOM and the CDK consult before delivering a click.
63
+ */
64
+ async isDisabled() {
65
+ return (await this.host()).getProperty('disabled');
66
+ }
67
+ /** Pick this action — emitting `pick` and closing the dial. */
68
+ async click() {
69
+ if (await this.isDisabled()) {
70
+ throw new Error(`WrSpeedDialActionHarness.click(): "${await this.getLabel()}" is disabled. The DOM swallows the click ` +
71
+ 'and the component checks `disabled` again before emitting, so nothing would happen.');
72
+ }
73
+ await (await this.host()).click();
74
+ }
75
+ /** Move keyboard focus to the action. */
76
+ async focus() {
77
+ return (await this.host()).focus();
78
+ }
79
+ /** Whether the action currently holds focus. */
80
+ async isFocused() {
81
+ return (await this.host()).isFocused();
82
+ }
83
+ }
84
+
85
+ /**
86
+ * @license
87
+ *
88
+ * Use of this source code is governed by an MIT-style license that can be
89
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
90
+ */
91
+ const DIRECTIONS = ['up', 'down', 'left', 'right'];
92
+ /**
93
+ * Test harness for `<wr-speed-dial>` — the floating trigger and the actions that fan
94
+ * out of it.
95
+ *
96
+ * **The actions never leave the DOM.** They are hidden with `visibility`, which is
97
+ * what actually takes them out of the tab order and the accessibility tree —
98
+ * `opacity: 0` alone would leave them focusable — and CSS is exactly what a unit
99
+ * test does not have. So a closed dial's buttons are queryable, clickable and
100
+ * readable from a spec while being unreachable in a browser. {@link getActions} and
101
+ * {@link pick} therefore REFUSE while the dial is closed rather than answering with
102
+ * elements nobody can see; open it first, which is what a user has to do too.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * const dial = await loader.getHarness(WrSpeedDialHarness);
107
+ *
108
+ * await dial.open();
109
+ * expect(await dial.getActionLabels()).toEqual(['Share', 'Copy link']);
110
+ *
111
+ * await dial.pick({ label: 'Share' });
112
+ * expect(await dial.isOpen()).toBe(false);
113
+ * ```
114
+ *
115
+ * @see https://ngwr.dev/guides/testing
116
+ */
117
+ class WrSpeedDialHarness extends ComponentHarness {
118
+ static hostSelector = 'wr-speed-dial';
119
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
120
+ static with(options = {}) {
121
+ return new HarnessPredicate(WrSpeedDialHarness, options)
122
+ .addOption('triggerLabel', options.triggerLabel, (harness, label) => HarnessPredicate.stringMatches(harness.getTriggerLabel(), label))
123
+ .addOption('direction', options.direction, async (harness, direction) => (await harness.getDirection()) === direction)
124
+ .addOption('open', options.open, async (harness, open) => (await harness.isOpen()) === open)
125
+ .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);
126
+ }
127
+ trigger = this.locatorFor('.wr-speed-dial__trigger');
128
+ menu = this.locatorFor('.wr-speed-dial__actions');
129
+ /** Whether the dial is fanned out, from the trigger's `aria-expanded`. */
130
+ async isOpen() {
131
+ return (await (await this.trigger()).getAttribute('aria-expanded')) === 'true';
132
+ }
133
+ /** The direction the actions travel, from the host modifier. */
134
+ async getDirection() {
135
+ const host = await this.host();
136
+ for (const direction of DIRECTIONS) {
137
+ if (await host.hasClass(`wr-speed-dial--${direction}`))
138
+ return direction;
139
+ }
140
+ throw new Error('WrSpeedDialHarness.getDirection(): this host carries no `wr-speed-dial--<direction>` class, which ' +
141
+ 'every dial sets — the element matched is probably not an ngwr speed dial.');
142
+ }
143
+ /**
144
+ * The trigger's accessible name.
145
+ *
146
+ * The trigger is icon-only, so this is its ONLY name — there is no text to fall
147
+ * back on, and a dial that loses it is announced as an unnamed button.
148
+ */
149
+ async getTriggerLabel() {
150
+ return (await this.trigger()).getAttribute('aria-label');
151
+ }
152
+ /** The trigger icon's registered name, from `wr-icon`'s reflected `data-icon`. */
153
+ async getTriggerIcon() {
154
+ const icon = await this.locatorForOptional('.wr-speed-dial__trigger wr-icon')();
155
+ return icon ? icon.getAttribute('data-icon') : null;
156
+ }
157
+ /** Whether the dial refuses interaction, from the trigger's own `disabled` property. */
158
+ async isDisabled() {
159
+ return (await this.trigger()).getProperty('disabled');
160
+ }
161
+ /** Whether the host is padded for the device's safe area (`safeArea`). */
162
+ async hasSafeArea() {
163
+ return (await this.host()).hasClass('wr-speed-dial--safe-area');
164
+ }
165
+ /** The role the action list announces — `menu`. */
166
+ async getMenuRole() {
167
+ return (await this.menu()).getAttribute('role');
168
+ }
169
+ /**
170
+ * Whether the trigger's `aria-controls` names THIS dial's menu, and nothing else on
171
+ * the page.
172
+ *
173
+ * Both halves matter. The id is resolved inside the host first, so a trigger
174
+ * pointing at a SIBLING dial's menu — which resolves perfectly well from the
175
+ * document — fails here. Then it is counted across the document, because two menus
176
+ * answering to one id hand every reference to whichever comes first.
177
+ */
178
+ async isMenuBound() {
179
+ const id = await (await this.trigger()).getAttribute('aria-controls');
180
+ if (!id)
181
+ return false;
182
+ const own = await this.locatorForOptional(`.wr-speed-dial__actions#${id}`)();
183
+ if (!own)
184
+ return false;
185
+ const everywhere = await this.documentRootLocatorFactory().locatorForAll(`#${id}`)();
186
+ return everywhere.length === 1;
187
+ }
188
+ /**
189
+ * The actions, in DOM order.
190
+ *
191
+ * Throws while the dial is closed. The buttons are still in the DOM then — they
192
+ * have to be, for the fan-out to animate — and jsdom applies no `visibility`, so
193
+ * they would come back looking perfectly reachable when a real user cannot tab to
194
+ * one. A spec that read them closed would pass on a dial that never opens.
195
+ */
196
+ async getActions(filters = {}) {
197
+ await this.requireOpen('getActions');
198
+ return this.locatorForAll(WrSpeedDialActionHarness.with(filters))();
199
+ }
200
+ /** The accessible names of the actions, in DOM order. Throws while closed. */
201
+ async getActionLabels() {
202
+ const actions = await this.getActions();
203
+ return Promise.all(actions.map(action => action.getLabel()));
204
+ }
205
+ /** How many actions the dial holds — readable while closed, since it is not a reach. */
206
+ async getActionCount() {
207
+ return (await this.locatorForAll('.wr-speed-dial__action')()).length;
208
+ }
209
+ /** Open the dial. An already-open one is left alone; a disabled one throws. */
210
+ async open() {
211
+ if (await this.isOpen())
212
+ return;
213
+ await this.toggle();
214
+ if (!(await this.isOpen())) {
215
+ throw new Error('WrSpeedDialHarness.open(): the dial did not open. A disabled trigger refuses the click twice over — ' +
216
+ 'the DOM swallows it, and the component checks `disabled` again.');
217
+ }
218
+ }
219
+ /** Close the dial. An already-closed one is left alone. */
220
+ async close() {
221
+ if (!(await this.isOpen()))
222
+ return;
223
+ await this.toggle();
224
+ }
225
+ /** Click the trigger once, whichever way that takes it. */
226
+ async toggle() {
227
+ return (await this.trigger()).click();
228
+ }
229
+ /**
230
+ * Pick the first action matching the filters — emitting `pick` and closing the dial.
231
+ *
232
+ * Opens first if it has to, the way `WrDropdownHarness.clickItem` does: a spec that
233
+ * says "pick Share" means the whole gesture. Throws when nothing matches, naming
234
+ * what the dial does offer.
235
+ */
236
+ async pick(filters) {
237
+ await this.open();
238
+ const [action] = await this.getActions(filters);
239
+ if (!action) {
240
+ const offered = await this.getActionLabels();
241
+ throw new Error(`WrSpeedDialHarness.pick(): no action matched ${JSON.stringify(filters)}. ` +
242
+ `The dial offers: ${offered.join(', ')}.`);
243
+ }
244
+ await action.click();
245
+ }
246
+ /**
247
+ * Press Escape — closing the dial and putting focus back on the trigger.
248
+ *
249
+ * Sent to the host, which is where the component listens. The focus return is the
250
+ * half worth asserting: `role="menu"` promises a way out, and the actions are
251
+ * ordinary tab stops, so without it a keyboard user has to walk through every
252
+ * action to leave.
253
+ */
254
+ async sendEscape() {
255
+ await (await this.host()).sendKeys(TestKey.ESCAPE);
256
+ }
257
+ /** Move keyboard focus to the trigger. */
258
+ async focusTrigger() {
259
+ return (await this.trigger()).focus();
260
+ }
261
+ /** Whether the trigger currently holds focus. */
262
+ async isTriggerFocused() {
263
+ return (await this.trigger()).isFocused();
264
+ }
265
+ async requireOpen(method) {
266
+ if (await this.isOpen())
267
+ return;
268
+ throw new Error(`WrSpeedDialHarness.${method}(): the dial is closed. Its actions stay in the DOM while collapsed — the ` +
269
+ 'fan-out animates them — and only `visibility` hides them, which a unit test does not apply. Reading ' +
270
+ 'them now would assert buttons nobody can reach. Call open() first, or getActionCount().');
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Generated bundle index. Do not edit.
276
+ */
277
+
278
+ export { WrSpeedDialActionHarness, WrSpeedDialHarness };
279
+ //# sourceMappingURL=ngwr-speed-dial-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-speed-dial-testing.mjs","sources":["../../../projects/lib/speed-dial/testing/wr-speed-dial-action-harness.ts","../../../projects/lib/speed-dial/testing/wr-speed-dial-harness.ts","../../../projects/lib/speed-dial/testing/ngwr-speed-dial-testing.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrSpeedDialActionHarnessFilters } from './interfaces';\n\n/**\n * Test harness for one action inside a {@link WrSpeedDialHarness}.\n *\n * The label is read from `aria-label` rather than from the button's text, and that\n * is the whole point of the control: an action renders an ICON, or — with no icon —\n * the first glyph of its label. Its visible text is therefore `\"S\"` for \"Share\", or\n * nothing at all. {@link getInitial} answers what is drawn; {@link getLabel} answers\n * what it is.\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSpeedDialActionHarness extends ComponentHarness {\n /** The action button. `role=\"menuitem\"` sits on this element too. */\n static hostSelector = '.wr-speed-dial__action';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSpeedDialActionHarnessFilters = {}): HarnessPredicate<WrSpeedDialActionHarness> {\n return new HarnessPredicate(WrSpeedDialActionHarness, options)\n .addOption('label', options.label, (harness, label) => HarnessPredicate.stringMatches(harness.getLabel(), label))\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n /** The action's accessible name — what a screen reader reads, and what `title` shows. */\n async getLabel(): Promise<string | null> {\n return (await this.host()).getAttribute('aria-label');\n }\n\n /** The role the action announces — `menuitem`. */\n async getRole(): Promise<string | null> {\n return (await this.host()).getAttribute('role');\n }\n\n /** Whether the action draws an icon rather than a letter. */\n async hasIcon(): Promise<boolean> {\n return (await this.locatorForOptional('wr-icon')()) !== null;\n }\n\n /** The icon's registered name, from `wr-icon`'s reflected `data-icon`, or `null`. */\n async getIconName(): Promise<string | null> {\n const icon = await this.locatorForOptional('wr-icon')();\n return icon ? icon.getAttribute('data-icon') : null;\n }\n\n /**\n * The single glyph drawn in place of a missing icon, or `null` when there is one.\n *\n * A GLYPH, not a character: the component splits the label by code point, so an\n * emoji label draws the whole emoji rather than the broken half of a surrogate pair.\n */\n async getInitial(): Promise<string | null> {\n if (await this.hasIcon()) return null;\n return (await this.host()).text();\n }\n\n /**\n * Whether the action refuses interaction.\n *\n * Every action follows the DIAL's `disabled` — there is no per-action flag — so\n * this is really \"is the dial disabled\", asked at the action. Worth having anyway:\n * it is what the DOM and the CDK consult before delivering a click.\n */\n async isDisabled(): Promise<boolean> {\n return (await this.host()).getProperty<boolean>('disabled');\n }\n\n /** Pick this action — emitting `pick` and closing the dial. */\n async click(): Promise<void> {\n if (await this.isDisabled()) {\n throw new Error(\n `WrSpeedDialActionHarness.click(): \"${await this.getLabel()}\" is disabled. The DOM swallows the click ` +\n 'and the component checks `disabled` again before emitting, so nothing would happen.'\n );\n }\n await (await this.host()).click();\n }\n\n /** Move keyboard focus to the action. */\n async focus(): Promise<void> {\n return (await this.host()).focus();\n }\n\n /** Whether the action currently holds focus. */\n async isFocused(): Promise<boolean> {\n return (await this.host()).isFocused();\n }\n}\n","/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';\n\nimport type { WrSpeedDialDirection } from 'ngwr/speed-dial';\n\nimport type { WrSpeedDialActionHarnessFilters, WrSpeedDialHarnessFilters } from './interfaces';\nimport { WrSpeedDialActionHarness } from './wr-speed-dial-action-harness';\n\nconst DIRECTIONS: readonly WrSpeedDialDirection[] = ['up', 'down', 'left', 'right'];\n\n/**\n * Test harness for `<wr-speed-dial>` — the floating trigger and the actions that fan\n * out of it.\n *\n * **The actions never leave the DOM.** They are hidden with `visibility`, which is\n * what actually takes them out of the tab order and the accessibility tree —\n * `opacity: 0` alone would leave them focusable — and CSS is exactly what a unit\n * test does not have. So a closed dial's buttons are queryable, clickable and\n * readable from a spec while being unreachable in a browser. {@link getActions} and\n * {@link pick} therefore REFUSE while the dial is closed rather than answering with\n * elements nobody can see; open it first, which is what a user has to do too.\n *\n * @example\n * ```ts\n * const dial = await loader.getHarness(WrSpeedDialHarness);\n *\n * await dial.open();\n * expect(await dial.getActionLabels()).toEqual(['Share', 'Copy link']);\n *\n * await dial.pick({ label: 'Share' });\n * expect(await dial.isOpen()).toBe(false);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSpeedDialHarness extends ComponentHarness {\n static hostSelector = 'wr-speed-dial';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSpeedDialHarnessFilters = {}): HarnessPredicate<WrSpeedDialHarness> {\n return new HarnessPredicate(WrSpeedDialHarness, options)\n .addOption('triggerLabel', options.triggerLabel, (harness, label) =>\n HarnessPredicate.stringMatches(harness.getTriggerLabel(), label)\n )\n .addOption(\n 'direction',\n options.direction,\n async (harness, direction) => (await harness.getDirection()) === direction\n )\n .addOption('open', options.open, async (harness, open) => (await harness.isOpen()) === open)\n .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled);\n }\n\n private readonly trigger = this.locatorFor('.wr-speed-dial__trigger');\n private readonly menu = this.locatorFor('.wr-speed-dial__actions');\n\n /** Whether the dial is fanned out, from the trigger's `aria-expanded`. */\n async isOpen(): Promise<boolean> {\n return (await (await this.trigger()).getAttribute('aria-expanded')) === 'true';\n }\n\n /** The direction the actions travel, from the host modifier. */\n async getDirection(): Promise<WrSpeedDialDirection> {\n const host = await this.host();\n for (const direction of DIRECTIONS) {\n if (await host.hasClass(`wr-speed-dial--${direction}`)) return direction;\n }\n throw new Error(\n 'WrSpeedDialHarness.getDirection(): this host carries no `wr-speed-dial--<direction>` class, which ' +\n 'every dial sets — the element matched is probably not an ngwr speed dial.'\n );\n }\n\n /**\n * The trigger's accessible name.\n *\n * The trigger is icon-only, so this is its ONLY name — there is no text to fall\n * back on, and a dial that loses it is announced as an unnamed button.\n */\n async getTriggerLabel(): Promise<string | null> {\n return (await this.trigger()).getAttribute('aria-label');\n }\n\n /** The trigger icon's registered name, from `wr-icon`'s reflected `data-icon`. */\n async getTriggerIcon(): Promise<string | null> {\n const icon = await this.locatorForOptional('.wr-speed-dial__trigger wr-icon')();\n return icon ? icon.getAttribute('data-icon') : null;\n }\n\n /** Whether the dial refuses interaction, from the trigger's own `disabled` property. */\n async isDisabled(): Promise<boolean> {\n return (await this.trigger()).getProperty<boolean>('disabled');\n }\n\n /** Whether the host is padded for the device's safe area (`safeArea`). */\n async hasSafeArea(): Promise<boolean> {\n return (await this.host()).hasClass('wr-speed-dial--safe-area');\n }\n\n /** The role the action list announces — `menu`. */\n async getMenuRole(): Promise<string | null> {\n return (await this.menu()).getAttribute('role');\n }\n\n /**\n * Whether the trigger's `aria-controls` names THIS dial's menu, and nothing else on\n * the page.\n *\n * Both halves matter. The id is resolved inside the host first, so a trigger\n * pointing at a SIBLING dial's menu — which resolves perfectly well from the\n * document — fails here. Then it is counted across the document, because two menus\n * answering to one id hand every reference to whichever comes first.\n */\n async isMenuBound(): Promise<boolean> {\n const id = await (await this.trigger()).getAttribute('aria-controls');\n if (!id) return false;\n\n const own = await this.locatorForOptional(`.wr-speed-dial__actions#${id}`)();\n if (!own) return false;\n\n const everywhere = await this.documentRootLocatorFactory().locatorForAll(`#${id}`)();\n return everywhere.length === 1;\n }\n\n /**\n * The actions, in DOM order.\n *\n * Throws while the dial is closed. The buttons are still in the DOM then — they\n * have to be, for the fan-out to animate — and jsdom applies no `visibility`, so\n * they would come back looking perfectly reachable when a real user cannot tab to\n * one. A spec that read them closed would pass on a dial that never opens.\n */\n async getActions(filters: WrSpeedDialActionHarnessFilters = {}): Promise<WrSpeedDialActionHarness[]> {\n await this.requireOpen('getActions');\n return this.locatorForAll(WrSpeedDialActionHarness.with(filters))();\n }\n\n /** The accessible names of the actions, in DOM order. Throws while closed. */\n async getActionLabels(): Promise<(string | null)[]> {\n const actions = await this.getActions();\n return Promise.all(actions.map(action => action.getLabel()));\n }\n\n /** How many actions the dial holds — readable while closed, since it is not a reach. */\n async getActionCount(): Promise<number> {\n return (await this.locatorForAll('.wr-speed-dial__action')()).length;\n }\n\n /** Open the dial. An already-open one is left alone; a disabled one throws. */\n async open(): Promise<void> {\n if (await this.isOpen()) return;\n await this.toggle();\n\n if (!(await this.isOpen())) {\n throw new Error(\n 'WrSpeedDialHarness.open(): the dial did not open. A disabled trigger refuses the click twice over — ' +\n 'the DOM swallows it, and the component checks `disabled` again.'\n );\n }\n }\n\n /** Close the dial. An already-closed one is left alone. */\n async close(): Promise<void> {\n if (!(await this.isOpen())) return;\n await this.toggle();\n }\n\n /** Click the trigger once, whichever way that takes it. */\n async toggle(): Promise<void> {\n return (await this.trigger()).click();\n }\n\n /**\n * Pick the first action matching the filters — emitting `pick` and closing the dial.\n *\n * Opens first if it has to, the way `WrDropdownHarness.clickItem` does: a spec that\n * says \"pick Share\" means the whole gesture. Throws when nothing matches, naming\n * what the dial does offer.\n */\n async pick(filters: WrSpeedDialActionHarnessFilters): Promise<void> {\n await this.open();\n\n const [action] = await this.getActions(filters);\n if (!action) {\n const offered = await this.getActionLabels();\n throw new Error(\n `WrSpeedDialHarness.pick(): no action matched ${JSON.stringify(filters)}. ` +\n `The dial offers: ${offered.join(', ')}.`\n );\n }\n await action.click();\n }\n\n /**\n * Press Escape — closing the dial and putting focus back on the trigger.\n *\n * Sent to the host, which is where the component listens. The focus return is the\n * half worth asserting: `role=\"menu\"` promises a way out, and the actions are\n * ordinary tab stops, so without it a keyboard user has to walk through every\n * action to leave.\n */\n async sendEscape(): Promise<void> {\n await (await this.host()).sendKeys(TestKey.ESCAPE);\n }\n\n /** Move keyboard focus to the trigger. */\n async focusTrigger(): Promise<void> {\n return (await this.trigger()).focus();\n }\n\n /** Whether the trigger currently holds focus. */\n async isTriggerFocused(): Promise<boolean> {\n return (await this.trigger()).isFocused();\n }\n\n private async requireOpen(method: string): Promise<void> {\n if (await this.isOpen()) return;\n throw new Error(\n `WrSpeedDialHarness.${method}(): the dial is closed. Its actions stay in the DOM while collapsed — the ` +\n 'fan-out animates them — and only `visibility` hides them, which a unit test does not apply. Reading ' +\n 'them now would assert buttons nobody can reach. Call open() first, or getActionCount().'\n );\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;AAUG;AACG,MAAO,wBAAyB,SAAQ,gBAAgB,CAAA;;AAE5D,IAAA,OAAO,YAAY,GAAG,wBAAwB;;AAG9C,IAAA,OAAO,IAAI,CAAC,OAAA,GAA2C,EAAE,EAAA;AACvD,QAAA,OAAO,IAAI,gBAAgB,CAAC,wBAAwB,EAAE,OAAO;aAC1D,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,KAAK,KAAK,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,KAAK,CAAC;aAC/G,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC,UAAU,EAAE,MAAM,QAAQ,CAAC;IACpH;;AAGA,IAAA,MAAM,QAAQ,GAAA;AACZ,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,YAAY,CAAC;IACvD;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,MAAM,CAAC;IACjD;;AAGA,IAAA,MAAM,OAAO,GAAA;AACX,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,EAAE,MAAM,IAAI;IAC9D;;AAGA,IAAA,MAAM,WAAW,GAAA;QACf,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,EAAE;AACvD,QAAA,OAAO,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,IAAI;IACrD;AAEA;;;;;AAKG;AACH,IAAA,MAAM,UAAU,GAAA;AACd,QAAA,IAAI,MAAM,IAAI,CAAC,OAAO,EAAE;AAAE,YAAA,OAAO,IAAI;QACrC,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE;IACnC;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,UAAU,GAAA;AACd,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,WAAW,CAAU,UAAU,CAAC;IAC7D;;AAGA,IAAA,MAAM,KAAK,GAAA;AACT,QAAA,IAAI,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE;YAC3B,MAAM,IAAI,KAAK,CACb,CAAA,mCAAA,EAAsC,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAA,0CAAA,CAA4C;AACrG,gBAAA,qFAAqF,CACxF;QACH;QACA,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE;IACnC;;AAGA,IAAA,MAAM,KAAK,GAAA;QACT,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE;IACpC;;AAGA,IAAA,MAAM,SAAS,GAAA;QACb,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE;IACxC;;;AC/FF;;;;;AAKG;AASH,MAAM,UAAU,GAAoC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC;AAEnF;;;;;;;;;;;;;;;;;;;;;;;;AAwBG;AACG,MAAO,kBAAmB,SAAQ,gBAAgB,CAAA;AACtD,IAAA,OAAO,YAAY,GAAG,eAAe;;AAGrC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAqC,EAAE,EAAA;AACjD,QAAA,OAAO,IAAI,gBAAgB,CAAC,kBAAkB,EAAE,OAAO;aACpD,SAAS,CAAC,cAAc,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,KAAK,KAC9D,gBAAgB,CAAC,aAAa,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,KAAK,CAAC;aAEjE,SAAS,CACR,WAAW,EACX,OAAO,CAAC,SAAS,EACjB,OAAO,OAAO,EAAE,SAAS,KAAK,CAAC,MAAM,OAAO,CAAC,YAAY,EAAE,MAAM,SAAS;aAE3E,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,OAAO,EAAE,IAAI,KAAK,CAAC,MAAM,OAAO,CAAC,MAAM,EAAE,MAAM,IAAI;aAC1F,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC,UAAU,EAAE,MAAM,QAAQ,CAAC;IACpH;AAEiB,IAAA,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,yBAAyB,CAAC;AACpD,IAAA,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,yBAAyB,CAAC;;AAGlE,IAAA,MAAM,MAAM,GAAA;AACV,QAAA,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC,MAAM,MAAM;IAChF;;AAGA,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE;AAC9B,QAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;YAClC,IAAI,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAA,eAAA,EAAkB,SAAS,EAAE,CAAC;AAAE,gBAAA,OAAO,SAAS;QAC1E;QACA,MAAM,IAAI,KAAK,CACb,oGAAoG;AAClG,YAAA,2EAA2E,CAC9E;IACH;AAEA;;;;;AAKG;AACH,IAAA,MAAM,eAAe,GAAA;AACnB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,YAAY,CAAC;IAC1D;;AAGA,IAAA,MAAM,cAAc,GAAA;QAClB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,iCAAiC,CAAC,EAAE;AAC/E,QAAA,OAAO,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,IAAI;IACrD;;AAGA,IAAA,MAAM,UAAU,GAAA;AACd,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,CAAU,UAAU,CAAC;IAChE;;AAGA,IAAA,MAAM,WAAW,GAAA;AACf,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,0BAA0B,CAAC;IACjE;;AAGA,IAAA,MAAM,WAAW,GAAA;AACf,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,YAAY,CAAC,MAAM,CAAC;IACjD;AAEA;;;;;;;;AAQG;AACH,IAAA,MAAM,WAAW,GAAA;AACf,QAAA,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,eAAe,CAAC;AACrE,QAAA,IAAI,CAAC,EAAE;AAAE,YAAA,OAAO,KAAK;AAErB,QAAA,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,CAAA,wBAAA,EAA2B,EAAE,CAAA,CAAE,CAAC,EAAE;AAC5E,QAAA,IAAI,CAAC,GAAG;AAAE,YAAA,OAAO,KAAK;AAEtB,QAAA,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,0BAA0B,EAAE,CAAC,aAAa,CAAC,CAAA,CAAA,EAAI,EAAE,CAAA,CAAE,CAAC,EAAE;AACpF,QAAA,OAAO,UAAU,CAAC,MAAM,KAAK,CAAC;IAChC;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,UAAU,CAAC,OAAA,GAA2C,EAAE,EAAA;AAC5D,QAAA,MAAM,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC;AACpC,QAAA,OAAO,IAAI,CAAC,aAAa,CAAC,wBAAwB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE;IACrE;;AAGA,IAAA,MAAM,eAAe,GAAA;AACnB,QAAA,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE;AACvC,QAAA,OAAO,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC9D;;AAGA,IAAA,MAAM,cAAc,GAAA;AAClB,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,aAAa,CAAC,wBAAwB,CAAC,EAAE,EAAE,MAAM;IACtE;;AAGA,IAAA,MAAM,IAAI,GAAA;AACR,QAAA,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE;YAAE;AACzB,QAAA,MAAM,IAAI,CAAC,MAAM,EAAE;QAEnB,IAAI,EAAE,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE;YAC1B,MAAM,IAAI,KAAK,CACb,sGAAsG;AACpG,gBAAA,iEAAiE,CACpE;QACH;IACF;;AAGA,IAAA,MAAM,KAAK,GAAA;AACT,QAAA,IAAI,EAAE,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YAAE;AAC5B,QAAA,MAAM,IAAI,CAAC,MAAM,EAAE;IACrB;;AAGA,IAAA,MAAM,MAAM,GAAA;QACV,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE;IACvC;AAEA;;;;;;AAMG;IACH,MAAM,IAAI,CAAC,OAAwC,EAAA;AACjD,QAAA,MAAM,IAAI,CAAC,IAAI,EAAE;QAEjB,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;QAC/C,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,eAAe,EAAE;YAC5C,MAAM,IAAI,KAAK,CACb,CAAA,6CAAA,EAAgD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA,EAAA,CAAI;gBACzE,CAAA,iBAAA,EAAoB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA,CAAA,CAAG,CAC5C;QACH;AACA,QAAA,MAAM,MAAM,CAAC,KAAK,EAAE;IACtB;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,UAAU,GAAA;AACd,QAAA,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;IACpD;;AAGA,IAAA,MAAM,YAAY,GAAA;QAChB,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE;IACvC;;AAGA,IAAA,MAAM,gBAAgB,GAAA;QACpB,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE;IAC3C;IAEQ,MAAM,WAAW,CAAC,MAAc,EAAA;AACtC,QAAA,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE;YAAE;AACzB,QAAA,MAAM,IAAI,KAAK,CACb,CAAA,mBAAA,EAAsB,MAAM,CAAA,0EAAA,CAA4E;YACtG,sGAAsG;AACtG,YAAA,yFAAyF,CAC5F;IACH;;;ACpOF;;AAEG;;;;"}
@@ -0,0 +1,89 @@
1
+ import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
2
+
3
+ /**
4
+ * @license
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE
8
+ */
9
+ /**
10
+ * Test harness for `<wr-splash-cursor>` — a WebGL fluid that splashes dye under the
11
+ * pointer.
12
+ *
13
+ * **This is a deliberately small surface, and the reason is the component.** The
14
+ * simulation is GL programs and framebuffers driven by `window` pointer listeners; the
15
+ * layout is `position: fixed`, `inset: 0`, `z-index: 50` and a `100dvh` canvas, all of
16
+ * it in the stylesheet. A unit test gets no WebGL context — the simulation is never
17
+ * constructed — and loads no stylesheet, so asking `getCssValue('position')` would
18
+ * answer `static` about a component whose whole layout contract is `fixed`. Three
19
+ * questions survive that, and they are the three below.
20
+ *
21
+ * There is no `isSimulating()`, `getSplatCount()` or `splat()`: the simulation returns
22
+ * `null` the moment the context is refused, so every one of those would report on
23
+ * something that was never built. No `moveMouse()` either — the listeners live on
24
+ * `window` and are only attached once a context exists, so a dispatched move reaches
25
+ * nothing. And no `isReducedMotion()` or `hasWebGL()`: the component renders
26
+ * byte-identical DOM whether it is running, standing down for reduced motion, or on a
27
+ * machine with no WebGL at all, so any answer would be the same for a working component
28
+ * and a broken one.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const splash = await loader.getHarness(WrSplashCursorHarness);
33
+ *
34
+ * expect(await splash.isFullscreen()).toBe(true);
35
+ * expect(await splash.isDecorative()).toBe(true);
36
+ * ```
37
+ *
38
+ * @see https://ngwr.dev/guides/testing
39
+ */
40
+ class WrSplashCursorHarness extends ComponentHarness {
41
+ static hostSelector = 'wr-splash-cursor';
42
+ /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */
43
+ static with(options = {}) {
44
+ return new HarnessPredicate(WrSplashCursorHarness, options).addOption('fullscreen', options.fullscreen, async (harness, fullscreen) => (await harness.isFullscreen()) === fullscreen);
45
+ }
46
+ /**
47
+ * Whether the effect covers the whole viewport, rather than filling the nearest
48
+ * positioned ancestor.
49
+ *
50
+ * The component's one behavioural switch: fullscreen is a fixed, click-through
51
+ * overlay above the page, contained is an absolutely positioned box whose walls the
52
+ * dye collides with. Note what is being read — there is no `--fullscreen` class, only
53
+ * a `--contained` one written from a NEGATED binding, so this method answers the
54
+ * absence of a class. That inversion is exactly the kind that gets flipped in a
55
+ * refactor and looks fine on a page where the effect is full-viewport anyway, which
56
+ * is the whole reason the method exists.
57
+ */
58
+ async isFullscreen() {
59
+ return !(await (await this.host()).hasClass('wr-splash-cursor--contained'));
60
+ }
61
+ /**
62
+ * Whether the effect is hidden from assistive tech.
63
+ *
64
+ * It has no text and nothing to say, and at full size it sits in front of the entire
65
+ * page — so losing this puts a nameless graphic over everything a screen reader is
66
+ * trying to get through.
67
+ */
68
+ async isDecorative() {
69
+ return (await (await this.locatorFor('.wr-splash-cursor__canvas')()).getAttribute('aria-hidden')) === 'true';
70
+ }
71
+ /**
72
+ * Whether the drawing surface is in the markup.
73
+ *
74
+ * It is there in all three states where nothing is ever drawn on it — prerendered on
75
+ * the server, WebGL refused, or reduced motion — because the documented contract is
76
+ * that the component renders nothing VISIBLE and stops, never that it renders no
77
+ * element and never that it throws on a null context.
78
+ */
79
+ async hasCanvas() {
80
+ return (await this.locatorForOptional('.wr-splash-cursor__canvas')()) !== null;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Generated bundle index. Do not edit.
86
+ */
87
+
88
+ export { WrSplashCursorHarness };
89
+ //# sourceMappingURL=ngwr-splash-cursor-testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ngwr-splash-cursor-testing.mjs","sources":["../../../projects/lib/splash-cursor/testing/wr-splash-cursor-harness.ts","../../../projects/lib/splash-cursor/testing/ngwr-splash-cursor-testing.ts"],"sourcesContent":["/**\n * @license\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://github.com/thekhegay/ngwr/blob/main/LICENSE\n */\n\nimport { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';\n\nimport type { WrSplashCursorHarnessFilters } from './interfaces';\n\n/**\n * Test harness for `<wr-splash-cursor>` — a WebGL fluid that splashes dye under the\n * pointer.\n *\n * **This is a deliberately small surface, and the reason is the component.** The\n * simulation is GL programs and framebuffers driven by `window` pointer listeners; the\n * layout is `position: fixed`, `inset: 0`, `z-index: 50` and a `100dvh` canvas, all of\n * it in the stylesheet. A unit test gets no WebGL context — the simulation is never\n * constructed — and loads no stylesheet, so asking `getCssValue('position')` would\n * answer `static` about a component whose whole layout contract is `fixed`. Three\n * questions survive that, and they are the three below.\n *\n * There is no `isSimulating()`, `getSplatCount()` or `splat()`: the simulation returns\n * `null` the moment the context is refused, so every one of those would report on\n * something that was never built. No `moveMouse()` either — the listeners live on\n * `window` and are only attached once a context exists, so a dispatched move reaches\n * nothing. And no `isReducedMotion()` or `hasWebGL()`: the component renders\n * byte-identical DOM whether it is running, standing down for reduced motion, or on a\n * machine with no WebGL at all, so any answer would be the same for a working component\n * and a broken one.\n *\n * @example\n * ```ts\n * const splash = await loader.getHarness(WrSplashCursorHarness);\n *\n * expect(await splash.isFullscreen()).toBe(true);\n * expect(await splash.isDecorative()).toBe(true);\n * ```\n *\n * @see https://ngwr.dev/guides/testing\n */\nexport class WrSplashCursorHarness extends ComponentHarness {\n static hostSelector = 'wr-splash-cursor';\n\n /** Build a predicate that narrows the query — pass to `getHarness` / `getAllHarnesses`. */\n static with(options: WrSplashCursorHarnessFilters = {}): HarnessPredicate<WrSplashCursorHarness> {\n return new HarnessPredicate(WrSplashCursorHarness, options).addOption(\n 'fullscreen',\n options.fullscreen,\n async (harness, fullscreen) => (await harness.isFullscreen()) === fullscreen\n );\n }\n\n /**\n * Whether the effect covers the whole viewport, rather than filling the nearest\n * positioned ancestor.\n *\n * The component's one behavioural switch: fullscreen is a fixed, click-through\n * overlay above the page, contained is an absolutely positioned box whose walls the\n * dye collides with. Note what is being read — there is no `--fullscreen` class, only\n * a `--contained` one written from a NEGATED binding, so this method answers the\n * absence of a class. That inversion is exactly the kind that gets flipped in a\n * refactor and looks fine on a page where the effect is full-viewport anyway, which\n * is the whole reason the method exists.\n */\n async isFullscreen(): Promise<boolean> {\n return !(await (await this.host()).hasClass('wr-splash-cursor--contained'));\n }\n\n /**\n * Whether the effect is hidden from assistive tech.\n *\n * It has no text and nothing to say, and at full size it sits in front of the entire\n * page — so losing this puts a nameless graphic over everything a screen reader is\n * trying to get through.\n */\n async isDecorative(): Promise<boolean> {\n return (await (await this.locatorFor('.wr-splash-cursor__canvas')()).getAttribute('aria-hidden')) === 'true';\n }\n\n /**\n * Whether the drawing surface is in the markup.\n *\n * It is there in all three states where nothing is ever drawn on it — prerendered on\n * the server, WebGL refused, or reduced motion — because the documented contract is\n * that the component renders nothing VISIBLE and stops, never that it renders no\n * element and never that it throws on a null context.\n */\n async hasCanvas(): Promise<boolean> {\n return (await this.locatorForOptional('.wr-splash-cursor__canvas')()) !== null;\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;AAKG;AAMH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BG;AACG,MAAO,qBAAsB,SAAQ,gBAAgB,CAAA;AACzD,IAAA,OAAO,YAAY,GAAG,kBAAkB;;AAGxC,IAAA,OAAO,IAAI,CAAC,OAAA,GAAwC,EAAE,EAAA;AACpD,QAAA,OAAO,IAAI,gBAAgB,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC,SAAS,CACnE,YAAY,EACZ,OAAO,CAAC,UAAU,EAClB,OAAO,OAAO,EAAE,UAAU,KAAK,CAAC,MAAM,OAAO,CAAC,YAAY,EAAE,MAAM,UAAU,CAC7E;IACH;AAEA;;;;;;;;;;;AAWG;AACH,IAAA,MAAM,YAAY,GAAA;AAChB,QAAA,OAAO,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,6BAA6B,CAAC,CAAC;IAC7E;AAEA;;;;;;AAMG;AACH,IAAA,MAAM,YAAY,GAAA;QAChB,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,2BAA2B,CAAC,EAAE,EAAE,YAAY,CAAC,aAAa,CAAC,MAAM,MAAM;IAC9G;AAEA;;;;;;;AAOG;AACH,IAAA,MAAM,SAAS,GAAA;AACb,QAAA,OAAO,CAAC,MAAM,IAAI,CAAC,kBAAkB,CAAC,2BAA2B,CAAC,EAAE,MAAM,IAAI;IAChF;;;AC3FF;;AAEG;;;;"}