playbook_ui 16.11.0.pre.rc.2 → 16.11.0.pre.rc.3

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_draggable/_draggable.scss +3 -0
  3. data/app/pb_kits/playbook/pb_draggable/subcomponents/DraggableItem.tsx +53 -29
  4. data/app/pb_kits/playbook/pb_draggable/touchDrag.test.js +46 -0
  5. data/app/pb_kits/playbook/pb_draggable/utilities/touchDrag.ts +272 -54
  6. data/app/pb_kits/playbook/pb_nav/_item.tsx +3 -2
  7. data/app/pb_kits/playbook/pb_nav/_nav.tsx +1 -1
  8. data/app/pb_kits/playbook/pb_nav/docs/_playground.json +117 -213
  9. data/app/pb_kits/playbook/pb_nav/docs/_playground.overrides.json +127 -319
  10. data/app/pb_kits/playbook/pb_nav/kit.schema.json +2 -1
  11. data/app/pb_kits/playbook/pb_nav/navTypes.ts +1 -1
  12. data/app/pb_kits/playbook/pb_rich_text_editor/_rich_text_editor.scss +1 -1
  13. data/app/pb_kits/playbook/pb_rich_text_editor/_tiptap_styles.scss +2 -2
  14. data/app/pb_kits/playbook/pb_typeahead/_typeahead.scss +45 -0
  15. data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +144 -0
  16. data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +25 -1
  17. data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +29 -85
  18. data/app/pb_kits/playbook/pb_typeahead/components/PillRenderer.tsx +187 -0
  19. data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +236 -10
  20. data/app/pb_kits/playbook/pb_typeahead/docs/_playground.json +8 -2
  21. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.html.erb +39 -0
  22. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.jsx +63 -0
  23. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills.md +7 -0
  24. data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_draggable_pills_rails.md +7 -0
  25. data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
  26. data/app/pb_kits/playbook/pb_typeahead/docs/index.js +1 -0
  27. data/app/pb_kits/playbook/pb_typeahead/kit.schema.json +24 -0
  28. data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +9 -0
  29. data/dist/chunks/{_typeahead-COksV0jV.js → _typeahead-DjVWQJpA.js} +2 -2
  30. data/dist/chunks/vendor.js +1 -1
  31. data/dist/playbook-rails-react-bindings.js +1 -1
  32. data/dist/playbook-rails.js +1 -1
  33. data/dist/playbook.css +1 -1
  34. data/lib/playbook/version.rb +1 -1
  35. metadata +8 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3240b5088da697dc630863038b107d3afb359c41a2a7fb782c538c69b58e7f84
4
- data.tar.gz: 2935c4cbffb05815b2a60e1d1d365f06b166a560a88472d38acaaf0f4d63fd9f
3
+ metadata.gz: 7ac0205b50ee49eadbe6eace5e060cf97a7cb52ad2187adfd0f367a69de229d7
4
+ data.tar.gz: af1a7209ac89983dd1aebf1fb746f119e7251301417f9c857319e7ef09a28f72
5
5
  SHA512:
6
- metadata.gz: f8ff49b8d4936b46baaeae064f01de7d4a5e78b32579c80e647fb453f37e30ecd03d1f321e29795e89d21b78cddb6e34bd5eaa185b7b30156801976968a7f96b
7
- data.tar.gz: 1da4e486c2dd4509bfac64af064bea02fce52baeeb904adc11c9f5dd37ddc06d216b1ad5217525a4de6c975128c07ebf69d11e32e20c9698c9da49c9b8fb5bc8
6
+ metadata.gz: cf7787a91d5395c55f3b86afc10a59ab765cc90d005a536672d75842d989ace47bb840f35bf783ba6765f00d71e3111f4fd85dab554a3194b00a7a031ddd8953
7
+ data.tar.gz: 004b1e4061a164048d880c73918a9292f43588c1e48656c667ae68878faade0114d588b69a033816b676dbfca704a947edcd03c3b175405a6bc526933a75ae2e
@@ -9,6 +9,9 @@
9
9
  }
10
10
 
