Skip to content
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

Closed
wants to merge 14 commits into from
Closed

Adixon 1189 menu prep #1475

wants to merge 14 commits into from

Conversation

adixon-adobe
Copy link
Collaborator

@adixon-adobe adixon-adobe commented May 22, 2021

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 underlying sp-menu-group's sp-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.
Screen Shot 2021-05-22 at 11 54 57 AM

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:

  • [ x ] I have signed the Adobe Open Source CLA.
  • [ x ] My code follows the code style of this project.
  • [ x ] My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • [ x ] I have read the CONTRIBUTING document.
  • [ x ] I have added tests to cover my changes.
  • All new and existing tests passed.

packages/picker/src/Picker.ts Outdated Show resolved Hide resolved
packages/menu/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/menu/src/Menu.ts Outdated Show resolved Hide resolved
packages/menu/src/Menu.ts Outdated Show resolved Hide resolved
@adixon-adobe
Copy link
Collaborator Author

I think I have a bit more work to do with inherits & selection styles here as well -- I bet we're not applying no selection styles in some menu group cases we should.

Copy link
Contributor

@Westbrook Westbrook left a 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.


// For the multiple select case, we'll join the value strings together
// for the value property with this separator
@property({ type: String })
Copy link
Contributor

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?

Copy link
Collaborator Author

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.

packages/menu/src/Menu.ts Outdated Show resolved Hide resolved
packages/menu/src/Menu.ts Show resolved Hide resolved
packages/menu/src/Menu.ts Outdated Show resolved Hide resolved
packages/menu/src/Menu.ts Outdated Show resolved Hide resolved
@@ -41,13 +46,38 @@ export class MenuGroup extends SpectrumElement {
MenuGroup.instances += 1;
}

private get menuRole() {
if (this.selects) {
return 'menu';
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for reminding me, this looks good right now!

image

packages/menu/src/MenuGroup.ts Outdated Show resolved Hide resolved
packages/menu/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/menu/src/MenuItem.ts Outdated Show resolved Hide resolved
packages/picker/src/Picker.ts Outdated Show resolved Hide resolved
@adixon-adobe
Copy link
Collaborator Author

@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
private addItem(item: MenuItem) {
this.menuItemSet.add(item);
this.updateMenuItems();
}
Copy link
Collaborator Author

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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>

Copy link
Collaborator Author

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.

Copy link
Contributor

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...

Copy link
Contributor

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[];

Copy link
Collaborator Author

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)?

Copy link
Contributor

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.

packages/menu/src/Menu.ts Outdated Show resolved Hide resolved
private addItem(item: MenuItem) {
this.menuItemSet.add(item);
this.updateMenuItems();
}
Copy link
Contributor

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?

] as MenuItem[]);
for (const childItem of childItems) {
if (this.menuItemSet.has(childItem)) {
this.menuItems.push(childItem);
Copy link
Contributor

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...

Copy link
Collaborator Author

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.

Copy link
Collaborator Author

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();
Copy link
Contributor

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?

Copy link
Collaborator Author

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 Show resolved Hide resolved
packages/menu/src/MenuItem.ts Show resolved Hide resolved
Comment on lines 229 to 230
let selected: string[] = [];
let selectedItems: MenuItem[] = [];
Copy link
Collaborator Author

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.

] as MenuItem[]);
for (const childItem of childItems) {
if (this.menuItemSet.has(childItem)) {
this.menuItems.push(childItem);
Copy link
Collaborator Author

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();
}
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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">
Copy link
Collaborator Author

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).

Copy link
Collaborator Author

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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.

Copy link
Contributor

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?

Copy link
Collaborator Author

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.

@adixon-adobe
Copy link
Collaborator Author

@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).

@Westbrook Westbrook changed the base branch from main to next June 12, 2021 00:54
@Westbrook
Copy link
Contributor

Closing in favor of adixon-1189-menu-selects

@Westbrook Westbrook closed this Jun 14, 2021
@Westbrook Westbrook deleted the adixon-1189-menu-prep branch June 24, 2021 23:00
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.

Menu should manage its own state
2 participants