Repository: callstack/react-native-paper
Pull request #739 (2019-01-19 23:12:17)

feat: ListSubheader component
<!-- Please provide enough information so that others can review your pull request. -->
<!-- Keep pull requests small and focused on a single change. -->

### Motivation
Add new component `ListSubheader` to be use in `FlatList`.
I didn't found specific specs for this component in material.io but there are some examples of lists with subheader. https://material.io/design/components/lists.html#anatomy

closes https://github.com/callstack/react-native-paper/issues/716

<!-- What existing problem does the pull request solve? Can you solve the issue with a different approach? -->

### Test plan
|Reference|react-native-paper|
|----------|-------------------|
|![reference]|![paper]|


<!-- List the steps with which we can test this change. Provide screenshots if this changes anything visual. -->
[reference]: https://user-images.githubusercontent.com/11599942/51439310-8efcc080-1cb8-11e9-9347-3d8ff99856ac.png
[paper]: https://user-images.githubusercontent.com/11599942/53601703-fb6ab980-3bac-11e9-8efc-9d390a61920e.png


callstack-bot: Hey @ocarreterom, thank you for your pull request 🤗. The documentation from this branch can be viewed [here](https://6902-71323749-gh.circle-artifacts.com/0/docs/index.html). Please remember to update Typescript types if you changed API.
ferrannp: @ocarreterom can you fix merge conflicts?
ocarreterom: Rebased. 👌
Trancever: @satya164 Any comments on this? Imo this change is ok and it allows passing `Subheader` to `FlatList`. I am also thinking if we should keep title prop on List.Section, remove it or maybe mark as deprecated and remove while releasing 3.0 version. What do you think?

Patch:
diff --git a/docs/.babelrc b/docs/.babelrc
deleted file mode 100644
index 6486134f4..000000000
--- a/docs/.babelrc
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "presets": [
-    "@babel/preset-env",
-    "@babel/preset-react",
-    "@babel/preset-flow",
-    "linaria/babel"
-  ],
-  "plugins": [
-    "@babel/plugin-proposal-class-properties"
-  ]
-}
diff --git a/docs/package.json b/docs/package.json
index 0b0114abc..2878cfb79 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -23,7 +23,7 @@
   "license": "MIT",
   "dependencies": {
     "color": "^2.0.1",
-    "component-docs": "^0.18.1",
-    "linaria": "^1.1.4"
+    "component-docs": "^0.19.4",
+    "linaria": "^1.2.1"
   }
 }
diff --git a/docs/pages/4.react-native-web.md b/docs/pages/4.react-native-web.md
index 69e4e52e7..8d6ae2afc 100644
--- a/docs/pages/4.react-native-web.md
+++ b/docs/pages/4.react-native-web.md
@@ -16,6 +16,63 @@ To install `react-native-web`, run:
 yarn add react-native-web react-dom react-art
 ```
 
+### Using CRA ([Create React App](https://github.com/facebook/create-react-app))
+
+Install [`react-app-rewired`](https://github.com/timarney/react-app-rewired) to override `webpack` configuration:
+
+```sh
+yarn add --dev react-app-rewired
+```
+
+[Configure `babel-loader`](#2-configure-babel-loader) using a new file called `config-overrides.js`:
+
+```js
+module.exports = function override(config, env) {
+  config.module.rules.push({
+    test: /\.js$/,
+    exclude: /node_modules[/\\](?!react-native-paper|react-native-vector-icons|react-native-safe-area-view)/,
+    use: {
+      loader: "babel-loader",
+      options: {
+        // Disable reading babel configuration
+        babelrc: false,
+        configFile: false,
+
+        // The configration for compilation
+        presets: [
+          ["@babel/preset-env", { useBuiltIns: "usage" }],
+          "@babel/preset-react",
+          "@babel/preset-flow"
+        ],
+        plugins: [
+          "@babel/plugin-proposal-class-properties",
+          "@babel/plugin-proposal-object-rest-spread"
+        ]
+      }
+    }
+  });
+
+  return config;
+};
+```
+
+Change your script in `package.json`:
+
+```diff
+/* package.json */
+
+  "scripts": {
+-   "start": "react-scripts start",
++   "start": "react-app-rewired start",
+-   "build": "react-scripts build",
++   "build": "react-app-rewired build",
+-   "test": "react-scripts test --env=jsdom",
++   "test": "react-app-rewired test --env=jsdom"
+}
+```
+
+### Custom webpack setup
+
 To install `webpack`, run:
 
 ```sh