11
11
  &.is_touch_active {
12
+ user-select: none;
13
+ -webkit-user-select: none;
14
+
12
15
  .pb_draggable_handle,
13
16
  .card_draggable_handle {
14
17
  color: $text_lt_default;
@@ -9,7 +9,7 @@ import {
9
9
  import { globalProps } from "../../utilities/globalProps";
10
10
  import { DraggableContext } from "../context";
11
11
  import { noop } from '../../utilities/object'
12
- import { bindTouchDrag, isTouchDragDevice } from "../utilities/touchDrag";
12
+ import { bindMousePointerDrag, bindTouchDrag, isTouchDragDevice } from "../utilities/touchDrag";
13
13
 
14
14
  type DraggableItemProps = {
15
15
  aria?: { [key: string]: string };
@@ -27,6 +27,10 @@ type DraggableItemProps = {
27
27
  onDragStart?: () => void,
28
28
  onDrop?: () => void,
29
29
  dragId?: string;
30
+ /** Whole-item pointer drag (e.g. Typeahead pills without a grip). Disables HTML5 drag. */
31
+ pointerDrag?: boolean;
32
+ /** Handle-only pointer drag (e.g. Typeahead pills with a grip). Disables HTML5 drag. */
33
+ handlePointerDrag?: boolean;
30
34
  tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div' | 'tr' | 'th' | 'td' | 'thead' | 'col' | 'tbody',
31
35
  };
32
36
 
@@ -48,6 +52,8 @@ const DraggableItem = (props: DraggableItemProps) => {
48
52
  onDragOver = noop,
49
53
  onDragStart = noop,
50
54
  onDrop = noop,
55
+ pointerDrag = false,
56
+ handlePointerDrag = false,
51
57
  } = props;
52
58
 
53
59
  const {
@@ -59,11 +65,11 @@ const DraggableItem = (props: DraggableItemProps) => {
59
65
  handleDragOver,
60
66
  dropZone = 'ghost',
61
67
  dropZoneColor = 'neutral',
62
- direction = 'horizontal'
63
68
  } = DraggableContext();
64
69
 
65
70
  const itemRef = React.useRef<HTMLElement>(null);
66
71
  const [useTouchDrag, setUseTouchDrag] = React.useState(false);
72
+ const [usePointerMouseDrag, setUsePointerMouseDrag] = React.useState(false);
67
73
  const handlersRef = React.useRef({
68
74
  handleDragStart,
69
75
  handleDragEnter,
@@ -90,9 +96,36 @@ const DraggableItem = (props: DraggableItemProps) => {
90
96
  onDragEnd,
91
97
  };
92
98
 
93
- React.useEffect(() => {
94
- setUseTouchDrag(isTouchDragDevice());
95
- }, []);
99
+ const dragHandlers = React.useMemo(() => ({
100
+ onDragStart: (id: string, itemContainer?: string) => {
101
+ handlersRef.current.handleDragStart(id, itemContainer);
102
+ handlersRef.current.onDragStart();
103
+ },
104
+ onDragEnter: (targetDragId: string, targetContainer?: string) => {
105
+ handlersRef.current.handleDragEnter(targetDragId, targetContainer);
106
+ handlersRef.current.onDragEnter();
107
+ },
108
+ onDragOver: (event: Event, targetContainer?: string) => {
109
+ handlersRef.current.handleDragOver(event, targetContainer);
110
+ handlersRef.current.onDragOver();
111
+ },
112
+ onDrop: (dropContainer?: string) => {
113
+ handlersRef.current.handleDrop(dropContainer);
114
+ handlersRef.current.onDrop();
115
+ },
116
+ onDragEnd: () => {
117
+ handlersRef.current.handleDragEnd();
118
+ handlersRef.current.onDragEnd();
119
+ },
120
+ }), []);
121
+
122
+ React.useLayoutEffect(() => {
123
+ const touchMode = isTouchDragDevice();
124
+
125
+ setUseTouchDrag(touchMode);
126
+ // Pointer drag is opt-in only — regular Draggable items keep HTML5 drag on the whole item.
127
+ setUsePointerMouseDrag(!touchMode && (pointerDrag || handlePointerDrag));
128
+ }, [dragId, container, children, pointerDrag, handlePointerDrag]);
96
129
 
97
130
  React.useEffect(() => {
98
131
  if (!useTouchDrag || !itemRef.current || !dragId) return;
@@ -101,30 +134,21 @@ const DraggableItem = (props: DraggableItemProps) => {
101
134
  dragId,
102
135
  container,
103
136
  itemElement: itemRef.current,
104
- handlers: {
105
- onDragStart: (id, itemContainer) => {
106
- handlersRef.current.handleDragStart(id, itemContainer);
107
- handlersRef.current.onDragStart();
108
- },
109
- onDragEnter: (targetDragId, targetContainer) => {
110
- handlersRef.current.handleDragEnter(targetDragId, targetContainer);
111
- handlersRef.current.onDragEnter();
112
- },
113
- onDragOver: (event, targetContainer) => {
114
- handlersRef.current.handleDragOver(event, targetContainer);
115
- handlersRef.current.onDragOver();
116
- },
117
- onDrop: (dropContainer) => {
118
- handlersRef.current.handleDrop(dropContainer);
119
- handlersRef.current.onDrop();
120
- },
121
- onDragEnd: () => {
122
- handlersRef.current.handleDragEnd();
123
- handlersRef.current.onDragEnd();
124
- },
125
- },
137
+ handlers: dragHandlers,
138
+ });
139
+ }, [useTouchDrag, dragId, container, dragHandlers]);
140
+
141
+ React.useEffect(() => {
142
+ if (!usePointerMouseDrag || !itemRef.current || !dragId) return;
143
+
144
+ return bindMousePointerDrag({
145
+ dragId,
146
+ container,
147
+ itemElement: itemRef.current,
148
+ handlers: dragHandlers,
149
+ allowWholeItem: pointerDrag,
126
150
  });
127
- }, [useTouchDrag, dragId, container]);
151
+ }, [usePointerMouseDrag, dragId, container, dragHandlers, pointerDrag]);
128
152
 
129
153
  const ariaProps = buildAriaProps(aria);
130
154
  const dataProps = buildDataProps(data);
@@ -188,7 +212,7 @@ const DraggableItem = (props: DraggableItemProps) => {
188
212
  {...htmlProps}
189
213
  className={classes}
190
214
  data-pb-drag-id={dragId}
191
- draggable={!useTouchDrag}
215
+ draggable={!useTouchDrag && !usePointerMouseDrag}
192
216
  id={id}
193
217
  key={dragId}
194
218
  onDrag={onDrag}
@@ -35,4 +35,50 @@ describe("touchDrag utilities", () => {
35
35
  const item = document.querySelector(".pb_draggable_item");
36
36
  expect(getContainerFromElement(item)).toBe("To Do");
37
37
  });
38
+
39
+ test("resolvePointerDragTarget uses sibling slots when pointer stays on dragged pill", () => {
40
+ document.body.innerHTML = `
41
+ <div class="pb_draggable_container" data-pb-drag-container="typeahead-pills">
42
+ <div class="pb_draggable_item" data-pb-drag-id="b"></div>
43
+ <div class="pb_draggable_item" data-pb-drag-id="c"></div>
44
+ <div class="pb_draggable_item" data-pb-drag-id="a"></div>
45
+ </div>
46
+ `;
47
+
48
+ const rects = {
49
+ b: { left: 0, width: 100 },
50
+ c: { left: 100, width: 100 },
51
+ a: { left: 200, width: 100 },
52
+ };
53
+
54
+ document.querySelectorAll(".pb_draggable_item").forEach((item) => {
55
+ const id = item.getAttribute("data-pb-drag-id");
56
+ item.getBoundingClientRect = () => ({
57
+ left: rects[id].left,
58
+ width: rects[id].width,
59
+ right: rects[id].left + rects[id].width,
60
+ top: 0,
61
+ bottom: 20,
62
+ height: 20,
63
+ x: rects[id].left,
64
+ y: 0,
65
+ toJSON: () => ({}),
66
+ });
67
+ });
68
+
69
+ const { resolvePointerDragTarget } = require("./utilities/touchDrag");
70
+
71
+ document.elementFromPoint = () => document.querySelector('[data-pb-drag-id="a"]');
72
+ document.elementsFromPoint = () => [document.querySelector('[data-pb-drag-id="a"]')];
73
+
74
+ expect(resolvePointerDragTarget(250, 10, "a")).toEqual({
75
+ targetDragId: null,
76
+ targetContainer: "typeahead-pills",
77
+ });
78
+
79
+ expect(resolvePointerDragTarget(120, 10, "a")).toEqual({
80
+ targetDragId: "c",
81
+ targetContainer: "typeahead-pills",
82
+ });
83
+ });
38
84
  });
@@ -5,12 +5,14 @@ const DRAG_THRESHOLD_PX = 5;
5
5
  export const isTouchDragDevice = (): boolean => {
6
6
  if (typeof window === 'undefined') return false;
7
7
 
8
- const hasTouch = 'ontouchstart' in window;
8
+ // Only treat true touch-primary devices as touch-drag targets.
9
+ // `ontouchstart` alone is true on many desktops (e.g. macOS trackpads) and
10
+ // must not disable HTML5 mouse dragging.
9
11
  const hasCoarsePointer =
10
12
  typeof window.matchMedia === 'function' &&
11
13
  window.matchMedia('(hover: none) and (pointer: coarse)').matches;
12
14
 
13
- return hasTouch || hasCoarsePointer;
15
+ return hasCoarsePointer;
14
16
  };
15
17
 
16
18
  export const getDragIdFromElement = (element: Element | null): string | null => {
@@ -27,6 +29,99 @@ export const getContainerFromElement = (element: Element | null): string | undef
27
29
  return container.getAttribute('data-pb-drag-container') || undefined;
28
30
  };
29
31
 
32
+ type SortedDragItem = {
33
+ id: string;
34
+ rect: DOMRect;
35
+ };
36
+
37
+ const getSortedDragItems = (containerElement: Element): SortedDragItem[] => (
38
+ Array.from(containerElement.querySelectorAll('.pb_draggable_item[data-pb-drag-id]'))
39
+ .map((element) => ({
40
+ id: element.getAttribute('data-pb-drag-id') as string,
41
+ rect: element.getBoundingClientRect(),
42
+ }))
43
+ .sort((a, b) => a.rect.left - b.rect.left)
44
+ );
45
+
46
+ const getDragIdFromElementsAtPoint = (
47
+ clientX: number,
48
+ clientY: number,
49
+ dragId: string,
50
+ ): string | null => {
51
+ if (typeof document.elementsFromPoint !== 'function') return null;
52
+
53
+ return document.elementsFromPoint(clientX, clientY)
54
+ .map((element) => getDragIdFromElement(element))
55
+ .find((id) => id && id !== dragId) ?? null;
56
+ };
57
+
58
+ // When the pointer stays on the dragged item, use sibling midpoints (not the
59
+ // dragged pill's own rect) to decide which slot the cursor is in.
60
+ const getTargetWhenPointerOnDraggedItem = (
61
+ containerElement: Element,
62
+ clientX: number,
63
+ dragId: string,
64
+ ): string | null => {
65
+ const items = getSortedDragItems(containerElement);
66
+ const currentIndex = items.findIndex((item) => item.id === dragId);
67
+ if (currentIndex === -1) return null;
68
+
69
+ const others = items.filter((item) => item.id !== dragId);
70
+ if (!others.length) return null;
71
+
72
+ let slot = 0;
73
+ others.forEach((item, index) => {
74
+ const midpoint = item.rect.left + item.rect.width / 2;
75
+ if (clientX > midpoint) {
76
+ slot = index + 1;
77
+ }
78
+ });
79
+
80
+ const desiredIndex = Math.min(slot, items.length - 1);
81
+ if (desiredIndex === currentIndex) return null;
82
+
83
+ const targetItem = items[desiredIndex];
84
+ if (!targetItem || targetItem.id === dragId) return null;
85
+
86
+ return targetItem.id;
87
+ };
88
+
89
+ export const resolvePointerDragTarget = (
90
+ clientX: number,
91
+ clientY: number,
92
+ dragId: string,
93
+ ): { targetDragId: string | null; targetContainer: string | undefined } => {
94
+ const elementBelow = typeof document.elementFromPoint === 'function'
95
+ ? document.elementFromPoint(clientX, clientY)
96
+ : null;
97
+ const hitId = getDragIdFromElement(elementBelow);
98
+
99
+ if (hitId && hitId !== dragId) {
100
+ return {
101
+ targetDragId: hitId,
102
+ targetContainer: getContainerFromElement(elementBelow),
103
+ };
104
+ }
105
+
106
+ const stackId = getDragIdFromElementsAtPoint(clientX, clientY, dragId);
107
+ if (stackId) {
108
+ return {
109
+ targetDragId: stackId,
110
+ targetContainer: getContainerFromElement(elementBelow),
111
+ };
112
+ }
113
+
114
+ const containerElement = elementBelow?.closest('.pb_draggable_container');
115
+ if (!containerElement) {
116
+ return { targetDragId: null, targetContainer: undefined };
117
+ }
118
+
119
+ return {
120
+ targetDragId: getTargetWhenPointerOnDraggedItem(containerElement, clientX, dragId),
121
+ targetContainer: getContainerFromElement(containerElement),
122
+ };
123
+ };
124
+
30
125
  type TouchDragHandlers = {
31
126
  onDragStart: (dragId: string, container?: string) => void;
32
127
  onDragEnter: (targetDragId: string, container?: string) => void;
@@ -42,7 +137,7 @@ type TouchDragOptions = {
42
137
  handlers: TouchDragHandlers;
43
138
  };
44
139
 
45
- type TouchDragState = {
140
+ type DragPointerState = {
46
141
  active: boolean;
47
142
  dragging: boolean;
48
143
  startX: number;
@@ -55,18 +150,85 @@ const createPreventableEvent = (): Event => ({
55
150
  preventDefault: () => undefined,
56
151
  } as Event);
57
152
 
153
+ const runDragPointerMove = (
154
+ state: DragPointerState,
155
+ clientX: number,
156
+ clientY: number,
157
+ dragId: string,
158
+ container: string | undefined,
159
+ handlers: TouchDragHandlers,
160
+ ) => {
161
+ if (!state.dragging) {
162
+ const deltaX = clientX - state.startX;
163
+ const deltaY = clientY - state.startY;
164
+
165
+ if (Math.hypot(deltaX, deltaY) < DRAG_THRESHOLD_PX) return false;
166
+
167
+ state.dragging = true;
168
+ handlers.onDragStart(dragId, container);
169
+ }
170
+
171
+ const { targetDragId, targetContainer } = resolvePointerDragTarget(clientX, clientY, dragId);
172
+
173
+ if (!targetDragId || targetDragId === dragId) {
174
+ state.lastTargetDragId = null;
175
+ } else if (targetDragId !== state.lastTargetDragId) {
176
+ state.lastTargetDragId = targetDragId;
177
+ handlers.onDragEnter(targetDragId, targetContainer ?? container);
178
+ }
179
+
180
+ if (targetContainer && targetContainer !== state.lastContainer) {
181
+ state.lastContainer = targetContainer;
182
+ handlers.onDragOver(createPreventableEvent(), targetContainer);
183
+ } else if (targetContainer) {
184
+ handlers.onDragOver(createPreventableEvent(), targetContainer);
185
+ }
186
+
187
+ return true;
188
+ };
189
+
190
+ const finishDragPointer = (
191
+ state: DragPointerState,
192
+ clientX: number | null,
193
+ clientY: number | null,
194
+ container: string | undefined,
195
+ handlers: TouchDragHandlers,
196
+ itemElement: HTMLElement,
197
+ ) => {
198
+ if (!state.active) return;
199
+
200
+ if (state.dragging && clientX !== null && clientY !== null) {
201
+ const elementBelow = document.elementFromPoint(clientX, clientY);
202
+ const dropContainer = getContainerFromElement(elementBelow) ?? container;
203
+
204
+ if (dropContainer) {
205
+ handlers.onDrop(dropContainer);
206
+ }
207
+
208
+ handlers.onDragEnd();
209
+ }
210
+
211
+ state.active = false;
212
+ state.dragging = false;
213
+ state.lastTargetDragId = null;
214
+ state.lastContainer = undefined;
215
+ itemElement.classList.remove('is_touch_active');
216
+ };
217
+
58
218
  export const bindTouchDrag = ({
59
219
  dragId,
60
220
  container,
61
221
  itemElement,
62
222
  handlers,
63
223
  }: TouchDragOptions): (() => void) => {
64
- if (!isTouchDragDevice()) return () => undefined;
224
+ if (!isTouchDragDevice()) {
225
+ return () => undefined;
226
+ }
65
227
 
66
228
  const handle = itemElement.querySelector(DRAG_HANDLE_SELECTOR) as HTMLElement | null;
67
229
  const dragTarget = handle || itemElement;
68
230
 
69
- const state: TouchDragState = {
231
+ const state: DragPointerState = {
70
232
  active: false,
71
233
  dragging: false,
72
234
  startX: 0,
@@ -75,16 +237,10 @@ export const bindTouchDrag = ({
75
237
  lastContainer: undefined,
76
238
  };
77
239
 
78
- const resetState = () => {
79
- state.active = false;
80
- state.dragging = false;
81
- state.lastTargetDragId = null;
82
- state.lastContainer = undefined;
83
- itemElement.classList.remove('is_touch_active');
84
- };
85
-
86
240
  const handleTouchStart = (event: TouchEvent) => {
87
- if (handle && !handle.contains(event.target as Node)) return;
241
+ if (handle && !handle.contains(event.target as Node)) {
242
+ return;
243
+ }
88
244
 
89
245
  const touch = event.touches[0];
90
246
  if (!touch) return;
@@ -101,50 +257,20 @@ export const bindTouchDrag = ({
101
257
  const touch = event.touches[0];
102
258
  if (!touch) return;
103
259
 
104
- if (!state.dragging) {
105
- const deltaX = touch.clientX - state.startX;
106
- const deltaY = touch.clientY - state.startY;
107
-
108
- if (Math.hypot(deltaX, deltaY) < DRAG_THRESHOLD_PX) return;
109
-
110
- state.dragging = true;
111
- handlers.onDragStart(dragId, container);
112
- }
113
-
114
- event.preventDefault();
115
-
116
- const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
117
- const targetDragId = getDragIdFromElement(elementBelow);
118
- const targetContainer = getContainerFromElement(elementBelow);
119
-
120
- if (targetDragId && targetDragId !== dragId && targetDragId !== state.lastTargetDragId) {
121
- state.lastTargetDragId = targetDragId;
122
- handlers.onDragEnter(targetDragId, targetContainer ?? container);
123
- }
124
-
125
- if (targetContainer && targetContainer !== state.lastContainer) {
126
- state.lastContainer = targetContainer;
127
- handlers.onDragOver(createPreventableEvent(), targetContainer);
128
- } else if (targetContainer) {
129
- handlers.onDragOver(createPreventableEvent(), targetContainer);
260
+ if (runDragPointerMove(state, touch.clientX, touch.clientY, dragId, container, handlers)) {
261
+ event.preventDefault();
130
262
  }
131
263
  };
132
264
 
133
265
  const finishDrag = (touch: Touch | null) => {
134
- if (!state.active) return;
135
-
136
- if (state.dragging && touch) {
137
- const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
138
- const dropContainer = getContainerFromElement(elementBelow) ?? container;
139
-
140
- if (dropContainer) {
141
- handlers.onDrop(dropContainer);
142
- }
143
-
144
- handlers.onDragEnd();
145
- }
146
-
147
- resetState();
266
+ finishDragPointer(
267
+ state,
268
+ touch?.clientX ?? null,
269
+ touch?.clientY ?? null,
270
+ container,
271
+ handlers,
272
+ itemElement,
273
+ );
148
274
  };
149
275
 
150
276
  const handleTouchEnd = (event: TouchEvent) => {
@@ -171,3 +297,95 @@ export const bindTouchDrag = ({
171
297
  dragTarget.removeEventListener('touchcancel', handleTouchCancel);
172
298
  };
173
299
  };
300
+
301
+ const DRAG_INTERACTION_EXCLUDE_SELECTOR = '.pb_form_pill_close';
302
+
303
+ const isHandleEventTarget = (target: EventTarget | null): boolean => {
304
+ return Boolean((target as Element | null)?.closest(DRAG_HANDLE_SELECTOR));
305
+ };
306
+
307
+ const isExcludedDragTarget = (target: EventTarget | null): boolean => {
308
+ return Boolean((target as Element | null)?.closest(DRAG_INTERACTION_EXCLUDE_SELECTOR));
309
+ };
310
+
311
+ type MousePointerDragOptions = TouchDragOptions & {
312
+ allowWholeItem?: boolean;
313
+ };
314
+
315
+ export const bindMousePointerDrag = ({
316
+ dragId,
317
+ container,
318
+ itemElement,
319
+ handlers,
320
+ allowWholeItem = false,
321
+ }: MousePointerDragOptions): (() => void) => {
322
+ const hasHandle = Boolean(itemElement.querySelector(DRAG_HANDLE_SELECTOR));
323
+
324
+ if (!hasHandle && !allowWholeItem) {
325
+ return () => undefined;
326
+ }
327
+
328
+ const state: DragPointerState = {
329
+ active: false,
330
+ dragging: false,
331
+ startX: 0,
332
+ startY: 0,
333
+ lastTargetDragId: null,
334
+ lastContainer: undefined,
335
+ };
336
+
337
+ const handleMouseMove = (event: MouseEvent) => {
338
+ if (!state.active) return;
339
+
340
+ if (runDragPointerMove(state, event.clientX, event.clientY, dragId, container, handlers)) {
341
+ event.preventDefault();
342
+ }
343
+ };
344
+
345
+ const handleMouseUp = (event: MouseEvent) => {
346
+ document.removeEventListener('mousemove', handleMouseMove);
347
+ document.removeEventListener('mouseup', handleMouseUp);
348
+
349
+ finishDragPointer(
350
+ state,
351
+ event.clientX,
352
+ event.clientY,
353
+ container,
354
+ handlers,
355
+ itemElement,
356
+ );
357
+ };
358
+
359
+ const handleMouseDown = (event: MouseEvent) => {
360
+ if (event.button !== 0) return;
361
+ if (isExcludedDragTarget(event.target)) return;
362
+
363
+ if (hasHandle) {
364
+ if (!isHandleEventTarget(event.target)) return;
365
+ } else if (!itemElement.contains(event.target as Node)) {
366
+ return;
367
+ }
368
+
369
+ // Pointer drag only — HTML5 drag is disabled on these items. preventDefault stops text selection.
370
+ event.preventDefault();
371
+ event.stopPropagation();
372
+
373
+ state.active = true;
374
+ state.startX = event.clientX;
375
+ state.startY = event.clientY;
376
+ itemElement.classList.add('is_touch_active');
377
+ document.addEventListener('mousemove', handleMouseMove);
378
+ document.addEventListener('mouseup', handleMouseUp);
379
+ };
380
+
381
+ // Delegate on the stable draggable item element so re-rendered handles stay wired
382
+ itemElement.addEventListener('mousedown', handleMouseDown);
383
+
384
+ return () => {
385
+ itemElement.removeEventListener('mousedown', handleMouseDown);
386
+ document.removeEventListener('mousemove', handleMouseMove);
387
+ document.removeEventListener('mouseup', handleMouseUp);
388
+ };
389
+ };
390
+
391
+ export const bindMouseHandleDrag = bindMousePointerDrag;
@@ -7,7 +7,7 @@ import { globalProps, GlobalProps } from "../utilities/globalProps";
7
7
  import Icon from "../pb_icon/_icon";
8
8
  import Image from "../pb_image/_image";
9
9
  import Collapsible from "../pb_collapsible/_collapsible";
10
- import { NavChildProps } from "./navTypes";
10
+ import { NavChildProps, SpacingObject } from "./navTypes";
11
11
  import { Spacing } from "../types";
12
12
 
13
13
  type NavItemProps = {
@@ -35,7 +35,7 @@ type NavItemProps = {
35
35
  collapsibleTrail?: boolean;
36
36
  collapsed?: boolean;
37
37
  orientation?: "vertical" | "horizontal";
38
- variant?: "normal" | "subtle";
38
+ variant?: "normal" | "subtle" | "bold";
39
39
  margin?: Spacing;
40
40
  marginBottom?: Spacing;
41
41
  marginTop?: Spacing;
@@ -44,6 +44,7 @@ type NavItemProps = {
44
44
  marginX?: Spacing;
45
45
  marginY?: Spacing;
46
46
  disabled?: boolean;
47
+ itemSpacing?: SpacingObject;
47
48
  } & GlobalProps;
48
49
 
49
50
  const NavItem = (props: NavItemProps) => {
@@ -22,7 +22,7 @@ type NavProps = {
22
22
  orientation?: "vertical" | "horizontal",
23
23
  link?: string,
24
24
  title?: string,
25
- variant?: "normal" | "subtle",
25
+ variant?: "normal" | "subtle" | "bold",
26
26
  itemSpacing?: SpacingObject
27
27
  } & GlobalProps
28
28