From 4aedf382d57169c0502965996a196fc7ace58410 Mon Sep 17 00:00:00 2001
From: julianajlk
Date: Tue, 20 Aug 2019 15:58:17 -0400
Subject: [PATCH] feat: Collapsible Component (#504)
Refactor of the Collapsible Component. It is now a composable component and set of subcomponents.
BREAKING CHANGE: The prop api has changed significantly to simplify usages and enable flexibility. See the docs for details.
BREAKING CHANGE: Related styles are included in paragon scss and no longer exist as a sibling to the component.
---
.../__snapshots__/Storyshots.test.js.snap | 206 ++-----------
package-lock.json | 69 ++---
package.json | 4 +-
scss/core/_variables.scss | 7 +
scss/core/extensions/_collapsible.scss | 55 ++++
src/Collapsible/Collapsible.scss | 56 ----
src/Collapsible/Collapsible.stories.jsx | 59 +---
src/Collapsible/Collapsible.test.jsx | 286 +++++++++---------
src/Collapsible/CollapsibleAdvanced.jsx | 111 +++++++
src/Collapsible/CollapsibleBody.jsx | 38 +++
src/Collapsible/CollapsibleTrigger.jsx | 71 +++++
src/Collapsible/CollapsibleVisible.jsx | 32 ++
src/Collapsible/README.md | 42 ---
.../__snapshots__/Collapsible.test.jsx.snap | 208 +++++++++++++
src/Collapsible/index.jsx | 198 ++++--------
src/Dropdown/index.jsx | 1 -
src/index.scss | 2 +-
www/gatsby-node.js | 8 +
www/src/pages/components/collapsible.mdx | 224 ++++++++++++--
www/src/scss/_code-block.scss | 2 +-
www/src/scss/index.scss | 12 +-
21 files changed, 1010 insertions(+), 681 deletions(-)
create mode 100644 scss/core/extensions/_collapsible.scss
delete mode 100644 src/Collapsible/Collapsible.scss
create mode 100644 src/Collapsible/CollapsibleAdvanced.jsx
create mode 100644 src/Collapsible/CollapsibleBody.jsx
create mode 100644 src/Collapsible/CollapsibleTrigger.jsx
create mode 100644 src/Collapsible/CollapsibleVisible.jsx
delete mode 100644 src/Collapsible/README.md
create mode 100644 src/Collapsible/__snapshots__/Collapsible.test.jsx.snap
diff --git a/.storybook/__snapshots__/Storyshots.test.js.snap b/.storybook/__snapshots__/Storyshots.test.js.snap
index f7fa86b127..8f5f29eac6 100644
--- a/.storybook/__snapshots__/Storyshots.test.js.snap
+++ b/.storybook/__snapshots__/Storyshots.test.js.snap
@@ -382,214 +382,58 @@ Array [
]
`;
-exports[`Storyshots Collapsible basic usage with resizing 1`] = `
-
-
-
- Try resizing the screen to medium or small
-
-
-
-
- You can fit lots of things in here
-
-
-
- 1 thing
-
-
- 2 things
-
-
- 3 things
-
-
-
-
-
-
-`;
-
-exports[`Storyshots Collapsible basic usage without resizing 1`] = `
+exports[`Storyshots Collapsible usage 1`] = `
-
-
- Click me to expand
-
-
-
-
-
-
- Your stuff goes here
-
-
-
-`;
-
-exports[`Storyshots Collapsible fires onToggle callback when toggled 1`] = `
-
-
-
+
- Click me to expand
-
-
-
-
- Your stuff goes here
-
+
-
-`;
-
-exports[`Storyshots Collapsible initially open collapsible 1`] = `
-
-
-
- Click me to expand
-
-
-
-
-
-
- Your stuff goes here
-
-
-
-`;
-
-exports[`Storyshots Collapsible with custom icon 1`] = `
-
-
- Click me to expand
-
-
-
+
-
-
-
- Your stuff goes here
-
`;
diff --git a/package-lock.json b/package-lock.json
index fdb128f5e7..d4c1a669e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1409,24 +1409,24 @@
"dev": true
},
"@fortawesome/fontawesome-common-types": {
- "version": "0.2.19",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.19.tgz",
- "integrity": "sha512-nd2Ul/CUs8U9sjofQYAALzOGpgkVJQgEhIJnOHaoyVR/LeC3x2mVg4eB910a4kS6WgLPebAY0M2fApEI497raQ=="
+ "version": "0.2.21",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.21.tgz",
+ "integrity": "sha512-iJtcrU2BtF9Wyr0zm3tHEJy3HqA6sADExhCqCv3SKaJJKKp4ORJ40t4nyHvcWXSVFtd7r1gcdqcRsAfoREGTFA=="
},
"@fortawesome/fontawesome-svg-core": {
- "version": "1.2.19",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.19.tgz",
- "integrity": "sha512-D4ICXg9oU08eF9o7Or392gPpjmwwgJu8ecCFusthbID95CLVXOgIyd4mOKD9Nud5Ckz+Ty59pqkNtThDKR0erA==",
+ "version": "1.2.21",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.21.tgz",
+ "integrity": "sha512-EhrgMZLJS0tTYZhUbodurZBqDgAFLDNdxJP/q5unrZJwiFo8Dd7xGvJdhAhY5WcX4khzkPQcbLTCMPHBtutD7Q==",
"requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.19"
+ "@fortawesome/fontawesome-common-types": "^0.2.21"
}
},
"@fortawesome/free-solid-svg-icons": {
- "version": "5.9.0",
- "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.9.0.tgz",
- "integrity": "sha512-U8YXPfWcSozsCW0psCtlRGKjjRs5+Am5JJwLOUmVHFZbIEWzaz4YbP84EoPwUsVmSAKrisu3QeNcVOtmGml0Xw==",
+ "version": "5.10.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.10.1.tgz",
+ "integrity": "sha512-MKH+SCt0DnVoXdemxf6JEdTRtCPwYLMCWZcwgGccYU/ab6QcDtbAMn6Xm4Zub6YqQCcaiy0hU294YdHOldSBRA==",
"requires": {
- "@fortawesome/fontawesome-common-types": "^0.2.19"
+ "@fortawesome/fontawesome-common-types": "^0.2.21"
}
},
"@fortawesome/react-fontawesome": {
@@ -9092,8 +9092,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"aproba": {
"version": "1.2.0",
@@ -9114,14 +9113,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -9136,20 +9133,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"core-util-is": {
"version": "1.0.2",
@@ -9266,8 +9260,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"ini": {
"version": "1.3.5",
@@ -9279,7 +9272,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -9294,7 +9286,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -9302,14 +9293,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -9328,7 +9317,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -9409,8 +9397,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"object-assign": {
"version": "4.1.1",
@@ -9422,7 +9409,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"wrappy": "1"
}
@@ -9508,8 +9494,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -9545,7 +9530,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -9565,7 +9549,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
- "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -9609,14 +9592,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
- "dev": true,
- "optional": true
+ "dev": true
}
}
},
@@ -13622,7 +13603,7 @@
"dependencies": {
"semver": {
"version": "5.3.0",
- "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true
}
@@ -13788,7 +13769,7 @@
},
"chalk": {
"version": "1.1.3",
- "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@@ -20341,7 +20322,7 @@
},
"os-locale": {
"version": "1.4.0",
- "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
+ "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
"dev": true,
"requires": {
diff --git a/package.json b/package.json
index f3a752d3a1..8bdcd57dba 100644
--- a/package.json
+++ b/package.json
@@ -35,8 +35,8 @@
"travis-deploy-once": "travis-deploy-once"
},
"dependencies": {
- "@fortawesome/fontawesome-svg-core": "^1.2.18",
- "@fortawesome/free-solid-svg-icons": "^5.8.2",
+ "@fortawesome/fontawesome-svg-core": "^1.2.21",
+ "@fortawesome/free-solid-svg-icons": "^5.10.1",
"@fortawesome/react-fontawesome": "^0.1.4",
"airbnb-prop-types": "^2.12.0",
"bootstrap": "^4.3.1",
diff --git a/scss/core/_variables.scss b/scss/core/_variables.scss
index cfaa047948..17e9fd8fb6 100644
--- a/scss/core/_variables.scss
+++ b/scss/core/_variables.scss
@@ -754,6 +754,13 @@ $card-columns-gap: 1.25rem !default;
$card-columns-margin: $card-spacer-y !default;
+// Collapsible
+
+$collapsible-card-spacer-y: .45rem !default;
+$collapsible-card-spacer-x: .75rem !default;
+$collapsible-card-spacer-y-lg: $card-spacer-y !default;
+$collapsible-card-spacer-x-lg: $card-spacer-x !default;
+
// Tooltips
$tooltip-font-size: $font-size-sm !default;
diff --git a/scss/core/extensions/_collapsible.scss b/scss/core/extensions/_collapsible.scss
new file mode 100644
index 0000000000..0ebb02176f
--- /dev/null
+++ b/scss/core/extensions/_collapsible.scss
@@ -0,0 +1,55 @@
+.collapsible-card {
+ @extend .card;
+
+ .collapsible-trigger {
+ padding: $collapsible-card-spacer-y $collapsible-card-spacer-x;
+ border-radius: $card-inner-border-radius;
+ border-bottom: $card-border-width solid transparent;
+ transition: border-color 100ms ease 150ms;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ & > * {
+ margin-bottom: 0;
+ margin-top: 0;
+ }
+
+ &[aria-expanded="true"] {
+ border-radius: $card-inner-border-radius $card-inner-border-radius 0 0;
+ border-color: $card-border-color;
+ transition: none;
+ }
+ }
+
+ .collapsible-body {
+ @extend .card-body;
+ padding: $collapsible-card-spacer-y $collapsible-card-spacer-x;
+ & > *:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.collapsible-card-lg {
+ @extend .collapsible-card;
+
+ .collapsible-trigger {
+ padding: $collapsible-card-spacer-y-lg $collapsible-card-spacer-x-lg;
+ }
+
+ .collapsible-body {
+ padding: $collapsible-card-spacer-y-lg $collapsible-card-spacer-x-lg;
+ }
+}
+
+.collapsible-basic {
+ .collapsible-trigger {
+ display: flex;
+ cursor: pointer;
+ align-items: center;
+ text-decoration: underline;
+ color: theme-color('primary', 'default');
+ }
+}
diff --git a/src/Collapsible/Collapsible.scss b/src/Collapsible/Collapsible.scss
deleted file mode 100644
index f91d2778ef..0000000000
--- a/src/Collapsible/Collapsible.scss
+++ /dev/null
@@ -1,56 +0,0 @@
-// Local Variables
-$collapsible-border: 1px solid silver;
-
-.collapsible {
- border-radius: 4px;
- border: 1px solid transparent;
- transition: border 0.3s ease;
-
- .btn-collapsible {
- border: $collapsible-border;
- white-space: normal;
- background-color: white;
-
- &:hover {
- background-color: #f5f8ff;
- }
- }
-
- .btn-collapsible.open {
- border: 1px solid transparent;
- }
-
- .collapsible-body {
- visibility: collapse;
- opacity: 0;
- overflow: hidden;
- padding: 0 15px;
- max-height: 0;
- transition: all 0.3s ease;
- }
-
- &.open {
- border: $collapsible-border;
-
- .collapsible-body {
- visibility: visible;
- opacity: 1;
- max-height: 99999px; // Set to a very high number since we don't know how large the body will be
- padding: 15px;
- }
-
- .btn-collapsible {
- background-color: white;
- }
- }
-
- &.expanded {
- .collapsible-body {
- visibility: visible;
- opacity: 1;
- max-height: none;
- padding: 0;
- transition: none;
- }
- }
-}
diff --git a/src/Collapsible/Collapsible.stories.jsx b/src/Collapsible/Collapsible.stories.jsx
index 41f28ed1a0..48f9109d72 100644
--- a/src/Collapsible/Collapsible.stories.jsx
+++ b/src/Collapsible/Collapsible.stories.jsx
@@ -1,60 +1,19 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
-import { faChevronCircleDown, faChevronCircleUp } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-
-import README from './README.md';
import Collapsible from './index';
-import { breakpoints } from '../Responsive';
storiesOf('Collapsible', module)
- .addParameters({ info: { text: README } })
- .add('basic usage without resizing', () => (
-
- Your stuff goes here
-
- ))
- .add('basic usage with resizing', () => (
-
-
Try resizing the screen to medium or small}
- title="Try resizing the screen to large"
- isCollapsible={() => global.innerWidth >= breakpoints.large.minWidth ||
- global.matchMedia(`(min-width: ${breakpoints.large.minWidth}px)`).matches}
- >
-
-
You can fit lots of things in here
-
- 1 thing
- 2 things
- 3 things
-
-
-
-
- ))
- .add('initially open collapsible', () => (
-
- Your stuff goes here
-
- ))
- .add('fires onToggle callback when toggled', () => (
+ .add('usage', () => (
console.log(`this.state.isOpen = ${isOpen}`)} // eslint-disable-line no-console
+ onToggle={isOpen => console.log('Collapsible toggled and open is: ', isOpen)}
+ onOpen={() => console.log('Collapsible opened.')}
+ onClose={() => console.log('Collapsible closed.')}
>
- Your stuff goes here
-
- ))
- .add('with custom icon', () => (
- ,
- collapsed: ,
- }}
- >
- Your stuff goes here
+ Your stuff goes here.
+
+
+ Close
+
));
diff --git a/src/Collapsible/Collapsible.test.jsx b/src/Collapsible/Collapsible.test.jsx
index 6afb224efc..786123bbca 100644
--- a/src/Collapsible/Collapsible.test.jsx
+++ b/src/Collapsible/Collapsible.test.jsx
@@ -1,173 +1,187 @@
import React from 'react';
import { mount } from 'enzyme';
-import { faChevronCircleUp, faChevronCircleDown } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import renderer from 'react-test-renderer';
+import Collapsible from '../Collapsible';
-import { breakpoints } from '../Responsive';
-import '../__mocks__/reactResponsive.mock';
-import Collapsible from '../Collapsible';
+const collapsibleContent = (
+
+
+ A heading
+
+
+ +
+ -
+
+
+
+ Close
+ Open
-const childElements = (
-
+ }>
+
+ Example content
+
+
+
);
-const defaultProps = {
- title: 'Collapsible',
- children: childElements,
+const collapsibleIsOpen = (wrapper) => {
+ expect(wrapper.find('.example-content').length).toEqual(1);
};
-describe(' ', () => {
- describe('without resizing', () => {
- describe('correct rendering', () => {
- it('renders in closed form by default', () => {
- const wrapper = mount( );
+const collapsibleIsClosed = (wrapper) => {
+ expect(wrapper.find('.example-content').length).toEqual(0);
+};
- expect(wrapper.find('.collapsible').exists()).toEqual(true);
- expect(wrapper.find('.collapsible.open').exists()).toEqual(false);
- expect(wrapper.find('.btn-collapsible').exists()).toEqual(true);
- expect(wrapper.find('.btn-collapsible.open').exists()).toEqual(false);
- expect(wrapper.find('button').prop('aria-expanded')).toEqual(false);
+describe(' ', () => {
+ describe('Uncontrolled Rendering', () => {
+ it('renders closed by default', () => {
+ const tree = renderer.create((
+
+ {collapsibleContent}
+
+ )).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ it('renders open by default', () => {
+ const tree = renderer.create((
+
+ {collapsibleContent}
+
+ )).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ });
- expect(wrapper.find('.collapsible-body.open').exists()).toEqual(false);
- });
+ describe('Controlled Rendering', () => {
+ it('renders closed by default', () => {
+ const tree = renderer.create((
+
+ {collapsibleContent}
+
+ )).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ it('renders open by default', () => {
+ const tree = renderer.create((
+
+ {collapsibleContent}
+
+ )).toJSON();
+ expect(tree).toMatchSnapshot();
+ });
+ });
- it('renders in open form if specified open', () => {
- const wrapper = mount( );
+ describe('Imperative Methods', () => {
+ const wrapper = mount({collapsibleContent} );
+ const collapsible = wrapper.instance();
- expect(wrapper.find('.collapsible.open').exists()).toEqual(true);
- expect(wrapper.find('.btn-collapsible.open').exists()).toEqual(true);
- expect(wrapper.find('button').prop('aria-expanded')).toEqual(true);
- expect(wrapper.find('.collapsible-body.open').exists()).toEqual(true);
- });
+ collapsibleIsClosed(wrapper);
- it('changes the isOpen state if the isOpen prop changes', () => {
- const wrapper = mount( );
- expect(wrapper.instance().state.isOpen).toBe(true);
- wrapper.setProps({ isOpen: false });
- expect(wrapper.instance().state.isOpen).toBe(false);
- });
+ it('opens on .open()', () => {
+ collapsible.open();
+ wrapper.update();
+ collapsibleIsOpen(wrapper);
+ });
+
+ it('closes on .close()', () => {
+ collapsible.close();
+ wrapper.update();
+ collapsibleIsClosed(wrapper);
+ });
+ });
- it('renders the title on the open/close button', () => {
- const wrapper = mount( );
+ describe('Mouse Interactions', () => {
+ const wrapper = mount({collapsibleContent} );
+ const collapsible = wrapper.instance();
+ const trigger = wrapper.find('.trigger[role="button"]');
+ const closeOnlyTrigger = wrapper.find('.close-only[role="button"]');
+ const openOnlyTrigger = wrapper.find('.open-only[role="button"]');
- expect(wrapper.find('.collapsible-title').text()).toEqual(defaultProps.title);
- });
+ it('opens on trigger click', () => {
+ trigger.simulate('click'); // Open
+ collapsibleIsOpen(wrapper);
});
- it('does not change to expanded form on resizing window', () => {
- // Change to a small window and it should show the collapsible button
- global.innerWidth = breakpoints.small.minWidth;
- let wrapper = mount( );
- expect(wrapper.find('.btn-collapsible').exists()).toEqual(true);
+ it('closes on trigger click', () => {
+ trigger.simulate('click'); // Close
+ collapsibleIsClosed(wrapper);
+ });
- // Change to a large window and it should still show the collapsible button
- global.innerWidth = breakpoints.large.minWidth;
- wrapper = mount( );
- expect(wrapper.find('.btn-collapsible').exists()).toEqual(true);
+ it('does not open on close only trigger click', () => {
+ collapsible.close();
+ wrapper.update();
+ closeOnlyTrigger.simulate('click'); // No-op
+ collapsibleIsClosed(wrapper);
});
- it('open to show the body when the collapsible button is clicked', () => {
- const wrapper = mount( );
+ it('closes on close only trigger click', () => {
+ collapsible.open();
+ wrapper.update();
+ closeOnlyTrigger.simulate('click'); // Close
+ collapsibleIsClosed(wrapper);
+ });
- expect(wrapper.find('.collapsible-body.open').exists()).toEqual(false);
+ it('does not close on open only trigger click', () => {
+ collapsible.open();
+ wrapper.update();
+ openOnlyTrigger.simulate('click'); // No-op
+ collapsibleIsOpen(wrapper);
+ });
- wrapper.find('button').simulate('click');
- expect(wrapper.find('.btn-collapsible.open').exists()).toEqual(true);
- expect(wrapper.find('button').prop('aria-expanded')).toEqual(true);
- expect(wrapper.find('.collapsible-body.open').exists()).toEqual(true);
+ it('opens on opens only trigger click', () => {
+ collapsible.close();
+ wrapper.update();
+ openOnlyTrigger.simulate('click'); // Open
+ collapsibleIsOpen(wrapper);
});
+ });
- it('calls the onToggle callback correctly', () => {
- const spy = jest.fn();
- const wrapper = mount( );
+ describe('Keyboard Interactions', () => {
+ const wrapper = mount({collapsibleContent} );
+ const collapsible = wrapper.instance();
+ const trigger = wrapper.find('.trigger[role="button"]');
+ const closeOnlyTrigger = wrapper.find('.close-only[role="button"]');
+ const openOnlyTrigger = wrapper.find('.open-only[role="button"]');
- expect(spy).toHaveBeenCalledTimes(0);
+ it('opens on trigger enter keydown', () => {
+ trigger.simulate('keyDown', { key: 'Enter' }); // Open
+ collapsibleIsOpen(wrapper);
+ });
- wrapper.find('button').simulate('click');
- expect(spy).toHaveBeenCalledTimes(1);
- expect(spy).toHaveBeenCalledWith(true);
+ it('closes on trigger enter keydown', () => {
+ trigger.simulate('keyDown', { key: 'Enter' }); // Close
+ collapsibleIsClosed(wrapper);
+ });
- wrapper.find('button').simulate('click');
- expect(spy).toHaveBeenCalledTimes(2);
- expect(spy).toHaveBeenCalledWith(false);
+ it('does not open on close only trigger enter keydown', () => {
+ collapsible.close();
+ wrapper.update();
+ closeOnlyTrigger.simulate('keyDown', { key: 'Enter' }); // No-op
+ collapsibleIsClosed(wrapper);
});
- it('uses icon elements if they are supplied', () => {
- const wrapper = mount((
- ,
- collapsed: ,
- }}
- />
- ));
+ it('closes on close only trigger enter keydown', () => {
+ collapsible.open();
+ wrapper.update();
+ closeOnlyTrigger.simulate('keyDown', { key: 'Enter' }); // Close
+ collapsibleIsClosed(wrapper);
+ });
- expect(wrapper.find('FontAwesomeIcon').prop('icon').iconName).toEqual('chevron-circle-down');
- wrapper.find('.btn-collapsible').first().simulate('click');
- expect(wrapper.find('FontAwesomeIcon').prop('icon').iconName).toEqual('chevron-circle-up');
+ it('does not close on open only trigger enter keydown', () => {
+ collapsible.open();
+ wrapper.update();
+ openOnlyTrigger.simulate('keyDown', { key: 'Enter' }); // No-op
+ collapsibleIsOpen(wrapper);
});
- });
- describe('with resizing', () => {
- const expandFunction = () => global.innerWidth >= breakpoints.large.minWidth;
- const expandedTitle = Collapsible ;
-
- beforeEach(() => {
- global.innerWidth = breakpoints.large.minWidth;
- });
-
- describe('correct rendering', () => {
- it('renders in expanded form without a title by default', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('.expanded-title').exists()).toEqual(false);
- expect(wrapper.find('.btn-collapsible').exists()).toEqual(false);
- expect(wrapper.find('.collapsible.open').exists()).toEqual(false);
-
- expect(wrapper.find('.collapsible-body').exists()).toEqual(true);
- });
-
- it('renders in expanded form with a title if given one', () => {
- const wrapper = mount( );
-
- expect(wrapper.find('.expanded-title').exists()).toEqual(true);
- expect(wrapper.find('.collapsible.open').exists()).toEqual(false);
-
- expect(wrapper.find('.collapsible-body').exists()).toEqual(true);
- });
- });
-
- it('shows the expanded form for large windows, and the collapsible for smaller windows', () => {
- // Change to a small window to view the collapsible button
- global.innerWidth = breakpoints.small.minWidth;
- let wrapper = mount( );
- expect(wrapper.find('.expanded-title').exists()).toEqual(false);
- expect(wrapper.find('.btn-collapsible').exists()).toEqual(true);
- expect(wrapper.find('.collapsible-body.open').exists()).toEqual(false);
-
- // Change back to a large window to see the expanded view again
- global.innerWidth = breakpoints.large.minWidth;
- wrapper = mount( );
- expect(wrapper.find('.expanded-title').exists()).toEqual(true);
- expect(wrapper.find('.btn-collapsible').exists()).toEqual(false);
- expect(wrapper.find('.collapsible-body.open').exists()).toEqual(true);
+ it('opens on opens only trigger enter keydown', () => {
+ collapsible.close();
+ wrapper.update();
+ openOnlyTrigger.simulate('keyDown', { key: 'Enter' }); // Open
+ collapsibleIsOpen(wrapper);
});
});
});
diff --git a/src/Collapsible/CollapsibleAdvanced.jsx b/src/Collapsible/CollapsibleAdvanced.jsx
new file mode 100644
index 0000000000..19f97b8243
--- /dev/null
+++ b/src/Collapsible/CollapsibleAdvanced.jsx
@@ -0,0 +1,111 @@
+import React from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+
+export const CollapsibleContext = React.createContext();
+
+class CollapsibleAdvanced extends React.Component {
+ static getDerivedStateFromProps(props) {
+ if (props.open !== undefined) {
+ return {
+ // Since this method fires on both props and state changes, local updates
+ // to the controlled value will be ignored, because the props version
+ // always overrides it. In this case, this is exactly what we want.
+ isOpen: props.open,
+ };
+ }
+ return null;
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isOpen: props.open !== undefined ? props.open : props.defaultOpen,
+ };
+ }
+
+ open = () => {
+ this.setState({ isOpen: true });
+
+ if (this.props.onOpen) {
+ this.props.onOpen();
+ }
+ }
+
+ close = () => {
+ this.setState({ isOpen: false });
+
+ if (this.props.onClose) {
+ this.props.onClose();
+ }
+ }
+
+ toggle = () => {
+ if (this.state.isOpen) {
+ this.close();
+ } else {
+ this.open();
+ }
+
+ if (this.props.onToggle) {
+ this.props.onToggle(this.state.isOpen);
+ }
+ }
+
+ render() {
+ const {
+ children,
+ className,
+ ...props
+ } = this.props;
+
+ // Unneeded for passthrough props
+ delete props.defaultOpen;
+ delete props.onToggle;
+ delete props.onOpen;
+ delete props.onClose;
+
+ return (
+
+
+ {children}
+
+
+ );
+ }
+}
+
+CollapsibleAdvanced.propTypes = {
+ children: PropTypes.node,
+ className: PropTypes.string,
+ defaultOpen: PropTypes.bool,
+ open: PropTypes.bool,
+ onToggle: PropTypes.func,
+ onOpen: PropTypes.func,
+ onClose: PropTypes.func,
+};
+
+CollapsibleAdvanced.defaultProps = {
+ children: undefined,
+ className: undefined,
+ defaultOpen: false,
+ open: undefined,
+ onToggle: undefined,
+ onOpen: undefined,
+ onClose: undefined,
+};
+
+export default CollapsibleAdvanced;
diff --git a/src/Collapsible/CollapsibleBody.jsx b/src/Collapsible/CollapsibleBody.jsx
new file mode 100644
index 0000000000..2c7be8c9c6
--- /dev/null
+++ b/src/Collapsible/CollapsibleBody.jsx
@@ -0,0 +1,38 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+
+import { CollapsibleContext } from './CollapsibleAdvanced';
+import TransitionReplace from '../TransitionReplace';
+
+function CollapsibleBody({
+ children, transitionWrapper, tag, ...props
+}) {
+ const { isOpen } = useContext(CollapsibleContext);
+
+ // Keys are added to these elements so that TransitionReplace
+ // will recognize them as unique components and perform the
+ // transition properly.
+ const content = isOpen ?
+ React.createElement(tag, { key: 'body', ...props }, children) :
+
;
+
+ if (transitionWrapper) {
+ return React.cloneElement(transitionWrapper, {}, content);
+ }
+ /* istanbul ignore next */
+ return {content} ;
+}
+
+CollapsibleBody.propTypes = {
+ children: PropTypes.node,
+ tag: PropTypes.string,
+ transitionWrapper: PropTypes.element,
+};
+
+CollapsibleBody.defaultProps = {
+ children: undefined,
+ tag: 'div',
+ transitionWrapper: undefined,
+};
+
+export default CollapsibleBody;
diff --git a/src/Collapsible/CollapsibleTrigger.jsx b/src/Collapsible/CollapsibleTrigger.jsx
new file mode 100644
index 0000000000..8bb447bf73
--- /dev/null
+++ b/src/Collapsible/CollapsibleTrigger.jsx
@@ -0,0 +1,71 @@
+import React, { useContext, useCallback } from 'react';
+import PropTypes from 'prop-types';
+
+import { CollapsibleContext } from './CollapsibleAdvanced';
+
+function CollapsibleTrigger({
+ tag, children, openOnly, closeOnly, ...props
+}) {
+ const {
+ isOpen, open, close, toggle,
+ } = useContext(CollapsibleContext);
+
+ const handleToggle = (e) => {
+ if (openOnly) {
+ open(e);
+ } else if (closeOnly) {
+ close(e);
+ } else {
+ toggle(e);
+ }
+ };
+
+ const handleClick = useCallback((e) => {
+ if (props.onClick) {
+ props.onClick(e);
+ }
+ handleToggle(e);
+ });
+
+ const handleKeyDown = useCallback((e) => {
+ if (props.onKeyDown) {
+ props.onKeyDown(e);
+ }
+ if (e.key === 'Enter') {
+ handleToggle(e);
+ }
+ });
+
+ return React.createElement(
+ tag,
+ {
+ ...props,
+ onClick: handleClick,
+ onKeyDown: handleKeyDown,
+ role: 'button',
+ tabIndex: 0,
+ 'aria-expanded': isOpen,
+ },
+ children,
+ );
+}
+
+CollapsibleTrigger.propTypes = {
+ children: PropTypes.node,
+ tag: PropTypes.string,
+ openOnly: PropTypes.bool,
+ closeOnly: PropTypes.bool,
+ onClick: PropTypes.func,
+ onKeyDown: PropTypes.func,
+};
+
+CollapsibleTrigger.defaultProps = {
+ children: undefined,
+ tag: 'div',
+ openOnly: false,
+ closeOnly: false,
+ onClick: undefined,
+ onKeyDown: undefined,
+};
+
+export default CollapsibleTrigger;
diff --git a/src/Collapsible/CollapsibleVisible.jsx b/src/Collapsible/CollapsibleVisible.jsx
new file mode 100644
index 0000000000..e8929a0595
--- /dev/null
+++ b/src/Collapsible/CollapsibleVisible.jsx
@@ -0,0 +1,32 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+
+import { CollapsibleContext } from './CollapsibleAdvanced';
+
+function CollapsibleVisible({
+ children,
+ whenOpen: visibleWhenOpen,
+ whenClosed: visibleWhenClosed,
+}) {
+ const { isOpen } = useContext(CollapsibleContext);
+ const isVisible = (isOpen && visibleWhenOpen) || (!isOpen && visibleWhenClosed);
+
+ if (isVisible) {
+ return {children} ;
+ }
+ return null;
+}
+
+CollapsibleVisible.propTypes = {
+ children: PropTypes.node,
+ whenOpen: PropTypes.bool,
+ whenClosed: PropTypes.bool,
+};
+
+CollapsibleVisible.defaultProps = {
+ children: undefined,
+ whenOpen: false,
+ whenClosed: false,
+};
+
+export default CollapsibleVisible;
diff --git a/src/Collapsible/README.md b/src/Collapsible/README.md
deleted file mode 100644
index 66273ea167..0000000000
--- a/src/Collapsible/README.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Collapsible Component
-
-Provides a component that can collapse to hide its child elements and
-optionally has the ability to display without the open/close button based on the
-screen size. The collapsible functionality mimics that of an accordion section,
-with the exception that multiple collapsibles can be open at the same time.
-
-Note: The CSS is required for the hide/show functionality and animations to
-work properly.
-
-## API
-
-### `children` (object; required)
-`children` are the objects that are the children of the collapsible that should
-be hidden when the collapsible is closed.
-
-### `title` (string; required)
-`title` is the string to be displayed on the collapsible button.
-
-### `expandedTitle` (element; optional)
-`expandedTitle` is the element to be displayed as the title when the collapsible
-is expanded. Defaults to undefined.
-
-### `isOpen` (boolean; optional)
-`isOpen` specifies whether the collapsible should initially be open. Defaults
-to false.
-
-### `isCollapsible` (function; optional)
-`isCollapsible` is the optional function that, if given, will be used on
-resize to determine whether to display the collapsible or regular view. The
-example below demonstrates a collapsible that will only show the open/close
-button for non-desktop screens.
-
-If no function is given, the collapsible does not handle resizing and will
-always show the open/close button.
-
-### `iconId` (string; optional)
-`iconId` is the id attribute that is passed to the icon on the collapsible.
-Defaults to the empty string.
-
-### `onToggle` (function; optional)
-`onToggle` is an optional callback that is trigged when the Collapsible components is opened or closed. A boolean is passed to the callback with the value of `isOpen` from the component's state.
\ No newline at end of file
diff --git a/src/Collapsible/__snapshots__/Collapsible.test.jsx.snap b/src/Collapsible/__snapshots__/Collapsible.test.jsx.snap
new file mode 100644
index 0000000000..f0a624ab3b
--- /dev/null
+++ b/src/Collapsible/__snapshots__/Collapsible.test.jsx.snap
@@ -0,0 +1,208 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` Controlled Rendering renders closed by default 1`] = `
+
+
+
+ A heading
+
+
+ +
+
+
+
+ Close
+
+
+ Open
+
+
+
+`;
+
+exports[` Controlled Rendering renders open by default 1`] = `
+
+
+
+ A heading
+
+
+ +
+
+
+
+ Close
+
+
+ Open
+
+
+
+`;
+
+exports[` Uncontrolled Rendering renders closed by default 1`] = `
+
+
+
+ A heading
+
+
+ +
+
+
+
+ Close
+
+
+ Open
+
+
+
+`;
+
+exports[` Uncontrolled Rendering renders open by default 1`] = `
+
+
+
+ A heading
+
+
+ -
+
+
+
+ Close
+
+
+ Open
+
+
+
+`;
diff --git a/src/Collapsible/index.jsx b/src/Collapsible/index.jsx
index 0b3e014a4d..ff4c586508 100644
--- a/src/Collapsible/index.jsx
+++ b/src/Collapsible/index.jsx
@@ -1,143 +1,79 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
-import { faAngleUp, faAngleDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ faPlusCircle,
+ faMinusCircle,
+ faPlus,
+ faMinus,
+} from '@fortawesome/free-solid-svg-icons';
+
+import CollapsibleAdvanced from './CollapsibleAdvanced';
+import CollapsibleBody from './CollapsibleBody';
+import CollapsibleTrigger from './CollapsibleTrigger';
+import CollapsibleVisible from './CollapsibleVisible';
+
+const styleIcons = {
+ basic: {
+ iconWhenClosed: ,
+ iconWhenOpen: ,
+ },
+ // card and card-lg use the defaults specified in defaultProps
+};
-import Button from '../Button';
-
-class Collapsible extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- isExpanded: false,
- isOpen: props.isOpen,
- };
-
- this.handleClick = this.handleClick.bind(this);
- }
-
- componentDidMount() {
- if (this.props.isCollapsible) {
- this.handleResize();
- global.addEventListener('resize', this.handleResize.bind(this));
- }
- }
-
- /**
- * "Note that you may call setState() immediately in componentDidUpdate() but,
- * it must be wrapped in a conditional check against the previous props, or
- * you'll cause an infinite loop."
- * See https://reactjs.org/docs/react-component.html#componentdidupdate for
- * more information.
- */
- componentDidUpdate(prevProps) {
- if (this.props.isOpen !== prevProps.isOpen) {
- // eslint-disable-next-line react/no-did-update-set-state
- this.setState({
- isOpen: this.props.isOpen,
- });
- }
- }
-
- componentWillUnmount() {
- if (this.props.isCollapsible) {
- global.removeEventListener('resize', this.handleResize);
- }
- }
-
- handleResize() {
- const { isExpanded } = this.state;
-
- if (isExpanded !== this.props.isCollapsible()) {
- this.setState({
- isExpanded: !isExpanded,
- });
- }
- }
-
- handleClick() {
- const isOpen = !this.state.isOpen;
- this.setState({ isOpen });
- this.props.onToggle(isOpen);
- }
-
- renderIcon() {
- const { icons } = this.props;
- const { isOpen } = this.state;
-
- if (icons) {
- return isOpen ? icons.expanded : icons.collapsed;
- }
-
- return ;
- }
-
- render() {
- const {
- children,
- expandedTitle,
- title,
- } = this.props;
-
- const { isExpanded, isOpen } = this.state;
-
- return (
-
- {isExpanded ? (
- expandedTitle
- ) : (
-
-
- {title}
- {this.renderIcon()}
-
-
- )}
-
- {children}
-
-
- );
- }
-}
+const Collapsible = React.forwardRef((props, ref) => {
+ const {
+ children,
+ className,
+ title,
+ styling,
+ iconWhenClosed,
+ iconWhenOpen,
+ ...other
+ } = props;
+
+ const icons = Object.assign({ iconWhenClosed, iconWhenOpen }, styleIcons[styling]);
+ const titleElement = React.isValidElement(title) ? title : {title} ;
+
+ return (
+
+
+ {titleElement}
+
+ {icons.iconWhenClosed}
+ {icons.iconWhenOpen}
+
+
+
+ {children}
+
+ );
+});
Collapsible.propTypes = {
- children: PropTypes.instanceOf(Object).isRequired,
- expandedTitle: PropTypes.element,
- icons: PropTypes.shape({
- expanded: PropTypes.element.isRequired,
- collapsed: PropTypes.element.isRequired,
- }),
- isCollapsible: PropTypes.func,
- isOpen: PropTypes.bool,
- onToggle: PropTypes.func,
- title: PropTypes.string.isRequired,
+ children: PropTypes.node.isRequired,
+ className: PropTypes.string,
+ title: PropTypes.node.isRequired,
+ styling: PropTypes.oneOf(['basic', 'card', 'card-lg']),
+ iconWhenClosed: PropTypes.element,
+ iconWhenOpen: PropTypes.element,
};
-
Collapsible.defaultProps = {
- expandedTitle: undefined,
- icons: null,
- isCollapsible: undefined,
- isOpen: false,
- onToggle: () => {},
+ className: undefined,
+ styling: 'card',
+ iconWhenClosed: ,
+ iconWhenOpen: ,
+
};
+Collapsible.Advanced = CollapsibleAdvanced;
+Collapsible.Body = CollapsibleBody;
+Collapsible.Trigger = CollapsibleTrigger;
+Collapsible.Visible = CollapsibleVisible;
+
export default Collapsible;
diff --git a/src/Dropdown/index.jsx b/src/Dropdown/index.jsx
index 68dbfe6ed1..aa6eb0d954 100644
--- a/src/Dropdown/index.jsx
+++ b/src/Dropdown/index.jsx
@@ -8,7 +8,6 @@ import DropdownItem from './DropdownItem';
import withDeprecatedProps, { DEPR_TYPES } from '../withDeprecatedProps';
const { Provider, Consumer } = React.createContext();
-// const DropdownContext = React.createContext();
class Dropdown extends React.Component {
static idCounter = 0; // For creating unique ids
diff --git a/src/index.scss b/src/index.scss
index 68637b60bb..0049634de6 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -1,5 +1,5 @@
@import './asInput/asInput.scss';
-@import './Collapsible/Collapsible.scss';
+@import '../scss/core/extensions/collapsible.scss';
@import './Fieldset/Fieldset.scss';
@import './Modal/Modal.scss';
@import './Pagination/Pagination.scss';
diff --git a/www/gatsby-node.js b/www/gatsby-node.js
index 3b80e2ec6e..a6b0cc9051 100644
--- a/www/gatsby-node.js
+++ b/www/gatsby-node.js
@@ -10,6 +10,14 @@ exports.onCreateWebpackConfig = ({ actions }) => {
alias: {
'~paragon-react': path.resolve(__dirname, '../src'),
'~paragon-style': path.resolve(__dirname, '../scss'),
+ // Prevent multiple copies of react getting loaded
+ // paragon react components would naturally import
+ // react and react-dom from the node_modules folder
+ // one level above if it is present. This approach forces
+ // all uses of react and react-dom to resolve to those
+ // in ./node_modules
+ 'react': path.resolve(__dirname, 'node_modules/react/'),
+ 'react-dom': path.resolve(__dirname, 'node_modules/react-dom/'),
},
},
});
diff --git a/www/src/pages/components/collapsible.mdx b/www/src/pages/components/collapsible.mdx
index 58093eccde..faa1c4e957 100644
--- a/www/src/pages/components/collapsible.mdx
+++ b/www/src/pages/components/collapsible.mdx
@@ -1,78 +1,236 @@
---
title: "Collapsible"
type: "component"
-status: "Needs Work"
-designStatus: "To Do"
-devStatus: "To Do"
-notes: |
- Transition to fully uncontrolled component and add imperative .open().
- Remove use of Button component, it breaks with new styles.
- Assess removing the responsive toggle to stay expanded.
+status: "Stable"
+designStatus: "Needs Review"
+devStatus: "Done"
+notes:
---
import { StaticQuery, graphql } from 'gatsby';
import { Collapsible } from '~paragon-react';
+import { Icons } from '~paragon-react';
import PropsTable from '../../components/PropsTable';
# Collapsible
+### Basic Usage
-##### basic usage without resizing
+The `styling` prop at the top level ` ` component determines if the collapsible has basic styling, card, or card with heading.
+
+##### Basic Style ` `
```jsx live=true
-
- Your stuff goes here
+
+ Your stuff goes here.
```
-##### basic usage with resizing
+##### Card Style ` `
-[TODO]
+This is the default style if no `styling` prop is supplied.
+```jsx live=true
+Toggle Collapsible
}
+ className="shadow"
+>
+ Your stuff goes here.
+
+```
-##### initially open collapsible
+##### Large Card Style ` `
```jsx live=true
-
- Your stuff goes here
+Toggle Collapsible}
+ className="shadow"
+>
+ Your stuff goes here.
```
-
-##### fires onToggle callback when toggled
+##### Card with custom icons ` `
```jsx live=true
console.log(`this.state.isOpen = ${isOpen}`)} // eslint-disable-line no-console
+ styling="card"
+ title={Toggle Collapsible
}
+ iconWhenOpen={CLOSE SESAME }
+ iconWhenClosed={OPEN SESAME }
+ className="shadow"
>
- Your stuff goes here
+ Your stuff goes here.
```
+##### Default Open
-##### with custom icon
+```jsx live=true
+
+ Your stuff goes here.
+
+```
+
+
+
+### Advanced Usage
+
+For needs that deviate from the three styles above, use ` `
+
+##### Bare minimum
```jsx live=true
- ,
- collapsed: ,
- }}
+
+
+ Toggle Collapsible
+
+
+ Your stuff goes here
+
+
+```
+
+##### Card style with advanced usage
+
+```jsx live=true
+
+
+ This is the title
+ +
+ -
+
+
+
+ The content
+
+
+```
+
+##### With a close button
+
+```jsx live=true
+
+
+ This is the title
+ +
+ -
+
+
+
+ The content
+
+
+ Close
+
+
+
+```
+
+
+##### onOpen, onClose and onToggle callbacks
+
+See the developer console for logging.
+
+```jsx live=true
+ console.log('Collapsible toggled and open is: ', isOpen)}
+ onOpen={() => console.log('Collapsible opened.')}
+ onClose={() => console.log('Collapsible closed.')}
>
- Your stuff goes here
-
+
+ I'm a heading
+
+
+ +
+
+
+
+ -
+
+
+
+
+ Your stuff goes here.
+
+
+ Close
+
+
+
+```
+
+### Controlled usage
+
+```jsx live=true
+function() {
+ const [collapseIsOpen, setCollapseOpen] = React.useState(true);
+
+ return (
+ setCollapseOpen(!isOpen)}
+ className="collapsible-card"
+ >
+
+ I'm a heading
+
+
+ +
+
+
+
+ -
+
+
+
+
+ Your stuff goes here.
+
+
+ Close
+
+
+
+ );
+}
```
+### Imperative methods
+
+If you need to open or close the Collapsible intermittently due to an event,
+such as loading the page or clicking a link, you can open or close
+an **uncontrolled** Collapsible by getting a ref to the component and calling
+`collapsibleRef.open()` or `collapsibleRef.close()`. The internal state of
+the component will be updated accordingly.
+
+
##### Props
{
- if (componentMetadata == null) return null;
- return ;
+ render={({ collapsible, collapsibleTrigger, collapsibleBody, collapsibleVisible }) => {
+ return (
+
+ {collapsible ?
: null}
+
Collapsible.Trigger
+ {collapsibleTrigger ?
: null}
+
Collapsible.Body
+ {collapsibleBody ?
: null}
+
Collapsible.Visible
+ {collapsibleVisible ?
: null}
+
+ )
}}
/>
diff --git a/www/src/scss/_code-block.scss b/www/src/scss/_code-block.scss
index 5fa240f31e..cdbc4398a6 100644
--- a/www/src/scss/_code-block.scss
+++ b/www/src/scss/_code-block.scss
@@ -1,5 +1,5 @@
.pgn-doc__code-block {
- margin-bottom: 1rem;
+ margin-bottom: 4rem;
}
.pgn-doc__code-block-preview {
padding: 1rem 0;
diff --git a/www/src/scss/index.scss b/www/src/scss/index.scss
index b66671a2fe..32d3dc98f9 100644
--- a/www/src/scss/index.scss
+++ b/www/src/scss/index.scss
@@ -9,10 +9,16 @@
// Typography
.pgn-doc__main-content {
- h1, h2, h3, h4, h5 {
- margin-top: 2em;
+ h1 {
+ margin: 4rem 0;
}
-
+
+ & > div {
+ & > h1, & > h2, & > h3, & > h4, & > h5 {
+ margin-top: 2em;
+ }
+ }
+
// Typography Page
.demo-georgia {