Skip to content

Commit 320b49b

Browse files
committed
Restyle the batch badge on Primer's Counter
Reuses the Counter contract at the CSS level so the badge is stylesheet-targetable instead of carrying a one-off inline skin, rendered with lit-html per existing precedent. Firefox folds overflow into its drag-image snapshot and misaligns the grab offset, so the badge gets an inset, shadowless variant there. https://community.openproject.org/wp/AGILE-278
1 parent 454f233 commit 320b49b

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)