@shortfuse/materialdesignweb 0.7.6 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +57 -68
  2. package/components/Badge.js +2 -2
  3. package/components/BottomAppBar.js +3 -5
  4. package/components/Box.js +33 -3
  5. package/components/Button.js +48 -21
  6. package/components/Button.md +9 -9
  7. package/components/Card.js +9 -16
  8. package/components/Checkbox.js +45 -36
  9. package/components/CheckboxIcon.js +2 -2
  10. package/components/Chip.js +1 -1
  11. package/components/Dialog.js +228 -359
  12. package/components/DialogActions.js +2 -2
  13. package/components/Divider.js +3 -3
  14. package/components/ExtendedFab.js +4 -8
  15. package/components/Fab.js +1 -2
  16. package/components/FilterChip.js +4 -4
  17. package/components/Headline.js +1 -1
  18. package/components/Icon.js +8 -8
  19. package/components/IconButton.js +9 -14
  20. package/components/Input.js +273 -1
  21. package/components/Layout.js +485 -16
  22. package/components/List.js +6 -4
  23. package/components/ListItem.js +12 -12
  24. package/components/ListOption.js +21 -5
  25. package/components/Listbox.js +239 -0
  26. package/components/Menu.js +77 -526
  27. package/components/MenuItem.js +12 -14
  28. package/components/Nav.js +0 -2
  29. package/components/NavBar.js +8 -79
  30. package/components/NavDrawer.js +12 -11
  31. package/components/NavDrawerItem.js +2 -1
  32. package/components/NavItem.js +18 -8
  33. package/components/NavRail.js +15 -7
  34. package/components/NavRailItem.js +3 -1
  35. package/components/Popup.js +20 -0
  36. package/components/Progress.js +24 -23
  37. package/components/Radio.js +42 -35
  38. package/components/RadioIcon.js +3 -3
  39. package/components/Ripple.js +2 -3
  40. package/components/Search.js +85 -0
  41. package/components/SegmentedButton.js +1 -10
  42. package/components/SegmentedButtonGroup.js +16 -10
  43. package/components/Select.js +4 -4
  44. package/components/Shape.js +1 -1
  45. package/components/Slider.js +43 -50
  46. package/components/Snackbar.js +4 -5
  47. package/components/Surface.js +3 -3
  48. package/components/Switch.js +55 -21
  49. package/components/SwitchIcon.js +10 -8
  50. package/components/Tab.js +11 -9
  51. package/components/TabContent.js +4 -3
  52. package/components/TabList.js +2 -2
  53. package/components/TabPanel.js +11 -8
  54. package/components/TextArea.js +38 -35
  55. package/components/Tooltip.js +2 -2
  56. package/components/TopAppBar.js +65 -147
  57. package/core/Composition.js +985 -628
  58. package/core/CompositionAdapter.js +315 -0
  59. package/core/CustomElement.js +153 -90
  60. package/core/DomAdapter.js +586 -0
  61. package/core/ICustomElement.d.ts +2 -2
  62. package/core/css.js +8 -7
  63. package/core/customTypes.js +53 -31
  64. package/{utils → core}/jsonMergePatch.js +36 -14
  65. package/core/observe.js +111 -57
  66. package/core/optimizations.js +23 -0
  67. package/core/template.js +17 -11
  68. package/core/test.js +126 -0
  69. package/core/typings.d.ts +11 -5
  70. package/core/uid.js +13 -0
  71. package/dist/index.min.js +83 -152
  72. package/dist/index.min.js.map +4 -4
  73. package/dist/meta.json +1 -1
  74. package/mixins/AriaReflectorMixin.js +1 -2
  75. package/mixins/AriaToolbarMixin.js +2 -3
  76. package/mixins/ControlMixin.js +25 -17
  77. package/mixins/DensityMixin.js +0 -1
  78. package/mixins/FlexableMixin.js +1 -2
  79. package/mixins/FormAssociatedMixin.js +13 -10
  80. package/mixins/InputMixin.js +2 -9
  81. package/mixins/KeyboardNavMixin.js +14 -1
  82. package/mixins/PopupMixin.js +757 -0
  83. package/mixins/RTLObserverMixin.js +0 -1
  84. package/mixins/ResizeObserverMixin.js +0 -1
  85. package/mixins/RippleMixin.js +3 -4
  86. package/mixins/ScrollListenerMixin.js +41 -32
  87. package/mixins/SemiStickyMixin.js +151 -0
  88. package/mixins/ShapeMixin.js +29 -24
  89. package/mixins/StateMixin.js +11 -6
  90. package/mixins/SurfaceMixin.js +3 -57
  91. package/mixins/TextFieldMixin.js +57 -65
  92. package/mixins/ThemableMixin.js +78 -156
  93. package/mixins/TooltipTriggerMixin.js +7 -13
  94. package/mixins/TouchTargetMixin.js +4 -3
  95. package/package.json +9 -5
  96. package/theming/index.js +1 -1
  97. package/theming/themableMixinLoader.js +12 -0
  98. package/utils/{hct → material-color}/blend.js +8 -10
  99. package/utils/{hct → material-color/hct}/Cam16.js +196 -69
  100. package/utils/{hct → material-color/hct}/Hct.js +61 -19
  101. package/utils/{hct → material-color/hct}/ViewingConditions.js +3 -3
  102. package/utils/{hct → material-color/hct}/hctSolver.js +9 -16
  103. package/utils/{hct → material-color}/helper.js +11 -18
  104. package/utils/{hct → material-color/palettes}/CorePalette.js +79 -19
  105. package/utils/{hct → material-color/palettes}/TonalPalette.js +12 -4
  106. package/utils/material-color/scheme/Scheme.js +376 -0
  107. package/utils/{hct/colorUtils.js → material-color/utils/color.js} +61 -1
  108. package/utils/popup.js +46 -25
  109. package/components/ListSelect.js +0 -220
  110. package/components/Option.js +0 -91
  111. package/components/Pane.js +0 -281
  112. package/core/identify.js +0 -40
  113. package/utils/hct/Scheme.js +0 -587
  114. /package/utils/{hct/mathUtils.js → material-color/utils/math.js} +0 -0
