kmaterialize 2.3.3-1.0.28 → 2.3.3-1.0.29

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.
@@ -1,10 +1,21 @@
1
1
  .breadcrumb-wrapper {
2
2
  display: flex;
3
3
  align-items: center;
4
+ min-width: 0;
5
+ max-width: 100%;
6
+ overflow-x: auto;
7
+ white-space: nowrap;
8
+
9
+ // Support both direct breadcrumbs and an optional inner wrapper.
10
+ > * { flex-shrink: 0; }
11
+
12
+ .breadcrumb,
13
+ .breadcrumb > * { flex-shrink: 0; }
4
14
  }
5
15
 
6
16
  .breadcrumb {
7
17
  display: inline-flex;
18
+ vertical-align: middle;
8
19
  align-items: center;
9
20
 
10
21
  i,
@@ -18,10 +29,11 @@
18
29
 
19
30
  &:before {
20
31
  content: '\E5CC';
32
+ opacity: 0.6;
21
33
  font-family: 'Material Symbols Outlined', 'Material Symbols Rounded', 'Material Symbols Sharp', 'Material Icons';
22
34
  }
23
35
 
24
36
  &:first-child:before {
25
- visibility: hidden;
37
+ display: none;
26
38
  }
27
39
  }
@@ -36,7 +36,7 @@
36
36
  outline: 0
37
37
  }
38
38
 
