Skip to content

Commit a31087a

Browse files
committed
Restyle the batch badge on Primer's Counter
Reuses the Counter contract at the CSS level instead of inline styles, so the badge is stylesheet-targetable like everything else on the page rather than carrying its own one-off skin. Renders it with lit-html, matching this codebase's existing precedent for building preview markup outside Angular's own render tree. Firefox folds a card's ink and overflow into its native drag-image snapshot, shifting the snapshot origin off the border box and misaligning the grab offset. The badge gets an inset, shadowless variant there, following the same fork already established for the card preview's own lift shadow. https://community.openproject.org/wp/AGILE-278
1 parent 454f233 commit a31087a

3 files changed

Lines changed: 97 additions & 32 deletions

File tree

frontend/src/global_styles/content/drag_and_drop.sass

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,35 @@
4545

4646
&:active
4747
cursor: grabbing
48+
49+
// Multi-card batch count badge on a sortable-lists drag preview. Renders on
50+
// Primer's Counter contract (see PrimerCounterComponent) so its skin is
51+
// stylesheet-targetable rather than inline; this class only carries the
52+
// positioning Counter itself doesn't own.
53+
.op-sortable-lists-drag-preview-batch-badge
54+
position: absolute
55+
top: -8px
56+
right: -8px
57+
min-width: 20px
58+
height: 20px
59+
padding: 0 6px
60+
border-radius: 999px
61+
background-color: var(--bgColor-emphasis)
62+
color: var(--fgColor-onEmphasis)
63+
font-size: 12px
64+
font-weight: 600
65+
line-height: 20px
66+
text-align: center
67+
box-shadow: var(--shadow-floating-medium)
68+
69+
// Firefox folds a badge's box-shadow (and its overhang past the card's
70+
// border box) into the native drag-image snapshot, shifting the snapshot
71+
// origin off the border box so the grab offset no longer lines up under
72+
// the pointer — the same failure mode as the .Box-card[data-preview] lift
73+
// shadow in border_box_list_component.sass. Inset the badge fully inside
74+
// the border box and drop the shadow on Firefox only; other browsers
75+
// snapshot the border box and are unaffected.
76+
.-browser-firefox &
77+
top: 4px
78+
right: 4px
79+
box-shadow: none

frontend/src/stimulus/controllers/dynamic/sortable-lists/preview.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,44 @@ describe('sortable lists drag preview', () => {
208208
expect(badge?.textContent).toEqual('3');
209209
expect(container.contains(badge)).toBe(true);
210210
});
211+
212+
// Styling now lives entirely in sass, targeted through the Primer
213+
// Counter contract plus this file's own class — no inline styles left
214+
// to assert on.
215+
it('carries the Primer Counter classes and the batch-badge class', () => {
216+
const target = withWidth(previewTarget(), 320);
217+
const container = document.createElement('div');
218+
219+
renderDragPreview({
220+
previewTarget: target, sourceElement: target, container, batchSize: 3,
221+
});
222+
223+
const badge = container.querySelector(badgeSelector);
224+
225+
expect(badge?.classList.contains('Counter')).toBe(true);
226+
expect(badge?.classList.contains('Counter--primary')).toBe(true);
227+
expect(badge?.classList.contains('op-sortable-lists-drag-preview-batch-badge')).toBe(true);
228+
});
229+
230+
it('anchors the badge to the container without disturbing the already-appended preview clone', () => {
231+
const target = withWidth(previewTarget(), 320);
232+
const container = document.createElement('div');
233+
234+
renderDragPreview({
235+
previewTarget: target, sourceElement: target, container, batchSize: 3,
236+
});
237+
238+
const preview = container.querySelector('[data-preview]');
239+
const badge = container.querySelector(badgeSelector);
240+
241+
// The preview clone survives lit-html's render() alongside the badge:
242+
// both are present in the container at once.
243+
expect(preview).not.toBeNull();
244+
expect(badge).not.toBeNull();
245+
expect(container.contains(preview)).toBe(true);
246+
expect(container.contains(badge)).toBe(true);
247+
expect(container.style.position).toEqual('relative');
248+
});
211249
});
212250
});
213251
});

frontend/src/stimulus/controllers/dynamic/sortable-lists/preview.ts

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
// See COPYRIGHT and LICENSE files for more details.
2727
//++
2828