@@ -5,86 +5,27 @@ import CustomElement from '../core/CustomElement.js';
5
5
  import { attemptFocus } from '../core/dom.js';
6
6
  import DensityMixin from '../mixins/DensityMixin.js';
7
7
  import KeyboardNavMixin from '../mixins/KeyboardNavMixin.js';
8
- import { canAnchorPopup } from '../utils/popup.js';
9
-
10
- /**
11
- * @typedef {Object} MenuStack
12
- * @prop {HTMLElement} element
13
- * @prop {Element} previousFocus
14
- * @prop {Object} [state]
15
- * @prop {Object} [previousState]
16
- * @prop {MouseEvent|PointerEvent|HTMLElement|Element} [originalEvent]
17
- * @prop {any} [pendingResizeOperation]
18
- * @prop {window['history']['scrollRestoration']} [scrollRestoration]
19
- */
20
-
21
- const supportsHTMLDialogElement = typeof HTMLDialogElement !== 'undefined';
22
- /** @type {MenuStack[]} */
23
- const OPEN_MENUS = [];
24
-
25
- /**
26
- * @return {void}
27
- */
28
- function onWindowResize() {
29
- const lastOpenMenu = OPEN_MENUS.at(-1);
30
- if (!lastOpenMenu || !lastOpenMenu.originalEvent) {
31
- return;
32
- }
33
- if (lastOpenMenu.pendingResizeOperation) {
34
- cancelAnimationFrame(lastOpenMenu.pendingResizeOperation);
35
- }
36
- lastOpenMenu.pendingResizeOperation = requestAnimationFrame(() => {
37
- lastOpenMenu.element.updateMenuPosition(lastOpenMenu.originalEvent);
38
- lastOpenMenu.pendingResizeOperation = null;
39
- });
40
- }
41
-
42
- /**
43
- * @param {PopStateEvent} event
44
- */
45
- function onPopState(event) {
46
- if (!event.state) return;
47
- const lastOpenMenu = OPEN_MENUS.at(-1);
48
- if (!lastOpenMenu || !lastOpenMenu.previousState) {
49
- return;
50
- }
51
- if ((lastOpenMenu.previousState === event.state) || Object.keys(event.state)
52
- .every((key) => event.state[key] === lastOpenMenu.previousState[key])) {
53
- lastOpenMenu.element.close();
54
- }
55
- }
56
-
57
- /** @param {BeforeUnloadEvent} event */
58
- function onBeforeUnload(event) {
59
- if (!OPEN_MENUS.length) return;
60
- console.warn('Menu was open during page unload (refresh?).');
61
- }
8
+ import PopupMixin from '../mixins/PopupMixin.js';
9
+ import ShapeMixin from '../mixins/ShapeMixin.js';
10
+ import SurfaceMixin from '../mixins/SurfaceMixin.js';
11
+ import ThemableMixin from '../mixins/ThemableMixin.js';
62
12
 
