Pārlūkot izejas kodu

A11y site support (#1230)

* docs: add a11y suppport to semi site

* chore: add a11y site support
YannLynn 3 gadi atpakaļ
vecāks
revīzija
9b7ad5f886

+ 4 - 4
package.json

@@ -42,10 +42,10 @@
         "coverage:merge": "npx istanbul-combine -d test/merged -p detail -r lcov -r json cypress/coverage/coverage-final.json test/coverage/coverage-final.json"
     },
     "dependencies": {
-        "@douyinfe/semi-site-banner": "^0.0.2",
+        "@douyinfe/semi-site-banner": "^0.1.0",
         "@douyinfe/semi-site-doc-style": "0.0.1",
-        "@douyinfe/semi-site-header": "^0.0.10",
-        "@douyinfe/semi-site-markdown-blocks": "^0.0.8",
+        "@douyinfe/semi-site-header": "^0.0.13",
+        "@douyinfe/semi-site-markdown-blocks": "^0.0.9",
         "@mdx-js/react": "^1.6.22",
         "@svgr/core": "^5.5.0",
         "@types/react-window": "^1.8.5",
@@ -225,4 +225,4 @@
         ]
     },
     "license": "MIT"
-}
+}

+ 2 - 2
packages/semi-ui/tabs/TabPane.tsx

