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

feat: Menu component #485

Merged
merged 22 commits into from
Mar 21, 2019
Merged

feat: Menu component #485

merged 22 commits into from
Mar 21, 2019

Conversation

iyadthayyil
Copy link
Contributor

Motivation

Menu component with auto positioning and RTL support:
https://material.io/design/components/menus.html
fixes: #9

Test plan

menu-2

@callstack-bot
Copy link

callstack-bot commented Aug 8, 2018

Hey @iyadthayyil, thank you for your pull request 🤗. The documentation from this branch can be viewed here. Please remember to update Typescript types if you changed API.

Copy link
Contributor

@kpsroka kpsroka left a comment

Choose a reason for hiding this comment

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

Hi Iyad!
First of all, please excuse me for taking so long to review this PR. This should have happened much earlier. Thank you for your contribution. It's exciting to see that you tackle writing this -- quite important -- component.

However, there are two major issues with your approach:

  1. The position of the menu is not updated when the screen changes orientation:
    rnp-menu-flip
  2. If the menu is larger than available vertical space, it should become a scrollable view. Instead, it flows outside of the screen:
    rnp-menu-large

Also, another minor blocker is that the dropdown menu default placement should be below the element that generates it (see relevant material docs). While this is not so for all of elements (indeed some menus should appear on top of the generating element), it should be the default. I believe that we should support steering of the placement of the menu, relatively to the generating element, but it's good enough for the initial version to stick with the default mentioned above.

I also have several other comments about the code. Please address them, or give more support for the choices that you made.

Thanks again!