63
13
  export default CustomElement
14
+ .extend()
15
+ .mixin(ThemableMixin)
16
+ .mixin(SurfaceMixin)
17
+ .mixin(ShapeMixin)
18
+ .mixin(PopupMixin)
64
19
  .mixin(DensityMixin)
65
20
  .mixin(KeyboardNavMixin)
66
- .extend()
67
- .observe({
68
- open: 'boolean',
69
- flow: {
70
- type: 'string',
71
- /** @type {'corner'|'adjacent'|'overflow'|'vcenter'|'hcenter'|'center'} */
72
- value: null,
73
- },
74
- submenu: 'boolean',
75
- modal: 'boolean',
76
- _isNativeModal: 'boolean',
77
- color: { empty: 'surface' },
78
- ink: 'string',
79
- elevation: { empty: 2 },
80
- outlined: 'boolean',
81
- })
82
21
  .set({
83
- returnValue: '',
84
- delegatesFocus: true,
22
+ scrollable: true,
23
+ flow: 'corner',
24
+ _useScrim: false,
85
25
  /** @type {WeakRef<HTMLElement>} */
86
26
  _cascader: null,
87
- _closing: false,
27
+ /** @type {WeakRef<HTMLElement>} */
28
+ _submenu: null,
88
29
  })
89
30
  .define({
90
31
  kbdNavChildren() {
@@ -107,484 +48,95 @@ export default CustomElement
107
48
  this._cascader = value ? new WeakRef(value) : null;
108
49
  },
109
50
  },
51
+ submenu: {
52
+ get() {
53
+ return this._submenu?.deref();
54
+ },
55
+ /**
56
+ * @param {HTMLElement} value
57
+ */
58
+ set(value) {
59
+ this._submenu = value ? new WeakRef(value) : null;
60
+ },
61
+ },
62
+ })
63
+ .on({
64
+ composed() {
65
+ const { shape, surface, dialog, scrim } = this.refs;
66
+ surface.append(shape);
67
+ dialog.prepend(surface);
68
+ scrim.setAttribute('invisible', '');
69
+
70
+ // Wrap slot in scroller
71
+ },
110
72
  })
