diff --git a/addons/rose/addon/components/rose/form/input.js b/addons/rose/addon/components/rose/form/input.js new file mode 100644 index 000000000..dcd34f179 --- /dev/null +++ b/addons/rose/addon/components/rose/form/input.js @@ -0,0 +1,19 @@ +import Component from '@ember/component'; +import layout from '../../../templates/components/rose/form/input'; + +/** + * A simple HTML `` element. + * + * @example + * + */ +export default Component.extend({ + layout, +}); diff --git a/addons/rose/addon/components/rose/form/textarea.js b/addons/rose/addon/components/rose/form/textarea.js new file mode 100644 index 000000000..e8a8eb7a2 --- /dev/null +++ b/addons/rose/addon/components/rose/form/textarea.js @@ -0,0 +1,20 @@ +import Component from '@ember/component'; +import layout from '../../../templates/components/rose/form/textarea'; + +/** + * A `` element essentially identical to `Rose::Form::Input` except + * for the field element. + * + * @example + * + */ +export default Component.extend({ + layout, +}); diff --git a/addons/rose/addon/styles/rose/components/_button.scss b/addons/rose/addon/styles/rose/components/_button.scss index a043f2e62..a195dd275 100644 --- a/addons/rose/addon/styles/rose/components/_button.scss +++ b/addons/rose/addon/styles/rose/components/_button.scss @@ -2,10 +2,10 @@ @use '../utilities/type' as typography; @use '../variables/sizing'; -$size-xs: sizing.rems(xs); // 8 -$size-xxs: sizing.rems(xxs); // 4 -$size-xxxs: sizing.rems(xxxs); // 2 -$size-xxxxs: sizing.rems(xxxxs); // 1 +$size-xs: sizing.rems(xs); // 8 +$size-xxs: sizing.rems(xxs); // 4 +$size-xxxs: sizing.rems(xxxs); // 2 +$size-xxxxs: sizing.rems(xxxxs); // 1 @include button.button-style( 'primary', @@ -13,8 +13,10 @@ $size-xxxxs: sizing.rems(xxxxs); // 1 color: var(--white), background-color: var(--info), border-color: var(--info-starker-2), - box-shadow: 0 $size-xxs $size-xxs rgba(var(--stark-components), var(--opacity-2)), - text-shadow: 0 $size-xxxxs $size-xxxxs rgba(var(--black-components), var(--opacity-2)), + box-shadow: 0 $size-xxs $size-xxs + rgba(var(--stark-components), var(--opacity-2)), + text-shadow: 0 $size-xxxxs $size-xxxxs + rgba(var(--black-components), var(--opacity-2)), ), ( background-color: var(--info-subtler-1), @@ -39,7 +41,8 @@ $size-xxxxs: sizing.rems(xxxxs); // 1 color: var(--ui-gray-starker-3), background-color: var(--ui-gray-subtler-5), border-color: var(--ui-gray-subtler-2), - box-shadow: 0 $size-xxs $size-xxs rgba(var(--stark-components), var(--opacity-2)), + box-shadow: 0 $size-xxs $size-xxs + rgba(var(--stark-components), var(--opacity-2)), ), ( color: var(--ui-gray-starker-4), @@ -68,7 +71,8 @@ $size-xxxxs: sizing.rems(xxxxs); // 1 color: var(--failure), background-color: var(--subtle), border-color: var(--failure), - box-shadow: 0 $size-xxs $size-xxs rgba(var(--stark-components), var(--opacity-2)), + box-shadow: 0 $size-xxs $size-xxs + rgba(var(--stark-components), var(--opacity-2)), ), ( color: var(--white), @@ -78,7 +82,8 @@ $size-xxxxs: sizing.rems(xxxxs); // 1 0 $size-xxs $size-xxs rgba(var(--stark-components), var(--opacity-3)), 0 $size-xxs $size-xs rgba(var(--stark-components), var(--opacity-4)), ), - text-shadow: 0 $size-xxxxs $size-xxxxs rgba(var(--black-components), var(--opacity-2)), + text-shadow: 0 $size-xxxxs $size-xxxxs + rgba(var(--black-components), var(--opacity-2)), ), ( color: var(--white), @@ -88,7 +93,8 @@ $size-xxxxs: sizing.rems(xxxxs); // 1 0 $size-xxs $size-xxs rgba(var(--stark-components), var(--opacity-3)), 0 $size-xxs $size-xs rgba(var(--stark-components), var(--opacity-4)), ), - text-shadow: 0 $size-xxxxs $size-xxxxs rgba(var(--black-components), var(--opacity-2)), + text-shadow: 0 $size-xxxxs $size-xxxxs + rgba(var(--black-components), var(--opacity-2)), ), ( border-color: var(--ui-gray-subtler-2), diff --git a/addons/rose/addon/styles/rose/components/_index.scss b/addons/rose/addon/styles/rose/components/_index.scss index 2d4322481..dd972080b 100644 --- a/addons/rose/addon/styles/rose/components/_index.scss +++ b/addons/rose/addon/styles/rose/components/_index.scss @@ -1 +1,2 @@ @import 'button'; +@import 'form'; diff --git a/addons/rose/addon/styles/rose/components/form/_index.scss b/addons/rose/addon/styles/rose/components/form/_index.scss new file mode 100644 index 000000000..fdc60b1cd --- /dev/null +++ b/addons/rose/addon/styles/rose/components/form/_index.scss @@ -0,0 +1 @@ +@import 'input'; diff --git a/addons/rose/addon/styles/rose/components/form/_input.scss b/addons/rose/addon/styles/rose/components/form/_input.scss new file mode 100644 index 000000000..7116c6a76 --- /dev/null +++ b/addons/rose/addon/styles/rose/components/form/_input.scss @@ -0,0 +1,64 @@ +@use '../../variables/sizing'; +@use '../../utilities/type'; + +.rose-form-input { + margin-bottom: sizing.rems(m); + + .rose-form-input-label { + @include type.type(s, semibold); + display: block; + margin-bottom: sizing.rems(xxs); + } + + .rose-form-input-helper-text { + @include type.type(xs); + display: block; + color: var(--ui-gray); + margin-bottom: sizing.rems(xxs); + } + + .rose-form-input-field { + @include type.type(m); + padding: sizing.rems(xs); + color: var(--stark); + background-color: var(--subtle); + border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-2); + border-radius: sizing.rems(xxxs); + box-shadow: inset 0px 3px 1px + rgba(var(--stark-components), var(--opacity-4)); + + &:not(:disabled):not([readonly]) { + &:hover { + border-color: var(--ui-gray); + } + + &:focus { + border-color: var(--info); + } + } + + &:disabled { + cursor: not-allowed; + color: var(--ui-gray); + background-color: var(--ui-gray-subtler-6); + } + + &[readonly] { + border-color: transparent; + box-shadow: none; + } + } + + &.error { + color: var(--failure); + + .rose-form-input-helper-text { + color: currentColor; + } + + .rose-form-input-field { + color: var(--failure); + border-color: currentColor; + } + } +} diff --git a/addons/rose/addon/styles/rose/variables/_sizing.scss b/addons/rose/addon/styles/rose/variables/_sizing.scss index ac73d8fae..93ebe4da7 100644 --- a/addons/rose/addon/styles/rose/variables/_sizing.scss +++ b/addons/rose/addon/styles/rose/variables/_sizing.scss @@ -5,16 +5,25 @@ */ $size-coefficients: ( - xxl: 3, // 48 - xl: 2.25, // 36 - l: 1.5, // 24 - m: 1, // 16 - base - s: 0.75, // 12 - xs: 0.5, // 8 - off-xs:0.3125, // 5 - xxs: 0.25, // 4 - xxxs: 0.125, // 2 - xxxxs: 0.0625 // 1 + xxl: 3, + // 48 + xl: 2.25, + // 36 + l: 1.5, + // 24 + m: 1, + // 16 - base + s: 0.75, + // 12 + xs: 0.5, + // 8 + off-xs: 0.3125, + // 5 + xxs: 0.25, + // 4 + xxxs: 0.125, + // 2 + xxxxs: 0.0625 // 1,,, ); /** diff --git a/addons/rose/addon/styles/rose/variables/_typography.scss b/addons/rose/addon/styles/rose/variables/_typography.scss index e17769890..beaf59a5a 100644 --- a/addons/rose/addon/styles/rose/variables/_typography.scss +++ b/addons/rose/addon/styles/rose/variables/_typography.scss @@ -5,33 +5,18 @@ * SASS variables defining typography. */ -$family-primary: - system-ui, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - 'Roboto', - 'Oxygen', - 'Ubuntu', - 'Cantarell', - 'Fira Sans', - 'Droid Sans', - 'Helvetica Neue', - sans-serif; +$family-primary: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', + 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', + 'Helvetica Neue', sans-serif; -$family-monospace: - 'SFMono-Regular', - Consolas, - 'Liberation Mono', - Menlo, - Courier, +$family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; // Available font weights by name $font-weights: ( normal: 400, semibold: 600, - bold: 700 + bold: 700, ); // unitless value to start, but think pixels or points @@ -39,17 +24,23 @@ $base-font-size: 16; // Font sizes don't quite match up with the size scale, so they're recalculated. $font-sizes: ( - xl: sizing.size(m) * 1.75, // 28 - l: sizing.size(m) * 1.25, // 20 - m: sizing.size(m), // 16 - s: sizing.size(m) * 0.875, // 14 - xs: sizing.size(m) * 0.75, // 12 - - h1: sizing.size(m) * 1.75, // 28 - h2: sizing.size(m) * 1.25, // 20 - h3: sizing.size(m), // 16 - - button: sizing.size(m) * 0.8125 // 13 + xl: sizing.size(m) * 1.75, + // 28 + l: sizing.size(m) * 1.25, + // 20 + m: sizing.size(m), + // 16 + s: sizing.size(m) * 0.875, + // 14 + xs: sizing.size(m) * 0.75, + // 12 + h1: sizing.size(m) * 1.75, + // 28 + h2: sizing.size(m) * 1.25, + // 20 + h3: sizing.size(m), + // 16 + button: sizing.size(m) * 0.8125 // 13,,, ); // Line height expressed as ratio. @@ -59,12 +50,10 @@ $line-heights: ( m: 20 / 16, s: 18 / 14, xs: 16 / 12, - h1: 48 / 28, h2: 36 / 20, h3: 24 / 16, - - button: 24 / 13 + button: 24 / 13, ); /** diff --git a/addons/rose/addon/templates/components/rose/form/input.hbs b/addons/rose/addon/templates/components/rose/form/input.hbs new file mode 100644 index 000000000..dbca19a99 --- /dev/null +++ b/addons/rose/addon/templates/components/rose/form/input.hbs @@ -0,0 +1,19 @@ + + + + {{@label}} + + + {{#if @helperText}} + {{@helperText}} + {{/if}} + + + + {{yield}} + + diff --git a/addons/rose/addon/templates/components/rose/form/textarea.hbs b/addons/rose/addon/templates/components/rose/form/textarea.hbs new file mode 100644 index 000000000..3cb51c4ec --- /dev/null +++ b/addons/rose/addon/templates/components/rose/form/textarea.hbs @@ -0,0 +1,18 @@ + + + + {{@label}} + + + {{#if @helperText}} + {{@helperText}} + {{/if}} + + + + {{yield}} + + diff --git a/addons/rose/app/components/rose/form/input.js b/addons/rose/app/components/rose/form/input.js new file mode 100644 index 000000000..a2831b1f0 --- /dev/null +++ b/addons/rose/app/components/rose/form/input.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/form/input'; diff --git a/addons/rose/app/components/rose/form/textarea.js b/addons/rose/app/components/rose/form/textarea.js new file mode 100644 index 000000000..ac6c40308 --- /dev/null +++ b/addons/rose/app/components/rose/form/textarea.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/form/textarea'; diff --git a/addons/rose/app/templates/index.hbs b/addons/rose/app/templates/index.hbs index 64fb78428..45c285a23 100644 --- a/addons/rose/app/templates/index.hbs +++ b/addons/rose/app/templates/index.hbs @@ -9,3 +9,57 @@ Dummy app index template Secondary + + + + + + + + + + + + + + + + + + diff --git a/addons/rose/stories/form/input.stories.js b/addons/rose/stories/form/input.stories.js new file mode 100644 index 000000000..df2f09729 --- /dev/null +++ b/addons/rose/stories/form/input.stories.js @@ -0,0 +1,27 @@ +import { hbs } from 'ember-cli-htmlbars'; +import { text, boolean } from '@storybook/addon-knobs'; + +export default { + title: 'Rose::Form::Input', +}; + +export const Normal = () => ({ + template: hbs` + + `, + context: { + value: text('value', 'Value'), + label: text('label', 'Label'), + helperText: text('helperText', 'Helper text'), + disabled: boolean('disabled', false), + 'read-only': boolean('readonly', false), + error: boolean('error', false), + }, +}); diff --git a/addons/rose/stories/form/textarea.stories.js b/addons/rose/stories/form/textarea.stories.js new file mode 100644 index 000000000..f3ae74240 --- /dev/null +++ b/addons/rose/stories/form/textarea.stories.js @@ -0,0 +1,27 @@ +import { hbs } from 'ember-cli-htmlbars'; +import { text, boolean } from '@storybook/addon-knobs'; + +export default { + title: 'Rose::Form::Textarea', +}; + +export const Normal = () => ({ + template: hbs` + + `, + context: { + value: text('value', 'Value'), + label: text('label', 'Label'), + helperText: text('helperText', 'Helper text'), + disabled: boolean('disabled', false), + 'read-only': boolean('readonly', false), + error: boolean('error', false), + }, +}); diff --git a/addons/rose/stories/style-guide.stories.js b/addons/rose/stories/style-guide.stories.js index c7bfc8dd2..8400d9f10 100644 --- a/addons/rose/stories/style-guide.stories.js +++ b/addons/rose/stories/style-guide.stories.js @@ -326,18 +326,18 @@ border: sizing.rems(xxxxs) solid; `, context: { sizes: [ - {code: 'xxl', pixels: 48}, - {code: 'xl', pixels: 36}, - {code: 'l', pixels: 24}, - {code: 'm', pixels: 16, isBase: true}, - {code: 's', pixels: 12}, - {code: 'xs', pixels: 8}, - {code: 'off-xs', pixels: 5}, - {code: 'xxs', pixels: 4}, - {code: 'xxxs', pixels: 2}, - {code: 'xxxxs', pixels: 1}, - ] - } + { code: 'xxl', pixels: 48 }, + { code: 'xl', pixels: 36 }, + { code: 'l', pixels: 24 }, + { code: 'm', pixels: 16, isBase: true }, + { code: 's', pixels: 12 }, + { code: 'xs', pixels: 8 }, + { code: 'off-xs', pixels: 5 }, + { code: 'xxs', pixels: 4 }, + { code: 'xxxs', pixels: 2 }, + { code: 'xxxxs', pixels: 1 }, + ], + }, }); export const Typography = () => ({ @@ -424,15 +424,19 @@ export const Typography = () => ({ `, context: { sizes: [ - {code: 'xl', pixels: 28}, - {code: 'l', pixels: 20}, - {code: 'm', pixels: 16}, - {code: 's', pixels: 14}, - {code: 'xs', pixels: 12}, - {code: 'h1', pixels: 28, note: 'Same as xl, but different line height.'}, - {code: 'h2', pixels: 20, note: 'Same as l, but different line height.'}, - {code: 'h3', pixels: 16, note: 'Same as m, but different line height.'}, - {code: 'button', pixels: 13} - ] - } + { code: 'xl', pixels: 28 }, + { code: 'l', pixels: 20 }, + { code: 'm', pixels: 16 }, + { code: 's', pixels: 14 }, + { code: 'xs', pixels: 12 }, + { + code: 'h1', + pixels: 28, + note: 'Same as xl, but different line height.', + }, + { code: 'h2', pixels: 20, note: 'Same as l, but different line height.' }, + { code: 'h3', pixels: 16, note: 'Same as m, but different line height.' }, + { code: 'button', pixels: 13 }, + ], + }, }); diff --git a/addons/rose/tests/integration/components/rose/form/input-test.js b/addons/rose/tests/integration/components/rose/form/input-test.js new file mode 100644 index 000000000..85c78eeb7 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/form/input-test.js @@ -0,0 +1,47 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, find } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +module('Integration | Component | rose/form/input', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(hbs``); + assert.equal(await find('label').textContent.trim(), 'Label'); + assert.equal(await find('input').value, 'Text'); + }); + + test('it displays optional helper text', async function (assert) { + await render(hbs``); + assert.equal( + await find('.rose-form-input-helper-text').textContent.trim(), + 'Help me' + ); + }); + + test('it is type="text" by default', async function (assert) { + await render(hbs``); + assert.equal(await find('input').type, 'text'); + }); + + test('it supports a @type attribute', async function (assert) { + await render(hbs``); + assert.equal(await find('input').type, 'password'); + }); + + test('it supports disabled attribute', async function (assert) { + await render(hbs``); + assert.ok(await find('[disabled]')); + }); + + test('it supports readonly attribute', async function (assert) { + await render(hbs``); + assert.ok(await find('[readonly]')); + }); + + test('it gets an `error` class when @error=true', async function (assert) { + await render(hbs``); + assert.ok(await find('.error')); + }); +}); diff --git a/addons/rose/tests/integration/components/rose/form/textarea-test.js b/addons/rose/tests/integration/components/rose/form/textarea-test.js new file mode 100644 index 000000000..cce85cc32 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/form/textarea-test.js @@ -0,0 +1,37 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, find } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +module('Integration | Component | rose/form/textarea', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(hbs``); + assert.equal(await find('label').textContent.trim(), 'Label'); + assert.equal(await find('textarea').value, 'Text'); + }); + + test('it displays optional helper text', async function (assert) { + await render(hbs``); + assert.equal( + await find('.rose-form-input-helper-text').textContent.trim(), + 'Help me' + ); + }); + + test('it supports disabled attribute', async function (assert) { + await render(hbs``); + assert.ok(await find('[disabled]')); + }); + + test('it supports readonly attribute', async function (assert) { + await render(hbs``); + assert.ok(await find('[readonly]')); + }); + + test('it gets an `error` class when @error=true', async function (assert) { + await render(hbs``); + assert.ok(await find('.error')); + }); +});