@brightspace-ui/core 3.323.0 → 3.325.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.
@@ -1,9 +1,10 @@
1
1
  import './list-item-drag-image.js';
2
2
  import { css, html, nothing } from 'lit';
3
- import { findComposedAncestor, isComposedAncestor } from '../../helpers/dom.js';
3
+ import { findComposedAncestor, getScrollableAncestor, isComposedAncestor } from '../../helpers/dom.js';
4
4
  import { announce } from '../../helpers/announce.js';
5
5
  import { classMap } from 'lit/directives/class-map.js';
6
6
  import { dragActions } from './list-item-drag-handle.js';
7
+ import { getFlag } from '../../helpers/flags.js';
7
8
  import { getUniqueId } from '../../helpers/uniqueId.js';
8
9
  import { ifDefined } from 'lit/directives/if-defined.js';
9
10
  import { SelectionInfo } from '../selection/selection-mixin.js';
@@ -24,6 +25,8 @@ export const dropLocation = moveLocations; // backwards compatibility
24
25
  const dropTargetLeaveDelay = 1000; // ms
25
26
  const touchHoldDuration = 400; // length of time user needs to hold down touch before dragging occurs
26
27
  const scrollSensitivity = 150; // pixels between top/bottom of viewport to scroll for mobile
28
+ const scrollContainerSensitivity = 60; // pixels between top/bottom of scrollable container to scroll for mobile
29
+ const scrollSpeed = 10; // pixels to scroll for mobile
27
30
 
28
31
  const createDragEvent = (name) => {
29
32
  const event = new Event(name, { bubbles: true });
@@ -427,6 +430,14 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
427
430
  if (this.shadowRoot) this.shadowRoot.querySelector(`#${this._itemDragId}`).activateKeyboardMode();
428
431
  }
429
432
 