111
- .html/* html */`
112
- <dialog id=dialog role=menu aria-hidden=${({ open }) => (open ? 'false' : 'true')}>
113
- <div id=scrim aria-hidden=true modal={modal}></div>
114
- <form id=form method=dialog role=none>
115
- <mdw-surface id=surface elevation={elevation} color={color} ink={ink} outlined={outlined}>
116
- <div id=scroller>
117
- <slot id=slot on-slotchange={refreshTabIndexes}></slot>
118
- </div>
119
- </mdw-surface>
120
- <slot id=submenu-slot name=submenu></slot>
121
- </form>
122
- </dialog>
123
- `
124
73
  .css`
125
74
  /* https://m3.material.io/components/menus/specs */
126
75
 
127
76
  :host {
128
- --mdw-menu__transform-origin-inline-start: left;
129
- --mdw-menu__transform-origin-inline-end: right;
130
- /* Normal */
131
- --mdw-menu__transform-origin-x: var(--mdw-menu__transform-origin-inline-start);
132
- /* Down */
133
- --mdw-menu__transform-origin-y: top;
134
- --mdw-menu__inline-base: 56px;
135
- --mdw-menu__size: 2;
136
- --mdw-bg: var(--mdw-color__surface);
77
+ --mdw-shape__size: var(--mdw-shape__extra-small);
78
+ --mdw-bg: var(--mdw-color__surface-container);
137
79
  --mdw-ink: var(--mdw-color__on-surface);
138
- position: absolute;
139
- /* Default position is bottom */
140
- /* Default direction is start */
141
- inset-block: 100% auto;
142
- inset-inline: auto 0;
143
80
 
81
+ --mdw-surface__shadow__resting: var(--mdw-surface__shadow__2);
82
+ --mdw-surface__shadow__raised: var(--mdw-surface__shadow__resting);
144
83
  display: block;
145
- /* Hide scrollbar */
146
- -ms-overflow-style: none;
147
- /* Scroll mask */
148
- overscroll-behavior: none;
149
- overscroll-behavior: contain;
150
- scrollbar-width: none;
151
-
152
- pointer-events: none;
153
-
154
- transform-origin: var(--mdw-menu__transform-origin-x) var(--mdw-menu__transform-origin-y);
155
-
156
- transition-duration: motion.$fadeOutDuration;
157
- transition-property: none;
158
- transition-timing-function: motion.$decelerateEasing;
159
- }
160
-
161
- :host(::after) {
162
- content: '';
163
-
164
- display: block;
165
-
166
- block-size: 200%;
167
- inline-size: 200%;
168
- }
169
-
170
- :host(::-webkit-scrollbar) {
171
- display: none;
172
- }
173
-
174
- dialog {
175
- position: fixed;
176
- inset: 0;
177
-
178
- box-sizing: border-box;
179
- block-size:100%;
180
- max-block-size: none;
181
- inline-size:100%;
182
- max-inline-size: none;
183
- margin: 0;
184
- border: none;
185
- padding: 0;
186
-
187
- opacity: 0;
188
- visibility: hidden;
189
- z-index: 24;
190
-
191
- background-color: transparent;
192
-
193
- transition: none;
194
- transition-property: opacity;
195
- will-change: opacity;
196
- }
197
-
198
- dialog::backdrop {
199
- /** Use scrim instead */
200
- display: none;
201
- }
202
-
203
- dialog[aria-hidden="false"],
204
- dialog:modal {
205
- display: block;
206
-
207
- pointer-events: none;
208
-
209
- opacity: 1;
210
- visibility: visible;
211
-
212
- transition-duration: var(--mdw-dialog__fade-in-duration);
213
- transition-property: opacity;
214
- transition-timing-function: var(--mdw-dialog__deceleration-easing);
215
- }
216
-
217
- #scrim {
218
- position: fixed;
219
- inset: 0;
220
-
221
- overflow-y: scroll;
222
- overscroll-behavior: none;
223
- overscroll-behavior: contain;
224
- scrollbar-width: none;
225
-
226
- block-size: 100%;
227
- inline-size: 100%;
228
-
229
- cursor: default;
230
- pointer-events: auto;
231
- -webkit-tap-highlight-color: transparent;
232
-
233
- visibility: hidden; /* Only show if [modal] */
234
-
235
- z-index:0;
236
- }
237
-
238
- #form {
239
- display: contents;
240
- }
241
-
242
- #scrim::-webkit-scrollbar {
243
- display: none;
244
- }
245
-
246
- #scrim::after {
247
- content: '';
248
-
249
- display: block;
250
-
251
- block-size: 200%;
252
- inline-size: 200%;
253
- }
254
-
255
- #surface {
256
- --mdw-shape__size: var(--mdw-shape__extra-small);
257
- position: sticky;
258
84
 
259
- display: inline-flex;
260
- flex-direction: column;
261
-
262
- inline-size: calc(var(--mdw-menu__size) * var(--mdw-menu__inline-base));
85
+ inline-size: auto;
263
86
  min-inline-size: calc(var(--mdw-menu__inline-base) * 2);
264
87
  max-inline-size: 100vw;
265
- flex:1;
266
-
267
- pointer-events: auto;
268
- /* background-color: rgb(var(--mdw-color__surface)); */
269
- /* color: rgb(var(--mdw-color__on-surface)); */
270
- /* stylelint-disable-next-line liberty/use-logical-spec */
271
- will-change: top, left;
272
88
  }
273
89
 
274
- @supports(-moz-appearance: none) {
275
- #surface {
276
- position: absolute;
277
- }
90
+ #shape {
91
+ background-color: rgb(var(--mdw-bg));
278
92
  }
279
93
 
280
- #scroller {
281
- display: flex;
282
- align-items: stretch;
283
- flex-direction: column;
284
- overflow-y: auto;
285
- overscroll-behavior: none;
286
- overscroll-behavior: contain;
287
-
288
- flex: 1;
289
-
290
- padding-block: 8px;
291
- }
292
-
293
- #scrim[modal] {
294
- visibility: visible;
94
+ #form {
95
+ display: contents;
295
96
  }
296
97
  `