29+
import { html, render } from 'lit-html';
30+
2931
// Builds the custom native drag preview for a sortable item: a sanitised clone
3032
// of the item's preview target, sized to match and carrying the originating
3133
// Box's density so its card styling survives being mounted outside the Box.
@@ -54,11 +56,14 @@ const PREVIEW_STRIPPED_ATTRIBUTES = [
5456
// `.Box--condensed .Box-card`) would not apply to it otherwise.
5557
const BOX_DENSITY_VARIANT_CLASSES = ['Box--condensed', 'Box--spacious'] as const;
5658

57-
// The count badge added to a multi-card drag's preview. Styled inline rather
58-
// than through a stylesheet class, matching this file's own approach for the
59-
// clone's width and margin above: the preview is a native drag image built
60-
// outside the page's normal render tree, so it must be legible even where no
61-
// stylesheet has had a chance to apply to it.
59+
// The count badge added to a multi-card drag's preview. Styled on Primer's
60+
// Counter contract (the same `Counter`/`Counter--primary` classes the
61+
// Angular PrimerCounterComponent renders) plus this class for the
62+
// positioning that Counter itself doesn't own; see
63+
// frontend/src/global_styles/content/drag_and_drop.sass. Building it as an
64+
// Angular custom element is not an option here: the native drag snapshot is
65+
// taken synchronously at dragstart, before an Angular element would have
66+
// painted, so this layer stays framework-agnostic by contract.
6267
const BATCH_BADGE_CLASS = 'op-sortable-lists-drag-preview-batch-badge';
6368

6469
export function renderDragPreview({
@@ -103,36 +108,26 @@ export function renderDragPreview({
103108
// Anchors the badge's absolute positioning to the container itself
104109
// rather than whatever ancestor Pragmatic happens to mount it under.
105110
container.style.position = 'relative';
106-
container.append(renderBatchBadge(preview.ownerDocument, batchSize));
111+
renderBatchBadge(container, batchSize);
107112
}
108113
}
109114

110-
// Absolutely positioned over the card clone's top-right corner. The
111-
// container is the preview mount Pragmatic hands render(); giving it
112-
// position:relative here (rather than assuming the caller already set it)
113-
// keeps the badge anchored to the card regardless of what else mounts there.
114-
function renderBatchBadge(document:Document, batchSize:number):HTMLElement {
115-
const badge = document.createElement('span');
116-
badge.className = BATCH_BADGE_CLASS;
117-
badge.textContent = String(batchSize);
118-
Object.assign(badge.style, {
119-
position: 'absolute',
120-
top: '-8px',
121-
right: '-8px',
122-
minWidth: '20px',
123-
height: '20px',
124-
padding: '0 6px',
125-
borderRadius: '999px',
126-
backgroundColor: 'var(--bgColor-emphasis, #1f2328)',
127-
color: 'var(--fgColor-onEmphasis, #ffffff)',
128-
fontSize: '12px',
129-
fontWeight: '600',
130-
lineHeight: '20px',
131-
textAlign: 'center',
132-
boxShadow: 'var(--shadow-floating-medium, 0 1px 3px rgba(0, 0, 0, 0.3))',
133-
});
134-
135-
return badge;
115+
// Absolutely positioned over the card clone's top-right corner (see the sass
116+
// block in drag_and_drop.sass for the geometry, including the Firefox
117+
// inset). The container is the preview mount Pragmatic hands render();
118+
// giving it position:relative here (rather than assuming the caller already
119+
// set it) keeps the badge anchored to the card regardless of what else
120+
// mounts there.
121+
//
122+
// lit-html's render() is safe to call directly on `container` here even
123+
// though the sanitised preview clone was already appended to it above:
124+
// render() only inserts a marker comment before its own end node (the
125+
// container's end, when unset) and manages content from that marker
126+
// onward — it does not clear pre-existing children. The clone and the
127+
// badge coexist; see the "anchors the badge to the container without
128+
// disturbing the already-appended preview clone" spec.
129+
function renderBatchBadge(container:HTMLElement, batchSize:number):void {
130+
render(html`<span class="Counter Counter--primary ${BATCH_BADGE_CLASS}">${batchSize}</span>`, container);
136131
}
137132

138133
export function sanitizePreview(element:HTMLElement):void {

0 commit comments

Comments
 (0)