feat: 🎸 add key/value list component

This commit is contained in:
Randall Morey committed 2020-08-07 14:35:00 -04:00
1 parent e3f7f5cebb
commit 13f76659e4
15 files changed
+269 -23

No files matched your search

@@ -0,0 +1,7 @@
import Component from '@ember/component';
import layout from '../../../templates/components/rose/list/key-value';
export default Component.extend({
layout,
tagName: ''
});
@@ -0,0 +1,103 @@
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
import { hbs } from 'ember-cli-htmlbars';
import { action } from '@storybook/addon-actions';
<Meta title="Rose|List|KeyValue" component="List/KeyValue" />
# Key/Value List
A simple key/value list with support for embedded form fields and buttons.
<Preview>
<Story name="Basic Key/Value List" height="17rem">{{
template: hbs`
<Rose::List::KeyValue as |list|>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<Rose::Form::Input @contextual={{true}} as |input|>
<list.item as |item|>
<item.key>
<input.label>Key</input.label>
</item.key>
<item.cell>
<input.field />
</item.cell>
<item.cell>
<Rose::Button @style="secondary">Add</Rose::Button>
</item.cell>
</list.item>
</Rose::Form::Input>
<Rose::Form::Input @contextual={{true}} as |input|>
<list.item as |item|>
<item.key>
<input.label>Key</input.label>
</item.key>
<item.cell>
<input.field />
</item.cell>
<item.cell>
<Rose::Button @style="secondary" @iconOnly="trash" title="Remove">Remove</Rose::Button>
</item.cell>
</list.item>
</Rose::Form::Input>
</Rose::List::KeyValue>
`,
}}</Story>
</Preview>
```html
<Rose::List::KeyValue as |list|>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
{{!-- In actual use, form fields should be yielded from an outer form component --}}
<Rose::Form::Input @contextual={{true}} as |input|>
<list.item as |item|>
<item.key>
<input.label>Key</input.label>
</item.key>
<item.cell>
<input.field />
</item.cell>
<item.cell>
<Rose::Button @style="secondary">Add</Rose::Button>
</item.cell>
</list.item>
</Rose::Form::Input>
<Rose::Form::Input @contextual={{true}} as |input|>
<list.item as |item|>
<item.key>
<input.label>Key</input.label>
</item.key>
<item.cell>
<input.field />
</item.cell>
<item.cell>
<Rose::Button @style="secondary" @iconOnly="trash" title="Remove">Remove</Rose::Button>
</item.cell>
</list.item>
</Rose::Form::Input>
</Rose::List::KeyValue>
```
@@ -0,0 +1,8 @@
import Component from '@ember/component';
import layout from '../../../../templates/components/rose/list/key-value/item';
import ComponentAutoId from '../../../../mixins/component-auto-id';
export default Component.extend(ComponentAutoId, {
layout,
tagName: ''
});
@@ -4,6 +4,7 @@
@import 'header';
@import 'layout';
@import 'icon';
@import 'list';
@import 'notification';
@import 'card';
@import 'cards';
@@ -9,6 +9,7 @@
@include type.type(m);
display: inline-block;
box-sizing: border-box;
width: 100%;
margin-bottom: sizing.rems(xs);
padding: sizing.rems(xs);
color: var(--stark);
@@ -13,6 +13,7 @@
vertical-align: middle;
display: inline-block;
box-sizing: border-box;
width: 100%;
margin-bottom: sizing.rems(xs);
padding: sizing.rems(xs) sizing.rems(xxl) sizing.rems(xs) sizing.rems(xs);
border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2);
@@ -0,0 +1 @@
@import 'key-value';
@@ -0,0 +1,45 @@
@use '../../variables/sizing';
@use '../../variables/typography';
@use '../../utilities/type';
.rose-list-key-value-item {
display: flex;
border-bottom: 1px solid var(--ui-gray-subtler-3);
@include type.type(s);
$lineHeight: 36 / 14;
line-height: $lineHeight;
padding: (sizing.rems(s) / 2) 0;
.rose-list-key-value-item-cell {
display: flex;
align-items: center;
margin-right: sizing.rems(m);
// Eliminate top/bottom margin from children
> * {
margin-bottom: 0;
margin-top: 0;
}
&:last-child {
&,
& > * {
margin-right: 0;
}
}
// First cell (key) is approximately 25%
&:nth-child(1) {
width: 25%;
font-weight: typography.font-weight(semibold);
}
// Second cell (value) is approximately 75%
&:nth-child(2) {
flex-grow: 1;
}
// Remaining cells will take up as little space as necessary to
// suit their content.
}
}
@@ -0,0 +1,5 @@
<ul class="rose-list-key-value">
{{yield (hash
item=(component 'rose/list/key-value/item')
)}}
</ul>
@@ -0,0 +1,13 @@
<li class="rose-list-key-value-item" aria-labelledby={{concat "item-key-" this.id}}>
{{yield (hash
key=(component 'rose/anonymous'
id=(concat "item-key-" this.id)
tagName='div'
classNames='rose-list-key-value-item-cell'
)
cell=(component 'rose/anonymous'
tagName='div'
classNames='rose-list-key-value-item-cell'
)
)}}
</li>
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/list/key-value';
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/list/key-value/item';
@@ -1,21 +1,6 @@
<Rose::Badge>Default</Rose::Badge>
<Rose::Badge @style="outline">Default Outline</Rose::Badge>
<Rose::Badge @icon="user-organization">Default with Icon</Rose::Badge>
<Rose::Badge @style="light">Light</Rose::Badge>
<Rose::Badge @style="dark">Dark</Rose::Badge>
<Rose::Badge @style="informational">Informational</Rose::Badge>
<Rose::Badge @style="informational-outline">Informational Outline</Rose::Badge>
<Rose::Badge @style="alert">Alert</Rose::Badge>
<Rose::Badge @style="alert-outline">Alert Outline</Rose::Badge>
<Rose::Badge @style="error">Error</Rose::Badge>
<Rose::Badge @style="error-outline">Error Outline</Rose::Badge>
<Rose::Badge @style="success">Success</Rose::Badge>
<Rose::Badge @style="success-outline">Success Outline</Rose::Badge>
<Rose::Badge @style="warning">Warning</Rose::Badge>
<Rose::Badge @style="warning-outline">Warning Outline</Rose::Badge>
<Rose::List::KeyValue as |list|>
<list.item as |item|>
<item.cell>Key</item.cell>
<item.cell>Value</item.cell>
</list.item>
</Rose::List::KeyValue>
@@ -33,7 +33,7 @@
<layout.body>
<Rose::Layout::Sidebar as |layout|>
<layout.sidebar>
<Rose::Nav::Sidebar @title="Title" as |nav|>
<nav.link @route="index">Item Name</nav.link>
@@ -44,7 +44,51 @@
</layout.sidebar>
<layout.body @tagName="main">
Body content
<Rose::List::KeyValue as |list|>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<Rose::Form::Input @contextual={{true}} as |input|>
<list.item as |item|>
<item.key>
<input.label>Key</input.label>
</item.key>
<item.cell>
<input.field />
</item.cell>
<item.cell>
<Rose::Button @style="secondary">Add</Rose::Button>
</item.cell>
</list.item>
</Rose::Form::Input>
<Rose::Form::Input @contextual={{true}} as |input|>
<list.item as |item|>
<item.key>
<input.label>Key</input.label>
</item.key>
<item.cell>
<input.field />
</item.cell>
<item.cell>
<Rose::Button @style="secondary" @iconOnly="trash" title="Remove">Remove</Rose::Button>
</item.cell>
</list.item>
</Rose::Form::Input>
</Rose::List::KeyValue>
</layout.body>
</Rose::Layout::Sidebar>
@@ -0,0 +1,30 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find, findAll } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | rose/list/key-value', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
assert.expect(2);
await render(hbs`
<Rose::List::KeyValue as |list|>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
<list.item as |item|>
<item.key>Key</item.key>
<item.cell>Value</item.cell>
</list.item>
</Rose::List::KeyValue>
`);
const itemLabelledBy = find('.rose-list-key-value-item:first-child').getAttribute('aria-labelledby');
assert.equal(findAll('.rose-list-key-value-item').length, 2, 'it has two items');
assert.ok(
find(`.rose-list-key-value-item:first-child > .rose-list-key-value-item-cell:first-child#${itemLabelledBy}`),
'The key cell has an ID associated with the `aria-labelledby` attribute of its parent item element'
);
});
});