297
98
  .methods({
99
+ showModal(...args) {
100
+ this._useScrim = true;
101
+ const result = this.showPopup(...args);
102
+ this._useScrim = false;
103
+ return result;
104
+ },
298
105
  focus() {
299
106
  const [firstItem] = this.kbdNavChildren;
300
107
  if (!attemptFocus(firstItem)) {
301
108
  this.focusNext(firstItem);
302
109
  }
303
110
  },
304
- /**
305
- * @param {DOMRect|Element} [anchor]
306
- * @return {void}
307
- */
308
- updateMenuPosition(anchor) {
309
- const surface = this.refs.surface;
310
- surface.style.setProperty('max-height', 'none');
311
- surface.style.setProperty('width', 'auto');
312
- const newSize = Math.ceil(surface.clientWidth / 56);
313
- surface.style.removeProperty('width');
314
- surface.style.setProperty('--mdw-menu__size', newSize.toString(10));
315
-
316
- /** @type {import('../utils/popup.js').CanAnchorPopUpOptions} */
317
- const anchorOptions = {
318
- anchor: anchor ?? this.getBoundingClientRect(),
319
- width: surface.clientWidth,
320
- height: surface.clientHeight,
321
- // margin,
322
- };
323
-
324
- const isPageRTL = (getComputedStyle(this).direction === 'rtl');
325
- const xStart = isPageRTL ? 'right' : 'left';
326
- const xEnd = isPageRTL ? 'left' : 'right';
327
-
328
- /* Automatic Positioning
329
- *
330
- * Positions:
331
- * 3 7 4
332
- * ┌─────────┐
333
- * │ │
334
- * 5 │ 9 │ 6
335
- * │ │
336
- * └─────────┘
337
- * 1 8 2
338
- *
339
- * 1: Bottom Left
340
- * 2: Bottom Right
341
- * 3: Top Left
342
- * 4: Top Right
343
- * 5: VCenter Left
344
- * 6: VCenter Right
345
- * 7: HCenter Top
346
- * 8: HCenter Bottom
347
- * 9: VCenter HCenter
348
- *
349
- * Directions:
350
- * a - Down LTR
351
- * b - Down RTL
352
- * c - Up LTR
353
- * d - Up RTL
354
- * e - LTR
355
- * f - RTL
356
- * g - Down
357
- * h - Up
358
- * i - Center
359
- *
360
- *
361
- * 16 total combos
362
- * 1a 1b 1c 1d └↘ └↙ └↗ └↖
363
- * 2a 2b 2c 2d ┘↘ ┘↙ ┘↗ ┘↖
364
- * 3a 3b 3c 3d ┌↘ ┌↙ ┌↗ ┌↖
365
- * 4a 4b 4c 4d ┐↘ ┐↙ ┐↗ ┐↖
366
- *
367
- * Avoid using opposite angle
368
- *
369
- * 1a XX 1c 1d └↘ ██ └↗ └↖
370
- * XX 2b 2c 2d ██ ┘↙ ┘↗ ┘↖
371
- * 1a 3b 3c XX ┌↘ ┌↙ ┌↗ ██
372
- * 4a 4b XX 4d ┐↘ ┐↙ ██ ┐↖
373
- *
374
- *
375
- * Preference Order:
376
- * - Flow from corner 1a 2b 3c 4d └↘ ┘↙ ┌↗ ┐↖
377
- * - Open adjacent to target 4a 3b 2c 1d ┐↘ ┌↙ ┘↗ └↖
378
- * - Overlay target 3a 4b 1c 2d ┌↘ ┐↙ └↗ ┘↖
379
- * - Open from horizontal side 5e 6f │→ │←
380
- * - Open from center 9i █·
381
- */
382
-
383
- /** @type {import('../utils/popup.js').CanAnchorPopUpOptions[]} */
384
- const preferences = [
385
- (!this.submenu && (this.flow ?? 'corner') === 'corner') ? [
386
- { clientY: 'bottom', clientX: xStart },
387
- { clientY: 'bottom', clientX: xEnd },
388
- { clientY: 'top', clientX: xStart },
389
- { clientY: 'top', clientX: xEnd },
390
- ] : [],
391
- (this.submenu || (this.flow ?? 'adjacent') === 'adjacent') ? [
392
- { clientY: 'top', clientX: xEnd, directionX: xEnd, directionY: 'down' },
393
- { clientY: 'top', clientX: xStart, directionX: xStart, directionY: 'down' },
394
- { clientY: 'bottom', clientX: xEnd, directionX: xEnd, directionY: 'up' },
395
- { clientY: 'bottom', clientX: xStart, directionX: xStart, directionY: 'up' },
396
- ] : [],
397
- (!this.submenu && (this.flow ?? 'overlay') === 'overlay') ? [
398
- { clientY: 'top', clientX: xStart, directionX: xEnd, directionY: 'down' },
399
- { clientY: 'top', clientX: xEnd, directionX: xStart, directionY: 'down' },
400
- { clientY: 'bottom', clientX: xStart, directionX: xEnd, directionY: 'up' },
401
- { clientY: 'bottom', clientX: xEnd, directionX: xStart, directionY: 'up' },
402
- ] : [],
403
- (!this.submenu && (this.flow ?? 'vcenter') === 'vcenter') ? [
404
- { clientY: 'center', clientX: xEnd, directionX: xEnd, directionY: 'center' },
405
- { clientY: 'center', clientX: xStart, directionX: xStart, directionY: 'center' },
406
- ] : [],
407
- (!this.submenu && (this.flow ?? 'hcenter') === 'hcenter') ? [
408
- { clientY: 'bottom', clientX: 'center', directionX: 'center', directionY: 'down' },
409
- { clientY: 'top', clientX: 'center', directionX: 'center', directionY: 'up' },
410
- ] : [],
411
- (!this.submenu && (this.flow ?? 'center') === 'center') ? [
412
- { clientY: 'center', clientX: 'center', directionX: 'center', directionY: 'center' },
413
- ] : [],
414
- ].flat();
415
-
416
- let anchorResult;
417
- for (const preference of preferences) {
418
- anchorResult = canAnchorPopup({
419
- ...anchorOptions,
420
- ...preference,
421
- });
422
- if (anchorResult) break;
423
- }
424
-
425
- if (!anchorResult) {
426
- anchorResult = canAnchorPopup({
427
- ...anchorOptions,
428
- ...preferences[0],
429
- force: true,
430
- });
431
- }
432
-
433
- surface.style.setProperty('inset-block-start', `${anchorResult.pageY}px`);
434
- surface.style.setProperty('inset-inline-start', `${anchorResult.pageX}px`);
435
- surface.style.setProperty('margin', '0');
436
- surface.style.setProperty('transform-origin', `${anchorResult.transformOriginY} ${anchorResult.transformOriginX}`);
437
- surface.scrollIntoView();
438
- },
439
- /**
440
- * @param {MouseEvent|PointerEvent|HTMLElement|Element} source
441
- * @return {boolean} handled
442
- */
443
- showModal(source) {
444
- if (this.open) return false;
445
- this.modal = true;
446
- if (supportsHTMLDialogElement) {
447
- this._dialog.showModal();
448
- this._isNativeModal = true;
449
- }
450
- return this.showPopup(source);
451
- },
452
- /**
453
- * @param {MouseEvent|PointerEvent|HTMLElement|Element} source
454
- * @return {boolean} handled
455
- */
456
- showPopup(source) {
457
- if (this.open) return false;
458
- this.open = true;
459
-
460
- const previousFocus = source instanceof HTMLElement ? source : document.activeElement;
461
- this.updateMenuPosition(source);
462
- if (supportsHTMLDialogElement && !this._dialog.open) {
463
- this._dialog.show();
464
- }
465
-
466
- const newState = { hash: Math.random().toString(36).slice(2, 18) };
467
- let previousState = null;
468
-
469
- if (!window.history.state) {
470
- // Create new previous state
471
- window.history.replaceState({
472
- hash: Math.random().toString(36).slice(2, 18),
473
- }, document.title);
474
- }
475
- previousState = window.history.state;
476
- const scrollRestoration = window.history.scrollRestoration;
477
- window.history.scrollRestoration = 'manual';
478
- window.history.pushState(newState, document.title);
479
- console.debug('Menu pushed page');
480
- window.addEventListener('popstate', onPopState);
481
- window.addEventListener('beforeunload', onBeforeUnload);
482
-
483
- window.addEventListener('resize', onWindowResize);
484
- window.addEventListener('scroll', onWindowResize);
485
-
486
- OPEN_MENUS.push({
487
- element: this,
488
- previousFocus,
489
- state: newState,
490
- previousState,
491
- originalEvent: source,
492
- scrollRestoration,
493
- });
494
-
495
- this.focus();
496
-
497
- return true;
498
- },
499
- /**
500
- * @param {boolean} returnFocus Return focus to element focused during open
501
- * @return {boolean} handled
502
- */
503
- close(returnFocus = true) {
504
- if (!this.open) return false;
505
- if (this._closing) return false;
506
- this._closing = true;
507
- this.modal = false;
508
- if (this._isNativeModal) {
509
- this._isNativeModal = false;
510
- } else {
511
- const main = document.querySelector('main');
512
- if (main) {
513
- main.removeAttribute('aria-hidden');
514
- }
515
- }
516
- // if (this.dialogElement.getAttribute('aria-hidden') === 'true') return false;
517
- if (supportsHTMLDialogElement && this._dialog.open) {
518
- const previousFocus = document.activeElement;
519
- // Closing a native dialog will return focus automatically.
520
- this._dialog.close();
521
- if (!attemptFocus(previousFocus, { preventScroll: true })) {
522
- document.activeElement?.blur?.();
523
- }
524
- }
525
-
526
- // Will invoke observed attribute change: ('aria-hidden', 'true');
527
-
528
- this.open = false;
529
- this.dispatchEvent(new Event('close'));
530
-
531
- const len = OPEN_MENUS.length;
532
- for (let i = len - 1; i >= 0; i--) {
533
- const entry = OPEN_MENUS[i];
534
- if (entry.element === this) {
535
- if (entry.state && window.history && window.history.state && entry.state.hash === window.history.state.hash) {
536
- window.removeEventListener('popstate', onPopState);
537
- window.history.back();
538
- // Back does not set state immediately
539
- // Needed to track submenu
540
- // TODO: use window.history.go(indexDelta) instead for Safari (not Wekbit) submenu support
541
- window.history.replaceState(entry.previousState, document.title);
542
- window.history.scrollRestoration = entry.scrollRestoration || 'auto';
543
- window.addEventListener('popstate', onPopState);
544
- } else {
545
- console.warn('Menu state mismatch?', entry, window.history.state);
546
- }
547
- if (returnFocus) {
548
- entry.previousFocus?.focus?.({ preventScroll: true });
549
- }
550
- OPEN_MENUS.splice(i, 1);
551
- break;
552
- } else if (this.contains(entry.element)) {
553
- console.debug('Closing submenu first');
554
- entry.element.close(false);
555
- console.debug('Sub menu closed. Continuing...');
556
- }
557
- }
558
- if (!OPEN_MENUS.length) {
559
- window.removeEventListener('popstate', onPopState);
560
- window.removeEventListener('beforeunload', onBeforeUnload);
561
- window.removeEventListener('resize', onWindowResize);
562
- console.debug('All menus closed');
563
- }
564
- this._closing = false;
565
- return true;
566
- },
567
111
  /**
568
112
  * @param {HTMLElement} cascader Element that calls for submenu cascade
569
113
  */
570
114
  cascade(cascader) {
571
115
  this.cascader = cascader;
572
- this.showPopup(cascader);
573
- },
574
- /**
575
- * @param {MouseEvent|PointerEvent|HTMLElement|Element} source
576
- * @return {boolean} handled
577
- */
578
- show(source) {
579
- // Auto-select type based on default platform convention
580
- // Mac OS X / iPad does not expect clickthrough
581
- if (navigator.userAgent.includes('Mac OS X')) {
582
- return this.showModal(source);
583
- }
584
- return this.showPopup(source);
116
+ this.showPopup(cascader, true, 'adjacent');
585
117
  },
586
118
  })
