Skip to content

Commit

Permalink
✨ [bento][amp-accordion] Add Imperative API for amp-accordion with Te…
Browse files Browse the repository at this point in the history
…sts and Storybook (#30754)

* Draft imperative api changes for accordion

* Basic outline of preact imperative API

* Next revision accordion

* Imperative API for AMP, tests, and storybook

* Fix a few small items

* Fix a few small items

* Address review comments

* Updates for gulp check-types

* Mark forbidden terms as *OK*
  • Loading branch information
krdwan committed Oct 30, 2020
1 parent 45036a8 commit 135ab67
Show file tree
Hide file tree
Showing 8 changed files with 613 additions and 45 deletions.
105 changes: 94 additions & 11 deletions extensions/amp-accordion/1.0/accordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import * as Preact from '../../../src/preact';
import {animateCollapse, animateExpand} from './animations';
import {forwardRef} from '../../../src/preact/compat';
import {omit} from '../../../src/utils/object';
import {
randomIdGenerator,
Expand All @@ -25,6 +26,7 @@ import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useLayoutEffect,
useMemo,
useRef,
Expand All @@ -44,16 +46,20 @@ const generateRandomId = randomIdGenerator(100000);

/**
* @param {!AccordionDef.Props} props
* @param {{current: (!AccordionDef.AccordionApi|null)}} ref
* @return {PreactDef.Renderable}
*/
export function Accordion({
as: Comp = 'section',
expandSingleSection = false,
animate = false,
children,
id,
...rest
}) {
function AccordionWithRef(
{
as: Comp = 'section',
expandSingleSection = false,
animate = false,
children,
id,
...rest
},
ref
) {
const [expandedMap, setExpandedMap] = useState(EMPTY_EXPANDED_MAP);
const [randomPrefix] = useState(generateRandomId);
const prefix = id || `a${randomPrefix}`;
Expand Down Expand Up @@ -97,16 +103,87 @@ export function Accordion({
[expandSingleSection]
);

const isExpanded = useCallback(
(id, defaultExpanded) => expandedMap[id] ?? defaultExpanded,
[expandedMap]
);

const toggle = useCallback(
(id) => {
if (id) {
if (id in expandedMap) {
toggleExpanded(id);
}
} else {
// Toggle all should do nothing when expandSingleSection is true
if (!expandSingleSection) {
for (const k in expandedMap) {
toggleExpanded(k);
}
}
}
},
[expandedMap, toggleExpanded, expandSingleSection]
);

const expand = useCallback(
(id) => {
if (id) {
if (!isExpanded(id, true)) {
toggleExpanded(id);
}
} else {
// Expand all should do nothing when expandSingleSection is true
if (!expandSingleSection) {
for (const k in expandedMap) {
if (!isExpanded(k, true)) {
toggleExpanded(k);
}
}
}
}
},
[expandedMap, toggleExpanded, isExpanded, expandSingleSection]
);

const collapse = useCallback(
(id) => {
if (id) {
if (isExpanded(id, false)) {
toggleExpanded(id);
}
} else {
for (const k in expandedMap) {
if (isExpanded(k, false)) {
toggleExpanded(k);
}
}
}
},
[expandedMap, toggleExpanded, isExpanded]
);

useImperativeHandle(
ref,
() =>
/** @type {!AccordionDef.AccordionApi} */ ({
toggle,
expand,
collapse,
}),
[toggle, collapse, expand]
);

const context = useMemo(
() =>
/** @type {!AccordionDef.ContextProps} */ ({
registerSection,
toggleExpanded,
isExpanded: (id, defaultExpanded) => expandedMap[id] ?? defaultExpanded,
isExpanded,
animate,
prefix,
}),
[animate, expandedMap, registerSection, toggleExpanded, prefix]
[animate, registerSection, toggleExpanded, prefix, isExpanded]
);

return (
Expand All @@ -118,6 +195,10 @@ export function Accordion({
);
}

const Accordion = forwardRef(AccordionWithRef);
Accordion.displayName = 'Accordion'; // Make findable for tests.
export {Accordion};

/**
* @param {string} id
* @param {boolean} value
Expand Down Expand Up @@ -152,11 +233,13 @@ export function AccordionSection({
animate: defaultAnimate = false,
headerClassName = '',
contentClassName = '',
id: propId,
header,
children,
...rest
}) {
const [id] = useState(generateSectionId);
const [genId] = useState(generateSectionId);
const id = propId || genId;
const [suffix] = useState(generateRandomId);
const [expandedState, setExpandedState] = useState(defaultExpanded);
const contentRef = useRef(null);
Expand Down
18 changes: 18 additions & 0 deletions extensions/amp-accordion/1.0/accordion.type.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,21 @@ AccordionDef.ContentProps;
* }}
*/
AccordionDef.ContextProps;

/** @interface */
AccordionDef.AccordionApi = class {
/**
* @param {string|undefined} section
*/
toggle(section) {}

/**
* @param {string|undefined} section
*/
expand(section) {}

/**
* @param {string|undefined} section
*/
collapse(section) {}
};
12 changes: 12 additions & 0 deletions extensions/amp-accordion/1.0/amp-accordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,20 @@ const HEADER_SHIM_PROP = '__AMP_H_SHIM';
const CONTENT_SHIM_PROP = '__AMP_C_SHIM';
const SECTION_POST_RENDER = '__AMP_PR';

/** @extends {PreactBaseElement<AccordionDef.AccordionApi>} */
class AmpAccordion extends PreactBaseElement {
/** @override */
init() {
this.registerApiAction('toggle', (api, invocation) =>
api./*OK*/ toggle(invocation.args && invocation.args['section'])
);
this.registerApiAction('expand', (api, invocation) =>
api./*OK*/ expand(invocation.args && invocation.args['section'])
);
this.registerApiAction('collapse', (api, invocation) =>
api./*OK*/ collapse(invocation.args && invocation.args['section'])
);

const {element} = this;

const mu = new MutationObserver(() => {
Expand Down Expand Up @@ -94,6 +105,7 @@ function getState(element, mu) {
'headerAs': headerShim,
'contentAs': contentShim,
'expanded': expanded,
'id': section.getAttribute('id'),
});
return <AccordionSection {...props} />;
});
Expand Down
54 changes: 37 additions & 17 deletions extensions/amp-accordion/1.0/storybook/Basic.amp.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,43 @@ export default {
export const _default = () => {
const expandSingleSection = boolean('expandSingleSection', false);
const animate = boolean('animate', false);
const createApiString = (action, section) =>
`tap:accordion.${action}(${section ? `section='${section}'` : ''})`;
return (
<amp-accordion
expand-single-section={expandSingleSection}
animate={animate}
>
<section>
<h2>Section 1</h2>
<p>Content in section 1.</p>
</section>
<section>
<h2>Section 2</h2>
<div>Content in section 2.</div>
</section>
<section expanded>
<h2>Section 3</h2>
<div>Content in section 3.</div>
</section>
</amp-accordion>
<main>
<amp-accordion
id="accordion"
expand-single-section={expandSingleSection}
animate={animate}
>
<section id="section1">
<h2>Section 1</h2>
<p>Content in section 1.</p>
</section>
<section>
<h2>Section 2</h2>
<div>Content in section 2.</div>
</section>
<section expanded>
<h2>Section 3</h2>
<div>Content in section 3.</div>
</section>
</amp-accordion>

<div class="buttons" style={{marginTop: 8}}>
<button on={createApiString('toggle', 'section1')}>
toggle(section1)
</button>
<button on={createApiString('toggle')}>toggle all</button>
<button on={createApiString('expand', 'section1')}>
expand(section1)
</button>
<button on={createApiString('expand')}>expand all</button>
<button on={createApiString('collapse', 'section1')}>
collapse(section1)
</button>
<button on={createApiString('collapse')}>collapse all</button>
</div>
</main>
);
};
58 changes: 47 additions & 11 deletions extensions/amp-accordion/1.0/storybook/Basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,56 @@ export default {
decorators: [withA11y, withKnobs],
};

/**
* @param {!Object} props
* @return {*}
*/
function AccordionWithActions(props) {
// TODO(#30447): replace imperative calls with "button" knobs when the
// Storybook 6.1 is released.
const ref = Preact.useRef();
return (
<section>
<Accordion ref={ref} {...props} />
<div style={{marginTop: 8}}>
<button onClick={() => ref.current./*OK*/ toggle('section1')}>
toggle(section1)
</button>
<button onClick={() => ref.current./*OK*/ toggle()}>toggle all</button>
<button onClick={() => ref.current./*OK*/ expand('section1')}>
expand(section1)
</button>
<button onClick={() => ref.current./*OK*/ expand()}>expand all</button>
<button onClick={() => ref.current./*OK*/ collapse('section1')}>
collapse(section1)
</button>
<button onClick={() => ref.current./*OK*/ collapse()}>
collapse all
</button>
</div>
</section>
);
}

export const _default = () => {
const expandSingleSection = boolean('expandSingleSection', false);
const animate = boolean('animate', false);
return (
<Accordion expandSingleSection={expandSingleSection} animate={animate}>
<AccordionSection key={1} header={<h2>Section 1</h2>}>
<p>Content in section 1.</p>
</AccordionSection>
<AccordionSection key={2} header={<h2>Section 2</h2>}>
<div>Content in section 2.</div>
</AccordionSection>
<AccordionSection key={3} expanded header={<h2>Section 3</h2>}>
<div>Content in section 2.</div>
</AccordionSection>
</Accordion>
<main>
<AccordionWithActions
expandSingleSection={expandSingleSection}
animate={animate}
>
<AccordionSection id="section1" key={1} header={<h2>Section 1</h2>}>
<p>Content in section 1.</p>
</AccordionSection>
<AccordionSection key={2} header={<h2>Section 2</h2>}>
<div>Content in section 2.</div>
</AccordionSection>
<AccordionSection key={3} expanded header={<h2>Section 3</h2>}>
<div>Content in section 2.</div>
</AccordionSection>
</AccordionWithActions>
</main>
);
};

0 comments on commit 135ab67

Please sign in to comment.