@atlaskit/page-layout
Version:
A collection of components which let you compose an application's page layout.
164 lines (160 loc) • 5.3 kB
JavaScript
/* eslint-disable @repo/internal/dom-events/no-unsafe-event-listeners */
/**
* @jsxRuntime classic
* @jsx jsx
*/
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
import { css, jsx } from '@emotion/react';
import Link from '@atlaskit/link';
import { easeOut, prefersReducedMotion } from '@atlaskit/motion';
import { N30A, N60A } from '@atlaskit/theme/colors';
import { DEFAULT_I18N_PROPS_SKIP_LINKS, PAGE_LAYOUT_CONTAINER_SELECTOR } from '../../common/constants';
import { useSkipLinks } from '../../controllers';
// eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage, @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
const prefersReducedMotionStyles = css(prefersReducedMotion());
const skipLinkStyles = css({
margin: "var(--ds-space-250, 10px)",
padding: '0.8rem 1rem',
position: 'fixed',
zIndex: -1,
background: "var(--ds-surface-overlay, white)",
border: 'none',
borderRadius: "var(--ds-border-radius, 3px)",
boxShadow: `var(--ds-shadow-overlay, ${`0 0 0 1px ${N30A}, 0 2px 10px ${N30A}, 0 0 20px -4px ${N60A}`})`,
insetInlineStart: -999999,
opacity: 0,
transform: 'translateY(-50%)',
transition: `transform 0.3s ${easeOut}`,
'&:focus-within': {
zIndex: 2147483640,
insetInlineStart: 0,
opacity: 1,
transform: 'translateY(0%)'
}
});
const skipLinkHeadingStyles = css({
fontWeight: "var(--ds-font-weight-semibold, 600)"
});
const skipLinkListStyles = css({
listStylePosition: 'outside',
listStyleType: 'none',
marginBlockStart: "var(--ds-space-050, 4px)",
paddingInlineStart: 0
});
const skipLinkListItemStyles = css({
marginBlockStart: 0
});
const assignIndex = (num, arr) => {
if (!arr.includes(num)) {
return num;
}
return assignIndex(num + 1, arr);
};
/**
* The default label will be used when the `skipLinksLabel` attribute is not
* provided or the attribute is an empty string. If a string comprised only of
* spaces is provided, the skip link heading element will be removed, but the
* default label will still be used in `title` attribute of the skip links
* themselves.
*/
export const SkipLinkWrapper = ({
skipLinksLabel
}) => {
const {
skipLinksData
} = useSkipLinks();
if (skipLinksData.length === 0) {
return null;
}
const sortSkipLinks = arr => {
const customLinks = arr.filter(link => Number.isInteger(link.listIndex));
if (customLinks.length === 0) {
return arr;
}
const usedIndexes = customLinks.map(a => a.listIndex);
const regularLinksWithIdx = arr.filter(link => link.listIndex === undefined).map((link, idx, currArr) => {
const listIndex = assignIndex(idx, usedIndexes);
usedIndexes.push(listIndex);
return {
...link,
listIndex
};
});
return [...customLinks, ...regularLinksWithIdx].sort((a, b) => a.listIndex - b.listIndex);
};
const escapeHandler = event => {
if (event.keyCode === 27) {
const container = document.querySelector(`[${PAGE_LAYOUT_CONTAINER_SELECTOR}="true"]`);
if (container !== null) {
container.focus();
}
}
};
const attachEscHandler = () => window.addEventListener('keydown', escapeHandler, false);
const removeEscHandler = () => window.removeEventListener('keydown', escapeHandler, false);
const emptyLabelOverride = !!(skipLinksLabel !== null && skipLinksLabel !== void 0 && skipLinksLabel.match(/^\s+$/));
const label = skipLinksLabel || DEFAULT_I18N_PROPS_SKIP_LINKS;
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
jsx("div", {
onFocus: attachEscHandler,
onBlur: removeEscHandler,
css: [skipLinkStyles, prefersReducedMotionStyles],
"data-skip-link-wrapper": true
}, emptyLabelOverride ? null : jsx("p", {
css: skipLinkHeadingStyles
}, label), jsx("ol", {
css: skipLinkListStyles
}, sortSkipLinks(skipLinksData).map(({
id,
skipLinkTitle
}) => jsx(SkipLink, {
key: id,
href: `#${id}`,
isFocusable: true
}, skipLinkTitle))))
);
};
const handleBlur = event => {
// @ts-ignore
event.target.removeAttribute('tabindex');
// @ts-ignore
event.target.removeEventListener('blur', handleBlur);
};
const focusTargetRef = href => event => {
event.preventDefault();
const targetRef = document.querySelector(href);
// @ts-ignore
const key = event.which || event.keycode;
// if it is a keypress and the key is not
// space or enter, just ignore it.
if (key && key !== 13 && key !== 32) {
return;
}
if (targetRef) {
targetRef.setAttribute('tabindex', '-1');
// @ts-ignore
targetRef.addEventListener('blur', handleBlur);
// @ts-ignore
targetRef.focus();
document.activeElement && document.activeElement.scrollIntoView({
behavior: 'smooth'
});
window.scrollTo(0, 0);
}
return false;
};
// eslint-disable-next-line @repo/internal/react/require-jsdoc
export const SkipLink = ({
href,
children,
isFocusable
}) => {
return jsx("li", {
css: skipLinkListItemStyles
}, jsx(Link, {
tabIndex: isFocusable ? 0 : -1,
href: href,
onClick: focusTargetRef(href)
}, children));
};