diff --git a/docs/yarn.lock b/docs/yarn.lock
index 725eebd23..be3727007 100644
--- a/docs/yarn.lock
+++ b/docs/yarn.lock
@@ -405,6 +405,13 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
 
+"@babel/plugin-syntax-typescript@^7.2.0":
+  version "7.3.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz#a7cc3f66119a9f7ebe2de5383cce193473d65991"
+  integrity sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+
 "@babel/plugin-transform-arrow-functions@^7.2.0":
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550"
@@ -658,6 +665,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.0.0"
 
+"@babel/plugin-transform-typescript@^7.3.2":
+  version "7.3.2"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.3.2.tgz#59a7227163e55738842f043d9e5bd7c040447d96"
+  integrity sha512-Pvco0x0ZSCnexJnshMfaibQ5hnK8aUHSvjCQhC1JR8eeg+iBwt0AtCO7gWxJ358zZevuf9wPSO5rv+WJcbHPXQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-syntax-typescript" "^7.2.0"
+
 "@babel/plugin-transform-unicode-regex@^7.2.0":
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b"
@@ -790,6 +805,14 @@
     "@babel/plugin-transform-react-jsx-self" "^7.0.0"
     "@babel/plugin-transform-react-jsx-source" "^7.0.0"
 
+"@babel/preset-typescript@^7.3.3":
+  version "7.3.3"
+  resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz#88669911053fa16b2b276ea2ede2ca603b3f307a"
+  integrity sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-transform-typescript" "^7.3.2"
+
 "@babel/register@^7.0.0":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.0.0.tgz#fa634bae1bfa429f60615b754fc1f1d745edd827"
@@ -1834,16 +1857,17 @@ commondir@^1.0.1:
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
   integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
 
-component-docs@^0.18.1:
-  version "0.18.1"
-  resolved "https://registry.yarnpkg.com/component-docs/-/component-docs-0.18.1.tgz#702168b459ed60fbb7858f5bd9cae5425904f382"
-  integrity sha512-FT3kO8ndHxbNXc0nz9ACEyZ2eYXhGG0IQRr/jFd/8Xlg5ZD2a1OoHwipdE2wec8PMpYYjh7MYrdaLb1ZFtuz8g==
+component-docs@^0.19.4:
+  version "0.19.4"
+  resolved "https://registry.yarnpkg.com/component-docs/-/component-docs-0.19.4.tgz#5fbe9d5ad829090890417e619942a62e5e5a2975"
+  integrity sha512-NipAEX+wjVzM0lPTJ9M8IOKrdCTm7OR52qH7wNHYLKO4zwPv8Zz/CHI0cp9/hzrnpz3YO1dswYPBLNLfR5Qkcg==
   dependencies:
     "@babel/core" "^7.3.3"
     "@babel/plugin-proposal-class-properties" "^7.3.3"
     "@babel/preset-env" "^7.3.1"
     "@babel/preset-flow" "^7.0.0"
     "@babel/preset-react" "^7.0.0"
+    "@babel/preset-typescript" "^7.3.3"
     "@babel/register" "^7.0.0"
     "@mdx-js/mdx" "^0.17.5"
     "@mdx-js/tag" "^0.17.5"
@@ -1865,7 +1889,7 @@ component-docs@^0.18.1:
     hast-util-to-string "^1.0.1"
     history "^4.7.2"
     ignore-styles "^5.0.1"
-    linaria "^1.1.4"
+    linaria "^1.2.1"
     marked "^0.6.1"
     mini-css-extract-plugin "^0.5.0"
     opn "^5.4.0"
