Repository: grommet/grommet (JavaScript)

Issue:
DataTable - Ability to sort on a column.property which references a nested object value
<!--- Provide a general summary of the issue in the Title above -->

### Expected Behavior

If I have a data set in the shape of:
```
[
    {
        name: 'Johannes',
        favorites: {
            color: 'blue',
            movie: 'Butch Cassidy and the Sundance Kid',
            number: 3,
        },
    },
    ...
]
```

I would like the ability to sort on a column.property which references a nested object value.

### Actual Behavior

When
```
<DataTable data={data} columns={columns} sortable />
```
And `columns` contains
```
[
    ...
    {
        property: 'favorites.number'
    }
    ...
]
```
the DataTable renders the values, but the column is not sortable.

### URL, screen shot, or Codepen exhibiting the issue

https://codesandbox.io/s/jolly-cookies-l5x15?file=/src/App.js

[Related Issue](https://github.com/hpe-design/design-system/issues/1079)

### Steps to Reproduce

1. 2. 3.

### Your Environment

<!--- Include as many relevant details about the environment you experienced the bug in -->

- Grommet version: 2.15 / Stable
- Browser Name and version:
- Operating System and version (desktop or mobile):



Discussion:

Patch:
diff --git a/src/js/components/DataTable/buildState.js b/src/js/components/DataTable/buildState.js
index 48341196434..7113776bb50 100644
--- a/src/js/components/DataTable/buildState.js
+++ b/src/js/components/DataTable/buildState.js
@@ -71,8 +71,8 @@ export const filterAndSortData = (data, filters, onSearch, sort) => {
     const before = direction === 'asc' ? 1 : -1;
     const after = direction === 'asc' ? -1 : 1;
     result.sort((d1, d2) => {
-      if (d1[property] > d2[property]) return before;
-      if (d1[property] < d2[property]) return after;
+      if (datumValue(d1, property) > datumValue(d2, property)) return before;
+      if (datumValue(d1, property) < datumValue(d2, property)) return after;
       return 0;
     });
   }


Test patch:
diff --git a/src/js/components/DataTable/__tests__/DataTable-test.js b/src/js/components/DataTable/__tests__/DataTable-test.js
index 810a443daf2..6cda67ea531 100644
--- a/src/js/components/DataTable/__tests__/DataTable-test.js
+++ b/src/js/components/DataTable/__tests__/DataTable-test.js
@@ -182,6 +182,82 @@ describe('DataTable', () => {
     expect(container.firstChild).toMatchSnapshot();
   });
 
+  test('sort nested object', () => {
+    const { container, getByText } = render(
+      <Grommet>
+        <DataTable
+          columns={[
+            { property: 'a', header: 'A' },
+            {
+              property: 'b.value',
+              header: 'Value',
+              render: datum => datum.b && datum.b.value,
+            },
+          ]}
+          data={[
+            { a: 'zero', b: { value: 1 } },
+            { a: 'one', b: { value: 2 } },
+            { a: 'two', b: { value: 3 } },
+          ]}
+          sort={{ property: 'b.value', direction: 'asc' }}
+        />
+      </Grommet>,
+    );
+
+    expect(container.querySelectorAll('td').item(0).textContent).toBe('1');
+    expect(container.querySelectorAll('td').item(1).textContent).toBe('2');
+    expect(container.querySelectorAll('td').item(2).textContent).toBe('3');
+
+    fireEvent.click(getByText('Value'));
+
+    expect(container.querySelectorAll('td').item(0).textContent).toBe('3');
+    expect(container.querySelectorAll('td').item(1).textContent).toBe('2');
+    expect(container.querySelectorAll('td').item(2).textContent).toBe('1');
+
+    expect(container.firstChild).toMatchSnapshot();
+  });
+
+  test('sort nested object with onSort', () => {
+    const onSort = jest.fn();
+    const { container, getByText } = render(
+      <Grommet>
+        <DataTable
+          columns={[
+            { property: 'a', header: 'A' },
+            {
+              property: 'b.value',
+              header: 'Value',
+              render: datum => datum.b && datum.b.value,
+            },
+          ]}
+          data={[
+            { a: 'zero', b: { value: 1 } },
+            { a: 'one', b: { value: 2 } },
+            { a: 'two', b: { value: 3 } },
+          ]}
+          onSort={onSort}
+          sort={{ property: 'b.value', direction: 'asc' }}
+        />
+      </Grommet>,
+    );
+
+    expect(container.querySelectorAll('td').item(0).textContent).toBe('1');
+    expect(container.querySelectorAll('td').item(1).textContent).toBe('2');
+    expect(container.querySelectorAll('td').item(2).textContent).toBe('3');
+
+    fireEvent.click(getByText('Value'));
+
+    expect(onSort).toBeCalledWith(
+      expect.objectContaining({ property: 'b.value' }),
+    );
+
+    expect(container.querySelectorAll('td').item(0).textContent).toBe('3');
+    expect(container.querySelectorAll('td').item(1).textContent).toBe('2');
+    expect(container.querySelectorAll('td').item(2).textContent).toBe('1');
+
+    expect(container.firstChild).toMatchSnapshot();
+  });
+
   test('sort external', () => {
     const { container } = render(
       <Grommet>
diff --git a/src/js/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap b/src/js/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap
index 4bcb15252a3..abeacbef63b 100644
--- a/src/js/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap
+++ b/src/js/components/DataTable/__tests__/__snapshots__/DataTable-test.js.snap
@@ -9218,6 +9218,722 @@ exports[`DataTable sort external 1`] = `
 </div>
 `;
 
+exports[`DataTable sort nested object 1`] = `
+.c8 {
+  display: inline-block;
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+  width: 24px;
+  height: 24px;
+  fill: #666666;
+  stroke: #666666;
+}
+
+.c8 g {
+  fill: inherit;
+  stroke: inherit;
+}
+
+.c8 *:not([stroke])[fill="none"] {
+  stroke-width: 0;
+}
+
+.c8 *[stroke*="#"],
+.c8 *[STROKE*="#"] {
+  stroke: inherit;
+  fill: none;
+}
+
+.c8 *[fill-rule],
+.c8 *[FILL-RULE],
+.c8 *[fill*="#"],
+.c8 *[FILL*="#"] {
+  fill: inherit;
+  stroke: none;
+}
+
+.c0 {
+  font-size: 18px;
+  line-height: 24px;
+  box-sizing: border-box;
+  -webkit-text-size-adjust: 100%;
+  -ms-text-size-adjust: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+}
+
+.c5 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  box-sizing: border-box;
+  max-width: 100%;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  min-width: 0;
+  min-height: 0;
+  -webkit-flex-direction: row;
+  -ms-flex-direction: row;
+  flex-direction: row;
+}
+
+.c11 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  box-sizing: border-box;
+  max-width: 100%;
+  min-width: 0;
+  min-height: 0;
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+}
+
+.c7 {
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+  -webkit-align-self: stretch;
+  -ms-flex-item-align: stretch;
+  align-self: stretch;
+  width: 6px;
+}
+
+.c4 {
+  display: inline-block;
+  box-sizing: border-box;
+  cursor: pointer;
+  font: inherit;
+  -webkit-text-decoration: none;
+  text-decoration: none;
+  margin: 0;
+  background: transparent;
+  overflow: visible;
+  text-transform: none;
+  color: inherit;
+  outline: none;
+  border: none;
+  padding: 0;
+  text-align: inherit;
+  height: 100%;
+}
+
+.c4:focus {
+  outline: none;
+  box-shadow: 0 0 2px 2px #6FFFB0;
+}
+
+.c4:focus > circle,
+.c4:focus > ellipse,
+.c4:focus > line,
+.c4:focus > path,
+.c4:focus > polygon,
+.c4:focus > polyline,
+.c4:focus > rect {
+  outline: none;
+  box-shadow: 0 0 2px 2px #6FFFB0;
+}
+
+.c4:focus::-moz-focus-inner {
+  border: 0;
+}
+
+.c3 {
+  margin: 0;
+  padding: 0;
+  font-weight: inherit;
+  text-align: inherit;
+  text-align: start;
+  border-bottom: solid 1px rgba(0,0,0,0.33);
+  padding-left: 12px;
+  padding-right: 12px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+.c10 {
+  margin: 0;
+  padding: 0;
+  font-weight: inherit;
+  text-align: inherit;
+  text-align: start;
+  padding-left: 12px;
+  padding-right: 12px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+.c1 {
+  border-spacing: 0;
+  border-collapse: collapse;
+  width: inherit;
+}
+
+.c6 {
+  font-size: 18px;
+  line-height: 24px;
+}
+
+.c12 {
+  font-size: 18px;
+  line-height: 24px;
+  font-weight: bold;
+}
+
+.c2 {
+  border-spacing: 0;
+  border-collapse: separate;
+  height: auto;
+}
+
+.c9:focus {
+  outline: 2px solid #6FFFB0;
+}
+
+@media only screen and (max-width:768px) {
+  .c7 {
+    width: 3px;
+  }
+}
+
+@media all and (min--moz-device-pixel-ratio:0) {
+  .c1 {
+    table-layout: fixed;
+  }
+}
+
+<div
+  class="c0"
+>
+  <table
+    class="c1 c2"
+  >
+    <thead
+      class=""
+    >
+      <tr
+        class=""
+      >
+        <th
+          class="c3 "
+          scope="col"
+        >
+          <button
+            class="c4"
+            type="button"
+          >
+            <div
+              class="c5"
+            >
+              <span
+                class="c6"
+              >
+                A
+              </span>
+            </div>
+          </button>
+        </th>
+        <th
+          class="c3 "
+          scope="col"
+        >
+          <button
+            class="c4"
+            type="button"
+          >
+            <div
+              class="c5"
+            >
+              <span
+                class="c6"
+              >
+                Value
+              </span>
+              <div
+                class="c7"
+              />
+              <svg
+                aria-label="FormDown"
+                class="c8"
+                viewBox="0 0 24 24"
+              >
+                <polyline
+                  fill="none"
+                  points="18 9 12 15 6 9"
+                  stroke="#000"
+                  stroke-width="2"
+                />
+              </svg>
+            </div>
+          </button>
+        </th>
+      </tr>
+    </thead>
+    <tbody
+      class="c9"
+    >
+      <tr
+        class=""
+      >
+        <th
+          class="c10 "
+          scope="row"
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c12"
+            >
+              two
+            </span>
+          </div>
+        </th>
+        <td
+          class="c10 "
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c6"
+            >
+              3
+            </span>
+          </div>
+        </td>
+      </tr>
+      <tr
+        class=""
+      >
+        <th
+          class="c10 "
+          scope="row"
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c12"
+            >
+              one
+            </span>
+          </div>
+        </th>
+        <td
+          class="c10 "
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c6"
+            >
+              2
+            </span>
+          </div>
+        </td>
+      </tr>
+      <tr
+        class=""
+      >
+        <th
+          class="c10 "
+          scope="row"
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c12"
+            >
+              zero
+            </span>
+          </div>
+        </th>
+        <td
+          class="c10 "
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c6"
+            >
+              1
+            </span>
+          </div>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+`;
+
+exports[`DataTable sort nested object with onSort 1`] = `
+.c8 {
+  display: inline-block;
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+  width: 24px;
+  height: 24px;
+  fill: #666666;
+  stroke: #666666;
+}
+
+.c8 g {
+  fill: inherit;
+  stroke: inherit;
+}
+
+.c8 *:not([stroke])[fill="none"] {
+  stroke-width: 0;
+}
+
+.c8 *[stroke*="#"],
+.c8 *[STROKE*="#"] {
+  stroke: inherit;
+  fill: none;
+}
+
+.c8 *[fill-rule],
+.c8 *[FILL-RULE],
+.c8 *[fill*="#"],
+.c8 *[FILL*="#"] {
+  fill: inherit;
+  stroke: none;
+}
+
+.c0 {
+  font-size: 18px;
+  line-height: 24px;
+  box-sizing: border-box;
+  -webkit-text-size-adjust: 100%;
+  -ms-text-size-adjust: 100%;
+  -moz-osx-font-smoothing: grayscale;
+  -webkit-font-smoothing: antialiased;
+}
+
+.c5 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  box-sizing: border-box;
+  max-width: 100%;
+  -webkit-align-items: center;
+  -webkit-box-align: center;
+  -ms-flex-align: center;
+  align-items: center;
+  min-width: 0;
+  min-height: 0;
+  -webkit-flex-direction: row;
+  -ms-flex-direction: row;
+  flex-direction: row;
+}
+
+.c11 {
+  display: -webkit-box;
+  display: -webkit-flex;
+  display: -ms-flexbox;
+  display: flex;
+  box-sizing: border-box;
+  max-width: 100%;
+  min-width: 0;
+  min-height: 0;
+  -webkit-flex-direction: column;
+  -ms-flex-direction: column;
+  flex-direction: column;
+}
+
+.c7 {
+  -webkit-flex: 0 0 auto;
+  -ms-flex: 0 0 auto;
+  flex: 0 0 auto;
+  -webkit-align-self: stretch;
+  -ms-flex-item-align: stretch;
+  align-self: stretch;
+  width: 6px;
+}
+
+.c4 {
+  display: inline-block;
+  box-sizing: border-box;
+  cursor: pointer;
+  font: inherit;
+  -webkit-text-decoration: none;
+  text-decoration: none;
+  margin: 0;
+  background: transparent;
+  overflow: visible;
+  text-transform: none;
+  color: inherit;
+  outline: none;
+  border: none;
+  padding: 0;
+  text-align: inherit;
+  height: 100%;
+}
+
+.c4:focus {
+  outline: none;
+  box-shadow: 0 0 2px 2px #6FFFB0;
+}
+
+.c4:focus > circle,
+.c4:focus > ellipse,
+.c4:focus > line,
+.c4:focus > path,
+.c4:focus > polygon,
+.c4:focus > polyline,
+.c4:focus > rect {
+  outline: none;
+  box-shadow: 0 0 2px 2px #6FFFB0;
+}
+
+.c4:focus::-moz-focus-inner {
+  border: 0;
+}
+
+.c3 {
+  margin: 0;
+  padding: 0;
+  font-weight: inherit;
+  text-align: inherit;
+  text-align: start;
+  border-bottom: solid 1px rgba(0,0,0,0.33);
+  padding-left: 12px;
+  padding-right: 12px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+.c10 {
+  margin: 0;
+  padding: 0;
+  font-weight: inherit;
+  text-align: inherit;
+  text-align: start;
+  padding-left: 12px;
+  padding-right: 12px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+
+.c1 {
+  border-spacing: 0;
+  border-collapse: collapse;
+  width: inherit;
+}
+
+.c6 {
+  font-size: 18px;
+  line-height: 24px;
+}
+
+.c12 {
+  font-size: 18px;
+  line-height: 24px;
+  font-weight: bold;
+}
+
+.c2 {
+  border-spacing: 0;
+  border-collapse: separate;
+  height: auto;
+}
+
+.c9:focus {
+  outline: 2px solid #6FFFB0;
+}
+
+@media only screen and (max-width:768px) {
+  .c7 {
+    width: 3px;
+  }
+}
+
+@media all and (min--moz-device-pixel-ratio:0) {
+  .c1 {
+    table-layout: fixed;
+  }
+}
+
+<div
+  class="c0"
+>
+  <table
+    class="c1 c2"
+  >
+    <thead
+      class=""
+    >
+      <tr
+        class=""
+      >
+        <th
+          class="c3 "
+          scope="col"
+        >
+          <button
+            class="c4"
+            type="button"
+          >
+            <div
+              class="c5"
+            >
+              <span
+                class="c6"
+              >
+                A
+              </span>
+            </div>
+          </button>
+        </th>
+        <th
+          class="c3 "
+          scope="col"
+        >
+          <button
+            class="c4"
+            type="button"
+          >
+            <div
+              class="c5"
+            >
+              <span
+                class="c6"
+              >
+                Value
+              </span>
+              <div
+                class="c7"
+              />
+              <svg
+                aria-label="FormDown"
+                class="c8"
+                viewBox="0 0 24 24"
+              >
+                <polyline
+                  fill="none"
+                  points="18 9 12 15 6 9"
+                  stroke="#000"
+                  stroke-width="2"
+                />
+              </svg>
+            </div>
+          </button>
+        </th>
+      </tr>
+    </thead>
+    <tbody
+      class="c9"
+    >
+      <tr
+        class=""
+      >
+        <th
+          class="c10 "
+          scope="row"
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c12"
+            >
+              two
+            </span>
+          </div>
+        </th>
+        <td
+          class="c10 "
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c6"
+            >
+              3
+            </span>
+          </div>
+        </td>
+      </tr>
+      <tr
+        class=""
+      >
+        <th
+          class="c10 "
+          scope="row"
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c12"
+            >
+              one
+            </span>
+          </div>
+        </th>
+        <td
+          class="c10 "
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c6"
+            >
+              2
+            </span>
+          </div>
+        </td>
+      </tr>
+      <tr
+        class=""
+      >
+        <th
+          class="c10 "
+          scope="row"
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c12"
+            >
+              zero
+            </span>
+          </div>
+        </th>
+        <td
+          class="c10 "
+        >
+          <div
+            class="c11"
+          >
+            <span
+              class="c6"
+            >
+              1
+            </span>
+          </div>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+`;
+
 exports[`DataTable sortable 1`] = `
 .c0 {
   font-size: 18px;