Skip to content

Add toggleMode to accordion and accordion section #2559

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

maddy531
Copy link

@maddy531 maddy531 commented Apr 16, 2025

Summary:

This PR updates the Accordion and AccordionSection so that sections can be nested and exposes an internal toggle handler.

Updates:

  • Nesting is now supported.
  • The toggle handler is exposed for the consumer.
  • They can programmatically control open/close states.

Screenshots:

Screen.Recording.2025-04-15.at.9.50.50.PM.mov

Issue: CLASS-11631

Test plan:

@maddy531 maddy531 self-assigned this Apr 16, 2025
Copy link

changeset-bot bot commented Apr 16, 2025

🦋 Changeset detected

Latest commit: b5a04ab

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@khanacademy/wonder-blocks-accordion Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

github-actions bot commented Apr 16, 2025

Size Change: +218 B (+0.22%)

Total Size: 100 kB

Filename Size Change
packages/wonder-blocks-accordion/dist/es/index.js 3.21 kB +218 B (+7.28%) 🔍
ℹ️ View Unchanged
Filename Size
packages/wonder-blocks-announcer/dist/es/index.js 1.74 kB
packages/wonder-blocks-badge/dist/es/index.js 1.75 kB
packages/wonder-blocks-banner/dist/es/index.js 1.42 kB
packages/wonder-blocks-birthday-picker/dist/es/index.js 1.91 kB
packages/wonder-blocks-breadcrumbs/dist/es/index.js 723 B
packages/wonder-blocks-button/dist/es/index.js 4.12 kB
packages/wonder-blocks-cell/dist/es/index.js 2.06 kB
packages/wonder-blocks-clickable/dist/es/index.js 2.67 kB
packages/wonder-blocks-core/dist/es/index.js 2.48 kB
packages/wonder-blocks-data/dist/es/index.js 5.48 kB
packages/wonder-blocks-dropdown/dist/es/index.js 16.6 kB
packages/wonder-blocks-form/dist/es/index.js 4.94 kB
packages/wonder-blocks-grid/dist/es/index.js 1.24 kB
packages/wonder-blocks-icon-button/dist/es/index.js 3.05 kB
packages/wonder-blocks-icon/dist/es/index.js 2.02 kB
packages/wonder-blocks-labeled-field/dist/es/index.js 2.92 kB
packages/wonder-blocks-layout/dist/es/index.js 1.63 kB
packages/wonder-blocks-link/dist/es/index.js 1.64 kB
packages/wonder-blocks-modal/dist/es/index.js 4.99 kB
packages/wonder-blocks-pill/dist/es/index.js 1.31 kB
packages/wonder-blocks-popover/dist/es/index.js 4.32 kB
packages/wonder-blocks-progress-spinner/dist/es/index.js 1.48 kB
packages/wonder-blocks-search-field/dist/es/index.js 1.09 kB
packages/wonder-blocks-styles/dist/es/index.js 467 B
packages/wonder-blocks-switch/dist/es/index.js 1.55 kB
packages/wonder-blocks-tabs/dist/es/index.js 3.67 kB
packages/wonder-blocks-testing-core/dist/es/index.js 3.51 kB
packages/wonder-blocks-testing/dist/es/index.js 985 B
packages/wonder-blocks-theming/dist/es/index.js 577 B
packages/wonder-blocks-timing/dist/es/index.js 1.37 kB
packages/wonder-blocks-tokens/dist/es/index.js 4.82 kB
packages/wonder-blocks-toolbar/dist/es/index.js 900 B
packages/wonder-blocks-tooltip/dist/es/index.js 6.42 kB
packages/wonder-blocks-typography/dist/es/index.js 1.15 kB

compressed-size-action

Copy link
Contributor

github-actions bot commented Apr 16, 2025

A new build was pushed to Chromatic! 🚀

https://5e1bf4b385e3fb0020b7073c-mluqphpong.chromatic.com/

Chromatic results:

