@nexxtmove/ui 2.5.0 → 2.7.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.
- package/dist/components/editor/molecules/EditorBorder/EditorBorder.vue.d.ts +12 -0
- package/dist/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue.d.ts +16 -0
- package/dist/index.js +1094 -1135
- package/dist/nuxt.js +0 -1
- package/package.json +38 -36
- package/src/attribute-contract.test.ts +151 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.stories.ts +36 -0
- package/src/components/atoms/AnimatedNumber/AnimatedNumber.test.ts +21 -0
- package/src/components/atoms/Avatar/Avatar.stories.ts +122 -0
- package/src/components/atoms/Avatar/Avatar.test.ts +134 -0
- package/src/components/atoms/Button/Button.stories.ts +290 -0
- package/src/components/atoms/Button/Button.test.ts +406 -0
- package/src/components/atoms/Card/Card.stories.ts +45 -0
- package/src/components/atoms/Card/Card.test.ts +46 -0
- package/src/components/atoms/Checkbox/Checkbox.stories.ts +35 -0
- package/src/components/atoms/Checkbox/Checkbox.test.ts +55 -0
- package/src/components/atoms/Chip/Chip.stories.ts +274 -0
- package/src/components/atoms/Chip/Chip.test.ts +230 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.stories.ts +189 -0
- package/src/components/atoms/EnergyLabel/EnergyLabel.test.ts +100 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.stories.ts +677 -0
- package/src/components/atoms/FloatingPanel/FloatingPanel.test.ts +864 -0
- package/src/components/atoms/Icon/Icon.stories.ts +63 -0
- package/src/components/atoms/Icon/Icon.test.ts +89 -0
- package/src/components/atoms/Loader/Loader.stories.ts +36 -0
- package/src/components/atoms/Loader/Loader.test.ts +40 -0
- package/src/components/atoms/PickerItem/PickerItem.stories.ts +172 -0
- package/src/components/atoms/PickerItem/PickerItem.test.ts +142 -0
- package/src/components/atoms/PickerPreview/PickerPreview.stories.ts +183 -0
- package/src/components/atoms/PickerPreview/PickerPreview.test.ts +137 -0
- package/src/components/atoms/PresenceIndicator/PresenceIndicator.stories.ts +21 -0
- package/src/components/atoms/PropertyListing/PropertyListing.stories.ts +38 -0
- package/src/components/atoms/PropertyListing/PropertyListing.test.ts +46 -0
- package/src/components/atoms/SelectableOption/SelectableOption.stories.ts +131 -0
- package/src/components/atoms/SelectableOption/SelectableOption.test.ts +96 -0
- package/src/components/atoms/Skeleton/Skeleton.stories.ts +27 -0
- package/src/components/atoms/Skeleton/Skeleton.test.ts +28 -0
- package/src/components/atoms/Slider/Slider.stories.ts +151 -0
- package/src/components/atoms/Slider/Slider.test.ts +191 -0
- package/src/components/atoms/Squircle/Squircle.stories.ts +114 -0
- package/src/components/atoms/Squircle/Squircle.test.ts +70 -0
- package/src/components/atoms/Switch/Switch.stories.ts +91 -0
- package/src/components/atoms/Switch/Switch.test.ts +183 -0
- package/src/components/atoms/Tooltip/Tooltip.stories.ts +158 -0
- package/src/components/atoms/Tooltip/Tooltip.test.ts +134 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.stories.ts +124 -0
- package/src/components/editor/atoms/EditorDimensionField/EditorDimensionField.test.ts +464 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.stories.ts +266 -0
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.test.ts +421 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.stories.ts +365 -0
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.test.ts +484 -0
- package/src/components/editor/composables/useRadioGrid.test.ts +261 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.stories.ts +73 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.test.ts +189 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.stories.ts +309 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.test.ts +359 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.stories.ts +138 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.test.ts +344 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.stories.ts +87 -0
- package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.test.ts +262 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.stories.ts +115 -0
- package/src/components/editor/molecules/EditorBlur/EditorBlur.test.ts +243 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.stories.ts +187 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.test.ts +456 -0
- package/src/components/editor/molecules/EditorBorder/EditorBorder.vue +58 -2
- package/src/components/editor/molecules/EditorColor/EditorColor.stories.ts +563 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.test.ts +689 -0
- package/src/components/editor/molecules/EditorColor/EditorColorGradient.test.ts +793 -0
- package/src/components/editor/molecules/EditorColor/_ColorField.vue +7 -29
- package/src/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue +57 -0
- package/src/components/editor/molecules/EditorColor/gradientClass.test.ts +303 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.stories.ts +72 -0
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.test.ts +124 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.stories.ts +46 -0
- package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.test.ts +97 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.stories.ts +248 -0
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.test.ts +288 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.stories.ts +129 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.test.ts +253 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.stories.ts +68 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.test.ts +150 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.stories.ts +71 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.test.ts +161 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.stories.ts +204 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.test.ts +238 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.stories.ts +257 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.test.ts +286 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.stories.ts +135 -0
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.test.ts +230 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.stories.ts +154 -0
- package/src/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.test.ts +112 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.stories.ts +206 -0
- package/src/components/editor/molecules/EditorOverrideStatus/EditorOverrideStatus.test.ts +332 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.stories.ts +374 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.test.ts +712 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.stories.ts +64 -0
- package/src/components/editor/molecules/EditorRadius/EditorRadius.test.ts +137 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.stories.ts +103 -0
- package/src/components/editor/molecules/EditorShadow/EditorShadow.test.ts +168 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.stories.ts +84 -0
- package/src/components/editor/molecules/EditorSize/EditorSize.test.ts +253 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.stories.ts +49 -0
- package/src/components/editor/molecules/EditorSizeLimit/EditorSizeLimit.test.ts +43 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.stories.ts +69 -0
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.test.ts +155 -0
- package/src/components/molecules/Announcement/Announcement.stories.ts +224 -0
- package/src/components/molecules/Announcement/Announcement.test.ts +161 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.stories.ts +99 -0
- package/src/components/molecules/ContactInfoCard/ContactInfoCard.test.ts +70 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.stories.ts +148 -0
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.test.ts +271 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.stories.ts +45 -0
- package/src/components/molecules/DoughnutChart/DoughnutChart.test.ts +205 -0
- package/src/components/molecules/InfoBlock/InfoBlock.stories.ts +84 -0
- package/src/components/molecules/InfoBlock/InfoBlock.test.ts +101 -0
- package/src/components/molecules/InputField/InputField.stories.ts +117 -0
- package/src/components/molecules/InputField/InputField.test.ts +138 -0
- package/src/components/molecules/InputSelect/InputSelect.stories.ts +116 -0
- package/src/components/molecules/InputSelect/InputSelect.test.ts +127 -0
- package/src/components/molecules/ModalFooter/ModalFooter.stories.ts +204 -0
- package/src/components/molecules/ModalFooter/ModalFooter.test.ts +276 -0
- package/src/components/molecules/PageCard/PageCard.stories.ts +100 -0
- package/src/components/molecules/PageCard/PageCard.test.ts +91 -0
- package/src/components/molecules/PageContainer/PageContainer.stories.ts +90 -0
- package/src/components/molecules/PageContainer/PageContainer.test.ts +50 -0
- package/src/components/molecules/PageMockup/PageMockup.stories.ts +38 -0
- package/src/components/molecules/PageMockup/PageMockup.test.ts +20 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.stories.ts +63 -0
- package/src/components/molecules/PageTypeTile/PageTypeTile.test.ts +41 -0
- package/src/components/molecules/Pagination/Pagination.stories.ts +72 -0
- package/src/components/molecules/Pagination/Pagination.test.ts +130 -0
- package/src/components/molecules/PickerGroup/PickerGroup.stories.ts +90 -0
- package/src/components/molecules/PickerGroup/PickerGroup.test.ts +145 -0
- package/src/components/molecules/PickerList/PickerList.stories.ts +436 -0
- package/src/components/molecules/PickerList/PickerList.test.ts +870 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.stories.ts +48 -0
- package/src/components/molecules/PlaceholderInput/PlaceholderInput.test.ts +107 -0
- package/src/components/molecules/PropertyCard/PropertyCard.stories.ts +46 -0
- package/src/components/molecules/PropertyCard/PropertyCard.test.ts +59 -0
- package/src/components/molecules/Select/Select.stories.ts +264 -0
- package/src/components/molecules/Select/Select.test.ts +315 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.stories.ts +84 -0
- package/src/components/molecules/SidebarMenuItem/SidebarMenuItem.test.ts +103 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.stories.ts +11 -0
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.test.ts +38 -0
- package/src/components/molecules/SocialIcons/SocialIcons.stories.ts +28 -0
- package/src/components/molecules/SocialIcons/SocialIcons.test.ts +58 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.stories.ts +11 -0
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.test.ts +116 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.stories.ts +37 -0
- package/src/components/molecules/SocialMediaType/SocialMediaType.test.ts +126 -0
- package/src/components/molecules/Source/Source.stories.ts +35 -0
- package/src/components/molecules/Source/Source.test.ts +45 -0
- package/src/components/molecules/TabbedContent/TabbedContent.stories.ts +96 -0
- package/src/components/molecules/TabbedContent/TabbedContent.test.ts +109 -0
- package/src/components/molecules/Tabs/Tabs.stories.ts +230 -0
- package/src/components/molecules/Tabs/Tabs.test.ts +280 -0
- package/src/components/molecules/TemplateCard/TemplateCard.stories.ts +327 -0
- package/src/components/molecules/TemplateCard/TemplateCard.test.ts +339 -0
- package/src/components/molecules/Textarea/Textarea.stories.ts +216 -0
- package/src/components/molecules/Textarea/Textarea.test.ts +191 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.stories.ts +319 -0
- package/src/components/molecules/TimelineEvent/TimelineEvent.test.ts +224 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.stories.ts +29 -0
- package/src/components/molecules/TimelineLanes/TimelineLanes.test.ts +30 -0
- package/src/components/molecules/Toast/Toast.stories.ts +110 -0
- package/src/components/molecules/Toast/Toast.test.ts +103 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.stories.ts +297 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.test.ts +578 -0
- package/src/components/molecules/Tree/Tree.stories.ts +884 -0
- package/src/components/molecules/Tree/Tree.test.ts +1247 -0
- package/src/components/molecules/Tree/tree-drop.test.ts +223 -0
- package/src/components/organisms/Calendar/Calendar.stories.ts +108 -0
- package/src/components/organisms/Calendar/Calendar.test.ts +279 -0
- package/src/components/organisms/Calendar/_CalendarDayView.test.ts +104 -0
- package/src/components/organisms/Calendar/_CalendarHeader.test.ts +86 -0
- package/src/components/organisms/Calendar/_CalendarMonthView.test.ts +52 -0
- package/src/components/organisms/Calendar/_CalendarTimePicker.test.ts +89 -0
- package/src/components/organisms/Calendar/_CalendarYearView.test.ts +56 -0
- package/src/components/organisms/ColorPicker/ColorPicker.stories.ts +229 -0
- package/src/components/organisms/ColorPicker/ColorPicker.test.ts +638 -0
- package/src/components/organisms/ColorPicker/gradient.test.ts +67 -0
- package/src/components/organisms/ColorPicker/gradientGeometry.test.ts +206 -0
- package/src/components/organisms/Combobox/Combobox.stories.ts +234 -0
- package/src/components/organisms/Combobox/Combobox.test.ts +512 -0
- package/src/components/organisms/Combobox/Combobox.vue +1 -1
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.stories.ts +158 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.test.ts +524 -0
- package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +1 -1
- package/src/components/organisms/CountrySelect/CountrySelect.stories.ts +139 -0
- package/src/components/organisms/CountrySelect/CountrySelect.test.ts +149 -0
- package/src/components/organisms/DatePicker/DatePicker.stories.ts +163 -0
- package/src/components/organisms/DatePicker/DatePicker.test.ts +214 -0
- package/src/components/organisms/DropdownButton/DropdownButton.stories.ts +123 -0
- package/src/components/organisms/DropdownButton/DropdownButton.test.ts +379 -0
- package/src/components/organisms/FontPicker/FontPicker.stories.ts +476 -0
- package/src/components/organisms/FontPicker/FontPicker.test.ts +873 -0
- package/src/components/organisms/Header/Header.stories.ts +75 -0
- package/src/components/organisms/Header/Header.test.ts +233 -0
- package/src/components/organisms/IconPicker/IconPicker.stories.ts +734 -0
- package/src/components/organisms/IconPicker/IconPicker.test.ts +669 -0
- package/src/components/organisms/IconPicker/_IconPickerGrid.test.ts +184 -0
- package/src/components/organisms/IconPicker/_IconPickerRail.test.ts +102 -0
- package/src/components/organisms/IconPicker/useIconPickerNavigation.test.ts +215 -0
- package/src/components/organisms/Modal/Modal.stories.ts +350 -0
- package/src/components/organisms/Modal/Modal.test.ts +275 -0
- package/src/components/organisms/PickerPanel/PickerPanel.stories.ts +452 -0
- package/src/components/organisms/PickerPanel/PickerPanel.test.ts +358 -0
- package/src/components/organisms/ProgressBar/ProgressBar.stories.ts +100 -0
- package/src/components/organisms/ProgressBar/ProgressBar.test.ts +417 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.stories.ts +191 -0
- package/src/components/organisms/ReferenceCard/ReferenceCard.test.ts +165 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.stories.ts +65 -0
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.test.ts +466 -0
- package/src/components/organisms/StepperHeader/StepperHeader.stories.ts +91 -0
- package/src/components/organisms/StepperHeader/StepperHeader.test.ts +279 -0
- package/src/components/organisms/Table/Table.stories.ts +332 -0
- package/src/components/organisms/Table/Table.test.ts +320 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.stories.ts +198 -0
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.test.ts +283 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.stories.ts +120 -0
- package/src/components/organisms/TimelineTrack/TimelineTrack.test.ts +91 -0
- package/src/components/organisms/Toaster/Toaster.stories.ts +51 -0
- package/src/components/organisms/Toaster/Toaster.test.ts +75 -0
- package/src/components/organisms/VideoControls/VideoControls.stories.ts +792 -0
- package/src/components/organisms/VideoControls/VideoControls.test.ts +1056 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.stories.ts +327 -0
- package/src/components/organisms/VideoPlayer/VideoPlayer.test.ts +439 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
2
|
+
import { mount, type VueWrapper } from '@vue/test-utils'
|
|
3
|
+
import { defineComponent, h, ref, type Ref } from 'vue'
|
|
4
|
+
import { useRadioGrid, type UseRadioGridOptions } from './useRadioGrid'
|
|
5
|
+
|
|
6
|
+
const mounted: VueWrapper[] = []
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
mounted.splice(0).forEach((wrapper) => wrapper.unmount())
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A bare 3×3 host: one button per registered position, laid out by index only.
|
|
14
|
+
* The composable owns no markup, so this is all it needs.
|
|
15
|
+
*/
|
|
16
|
+
const mountGrid = (
|
|
17
|
+
options: Partial<Omit<UseRadioGridOptions, 'selected' | 'onChoose'>> & {
|
|
18
|
+
initial?: number | null
|
|
19
|
+
} = {},
|
|
20
|
+
) => {
|
|
21
|
+
const selected: Ref<number | null> = ref(options.initial === undefined ? 4 : options.initial)
|
|
22
|
+
const onChoose = vi.fn((position: number) => (selected.value = position))
|
|
23
|
+
const onLeave = vi.fn()
|
|
24
|
+
let api!: ReturnType<typeof useRadioGrid>
|
|
25
|
+
|
|
26
|
+
const Host = defineComponent({
|
|
27
|
+
setup() {
|
|
28
|
+
api = useRadioGrid({
|
|
29
|
+
rows: 3,
|
|
30
|
+
columns: 3,
|
|
31
|
+
...options,
|
|
32
|
+
selected,
|
|
33
|
+
onChoose,
|
|
34
|
+
onLeave,
|
|
35
|
+
})
|
|
36
|
+
const positions = () =>
|
|
37
|
+
(options.positions as readonly number[] | undefined) ?? [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
|
38
|
+
return () =>
|
|
39
|
+
h('div', [
|
|
40
|
+
h(
|
|
41
|
+
'div',
|
|
42
|
+
{ 'data-grid': '', onFocusout: api.onFocusout },
|
|
43
|
+
positions().map((position) =>
|
|
44
|
+
h('button', {
|
|
45
|
+
key: position,
|
|
46
|
+
'data-position': position,
|
|
47
|
+
tabindex: api.tabStop.value === position ? 0 : -1,
|
|
48
|
+
ref: api.cellRef(position),
|
|
49
|
+
onKeydown: (event: KeyboardEvent) => api.onKeydown(event, position),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
h('button', { 'data-outside': '' }),
|
|
54
|
+
])
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
59
|
+
mounted.push(wrapper)
|
|
60
|
+
return { wrapper, selected, onChoose, onLeave, api: () => api }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cell = (wrapper: VueWrapper, position: number) =>
|
|
64
|
+
wrapper.get(`[data-position="${position}"]`).element as HTMLElement
|
|
65
|
+
|
|
66
|
+
const focused = () => Number((document.activeElement as HTMLElement).dataset.position)
|
|
67
|
+
|
|
68
|
+
/** A real, cancelable keydown on the focused cell; returns whether it was prevented. */
|
|
69
|
+
const press = (key: string, init: KeyboardEventInit = {}) => {
|
|
70
|
+
const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...init })
|
|
71
|
+
document.activeElement!.dispatchEvent(event)
|
|
72
|
+
return event.defaultPrevented
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe('useRadioGrid', () => {
|
|
76
|
+
describe('tab stop', () => {
|
|
77
|
+
it('is the chosen position', () => {
|
|
78
|
+
const { api } = mountGrid({ initial: 7 })
|
|
79
|
+
|
|
80
|
+
expect(api().tabStop.value).toBe(7)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('is the preferred position while none is chosen', () => {
|
|
84
|
+
const { api } = mountGrid({ initial: null, preferred: () => 3 })
|
|
85
|
+
|
|
86
|
+
expect(api().tabStop.value).toBe(3)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('falls back to the first position without a usable preference', () => {
|
|
90
|
+
const { api } = mountGrid({ initial: null, preferred: () => 5, positions: [6, 0, 3] })
|
|
91
|
+
|
|
92
|
+
expect(api().tabStop.value).toBe(0)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('ignores a chosen position that holds no radio', () => {
|
|
96
|
+
const { api } = mountGrid({ initial: 4, positions: [0, 3, 6], preferred: () => 3 })
|
|
97
|
+
|
|
98
|
+
expect(api().tabStop.value).toBe(3)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('arrows', () => {
|
|
103
|
+
it('move in two dimensions, choose and focus', () => {
|
|
104
|
+
const { wrapper, onChoose } = mountGrid()
|
|
105
|
+
cell(wrapper, 4).focus()
|
|
106
|
+
|
|
107
|
+
press('ArrowUp')
|
|
108
|
+
press('ArrowRight')
|
|
109
|
+
press('ArrowDown')
|
|
110
|
+
press('ArrowLeft')
|
|
111
|
+
|
|
112
|
+
expect(onChoose.mock.calls.map(([position]) => position)).toEqual([1, 2, 5, 4])
|
|
113
|
+
expect(focused()).toBe(4)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('stop at the edge without wrapping, and still prevent the scroll', () => {
|
|
117
|
+
const { wrapper, onChoose } = mountGrid({ initial: 2 })
|
|
118
|
+
cell(wrapper, 2).focus()
|
|
119
|
+
|
|
120
|
+
expect(press('ArrowRight')).toBe(true)
|
|
121
|
+
expect(press('ArrowUp')).toBe(true)
|
|
122
|
+
|
|
123
|
+
expect(onChoose).not.toHaveBeenCalled()
|
|
124
|
+
expect(focused()).toBe(2)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('only move along the given positions', () => {
|
|
128
|
+
const { wrapper, onChoose } = mountGrid({ initial: 3, positions: [0, 3, 6] })
|
|
129
|
+
cell(wrapper, 3).focus()
|
|
130
|
+
|
|
131
|
+
press('ArrowRight')
|
|
132
|
+
press('ArrowLeft')
|
|
133
|
+
expect(onChoose).not.toHaveBeenCalled()
|
|
134
|
+
|
|
135
|
+
press('ArrowDown')
|
|
136
|
+
expect(onChoose).toHaveBeenLastCalledWith(6)
|
|
137
|
+
expect(focused()).toBe(6)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('choose the cell they stay on while nothing is chosen', () => {
|
|
141
|
+
const { wrapper, onChoose } = mountGrid({ initial: null, preferred: () => 3 })
|
|
142
|
+
cell(wrapper, 3).focus()
|
|
143
|
+
|
|
144
|
+
press('ArrowLeft')
|
|
145
|
+
|
|
146
|
+
expect(onChoose).toHaveBeenCalledWith(3)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('leave keys with a modifier to the browser', () => {
|
|
150
|
+
const { wrapper, onChoose } = mountGrid()
|
|
151
|
+
cell(wrapper, 4).focus()
|
|
152
|
+
|
|
153
|
+
for (const init of [
|
|
154
|
+
{ altKey: true },
|
|
155
|
+
{ ctrlKey: true },
|
|
156
|
+
{ metaKey: true },
|
|
157
|
+
{ shiftKey: true },
|
|
158
|
+
]) {
|
|
159
|
+
expect(press('ArrowLeft', init)).toBe(false)
|
|
160
|
+
}
|
|
161
|
+
expect(onChoose).not.toHaveBeenCalled()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('leave other keys alone', () => {
|
|
165
|
+
const { wrapper, onChoose } = mountGrid()
|
|
166
|
+
cell(wrapper, 4).focus()
|
|
167
|
+
|
|
168
|
+
expect(press('a')).toBe(false)
|
|
169
|
+
expect(onChoose).not.toHaveBeenCalled()
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
describe('Home and End', () => {
|
|
174
|
+
it("jump to the first and last radio of the grid with 'grid'", () => {
|
|
175
|
+
const { wrapper, onChoose } = mountGrid({ homeEnd: 'grid' })
|
|
176
|
+
cell(wrapper, 4).focus()
|
|
177
|
+
|
|
178
|
+
press('Home')
|
|
179
|
+
expect(focused()).toBe(0)
|
|
180
|
+
press('End')
|
|
181
|
+
expect(focused()).toBe(8)
|
|
182
|
+
expect(onChoose.mock.calls.map(([position]) => position)).toEqual([0, 8])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("jump within the current row with 'row'", () => {
|
|
186
|
+
const { wrapper } = mountGrid({ homeEnd: 'row' })
|
|
187
|
+
cell(wrapper, 4).focus()
|
|
188
|
+
|
|
189
|
+
press('Home')
|
|
190
|
+
expect(focused()).toBe(3)
|
|
191
|
+
press('End')
|
|
192
|
+
expect(focused()).toBe(5)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("default to 'grid'", () => {
|
|
196
|
+
const { wrapper } = mountGrid()
|
|
197
|
+
cell(wrapper, 4).focus()
|
|
198
|
+
|
|
199
|
+
press('End')
|
|
200
|
+
expect(focused()).toBe(8)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('focusTabStop', () => {
|
|
205
|
+
it('focuses the radio that holds the tab stop', () => {
|
|
206
|
+
const { wrapper, api } = mountGrid({ initial: 7 })
|
|
207
|
+
|
|
208
|
+
api().focusTabStop()
|
|
209
|
+
|
|
210
|
+
expect(document.activeElement).toBe(cell(wrapper, 7))
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('leaving', () => {
|
|
215
|
+
it('signals the end of a keyboard run when the focus leaves', () => {
|
|
216
|
+
const { wrapper, onLeave } = mountGrid()
|
|
217
|
+
cell(wrapper, 4).focus()
|
|
218
|
+
|
|
219
|
+
press('ArrowUp')
|
|
220
|
+
press('ArrowUp')
|
|
221
|
+
cell(wrapper, 0).focus()
|
|
222
|
+
expect(onLeave).not.toHaveBeenCalled()
|
|
223
|
+
|
|
224
|
+
;(wrapper.get('[data-outside]').element as HTMLElement).focus()
|
|
225
|
+
expect(onLeave).toHaveBeenCalledOnce()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('stays silent when the focus only passes through, without a key', () => {
|
|
229
|
+
const { wrapper, onLeave } = mountGrid()
|
|
230
|
+
|
|
231
|
+
cell(wrapper, 4).focus()
|
|
232
|
+
cell(wrapper, 5).focus()
|
|
233
|
+
;(wrapper.get('[data-outside]').element as HTMLElement).focus()
|
|
234
|
+
|
|
235
|
+
expect(onLeave).not.toHaveBeenCalled()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('signals one run once, not again on a later visit without a key', () => {
|
|
239
|
+
const { wrapper, onLeave } = mountGrid()
|
|
240
|
+
cell(wrapper, 4).focus()
|
|
241
|
+
|
|
242
|
+
press('ArrowRight')
|
|
243
|
+
;(wrapper.get('[data-outside]').element as HTMLElement).focus()
|
|
244
|
+
cell(wrapper, 4).focus()
|
|
245
|
+
;(wrapper.get('[data-outside]').element as HTMLElement).focus()
|
|
246
|
+
|
|
247
|
+
expect(onLeave).toHaveBeenCalledOnce()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('stays silent on leaving after reset closed the run', () => {
|
|
251
|
+
const { wrapper, onLeave, api } = mountGrid()
|
|
252
|
+
cell(wrapper, 4).focus()
|
|
253
|
+
|
|
254
|
+
press('ArrowRight')
|
|
255
|
+
api().reset()
|
|
256
|
+
;(wrapper.get('[data-outside]').element as HTMLElement).focus()
|
|
257
|
+
|
|
258
|
+
expect(onLeave).not.toHaveBeenCalled()
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import EditorAlignItems from './EditorAlignItems.vue'
|
|
4
|
+
import type { EditorAlignItemsValue } from './EditorAlignItems.vue'
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
title: 'Components/Editor Components/Molecules/Editor Align Items',
|
|
8
|
+
component: EditorAlignItems,
|
|
9
|
+
decorators: [() => ({ template: '<div class="w-100 mx-auto"><story /></div>' })],
|
|
10
|
+
argTypes: {
|
|
11
|
+
label: { control: 'text' },
|
|
12
|
+
},
|
|
13
|
+
} satisfies Meta<typeof EditorAlignItems>
|
|
14
|
+
|
|
15
|
+
type Story = StoryObj<typeof EditorAlignItems>
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
render: (args) => ({
|
|
19
|
+
components: { EditorAlignItems },
|
|
20
|
+
setup() {
|
|
21
|
+
const align = ref<EditorAlignItemsValue>('items-start')
|
|
22
|
+
return { args, align }
|
|
23
|
+
},
|
|
24
|
+
// The value is shown as-is: it is the Tailwind class the editor applies.
|
|
25
|
+
template: `
|
|
26
|
+
<div>
|
|
27
|
+
<EditorAlignItems v-bind="args" v-model="align" />
|
|
28
|
+
<p class="small-normal text-gray-700">{{ align }}</p>
|
|
29
|
+
</div>
|
|
30
|
+
`,
|
|
31
|
+
}),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const CustomMessages: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
label: 'Align items',
|
|
37
|
+
messages: { start: 'Start', center: 'Center', end: 'End', stretch: 'Stretch' },
|
|
38
|
+
},
|
|
39
|
+
render: (args) => ({
|
|
40
|
+
components: { EditorAlignItems },
|
|
41
|
+
setup() {
|
|
42
|
+
const align = ref<EditorAlignItemsValue>('items-start')
|
|
43
|
+
return { args, align }
|
|
44
|
+
},
|
|
45
|
+
template: `<EditorAlignItems v-bind="args" v-model="align" />`,
|
|
46
|
+
}),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A subset in canonical order: top, middle and bottom, as the vertical divider
|
|
51
|
+
* in a row group uses it. In a row the cross axis is vertical, so the
|
|
52
|
+
* `objects-align-top / center-vertical / bottom` icons say what they do.
|
|
53
|
+
*/
|
|
54
|
+
export const OptionsSubset: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
label: 'Uitlijning',
|
|
57
|
+
options: ['items-start', 'items-center', 'items-end'],
|
|
58
|
+
messages: { start: 'Boven', center: 'Midden', end: 'Onder' },
|
|
59
|
+
},
|
|
60
|
+
render: (args) => ({
|
|
61
|
+
components: { EditorAlignItems },
|
|
62
|
+
setup() {
|
|
63
|
+
const align = ref<EditorAlignItemsValue>('items-center')
|
|
64
|
+
return { args, align }
|
|
65
|
+
},
|
|
66
|
+
template: `
|
|
67
|
+
<div>
|
|
68
|
+
<EditorAlignItems v-bind="args" v-model="align" />
|
|
69
|
+
<p class="small-normal text-gray-700">{{ align }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
`,
|
|
72
|
+
}),
|
|
73
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { mount, type VueWrapper } from '@vue/test-utils'
|
|
3
|
+
import EditorAlignItems, { EDITOR_ALIGN_ITEMS_OPTIONS } from './EditorAlignItems.vue'
|
|
4
|
+
|
|
5
|
+
// ToggleGroup awaits document.fonts.ready on mount, which jsdom does not ship.
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
Object.defineProperty(document, 'fonts', {
|
|
8
|
+
value: { ready: Promise.resolve() },
|
|
9
|
+
configurable: true,
|
|
10
|
+
})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The options are icon-only, so their copy lives on the button as the
|
|
15
|
+
* accessible name rather than as visible text.
|
|
16
|
+
*/
|
|
17
|
+
const optionNames = (wrapper: VueWrapper) =>
|
|
18
|
+
wrapper.findAll('button').map((button) => button.attributes('aria-label'))
|
|
19
|
+
|
|
20
|
+
describe('EditorAlignItems', () => {
|
|
21
|
+
// The options sit in a ToggleGroup toolbar; its name is the block label, so
|
|
22
|
+
// a screen reader announces which setting the pressed option belongs to.
|
|
23
|
+
it('names the options toolbar with the block label', async () => {
|
|
24
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
25
|
+
expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Uitlijning items')
|
|
26
|
+
|
|
27
|
+
await wrapper.setProps({ label: 'Custom' })
|
|
28
|
+
expect(wrapper.get('[role="toolbar"]').attributes('aria-label')).toBe('Custom')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// No assertions on typography utilities: copy is found by structure or by
|
|
32
|
+
// accessible name, so restyling the block or the toggle group cannot turn
|
|
33
|
+
// these red.
|
|
34
|
+
it('renders the Dutch default label', () => {
|
|
35
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
36
|
+
|
|
37
|
+
expect(wrapper.find('p').text()).toBe('Uitlijning items')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('renders a button for each of the four options', () => {
|
|
41
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
42
|
+
|
|
43
|
+
expect(wrapper.findAll('button')).toHaveLength(4)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('names the options with the Dutch defaults', () => {
|
|
47
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
48
|
+
|
|
49
|
+
expect(optionNames(wrapper)).toEqual(['Begin', 'Midden', 'Einde', 'Uitrekken'])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('renders a custom label', () => {
|
|
53
|
+
const wrapper = mount(EditorAlignItems, {
|
|
54
|
+
props: { modelValue: 'items-start', label: 'Align items' },
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
expect(wrapper.find('p').text()).toBe('Align items')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('names the options with custom messages', () => {
|
|
61
|
+
const wrapper = mount(EditorAlignItems, {
|
|
62
|
+
props: {
|
|
63
|
+
modelValue: 'items-start',
|
|
64
|
+
messages: { start: 'Start', center: 'Center', end: 'End', stretch: 'Stretch' },
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(optionNames(wrapper)).toEqual(['Start', 'Center', 'End', 'Stretch'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('keeps the untouched defaults when only one message is overridden', () => {
|
|
72
|
+
const wrapper = mount(EditorAlignItems, {
|
|
73
|
+
props: { modelValue: 'items-start', messages: { stretch: 'Stretch' } },
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(optionNames(wrapper)).toEqual(['Begin', 'Midden', 'Einde', 'Stretch'])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('emits update:modelValue with the Tailwind class of the clicked option', async () => {
|
|
80
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
81
|
+
|
|
82
|
+
await wrapper.findAll('button')[3].trigger('click')
|
|
83
|
+
|
|
84
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['items-stretch']])
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('renders only icons, no visible option text', () => {
|
|
88
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
89
|
+
|
|
90
|
+
// Each icon is rendered twice: once per toggle group layer.
|
|
91
|
+
expect(wrapper.findAll('i')).toHaveLength(8)
|
|
92
|
+
wrapper.findAll('button').forEach((button) => expect(button.text()).toBe(''))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('renders no tooltip: the copy is the accessible name, not a hover overlay', () => {
|
|
96
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
97
|
+
|
|
98
|
+
// ToggleGroup only mounts NexxtTooltip -- and with it reka-ui's
|
|
99
|
+
// TooltipProvider and the trigger's data attribute -- for options that
|
|
100
|
+
// carry a `tooltip`.
|
|
101
|
+
expect(wrapper.findAllComponents({ name: 'TooltipProvider' })).toHaveLength(0)
|
|
102
|
+
expect(wrapper.find('[data-grace-area-trigger]').exists()).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('offers all four options when `options` is not given', () => {
|
|
106
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
107
|
+
|
|
108
|
+
expect(optionNames(wrapper)).toEqual(['Begin', 'Midden', 'Einde', 'Uitrekken'])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('offers only the options passed in `options`', () => {
|
|
112
|
+
const wrapper = mount(EditorAlignItems, {
|
|
113
|
+
props: { modelValue: 'items-center', options: ['items-start', 'items-center', 'items-end'] },
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(optionNames(wrapper)).toEqual(['Begin', 'Midden', 'Einde'])
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('keeps the canonical order and drops duplicates, whatever `options` says', () => {
|
|
120
|
+
const wrapper = mount(EditorAlignItems, {
|
|
121
|
+
props: {
|
|
122
|
+
modelValue: 'items-end',
|
|
123
|
+
options: ['items-stretch', 'items-end', 'items-start', 'items-end'],
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(optionNames(wrapper)).toEqual(['Begin', 'Einde', 'Uitrekken'])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('ignores values that are not on the list, like JustifyContent', () => {
|
|
131
|
+
const unknown = ['items-baseline', 'items-center'] as unknown as ['items-center']
|
|
132
|
+
const wrapper = mount(EditorAlignItems, {
|
|
133
|
+
props: { modelValue: 'items-center', options: unknown },
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
expect(optionNames(wrapper)).toEqual(['Midden'])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('renders no options for an empty subset, like JustifyContent', () => {
|
|
140
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-center', options: [] } })
|
|
141
|
+
|
|
142
|
+
expect(wrapper.findAll('button')).toHaveLength(0)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('keeps the top, middle and bottom icons in a subset', () => {
|
|
146
|
+
const wrapper = mount(EditorAlignItems, {
|
|
147
|
+
props: {
|
|
148
|
+
modelValue: 'items-center',
|
|
149
|
+
options: ['items-start', 'items-center', 'items-end'],
|
|
150
|
+
messages: { start: 'Boven', center: 'Midden', end: 'Onder' },
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
const icons = wrapper.findAll('button i').map((icon) => icon.classes().join(' '))
|
|
154
|
+
|
|
155
|
+
expect(optionNames(wrapper)).toEqual(['Boven', 'Midden', 'Onder'])
|
|
156
|
+
expect(icons[0]).toContain('objects-align-top')
|
|
157
|
+
expect(icons[1]).toContain('objects-align-center-vertical')
|
|
158
|
+
expect(icons[2]).toContain('objects-align-bottom')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('emits the class of a clicked option from a subset', async () => {
|
|
162
|
+
const wrapper = mount(EditorAlignItems, {
|
|
163
|
+
props: { modelValue: 'items-start', options: ['items-start', 'items-end'] },
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await wrapper.findAll('button')[1].trigger('click')
|
|
167
|
+
|
|
168
|
+
expect(wrapper.emitted('update:modelValue')).toEqual([['items-end']])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('is always visible, so it renders no switch', () => {
|
|
172
|
+
const wrapper = mount(EditorAlignItems, { props: { modelValue: 'items-start' } })
|
|
173
|
+
|
|
174
|
+
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(false)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// The consumer derives its class matchers from this list, so the list is a
|
|
179
|
+
// contract: any change here changes what the app recognises.
|
|
180
|
+
describe('EDITOR_ALIGN_ITEMS_OPTIONS', () => {
|
|
181
|
+
it('offers exactly the agreed options', () => {
|
|
182
|
+
expect(EDITOR_ALIGN_ITEMS_OPTIONS).toEqual([
|
|
183
|
+
{ token: 'start' },
|
|
184
|
+
{ token: 'center' },
|
|
185
|
+
{ token: 'end' },
|
|
186
|
+
{ token: 'stretch' },
|
|
187
|
+
])
|
|
188
|
+
})
|
|
189
|
+
})
|