-
Notifications
You must be signed in to change notification settings - Fork 200
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
Adixon 1189 menu prep #1475
Adixon 1189 menu prep #1475
Conversation
I think I have a bit more work to do with |
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.
This is a code only pass. I'll take a closer look at the actual experience, particularly as it relates to your questions either later today or tomorrow morning.
packages/menu/src/Menu.ts
Outdated
|
||
// For the multiple select case, we'll join the value strings together | ||
// for the value property with this separator | ||
@property({ type: String }) |
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.
Should this have a non-camelcase attribute? Or should this be only a property?
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.
Switched to non-camelCase. I should also add some tests around this (and a bit more for selection in general).
I don't see a reason for it not to be an attribute as long as it's a string.
@@ -41,13 +46,38 @@ export class MenuGroup extends SpectrumElement { | |||
MenuGroup.instances += 1; | |||
} | |||
|
|||
private get menuRole() { | |||
if (this.selects) { | |||
return 'menu'; |
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'm not sure this can ever see children through the slot in a screen reader, so the role here may actually go somewhere else. I'll have to visit the preview site to confirm.
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.
let me know if there's anything to do here.
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.
@Westbrook pushed up my WIP since I think I've finally got a solid approach to managing item ownership based on our discussions. |
Now "sp-menu-item-added" and "sp-menu-item-removed" events are handled to pick up the menu items a parent menu should own. To get the right DOM order we do a debounced update. Looks like some focus issues got introduced, at least in the Picker context, and there's likely more issues with this that need to be fixed for menus with groups that have different ownership
f98d2aa
to
be243a6
Compare
private addItem(item: MenuItem) { | ||
this.menuItemSet.add(item); | ||
this.updateMenuItems(); | ||
} |
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.
This and the updateMenuItems
method are the bulk of the new management logic. While there's a lot more to clean up in this PR, it'd be helpful to get some feedback on this approach.
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.
IIUC, the this.menuItems
array that is built in this.updateMenuItems()
only needs to be maintained in the original sp-menu
descendent, right? Like in this case:
<sp-menu> // I'm a menu.
<sp-menu-group> // I own a menu
<sp-menu-item></sp-menu-item>
</sp-menu-group>
<sp-menu-group> // I also own a meni
<sp-menu-item></sp-menu-item>
</sp-menu-group>
<sp-menu> // I'm also a menu
<sp-menu-item></sp-menu-item>
</sp-menu>
</sp-menu>
While technically there would be 4 sp-menu
elements in the various shadow trees, only the one at the root would need to coalesce this data in order to manage focus/arrow keys/etc. Do we need additional logic to skip that work, or am I missing something?
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 a sub-menu manages it's own selection things get trickier, since we still want arrow keys to behave sensibly. I haven't addressed that yet (realized this case right after I pushed up yesterday). So tweaking your example slightly:
<sp-menu> // I'm a menu.
<sp-menu-group> // I own a menu
<sp-menu-item></sp-menu-item>
</sp-menu-group>
<sp-menu-group selects="single"> // I'm a menu that manages my own selection
<sp-menu-item></sp-menu-item>
</sp-menu-group>
<sp-menu> // I'm also a menu
<sp-menu-item></sp-menu-item>
</sp-menu>
</sp-menu>
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.
One question I have here is whether I need to manage 2 lists. One for focus (every item the menu is a parent of), and one for just the items the menu directly manages.
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.
This is actually what I was thinking. There are ways to not do this, but I'm pretty certain that while the selection context is handled locally, the focus management has to happen globally. There is likely some tricky accessibility concerns with how the screen reader relates to the data here...
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.
You could also have a more complicated single list?
type ChildItem = {
element: MenuItem, // reference to DOM element
managed: boolean, // whether this item is part of the selection pool
active: boolean, // whether this is the "focused" item or not
}
type ChildItems = ChildItem[];
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.
Okay, this is helpful!
Would only the top-level parent menu need to manage the focus list (at least until we support nested menus)?
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 believe so.
For all intents and purposes, each nested menu will act as its own root: https://webcomponents.dev/edit/Aze4atcpZc7EsxqnCBve/src/index.ts anyways.
private addItem(item: MenuItem) { | ||
this.menuItemSet.add(item); | ||
this.updateMenuItems(); | ||
} |
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.
IIUC, the this.menuItems
array that is built in this.updateMenuItems()
only needs to be maintained in the original sp-menu
descendent, right? Like in this case:
<sp-menu> // I'm a menu.
<sp-menu-group> // I own a menu
<sp-menu-item></sp-menu-item>
</sp-menu-group>
<sp-menu-group> // I also own a meni
<sp-menu-item></sp-menu-item>
</sp-menu-group>
<sp-menu> // I'm also a menu
<sp-menu-item></sp-menu-item>
</sp-menu>
</sp-menu>
While technically there would be 4 sp-menu
elements in the various shadow trees, only the one at the root would need to coalesce this data in order to manage focus/arrow keys/etc. Do we need additional logic to skip that work, or am I missing something?
packages/menu/src/Menu.ts
Outdated
] as MenuItem[]); | ||
for (const childItem of childItems) { | ||
if (this.menuItemSet.has(childItem)) { | ||
this.menuItems.push(childItem); |
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.
Related to the below comment around who owns this.menuItems
to manage keyboard and focus work, it seems like this.menuItems
is a larger collection that this.menuItemSet
(which is about selection, right?) in way that questions this logic...
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.
thought I'd responded yesterday. With the approach here, this.menuItemSet
is used to manage what items are owned, while this.menuItems
has thee same items in the right DOM order.
I can't say I like it, and one thing I'm thinking is that moving more of the logic here into a menuItems
getter (along with an invalidation flag), might help clean this up, and address some of the other feedback you have here.
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.
Let me know what you think of the way this is re-worked now.
} | ||
} | ||
|
||
this.updateSelectedItemIndex(); |
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.
Possibly nit as O(1n) and O(2n) are kind of the same...
This walks the array we just built again, is there room to merge the work?
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.
there might be, but there are times we only need to do updateSelectedItemIndex. I'll take a look once I get more of the changes here cleaned up.
packages/menu/src/Menu.ts
Outdated
let selected: string[] = []; | ||
let selectedItems: MenuItem[] = []; |
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.
that's weird. I'd expect the linter to have at least caught this at commit time.
packages/menu/src/Menu.ts
Outdated
] as MenuItem[]); | ||
for (const childItem of childItems) { | ||
if (this.menuItemSet.has(childItem)) { | ||
this.menuItems.push(childItem); |
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.
thought I'd responded yesterday. With the approach here, this.menuItemSet
is used to manage what items are owned, while this.menuItems
has thee same items in the right DOM order.
I can't say I like it, and one thing I'm thinking is that moving more of the logic here into a menuItems
getter (along with an invalidation flag), might help clean this up, and address some of the other feedback you have here.
private addItem(item: MenuItem) { | ||
this.menuItemSet.add(item); | ||
this.updateMenuItems(); | ||
} |
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.
Okay, this is helpful!
Would only the top-level parent menu need to manage the focus list (at least until we support nested menus)?
@@ -679,7 +693,7 @@ describe('Picker', () => { | |||
const el = await pickerFixture(); | |||
el.addEventListener('change', (event: Event) => { | |||
const { value } = event.target as Picker; | |||
console.log('change', value); | |||
//console.log('change', 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.
these seemed noisy, but probably worth leaving in the code since it's useful for debugging. Happy to re-enable though!
|
||
describe('Menu group', () => { | ||
it('renders', async () => { | ||
const el = await fixture<Menu>( | ||
html` | ||
<sp-menu> | ||
<sp-menu selects="none"> |
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.
Right now this outer menu won't manage the child sp-menu-group
elements without selects being one of none
, single
, or multiple
, but I'm thinking it might be better for the behavior to default to selects="none"
for the outer parent menu (i.e. if there's nothing to inherit from the parent).
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 also realize that we need a way to have an "unmanaged" state for the PickerBase
case, since at least for this round PickerBase
is still managing it's own selection.
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.
Sub-note, styling for the Menu
in a Picker
will currently require a selects
attribute (value not withstanding) in order to display correctly when going between having a selected child and not. That also means that if <sp-menu selects="none">
actually doesn't take a selected child that it will be visual "incorrect".
This likely doesn't help. Can you clarify what information would be good for me to share my thoughts on here?
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 yeah -- I even looked at that styling earlier today. I think I also need to fix styling for nested menus so I'll leave this open until that's resolved.
It'd be good to get your thoughts on having the default behavior be that selection is unmanaged (if no parent is managing selection). I think this also means that by default the styling should allow for selection. As I think about it more it seems reasonable, but we could certainly go a different direction.
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 don't understand this. Can you share a couple of code samples and the expected output?
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's probably easiest to think about the difference between selects="none"
and no selects.
For selects="none"
we don't allow selection (and in the menu I'm capturing the click event, though that might be unnecessary now). This means that clients should add click events to the individual menu items here, at least the way it's coded now. When adding/updating items we also might clear the selected state on any items marked as selected.
When selects
isn't present, selection is totally unmanaged. Click events are passed up so either a parent menu or PickerBase can handle it in the way they need. In general this probably isn't the state you want if there's not a parent managing things.
The one piece of this I haven't thought through is whether the default styling with no selects
should be that the items have the selectable styling or not. Keeping it selectable is handy for the PickerBase case.
Also add a still-failing test related to selects changing
Also reworked the way menuItems updates to make it clearer it's derived from menuItemSet.
@Westbrook I'm seeing one unit test failure on Firefox I might need some help with. picker.test "opens without visible focus on a menu item on click" -- I don't see the same in storybook, but the test says it is getting visible focus (also possible I just need to rebase). |
Closing in favor of |
Continued work on selects attribute in sp-menu & sp-menu-group
Preview link: https://adixon-1189-menu-prep--spectrum-web-components.netlify.app/
Description
For this round I've tried to keep the changes as scoped to sp-menu as possible, given that the reparenting in PickerBase is going to be tricky to work through.
I'm stuck on initialization for sub-menus though --
prepItems
isn't getting called once the slotted items in the underlyingsp-menu-group
'ssp-menu
are present (meaning that after initializing menu groups always think they have 0 menu items). I've got some new unittests that are failing, and it's pretty easy to see in the storybook for "Menu Group Selects Multiple"The only (known) work left here outside of that is getting the documentation work.
Related Issue
Initial work for #1189
fixes #715
Motivation and Context
Gets us closer to more useful menus
How Has This Been Tested?
Storybook & unit tests.
Here's an example of what's failing: the "Oakland" menu group should be single-select but it initializes wrong. If you keep using the menu it eventually starts working properly once
prepItems
has been called correctly.Types of changes
Bug fix (non-breaking change which fixes an issue)
New feature (non-breaking change which adds functionality)
[ x ] Breaking change (fix or feature that would cause existing functionality to change)
Checklist: