@nyaruka/temba-components 0.122.0 → 0.124.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/.github/copilot-instructions.md +181 -0
- package/.github/workflows/build.yml +3 -3
- package/.github/workflows/cla.yml +6 -6
- package/.github/workflows/copilot-setup-steps.yml +86 -0
- package/CHANGELOG.md +44 -0
- package/demo/drag-drop-demo.html +141 -0
- package/demo/index.html +57 -0
- package/demo/test-drag-drop.html +94 -0
- package/dist/locales/es.js +1 -0
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +1 -0
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +1 -0
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +366 -247
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +81 -14
- package/out-tsc/src/chart/TembaChart.js.map +1 -1
- package/out-tsc/src/fields/FieldManager.js +27 -34
- package/out-tsc/src/fields/FieldManager.js.map +1 -1
- package/out-tsc/src/list/RunList.js +13 -8
- package/out-tsc/src/list/RunList.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +257 -60
- package/out-tsc/src/list/SortableList.js.map +1 -1
- package/out-tsc/src/locales/es.js +1 -0
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +1 -0
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/pt.js +1 -0
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/omnibox/Omnibox.js +1 -1
- package/out-tsc/src/omnibox/Omnibox.js.map +1 -1
- package/out-tsc/src/options/Options.js +36 -13
- package/out-tsc/src/options/Options.js.map +1 -1
- package/out-tsc/src/select/Select.js +226 -43
- package/out-tsc/src/select/Select.js.map +1 -1
- package/out-tsc/src/store/AppState.js +3 -3
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/utils/index.js +6 -1
- package/out-tsc/src/utils/index.js.map +1 -1
- package/out-tsc/src/vectoricon/VectorIcon.js +2 -1
- package/out-tsc/src/vectoricon/VectorIcon.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +5 -2
- package/out-tsc/src/webchat/WebChat.js.map +1 -1
- package/out-tsc/temba-modules.js +0 -2
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-appstate-language.test.js +176 -0
- package/out-tsc/test/temba-appstate-language.test.js.map +1 -0
- package/out-tsc/test/temba-chart.test.js +125 -0
- package/out-tsc/test/temba-chart.test.js.map +1 -1
- package/out-tsc/test/temba-dropdown.test.js +317 -0
- package/out-tsc/test/temba-dropdown.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor-node.test.js +273 -0
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor.test.js +244 -0
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -0
- package/out-tsc/test/temba-flow-plumber.test.js +145 -0
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -0
- package/out-tsc/test/temba-flow-render.test.js +171 -0
- package/out-tsc/test/temba-flow-render.test.js.map +1 -0
- package/out-tsc/test/temba-omnibox.test.js +2 -3
- package/out-tsc/test/temba-omnibox.test.js.map +1 -1
- package/out-tsc/test/temba-run-list.test.js +588 -0
- package/out-tsc/test/temba-run-list.test.js.map +1 -0
- package/out-tsc/test/temba-select.test.js +149 -52
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/out-tsc/test/temba-sortable-list.test.js +91 -15
- package/out-tsc/test/temba-sortable-list.test.js.map +1 -1
- package/out-tsc/test/temba-toast.test.js +299 -0
- package/out-tsc/test/temba-toast.test.js.map +1 -0
- package/out-tsc/test/temba-utils-index.test.js +1178 -0
- package/out-tsc/test/temba-utils-index.test.js.map +1 -0
- package/out-tsc/test/temba-webchat-lightbox-fix.test.js +42 -0
- package/out-tsc/test/temba-webchat-lightbox-fix.test.js.map +1 -0
- package/out-tsc/test/temba-webchat.test.js +816 -0
- package/out-tsc/test/temba-webchat.test.js.map +1 -0
- package/out-tsc/test/utils.test.js +33 -1
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +6 -8
- package/screenshots/truth/alert/error.png +0 -0
- package/screenshots/truth/alert/info.png +0 -0
- package/screenshots/truth/alert/warning.png +0 -0
- package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
- package/screenshots/truth/checkbox/checked.png +0 -0
- package/screenshots/truth/checkbox/default.png +0 -0
- package/screenshots/truth/colorpicker/default.png +0 -0
- package/screenshots/truth/colorpicker/focused.png +0 -0
- package/screenshots/truth/colorpicker/initialized.png +0 -0
- package/screenshots/truth/colorpicker/selected.png +0 -0
- package/screenshots/truth/compose/attachments-tab.png +0 -0
- package/screenshots/truth/compose/attachments-with-files-focused.png +0 -0
- package/screenshots/truth/compose/attachments-with-files.png +0 -0
- package/screenshots/truth/compose/intial-text.png +0 -0
- package/screenshots/truth/compose/no-counter.png +0 -0
- package/screenshots/truth/compose/wraps-text-and-spaces.png +0 -0
- package/screenshots/truth/compose/wraps-text-and-url.png +0 -0
- package/screenshots/truth/compose/wraps-text-no-spaces.png +0 -0
- package/screenshots/truth/contacts/badges.png +0 -0
- package/screenshots/truth/contacts/chat-failure.png +0 -0
- package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
- package/screenshots/truth/content-menu/button-no-items.png +0 -0
- package/screenshots/truth/content-menu/items-and-buttons.png +0 -0
- package/screenshots/truth/counter/summary.png +0 -0
- package/screenshots/truth/counter/text.png +0 -0
- package/screenshots/truth/counter/unicode-variables.png +0 -0
- package/screenshots/truth/counter/unicode.png +0 -0
- package/screenshots/truth/counter/variable.png +0 -0
- package/screenshots/truth/date/date-inline.png +0 -0
- package/screenshots/truth/date/date.png +0 -0
- package/screenshots/truth/date/datetime.png +0 -0
- package/screenshots/truth/date/duration.png +0 -0
- package/screenshots/truth/date/timedate.png +0 -0
- package/screenshots/truth/datepicker/date-truncated-time.png +0 -0
- package/screenshots/truth/datepicker/date.png +0 -0
- package/screenshots/truth/datepicker/initial-timezone.png +0 -0
- package/screenshots/truth/datepicker/updated-keyboard-date.png +0 -0
- package/screenshots/truth/dialog/focused.png +0 -0
- package/screenshots/truth/dropdown/after-blur.png +0 -0
- package/screenshots/truth/dropdown/bottom-edge-collision.png +0 -0
- package/screenshots/truth/dropdown/custom-arrow-size.png +0 -0
- package/screenshots/truth/dropdown/default.png +0 -0
- package/screenshots/truth/dropdown/narrow-toggle.png +0 -0
- package/screenshots/truth/dropdown/no-mask.png +0 -0
- package/screenshots/truth/dropdown/opened.png +0 -0
- package/screenshots/truth/dropdown/positioned.png +0 -0
- package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
- package/screenshots/truth/dropdown/with-mask.png +0 -0
- package/screenshots/truth/flow/editor-basic.png +0 -0
- package/screenshots/truth/label/custom.png +0 -0
- package/screenshots/truth/label/danger.png +0 -0
- package/screenshots/truth/label/dark.png +0 -0
- package/screenshots/truth/label/default-icon.png +0 -0
- package/screenshots/truth/label/no-icon.png +0 -0
- package/screenshots/truth/label/primary.png +0 -0
- package/screenshots/truth/label/secondary.png +0 -0
- package/screenshots/truth/label/shadow.png +0 -0
- package/screenshots/truth/label/tertiary.png +0 -0
- package/screenshots/truth/lightbox/img-zoomed.png +0 -0
- package/screenshots/truth/list/fields-dragging.png +0 -0
- package/screenshots/truth/list/fields-filtered.png +0 -0
- package/screenshots/truth/list/fields-hovered.png +0 -0
- package/screenshots/truth/list/fields.png +0 -0
- package/screenshots/truth/list/items-selected.png +0 -0
- package/screenshots/truth/list/items-updated.png +0 -0
- package/screenshots/truth/list/items.png +0 -0
- package/screenshots/truth/list/sortable-dragging.png +0 -0
- package/screenshots/truth/list/sortable-dropped.png +0 -0
- package/screenshots/truth/list/sortable.png +0 -0
- package/screenshots/truth/menu/menu-focused-with items.png +0 -0
- package/screenshots/truth/menu/menu-refresh-1.png +0 -0
- package/screenshots/truth/menu/menu-refresh-2.png +0 -0
- package/screenshots/truth/menu/menu-root.png +0 -0
- package/screenshots/truth/menu/menu-submenu.png +0 -0
- package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
- package/screenshots/truth/menu/menu-tasks.png +0 -0
- package/screenshots/truth/modax/form.png +0 -0
- package/screenshots/truth/modax/simple.png +0 -0
- package/screenshots/truth/omnibox/selected.png +0 -0
- package/screenshots/truth/options/block.png +0 -0
- package/screenshots/truth/run-list/basic.png +0 -0
- package/screenshots/truth/select/disabled-multi-selection.png +0 -0
- package/screenshots/truth/select/disabled-selection.png +0 -0
- package/screenshots/truth/select/disabled.png +0 -0
- package/screenshots/truth/select/embedded.png +0 -0
- package/screenshots/truth/select/empty-options.png +0 -0
- package/screenshots/truth/select/expression-selected.png +0 -0
- package/screenshots/truth/select/expressions.png +0 -0
- package/screenshots/truth/select/functions.png +0 -0
- package/screenshots/truth/select/local-options.png +0 -0
- package/screenshots/truth/select/multi-reorder-final.png +0 -0
- package/screenshots/truth/select/multi-reorder-initial.png +0 -0
- package/screenshots/truth/select/multi-with-endpoint.png +0 -0
- package/screenshots/truth/select/multiple-initial-values.png +0 -0
- package/screenshots/truth/select/remote-options.png +0 -0
- package/screenshots/truth/select/search-enabled.png +0 -0
- package/screenshots/truth/select/search-multi-no-matches.png +0 -0
- package/screenshots/truth/select/search-selected-focus.png +0 -0
- package/screenshots/truth/select/search-selected.png +0 -0
- package/screenshots/truth/select/search-with-selected.png +0 -0
- package/screenshots/truth/select/searching.png +0 -0
- package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
- package/screenshots/truth/select/selected-multi.png +0 -0
- package/screenshots/truth/select/selected-single.png +0 -0
- package/screenshots/truth/select/selection-clearable.png +0 -0
- package/screenshots/truth/select/static-initial-value.png +0 -0
- package/screenshots/truth/select/static-initial-via-selected.png +0 -0
- package/screenshots/truth/select/truncated-selection.png +0 -0
- package/screenshots/truth/select/with-placeholder.png +0 -0
- package/screenshots/truth/select/without-placeholder.png +0 -0
- package/screenshots/truth/slider/custom-min-custom-max-valid-value.png +0 -0
- package/screenshots/truth/slider/custom-min-default-max-no-value.png +0 -0
- package/screenshots/truth/slider/default-min-custom-max-no-value.png +0 -0
- package/screenshots/truth/slider/default-min-default-max-invalid-value.png +0 -0
- package/screenshots/truth/slider/default-min-default-max-valid-value.png +0 -0
- package/screenshots/truth/slider/update-slider-on-value-change.png +0 -0
- package/screenshots/truth/templates/default.png +0 -0
- package/screenshots/truth/templates/unapproved.png +0 -0
- package/screenshots/truth/textinput/input-disabled.png +0 -0
- package/screenshots/truth/textinput/input-focused.png +0 -0
- package/screenshots/truth/textinput/input-form.png +0 -0
- package/screenshots/truth/textinput/input-inserted.png +0 -0
- package/screenshots/truth/textinput/input-placeholder.png +0 -0
- package/screenshots/truth/textinput/input-updated.png +0 -0
- package/screenshots/truth/textinput/input.png +0 -0
- package/screenshots/truth/textinput/textarea-focused.png +0 -0
- package/screenshots/truth/textinput/textarea.png +0 -0
- package/screenshots/truth/tip/bottom.png +0 -0
- package/screenshots/truth/tip/left.png +0 -0
- package/screenshots/truth/tip/right.png +0 -0
- package/screenshots/truth/tip/top.png +0 -0
- package/screenshots/truth/webchat/closed-widget.png +0 -0
- package/screenshots/truth/webchat/connected-state.png +0 -0
- package/screenshots/truth/webchat/connecting-state.png +0 -0
- package/screenshots/truth/webchat/disconnected-state.png +0 -0
- package/screenshots/truth/webchat/opened-widget.png +0 -0
- package/src/chart/TembaChart.ts +86 -15
- package/src/fields/FieldManager.ts +30 -38
- package/src/list/RunList.ts +11 -8
- package/src/list/SortableList.ts +291 -67
- package/src/locales/es.ts +1 -0
- package/src/locales/fr.ts +1 -0
- package/src/locales/pt.ts +1 -0
- package/src/omnibox/Omnibox.ts +1 -1
- package/src/options/Options.ts +38 -13
- package/src/select/Select.ts +245 -47
- package/src/store/AppState.ts +3 -3
- package/src/utils/index.ts +17 -5
- package/src/vectoricon/VectorIcon.ts +2 -1
- package/src/webchat/WebChat.ts +5 -2
- package/temba-modules.ts +0 -2
- package/test/temba-appstate-language.test.ts +218 -0
- package/test/temba-chart.test.ts +161 -1
- package/test/temba-dropdown.test.ts +444 -0
- package/test/temba-flow-editor-node.test.ts +344 -0
- package/test/temba-flow-editor.test.ts +301 -0
- package/test/temba-flow-plumber.test.ts +189 -0
- package/test/temba-flow-render.test.ts +220 -0
- package/test/temba-omnibox.test.ts +2 -3
- package/test/temba-run-list.test.ts +774 -0
- package/test/temba-select.test.ts +206 -78
- package/test/temba-sortable-list.test.ts +108 -15
- package/test/temba-toast.test.ts +386 -0
- package/test/temba-utils-index.test.ts +1547 -0
- package/test/temba-webchat-lightbox-fix.test.ts +57 -0
- package/test/temba-webchat.test.ts +1095 -0
- package/test/utils.test.ts +56 -2
- package/test-assets/list/flow-results.json +17 -0
- package/test-assets/list/runs.json +126 -0
- package/test-assets/style.css +23 -0
- package/web-test-runner.config.mjs +33 -7
- package/xliff/es.xlf +3 -0
- package/xliff/fr.xlf +3 -0
- package/xliff/pt.xlf +3 -0
- package/out-tsc/src/outboxmonitor/OutboxMonitor.js +0 -136
- package/out-tsc/src/outboxmonitor/OutboxMonitor.js.map +0 -1
- package/src/outboxmonitor/OutboxMonitor.ts +0 -148
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { assert, expect } from '@open-wc/testing';
|
|
2
|
+
import { Dropdown } from '../src/dropdown/Dropdown';
|
|
3
|
+
import { assertScreenshot, getClip, getComponent } from './utils.test';
|
|
4
|
+
|
|
5
|
+
const TAG = 'temba-dropdown';
|
|
6
|
+
|
|
7
|
+
// Helper function to wait for stable rendering
|
|
8
|
+
const waitForStableRender = async (dropdown: Dropdown, timeoutMs = 200) => {
|
|
9
|
+
await dropdown.updateComplete;
|
|
10
|
+
// Double wait to ensure any async positioning is complete
|
|
11
|
+
await new Promise((resolve) => setTimeout(resolve, timeoutMs));
|
|
12
|
+
await dropdown.updateComplete;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Helper function to get expanded clip that includes dropdown content when open
|
|
16
|
+
const getDropdownClip = (dropdown: Dropdown) => {
|
|
17
|
+
if (!dropdown.open) {
|
|
18
|
+
// If closed, use regular clipping
|
|
19
|
+
return getClip(dropdown);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// For open dropdowns, include the positioned dropdown content
|
|
23
|
+
const dropdownDiv = dropdown.shadowRoot.querySelector(
|
|
24
|
+
'.dropdown'
|
|
25
|
+
) as HTMLDivElement;
|
|
26
|
+
const dropdownBounds = dropdownDiv.getBoundingClientRect();
|
|
27
|
+
const componentBounds = dropdown.getBoundingClientRect();
|
|
28
|
+
|
|
29
|
+
// If dropdown content has no meaningful size, fall back to regular clip
|
|
30
|
+
if (dropdownBounds.width < 10 || dropdownBounds.height < 10) {
|
|
31
|
+
return getClip(dropdown);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create a clipping region that includes both the component and the dropdown content
|
|
35
|
+
const minX = Math.min(componentBounds.x, dropdownBounds.x);
|
|
36
|
+
const minY = Math.min(componentBounds.y, dropdownBounds.y);
|
|
37
|
+
const maxX = Math.max(componentBounds.right, dropdownBounds.right);
|
|
38
|
+
const maxY = Math.max(componentBounds.bottom, dropdownBounds.bottom);
|
|
39
|
+
|
|
40
|
+
// Clamp to reasonable bounds to avoid excessive screenshot sizes
|
|
41
|
+
const x = Math.max(0, minX - 10);
|
|
42
|
+
const y = Math.max(0, minY - 10);
|
|
43
|
+
const width = Math.min(1000, maxX - minX + 20);
|
|
44
|
+
const height = Math.min(800, maxY - minY + 20);
|
|
45
|
+
|
|
46
|
+
return { x, y, width, height };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getDropdown = async (
|
|
50
|
+
attrs: {
|
|
51
|
+
open?: boolean;
|
|
52
|
+
dormant?: boolean;
|
|
53
|
+
arrowSize?: number;
|
|
54
|
+
margin?: number;
|
|
55
|
+
mask?: boolean;
|
|
56
|
+
} = {},
|
|
57
|
+
toggleSlot = '<button slot="toggle">Toggle</button>',
|
|
58
|
+
dropdownSlot = '<div slot="dropdown">Dropdown content</div>'
|
|
59
|
+
) => {
|
|
60
|
+
const dropdown = (await getComponent(
|
|
61
|
+
TAG,
|
|
62
|
+
attrs,
|
|
63
|
+
`${toggleSlot}${dropdownSlot}`,
|
|
64
|
+
400,
|
|
65
|
+
300
|
|
66
|
+
)) as Dropdown;
|
|
67
|
+
await dropdown.updateComplete;
|
|
68
|
+
return dropdown;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe(TAG, () => {
|
|
72
|
+
it('can be created', async () => {
|
|
73
|
+
const dropdown = await getDropdown();
|
|
74
|
+
assert.instanceOf(dropdown, Dropdown);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('has correct default properties', async () => {
|
|
78
|
+
const dropdown = await getDropdown();
|
|
79
|
+
|
|
80
|
+
// Test expected values first
|
|
81
|
+
expect(dropdown.open).to.equal(false);
|
|
82
|
+
expect(dropdown.dormant).to.equal(true);
|
|
83
|
+
expect(dropdown.arrowSize).to.equal(8);
|
|
84
|
+
expect(dropdown.margin).to.equal(10);
|
|
85
|
+
expect(dropdown.mask).to.equal(false);
|
|
86
|
+
// Position calculation happens automatically, so styles won't be empty
|
|
87
|
+
expect(typeof dropdown.dropdownStyle).to.equal('object');
|
|
88
|
+
expect(typeof dropdown.arrowStyle).to.equal('object');
|
|
89
|
+
|
|
90
|
+
// Then screenshot
|
|
91
|
+
await assertScreenshot('dropdown/default', getClip(dropdown));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders with mask when enabled', async () => {
|
|
95
|
+
const dropdown = await getDropdown({ mask: true });
|
|
96
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
97
|
+
|
|
98
|
+
// Open the dropdown properly by clicking
|
|
99
|
+
toggle.click();
|
|
100
|
+
await waitForStableRender(dropdown);
|
|
101
|
+
|
|
102
|
+
// Test expected values first
|
|
103
|
+
expect(dropdown.mask).to.equal(true);
|
|
104
|
+
expect(dropdown.open).to.equal(true);
|
|
105
|
+
expect(dropdown.dormant).to.equal(false);
|
|
106
|
+
|
|
107
|
+
// Then screenshot
|
|
108
|
+
await assertScreenshot('dropdown/with-mask', getDropdownClip(dropdown));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('handles toggle click and opens dropdown', async () => {
|
|
112
|
+
const dropdown = await getDropdown();
|
|
113
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
114
|
+
|
|
115
|
+
// Verify initial state
|
|
116
|
+
expect(dropdown.open).to.equal(false);
|
|
117
|
+
expect(dropdown.dormant).to.equal(true);
|
|
118
|
+
|
|
119
|
+
// Click the toggle
|
|
120
|
+
toggle.click();
|
|
121
|
+
await waitForStableRender(dropdown);
|
|
122
|
+
|
|
123
|
+
// Verify dropdown opened
|
|
124
|
+
expect(dropdown.open).to.equal(true);
|
|
125
|
+
expect(dropdown.dormant).to.equal(false);
|
|
126
|
+
|
|
127
|
+
// Screenshot the opened state with expanded clip
|
|
128
|
+
await assertScreenshot('dropdown/opened', getDropdownClip(dropdown));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('handles custom arrow size', async () => {
|
|
132
|
+
const dropdown = await getDropdown({ arrowSize: 12 });
|
|
133
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
134
|
+
|
|
135
|
+
// Open the dropdown properly by clicking
|
|
136
|
+
toggle.click();
|
|
137
|
+
await waitForStableRender(dropdown);
|
|
138
|
+
|
|
139
|
+
// Test expected values first
|
|
140
|
+
expect(dropdown.arrowSize).to.equal(12);
|
|
141
|
+
expect(dropdown.open).to.equal(true);
|
|
142
|
+
expect(dropdown.dormant).to.equal(false);
|
|
143
|
+
|
|
144
|
+
// Then screenshot
|
|
145
|
+
await assertScreenshot(
|
|
146
|
+
'dropdown/custom-arrow-size',
|
|
147
|
+
getDropdownClip(dropdown)
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('calculates position correctly', async () => {
|
|
152
|
+
const dropdown = await getDropdown();
|
|
153
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
154
|
+
|
|
155
|
+
// Open the dropdown properly by clicking
|
|
156
|
+
toggle.click();
|
|
157
|
+
await dropdown.updateComplete;
|
|
158
|
+
|
|
159
|
+
// Trigger position calculation
|
|
160
|
+
dropdown.calculatePosition();
|
|
161
|
+
await waitForStableRender(dropdown);
|
|
162
|
+
|
|
163
|
+
// Verify position styles were calculated
|
|
164
|
+
expect(Object.keys(dropdown.dropdownStyle).length).to.be.greaterThan(0);
|
|
165
|
+
expect(Object.keys(dropdown.arrowStyle).length).to.be.greaterThan(0);
|
|
166
|
+
expect(dropdown.open).to.equal(true);
|
|
167
|
+
expect(dropdown.dormant).to.equal(false);
|
|
168
|
+
|
|
169
|
+
// Screenshot positioned dropdown
|
|
170
|
+
await assertScreenshot('dropdown/positioned', getDropdownClip(dropdown));
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('handles blur events to close dropdown', async () => {
|
|
174
|
+
const dropdown = await getDropdown();
|
|
175
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
176
|
+
|
|
177
|
+
// Open the dropdown first
|
|
178
|
+
toggle.click();
|
|
179
|
+
await dropdown.updateComplete;
|
|
180
|
+
expect(dropdown.open).to.equal(true);
|
|
181
|
+
|
|
182
|
+
// Simulate blur event
|
|
183
|
+
const dropdownDiv = dropdown.shadowRoot.querySelector(
|
|
184
|
+
'.dropdown'
|
|
185
|
+
) as HTMLDivElement;
|
|
186
|
+
const blurEvent = new FocusEvent('blur', {
|
|
187
|
+
bubbles: true,
|
|
188
|
+
relatedTarget: document.body
|
|
189
|
+
});
|
|
190
|
+
dropdownDiv.dispatchEvent(blurEvent);
|
|
191
|
+
|
|
192
|
+
// Check that dropdown is closed after a short delay
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
194
|
+
expect(dropdown.open).to.equal(false);
|
|
195
|
+
|
|
196
|
+
// Screenshot closed state
|
|
197
|
+
await assertScreenshot('dropdown/after-blur', getClip(dropdown));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('handles blur events when focus moves within dropdown', async () => {
|
|
201
|
+
const dropdown = await getDropdown();
|
|
202
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
203
|
+
|
|
204
|
+
// Open the dropdown first
|
|
205
|
+
toggle.click();
|
|
206
|
+
await dropdown.updateComplete;
|
|
207
|
+
expect(dropdown.open).to.equal(true);
|
|
208
|
+
|
|
209
|
+
// Create an element within the dropdown
|
|
210
|
+
const dropdownContent = dropdown.querySelector(
|
|
211
|
+
'[slot="dropdown"]'
|
|
212
|
+
) as HTMLElement;
|
|
213
|
+
const internalButton = document.createElement('button');
|
|
214
|
+
internalButton.textContent = 'Internal';
|
|
215
|
+
dropdownContent.appendChild(internalButton);
|
|
216
|
+
|
|
217
|
+
// Simulate blur event where focus moves to internal element
|
|
218
|
+
const dropdownDiv = dropdown.shadowRoot.querySelector(
|
|
219
|
+
'.dropdown'
|
|
220
|
+
) as HTMLDivElement;
|
|
221
|
+
const blurEvent = new FocusEvent('blur', {
|
|
222
|
+
bubbles: true,
|
|
223
|
+
relatedTarget: internalButton
|
|
224
|
+
});
|
|
225
|
+
dropdownDiv.dispatchEvent(blurEvent);
|
|
226
|
+
await dropdown.updateComplete;
|
|
227
|
+
|
|
228
|
+
// Dropdown should remain open since focus moved within it
|
|
229
|
+
expect(dropdown.open).to.equal(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('prevents opening when already open', async () => {
|
|
233
|
+
const dropdown = await getDropdown();
|
|
234
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
235
|
+
|
|
236
|
+
// First, open the dropdown normally
|
|
237
|
+
toggle.click();
|
|
238
|
+
await dropdown.updateComplete;
|
|
239
|
+
expect(dropdown.open).to.equal(true);
|
|
240
|
+
expect(dropdown.dormant).to.equal(false);
|
|
241
|
+
|
|
242
|
+
// Now try to click toggle again - should not call openDropdown again
|
|
243
|
+
// since !dropdown.open is false
|
|
244
|
+
const originalOpen = dropdown.open;
|
|
245
|
+
toggle.click();
|
|
246
|
+
await dropdown.updateComplete;
|
|
247
|
+
|
|
248
|
+
// Should remain in the same state
|
|
249
|
+
expect(dropdown.open).to.equal(originalOpen);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('handles position calculation with right edge collision', async () => {
|
|
253
|
+
// Create dropdown positioned near right edge
|
|
254
|
+
const dropdown = await getDropdown(
|
|
255
|
+
{},
|
|
256
|
+
'<button slot="toggle" style="position: fixed; right: 50px; top: 100px; width: 100px; height: 30px;">Toggle</button>',
|
|
257
|
+
'<div slot="dropdown" style="width: 200px; height: 100px;">Wide content</div>'
|
|
258
|
+
);
|
|
259
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
260
|
+
|
|
261
|
+
// Open the dropdown properly by clicking
|
|
262
|
+
toggle.click();
|
|
263
|
+
await waitForStableRender(dropdown);
|
|
264
|
+
|
|
265
|
+
// Get actual element bounds to simulate collision
|
|
266
|
+
const dropdownDiv = dropdown.shadowRoot.querySelector(
|
|
267
|
+
'.dropdown'
|
|
268
|
+
) as HTMLDivElement;
|
|
269
|
+
const originalGetBoundingClientRect = dropdownDiv.getBoundingClientRect;
|
|
270
|
+
|
|
271
|
+
// Mock getBoundingClientRect to simulate right collision
|
|
272
|
+
dropdownDiv.getBoundingClientRect = function () {
|
|
273
|
+
return {
|
|
274
|
+
bottom: 200,
|
|
275
|
+
right: window.innerWidth + 100, // Extends beyond window
|
|
276
|
+
top: 100,
|
|
277
|
+
left: window.innerWidth - 50,
|
|
278
|
+
width: 200,
|
|
279
|
+
height: 100,
|
|
280
|
+
x: window.innerWidth - 50,
|
|
281
|
+
y: 100
|
|
282
|
+
} as DOMRect;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
// Trigger position calculation
|
|
287
|
+
dropdown.calculatePosition();
|
|
288
|
+
await waitForStableRender(dropdown);
|
|
289
|
+
|
|
290
|
+
// Verify position was adjusted for right edge
|
|
291
|
+
expect(dropdown.dropdownStyle).to.have.property('left');
|
|
292
|
+
|
|
293
|
+
// Screenshot positioned dropdown
|
|
294
|
+
await assertScreenshot(
|
|
295
|
+
'dropdown/right-edge-collision',
|
|
296
|
+
getDropdownClip(dropdown)
|
|
297
|
+
);
|
|
298
|
+
} finally {
|
|
299
|
+
// Restore original method
|
|
300
|
+
dropdownDiv.getBoundingClientRect = originalGetBoundingClientRect;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('handles position calculation with bottom edge collision', async () => {
|
|
305
|
+
// Create dropdown positioned near bottom edge
|
|
306
|
+
const dropdown = await getDropdown(
|
|
307
|
+
{},
|
|
308
|
+
'<button slot="toggle" style="position: fixed; left: 100px; bottom: 50px; width: 100px; height: 30px;">Toggle</button>',
|
|
309
|
+
'<div slot="dropdown" style="width: 200px; height: 100px; position: absolute;">Tall content</div>'
|
|
310
|
+
);
|
|
311
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
312
|
+
|
|
313
|
+
// Open the dropdown properly by clicking
|
|
314
|
+
toggle.click();
|
|
315
|
+
await waitForStableRender(dropdown);
|
|
316
|
+
|
|
317
|
+
// Get actual element bounds to simulate collision
|
|
318
|
+
const dropdownDiv = dropdown.shadowRoot.querySelector(
|
|
319
|
+
'.dropdown'
|
|
320
|
+
) as HTMLDivElement;
|
|
321
|
+
const originalGetBoundingClientRect = dropdownDiv.getBoundingClientRect;
|
|
322
|
+
|
|
323
|
+
// Mock getBoundingClientRect to simulate bottom collision
|
|
324
|
+
dropdownDiv.getBoundingClientRect = function () {
|
|
325
|
+
return {
|
|
326
|
+
bottom: window.innerHeight + 100, // Extends beyond window
|
|
327
|
+
right: 300,
|
|
328
|
+
top: window.innerHeight - 50,
|
|
329
|
+
left: 100,
|
|
330
|
+
width: 200,
|
|
331
|
+
height: 100,
|
|
332
|
+
x: 100,
|
|
333
|
+
y: window.innerHeight - 50
|
|
334
|
+
} as DOMRect;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
// Trigger position calculation
|
|
339
|
+
dropdown.calculatePosition();
|
|
340
|
+
await waitForStableRender(dropdown);
|
|
341
|
+
|
|
342
|
+
// Verify position was adjusted for bottom edge (bumped up)
|
|
343
|
+
expect(dropdown.dropdownStyle).to.have.property('top');
|
|
344
|
+
expect(dropdown.arrowStyle).to.have.property('transform');
|
|
345
|
+
expect(dropdown.arrowStyle['transform']).to.include('rotate(180deg)');
|
|
346
|
+
|
|
347
|
+
// Screenshot positioned dropdown
|
|
348
|
+
await assertScreenshot(
|
|
349
|
+
'dropdown/bottom-edge-collision',
|
|
350
|
+
getDropdownClip(dropdown)
|
|
351
|
+
);
|
|
352
|
+
} finally {
|
|
353
|
+
// Restore original method
|
|
354
|
+
dropdownDiv.getBoundingClientRect = originalGetBoundingClientRect;
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('handles arrow positioning when toggle is very narrow', async () => {
|
|
359
|
+
const dropdown = await getDropdown(
|
|
360
|
+
{},
|
|
361
|
+
'<button slot="toggle" style="width: 5px;">•</button>',
|
|
362
|
+
'<div slot="dropdown">Content</div>'
|
|
363
|
+
);
|
|
364
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
365
|
+
|
|
366
|
+
// Open the dropdown properly by clicking
|
|
367
|
+
toggle.click();
|
|
368
|
+
await dropdown.updateComplete;
|
|
369
|
+
|
|
370
|
+
// Trigger position calculation
|
|
371
|
+
dropdown.calculatePosition();
|
|
372
|
+
await waitForStableRender(dropdown);
|
|
373
|
+
|
|
374
|
+
// Verify arrow positioning was adjusted for narrow toggle
|
|
375
|
+
expect(dropdown.dropdownStyle).to.have.property('marginLeft');
|
|
376
|
+
expect(dropdown.dropdownStyle['marginLeft']).to.equal('-10px');
|
|
377
|
+
expect(dropdown.open).to.equal(true);
|
|
378
|
+
expect(dropdown.dormant).to.equal(false);
|
|
379
|
+
|
|
380
|
+
// Screenshot with adjusted arrow
|
|
381
|
+
await assertScreenshot('dropdown/narrow-toggle', getDropdownClip(dropdown));
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('handles position calculation when toggle element is missing', async () => {
|
|
385
|
+
const dropdown = await getDropdown(
|
|
386
|
+
{ open: true, dormant: false },
|
|
387
|
+
'', // No toggle element
|
|
388
|
+
'<div slot="dropdown">Content</div>'
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Trigger position calculation - should handle gracefully
|
|
392
|
+
dropdown.calculatePosition();
|
|
393
|
+
await dropdown.updateComplete;
|
|
394
|
+
|
|
395
|
+
// Should not crash and should have basic styles
|
|
396
|
+
expect(typeof dropdown.dropdownStyle).to.equal('object');
|
|
397
|
+
expect(typeof dropdown.arrowStyle).to.equal('object');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('handles resetBlurHandler when activeFocus exists', async () => {
|
|
401
|
+
const dropdown = await getDropdown();
|
|
402
|
+
const toggle = dropdown.querySelector('[slot="toggle"]') as HTMLElement;
|
|
403
|
+
|
|
404
|
+
// Open dropdown to trigger resetBlurHandler for the first time
|
|
405
|
+
toggle.click();
|
|
406
|
+
await dropdown.updateComplete;
|
|
407
|
+
expect(dropdown.open).to.equal(true);
|
|
408
|
+
|
|
409
|
+
// Now open it again - this should trigger the activeFocus cleanup branch
|
|
410
|
+
// First we need to close it
|
|
411
|
+
const dropdownDiv = dropdown.shadowRoot.querySelector(
|
|
412
|
+
'.dropdown'
|
|
413
|
+
) as HTMLDivElement;
|
|
414
|
+
const blurEvent = new FocusEvent('blur', {
|
|
415
|
+
bubbles: true,
|
|
416
|
+
relatedTarget: document.body
|
|
417
|
+
});
|
|
418
|
+
dropdownDiv.dispatchEvent(blurEvent);
|
|
419
|
+
|
|
420
|
+
// Wait for close
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
422
|
+
expect(dropdown.open).to.equal(false);
|
|
423
|
+
expect(dropdown.dormant).to.equal(true);
|
|
424
|
+
|
|
425
|
+
// Open again - this should trigger the cleanup in resetBlurHandler
|
|
426
|
+
toggle.click();
|
|
427
|
+
await dropdown.updateComplete;
|
|
428
|
+
expect(dropdown.open).to.equal(true);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('renders without mask by default', async () => {
|
|
432
|
+
const dropdown = await getDropdown(); // No mask explicitly set
|
|
433
|
+
|
|
434
|
+
// Test expected values first - mask should be false by default
|
|
435
|
+
expect(dropdown.mask).to.equal(false);
|
|
436
|
+
|
|
437
|
+
// Look for mask element in shadow DOM - should not exist when mask is false
|
|
438
|
+
const maskElement = dropdown.shadowRoot.querySelector('.mask');
|
|
439
|
+
expect(maskElement).to.be.null;
|
|
440
|
+
|
|
441
|
+
// Screenshot default rendering
|
|
442
|
+
await assertScreenshot('dropdown/no-mask', getClip(dropdown));
|
|
443
|
+
});
|
|
444
|
+
});
|