39
- i {
39
+ > i {
40
40
  width: 2rem;
41
41
  font-size: 1.6rem;
42
42
  display: inline-block;
@@ -22,6 +22,11 @@
22
22
  &.divider-middle {
23
23
  margin-inline: var(--divider-inset);
24
24
  }
25
+ &.divider-dashed,
26
+ &.divider-dotted {
27
+ --divider-color: var(--md-sys-color-outline);
28
+ --divider-thickness: 1.5px;
29
+ }
25
30
  &.divider-strong {
26
31
  --divider-color: var(--md-sys-color-outline);
27
32
  }
@@ -0,0 +1,35 @@
1
+ import { afterEach, expect, test, vi } from 'vitest';
2
+ import { createTooltipWith } from './tooltip-factory';
3
+
4
+ afterEach(() => vi.unstubAllGlobals());
5
+
6
+ for (const shape of ['function', 'default', 'nested-default']) {
7
+ test(`creates and cleans up a tooltip from a ${shape} export`, () => {
8
+ vi.stubGlobal('window', { matchMedia: () => ({ matches: false }) });
9
+ const document = { addEventListener: vi.fn(), removeEventListener: vi.fn() };
10
+ vi.stubGlobal('document', document);
11
+ const destroy = vi.fn();
12
+ const tooltip = { hide: vi.fn(), destroy };
13
+ const factory = vi.fn(() => tooltip);
14
+ const plugin = { name: 'animateFill' };
15
+ const module = shape === 'function' ? factory : shape === 'default'
16
+ ? { default: factory, animateFill: plugin }
17
+ : { default: { default: factory, animateFill: plugin } };
18
+ const target = {};
19
+ const result = createTooltipWith(module, undefined, target, 'classic', { content: 'Home & Tools' });
20
+ expect(factory).toHaveBeenCalledWith(target, expect.objectContaining({ content: 'Home & Tools' }));
21
+ if (shape !== 'function') expect(factory.mock.calls[0][1].plugins).toEqual([plugin]);
22
+ document.addEventListener.mock.calls[0][1]({ key: 'Escape' });
23
+ expect(tooltip.hide).toHaveBeenCalledOnce();
24
+ result.destroy();
25
+ expect(destroy).toHaveBeenCalledOnce();
26
+ expect(document.removeEventListener).toHaveBeenCalledWith('keydown', expect.any(Function));
27
+ });
28
+ }
29
+
30
+ test('rejects missing or cyclic exports with a useful error', () => {
31
+ const cyclic = {}; cyclic.default = cyclic;
32
+ for (const value of [undefined, {}, { default: {} }, cyclic]) {
33
+ expect(() => createTooltipWith(value, undefined, {}, 'classic')).toThrow('did not export a tooltip function');
34
+ }
35
+ });
@@ -11,10 +11,24 @@ export function createTooltipWith(
11
11
  style: TooltipStyle,
12
12
  options: Partial<Props> = {},
13
13
  ): Instance {
14
+ // Dynamic imports of CommonJS builds can wrap Tippy in one or more
15
+ // default exports. Static ESM imports and browser globals are callable already.
16
+ let resolved: unknown = factory;
17
+ const seen = new Set<unknown>();
18
+ while (resolved && typeof resolved === "object" && !seen.has(resolved)) {
19
+ seen.add(resolved);
20
+ const module = resolved as { default?: unknown; animateFill?: typeof animateFill };
21
+ fill ??= module.animateFill;
22
+ resolved = module.default;
23
+ }
24
+ if (typeof resolved !== "function") {
25
+ throw new TypeError('kmaterialize: "tippy.js" did not export a tooltip function.');
26
+ }
27
+ const create = resolved as typeof tippy;
14
28
  const reducedMotion = window.matchMedia(
15
29
  "(prefers-reduced-motion: reduce)",
16
30
  ).matches;
17
- const instance = factory(target, {
31
+ const instance = create(target, {
18
32
  animateFill: !reducedMotion && !!fill,
19
33
  arrow: false,
20
34
  plugins: fill ? [fill] : [],
@@ -20,6 +20,19 @@
20
20
  &[size="small"] .btn { --button-height: 30px; }
21
21
  &[size="extra-large"] .btn { --button-height: 100px; }
22
22
  }
23
+ // Headings center their button and text as a row instead of aligning their
24
+ // different font baselines. Keep this default easy to override in app layouts.
25
+ :where(h1, h2, h3, h4, h5, h6):has(> :where(crazy-button, regular-btn)) {
26
+ display: flex;
27
+ align-items: center;
28
+ > :where(crazy-button, regular-btn) {
29
+ flex-shrink: 0;
30
+ // Legacy inline-block overrides must not inherit the heading's line box.
31
+ // The inner .btn defines its own line-height for its label and icon.
32
+ line-height: 0;
33
+ }
34
+ }
35
+
23
36
  .btn-group > :is(crazy-button, regular-btn) {
24
37
  flex: 1 1 0;
25
38
  min-width: 0;
@@ -0,0 +1,50 @@
1
+ describe('Tabs with no tab links', () => {
2
+ let element;
3
+ let instance;
4
+
5
+ beforeEach(() => {
6
+ element = document.createElement('ul');
7
+ element.className = 'tabs';
8
+ document.body.appendChild(element);
9
+ });
10
+
11
+ afterEach(() => {
12
+ if (instance) instance.destroy();
13
+ instance = null;
14
+ element.remove();
15
+ });
16
+
17
+ [false, true].forEach((swipeable) => {
18
+ it(`handles an empty list with swipeable=${swipeable}`, () => {
19
+ instance = M.Tabs.init(element, { swipeable });
20
+ expect(M.Tabs.getInstance(element)).toBe(instance);
21
+ expect(instance.index).toBe(-1);
22
+ expect(element.querySelector('.indicator')).toBeNull();
23
+ expect(() => {
24
+ instance.updateTabIndicator();
25
+ instance.select('missing');
26
+ window.dispatchEvent(new Event('resize'));
27
+ element.click();
28
+ instance.destroy();
29
+ }).not.toThrow();
30
+ expect(M.Tabs.getInstance(element)).toBeUndefined();
31
+ instance = null;
32
+ });
33
+ });
34
+
35
+ it('ignores placeholder markup without tab anchors', () => {
36
+ element.innerHTML = '<li class="tab">Loading…</li>';
37
+ instance = M.Tabs.init(element);
38
+ expect(instance.index).toBe(-1);
39
+ });
40
+
41
+ it('can initialize normally after links are added', () => {
42
+ instance = M.Tabs.init(element);
43
+ element.innerHTML = '<li class="tab"><a href="#missing-panel">First</a></li>';
44
+ instance = M.Tabs.init(element);
45
+ expect(instance.index).toBe(0);
46
+ expect(element.querySelector('a').classList.contains('active')).toBeTrue();
47
+ expect(element.querySelector('.indicator')).not.toBeNull();
48
+ expect(() => instance.updateTabIndicator()).not.toThrow();
49
+ });
50
+ });
@@ -40,7 +40,7 @@ export class Tabs extends Component<TabsOptions> {
40
40
  _tabWidth: number;
41
41
  _tabsWidth: number;
42
42
  _tabsCarousel: Carousel;
43
- _activeTabLink: HTMLAnchorElement;
43
+ _activeTabLink: HTMLAnchorElement | null;
44
44
  _content: HTMLElement;
45
45
 
46
46
  constructor(el: HTMLElement, options: Partial<TabsOptions>) {
@@ -55,6 +55,8 @@ export class Tabs extends Component<TabsOptions> {
55
55
  this._tabLinks = this.el.querySelectorAll('li.tab > a');
56
56
  this._index = 0;
57
57
  this._setupActiveTabLink();
58
+ // Empty/unfinished tab markup is a valid inert instance. Reinitialize after adding links.
59
+ if (!this._activeTabLink) return;
58
60
  if (this.options.swipeable) {
59
61
  this._setupSwipeableTabs();
60
62
  } else {
@@ -100,9 +102,9 @@ export class Tabs extends Component<TabsOptions> {
100
102
 
101
103
  destroy() {
102
104
  this._removeEventHandlers();
103
- this._indicator.parentNode.removeChild(this._indicator);
105
+ this._indicator?.remove();
104
106
  if (this.options.swipeable) {
105
- this._teardownSwipeableTabs();
107
+ if (this._tabsCarousel) this._teardownSwipeableTabs();
106
108
  } else {
107
109
  this._teardownNormalTabs();
108
110
  }
@@ -134,6 +136,7 @@ export class Tabs extends Component<TabsOptions> {
134
136
  }
135
137
 
136
138
  _handleWindowResize = () => {
139
+ if (!this._activeTabLink || !this._indicator) return;
137
140
  this._setTabsAndTabWidth();
138
141
  if (this._tabWidth !== 0 && this._tabsWidth !== 0) {
139
142
  this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + 'px';
@@ -152,7 +155,7 @@ export class Tabs extends Component<TabsOptions> {
152
155
  }
153
156
 
154
157
  // Handle click on tab link only
155
- if (!tabLink || !tab.classList.contains('tab')) return;
158
+ if (!tabLink || !tab || !tab.classList.contains('tab')) return;
156
159
  // is disabled?
157
160
  if (tab.classList.contains('disabled')) {
158
161
  e.preventDefault();
@@ -221,6 +224,10 @@ export class Tabs extends Component<TabsOptions> {
221
224
  }
222
225
  this._activeTabLink = activeTabLink as HTMLAnchorElement;
223
226
  }
227
+ if (!this._activeTabLink) {
228
+ this._index = -1;
229
+ return;
230
+ }
224
231
  Array.from(this._tabLinks).forEach((a: HTMLAnchorElement) => a.classList.remove('active'));
225
232
  this._activeTabLink.classList.add('active');
226
233
 
@@ -324,6 +331,7 @@ export class Tabs extends Component<TabsOptions> {
324
331
  * the indicator position is not correct.
325
332
  */
326
333
  updateTabIndicator() {
334
+ if (!this._activeTabLink || !this._indicator) return;
327
335
  this._setTabsAndTabWidth();
328
336
  this._animateIndicator(this._index);
329
337
  }
@@ -371,3 +371,54 @@ textarea {
371
371
  text-indent: -9999px;
372
372
  }
373
373
  }
374
+
375
+ // Native legend cuts a real notch: no background-color patch is needed.
376
+ .input-field.outlined.has-outlined-notch,
377
+ .input-field.outlined > .select-wrapper.has-outlined-notch {
378
+ > input:not([type=checkbox]):not([type=radio]), > textarea {
379
+ border-color: transparent;
380
+ &:focus:not([readonly]), &:disabled, &[readonly] { border-color: transparent; }
381
+ + label,
382
+ &:focus:not([readonly]) + label { background-color: transparent; }
383
+ }
384
+ > .input-outline {
385
+ position: absolute;
386
+ pointer-events: none;
387
+ box-sizing: border-box;
388
+ min-width: 0;
389
+ margin: 0;
390
+ padding: 0 12px;
391
+ border: 1px solid var(--md-sys-color-on-surface-variant);
392
+ border-radius: 4px;
393
+ background: transparent;
394
+ > legend {
395
+ float: none;
396
+ width: auto;
397
+ display: block;
398
+ max-width: 0.01px;
399
+ height: 16px;
400
+ padding: 0;
401
+ font: inherit;
402
+ font-size: 12px;
403
+ line-height: 16px;
404
+ visibility: hidden;
405
+ white-space: nowrap;
406
+ overflow: hidden;
407
+ > span { display: inline-block; padding: 0 3px; }
408
+ }
409
+ }
410
+ > input:focus:not([readonly]) ~ .input-outline,
411
+ > textarea:focus:not([readonly]) ~ .input-outline {
412
+ border-width: 2px;
413
+ border-color: var(--input-color);
414
+ padding-inline: 11px;
415
+ }
416
+ > :is(input, textarea):is(:focus:not([readonly]), :not([placeholder=' ']), :not(:placeholder-shown)) ~ .input-outline > legend {
417
+ max-width: 100%;
418
+ }
419
+ > :is(input, textarea):is(:disabled, [readonly]:not(.select-dropdown)) ~ .input-outline {
420
+ border-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 12%);
421
+ }
422
+ > :is(input, textarea).invalid ~ .input-outline,
423
+ &.error > .input-outline { border-color: var(--md-sys-color-error); }
424
+ }
@@ -119,11 +119,11 @@ select:disabled {
119
119
  .input-field > .select-wrapper:has(.hide-select > select[placeholder]:not([placeholder=""])) > label,
120
120
  .input-field > .select-wrapper:focus-within > label {
121
121
  position: absolute !important;
122
- top: -12px !important;
122
+ top: 4px !important;
123
123
  left: 16px !important;
124
- padding: 0 4px !important;
124
+ padding: 0 !important;
125
125
  font-size: 16px !important;
126
- background: var(--md-sys-color-surface);
126
+ background: transparent;
127
127
  transform: scale(0.75) !important;
128
128
  }
129
129
 
@@ -156,3 +156,37 @@ select:disabled {
156
156
  padding-left: 1rem;
157
157
  }
158
158
  }
159
+
160
+ // Generated select inputs are readonly for typing, but remain interactive.
161
+ // They must not inherit the filled input's extra top padding or disabled outline.
162
+ .input-field.outlined > .select-wrapper {
163
+ &:focus-within { outline: none; }
164
+ > input.select-dropdown,
165
+ > input.select-dropdown:not([type=checkbox]):not([type=radio]):not(:focus):not(:placeholder-shown) {
166
+ padding-top: 0;
167
+ padding-bottom: 0;
168
+ padding-right: 40px;
169
+ }
170
+ &.has-outlined-notch {
171
+ > input.select-dropdown + label { background: transparent; }
172
+ > .input-outline { border-color: var(--md-sys-color-on-surface-variant); }
173
+ &:focus-within > .input-outline {
174
+ border-color: var(--input-color);
175
+ border-width: 2px;
176
+ padding-inline: 11px;
177
+ > legend { max-width: 100%; }
178
+ }
179
+ &.disabled > .input-outline {
180
+ border-color: color-mix(in srgb, transparent, var(--md-sys-color-on-surface) 12%);
181
+ }
182
+ }
183
+ }
184
+
185
+ // Filled selects share the text field's in-field floating label layout.
186
+ .input-field:not(.outlined) > .select-wrapper:has(> label) {
187
+ > input.select-dropdown:not([type=checkbox]):not([type=radio]):not(:placeholder-shown),
188
+ &:focus-within > input.select-dropdown:not([type=checkbox]):not([type=radio]) {
189
+ padding-top: 12px;
190
+ padding-right: 40px;
191
+ }
192
+ }
@@ -1,3 +1,4 @@
1
+ import { initOutlinedNotches } from './outlined-notch';
1
2
  import { Utils } from '../../src/utils';
2
3
 
3
4
  export class Forms {
@@ -110,6 +111,7 @@ export class Forms {
110
111
  }
111
112
 
112
113
  static Init() {
114
+ initOutlinedNotches();
113
115
  if (typeof document !== 'undefined')
114
116
  document?.addEventListener('DOMContentLoaded', () => {
115
117
  document.addEventListener('change', (e: KeyboardEvent) => {
@@ -0,0 +1,81 @@
1
+ /** Decorative native fieldset notches; the original input and label remain accessible. */
2
+ export function initOutlinedNotches() {
3
+ if (typeof document === 'undefined') return;
4
+ const start = () => {
5
+ const selector = '.input-field.outlined, .input-field.outlined > .select-wrapper';
6
+ const entries = new Map<HTMLElement, { input: HTMLElement; outline: HTMLFieldSetElement }>();
7
+ const position = (field: HTMLElement) => {
8
+ const entry = entries.get(field);
9
+ if (!entry) return;
10
+ const { input, outline } = entry;
11
+ outline.style.left = `${input.offsetLeft}px`;
12
+ outline.style.top = `${input.offsetTop - 8}px`;
13
+ outline.style.width = `${input.offsetWidth}px`;
14
+ outline.style.height = `${input.offsetHeight + 8}px`;
15
+ };
16
+ const resize = new ResizeObserver(records => records.forEach(record => {
17
+ const field = (record.target as HTMLElement).parentElement;
18
+ if (field) position(field);
19
+ }));
20
+ const sync = (field: HTMLElement) => {
21
+ const input = field.querySelector<HTMLElement>(':scope > input:not([type=checkbox]):not([type=radio]):not([type=hidden]), :scope > textarea');
22
+ const label = input?.nextElementSibling;
23
+ const old = entries.get(field);
24
+ if (!field.isConnected || !field.matches(selector) || !input || label?.tagName !== 'LABEL') {
25
+ if (old) {
26
+ resize.unobserve(old.input);
27
+ old.outline.remove();
28
+ entries.delete(field);
29
+ field.classList.remove('has-outlined-notch');
30
+ }
31
+ return;
32
+ }
33
+ if (old && old.input !== input) {
34
+ resize.unobserve(old.input);
35
+ old.outline.remove();
36
+ entries.delete(field);
37
+ }
38
+ let entry = entries.get(field);
39
+ if (!entry) {
40
+ const outline = document.createElement('fieldset');
41
+ outline.className = 'input-outline';
42
+ outline.setAttribute('aria-hidden', 'true');
43
+ const legend = document.createElement('legend');
44
+ legend.appendChild(document.createElement('span'));
45
+ outline.appendChild(legend);
46
+ entry = { input, outline };
47
+ entries.set(field, entry);
48
+ field.appendChild(outline);
49
+ field.classList.add('has-outlined-notch');
50
+ resize.observe(input);
51
+ }
52
+ const text = entry.outline.querySelector('span');
53
+ if (text.textContent !== label.textContent) text.textContent = label.textContent;
54
+ position(field);
55
+ };
56
+ document.querySelectorAll<HTMLElement>(selector).forEach(sync);
57
+ const observer = new MutationObserver(records => {
58
+ const fields = new Set<HTMLElement>();
59
+ for (const record of records) {
60
+ const target = record.target instanceof Element ? record.target : record.target.parentElement;
61
+ if (target?.closest('.input-outline')) continue;
62
+ const field = target?.closest<HTMLElement>('.input-field');
63
+ if (field) fields.add(field);
64
+ for (const node of record.addedNodes) {
65
+ if (!(node instanceof HTMLElement) || node.matches('.input-outline')) continue;
66
+ if (node.matches(selector)) fields.add(node);
67
+ node.querySelectorAll<HTMLElement>(selector).forEach(el => fields.add(el));
68
+ }
69
+ }
70
+ // Release observers for detached partials; reinsertion creates a fresh outline.
71
+ for (const field of entries.keys()) if (!field.isConnected) fields.add(field);
72
+ for (const field of [...fields]) {
73
+ field.querySelectorAll<HTMLElement>(selector).forEach(el => fields.add(el));
74
+ }
75
+ fields.forEach(sync);
76
+ });
77
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class', 'placeholder', 'type'] });
78
+ };
79
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start, { once: true });
80
+ else start();
81
+ }
@@ -0,0 +1,35 @@
1
+ describe('Outlined field background-independent notch', () => {
2
+ let field;
3
+ const settle = () => new Promise(resolve => setTimeout(resolve, 60));
4
+ beforeEach(async () => {
5
+ field = document.createElement('div');
6
+ field.className = 'input-field outlined';
7
+ field.style.background = 'linear-gradient(to right, #30393d, #546e7a)';
8
+ field.innerHTML = '<input id="notch-regression" placeholder=" "><label for="notch-regression">Email subject</label>';
9
+ document.body.append(field);
10
+ await settle();
11
+ });
12
+ afterEach(() => field.remove());
13
+ it('keeps the accessible label and opens a real notch on focus', async () => {
14
+ const input = field.querySelector('input');
15
+ const label = field.querySelector('label');
16
+ const outline = field.querySelector('.input-outline');
17
+ expect(input.labels[0]).toBe(label);
18
+ expect(outline.getAttribute('aria-hidden')).toBe('true');
19
+ expect(outline.querySelector('legend').getBoundingClientRect().width).toBeLessThan(1);
20
+ input.focus();
21
+ await settle();
22
+ expect(outline.querySelector('legend').getBoundingClientRect().width).toBeGreaterThan(10);
23
+ expect(getComputedStyle(label).backgroundColor).toBe('rgba(0, 0, 0, 0)');
24
+ expect(getComputedStyle(input).borderTopColor).toBe('rgba(0, 0, 0, 0)');
25
+ });
26
+ it('updates label text and removes the outline when switching to a filled field', async () => {
27
+ field.querySelector('label').textContent = 'New label';
28
+ await settle();
29
+ expect(field.querySelector('legend').textContent).toBe('New label');
30
+ expect(field.querySelectorAll('.input-outline').length).toBe(1);
31
+ field.classList.remove('outlined');
32
+ await settle();
33
+ expect(field.querySelector('.input-outline')).toBeNull();
34
+ });
35
+ });
@@ -0,0 +1,25 @@
1
+ describe('Outlined FormSelect notch', () => {
2
+ it('centers the display text and keeps the floating label transparent', async () => {
3
+ const field = document.createElement('div');
4
+ field.className = 'input-field outlined';
5
+ field.innerHTML = '<select id="outlined-select-test"><option value="">Choose your option</option><option value="one">One</option></select><label for="outlined-select-test">Issue type</label>';
6
+ document.body.append(field);
7
+ const select = field.querySelector('select');
8
+ const instance = M.FormSelect.init(select);
9
+ try {
10
+ await new Promise(resolve => setTimeout(resolve, 60));
11
+ const input = field.querySelector('input.select-dropdown');
12
+ const label = field.querySelector('label');
13
+ const outline = field.querySelector('.input-outline');
14
+ expect(getComputedStyle(input).paddingTop).toBe('0px');
15
+ expect(outline).not.toBeNull();
16
+ expect(getComputedStyle(label).backgroundColor).toBe('rgba(0, 0, 0, 0)');
17
+ input.focus();
18
+ expect(getComputedStyle(outline).borderTopWidth).toBe('2px');
19
+ expect(getComputedStyle(label).backgroundColor).toBe('rgba(0, 0, 0, 0)');
20
+ } finally {
21
+ instance.destroy();
22
+ field.remove();
23
+ }
24
+ });
25
+ });
@@ -255,7 +255,7 @@ export class FormSelect extends Component<FormSelectOptions> {
255
255
  // Add input dropdown
256
256
  this.input = document.createElement('input');
257
257
  this.input.id = 'm_select-input-' + Utils.guid();
258
- this.input.classList.add('select-dropdown', 'dropdown-trigger');
258
+ this.input.classList.add('select-dropdown', 'dropdown-trigger', 'no-autoinit');
259
259
  this.input.type = 'text';
260
260
  this.input.readOnly = true;
261
261
  this.input.setAttribute('data-target', this.dropdownOptions.id);
@@ -0,0 +1,26 @@
1
+ describe('FormSelect dropdown ownership', () => {
2
+ [false, true].forEach(multiple => {
3
+ it(`keeps its dropdown instance during generic initialization (multiple=${multiple})`, async () => {
4
+ const field = document.createElement('div');
5
+ field.className = 'input-field outlined';
6
+ field.innerHTML = `<select ${multiple ? 'multiple' : ''}><option value="1">One</option><option value="2">Two</option></select>`;
7
+ document.body.append(field);
8
+ const select = field.querySelector('select');
9
+ const instance = M.FormSelect.init(select, { dropdownOptions: { inDuration: 0, outDuration: 0 } });
10
+ try {
11
+ M.Dropdown.init(field.querySelectorAll('.dropdown-trigger:not(.no-autoinit)'));
12
+ expect(M.Dropdown.getInstance(instance.input)).toBe(instance.dropdown);
13
+ instance.input.click();
14
+ await new Promise(resolve => setTimeout(resolve, 30));
15
+ instance.dropdownOptions.querySelectorAll('li')[1].click();
16
+ await new Promise(resolve => setTimeout(resolve, 30));
17
+ expect(instance.getSelectedValues()).toContain('2');
18
+ expect(instance.dropdown.isOpen).toBe(multiple);
19
+ expect(getComputedStyle(instance.dropdownOptions).display === 'none').toBe(!multiple);
20
+ } finally {
21
+ instance.destroy();
22
+ field.remove();
23
+ }
24
+ });
25
+ });
26
+ });
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Materialize v2.3.3-1.0.28 (https://materializeweb.com)
2
+ * Materialize v2.3.3-1.0.29 (https://materializeweb.com)
3
3
  * Copyright 2014-2026 Materialize
4
4
  * MIT License (https://raw.githubusercontent.com/materializecss/materialize/master/LICENSE)
5
5
  */