Files
TREK/client/src/components/Studio/sidebar/StudioRail.tsx
T
Maurice 4051af58a7 fix(studio): a rail label wraps between words, never inside one
The rail gave its labels 48px and let them break anywhere, so Elements read Element and s, and nine languages broke a word at the default text size. Labels wrap only between words now, and the rail grows to its longest word from 64px.
2026-10-25 09:46:40 +01:00

78 lines
2.6 KiB
TypeScript

import { Fragment, useRef, type KeyboardEvent } from 'react';
import '../../../styles/studio-panels.css';
import { rovingTarget } from '../studioRoving';
import { RAIL_IDS, RAIL_SECTIONS, type RailSection } from './railSections';
/**
* The rail down the side of Studio: one tab per section, each an icon over its
* name.
*
* A list of tabs to a screen reader and to the keyboard, as it is to the eye:
* Tab reaches the open section, the arrows move up and down the rail, Home and
* End jump to its ends, and Enter or Space opens a section (studioRoving.ts).
* A name longer than the rail is wide wraps onto a second line, only ever
* between words: where one word alone is wider, the rail grows to fit it
* (studio.css) instead of cutting it. The title carries the name whole.
*/
export function StudioRail({
section,
onSection,
t,
}: {
section: RailSection;
onSection: (id: RailSection) => void;
t: (k: string) => string;
}) {
const tabs = useRef<(HTMLButtonElement | null)[]>([]);
const onKeyDown = (e: KeyboardEvent<HTMLButtonElement>, index: number) => {
const next = rovingTarget(e.key, index, RAIL_SECTIONS.length, { vertical: true, rtl: false });
if (next === null) return;
e.preventDefault();
tabs.current[next]?.focus();
};
return (
<div className="st-rail">
<div
className="st-rail-tabs"
role="tablist"
aria-orientation="vertical"
aria-label={t('journey.studio.sections')}
>
{RAIL_SECTIONS.map((entry, i) => {
const on = entry.id === section;
const label = t(entry.labelKey);
const Icon = entry.icon;
return (
<Fragment key={entry.id}>
{entry.startsGroup && <span className="st-rail-sep" aria-hidden />}
<button
type="button"
role="tab"
id={RAIL_IDS.tab(entry.id)}
aria-selected={on}
aria-controls={RAIL_IDS.panel}
tabIndex={on ? 0 : -1}
className={`st-rail-btn ${on ? 'is-on' : ''}`}
ref={(el) => {
tabs.current[i] = el;
}}
onClick={() => onSection(entry.id)}
onKeyDown={(e) => onKeyDown(e, i)}
title={label}
aria-label={label}
>
<Icon size={18} strokeWidth={1.7} aria-hidden />
<span className="st-rail-label" aria-hidden>
{label}
</span>
</button>
</Fragment>
);
})}
</div>
</div>
);
}