-
Notifications
You must be signed in to change notification settings - Fork 42
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
Accordion
- expand/collapse enhancements
#2147
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git βοΈ
|
ba65850
to
5b56b0c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given the requirements, this approach makes sense to me.
content: [ | ||
{ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
close: (...args: any[]) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be unknown
instead of any
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh interesting (unknown
vs. any
). Was looking at this: https://devblogs.microsoft.com/typescript/announcing-typescript-3-0-rc-2/#the-unknown-type
Seems like a better choice if it works for us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unknown
might be a better fit although, for consistency with other functions, I would be tempted to leave it as any
and review them holistically.
} | ||
|
||
@action | ||
onStateChange() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like you're passing @isOpen
from the template as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, @isOpen
can be accessed as an argument, but it's not required. The reason I pass it in from the template is for the ember render modifier to track it
packages/components/src/components/hds/disclosure-primitive/index.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alex-ju added a comment that may reveal to be a blocker, have a look
Accordion
enhancementsAccordion
- expand/collapse enhancements
5b56b0c
to
50d2ddd
Compare
50d2ddd
to
c38a3b6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few comments and considerations.
I haven't completed the review, because I want to discuss with you the controlled/uncontrolled states (still doesn't work for me, but maybe it's how I am thinking of the problem). Let's sync later in the day
@@ -4,5 +4,5 @@ | |||
}} | |||
|
|||
<div class="hds-accordion" ...attributes> | |||
{{yield (hash Item=(component "hds/accordion/item"))}} | |||
{{yield (hash Item=(component "hds/accordion/item" forceState=@forceState))}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
playing advocate's devil: should this new argument called something like forceIsOpen
(or forceOpen
) and be a boolean? π€
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another consideration: I was imagining that the forced state would be at the item level, not at the parent accordion level (the isOpen
is at item level). why is it necessary at this level? one can achieve the same "collapse all/expand all" by passing the @forceState
argument to all the items, plus (in that case) it's possible to control a single item independently from the others (while with the current implementation it's not possible)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my understanding is that our consumers want a way to manage the states of all accordion items (not individual items), hence the approach of setting it at the root and cascading it to items.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated with the suggested adjustments, adding onClickToggle
to DisclosurePrimitive
and forwarding it to Accordion::Item
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about having the forceState
argument as boolean (calling it something like forceIsOpen
or forceOpen
)? would it make sense? do we expect to have more than one type of forced state in the future?
Co-Authored-By: Cristiano Rastelli <686239+didoo@users.noreply.github.com>
to showcase `onClickToggle` Co-Authored-By: Cristiano Rastelli <686239+didoo@users.noreply.github.com>
Co-Authored-By: Cristiano Rastelli <686239+didoo@users.noreply.github.com>
Co-Authored-By: Cristiano Rastelli <686239+didoo@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few more comments
@@ -4,5 +4,5 @@ | |||
}} | |||
|
|||
<div class="hds-accordion" ...attributes> | |||
{{yield (hash Item=(component "hds/accordion/item"))}} | |||
{{yield (hash Item=(component "hds/accordion/item" forceState=@forceState))}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about having the forceState
argument as boolean (calling it something like forceIsOpen
or forceOpen
)? would it make sense? do we expect to have more than one type of forced state in the future?
` | ||
); | ||
// first item open at rendering | ||
assert.dom('.hds-accordion-item__content').exists({ count: 1 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
being a bit over-zealous here: should we be more specific to make sure it's the first item being open?
<:content as |c|> | ||
<button type="button" {{on "click" c.close}}>Close</button> | ||
</:content> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<:content as |c|> | |
<button type="button" {{on "click" c.close}}>Close</button> | |
</:content> | |
<:content as |c|>Content one</:content> |
assert.dom('.hds-accordion-item__content').doesNotExist(); | ||
// toggle to open | ||
await click('.hds-accordion-item__button'); | ||
assert.dom('.hds-accordion-item__content').exists(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is not really testing the onClickToggle
callback; probably we should have an "external state" that gets changed on onClickToggle
and check that external state value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make sure I wasn't missing something, I've commented out the content of the this.set('onClickToggle', () => {});
function, and the tests are still passing.
// if the state is controlled from outside, the argument overrides the internal state | ||
return this.args.isOpen ?? this._isOpen; | ||
} else { | ||
// if the state is changes internally, the internal state overrides the argument |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// if the state is changes internally, the internal state overrides the argument | |
// if the state changes internally, the internal state overrides the argument |
return this.args.isOpen ?? this._isOpen; | ||
} else { | ||
// if the state is changes internally, the internal state overrides the argument | ||
return this._isOpen !== undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in which case the value of _isOpen
would be undefined
? (I did a check, and to me seems it's not possible)
@@ -33,11 +35,44 @@ export interface HdsDisclosurePrimitiveSignature { | |||
} | |||
|
|||
export default class HdsDisclosurePrimitiveComponent extends Component<HdsDisclosurePrimitiveSignature> { | |||
@tracked isOpen = this.args.isOpen ?? false; | |||
@tracked _isOpen = false; | |||
@tracked _isControlled = this.args.isOpen != undefined; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tracked _isControlled = this.args.isOpen != undefined; | |
@tracked _isControlled = this.args.isOpen !== undefined; |
|
||
@action | ||
onStateChange() { | ||
if (this.args.isOpen !== undefined) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if I understand correctly, we don't use this._isControlled
here because this function is invoked when the value of @isOpen
changes, which triggers the did-update
which is run without re-rendering the component, so line 39 is not re-evaluated
if that is the case, what do you think of adding a comment to explain this "detail"? π (it took me a while to get it)
if not, then maybe we can use this._isControlled
as condition? would make sense (in which case TS complains, but we can cast the assignment via Boolean(this.args.isOpen)
like we do at line 49
</Hds::Accordion> | ||
</SG.Item> | ||
</Shw::Grid> | ||
|
||
<Shw::Text::H3>Edge cases</Shw::Text::H3> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<Shw::Text::H3>Edge cases</Shw::Text::H3> | |
<Shw::Divider @level={{2}} /> | |
<Shw::Text::H3>Edge cases</Shw::Text::H3> |
I tested this PR via the deployed showcase link https://hds-showcase-git-alex-ju-accordion-expand-all-hashicorp.vercel.app/components/accordion#externally-controlled and https://hds-showcase-git-alex-ju-accordion-expand-all-hashicorp.vercel.app/utilities/disclosure-primitive and the functionality and API looks good π |
π Summary
In this PR we propose a set of enhancements to our Accordion component to accommodate the following use cases without introducing any breaking changes:
Allow consumers to implement an "expand all"/"collapse all" functionality (request reference)
open
/close
) of each accordion item from outside the componentExample of usage
Expose a function to close an accordion item from within its content (request reference)
Example of usage
π οΈ Detailed description
DisclosurePrimitive
to allow external control of theisOpen
state. We do this by introducing two private properties (_isOpen
, representing the internal state and_isControlled
set totrue
every time the@isOpen
argument is changed externally). Tried to follow the concepts previously explored.Accordion::Item
andAccordion
adding a@forceState
argument (open
/close
) to control the state from outside the component after the initial render.Accordion::Item
component to forward theclose
action fromDisclosurePrimitive
.π External links
Jira ticket: HDS-3364
π Component checklist
types.ts
files㪠Please consider using conventional comments when reviewing this PR.