* });
* ```
*/
class Surface extends React.Component<Props> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Frankly, I don't see much value in having Surface as a separate component. It's used in just one place, and only adds some styles. Can you tell me what use this component has outside of how it's used in your PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a quick hack to overcome an animation problem I faced when creating a new animated view from the existing Surface component. This was the original way I did it:

const AnimatedSurface = Animated.createAnimatedComponent(Surface);

But this seems to render very choppy animations, would like to know if there is a way to just use Surface without this animation issue.

Copy link
Member

Choose a reason for hiding this comment

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

@iyadthayyil We can change View to Animated.View here: https://github.com/callstack/react-native-paper/blob/master/src/components/Surface.js#L65

Then we wouldn't need Animated.createAnimatedComponent(Surface)

};

/**
* Menus display a list of choices on temporary surfaces. Their placement varies based on the element that opens them.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: add 'elevated' before 'surfaces'. Also elaborate on the placement -- I believe it only adapts based on the source element's position + actual size of the menu.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok will do that.

<Animated.View
{...rest}
style={[
styles.surface,
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the only rule that styles.surface has is backgroundColor, it will be overridden by the next line. Please remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just used the same code from the original Surface component, but will change that, also should probably change that in the original Surface component aswell.

this.setState({ buttonWidth: width, buttonHeight: height });
};

_toggle = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This doesn't really "toggle" anything, it just updates the shown/hidden state, so I suggest renaming it to "_updateVisibility"

menuSizeAnimation,
} = this.state;

const menuSize = {
Copy link
Contributor

Choose a reason for hiding this comment

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

You use this constant only once, and really far away from this declaration. It's a bit confusing why you need this until line 282, especially since you keep using menuSizeAnimation.x and menuSizeAnimation.y. Please avoid defining it and instead inline it's use below.

}

// RTL support
const leftOrRight = I18nManager.isRTL ? { right: left } : { left };
Copy link
Contributor

Choose a reason for hiding this comment

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

You use this constant only once, please inline it where it's used.

};

/**
* A component to show list of options inside a Menu.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I believe this component just shows a single option inside a Menu, no?

*/
icon?: IconSource,
/**
* Whether the `MenuItem` is disabled. A disabled `MenuItem` is greyed out and `onPress` is not called on touch.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: write 'item' instead of `MenuItem`.

maxWidth: maxWidth - 16,
},
widthWithIcon: {
maxWidth: maxWidth - (8 * 6 + 40),
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 explain where does "8 * 6 + 40" come from? Possibly define a marginWidth const so that it's clear where does '8' come from.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok will clear this up with some variables.

.string();

if (disabled) {
iconColor = titleColor = color(theme.dark ? white : black)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please define const disabledColor instead, and use it with the ?: operator to define const titleColor and iconColor.

@ferrannp ferrannp assigned iyadthayyil and unassigned kpsroka Aug 31, 2018
@satya164 satya164 changed the base branch from material-next to master September 2, 2018 18:59
@ferrannp
Copy link
Collaborator

@iyadthayyil are you still working on this or you need some help? :)

@ferrannp ferrannp added the wip label Sep 20, 2018
@iyadthayyil
Copy link
Contributor Author

Ok, sorry guys, I have been busy with uni work, will try to get this ready soon.

@ferrannp
Copy link
Collaborator

ferrannp commented Oct 4, 2018

Maybe we can do it step by step and take a look to rotation later and other edge cases. Let us know if you need help @iyadthayyil.

@satya164 satya164 force-pushed the master branch 3 times, most recently from f33cab9 to 295a771 Compare October 8, 2018 01:48
@raajnadar
Copy link
Collaborator

Any ETA for this component?? This component looks beautiful

@thevikas
Copy link

knock knock

@ferrannp ferrannp removed the wip label Jan 31, 2019
@geminiyellow
Copy link

please move forward, we need menu

@waquidvp
Copy link
Contributor

waquidvp commented Feb 17, 2019

Hey guys, I want to help merge this PR. Can I rebase from 'upstream' master, the main reason is so that we can remove AnimatedSurface, the upstream Surface now uses Animated.View meaning that Animated.createAnimatedComponent(Surface) won't be needed anymore. See here for more.

@waquidvp
Copy link
Contributor

Hey guys, I have made some changes to follow the new way of doing things compared to when the PR was first created. Part of this was to add the typescript typings here: typings/components/Menu.d.ts, this is my first time doing some typescript so would be cool if someone could have a look and give some feedback.

@ferrannp
Copy link
Collaborator

Hey @waquidvp thanks for coming back to this. I guess you work together with @iyadthayyil ?

Would you mind adding: Screenshots and code for docs? You can see how to do it from other components.

I am also wondering if we could animate scale instead animating height and width, that would allow use to use useNativeDriver. However we can merge first this and then focus on that.

@waquidvp
Copy link
Contributor

There were already some screenshots and some code examples. They just weren't in the right place and so wasn't showing up in the generated docs. I have moved it to the right place and the docs look ok now.

@waquidvp
Copy link
Contributor

waquidvp commented Mar 3, 2019

To solve the status bar issue, should a combination SafeAreaView and StatusBar.currentHeight do, or is there any other we handle it in this library?

@satya164
Copy link
Member

satya164 commented Mar 3, 2019

To solve the status bar issue, should a combination SafeAreaView and StatusBar.currentHeight do, or is there any other we handle it in this library?

Let's do it for now. We can refactor to extract this logic later.

@ferrannp
Copy link
Collaborator

@satya164 can we have your approval here for merging it?

@Trancever Trancever merged commit 0c5371a into callstack:master Mar 21, 2019
@geminiyellow
Copy link

ohhh! amazing! thanks all.

@RichardLindhout
Copy link
Contributor

Great work @iyadthayyil! Thanks everyone!

@Zefau
Copy link

Zefau commented Mar 28, 2019

Can the Menu component be used as a Dropdown for TextInput as shown https://material.io/design/components/menus.html#exposed-dropdown-menu ?

Replacing the anchor attribute with TextInput (and using onFocus instead of onPress) won't work.

@mihaidaviddev
Copy link

mihaidaviddev commented Apr 3, 2019

@Zefau I also needed to do something similar and I ended up using focus and blur events. It's working on android simulator & device, but for ios is working just in the simulator, I think the surface height bug it's caused by the ScrollView issue described here Any ideea what might be the problem ? This is how the component looks like

import { Keyboard, ScrollView } from "react-native";
import { Menu, TextInput, Theme } from "react-native-paper";
import PaperMenu from "./PaperMenu";

export interface TextInputAutocompleteProps {
  width: number;
  items: string[];
  value: string;
  theme: Theme;
  label: string;
  onChange: (value: string) => void;
}

interface TextInputAutocompleteState {
  filteredItems: string[];
  value: string;
  showMenu: boolean;
}

export default class TextInputAutocomplete extends Component<
  TextInputAutocompleteProps,
  TextInputAutocompleteState
> {
  state = {
    filteredItems: [] as string[],
    value: "",
    showMenu: false
  };
  componentDidMount = () => this.updateStateFromProps(this.props);

  componentWillReceiveProps = (props: TextInputAutocompleteProps) =>
    this.updateStateFromProps(props);

  updateStateFromProps = (props: TextInputAutocompleteProps) =>
    this.setState({ value: props.value });

  filterItems = value => {
    const filteredItems =
      value.trim() !== ""
        ? this.props.items.filter(
            item =>
              item !== value &&
              item.toUpperCase().startsWith(value.toUpperCase())
          )
        : [];
    const showMenu = filteredItems.length > 0;
    this.setState({
      showMenu,
      filteredItems
    });
  };

  onTextInputFocus = () => this.filterItems(this.state.value);

  onTextInputBlur = () => this.setState({ showMenu: false });

  onMenuDismiss = () => {
    this.setState({ showMenu: false });
    Keyboard.dismiss();
  };

  onItemPress = (item: string) => {
    this.onMenuDismiss();
    this.setState({ value: item });
    this.props.onChange(item);
  };

  onTextInputChange = value => {
    this.setState({ value });
    this.filterItems(value);
  };

  render() {
    const menuItems = this.state.filteredItems.map((item, i) => {
      const onPress = () => this.onItemPress(item);
      return (
        <Menu.Item
          style={{ minWidth: this.props.width, maxWidth: this.props.width }}
          onPress={onPress}
          key={i}
          title={item}
        />
      );
    });
    return (
      <PaperMenu
        style={{ marginTop: 50, width: this.props.width }}
        visible={this.state.showMenu}
        onDismiss={this.onMenuDismiss}
        anchor={
          <TextInput
            label={this.props.label}
            mode="outlined"
            theme={this.props.theme}
            value={this.state.value}
            onChangeText={this.onTextInputChange}
            onFocus={this.onTextInputFocus}
            onBlur={this.onTextInputBlur}
          />
        }
      >
        <ScrollView
          scrollEnabled={true}
          showsVerticalScrollIndicator={true}
          keyboardShouldPersistTaps="always"
          style={{ height: 135 }}
        >
          {menuItems}
        </ScrollView>
      </PaperMenu>
    );
  }
}

The PaperMenu is a fork of Menu component to temporary fix the bug #970

@RichardLindhout
Copy link
Contributor

On iOS Statusbar.currentHeight does not work so ending up with a undefined top.

@mihaidaviddev
Copy link

Yes..I use a default value of 40 for now..so i removed StatusBar completely..the issue I have now is with the height of the menu..not the position

@callstack callstack locked as too heated and limited conversation to collaborators Apr 3, 2019
@raajnadar
Copy link
Collaborator

Please create issue ticket instead of posting here. This component is very new so you will see lot of issues.

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

Successfully merging this pull request may close these issues.

Menu and Dropdown