diff --git a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx index b11b894d43f..572dfc88c20 100644 --- a/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx +++ b/packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx @@ -228,7 +228,7 @@ class ConfigScreenComponent extends React.Component { if (syncTargetMd.supportsConfigCheck) { const messages = shared.checkSyncConfigMessages(this); const statusComp = !messages.length ? null : ( -
+
{messages[0]} {messages.length >= 1 ?

{messages[1]}

: null}
@@ -371,7 +371,7 @@ class ConfigScreenComponent extends React.Component { const settings = this.state.settings; - const containerStyle = { + const containerStyle: React.CSSProperties = { overflow: 'auto', padding: theme.configScreenPadding, paddingTop: 0, @@ -403,6 +403,35 @@ class ConfigScreenComponent extends React.Component { const rightStyle = { ...style, flex: 1 }; delete style.width; + const tabComponents: React.ReactNode[] = []; + for (const section of sections) { + const sectionId = `setting-section-${section.name}`; + let content = null; + const visible = section.name === this.state.selectedSectionName; + if (visible) { + content = ( + <> + {screenComp} +
{settingComps}
+ + ); + } + + tabComponents.push( + , + ); + } + return (
{ sections={sections} />
- {screenComp} {needRestartComp} -
{settingComps}
+ {tabComponents} void; + sections: MetadataBySection; } export const StyledRoot = styled.div` @@ -73,24 +77,63 @@ export const StyledListItemIcon = styled.i` `; export default function Sidebar(props: Props) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied; - const buttons: any[] = []; + const buttonRefs = useRef([]); + + // Making a tabbed region accessible involves supporting keyboard interaction. + // See https://www.w3.org/WAI/ARIA/apg/patterns/tabs/ for details + const onKeyDown: React.KeyboardEventHandler = useCallback((event) => { + const selectedIndex = props.sections.findIndex(section => section.name === props.selection); + let newIndex = selectedIndex; + + if (event.code === 'ArrowUp') { + newIndex --; + } else if (event.code === 'ArrowDown') { + newIndex ++; + } else if (event.code === 'Home') { + newIndex = 0; + } else if (event.code === 'End') { + newIndex = props.sections.length - 1; + } + + if (newIndex < 0) newIndex += props.sections.length; + newIndex %= props.sections.length; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied; - function renderButton(section: any) { + if (newIndex !== selectedIndex) { + event.preventDefault(); + props.onSelectionChange({ section: props.sections[newIndex] }); + + const targetButton = buttonRefs.current[newIndex]; + if (targetButton) { + focus('Sidebar', targetButton); + } + } + }, [props.sections, props.selection, props.onSelectionChange]); + + const buttons: React.ReactNode[] = []; + + function renderButton(section: SettingMetadataSection, index: number) { const selected = props.selection === section.name; return ( { buttonRefs.current[index] = item; }} + + id={`setting-tab-${section.name}`} + aria-controls={`setting-section-${section.name}`} aria-selected={selected} + tabIndex={selected ? 0 : -1} + isSubSection={Setting.isSubSection(section.name)} selected={selected} onClick={() => { props.onSelectionChange({ section: section }); }} + onKeyDown={onKeyDown} > {Setting.sectionNameToLabel(section.name)} @@ -109,13 +152,15 @@ export default function Sidebar(props: Props) { let pluginDividerAdded = false; + let index = 0; for (const section of props.sections) { if (section.source === SettingSectionSource.Plugin && !pluginDividerAdded) { buttons.push(renderDivider('divider-plugins')); pluginDividerAdded = true; } - buttons.push(renderButton(section)); + buttons.push(renderButton(section, index)); + index ++; } return ( diff --git a/packages/app-desktop/gui/ConfigScreen/styles/index.scss b/packages/app-desktop/gui/ConfigScreen/styles/index.scss index 473e027048d..6eba6a8a65c 100644 --- a/packages/app-desktop/gui/ConfigScreen/styles/index.scss +++ b/packages/app-desktop/gui/ConfigScreen/styles/index.scss @@ -2,3 +2,4 @@ @use "./setting-description.scss"; @use "./setting-label.scss"; @use "./setting-header.scss"; +@use "./setting-tab-panel.scss"; diff --git a/packages/app-desktop/gui/ConfigScreen/styles/setting-tab-panel.scss b/packages/app-desktop/gui/ConfigScreen/styles/setting-tab-panel.scss new file mode 100644 index 00000000000..7febf2e0db7 --- /dev/null +++ b/packages/app-desktop/gui/ConfigScreen/styles/setting-tab-panel.scss @@ -0,0 +1,18 @@ + +.setting-tab-panel { + display: flex; + flex-grow: 1; + flex-shrink: 1; + min-height: 0; + + &.-hidden { + display: none; + } + + &:focus-visible { + // Use a border rather than an outline -- an outline would be shown outside of the screen + // and thus invisible. + border: 1px solid var(--joplin-focus-outline-color); + outline: none; + } +} \ No newline at end of file diff --git a/packages/app-desktop/main.scss b/packages/app-desktop/main.scss index 3fa523a4303..d99d8188aef 100644 --- a/packages/app-desktop/main.scss +++ b/packages/app-desktop/main.scss @@ -143,7 +143,7 @@ a { *:focus-visible { - outline: 1px solid var(--joplin-color-warn); + outline: 1px solid var(--joplin-focus-outline-color); } // The browser-default focus-visible indicator was originally removed for aesthetic diff --git a/packages/lib/theme.ts b/packages/lib/theme.ts index f3b8986e52e..590054cd9e1 100644 --- a/packages/lib/theme.ts +++ b/packages/lib/theme.ts @@ -110,6 +110,7 @@ export function extraStyles(theme: Theme) { const bgColor4 = theme.backgroundColor4; const borderColor4: string = Color(theme.color).alpha(0.3); const iconColor = Color(theme.color).alpha(0.8); + const focusOutlineColor = theme.colorWarn; const backgroundColor5 = theme.backgroundColor5 ?? theme.color4; const backgroundColorHover5 = Color(backgroundColor5).darken(0.2).hex(); @@ -230,6 +231,7 @@ export function extraStyles(theme: Theme) { backgroundColor5, backgroundColorHover5, backgroundColorActive5, + focusOutlineColor, icon: { ...globalStyle.icon,