@@ -57,7 +57,7 @@ class TabPane extends PureComponent<TabPaneProps> {
 
     render(): ReactNode {
         const { tabPaneMotion: motion, tabPosition, prevActiveKey } = this.context;
-        const { className, style, children, itemKey, ...restProps } = this.props;
+        const { className, style, children, itemKey, tabIndex, ...restProps } = this.props;
         const active = this.context.activeKey === itemKey;
         const classNames = cls(className, {
             [cssClasses.TABS_PANE_INACTIVE]: !active,
@@ -93,7 +93,7 @@ class TabPane extends PureComponent<TabPaneProps> {
                 className={classNames}
                 style={style}
                 aria-hidden={active ? 'false' : 'true'}
-                tabIndex={0}
+                tabIndex={tabIndex ? tabIndex : 0}
                 {...getDataAttr(restProps)}
                 x-semi-prop="children"
             >

+ 2 - 1
packages/semi-ui/tabs/interface.ts

@@ -66,7 +66,8 @@ export interface TabPaneProps {
     itemKey?: string;
     style?: CSSProperties;
     tab?: ReactNode;
-    closable?: boolean
+    closable?: boolean;
+    tabIndex?: number
 }
 
 export interface TabPaneTransitionProps {

+ 1 - 1
src/components/Footer/index.jsx

@@ -10,7 +10,7 @@ export class Footer extends Component {
 
     render() {
         return (
-            <div className='footerMini8'><img alt="semi logo" aria-hidden src="https://lf9-static.semi.design/obj/semi-tos/images/a5768a90-324e-11ec-b393-ab4adc2e449f.svg" className='group6' />
+            <div className='footerMini8' id="footer"><img alt="semi logo" aria-hidden src="https://lf9-static.semi.design/obj/semi-tos/images/a5768a90-324e-11ec-b393-ab4adc2e449f.svg" className='group6' />
                 <div className='links'>
                     <a href={`/${getLocale()}/start/getting-started`} className='text'>{_t('footer.component')}</a>
                     <a href='https://figma.com/@semi' className='figmaUIKit' target="_blank" rel="noreferrer">Figma UIKit</a>

+ 20 - 4
src/components/layout.js

@@ -49,6 +49,12 @@ const AppLayout = ({ type, location, children }) => {
         if (window.insertSlardarAndHornbill) {
             return;
         }
+
+        // remove the tabIndex of the gatsby-focus-wrapper div, to prevent the focus from starting after a mouse click 
+        const gatsbyFocusWrapper = document.getElementById('gatsby-focus-wrapper');
+        if (gatsbyFocusWrapper) {
+            gatsbyFocusWrapper.removeAttribute('tabIndex');
+        }
     
         Promise.resolve()
             .then(() => {
@@ -144,20 +150,30 @@ const AppLayout = ({ type, location, children }) => {
             <IntlProvider locale={locale} messages={messages}>
                 <LocaleProvider locale={semiLocaleSource}>
                     <div style={{ position: 'fixed', width: '100%', top: 0, zIndex: 999 }}>
+                        <div className="skip-to-content">
+                            <div>{locale === "zh-CN" ? '跳转到:' : 'skip to:'}</div>
+                            <ol>
+                                {
+                                    showSideNav ? (<li><a className="skip-to-content-link" href='#side-nav'>{locale === "zh-CN" ? '跳转到侧边导航' : 'skip to navigation'}</a></li>):null
+                                }
+                                <li><a className="skip-to-content-link" href='#main-content'>{locale === "zh-CN" ? '跳转到主内容' : 'skip to main content'}</a></li>
+                                <li><a className="skip-to-content-link" href='#footer'>{locale === "zh-CN" ? '跳转到页脚' : 'skip to footer'}</a></li>
+                            </ol>
+                        </div>
                         <SemiSiteBanner ref={bannerRef} type="black" style={{ height: 32 }} icon={null} />
                         {/* ssr, can't use location directly, get location from layout and pass to children */}
                         <Header style={headerStyle} location={location} localeCode={locale} />
                     </div>
-                    <div className="content-area" style={contentAeraStyle}>
-                        {children}
-                        {!/showcase|teams/.test(location.pathname) && <Footer />}
-                    </div>
                     {showSideNav ? (
                         <>
                             <SideNav hasBanner={showBanner} type={type} style={sideNavStyle} location={location} edges={data.allMdx.edges} itemsArr={itemsArr} />
                             {/* {footer} */}
                         </>
                     ) : null}
+                    <div className="content-area" style={contentAeraStyle} id="main-content">
+                        {children}
+                        {!/showcase|teams/.test(location.pathname) && <Footer />}
+                    </div>
                 </LocaleProvider>
             </IntlProvider>
         </>

+ 7 - 9
src/components/side-nav.js

@@ -32,7 +32,7 @@ const SideNav = ({ location = null, type = null, itemsArr, edges, style, hasBann
                 navData[categoryIndex].items.push({
                     itemKey: `/${node.fields.slug}`,
                     text: node.frontmatter.title,
-                    icon: <Icon svg={<IconNode />} size={'extra-large'} /> || '',
+                    icon: <Icon svg={<IconNode />} size={'extra-large'} aria-hidden={true} /> || '',
                     order: node.frontmatter.order || 0,
                     // link: `/${node.fields.slug}`,
                     // linkOptions: {
@@ -46,19 +46,17 @@ const SideNav = ({ location = null, type = null, itemsArr, edges, style, hasBann
             }
         });
         return navData.map(category=>{
-            const { items, ...rest } = category
+            const { items, ...rest } = category;
             return (
                 <Nav.Sub {...rest} key={rest.itemKey}>
                     {items.map(item=>{
                         return (
-                            <Link to={item.itemKey} key={item.itemKey} >
-                                <Nav.Item {...item} />
-                            </Link>
-                        )
+                            <Nav.Item {...item} link={item.itemKey} key={item.itemKey} />
+                        );
                     })}
                 </Nav.Sub>
-            )
-        })
+            );
+        });
     }
 
     const computedNavData = useMemo(() => getNavData(itemsArr), [itemsArr, localeCode]);
@@ -95,7 +93,7 @@ const SideNav = ({ location = null, type = null, itemsArr, edges, style, hasBann
     };
 
     return (
-        <div className={'side-nav'} style={style}>
+        <div id="side-nav" className={'side-nav'} style={style}>
             <Nav
                 style={{
                     width: '100%',

+ 3 - 1
src/sitePages/newHome/components/operateButton/operateButton.jsx

@@ -14,7 +14,9 @@ function OperateButton() {
         window.open('https://github.com/DouyinFE/semi-design');
     };
     return (<div className={styles.group2835}>
-        <Button onClick={goStart} size="large" theme="solid" className={styles.extraLarge}>{_t("start_using", { }, "开始使用")}</Button>
+        <a href={`/${getLocale()}/start/getting-started`}>
+            <Button tabindex={-1} onClick={goStart} size="large" theme="solid" className={styles.extraLarge}>{_t("start_using", { }, "开始使用")}</Button>
+        </a>
         <Button
             onClick={goGithub} 
             size="large"

+ 5 - 0
src/sitePages/newHome/components/operateButton/operateButton.module.scss

@@ -5,6 +5,11 @@
     align-items: center;
     height: 48px;
     margin-top: 80px;
+
+    a:focus-visible {
+        border-radius: 6px;
+        outline-offset: -1px;
+    }
 }
 
 .extraLarge {

+ 19 - 2
src/styles/doc.scss

@@ -1,4 +1,8 @@
 // ??? 这里的主要是啥样式,得整理一下。
+a:focus-visible {
+    outline: 2px solid var(--semi-color-primary-light-active);
+    border-radius: 3px;
+}
 
 .header-tinyTitle {
     font-size: 18px;
@@ -42,12 +46,19 @@ a {
     align-items: center;
 
     .anchor-link-button-icon {
-        display: none;
+        display: inline;
+        opacity: 0;
+        &:focus-visible {
+            opacity: 1;
+            border-radius: 3px;
+            outline: 2px solid var(--semi-color-primary-light-active);
+        }
     }
 
     &:hover {
         .anchor-link-button-icon {
             display: inline;
+            opacity: 1;
         }
     }
 }
@@ -56,7 +67,13 @@ a {
     align-items: center;
 
     .anchor-link-button-icon {
-        display: none;
+        display: inline;
+        opacity: 0;
+        &:focus-visible {
+            opacity: 1;
+            border-radius: 3px;
+            outline: 2px solid var(--semi-color-primary-light-active);
+        }
     }
 
     &:hover {

+ 36 - 0
src/styles/layout.scss

@@ -135,6 +135,42 @@ body[theme-mode="dark"] {
     }
 }
 
+.skip-to-content {
+    width: 122px;
+    padding: 12px;
+    position: absolute;
+    left: 30px;
+    transform: translateY(-100%);   
+    transition: transform 0.3s;
+    background-color: #fff;
+    font-size: 14px;
+    border-radius: 3px;
+    z-index: 999;
+
+    ol {
+        margin-top: 0px;
+        padding-left: 0px;
+        list-style-type: none;
+        list-style-position: outside;
+
+        li {
+            margin-top: 8px;
+            line-height: 20px;
+            color: var(--semi-color-primary);
+        }
+    }
+}
+
+.skip-to-content:focus-within {
+    transform: translateY(0%);
+    top: 12px;
+    outline: 2px solid var( --semi-color-border);
+}
+
+.skip-to-content-link:focus {
+    outline: 2px solid var(--semi-color-primary-light-active);
+}
+
 .content-area {
     display: flex;
     justify-content: flex-start;

+ 42 - 27
src/templates/postTemplate.js

@@ -215,6 +215,15 @@ const components = {
     hr: ({ }) => <hr className={'gatsby-hr'} />,
     h2: ({ children }) => {
         const intl = useIntl();
+        const onIconLinkClick = () => {
+            copy(`${window.location.href.replace(window.location.hash, '')}#${window.encodeURI(children)}`);
+            Toast.success({
+                content: intl.formatMessage({
+                    id: 'editor.copy.success',
+                }),
+                duration: 3,
+            });
+        }
         return (
             <h2 className="md markdown gatsby-h2" id={makeAnchorId(children)}>
                 {children}
@@ -241,14 +250,13 @@ const components = {
                 }
                 <IconLink
                     className={'anchor-link-button-icon'}
-                    onClick={() => {
-                        copy(`${window.location.href.replace(window.location.hash, '')}#${window.encodeURI(children)}`);
-                        Toast.success({
-                            content: intl.formatMessage({
-                                id: 'editor.copy.success',
-                            }),
-                            duration: 3,
-                        });
+                    tabIndex={0}
+                    role="button"
+                    onClick={onIconLinkClick}
+                    onKeyPress={(e) => {
+                        if (['Enter', ' '].includes(e?.key)) {
+                            onIconLinkClick(e);
+                    }
                     }}
                 />
             </h2>
@@ -257,19 +265,27 @@ const components = {
     blockquote: ({ children }) => <blockquote className={'gatsby-blockquote'}>{children}</blockquote>,
     h3: ({ children }) => {
         const intl = useIntl();
+        const onIconLinkClick = () => {
+            copy(`${window.location.href.replace(window.location.hash, '')}#${window.encodeURI(children)}`);
+            Toast.success({
+                content: intl.formatMessage({
+                    id: 'editor.copy.success',
+                }),
+                duration: 3,
+            });
+        }
         return (
             <h3 className="md markdown gatsby-h3" id={makeAnchorId(children)}>
                 {children}
                 <IconLink
+                    tabIndex={0}
+                    role="button"
                     className={'anchor-link-button-icon'}
-                    onClick={() => {
-                        copy(`${window.location.href.replace(window.location.hash, '')}#${window.encodeURI(children)}`);
-                        Toast.success({
-                            content: intl.formatMessage({
-                                id: 'editor.copy.success',
-                            }),
-                            duration: 3,
-                        });
+                    onClick={onIconLinkClick}
+                    onKeyPress={(e) => {
+                        if (['Enter', ' '].includes(e?.key)) {
+                            onIconLinkClick(e);
+                        }
                     }}
                 />
             </h3>
@@ -645,6 +661,14 @@ export default function Template(args) {
     return (
         <div className={calcClassName()}>
             <SEO lang="zh-CN" title={`${current.frontmatter.title} - Semi Design`} />
+            <div className={'pageAnchor'}>
+                {(tabValue === 'rd' || (["Accessibility "].includes(enTitle))) && (
+                    <PageAnchor slug={pageContext.slug} data={current.tableOfContents.items} />
+                )}
+                {
+                    iframeAnchorData && tabValue === 'ued' && <DesignPageAnchor data={iframeAnchorData} />
+                }
+            </div>
             <div className="title-area" style={haveUedDoc ? {} : { borderBottom: `1px solid var(--semi-color-border)` }}>
                 <div>
                     {current.frontmatter.draft ? (
@@ -671,7 +695,6 @@ export default function Template(args) {
                         />
                     )}
                 </div>
-
                 {current.fields.type !== 'start' && haveUedDoc && (
                     <Tabs activeKey={tabValue} onTabClick={(key) => {
                         if (key === 'ued') {
@@ -687,19 +710,11 @@ export default function Template(args) {
                             setTabValue('rd');
                         }
                     }}>
-                        <TabPane tab={intl.formatMessage({ id: 'apiDoc' })} itemKey={'rd'} />
-                        <TabPane tab={intl.formatMessage({ id: 'designDoc' })} itemKey={'ued'} />
+                        <TabPane tab={intl.formatMessage({ id: 'apiDoc' })} itemKey={'rd'} tabIndex={-1}/>
+                        <TabPane tab={intl.formatMessage({ id: 'designDoc' })} itemKey={'ued'} tabIndex={-1} />
                     </Tabs>
                 )}
             </div>
-            <div className={'pageAnchor'}>
-                {(tabValue === 'rd' || (["Accessibility "].includes(enTitle))) && (
-                    <PageAnchor slug={pageContext.slug} data={current.tableOfContents.items} />
-                )}
-                {
-                    iframeAnchorData && tabValue === 'ued' && <DesignPageAnchor data={iframeAnchorData} />
-                }
-            </div>
             <div className="main-article">
                 <MDXProvider components={components}>
                     <MDXRenderer>{current.body}</MDXRenderer>

+ 17 - 17
yarn.lock

@@ -1515,10 +1515,10 @@
   dependencies:
     "@babel/runtime-corejs3" "^7.15.4"
 
-"@douyinfe/semi-site-banner@^0.0.2":
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-banner/-/semi-site-banner-0.0.2.tgz#e0cd338e89659c02e45fb7235374407a1a718576"
-  integrity sha512-UxTlAHEXo4fYZOjRtR5MiydJEPHWnqJvyqflixpawm9dpXRkgbdDgMsMimBe5nx0blvWx+Z4+85BAcWhTKLbEQ==
+"@douyinfe/semi-site-banner@^0.1.0":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-banner/-/semi-site-banner-0.1.0.tgz#d6fbffb71a5c3ba88f8148ac6eab0cbfadbc12bd"
+  integrity sha512-/eN6vwo8scZkYFdK0jOdh74IEHq1a0YWWlg2vNVgCEnq978fOYL14jKtJdXvVcOG9+GbESU25c9lQIgk8OfIPQ==
   dependencies:
     "@douyinfe/semi-icons" latest
     "@douyinfe/semi-ui" latest
@@ -1532,10 +1532,10 @@
   resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-doc-style/-/semi-site-doc-style-0.0.1.tgz#c3c803014218ec00441dac32db9a875f6222ed0b"
   integrity sha512-y7Jc1i9q/O2idfaqckSJvghpt4AboQJgZ4iTEK8UMqjQkyWmb5I/NRzVWjOP9S0LEbJNs76OKfZil7DwsOmY/A==
 
-"@douyinfe/semi-site-header@^0.0.10":
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-header/-/semi-site-header-0.0.10.tgz#082a03b4f7efb929e5dd4c743fd434f652083475"
-  integrity sha512-ubJUfm+xM7m3WU7hKcKgC90KtM+O3oP9C2zcZedTCiK11aeYHqDxbmdZwikkfV9ecQR0au0XllmjraTilsW/6A==
+"@douyinfe/semi-site-header@^0.0.13":
+  version "0.0.13"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-header/-/semi-site-header-0.0.13.tgz#2e44fcd59b3112b9e6678334346b35dc2c852e4d"
+  integrity sha512-HYLlYXcS/e3TjQ/etRbYEP7LGyiqPY2fnBzKG4gWlAZ0vq4D6qCmhuliZ2XUKwjXyP19Sx1XqQluowPNXE+EwA==
   dependencies:
     "@douyinfe/semi-icons" "^2.0.0"
     "@douyinfe/semi-ui" "^2.0.0"
@@ -1546,20 +1546,20 @@
     lodash-es "^4.17.21"
     react-i18next "^11.12.0"
 
-"@douyinfe/semi-site-markdown-blocks@^0.0.8":
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-markdown-blocks/-/semi-site-markdown-blocks-0.0.8.tgz#692cf395ce399dc544d1c6ab7310b36c47734724"
-  integrity sha512-7OLcwhwGhzyL//lcgYSQ/pxIa193LVixv/QouN1Tf+ulK4fSz1EfDH3uLE+yC4w2LRqtkLNVkE+t5hVpauDseA==
+"@douyinfe/semi-site-markdown-blocks@^0.0.9":
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-markdown-blocks/-/semi-site-markdown-blocks-0.0.9.tgz#df0c02b0867b08e499143b26a189280c909bb438"
+  integrity sha512-mEKHb9LNlo5QjDK6Rw3RkjGBmE5Ivg5ChCAPeqtNsBkzNcF78D7SvrL5U8U6R/F4iIfTqw7gHfvuMte1L559iw==
   dependencies:
-    "@douyinfe/semi-site-playground" "0.0.8"
+    "@douyinfe/semi-site-playground" "0.0.9"
     classnames "^2.2.6"
     prism-react-renderer "^1.0.1"
     prop-types "^15.7.2"
 
-"@douyinfe/[email protected].8":
-  version "0.0.8"
-  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-playground/-/semi-site-playground-0.0.8.tgz#faf19a7c3e36d2737148e4dda01ea081e1d9a9f0"
-  integrity sha512-u2EF4GNfW5AsCu1HfczIfkyjdoKYxk+jq30lZqQ04hVv9eD65kKMkG6YIPllemLmf63/dG9XBhFMbGPsYO6smw==
+"@douyinfe/[email protected].9":
+  version "0.0.9"
+  resolved "https://registry.yarnpkg.com/@douyinfe/semi-site-playground/-/semi-site-playground-0.0.9.tgz#cb1362c636f73f7251c887a1e2b03e4e34153ee3"
+  integrity sha512-ojp5HQUN/2PD5CQEszUdU5oXsQIYUacZhbzfSw+Y7YdU1UPh/L31GmXfOjXccziIYZoqAyk6foqLIU1wtg+Ipw==
   dependencies:
     "@douyinfe/semi-icons" "^2.0.0"
     "@douyinfe/semi-ui" "^2.0.0"