Skip to content
Merged
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 @@ -132,30 +132,31 @@
}
}

<!-- Sidebar toggle: always visible, icon reflects open/closed state (matches UVE pattern) -->
<!-- Sidebar toggle: always visible. Single `dock_to_left` glyph, rendered as-is (NOT
mirrored — the previous SVG pair carried a `-scale-x-100` flip that pointed it the wrong
way). The glyph no longer changes per state, so the open/closed cue lives on the
accessible name — `aria-hidden` keeps screen readers off the ligature text.
The name MUST go through PrimeNG's `[ariaLabel]` input, not `[attr.aria-label]`: the
latter lands on the non-focusable `<p-button>` host, leaving the real `<button>` (whose
only content is the aria-hidden icon) with no accessible name at all. -->
<div
class="-mr-4 flex min-h-[52px] shrink-0 items-center justify-center"
data-testId="sidebar-toggle">
<p-button
[rounded]="true"
[text]="true"
(click)="$store.toggleSidebar()"
[ariaLabel]="
(showSidebar ? 'edit.content.sidebar.close' : 'edit.content.sidebar.open') | dm
"
size="small"
data-testId="sidebar-toggle-button">
<img
src="/dotAdmin/assets/edit-uve-toolbar/left_panel_open.svg"
[alt]="'edit.content.sidebar.close' | dm"
[class.hidden]="!showSidebar"
class="-scale-x-100"
draggable="false"
data-testId="sidebar-open-icon" />
<img
src="/dotAdmin/assets/edit-uve-toolbar/left_panel_close.svg"
[alt]="'edit.content.sidebar.open' | dm"
[class.hidden]="showSidebar"
class="-scale-x-100"
draggable="false"
data-testId="sidebar-close-icon" />
<i
class="material-symbols-outlined"
aria-hidden="true"
data-testId="sidebar-toggle-icon">
dock_to_left
</i>
</p-button>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,15 +458,67 @@ describe('DotFormComponent', () => {
expect(toggleSidebarSpy).toHaveBeenCalled();
});

