]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11014 Create a collapsible component
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Fri, 20 Jul 2018 14:11:03 +0000 (16:11 +0200)
committerSonarTech <sonartech@sonarsource.com>
Wed, 25 Jul 2018 18:21:21 +0000 (20:21 +0200)
server/sonar-docs/gatsby-config.js
server/sonar-docs/src/images/close.svg [new file with mode: 0644]
server/sonar-docs/src/images/open.svg [new file with mode: 0644]
server/sonar-docs/src/templates/page.css
server/sonar-docs/src/templates/page.js
server/sonar-web/src/main/js/apps/documentation/styles.css
server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/docs/DocMarkdownBlock.tsx
server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap [new file with mode: 0644]

index 19d540c5c43a3ab5c5973da60fef5b80bdd9d6aa..831aa76758d6ac65c3f33acd7513b1340bea374b 100644 (file)
@@ -42,7 +42,8 @@ module.exports = {
                 danger: 'alert alert-danger',
                 warning: 'alert alert-warning',
                 info: 'alert alert-info',
-                success: 'alert alert-success'
+                success: 'alert alert-success',
+                collapse: 'collapse'
               }
             }
           }
diff --git a/server/sonar-docs/src/images/close.svg b/server/sonar-docs/src/images/close.svg
new file mode 100644 (file)
index 0000000..10b0b0a
--- /dev/null
@@ -0,0 +1 @@
+<svg width="16" height="16"  viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M11.596 8.28l-4.604 4.602a.382.382 0 0 1-.279.118.382.382 0 0 1-.279-.118l-1.03-1.03a.382.382 0 0 1-.117-.278c0-.108.04-.201.117-.28L8.7 8 5.404 4.706a.382.382 0 0 1-.117-.28c0-.108.04-.2.117-.279l1.03-1.03A.382.382 0 0 1 6.714 3c.107 0 .2.04.278.118l4.604 4.603a.382.382 0 0 1 .117.279c0 .108-.04.201-.117.28z" fill="#236a97" /></svg>
\ No newline at end of file
diff --git a/server/sonar-docs/src/images/open.svg b/server/sonar-docs/src/images/open.svg
new file mode 100644 (file)
index 0000000..b5d7e4c
--- /dev/null
@@ -0,0 +1 @@
+<svg width="16" height="16"  viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.414"><path d="M7.72 11.596L3.119 6.992A.382.382 0 0 1 3 6.713c0-.108.04-.2.118-.279l1.03-1.03a.382.382 0 0 1 .278-.117c.108 0 .201.04.28.117L8 8.7l3.294-3.295a.382.382 0 0 1 .28-.117c.108 0 .2.04.279.117l1.03 1.03a.382.382 0 0 1 .117.28c0 .107-.04.2-.118.278L8.28 11.596a.382.382 0 0 1-.279.117.382.382 0 0 1-.28-.117z" fill="#236a97" /></svg>
\ No newline at end of file
index cf8ff3dd405270eaedec39f2f9fb884390268102..1e6c4dd8de57fa526fcab9620628c0fffeebfcec 100644 (file)
   background-color: #dff0d8;
   color: #3c763d;
 }
+
+.collapse {
+  border: 1px solid #e6e6e6;
+  border-radius: 2px;
+  background-color: #f3f3f3;
+  padding: 8px;
+  margin: 0 -1em 1.5rem;
+}
+
+.collapse > a:first-child {
+  background: url(../images/open.svg) no-repeat 0 50%;
+  padding-left: 20px;
+  display: block;
+  color: #236a97;
+  display: block;
+  cursor: pointer;
+  margin-bottom: 0.5rem;
+  font-size: 16px;
+  text-decoration: none;
+}
+
+.collapse.close > a:first-child {
+  background: url(../images/close.svg) no-repeat 0 50%;
+}
+
+.collapse.close > * {
+  display: none;
+}
+
+.collapse.close > a:first-child {
+  margin: 0;
+}
+
+.collapse *:last-child {
+  margin-bottom: 0;
+}
+
+.collapse .alert {
+  margin: 0 0.5em 1.5rem;
+}
index 45040ee4807c009b2372eff634cf9d3b87b58dd2..2c04417fe558da891a2b47723957d78d30f87100 100644 (file)
@@ -21,43 +21,57 @@ import React from 'react';
 import Helmet from 'react-helmet';
 import './page.css';
 
