mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Add "skip to content", "skip to navigation" links (#38006)
This commit is contained in:
@@ -4,8 +4,11 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { getColumnSkipLinkId } from 'mastodon/features/ui/components/skip_links';
|
||||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||||
|
|
||||||
|
import { useColumnIndexContext } from '../features/ui/components/columns_area';
|
||||||
|
|
||||||
import { useAppHistory } from './router';
|
import { useAppHistory } from './router';
|
||||||
|
|
||||||
type OnClickCallback = () => void;
|
type OnClickCallback = () => void;
|
||||||
@@ -28,9 +31,15 @@ export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const handleClick = useHandleClick(onClick);
|
const handleClick = useHandleClick(onClick);
|
||||||
|
const columnIndex = useColumnIndexContext();
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
<button onClick={handleClick} className='column-back-button' type='button'>
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
id={getColumnSkipLinkId(columnIndex)}
|
||||||
|
className='column-back-button'
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
id='chevron-left'
|
id='chevron-left'
|
||||||
icon={ArrowBackIcon}
|
icon={ArrowBackIcon}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import { Icon } from 'mastodon/components/icon';
|
|||||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
|
|
||||||
|
import { useColumnIndexContext } from '../features/ui/components/columns_area';
|
||||||
|
import { getColumnSkipLinkId } from '../features/ui/components/skip_links';
|
||||||
|
|
||||||
import { useAppHistory } from './router';
|
import { useAppHistory } from './router';
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
@@ -33,10 +36,11 @@ export const messages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const BackButton: React.FC<{
|
const BackButton: React.FC<{
|
||||||
onlyIcon: boolean;
|
hasTitle: boolean;
|
||||||
}> = ({ onlyIcon }) => {
|
}> = ({ hasTitle }) => {
|
||||||
const history = useAppHistory();
|
const history = useAppHistory();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const columnIndex = useColumnIndexContext();
|
||||||
|
|
||||||
const handleBackClick = useCallback(() => {
|
const handleBackClick = useCallback(() => {
|
||||||
if (history.location.state?.fromMastodon) {
|
if (history.location.state?.fromMastodon) {
|
||||||
@@ -50,8 +54,9 @@ const BackButton: React.FC<{
|
|||||||
<button
|
<button
|
||||||
onClick={handleBackClick}
|
onClick={handleBackClick}
|
||||||
className={classNames('column-header__back-button', {
|
className={classNames('column-header__back-button', {
|
||||||
compact: onlyIcon,
|
compact: hasTitle,
|
||||||
})}
|
})}
|
||||||
|
id={!hasTitle ? getColumnSkipLinkId(columnIndex) : undefined}
|
||||||
aria-label={intl.formatMessage(messages.back)}
|
aria-label={intl.formatMessage(messages.back)}
|
||||||
type='button'
|
type='button'
|
||||||
>
|
>
|
||||||
@@ -60,7 +65,7 @@ const BackButton: React.FC<{
|
|||||||
icon={ArrowBackIcon}
|
icon={ArrowBackIcon}
|
||||||
className='column-back-button__icon'
|
className='column-back-button__icon'
|
||||||
/>
|
/>
|
||||||
{!onlyIcon && (
|
{!hasTitle && (
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -221,7 +226,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
!pinned &&
|
!pinned &&
|
||||||
((multiColumn && history.location.state?.fromMastodon) || showBackButton)
|
((multiColumn && history.location.state?.fromMastodon) || showBackButton)
|
||||||
) {
|
) {
|
||||||
backButton = <BackButton onlyIcon={!!title} />;
|
backButton = <BackButton hasTitle={!!title} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapsedContent = [extraContent];
|
const collapsedContent = [extraContent];
|
||||||
@@ -260,6 +265,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
const hasIcon = icon && iconComponent;
|
const hasIcon = icon && iconComponent;
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
const hasTitle = (hasIcon || backButton) && title;
|
const hasTitle = (hasIcon || backButton) && title;
|
||||||
|
const columnIndex = useColumnIndexContext();
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
<div className={wrapperClassName}>
|
<div className={wrapperClassName}>
|
||||||
@@ -272,6 +278,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
onClick={handleTitleClick}
|
onClick={handleTitleClick}
|
||||||
className='column-header__title'
|
className='column-header__title'
|
||||||
type='button'
|
type='button'
|
||||||
|
id={getColumnSkipLinkId(columnIndex)}
|
||||||
>
|
>
|
||||||
{!backButton && hasIcon && (
|
{!backButton && hasIcon && (
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
|||||||
import { WordmarkLogo } from 'mastodon/components/logo';
|
import { WordmarkLogo } from 'mastodon/components/logo';
|
||||||
import { Search } from 'mastodon/features/compose/components/search';
|
import { Search } from 'mastodon/features/compose/components/search';
|
||||||
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
|
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
|
||||||
|
import { getNavigationSkipLinkId } from 'mastodon/features/ui/components/skip_links';
|
||||||
import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
import {
|
import {
|
||||||
@@ -224,7 +225,11 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
|
|||||||
return (
|
return (
|
||||||
<div className='navigation-panel'>
|
<div className='navigation-panel'>
|
||||||
<div className='navigation-panel__logo'>
|
<div className='navigation-panel__logo'>
|
||||||
<Link to='/' className='column-link column-link--logo'>
|
<Link
|
||||||
|
to='/'
|
||||||
|
className='column-link column-link--logo'
|
||||||
|
id={getNavigationSkipLinkId()}
|
||||||
|
>
|
||||||
<WordmarkLogo />
|
<WordmarkLogo />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Children, cloneElement, useCallback } from 'react';
|
import { Children, cloneElement, createContext, useContext, useCallback } from 'react';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
@@ -53,6 +53,13 @@ const TabsBarPortal = () => {
|
|||||||
return <div id='tabs-bar__portal' ref={setRef} />;
|
return <div id='tabs-bar__portal' ref={setRef} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Simple context to allow column children to know which column they're in
|
||||||
|
export const ColumnIndexContext = createContext(1);
|
||||||
|
/**
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const useColumnIndexContext = () => useContext(ColumnIndexContext);
|
||||||
|
|
||||||
export default class ColumnsArea extends ImmutablePureComponent {
|
export default class ColumnsArea extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
columns: ImmutablePropTypes.list.isRequired,
|
columns: ImmutablePropTypes.list.isRequired,
|
||||||
@@ -140,18 +147,22 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
|
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
|
||||||
{columns.map(column => {
|
{columns.map((column, index) => {
|
||||||
const params = column.get('params', null) === null ? null : column.get('params').toJS();
|
const params = column.get('params', null) === null ? null : column.get('params').toJS();
|
||||||
const other = params && params.other ? params.other : {};
|
const other = params && params.other ? params.other : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Bundle key={column.get('uuid')} fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
|
<ColumnIndexContext.Provider value={index} key={column.get('uuid')}>
|
||||||
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
|
<Bundle fetchComponent={componentMap[column.get('id')]} loading={this.renderLoading(column.get('id'))} error={this.renderError}>
|
||||||
</Bundle>
|
{SpecificComponent => <SpecificComponent columnId={column.get('uuid')} params={params} multiColumn {...other} />}
|
||||||
|
</Bundle>
|
||||||
|
</ColumnIndexContext.Provider>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{Children.map(children, child => cloneElement(child, { multiColumn: true }))}
|
<ColumnIndexContext.Provider value={columns.size}>
|
||||||
|
{Children.map(children, child => cloneElement(child, { multiColumn: true }))}
|
||||||
|
</ColumnIndexContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { useCallback, useId } from 'react';
|
||||||
|
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import classes from './skip_links.module.scss';
|
||||||
|
|
||||||
|
export const getNavigationSkipLinkId = () => 'skip-link-target-nav';
|
||||||
|
export const getColumnSkipLinkId = (index: number) =>
|
||||||
|
`skip-link-target-content-${index}`;
|
||||||
|
|
||||||
|
export const SkipLinks: React.FC<{
|
||||||
|
multiColumn: boolean;
|
||||||
|
onFocusGettingStartedColumn: () => void;
|
||||||
|
}> = ({ multiColumn, onFocusGettingStartedColumn }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const columnCount = useAppSelector((state) => {
|
||||||
|
const settings = state.settings as Immutable.Collection<string, unknown>;
|
||||||
|
return (settings.get('columns') as Immutable.Map<number, unknown>).size;
|
||||||
|
});
|
||||||
|
|
||||||
|
const focusMultiColumnNavbar = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onFocusGettingStartedColumn();
|
||||||
|
},
|
||||||
|
[onFocusGettingStartedColumn],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className={classes.list}>
|
||||||
|
<li className={classes.listItem}>
|
||||||
|
<SkipLink target={getColumnSkipLinkId(1)} hotkey='1'>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'skip_links.skip_to_content',
|
||||||
|
defaultMessage: 'Skip to main content',
|
||||||
|
})}
|
||||||
|
</SkipLink>
|
||||||
|
</li>
|
||||||
|
<li className={classes.listItem}>
|
||||||
|
<SkipLink
|
||||||
|
target={multiColumn ? `/getting-started` : getNavigationSkipLinkId()}
|
||||||
|
onRouterLinkClick={multiColumn ? focusMultiColumnNavbar : undefined}
|
||||||
|
hotkey={multiColumn ? `${columnCount}` : '2'}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'skip_links.skip_to_navigation',
|
||||||
|
defaultMessage: 'Skip to main navigation',
|
||||||
|
})}
|
||||||
|
</SkipLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SkipLink: React.FC<{
|
||||||
|
children: string;
|
||||||
|
target: string;
|
||||||
|
onRouterLinkClick?: React.MouseEventHandler;
|
||||||
|
hotkey: string;
|
||||||
|
}> = ({ children, hotkey, target, onRouterLinkClick }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const id = useId();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a href={`#${target}`} aria-describedby={id} onClick={onRouterLinkClick}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
<span id={id} className={classes.hotkeyHint}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'skip_links.hotkey',
|
||||||
|
defaultMessage: '<span>Hotkey</span> {hotkey}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
hotkey,
|
||||||
|
span: (text) => <span className='sr-only'>{text}</span>,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.list {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
box-shadow: var(--dropdown-shadow);
|
||||||
|
|
||||||
|
/* Hide visually when not focused */
|
||||||
|
&:not(:focus-within) {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
clip-path: inset(50%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding-inline-end: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
background: var(--color-bg-brand-softer);
|
||||||
|
}
|
||||||
|
|
||||||
|
:any-link {
|
||||||
|
display: block;
|
||||||
|
padding: 8px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration-color: var(--color-text-secondary);
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hotkeyHint {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 2.5ch;
|
||||||
|
margin-inline-start: auto;
|
||||||
|
padding: 3px 5px;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--color-bg-primary);
|
||||||
|
border: 1px solid var(--color-border-primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
|||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
import '../../components/status';
|
import '../../components/status';
|
||||||
import { areCollectionsEnabled } from '../collections/utils';
|
import { areCollectionsEnabled } from '../collections/utils';
|
||||||
|
import { getNavigationSkipLinkId, SkipLinks } from './components/skip_links';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
|
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
|
||||||
@@ -253,9 +254,9 @@ class SwitchingColumnsArea extends PureComponent {
|
|||||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||||
{areCollectionsEnabled() &&
|
{areCollectionsEnabled() &&
|
||||||
[
|
[
|
||||||
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} />,
|
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} key='collections-editor' />,
|
||||||
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} />,
|
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} key='collections-detail' />,
|
||||||
<WrappedRoute path='/collections' component={Collections} content={children} />
|
<WrappedRoute path='/collections' component={Collections} content={children} key='collections-list' />
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
<Route component={BundleColumnError} />
|
<Route component={BundleColumnError} />
|
||||||
@@ -556,6 +557,14 @@ class UI extends PureComponent {
|
|||||||
|
|
||||||
handleHotkeyGoToStart = () => {
|
handleHotkeyGoToStart = () => {
|
||||||
this.props.history.push('/getting-started');
|
this.props.history.push('/getting-started');
|
||||||
|
// Set focus to the navigation after a timeout
|
||||||
|
// to allow for it to be displayed first
|
||||||
|
setTimeout(() => {
|
||||||
|
const navbarSkipTarget = document.querySelector(
|
||||||
|
`#${getNavigationSkipLinkId()}`,
|
||||||
|
);
|
||||||
|
navbarSkipTarget?.focus();
|
||||||
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyGoToFavourites = () => {
|
handleHotkeyGoToFavourites = () => {
|
||||||
@@ -617,6 +626,10 @@ class UI extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<Hotkeys global handlers={handlers}>
|
<Hotkeys global handlers={handlers}>
|
||||||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
|
||||||
|
<SkipLinks
|
||||||
|
multiColumn={layout === 'multi-column'}
|
||||||
|
onFocusGettingStartedColumn={this.handleHotkeyGoToStart}
|
||||||
|
/>
|
||||||
<SwitchingColumnsArea
|
<SwitchingColumnsArea
|
||||||
identity={this.props.identity}
|
identity={this.props.identity}
|
||||||
location={location}
|
location={location}
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
getColumnSkipLinkId,
|
||||||
|
getNavigationSkipLinkId,
|
||||||
|
} from '../components/skip_links';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Out of a list of elements, return the first one whose top edge
|
* Out of a list of elements, return the first one whose top edge
|
||||||
* is inside of the viewport, and return the element and its BoundingClientRect.
|
* is inside of the viewport, and return the element and its BoundingClientRect.
|
||||||
@@ -20,9 +25,29 @@ function findFirstVisibleWithRect(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusColumnTitle(index: number, multiColumn: boolean) {
|
||||||
|
if (multiColumn) {
|
||||||
|
const column = document.querySelector(`.column:nth-child(${index})`);
|
||||||
|
if (column) {
|
||||||
|
column
|
||||||
|
.querySelector<HTMLAnchorElement>(
|
||||||
|
`#${getColumnSkipLinkId(index - 1)}, #${getNavigationSkipLinkId()}`,
|
||||||
|
)
|
||||||
|
?.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const idSelector =
|
||||||
|
index === 2
|
||||||
|
? `#${getNavigationSkipLinkId()}`
|
||||||
|
: `#${getColumnSkipLinkId(1)}`;
|
||||||
|
|
||||||
|
document.querySelector<HTMLAnchorElement>(idSelector)?.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move focus to the column of the passed index (1-based).
|
* Move focus to the column of the passed index (1-based).
|
||||||
* Focus is placed on the topmost visible item
|
* Focus is placed on the topmost visible item, or the column title
|
||||||
*/
|
*/
|
||||||
export function focusColumn(index = 1) {
|
export function focusColumn(index = 1) {
|
||||||
// Skip the leftmost drawer in multi-column mode
|
// Skip the leftmost drawer in multi-column mode
|
||||||
@@ -35,11 +60,21 @@ export function focusColumn(index = 1) {
|
|||||||
`.column:nth-child(${index + indexOffset})`,
|
`.column:nth-child(${index + indexOffset})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!column) return;
|
function fallback() {
|
||||||
|
focusColumnTitle(index + indexOffset, isMultiColumnLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!column) {
|
||||||
|
fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const container = column.querySelector('.scrollable');
|
const container = column.querySelector('.scrollable');
|
||||||
|
|
||||||
if (!container) return;
|
if (!container) {
|
||||||
|
fallback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const focusableItems = Array.from(
|
const focusableItems = Array.from(
|
||||||
container.querySelectorAll<HTMLElement>(
|
container.querySelectorAll<HTMLElement>(
|
||||||
@@ -50,20 +85,23 @@ export function focusColumn(index = 1) {
|
|||||||
// Find first item visible in the viewport
|
// Find first item visible in the viewport
|
||||||
const itemToFocus = findFirstVisibleWithRect(focusableItems);
|
const itemToFocus = findFirstVisibleWithRect(focusableItems);
|
||||||
|
|
||||||
if (itemToFocus) {
|
if (!itemToFocus) {
|
||||||
const viewportWidth =
|
fallback();
|
||||||
window.innerWidth || document.documentElement.clientWidth;
|
return;
|
||||||
const { item, rect } = itemToFocus;
|
|
||||||
|
|
||||||
if (
|
|
||||||
container.scrollTop > item.offsetTop ||
|
|
||||||
rect.right > viewportWidth ||
|
|
||||||
rect.left < 0
|
|
||||||
) {
|
|
||||||
itemToFocus.item.scrollIntoView(true);
|
|
||||||
}
|
|
||||||
itemToFocus.item.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewportWidth =
|
||||||
|
window.innerWidth || document.documentElement.clientWidth;
|
||||||
|
const { item, rect } = itemToFocus;
|
||||||
|
|
||||||
|
if (
|
||||||
|
container.scrollTop > item.offsetTop ||
|
||||||
|
rect.right > viewportWidth ||
|
||||||
|
rect.left < 0
|
||||||
|
) {
|
||||||
|
itemToFocus.item.scrollIntoView(true);
|
||||||
|
}
|
||||||
|
itemToFocus.item.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1072,6 +1072,9 @@
|
|||||||
"sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.",
|
"sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.",
|
||||||
"sign_in_banner.sign_in": "Login",
|
"sign_in_banner.sign_in": "Login",
|
||||||
"sign_in_banner.sso_redirect": "Login or Register",
|
"sign_in_banner.sso_redirect": "Login or Register",
|
||||||
|
"skip_links.hotkey": "<span>Hotkey</span> {hotkey}",
|
||||||
|
"skip_links.skip_to_content": "Skip to main content",
|
||||||
|
"skip_links.skip_to_navigation": "Skip to main navigation",
|
||||||
"status.admin_account": "Open moderation interface for @{name}",
|
"status.admin_account": "Open moderation interface for @{name}",
|
||||||
"status.admin_domain": "Open moderation interface for {domain}",
|
"status.admin_domain": "Open moderation interface for {domain}",
|
||||||
"status.admin_status": "Open this post in the moderation interface",
|
"status.admin_status": "Open this post in the moderation interface",
|
||||||
|
|||||||
@@ -3903,6 +3903,11 @@ a.account__display-name {
|
|||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: var(--outline-focus-default);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header__back-button {
|
.column-header__back-button {
|
||||||
@@ -4036,15 +4041,17 @@ a.account__display-name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.column-link {
|
.column-link {
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px;
|
padding: 10px;
|
||||||
|
padding-inline-start: 14px;
|
||||||
|
overflow: hidden;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: color-mix(
|
color: color-mix(
|
||||||
in oklab,
|
in oklab,
|
||||||
@@ -4052,9 +4059,8 @@ a.account__display-name {
|
|||||||
var(--color-text-secondary)
|
var(--color-text-secondary)
|
||||||
);
|
);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 2px solid transparent;
|
||||||
border-left: 4px solid transparent;
|
border-radius: 4px;
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active,
|
&:active,
|
||||||
@@ -4066,17 +4072,15 @@ a.account__display-name {
|
|||||||
color: var(--color-text-brand);
|
color: var(--color-text-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
border-color: var(--color-text-brand);
|
border-color: var(--color-text-brand);
|
||||||
border-radius: 0;
|
background: var(--color-bg-brand-softer);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--logo {
|
&--logo {
|
||||||
padding: 10px;
|
padding: 8px;
|
||||||
|
padding-inline-start: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user