Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -85,41 +85,50 @@
data-testid="activities-input"
class="h-auto w-full resize-none text-sm leading-[1.4]"></textarea>

<!-- Character Counter -->
<!--
Meta row: validation error on the left, character counter on the
right. Two constraints shape this markup:

1. The counter is pushed right with `flex-1` on the message + `shrink-0`
on the counter, NOT with `justify-between`. When the field is valid
dot-field-validation-message renders nothing, but its host element
stays in the DOM; `justify-between` would then collapse and drag the
counter back to the left.
2. Fixed height (`min-h-4` + matching `leading-4` on both children):
activities-footer is `sticky bottom-0`, so if this row grew when the
error mounts, the whole footer would grow from its bottom anchor and
shift the textarea upward.
-->
<div
class="mt-1 text-left text-xs text-gray-600 transition-colors"
[class.text-red-500!]="isAtMaxLength()"
data-testid="activities-char-counter">
@if (!isAtMaxLength()) {
{{ commentLength() }}/{{ commentMaxLength }}
{{ 'edit.content.sidebar.activities.characters' | dm }}
} @else {
<span>
{{
'edit.content.sidebar.activities.char.counter'
| dm
: [
commentLength().toString(),
commentMaxLength.toString()
]
}}
</span>
}
</div>
</div>
class="mt-1 flex min-h-4 items-start gap-2 py-3"
data-testid="activities-meta-row">
<dot-field-validation-message
[field]="form.get('comment')"
[requiredErrorMessage]="'edit.content.sidebar.activities.required'"
class="block min-w-0 flex-1 leading-4 [&_small]:animate-[fadeIn_0.2s_ease-out]" />

<!--
Fixed-height slot: activities-footer is `sticky bottom-0`, so if this
row's height changed when the error message mounts/unmounts, the whole
footer would grow from its bottom anchor and shift the textarea upward.
Reserving the space up front keeps the footer height constant regardless
of whether dot-field-validation-message renders its <small> internally.
-->
<div class="h-3.5" data-testid="activities-error-slot">
<dot-field-validation-message
[field]="form.get('comment')"
[requiredErrorMessage]="'edit.content.sidebar.activities.required'"
class="block [&_small]:animate-[fadeIn_0.2s_ease-out]" />
<!-- Character Counter -->
<span
class="shrink-0 text-xs leading-4 text-gray-600 transition-colors"
[class.text-red-500!]="isAtMaxLength()"
data-testid="activities-char-counter">
@if (!isAtMaxLength()) {
{{ commentLength() }}/{{ commentMaxLength }}
{{ 'edit.content.sidebar.activities.characters' | dm }}
} @else {
<span>
{{
'edit.content.sidebar.activities.char.counter'
| dm
: [
commentLength().toString(),
commentMaxLength.toString()
]
}}
</span>
}
</span>
</div>
</div>

<div class="flex justify-end gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,27 +398,44 @@ describe('DotEditContentSidebarActivitiesComponent', () => {
expect(getValidationErrorMsg()).toBeFalsy();
});

it('should keep the error slot present with a fixed height regardless of error state, to avoid shifting the sticky footer', () => {
it('should keep the meta row present with a fixed height regardless of error state, to avoid shifting the sticky footer', () => {
const form = spectator.query(byTestId('activities-form'));
const getSlot = () => spectator.query(byTestId('activities-error-slot'));
const getMetaRow = () => spectator.query(byTestId('activities-meta-row'));

// No error yet: slot must still be present and reserve its height.
expect(getSlot()).toExist();
expect(getSlot()).toHaveClass('h-3.5');
// No error yet: row must still be present and reserve its height.
expect(getMetaRow()).toExist();
expect(getMetaRow()).toHaveClass('min-h-4');

// Error shown: slot's height class must not change.
// Error shown: row's height class must not change.
spectator.dispatchFakeEvent(form, 'submit');
spectator.detectChanges();
expect(getValidationErrorMsg()).toBeTruthy();
expect(getSlot()).toExist();
expect(getSlot()).toHaveClass('h-3.5');
expect(getMetaRow()).toExist();
expect(getMetaRow()).toHaveClass('min-h-4');

// Error cleared: slot's height class must still not change.
// Error cleared: row's height class must still not change.
spectator.component.clearComment();
spectator.detectChanges();
expect(getValidationErrorMsg()).toBeFalsy();
expect(getSlot()).toExist();
expect(getSlot()).toHaveClass('h-3.5');
expect(getMetaRow()).toExist();
expect(getMetaRow()).toHaveClass('min-h-4');
});

it('should lay out the error on the left and the character counter on the right of the same row', () => {
const metaRow = spectator.query(byTestId('activities-meta-row'));
const validationMessage = metaRow.querySelector('dot-field-validation-message');
const charCounter = metaRow.querySelector('[data-testid="activities-char-counter"]');

// Both live in the same row, error first (left), counter last (right).
expect(validationMessage).toExist();
expect(charCounter).toExist();
expect(metaRow.firstElementChild).toBe(validationMessage);
expect(metaRow.lastElementChild).toBe(charCounter);

// The counter is pinned right by the message taking the free space,
// so it stays right-aligned even when no error is rendered.
expect(validationMessage).toHaveClass('flex-1');
expect(charCounter).toHaveClass('shrink-0');
});

it('should reset form state when clearComment is called', () => {
Expand Down
Loading