-export default ({ data }) => {
-  const page = data.markdownRemark;
-  let htmlWithInclusions = cutSonarCloudContent(page.html).replace(
-    /\<p\>@include (.*)\<\/p\>/,
-    (_, path) => {
-      const chunk = data.allMarkdownRemark.edges.find(
-        edge => edge.node.fields && edge.node.fields.slug === path
-      );
-      return chunk ? chunk.node.html : '';
+export default class Page extends React.PureComponent {
+  componentDidMount() {
+    const collaspables = document.getElementsByClassName('collapse');
+    for (let i = 0; i < collaspables.length; i++) {
+      collaspables[i].classList.add('close');
+      collaspables[i].firstChild.outerHTML = collaspables[i].firstChild.outerHTML
+        .replace(/\<h2/gi, '<a href="#"')
+        .replace(/\<\/h2\>/gi, '</a>');
+      collaspables[i].firstChild.addEventListener('click', e => {
+        e.currentTarget.parentNode.classList.toggle('close');
+        e.preventDefault();
+      });
     }
-  );
-
-  if (
-    page.headings &&
-    page.headings.length > 0 &&
-    page.html.match(/<h[1-9]>Table Of Contents<\/h[1-9]>/i)
-  ) {
-    htmlWithInclusions = generateTableOfContents(htmlWithInclusions, page.headings);
   }
 
-  return (
-    <div css={{ paddingTop: 24, paddingBottom: 24 }}>
-      <Helmet title={page.frontmatter.title} />
-      <h1>{page.frontmatter.title}</h1>
-      <div
-        css={{
-          '& img[src$=".svg"]': {
-            position: 'relative',
-            top: '-2px',
-            verticalAlign: 'text-bottom'
-          }
-        }}
-        dangerouslySetInnerHTML={{ __html: htmlWithInclusions }}
-      />
-    </div>
-  );
-};
+  render() {
+    const page = this.props.data.markdownRemark;
+    let htmlWithInclusions = cutSonarCloudContent(page.html).replace(
+      /\<p\>@include (.*)\<\/p\>/,
+      (_, path) => {
+        const chunk = data.allMarkdownRemark.edges.find(edge => edge.node.fields.slug === path);
+        return chunk ? chunk.node.html : '';
+      }
+    );
+
+    if (
+      page.headings &&
+      page.headings.length > 0 &&
+      page.html.match(/<h[1-9]>Table Of Contents<\/h[1-9]>/i)
+    ) {
+      htmlWithInclusions = generateTableOfContents(htmlWithInclusions, page.headings);
+    }
+
+    return (
+      <div css={{ paddingTop: 24, paddingBottom: 24 }}>
+        <Helmet title={page.frontmatter.title} />
+        <h1>{page.frontmatter.title}</h1>
+        <div
+          css={{
+            '& img[src$=".svg"]': {
+              position: 'relative',
+              top: '-2px',
+              verticalAlign: 'text-bottom'
+            }
+          }}
+          dangerouslySetInnerHTML={{ __html: htmlWithInclusions }}
+        />
+      </div>
+    );
+  }
+}
 
 export const query = graphql`
   query PageQuery($slug: String!) {
index 04941089c06b21270b43b29f4d53e74c2f2b899c..5eb00833df48866d919a36c8c6c3cdadb563f1dc 100644 (file)
 .documentation-content.markdown .alert p {
   margin: 0;
 }
+
+.documentation-content.markdown .collapse-container {
+  border: 1px solid var(--barBorderColor);
+  border-radius: 2px;
+  background-color: var(--barBackgroundColor);
+  padding: 8px;
+  margin: 0.8em 0 2em;
+}
+
+.documentation-content.markdown .collapse-container > a:first-child {
+  display: block;
+}
+
+.documentation-content.markdown .collapse-container > a:first-child:focus {
+  color: var(--darkBlue);
+}
+
+.documentation-content.markdown .collapse-container *:last-child {
+  margin-bottom: 0;
+}
diff --git a/server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx b/server/sonar-web/src/main/js/components/docs/DocCollapsibleBlock.tsx
new file mode 100644 (file)
index 0000000..47b8c96
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import OpenCloseIcon from '../icons-components/OpenCloseIcon';
+
+interface State {
+  open: boolean;
+}
+
+export default class DocCollapsibleBlock extends React.PureComponent<{}, State> {
+  state = { open: false };
+
+  handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    this.setState(state => ({ open: !state.open }));
+    event.stopPropagation();
+    event.preventDefault();
+  };
+
+  renderTitle(children: any) {
+    return (
+      <a
+        aria-expanded={String(this.state.open)}
+        aria-haspopup={true}
+        className="link-no-underline"
+        href="#"
+        onClick={this.handleClick}>
+        <OpenCloseIcon className="vertical-middle little-spacer-right" open={this.state.open} />
+        {children.props ? children.props.children : children}
+      </a>
+    );
+  }
+
+  render() {
+    const childrenAsArray = React.Children.toArray(this.props.children);
+    if (childrenAsArray.length < 1) {
+      return null;
+    }
+
+    const firstChildChildren = React.Children.toArray(
+      (childrenAsArray[0] as React.ReactElement<any>).props.children
+    );
+    if (firstChildChildren.length < 2) {
+      return null;
+    }
+
+    return (
+      <div className="collapse-container">
+        {this.renderTitle(firstChildChildren[0])}
+        {this.state.open && firstChildChildren.slice(1)}
+      </div>
+    );
+  }
+}
index 72978acb947f40fe22daf9c15437ba68669804fd..bb033ac299943ce153544fdd7059bdab25a137d2 100644 (file)
@@ -26,6 +26,7 @@ import remarkCustomBlocks from 'remark-custom-blocks';
 import DocLink from './DocLink';
 import DocImg from './DocImg';
 import DocTooltipLink from './DocTooltipLink';
+import DocCollapsibleBlock from './DocCollapsibleBlock';
 import { separateFrontMatter, filterContent } from '../../helpers/markdown';
 import { scrollToElement } from '../../helpers/scrolling';
 
@@ -63,7 +64,8 @@ export default class DocMarkdownBlock extends React.PureComponent<Props> {
               danger: { classes: 'alert alert-danger' },
               warning: { classes: 'alert alert-warning' },
               info: { classes: 'alert alert-info' },
-              success: { classes: 'alert alert-success' }
+              success: { classes: 'alert alert-success' },
+              collapse: { classes: 'collapse' }
             })
             .use(reactRenderer, {
               remarkReactComponents: {
@@ -96,7 +98,11 @@ function withChildProps<P>(
 
 function Block(props: React.HtmlHTMLAttributes<HTMLDivElement>) {
   if (props.className) {
-    return <div className={classNames('cut-margins', props.className)}>{props.children}</div>;
+    if (props.className.includes('collapse')) {
+      return <DocCollapsibleBlock>{props.children}</DocCollapsibleBlock>;
+    } else {
+      return <div className={classNames('cut-margins', props.className)}>{props.children}</div>;
+    }
   } else {
     return props.children;
   }
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx b/server/sonar-web/src/main/js/components/docs/__tests__/DocCollapsibleBlock-test.tsx
new file mode 100644 (file)
index 0000000..c18666d
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import DocCollapsibleBlock from '../DocCollapsibleBlock';
+import { click } from '../../../helpers/testUtils';
+
+const children = (
+  <div>
+    <h2>Foo</h2>
+    <p>Bar</p>
+  </div>
+);
+
+it('should render a collapsible block', () => {
+  const wrapper = shallow(<DocCollapsibleBlock>{children}</DocCollapsibleBlock>);
+  expect(wrapper).toMatchSnapshot();
+
+  click(wrapper.find('a'));
+  wrapper.update();
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('should not render if not at least 2 children', () => {
+  const wrapper = shallow(
+    <DocCollapsibleBlock>
+      <div>foobar</div>
+    </DocCollapsibleBlock>
+  );
+  expect(wrapper).toMatchSnapshot();
+});
diff --git a/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/docs/__tests__/__snapshots__/DocCollapsibleBlock-test.tsx.snap
new file mode 100644 (file)
index 0000000..7632671
--- /dev/null
@@ -0,0 +1,48 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should not render if not at least 2 children 1`] = `""`;
+
+exports[`should render a collapsible block 1`] = `
+<div
+  className="collapse-container"
+>
+  <a
+    aria-expanded="false"
+    aria-haspopup={true}
+    className="link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <OpenCloseIcon
+      className="vertical-middle little-spacer-right"
+      open={false}
+    />
+    Foo
+  </a>
+</div>
+`;
+
+exports[`should render a collapsible block 2`] = `
+<div
+  className="collapse-container"
+>
+  <a
+    aria-expanded="true"
+    aria-haspopup={true}
+    className="link-no-underline"
+    href="#"
+    onClick={[Function]}
+  >
+    <OpenCloseIcon
+      className="vertical-middle little-spacer-right"
+      open={true}
+    />
+    Foo
+  </a>
+  <p
+    key=".1"
+  >
+    Bar
+  </p>
+</div>
+`;