587
119
  .events({
120
+ 'mdw-menu-item:cascade'(event) {
121
+ const menuItem = event.target;
122
+ const subMenuId = event.detail;
123
+ event.stopPropagation();
124
+
125
+ const submenu = this.getRootNode().getElementById(subMenuId);
126
+ this.submenu = submenu;
127
+ submenu.cascade(menuItem);
128
+ },
129
+ 'mdw-menu-item:cascader-blur'() {
130
+ const submenu = this.submenu;
131
+ // Wait for focus event (if mouse focus on sub menu item)
132
+ queueMicrotask(() => {
133
+ // Stay open if submenu is focused
134
+ if (submenu && submenu.matches(':focus-within,:focus')) return;
135
+
136
+ submenu.close(false);
137
+ });
138
+ },
139
+
588
140
  '~click'(event) {
589
141
  if (this !== event.target) return;
590
142
  // Clicked self (scrim-like)
@@ -608,7 +160,7 @@ export default CustomElement
608
160
  // Unless menu hiding is cancelled
609
161
  case 'ArrowLeft':
610
162
  case 'ArrowRight':
611
- if (!this.submenu) break;
163
+ // if (!this.submenu) break;
612
164
  if (getComputedStyle(this).direction === 'rtl') {
613
165
  if (event.key === 'ArrowLeft') break;
614
166
  } else if (event.key === 'ArrowRight') break;
@@ -622,17 +174,16 @@ export default CustomElement
622
174
  default:
623
175
  }
624
176
  },
625
- focusout(event) {
177
+ focusout() {
626
178
  if (!this.open) return;
627
179
  if (this.modal) return;
628
180
  // Wait until end of event loop cycle to see if focus really is lost
629
181
  queueMicrotask(() => {
630
182
  if (this.matches(':focus-within')) return;
631
- const activeElement = document.activeElement;
632
- if (activeElement
633
- && (this.cascader === activeElement || this.contains(activeElement))) {
634
- return;
635
- }
183
+ const { cascader, submenu } = this;
184
+
185
+ if (cascader && cascader.matches(':is(:focus-within,:focus)')) return;
186
+ if (submenu && submenu.matches(':is(:focus-within,:focus)')) return;
636
187
  this.close(false);
637
188
  });
638
189
  },