433
+ get _isListItemDragAndDropMixin() {
434
+ return true;
435
+ }
436
+
437
+ #scrollableContainer;
438
+ #touchAction = '';
439
+ #improvedMobileDragAndDropFlag = getFlag('GAUD-10642-improved-mobile-drag-and-drop', true);
440
+
430
441
  _annoucePositionChange(dragTargetKey, dropTargetKey, dropLocation) {
431
442
  /** Dispatched when a draggable list item's position changes in the list. See [Event Details: d2l-list-item-position-change](#event-details%3A-d2l-list-item-position-change). */
432
443
  this.dispatchEvent(new CustomEvent('d2l-list-item-position-change', {
@@ -557,7 +568,20 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
557
568
 
558
569
  _findListItemFromCoordinates(x, y) {
559
570
  const listNode = findComposedAncestor(this.parentNode, (node) => node && node.tagName === 'D2L-LIST');
560
- return listNode.shadowRoot.elementFromPoint(x, y);
571
+ // remove this if when cleaning 'GAUD-10642-improved-mobile-drag-and-drop'
572
+ if (!this.#improvedMobileDragAndDropFlag) {
573
+ return listNode.shadowRoot.elementFromPoint(x, y);
574
+ }
575
+
576
+ // This is to make sure that in Firefox it looks for the list item
577
+ // in the shadow DOM (if rendered as part of a template)
578
+ // or in the light DOM (when using the default slot of a parent element)
579
+ const node = listNode.getRootNode().elementFromPoint(x, y);
580
+ if (node?._isListItemDragAndDropMixin) {
581
+ return node;
582
+ }
583
+
584
+ return null;
561
585
  }
562
586
 
563
587
  _getKeyboardText() {
@@ -792,6 +816,7 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
792
816
  _onTouchCancel() {
793
817
  if (this._touchTimeoutId) clearTimeout(this._touchTimeoutId);
794
818
  this._touchStarted = false;
819
+ if (this.#improvedMobileDragAndDropFlag) this.#resetScrollableContainer();
795
820
  }
796
821
 
797
822
  /**
@@ -800,14 +825,22 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
800
825
  _onTouchEnd(e) {
801
826
  if (this._touchTimeoutId) clearTimeout(this._touchTimeoutId);
802
827
  if (!this._touchStarted) return;
803
- e.preventDefault();
828
+ if (e.cancelable) e.preventDefault();
804
829
  this._touchStarted = false;
830
+ // leave the code inside the if when removing 'GAUD-10642-improved-mobile-drag-and-drop'
831
+ if (this.#improvedMobileDragAndDropFlag) this.#resetScrollableContainer();
805
832
  this._currentTouchListItem = undefined;
806
833
  // simulate drop if over a drop area
807
834
  const touch = e.changedTouches[0];
808
835
  const listItem = this._findListItemFromCoordinates(touch.clientX, touch.clientY);
809
- const dropGrid = listItem.shadowRoot.querySelector('.d2l-list-item-drag-drop-grid');
810
- if (dropGrid) dropGrid.dispatchEvent(createDragEvent('drop'));
836
+ // remove the if when cleaning 'GAUD-10642-improved-mobile-drag-and-drop' and leave the else code block
837
+ if (!this.#improvedMobileDragAndDropFlag) {
838
+ const dropGrid = listItem.shadowRoot.querySelector('.d2l-list-item-drag-drop-grid');
839
+ if (dropGrid) dropGrid.dispatchEvent(createDragEvent('drop'));
840
+ } else if (listItem) {
841
+ const dropGrid = listItem.shadowRoot.querySelector('.d2l-list-item-drag-drop-grid');
842
+ if (dropGrid) dropGrid.dispatchEvent(createDragEvent('drop'));
843
+ }
811
844
  // simulate dragend
812
845
  if (this.shadowRoot)
813
846
  this.shadowRoot.querySelector('.d2l-list-item-drag-area').dispatchEvent(createDragEvent('dragend'));
@@ -823,6 +856,8 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
823
856
  e.preventDefault();
824
857
  }
825
858
  const touch = e.changedTouches[0];
859
+ if (this.#improvedMobileDragAndDropFlag) this.#doTouchScroll(touch);
860
+
826
861
  const listItem = this._findListItemFromCoordinates(touch.clientX, touch.clientY);
827
862
  if (!listItem) return;
828
863
  // simulate host dragenter
@@ -830,6 +865,7 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
830
865
  listItem.dispatchEvent(createDragEvent('dragenter'));
831
866
  this._currentTouchListItem = listItem;
832
867
  }
868
+
833
869
  // get the drop area
834
870
  const dropGrid = listItem.shadowRoot.querySelector('.d2l-list-item-drag-drop-grid');
835
871
  if (!dropGrid) return;
@@ -845,6 +881,8 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
845
881
  this._currentTouchDropArea = movingOverElem;
846
882
  }
847
883
  }
884
+ // Delete all of the following code when removing 'GAUD-10642-improved-scroll-in-mobile'
885
+ if (this.#improvedMobileDragAndDropFlag) return;
848
886
  // scroll the viewport if we've reached the end
849
887
  if (touch.clientY > window.innerHeight / 2 && window.innerHeight - touch.clientY < scrollSensitivity) {
850
888
  // scroll down
@@ -862,6 +900,17 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
862
900
  // simulate dragstart for touch and hold
863
901
  this._touchTimeoutId = setTimeout(() => {
864
902
  this._touchStarted = true;
903
+ // leave the code inside the if when removing 'GAUD-10642-improved-mobile-drag-and-drop'
904
+ if (this.#improvedMobileDragAndDropFlag) {
905
+ // search for scrollable container
906
+ this.#scrollableContainer = getScrollableAncestor(this);
907
+ if (this.#scrollableContainer) {
908
+ // check if it has the touch-action style already
909
+ const touchAction = this.#scrollableContainer.style.getPropertyValue('touch-action');
910
+ if (touchAction && touchAction !== 'none') this.#touchAction = touchAction;
911
+ this.#scrollableContainer.style.setProperty('touch-action', 'none');
912
+ }
913
+ }
865
914
  if (this.shadowRoot)
866
915
  this.shadowRoot.querySelector('.d2l-list-item-drag-area').dispatchEvent(createDragEvent('dragstart'));
867
916
  }, touchHoldDuration);
@@ -933,6 +982,38 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
933
982
  return this._dropLocation === dropLocation.above ? html`<div class="d2l-list-item-drag-top-marker">${renderTemplate}</div>` : null;
934
983
  }
935
984
 
985
+ #doTouchScroll(touch) {
986
+ if (!touch || !this.#scrollableContainer) return;
987
+ if (this.#scrollableContainer === document.body) {
988
+ this.#handleViewPortTouchScrolling(touch);
989
+ } else {
990
+ this.#handleContainerTouchScrolling(touch);
991
+ }
992
+ }
993
+
994
+ #handleContainerTouchScrolling(touch) {
995
+ // scroll the viewport if we've reached the end
996
+ const rect = this.#scrollableContainer.getBoundingClientRect();
997
+ if (rect.bottom - touch.clientY < scrollContainerSensitivity) {
998
+ // scroll down
999
+ this.#scrollableContainer.scrollBy(0, scrollSpeed);
1000
+ } else if (touch.clientY - rect.top < scrollContainerSensitivity) {
1001
+ // scroll up
1002
+ this.#scrollableContainer.scrollBy(0, -scrollSpeed);
1003
+ }
1004
+ }
1005
+
1006
+ #handleViewPortTouchScrolling(touch) {
1007
+ const height = window.innerHeight;
1008
+ if (touch.clientY > height / 2 && height - touch.clientY < scrollSensitivity) {
1009
+ // scroll down
1010
+ window.scrollBy(0, scrollSpeed);
1011
+ } else if (touch.clientY < height / 2 && touch.clientY < scrollSensitivity) {
1012
+ // scroll up
1013
+ window.scrollBy(0, -scrollSpeed);
1014
+ }
1015
+ }
1016
+
936
1017
  #onDropEnterHelper(e, isTopHalf, isMiddle) {
937
1018
  if (this.dropDisabled) return;
938
1019
  e.dataTransfer.dropEffect = 'move';
@@ -954,4 +1035,16 @@ export const ListItemDragDropMixin = superclass => class extends superclass {
954
1035
  const dragState = getDragState();
955
1036
  dragState.setActiveDropTarget(this, location);
956
1037
  }
1038
+
1039
+ #resetScrollableContainer() {
1040
+ if (this.#scrollableContainer) {
1041
+ if (this.#touchAction) {
1042
+ this.#scrollableContainer.style.setProperty('touch-action', this.#touchAction);
1043
+ } else {
1044
+ this.#scrollableContainer.style.removeProperty('touch-action');
1045
+ }
1046
+ this.#touchAction = '';
1047
+ }
1048
+ this.#scrollableContainer = undefined;
1049
+ }
957
1050
  };
@@ -0,0 +1,111 @@
1
+ import { css, html, LitElement } from 'lit';
2
+ import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
3
+ import { ifDefined } from 'lit/directives/if-defined.js';
4
+ import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js';
5
+ import { SlottedIconMixin } from '../icons/slotted-icon-mixin.js';
6
+ import { ThemeMixin } from '../../mixins/theme/theme-mixin.js';
7
+ import { toolbarButtonStyles } from './toolbar-item-styles.js';
8
+ import { ToolbarItemMixin } from './toolbar-item-mixin.js';
9
+
10
+ /**
11
+ * A toolbar toggle button that performs an action and supports pressed and expanded states
12
+ */
13
+ class ToolbarButtonToggle extends SlottedIconMixin(FocusMixin(ToolbarItemMixin(ThemeMixin(PropertyRequiredMixin(LitElement))))) {
14
+
15
+ static properties = {
16
+ /**
17
+ * Disables the toolbar button
18
+ * @type {boolean}
19
+ */
20
+ disabled: { type: Boolean },
21
+ /**
22
+ * Expandable state
23
+ * @type {boolean}
24
+ */
25
+ expandable: { type: Boolean },
26
+ /**
27
+ * Expanded state
28
+ * @type {boolean}
29
+ */
30
+ expanded: { type: Boolean },
31
+ /**
32
+ * Pressed state
33
+ * @type {boolean}
34
+ */
35
+ pressed: { type: Boolean },
36
+ /**
37
+ * ACCESSIBILITY: REQUIRED: Accessible text for the button
38
+ * @type {string}
39
+ */
40
+ text: { type: String, required: true }
41
+ };
42
+
43
+ static styles = [super.styles, toolbarButtonStyles, css`
44
+ button[aria-pressed="true"] > .background {
45
+ background-color: var(--d2l-color-celestine-plus-2);
46
+ transform: scale(1, 1);
47
+ }
48
+ button[aria-pressed="true"]:hover > .background {
49
+ background-color: #dbf5ff; /* not a daylight color */
50
+ }
51
+ button[aria-pressed="true"] ::slotted(d2l-icon-custom),
52
+ button[aria-pressed="true"] d2l-icon {
53
+ fill: var(--d2l-color-celestine);
54
+ }
55
+ :host([theme="dark"]) button[aria-pressed="true"] > .background,
56
+ :host([theme="dark"]) button[aria-pressed="true"]:hover > .background {
57
+ background-color: var(--d2l-color-tungsten);
58
+ }
59
+ :host([theme="dark"]) button[aria-pressed="true"] ::slotted(d2l-icon-custom),
60
+ :host([theme="dark"]) button[aria-pressed="true"] d2l-icon {
61
+ fill: var(--d2l-color-regolith);
62
+ }
63
+ `];
64
+
65
+ constructor() {
66
+ super();
67
+ this.disabled = false;
68
+ this.expandable = false;
69
+ this.expanded = false;
70
+ this.pressed = false;
71
+ this.text = '';
72
+ }
73
+
74
+ static get focusElementSelector() {
75
+ return 'button';
76
+ }
77
+
78
+ render() {
79
+ return html`
80
+ <button
81
+ aria-disabled="${this.disabled ? 'true' : 'false'}"
82
+ aria-expanded="${ifDefined(this.expandable ? (this.expanded ? 'true' : 'false') : undefined)}"
83
+ aria-label="${this.text}"
84
+ aria-pressed="${ifDefined(!this.expandable ? (this.pressed ? 'true' : 'false') : undefined)}"
85
+ @click="${this.#handleClick}"
86
+ tabindex="${this._activeFocusable ? 0 : -1}"
87
+ title="${this.text}"
88
+ type="button">
89
+ <div class="background"></div>
90
+ ${this._renderIcon()}
91
+ </button>
92
+ `;
93
+ }
94
+
95
+ #handleClick(e) {
96
+ e.stopPropagation();
97
+ if (this.disabled) return;
98
+
99
+ if (this.expandable) {
100
+ this.expanded = !this.expanded;
101
+ } else {
102
+ this.pressed = !this.pressed;
103
+ }
104
+
105
+ /** Dispatched when the pressed state changes. */
106
+ this.dispatchEvent(new CustomEvent('d2l-toolbar-button-toggle-change'));
107
+ }
108
+
109
+ }
110
+
111
+ customElements.define('d2l-toolbar-button-toggle', ToolbarButtonToggle);
@@ -4,11 +4,12 @@ import { PropertyRequiredMixin } from '../../mixins/property-required/property-r
4
4
  import { SlottedIconMixin } from '../icons/slotted-icon-mixin.js';
5
5
  import { ThemeMixin } from '../../mixins/theme/theme-mixin.js';
6
6
  import { toolbarButtonStyles } from './toolbar-item-styles.js';
7
+ import { ToolbarItemMixin } from './toolbar-item-mixin.js';
7
8
 
8
9
  /**
9
10
  * A toolbar button that performs an action
10
11
  */
11
- class ToolbarButton extends SlottedIconMixin(FocusMixin(ThemeMixin(PropertyRequiredMixin(LitElement)))) {
12
+ class ToolbarButton extends SlottedIconMixin(FocusMixin(ToolbarItemMixin(ThemeMixin(PropertyRequiredMixin(LitElement))))) {
12
13
 
13
14
  static properties = {
14
15
  /**
@@ -20,8 +21,7 @@ class ToolbarButton extends SlottedIconMixin(FocusMixin(ThemeMixin(PropertyRequi
20
21
  * ACCESSIBILITY: REQUIRED: Accessible text for the button
21
22
  * @type {string}
22
23
  */
23
- text: { type: String, required: true },
24
- _activeFocusable: { state: true }
24
+ text: { type: String, required: true }
25
25
  };
26
26
 
27
27
  static styles = [super.styles, toolbarButtonStyles];
@@ -30,7 +30,6 @@ class ToolbarButton extends SlottedIconMixin(FocusMixin(ThemeMixin(PropertyRequi
30
30
  super();
31
31
  this.disabled = false;
32
32
  this.text = '';
33
- this._activeFocusable = false;
34
33
  }
35
34
 
36
35
  static get focusElementSelector() {
@@ -53,7 +52,11 @@ class ToolbarButton extends SlottedIconMixin(FocusMixin(ThemeMixin(PropertyRequi
53
52
  }
54
53
 
55
54
  #handleClick(e) {
56
- if (this.disabled) e.stopPropagation();
55
+ e.stopPropagation();
56
+ if (this.disabled) return;
57
+
58
+ /** Dispatched when the toggle is clicked. */
59
+ this.dispatchEvent(new CustomEvent('click'));
57
60
  }
58
61
 
59
62
  }
@@ -0,0 +1,16 @@
1
+ export const ToolbarItemMixin = superclass => class extends superclass {
2
+
3
+ static properties = {
4
+ _activeFocusable: { state: true }
5
+ };
6
+
7
+ constructor() {
8
+ super();
9
+ this._activeFocusable = false;
10
+ }
11
+
12
+ get isToolbarItem() {
13
+ return true;
14
+ }
15
+
16
+ };
@@ -81,7 +81,7 @@ class Toolbar extends PropertyRequiredMixin(LitElement) {
81
81
 
82
82
  #getFocusables() {
83
83
  return this.#slotRef.value?.assignedElements({ flatten: true })
84
- .filter(elem => elem.tagName === 'D2L-TOOLBAR-BUTTON') ?? [];
84
+ .filter(elem => elem.isToolbarItem) ?? [];
85
85
  }
86
86
 
87
87
  async #handleKeyDown(e) {
@@ -17365,6 +17365,106 @@
17365
17365
  }
17366
17366
  ]
17367
17367
  },
17368
+ {
17369
+ "name": "d2l-toolbar-button-toggle",
17370
+ "path": "./components/toolbar/toolbar-button-toggle.js",
17371
+ "description": "A toolbar toggle button that performs an action and supports pressed and expanded states",
17372
+ "attributes": [
17373
+ {
17374
+ "name": "disabled",
17375
+ "description": "Disables the toolbar button",
17376
+ "type": "boolean",
17377
+ "default": "false"
17378
+ },
17379
+ {
17380
+ "name": "expandable",
17381
+ "description": "Expandable state",
17382
+ "type": "boolean",
17383
+ "default": "false"
17384
+ },
17385
+ {
17386
+ "name": "expanded",
17387
+ "description": "Expanded state",
17388
+ "type": "boolean",
17389
+ "default": "false"
17390
+ },
17391
+ {
17392
+ "name": "pressed",
17393
+ "description": "Pressed state",
17394
+ "type": "boolean",
17395
+ "default": "false"
17396
+ },
17397
+ {
17398
+ "name": "text",
17399
+ "description": "ACCESSIBILITY: REQUIRED: Accessible text for the button",
17400
+ "type": "string",
17401
+ "default": "\"\""
17402
+ },
17403
+ {
17404
+ "name": "icon",
17405
+ "description": "Preset icon key (e.g. \"tier1:gear\")",
17406
+ "type": "string"
17407
+ }
17408
+ ],
17409
+ "properties": [
17410
+ {
17411
+ "name": "styles",
17412
+ "type": "array",
17413
+ "default": "[[null],\"toolbarButtonStyles\",null]"
17414
+ },
17415
+ {
17416
+ "name": "disabled",
17417
+ "attribute": "disabled",
17418
+ "description": "Disables the toolbar button",
17419
+ "type": "boolean",
17420
+ "default": "false"
17421
+ },
17422
+ {
17423
+ "name": "expandable",
17424
+ "attribute": "expandable",
17425
+ "description": "Expandable state",
17426
+ "type": "boolean",
17427
+ "default": "false"
17428
+ },
17429
+ {
17430
+ "name": "expanded",
17431
+ "attribute": "expanded",
17432
+ "description": "Expanded state",
17433
+ "type": "boolean",
17434
+ "default": "false"
17435
+ },
17436
+ {
17437
+ "name": "pressed",
17438
+ "attribute": "pressed",
17439
+ "description": "Pressed state",
17440
+ "type": "boolean",
17441
+ "default": "false"
17442
+ },
17443
+ {
17444
+ "name": "text",
17445
+ "attribute": "text",
17446
+ "description": "ACCESSIBILITY: REQUIRED: Accessible text for the button",
17447
+ "type": "string",
17448
+ "default": "\"\""
17449
+ },
17450
+ {
17451
+ "name": "icon",
17452
+ "attribute": "icon",
17453
+ "description": "Preset icon key (e.g. \"tier1:gear\")",
17454
+ "type": "string"
17455
+ },
17456
+ {
17457
+ "name": "isToolbarItem",
17458
+ "type": "boolean"
17459
+ }
17460
+ ],
17461
+ "events": [
17462
+ {
17463
+ "name": "d2l-toolbar-button-toggle-change",
17464
+ "description": "Dispatched when the pressed state changes."
17465
+ }
17466
+ ]
17467
+ },
17368
17468
  {
17369
17469
  "name": "d2l-toolbar-button",
17370
17470
  "path": "./components/toolbar/toolbar-button.js",
@@ -17413,6 +17513,16 @@
17413
17513
  "attribute": "icon",
17414
17514
  "description": "Preset icon key (e.g. \"tier1:gear\")",
17415
17515
  "type": "string"
17516
+ },
17517
+ {
17518
+ "name": "isToolbarItem",
17519
+ "type": "boolean"
17520
+ }
17521
+ ],
17522
+ "events": [
17523
+ {
17524
+ "name": "click",
17525
+ "description": "Dispatched when the toggle is clicked."
17416
17526
  }
17417
17527
  ]
17418
17528
  },
package/helpers/dom.js CHANGED
@@ -57,6 +57,19 @@ export function findComposedAncestor(node, predicate) {
57
57
  return null;
58
58
  }
59
59
 
60
+ export function getScrollableAncestor(node) {
61
+ return findComposedAncestor(node, (el) => {
62
+ if (el.nodeType !== Node.ELEMENT_NODE) return false;
63
+ if (el === document.body) return true;
64
+ const styles = getComputedStyle(el);
65
+ const overflowY = styles.getPropertyValue('overflow-y');
66
+ const overflowX = styles.getPropertyValue('overflow-x');
67
+ const overflow = styles.getPropertyValue('overflow');
68
+ const overflowValues = ['auto', 'scroll'];
69
+ return (overflowValues.includes(overflowY) || overflowValues.includes(overflowX) || overflowValues.includes(overflow));
70
+ });
71
+ }
72
+
60
73
  export function getBoundingAncestor(node) {
61
74
  return findComposedAncestor(node, (node) => {
62
75
  if (node === document.body) return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.323.0",
3
+ "version": "3.325.0",
4
4
  "description": "A collection of accessible, free, open-source web components for building Brightspace applications",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/BrightspaceUI/core.git",