Metric Total
Captured snapshots 404
Tests with visual changes 3
Total stories 634
Inherited (not captured) snapshots [TurboSnap] 0
Tests on the build 404

@maddy531 maddy531 requested review from jandrade and nishasy April 16, 2025 02:01
@maddy531 maddy531 marked this pull request as ready for review April 16, 2025 02:01
@khan-actions-bot khan-actions-bot requested a review from a team April 16, 2025 02:01
@khan-actions-bot
Copy link
Contributor

Gerald

Required Reviewers
  • @Khan/wonder-blocks for changes to .changeset/smooth-actors-mix.md, __docs__/wonder-blocks-accordion/accordion.stories.tsx, packages/wonder-blocks-accordion/src/components/accordion-section-header.tsx, packages/wonder-blocks-accordion/src/components/accordion-section.tsx, packages/wonder-blocks-accordion/src/components/accordion.tsx, packages/wonder-blocks-accordion/src/components/__tests__/accordion.test.tsx

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

Copy link
Contributor

github-actions bot commented Apr 16, 2025

npm Snapshot: Published

🎉 Good news!! We've packaged up the latest commit from this PR (e9e7c62) and published all packages with changesets to npm.

You can install the packages in frontend by running:

./dev/tools/deploy_wonder_blocks.js --tag="PR2559"

Packages can also be installed manually by running:

pnpm add @khanacademy/wonder-blocks-<package-name>@PR2559

key={`nested-section-1`}
caretPosition="start"
>
<AccordionSection
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a small margin around the inner nested sections so it's visually more clear that they're nested?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely!

Copy link
Member

@marcysutton marcysutton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for contributing this PR! The toggleMode functionality is going to be super useful. I had some feedback primarily on nested accordions and tests!

key={`nested-section-1`}
caretPosition="start"
>
<AccordionSection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Looking at the rendered markup, it would be helpful to have Accordions output in a nested UL and LIs. That would indicate the structure semantically. Is there a way to handle this under the hood and make it automatic for consumers?

Design-wise, the nested accordions aren't super clear that they're nested beyond the caret icons and a subtle outline. You. might already be working on this, but can we make the nesting more obvious somehow? Some design ideas could include indentation or other spacing, or background/foreground colors.

</View>

<View>
<LabelLarge>Nested Accordion Example</LabelLarge>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Even though this uses toggle mode, could the nested example be broken out into a separate story? It would make it easier to find!

expect(await screen.findByText("Content 1")).toBeVisible();
expect(await screen.findByText("Content 2")).toBeVisible();

// Act: now re-render with toggleMode="collapse-all"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Can you break this out into a separate test to isolate the assertions? Since the first set of expectations are testing the same visibility, perhaps they could also be combined into one group with a different query selector, like findAllByRole?

There are some tips and reasoning in this doc: https://khanacademy.atlassian.net/wiki/spaces/ENG/pages/98402353/Unit+Testing+Best+Practices#Assert-One-Requirement-Per-Test

