mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-26 02:06:49 +00:00
feat: add a compact icon-only top navigation toggle (#1363)
* feat: add a compact icon-only top navigation toggle Add a browser-local "Top navigation labels" preference under Settings > Appearance. With it off, the desktop top navigation renders icon-only; each destination keeps an aria-label, gains a hover/focus tooltip, and stays reachable from the command palette. The setting defaults on, so current behavior is preserved, and the mobile navigation always keeps its labels. Also left-align the desktop nav (previously centered) and shorten the longest nav label from "Auto-Update" to "Update" so the bar scans faster. * feat: let the icon-only top nav be left or centered Add a "Top navigation alignment" preference under Settings > Appearance that appears only when top navigation labels are off. It places the icon-only bar against the left edge (the default) or centered. With labels on, the nav always stays left so the longer labels read from the edge. The choice is browser-local and persists per device.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useTopNavAlign, TOP_NAV_ALIGN_KEY } from '../use-top-nav-align';
|
||||
|
||||
describe('useTopNavAlign (left default)', () => {
|
||||
beforeEach(() => localStorage.clear());
|
||||
afterEach(() => localStorage.clear());
|
||||
|
||||
it('defaults to left when no value is stored', () => {
|
||||
const { result } = renderHook(() => useTopNavAlign());
|
||||
expect(result.current[0]).toBe('left');
|
||||
});
|
||||
|
||||
it('reads center only when explicitly set to center', () => {
|
||||
localStorage.setItem(TOP_NAV_ALIGN_KEY, 'center');
|
||||
const { result } = renderHook(() => useTopNavAlign());
|
||||
expect(result.current[0]).toBe('center');
|
||||
});
|
||||
|
||||
it('treats any unknown value as left', () => {
|
||||
localStorage.setItem(TOP_NAV_ALIGN_KEY, 'something-else');
|
||||
const { result } = renderHook(() => useTopNavAlign());
|
||||
expect(result.current[0]).toBe('left');
|
||||
});
|
||||
|
||||
it('setAlign persists and switches', () => {
|
||||
const { result } = renderHook(() => useTopNavAlign());
|
||||
act(() => result.current[1]('center'));
|
||||
expect(result.current[0]).toBe('center');
|
||||
expect(localStorage.getItem(TOP_NAV_ALIGN_KEY)).toBe('center');
|
||||
act(() => result.current[1]('left'));
|
||||
expect(result.current[0]).toBe('left');
|
||||
});
|
||||
|
||||
it('reacts to a storage event from another tab', () => {
|
||||
const { result } = renderHook(() => useTopNavAlign());
|
||||
expect(result.current[0]).toBe('left');
|
||||
act(() => {
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: TOP_NAV_ALIGN_KEY, newValue: 'center' }));
|
||||
});
|
||||
expect(result.current[0]).toBe('center');
|
||||
});
|
||||
|
||||
it('syncs a second hook in the same tab via the settings-changed event', () => {
|
||||
const a = renderHook(() => useTopNavAlign());
|
||||
const b = renderHook(() => useTopNavAlign());
|
||||
act(() => a.result.current[1]('center'));
|
||||
expect(b.result.current[0]).toBe('center');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useTopNavLabels, TOP_NAV_LABELS_KEY } from '../use-top-nav-labels';
|
||||
|
||||
describe('useTopNavLabels (opt-out default)', () => {
|
||||
beforeEach(() => localStorage.clear());
|
||||
afterEach(() => localStorage.clear());
|
||||
|
||||
it('defaults to showing labels when no value is stored', () => {
|
||||
const { result } = renderHook(() => useTopNavLabels());
|
||||
expect(result.current[0]).toBe(true);
|
||||
});
|
||||
|
||||
it('hides labels only when explicitly set to false', () => {
|
||||
localStorage.setItem(TOP_NAV_LABELS_KEY, 'false');
|
||||
const { result } = renderHook(() => useTopNavLabels());
|
||||
expect(result.current[0]).toBe(false);
|
||||
});
|
||||
|
||||
it('treats any non-false value as labels-on', () => {
|
||||
localStorage.setItem(TOP_NAV_LABELS_KEY, 'true');
|
||||
const { result } = renderHook(() => useTopNavLabels());
|
||||
expect(result.current[0]).toBe(true);
|
||||
});
|
||||
|
||||
it('restores labels when a storage event clears the key (newValue null)', () => {
|
||||
localStorage.setItem(TOP_NAV_LABELS_KEY, 'false');
|
||||
const { result } = renderHook(() => useTopNavLabels());
|
||||
expect(result.current[0]).toBe(false);
|
||||
act(() => {
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: TOP_NAV_LABELS_KEY, newValue: null }));
|
||||
});
|
||||
expect(result.current[0]).toBe(true);
|
||||
});
|
||||
|
||||
it('setShowLabels(false) persists false and hides labels', () => {
|
||||
const { result } = renderHook(() => useTopNavLabels());
|
||||
act(() => result.current[1](false));
|
||||
expect(result.current[0]).toBe(false);
|
||||
expect(localStorage.getItem(TOP_NAV_LABELS_KEY)).toBe('false');
|
||||
});
|
||||
|
||||
it('syncs a second hook in the same tab via the settings-changed event', () => {
|
||||
const a = renderHook(() => useTopNavLabels());
|
||||
const b = renderHook(() => useTopNavLabels());
|
||||
act(() => a.result.current[1](false));
|
||||
expect(b.result.current[0]).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user