it('should render both open and close sidebar icons, hiding one per state', () => {
const openIcon = spectator.query(byTestId('sidebar-open-icon'));
const closeIcon = spectator.query(byTestId('sidebar-close-icon'));
expect(openIcon).toBeTruthy();
expect(closeIcon).toBeTruthy();
// One of the two icons must be hidden at any given time
const openHidden = openIcon?.classList.contains('hidden');
const closeHidden = closeIcon?.classList.contains('hidden');
expect(openHidden).not.toBe(closeHidden);
it('should render a single dock_to_left sidebar icon', () => {
Comment thread
adrianjm-dotCMS marked this conversation as resolved.
const icon = spectator.query(byTestId('sidebar-toggle-icon'));

expect(icon).toBeTruthy();
// Material Symbols renders the glyph from the ligature text, so the exact content
// matters — a typo would silently render as plain words.
expect(icon?.textContent?.trim()).toBe('dock_to_left');
expect(icon?.classList.contains('material-symbols-outlined')).toBe(true);
// Rendered as-is: the flip the previous SVG pair carried pointed it the wrong way.
expect(icon?.classList.contains('-scale-x-100')).toBe(false);
// The glyph is decorative; the accessible name lives on the button.
expect(icon?.getAttribute('aria-hidden')).toBe('true');

// The previous two-icon (open/close SVG) markup is gone.
expect(spectator.query(byTestId('sidebar-open-icon'))).toBeFalsy();
expect(spectator.query(byTestId('sidebar-close-icon'))).toBeFalsy();
});

it('should name the native sidebar-toggle button per open/closed state', () => {
// Assert on the DESCENDANT <button>, not the p-button host: the accessible name must
// land on the focusable control. `[attr.aria-label]` would sit on the inert host and
// leave this button unnamed (its only content is the aria-hidden glyph).
const nativeLabel = (): string | null | undefined =>
spectator
.query(byTestId('sidebar-toggle-button'))
?.querySelector('button')
?.getAttribute('aria-label');

// DotMessageService is a bare mockProvider (returns undefined), so echo the key back
// to make the label observable.
(spectator.inject(DotMessageService).get as jest.Mock).mockImplementation(
(key: string) => key
);

// Opening the sidebar wakes the information feature's effect, which fetches the
// reference-page count. This describe never stubs it, so the bare mockProvider hands
// back `undefined` and the effect throws on `.pipe` — asynchronously, landing on
// whichever test runs next.
dotEditContentService.getReferencePages.mockReturnValue(of(0));

const expectedLabel = () =>
store.isSidebarOpen()
? 'edit.content.sidebar.close'
: 'edit.content.sidebar.open';

// `dm` is a PURE pipe, so it only re-runs when its input key changes — hence both
// assertions follow a toggle rather than reading the initial render. Derived from
// the live state instead of a hardcoded order: the initial value depends on how this
// describe initializes the store, not on the store's own default.
store.toggleSidebar();
spectator.detectChanges();
const afterFirstToggle = nativeLabel();
expect(afterFirstToggle).toBe(expectedLabel());

store.toggleSidebar();
spectator.detectChanges();
expect(nativeLabel()).toBe(expectedLabel());

// Both states were actually exercised — otherwise the two assertions above could
// both pass against a label that never changed.
expect(nativeLabel()).not.toBe(afterFirstToggle);
});

describe('TabView Styling', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
`!important` is required to override PrimeNG's default dimmed mask background, which the theme
and the `pt` API do not otherwise let us clear — it is deliberate, not a leftover.

The left-cast shadow on `pt.root.class` is an arbitrary Tailwind value on purpose: the drawer
uses `appendTo="body"` (teleported out of this component's DOM), so component-scoped SCSS /
`:host ::ng-deep` cannot reach it, and no dotCMS `$shadow-*` token is left-directional (they all
cast downward, but a right-anchored drawer needs the shadow cast to the left).
The shadow goes on `pt.root.class` rather than in component SCSS because the drawer uses
`appendTo="body"` (teleported out of this component's DOM), so component-scoped SCSS /
`:host ::ng-deep` cannot reach it.

`dismissible` is left OFF even though clicking outside DOES close the panel: PrimeNG's own
dismissible tears the drawer down immediately, bypassing the unsaved-changes guard. The mask
click is handled by the component instead (see `onMaskClick`).

The header buttons name themselves through PrimeNG's `[ariaLabel]` input, never
`[attr.aria-label]`: the latter lands on the non-focusable `<p-button>` host, leaving the real
`<button>` — whose only content is an icon glyph — with no accessible name.
-->
<p-drawer
[visible]="!!data()"
Expand All @@ -19,8 +26,8 @@
[pt]="{
root: {
'data-testid': 'edit-content-side-panel',
class: '!shadow-[-12px_0_24px_rgb(0_0_0_/_20%)]',
style: { width: $expanded() ? '100%' : '70%', transition: 'width 250ms ease' }
class: '!shadow-2xl',
Comment thread
adrianjm-dotCMS marked this conversation as resolved.
style: { width: $expanded() ? '100%' : '80%', transition: 'width 250ms ease' }
Comment thread
adrianjm-dotCMS marked this conversation as resolved.
},
content: { class: 'p-0!' }
}"
Expand All @@ -34,7 +41,7 @@
<p-button
[text]="true"
severity="secondary"
[attr.aria-label]="
[ariaLabel]="
$expanded()
? ('edit.content.side-panel.collapse' | dm)
: ('edit.content.side-panel.expand' | dm)
Expand All @@ -50,7 +57,7 @@
<p-button
[text]="true"
severity="secondary"
[attr.aria-label]="'edit.content.side-panel.close' | dm"
[ariaLabel]="'edit.content.side-panel.close' | dm"
(onClick)="requestClose()"
data-testid="side-panel-close">
<ng-template #icon>
Expand Down
Loading
Loading