@@ -73,6 +83,26 @@ type Props = AriaProps & {
* Custom styles for the overall accordion container.
*/
style?: StyleType;

/**
* Called whenever the accordion’s overall expansion state may have changed,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Thanks for these comments! They are super helpful!

@@ -17,7 +17,7 @@ type Props = {
// Unique ID for this section's button.
id: string;
// Header content.
header: string | React.ReactElement;
header: string | React.ReactElement | TemplateStringsArray;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: I'm curious, are there specific cases where TemplateStringsArray becomes necessary?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question here.

Copy link
Member

@jandrade jandrade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this implementation! I'm leaving some comments to understand more context around the callback prop and see if the API could be optimized a bit. Great progress!

});

export const WithToggleMode: StoryComponentType = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Could you please add a JSdoc here to add a description to this Story? This would be helpful to devs that want to understand how to use this variant.

https://storybook.js.org/docs/api/doc-blocks/doc-block-description#writing-descriptions

Screenshot 2025-04-30 at 11 23 36 AM

const [toggleMode, setToggleMode] =
React.useState<AccordionToggleMode>("collapse-all");

const [toggleModeForNextedSection, setToggleModeForNextedSection] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(typo):

Suggested change
const [toggleModeForNextedSection, setToggleModeForNextedSection] =
const [toggleModeForNestedSection, setToggleModeForNestedSection] =

@@ -17,7 +17,7 @@ type Props = {
// Unique ID for this section's button.
id: string;
// Header content.
header: string | React.ReactElement;
header: string | React.ReactElement | TemplateStringsArray;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question here.

Comment on lines +94 to +97
onToggleModeComplete?: (status: {
anyExpanded: boolean;
anyCollapsed: boolean;
}) => void;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Is there a specific use case for this callback? By looking at the Story, I don't follow how this could be used in an instance.

Comment on lines +215 to +226
// Detect partial vs all open vs all closed sections
const anyOpen = newSectionsOpened.some(Boolean);
const anyClosed = newSectionsOpened.some((sectionOpen) => !sectionOpen);

if (anyOpen && anyClosed) {
onToggleModeComplete?.({anyExpanded: true, anyCollapsed: true});
} else {
onToggleModeComplete?.({
anyExpanded: anyOpen,
anyCollapsed: anyClosed,
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: If this callback still needs to be exposed to the consumer, I think it would be best from the API perspective to return the same value of toggleMode. That way we could let the consumer know why kind of toggle is currently used.

Suggested change
// Detect partial vs all open vs all closed sections
const anyOpen = newSectionsOpened.some(Boolean);
const anyClosed = newSectionsOpened.some((sectionOpen) => !sectionOpen);
if (anyOpen && anyClosed) {
onToggleModeComplete?.({anyExpanded: true, anyCollapsed: true});
} else {
onToggleModeComplete?.({
anyExpanded: anyOpen,
anyCollapsed: anyClosed,
});
}
// Detect partial vs all open vs all closed sections
const currentToggleMode = anyOpen && anyClosed ? internalToggleMode : "none";
onToggleModeComplete?.(currentToggleMode);

By using this approach, it is easier for consumers to understand only a single type of input/output. instead of having to understand one input (union type) and a different output (object with booleans).

Comment on lines +101 to +103
* `expand-all` opens them all.
* `collapse-all` closes them all.
* `none` leaves their state user-controlled.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: This is great! just an extra - would help formatting the docs even more :)

Suggested change
* `expand-all` opens them all.
* `collapse-all` closes them all.
* `none` leaves their state user-controlled.
* - `expand-all` opens them all.
* - `collapse-all` closes them all.
* - `none` leaves their state user-controlled.
Screenshot 2025-05-01 at 10 25 17 AM

Comment on lines +25 to 26
| undefined;
type Props = AriaProps & {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it would be nice adding a line break (suuper nit 😄)

Suggested change
| undefined;
type Props = AriaProps & {
| undefined;
type Props = AriaProps & {

Comment on lines +194 to +198
if (toggleMode === "expand-all") {
setSectionsOpened(Array(children.length).fill(true));
} else if (toggleMode === "collapse-all") {
setSectionsOpened(Array(children.length).fill(false));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Thanks for adding the comment above! I think this could be simplified to:

Suggested change
if (toggleMode === "expand-all") {
setSectionsOpened(Array(children.length).fill(true));
} else if (toggleMode === "collapse-all") {
setSectionsOpened(Array(children.length).fill(false));
}
if (toggleMode !== "none") {
setSectionsOpened(Array(children.length).fill(toggleMode === "expand-all"));
}

@maddy531 maddy531 closed this Jun 17, 2025
@maddy531 maddy531 reopened this Jun 17, 2025
Copy link

codecov bot commented Jun 17, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 0.00%. Comparing base (1739e23) to head (b5a04ab).
Report is 131 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@     Coverage Diff      @@
##   main   #2559   +/-   ##
============================
============================

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1739e23...b5a04ab. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants