open-nomad/ui/stories/components/table.stories.js
Buck Doyle 576bcf554f
UI: Migrate to Storybook (#6507)
I originally planned to add component documentation, but as this dragged on and I found that JSDoc-to-Markdown sometimes needed hand-tuning, I decided to skip it and focus on replicating what was already present in Freestyle. Adding documentation is a finite task that can be revisited in the future.

My goal was to migrate everything from Freestyle with as few changes as possible. Some adaptations that I found necessary:
• the DelayedArray and DelayedTruth utilities that delay component rendering until slightly after initial render because without them:
  ◦ charts were rendering with zero width
  ◦ the JSON viewer was rendering with empty content
• Storybook in Ember renders components in a routerless/controllerless context by default, so some component stories needed changes:
  ◦ table pagination/sorting stories access to query params, which necessitates some reaching into Ember internals to start routing and dynamically generate a Storybook route/controller to render components into
  ◦ some stories have a faux controller as part of their Storybook context that hosts setInterval-linked dynamic computed properties
• some jiggery-pokery with anchor tags
  ◦ inert href='#' had to become href='javascript:;
  ◦ links that are actually meant to navigate need target='_parent' so they don’t navigate inside the Storybook iframe

Maybe some of these could be addressed by fixes in ember-cli-storybook but I’m wary of digging around in there any more than I already have, as I’ve lost a lot of time to Storybook confusion and frustrations already 😞

The STORYBOOK=true environment variable tweaks some environment settings to get things working as expected in the Storybook context.

I chose to:
• use angle bracket invocation within stories rather than have to migrate them soon after having moved to Storybook
• keep Freestyle around for now for its palette and typeface components
2020-01-21 15:46:32 -06:00

484 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import hbs from 'htmlbars-inline-precompile';
import productMetadata from '../../app/utils/styleguide/product-metadata';
import EmberObject, { computed } from '@ember/object';
import { getOwner } from '@ember/application';
import { on } from '@ember/object/evented';
import Controller from '@ember/controller';
export default {
title: 'Components|Table',
};
/**
* The Ember integration for Storybook renders a container component with no routing,
* which means things that need query parameters, like sorting and pagination, wont work.
* This initialiser turns on routing and accepts a controller definition that gets wired up
* to a generated `storybook` route. The controller is attached to the Storybook component
* as the `controller` property so its query parameters are accessible from the template.
*/
function injectRoutedController(controllerClass) {
return on('init', function() {
let container = getOwner(this);
container.register('controller:storybook', controllerClass);
let routerFactory = container.factoryFor('router:main');
routerFactory.class.map(function() {
this.route('storybook');
});
let router = container.lookup('router:main');
router.initialURL = 'storybook';
router.startRouting(true);
this.set('controller', container.lookup('controller:storybook'));
});
}
let longList = [
{ city: 'New York', growth: 0.048, population: '8405837', rank: '1', state: 'New York' },
{ city: 'Los Angeles', growth: 0.048, population: '3884307', rank: '2', state: 'California' },
{ city: 'Chicago', growth: -0.061, population: '2718782', rank: '3', state: 'Illinois' },
{ city: 'Houston', growth: 0.11, population: '2195914', rank: '4', state: 'Texas' },
{
city: 'Philadelphia',
growth: 0.026,
population: '1553165',
rank: '5',
state: 'Pennsylvania',
},
{ city: 'Phoenix', growth: 0.14, population: '1513367', rank: '6', state: 'Arizona' },
{ city: 'San Antonio', growth: 0.21, population: '1409019', rank: '7', state: 'Texas' },
{ city: 'San Diego', growth: 0.105, population: '1355896', rank: '8', state: 'California' },
{ city: 'Dallas', growth: 0.056, population: '1257676', rank: '9', state: 'Texas' },
{ city: 'San Jose', growth: 0.105, population: '998537', rank: '10', state: 'California' },
{ city: 'Austin', growth: 0.317, population: '885400', rank: '11', state: 'Texas' },
{ city: 'Indianapolis', growth: 0.078, population: '843393', rank: '12', state: 'Indiana' },
{ city: 'Jacksonville', growth: 0.143, population: '842583', rank: '13', state: 'Florida' },
{
city: 'San Francisco',
growth: 0.077,
population: '837442',
rank: '14',
state: 'California',
},
{ city: 'Columbus', growth: 0.148, population: '822553', rank: '15', state: 'Ohio' },
{
city: 'Charlotte',
growth: 0.391,
population: '792862',
rank: '16',
state: 'North Carolina',
},
{ city: 'Fort Worth', growth: 0.451, population: '792727', rank: '17', state: 'Texas' },
{ city: 'Detroit', growth: -0.271, population: '688701', rank: '18', state: 'Michigan' },
{ city: 'El Paso', growth: 0.194, population: '674433', rank: '19', state: 'Texas' },
{ city: 'Memphis', growth: -0.053, population: '653450', rank: '20', state: 'Tennessee' },
{ city: 'Seattle', growth: 0.156, population: '652405', rank: '21', state: 'Washington' },
{ city: 'Denver', growth: 0.167, population: '649495', rank: '22', state: 'Colorado' },
{
city: 'Washington',
growth: 0.13,
population: '646449',
rank: '23',
state: 'District of Columbia',
},
{ city: 'Boston', growth: 0.094, population: '645966', rank: '24', state: 'Massachusetts' },
{
city: 'Nashville-Davidson',
growth: 0.162,
population: '634464',
rank: '25',
state: 'Tennessee',
},
{ city: 'Baltimore', growth: -0.04, population: '622104', rank: '26', state: 'Maryland' },
{ city: 'Oklahoma City', growth: 0.202, population: '610613', rank: '27', state: 'Oklahoma' },
{
city: 'Louisville/Jefferson County',
growth: 0.1,
population: '609893',
rank: '28',
state: 'Kentucky',
},
{ city: 'Portland', growth: 0.15, population: '609456', rank: '29', state: 'Oregon' },
{ city: 'Las Vegas', growth: 0.245, population: '603488', rank: '30', state: 'Nevada' },
{ city: 'Milwaukee', growth: 0.003, population: '599164', rank: '31', state: 'Wisconsin' },
{ city: 'Albuquerque', growth: 0.235, population: '556495', rank: '32', state: 'New Mexico' },
{ city: 'Tucson', growth: 0.075, population: '526116', rank: '33', state: 'Arizona' },
{ city: 'Fresno', growth: 0.183, population: '509924', rank: '34', state: 'California' },
{ city: 'Sacramento', growth: 0.172, population: '479686', rank: '35', state: 'California' },
{ city: 'Long Beach', growth: 0.015, population: '469428', rank: '36', state: 'California' },
{ city: 'Kansas City', growth: 0.055, population: '467007', rank: '37', state: 'Missouri' },
{ city: 'Mesa', growth: 0.135, population: '457587', rank: '38', state: 'Arizona' },
{ city: 'Virginia Beach', growth: 0.051, population: '448479', rank: '39', state: 'Virginia' },
{ city: 'Atlanta', growth: 0.062, population: '447841', rank: '40', state: 'Georgia' },
{
city: 'Colorado Springs',
growth: 0.214,
population: '439886',
rank: '41',
state: 'Colorado',
},
{ city: 'Omaha', growth: 0.059, population: '434353', rank: '42', state: 'Nebraska' },
{ city: 'Raleigh', growth: 0.487, population: '431746', rank: '43', state: 'North Carolina' },
{ city: 'Miami', growth: 0.149, population: '417650', rank: '44', state: 'Florida' },
{ city: 'Oakland', growth: 0.013, population: '406253', rank: '45', state: 'California' },
{ city: 'Minneapolis', growth: 0.045, population: '400070', rank: '46', state: 'Minnesota' },
{ city: 'Tulsa', growth: 0.013, population: '398121', rank: '47', state: 'Oklahoma' },
{ city: 'Cleveland', growth: -0.181, population: '390113', rank: '48', state: 'Ohio' },
{ city: 'Wichita', growth: 0.097, population: '386552', rank: '49', state: 'Kansas' },
{ city: 'Arlington', growth: 0.133, population: '379577', rank: '50', state: 'Texas' },
];
export let Standard = () => {
return {
template: hbs`
<h5 class="title is-5">Table</h5>
<ListTable @source={{shortList}} as |t|>
<t.head>
<th>Name</th>
<th>Language</th>
<th>Description</th>
</t.head>
<t.body @key="model.name" as |row|>
<tr>
<td>{{row.model.name}}</td>
<td>{{row.model.lang}}</td>
<td>{{row.model.desc}}</td>
</tr>
</t.body>
</ListTable>
<p class="annotation">Tables have airy designs with a minimal amount of borders. This maximizes their utility.</p>
`,
context: {
shortList: productMetadata,
},
};
};
export let Search = () => {
return {
template: hbs`
<h5 class="title is-5">Table search</h5>
<div class="boxed-section">
<div class="boxed-section-head">
Table Name
<SearchBox
@searchTerm={{mut controller.searchTerm}}
@placeholder="Search..."
@class="is-inline pull-right"
@inputClass="is-compact" />
</div>
<div class="boxed-section-body {{if controller.filteredShortList.length "is-full-bleed"}}">
{{#if controller.filteredShortList.length}}
<ListTable @source={{controller.filteredShortList}} as |t|>
<t.head>
<th>Name</th>
<th>Language</th>
<th>Description</th>
</t.head>
<t.body @key="model.name" as |row|>
<tr>
<td>{{row.model.name}}</td>
<td>{{row.model.lang}}</td>
<td>{{row.model.desc}}</td>
</tr>
</t.body>
</ListTable>
{{else}}
<div class="empty-message">
<h3 class="empty-message-headline">No Matches</h3>
<p class="empty-message-body">No products match your query.</p>
</div>
{{/if}}
</div>
</div>
<p class="annotation">Tables compose with boxed-section and boxed-section composes with search box.</p>
`,
context: {
controller: EmberObject.extend({
searchTerm: '',
filteredShortList: computed('searchTerm', function() {
let term = this.searchTerm.toLowerCase();
return productMetadata.filter(product => product.name.toLowerCase().includes(term));
}),
}).create(),
},
};
};
export let SortableColumns = () => {
return {
template: hbs`
<h5 class="title is-5">Table with sortable columns</h5>
<ListTable @source={{sortedShortList}} @sortProperty={{controller.sortProperty}} @sortDescending={{controller.sortDescending}} as |t|>
<t.head>
<t.sort-by @prop="name">Name</t.sort-by>
<t.sort-by @prop="lang" @class="is-2">Language</t.sort-by>
<th>Description</th>
</t.head>
<t.body @key="model.name" as |row|>
<tr>
<td>{{row.model.name}}</td>
<td>{{row.model.lang}}</td>
<td>{{row.model.desc}}</td>
</tr>
</t.body>
</ListTable>
<p class="annotation">The list-table component provides a <code>sort-by</code> contextual component for building <code>link-to</code> components with the appropriate query params.</p>
<p class="annotation">This leaves the component stateless, relying on data to be passed down and sending actions back up via the router (via link-to).</p>
`,
context: {
injectRoutedController: injectRoutedController(
Controller.extend({
queryParams: ['sortProperty', 'sortDescending'],
sortProperty: 'name',
sortDescending: false,
})
),
sortedShortList: computed('controller.sortProperty', 'controller.sortDescending', function() {
let sorted = productMetadata.sortBy(this.get('controller.sortProperty') || 'name');
return this.get('controller.sortDescending') ? sorted.reverse() : sorted;
}),
},
};
};
export let MultiRow = () => {
return {
template: hbs`
<h5 class="title is-5">Multi-row Table</h5>
<ListTable @source={{sortedShortList}} @sortProperty={{controller.sortProperty}} @sortDescending={{controller.sortDescending}} @class="is-striped" as |t|>
<t.head>
<t.sort-by @prop="name">Name</t.sort-by>
<t.sort-by @prop="lang">Language</t.sort-by>
</t.head>
<t.body @key="model.name" as |row|>
<tr>
<td>{{row.model.name}}</td>
<td>{{row.model.lang}}</td>
</tr>
<tr>
<td colspan="2">{{row.model.desc}}</td>
</tr>
</t.body>
</ListTable>
<p class="annotation">The list-table component attempts to be as flexible as possible. For this reason, <code>t.body</code> does not provide the typical <code>tr</code> element. It's sometimes desired to have multiple elements per record.</p>
`,
context: {
injectRoutedController: injectRoutedController(
Controller.extend({
queryParams: ['sortProperty', 'sortDescending'],
sortProperty: 'name',
sortDescending: false,
})
),
sortedShortList: computed('controller.sortProperty', 'controller.sortDescending', function() {
let sorted = productMetadata.sortBy(this.get('controller.sortProperty') || 'name');
return this.get('controller.sortDescending') ? sorted.reverse() : sorted;
}),
},
};
};
export let Pagination = () => {
return {
template: hbs`
<h5 class="title is-5">Table pagination</h5>
<ListPagination @source={{longList}} @size={{5}} @page={{controller.currentPage}} as |p|>
<ListTable @source={{p.list}} @class="with-foot" as |t|>
<t.head>
<th class="is-1">Rank</th>
<th>City</th>
<th>State</th>
<th>Population</th>
<th>Growth</th>
</t.head>
<t.body @key="model.rank" as |row|>
<tr>
<td>{{row.model.rank}}</td>
<td>{{row.model.city}}</td>
<td>{{row.model.state}}</td>
<td>{{row.model.population}}</td>
<td>{{format-percentage row.model.growth total=1}}</td>
</tr>
</t.body>
</ListTable>
<div class="table-foot">
<nav class="pagination">
<span class="bumper-left">U.S. City population and growth from 2000-2013</span>
<div class="pagination-numbers">
{{p.startsAt}}&ndash;{{p.endsAt}} of {{longList.length}}
</div>
<p.prev @class="pagination-previous"> &lt; </p.prev>
<p.next @class="pagination-next"> &gt; </p.next>
<ul class="pagination-list"></ul>
</nav>
</div>
</ListPagination>
<p class="annotation">Pagination works like sorting: using <code>link-to</code>s to set a query param.</p>
<p class="annotation">Pagination, like Table, is a minimal design. Only a next and previous button are available. The current place in the set of pages is tracked by showing which slice of items is currently shown.</p>
<p class="annotation">The pagination component exposes first and last components (for jumping to the beginning and end of a list) as well as pageLinks for generating links around the current page.</p>
`,
context: {
injectRoutedController: injectRoutedController(
Controller.extend({
queryParams: ['currentPage'],
currentPage: 1,
})
),
longList,
},
};
};
export let RowLinks = () => {
return {
template: hbs`
<h5 class="title is-5">Table row links</h5>
<ListTable @source={{shortList}} as |t|>
<t.head>
<th>Name</th>
<th>Language</th>
<th>Description</th>
</t.head>
<t.body @key="model.name" as |row|>
<tr class="is-interactive">
<td><a href="javascript:;" class="is-primary">{{row.model.name}}</a></td>
<td>{{row.model.lang}}</td>
<td>{{row.model.desc}}</td>
</tr>
</t.body>
</ListTable>
<p class="annotation">It is common for tables to act as lists of links, (e.g., clients list all allocations, each row links to the allocation detail). The helper class <code>is-interactive</code> on the <code>tr</code> makes table rows have a pointer cursor. The helper class <code>is-primary</code> on the <code>a</code> element in a table row makes the link bold and black instead of blue. This makes the link stand out less, since the entire row is a link.</p>
<p class="annotation">
A few rules for using table row links:
<ol>
<li>The <code>is-primary</code> cell should always be the first cell</li>
<li>The <code>is-primary</code> cell should always contain a link to the destination in the form of an <code>a</code> element. This is to support opening a link in a new tab.</li>
<li>The full row should transition to the destination on click. This is to improve the usability of a table by creating a larger click area.</li>
</ol>
</p>
`,
context: {
shortList: productMetadata,
},
};
};
export let CellLinks = () => {
return {
template: hbs`
<h5 class="title is-5">Table cell links</h5>
<ListTable @source={{shortList}} as |t|>
<t.head>
<th>Name</th>
<th>Language</th>
<th>Description</th>
</t.head>
<t.body @key="model.name" as |row|>
<tr>
<td><a href={{row.model.link}} target="_parent">{{row.model.name}}</a></td>
<td>{{row.model.lang}}</td>
<td>{{row.model.desc}}</td>
</tr>
</t.body>
</ListTable>
<p class="annotation">Links in table cells are just links.</p>
`,
context: {
shortList: productMetadata,
},
};
};
export let CellDecorations = () => {
return {
template: hbs`
<h5 class="title is-5">Table cell decorations</h5>
<ListTable @source={{shortList}} as |t|>
<t.head>
<th>Name</th>
<th>Language</th>
<th>Description</th>
</t.head>
<t.body @key="model.name" as |row|>
<tr>
<td><a href={{row.model.link}}>{{row.model.name}}</a></td>
<td class="nowrap">
<span class="color-swatch
{{if (eq row.model.lang "ruby") "swatch-6"}}
{{if (eq row.model.lang "golang") "swatch-5"}}" />
{{row.model.lang}}
</td>
<td>{{row.model.desc}}</td>
</tr>
</t.body>
</ListTable>
<p class="annotation">Small icons and accents of color make tables easier to scan.</p>
`,
context: {
shortList: productMetadata,
},
};
};
export let CellIcons = () => {
return {
template: hbs`
<h5 class="title is-5">Table cell icons</h5>
<ListPagination @source={{longList}} @size={{5}} @page={{controller.currentPage}} as |p|>
<ListTable @source={{p.list}} @class="with-foot" as |t|>
<t.head>
<th class="is-narrow"></th>
<th class="is-1">Rank</th>
<th>City</th>
<th>State</th>
<th>Population</th>
<th>Growth</th>
</t.head>
<t.body @key="model.rank" as |row|>
<tr>
<td class="is-narrow">
{{#if (lt row.model.growth 0)}}
{{x-icon "warning" class="is-warning"}}
{{/if}}
</td>
<td>{{row.model.rank}}</td>
<td>{{row.model.city}}</td>
<td>{{row.model.state}}</td>
<td>{{row.model.population}}</td>
<td>{{format-percentage row.model.growth total=1}}</td>
</tr>
</t.body>
</ListTable>
<div class="table-foot">
<nav class="pagination">
<span class="bumper-left">U.S. City population and growth from 2000-2013. Cities with negative growth denoted.</span>
<div class="pagination-numbers">
{{p.startsAt}}&ndash;{{p.endsAt}} of {{longList.length}}
</div>
<p.prev @class="pagination-previous"> &lt; </p.prev>
<p.next @class="pagination-next"> &gt; </p.next>
<ul class="pagination-list"></ul>
</nav>
</div>
</ListPagination>
`,
context: {
injectRoutedController: injectRoutedController(
Controller.extend({
queryParams: ['currentPage'],
currentPage: 1,
})
),
longList,
},
};
};