@@ -3792,10 +3816,10 @@ lcid@^2.0.0:
   dependencies:
     invert-kv "^2.0.0"
 
-linaria@^1.1.4:
-  version "1.1.4"
-  resolved "https://registry.yarnpkg.com/linaria/-/linaria-1.1.4.tgz#ef833e25626a897b89ad218081869860611aee31"
-  integrity sha512-XHK/doHf1FkDqXU/mQGxJLLfHatlM0RTr5Yx1+c97EOc/RRvwSvsMrtQ9VgefSNYllk+yJ5h3hkrPaf1PJeEww==
+linaria@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/linaria/-/linaria-1.2.1.tgz#c830c295258b7b03ef0a7b3ee5b82b9414895846"
+  integrity sha512-2EpJhomogU2UQAJLsabL667lgIYz2+RemWboqn/WF6ULgN0+0ZarsjPiIm7BGTgglEo2F9Cs71lxYPKR1InGug==
   dependencies:
     "@babel/core" "^7.3.3"
     "@babel/generator" "^7.3.3"
diff --git a/example/src/Examples/ListSectionExample.js b/example/src/Examples/ListSectionExample.js
index 3bdb3229a..a44097ac6 100644
--- a/example/src/Examples/ListSectionExample.js
+++ b/example/src/Examples/ListSectionExample.js
@@ -19,7 +19,8 @@ class ListSectionExample extends React.Component<Props> {
     } = this.props;
     return (
       <ScrollView style={[styles.container, { backgroundColor: background }]}>
-        <List.Section title="Single line">
+        <List.Section>
+          <List.Subheader>Single line</List.Subheader>
           <List.Item
             left={props => <List.Icon {...props} icon="event" />}
             title="List item 1"
@@ -30,7 +31,8 @@ class ListSectionExample extends React.Component<Props> {
           />
         </List.Section>
         <Divider />
-        <List.Section title="Two line">
+        <List.Section>
+          <List.Subheader>Two line</List.Subheader>
           <List.Item
             left={() => (
               <Image
@@ -54,7 +56,8 @@ class ListSectionExample extends React.Component<Props> {
           />
         </List.Section>
         <Divider />
-        <List.Section title="Three line">
+        <List.Section>
+          <List.Subheader>Three line</List.Subheader>
           <List.Item
             left={() => (
               <Image
diff --git a/package.json b/package.json
index bdbabb9d5..0bdd54d5b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-native-paper",
-  "version": "2.11.1",
+  "version": "2.12.0",
   "description": "Material design for React Native",
   "main": "src/index.js",
   "typings": "typings/index.d.ts",
diff --git a/src/components/List/List.js b/src/components/List/List.js
index 921e556de..91c14fbf9 100644
--- a/src/components/List/List.js
+++ b/src/components/List/List.js
@@ -11,3 +11,6 @@ export { default as Item } from './ListItem';
 
 // @component ./ListSection.js
 export { default as Section } from './ListSection';
+
+// @component ./ListSubheader.js
+export { default as Subheader } from './ListSubheader';
diff --git a/src/components/List/ListSection.js b/src/components/List/ListSection.js
index dd7da2480..1091c63e0 100644
--- a/src/components/List/ListSection.js
+++ b/src/components/List/ListSection.js
@@ -1,9 +1,8 @@
 /* @flow */
 
-import color from 'color';
 import * as React from 'react';
 import { View, StyleSheet } from 'react-native';
-import Text from '../Typography/Text';
+import ListSubheader from './ListSubheader';
 import { withTheme } from '../../core/theming';
 import type { Theme } from '../../types';
 
@@ -38,7 +37,8 @@ type Props = React.ElementConfig<typeof View> & {
  * export default class MyComponent extends React.Component {
  *   render() {
  *     return (
- *       <List.Section title="Some title">
+ *       <List.Section>
+ *         <List.Subheader>Some title</List.Subheader>
  *         <List.Item
  *           title="First Item"
  *           left={() => <List.Icon icon="folder" />}
@@ -59,25 +59,11 @@ class ListSection extends React.Component<Props> {
   static displayName = 'List.Section';
 
   render() {
-    const { children, title, theme, style, ...rest } = this.props;
-    const { colors, fonts } = theme;
-
-    const titleColor = color(colors.text)
-      .alpha(0.54)
-      .rgb()
-      .string();
-    const fontFamily = fonts.medium;
+    const { children, title, style, ...rest } = this.props;
 
     return (
       <View {...rest} style={[styles.container, style]}>
-        {title && (
-          <Text
-            numberOfLines={1}
-            style={[styles.title, { color: titleColor, fontFamily }]}
-          >
-            {title}
-          </Text>
-        )}
+        {title && <ListSubheader>{title}</ListSubheader>}
         {children}
       </View>
     );
@@ -88,10 +74,6 @@ const styles = StyleSheet.create({
   container: {
     marginVertical: 8,
   },
-  title: {
-    marginVertical: 13,
-    marginHorizontal: 16,
-  },
 });
 
 export default withTheme(ListSection);
diff --git a/src/components/List/ListSubheader.js b/src/components/List/ListSubheader.js
new file mode 100644
index 000000000..2eb9b21c0
--- /dev/null
+++ b/src/components/List/ListSubheader.js
@@ -0,0 +1,64 @@
+/* @flow */
+
+import * as React from 'react';
+import { StyleSheet } from 'react-native';
+import color from 'color';
+import Text from '../Typography/Text';
+import { withTheme } from '../../core/theming';
+import type { Theme } from '../../types';
+
+type Props = React.ElementConfig<typeof Text> & {
+  /**
+   * @optional
+   */
+  theme: Theme,
+  style?: any,
+};
+
+/**
+ * A component used to display a header in lists.
+ *
+ * ## Usage
+ * ```js
+ * import * as React from 'react';
+ * import { List } from 'react-native-paper';
+ *
+ * class MyComponent extends React.Component {
+ *  render () {
+ *    return <List.Subheader>My List Title</List.Subheader>;
+ *  }
+ * }
+ *
+ * export default MyComponent;
+ * ```
+ */
+class ListSubheader extends React.Component<Props> {
+  static displayName = 'List.Subheader';
+
+  render() {
+    const { style, theme, ...rest } = this.props;
+    const { colors, fonts } = theme;
+    const fontFamily = fonts.medium;
+    const textColor = color(colors.text)
+      .alpha(0.54)
+      .rgb()
+      .string();
+
+    return (
+      <Text
+        numberOfLines={1}
+        {...rest}
+        style={[styles.container, { color: textColor, fontFamily }, style]}
+      />
+    );
+  }
+}
+
+const styles = StyleSheet.create({
+  container: {
+    paddingHorizontal: 16,
+    paddingVertical: 13,
+  },
+});
+
+export default withTheme(ListSubheader);
diff --git a/typings/components/List.d.ts b/typings/components/List.d.ts
index 310670c2d..993094e00 100644
--- a/typings/components/List.d.ts
+++ b/typings/components/List.d.ts
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import { StyleProp, ViewStyle, ViewProps } from 'react-native';
+import { StyleProp, ViewStyle, ViewProps, TextProps, TextStyle } from 'react-native';
 import { ThemeShape, IconSource } from '../types';
 import { TouchableRipplePropsWithoutChildren } from './TouchableRipple';
 
@@ -40,3 +40,10 @@ export interface SectionProps extends ViewProps {
 }
 
 export declare class Section extends React.Component<SectionProps> {}
+
+export interface SubheaderProps extends TextProps {
+  style?: StyleProp<TextStyle>;
+  theme?: ThemeShape
+}
+
+export declare class Subheader extends React.Component<SubheaderProps> {}


Test patch:
diff --git a/src/components/__tests__/ListSection.test.js b/src/components/__tests__/ListSection.test.js
index 0a760f88e..190e48e90 100644
--- a/src/components/__tests__/ListSection.test.js
+++ b/src/components/__tests__/ListSection.test.js
@@ -5,8 +5,9 @@ import renderer from 'react-test-renderer';
 import ListSection from '../List/ListSection';
 import ListItem from '../List/ListItem';
 import ListIcon from '../List/ListIcon';
+import ListSubheader from '../List/ListSubheader';
 
-it('renders list section without title', () => {
+it('renders list section without subheader', () => {
   const tree = renderer
     .create(
       <ListSection>
@@ -25,10 +26,11 @@ it('renders list section without title', () => {
   expect(tree).toMatchSnapshot();
 });
 
-it('renders list section with title', () => {
+it('renders list section with subheader', () => {
   const tree = renderer
     .create(
-      <ListSection title="Some title">
+      <ListSection>
+        <ListSubheader>Some title</ListSubheader>
         <ListItem
           title="First Item"
           left={props => <ListIcon {...props} icon="folder" />}
diff --git a/src/components/__tests__/__snapshots__/ListSection.test.js.snap b/src/components/__tests__/__snapshots__/ListSection.test.js.snap
index ccb243031..9bf48f8c2 100644
--- a/src/components/__tests__/__snapshots__/ListSection.test.js.snap
+++ b/src/components/__tests__/__snapshots__/ListSection.test.js.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`renders list section with title 1`] = `
+exports[`renders list section with subheader 1`] = `
 <View
   style={
     Array [
@@ -10,6 +10,30 @@ exports[`renders list section with title 1`] = `
       undefined,
     ]
   }
+  theme={
+    Object {
+      "colors": Object {
+        "accent": "#03dac4",
+        "backdrop": "rgba(0, 0, 0, 0.5)",
+        "background": "#f6f6f6",
+        "disabled": "rgba(0, 0, 0, 0.26)",
+        "error": "#B00020",
+        "notification": "#f50057",
+        "placeholder": "rgba(0, 0, 0, 0.54)",
+        "primary": "#6200ee",
+        "surface": "#ffffff",
+        "text": "#000000",
+      },
+      "dark": false,
+      "fonts": Object {
+        "light": "HelveticaNeue-Light",
+        "medium": "HelveticaNeue-Medium",
+        "regular": "Helvetica Neue",
+        "thin": "HelveticaNeue-Thin",
+      },
+      "roundness": 4,
+    }
+  }
 >
   <Text
     numberOfLines={1}
@@ -23,13 +47,14 @@ exports[`renders list section with title 1`] = `
         },
         Array [
           Object {
-            "marginHorizontal": 16,
-            "marginVertical": 13,
+            "paddingHorizontal": 16,
+            "paddingVertical": 13,
           },
           Object {
             "color": "rgba(0, 0, 0, 0.54)",
             "fontFamily": "HelveticaNeue-Medium",
           },
+          undefined,
         ],
       ]
     }
@@ -275,7 +300,7 @@ exports[`renders list section with title 1`] = `
 </View>
 `;
 
-exports[`renders list section without title 1`] = `
+exports[`renders list section without subheader 1`] = `
 <View
   style={
     Array [
@@ -285,6 +310,30 @@ exports[`renders list section without title 1`] = `
       undefined,
     ]
   }
+  theme={
+    Object {
+      "colors": Object {
+        "accent": "#03dac4",
+        "backdrop": "rgba(0, 0, 0, 0.5)",
+        "background": "#f6f6f6",
+        "disabled": "rgba(0, 0, 0, 0.26)",
+        "error": "#B00020",
+        "notification": "#f50057",
+        "placeholder": "rgba(0, 0, 0, 0.54)",
+        "primary": "#6200ee",
+        "surface": "#ffffff",
+        "text": "#000000",
+      },
+      "dark": false,
+      "fonts": Object {
+        "light": "HelveticaNeue-Light",
+        "medium": "HelveticaNeue-Medium",
+        "regular": "Helvetica Neue",
+        "thin": "HelveticaNeue-Thin",
+      },
+      "roundness": 4,
+    }
+  }
 >
   <View
     accessible={true}