From 20475705a54d516b8080375b716a3abc60c70278 Mon Sep 17 00:00:00 2001 From: Jesse Jordan Date: Tue, 25 Jul 2023 12:22:51 +0200 Subject: [PATCH 1/4] feat(mox:select): add light mode to mox/select components --- addon/components/mox/select.hbs | 18 +- addon/components/mox/select.js | 20 + addon/components/mox/select/option.hbs | 18 +- .../components/mxa/select/option/category.hbs | 2 +- package.json | 1 + stories/mox-select-light.stories.js | 220 +++++++++ stories/mox-select.stories.js | 24 +- .../integration/components/mox/select-test.js | 460 +++++++++++++----- 8 files changed, 615 insertions(+), 148 deletions(-) create mode 100644 stories/mox-select-light.stories.js diff --git a/addon/components/mox/select.hbs b/addon/components/mox/select.hbs index cf24e98c..1ae742bf 100644 --- a/addon/components/mox/select.hbs +++ b/addon/components/mox/select.hbs @@ -5,12 +5,10 @@ disabled={{@isDisabled}} aria-haspopup="listbox" aria-expanded="true" - aria-labelledby="listbox-label" class="relative w-full border rounded shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none sm:text-sm focus:ring-1 bg-gray-800 text-white - disabled:bg-gray-700 disabled:text-gray-500 disabled:border-gray-700 disabled:cursor-not-allowed - {{if this.isValid - 'border-gray-500 focus:border-cyan-500 focus:ring-cyan-500' - 'border-red-800 active:border-red-900 focus:border-red-900 focus:ring-red-900' - }}" + aria-label={{@label}} + class="relative w-full border rounded shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none sm:text-sm focus:ring-1 disabled:cursor-not-allowed + {{this.themeClasses}} + {{if this.isValid this.validThemeClasses this.invalidThemeClasses}}" data-test-select-button ...attributes {{on "click" this.toggleOptions}} @@ -38,14 +36,14 @@ {{#if this.isShowingOptions}}
- + \ No newline at end of file diff --git a/addon/components/mox/select.js b/addon/components/mox/select.js index 18902160..c2dc4d5c 100644 --- a/addon/components/mox/select.js +++ b/addon/components/mox/select.js @@ -28,6 +28,26 @@ export default class MoxSelectComponent extends Component { return get(this.args.selectedOption, optionNameKey); } + get themeClasses() { + let lightMode = `bg-gray-50 text-gray-800 disabled:bg-gray-200 disabled:text-gray-600 disabled:border-gray-200`; + let darkMode = `dark:bg-gray-800 dark:text-white dark:disabled:bg-gray-700 dark:disabled:text-gray-500 dark:disabled:border-gray-700`; + return `${lightMode} ${darkMode}`; + } + + get validThemeClasses() { + let lightMode = `border-gray-300 focus:border-cyan-500 focus:ring-cyan-500`; + let darkMode = + 'dark:border-gray-500 dark:focus:border-cyan-500 dark:focus:ring-cyan-500'; + return `${lightMode} ${darkMode}`; + } + + get invalidThemeClasses() { + let lightMode = `border-red-600 active:border-red-700 focus:border-red-500 focus:ring-red-500`; + let darkMode = + 'dark:border-red-800 dark:active:border-red-900 dark:focus:border-red-900 dark:focus:ring-red-900'; + return `${lightMode} ${darkMode}`; + } + @action toggleOptions() { if (this.args.isDisabled) { diff --git a/addon/components/mox/select/option.hbs b/addon/components/mox/select/option.hbs index c43b65cc..2859dbba 100644 --- a/addon/components/mox/select/option.hbs +++ b/addon/components/mox/select/option.hbs @@ -1,13 +1,17 @@ -{{!-- template-lint-disable require-presentational-children --}} +{{! template-lint-disable require-presentational-children }}
  • - {{!-- template-lint-disable no-invalid-interactive --}} + {{! template-lint-disable no-invalid-interactive }}
    {{/if}}
    - {{!-- template-lint-enable no-invalid-interactive --}} + {{! template-lint-enable no-invalid-interactive }} {{#if (eq this.optionValue this.selectedOptionValue)}} {{/if}}
  • -{{!-- template-lint-enable require-presentational-children --}} +{{! template-lint-enable require-presentational-children }} \ No newline at end of file diff --git a/addon/components/mxa/select/option/category.hbs b/addon/components/mxa/select/option/category.hbs index ffb59058..c7c764a1 100644 --- a/addon/components/mxa/select/option/category.hbs +++ b/addon/components/mxa/select/option/category.hbs @@ -1,4 +1,4 @@
    {{@name}} - {{@category}} + {{@category}}
    diff --git a/package.json b/package.json index ed03774f..7370599e 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "ember-concurrency": "^3.0.0", "ember-keyboard": "8.2.0", "ember-load-initializers": "^2.1.2", + "ember-modifier": "^4.1.0", "ember-page-title": "^7.0.0", "ember-qunit": "^6.2.0", "ember-resolver": "^10.1.0", diff --git a/stories/mox-select-light.stories.js b/stories/mox-select-light.stories.js new file mode 100644 index 00000000..640e198b --- /dev/null +++ b/stories/mox-select-light.stories.js @@ -0,0 +1,220 @@ +import { hbs } from 'ember-cli-htmlbars'; +import { action } from '@ember/object'; + +const connectorTypes = [ + { name: 'source', value: 'source' }, + { name: 'sink', value: 'sink' }, +]; + +const connectorTypesCustom = [ + { name: 'My environment', value: 'my-environment', type: 'Self-Hosted' }, + { name: 'Meroxa environment', value: 'meroxa-environment', type: 'Private' }, + { name: 'Common', value: 'common', type: 'Common' }, +]; + +const connectorTypesDisabled = [ + { name: 'My environment', value: 'my-environment', isDisabled: false }, + { name: 'Meroxa environment', value: 'meroxa-environment', isDisabled: true }, + { name: 'Common', value: 'common', isDisabled: false }, +]; + +const resourceTypes = [ + { name: 'Postgres', value: 'postgres' }, + { name: 'MongoDB', value: 'mongodb' }, +]; + +export default { + title: 'Mox Light/Mox::Select', + parameters: { + backgrounds: { + default: 'Mute', + values: [ + { + name: 'White', + value: '#ffffff', + }, + { + name: 'Mute', + value: '#F3F4F6', + }, + ], + }, + }, + argTypes: { + options: { control: 'text' }, + buttonType: { control: 'text' }, + }, +}; + +const Template = (args) => ({ + template: hbs` +
    + +
    +`, + context: args, +}); + +const CustomOptionsTemplate = (args) => ({ + template: hbs` +
    + + + {{#each this.connectorTypes as |connector|}} + + {{/each}} + +
    +`, + context: args, +}); + +const CustomOptionsBlockTemplate = (args) => ({ + template: hbs` +
    + + + {{#each this.resourceTypes as |resource|}} + + + + {{/each}} + +
    +`, + context: args, +}); + +const DisabledOptionsTemplate = (args) => ({ + template: hbs` +
    + + {{#each this.connectorTypes as |connector|}} + + {{/each}} + +
    +`, + context: args, +}); + +export const Default = Template.bind({}); +Default.args = { + connectorTypes: connectorTypes, + selectedConnectorType: connectorTypes[0], + isEditing: false, + setConnectorType: action(function (value) { + this.set('selectedConnectorType', value); + }), + wrapperClass: null, +}; + +export const Short = Template.bind({}); +Short.args = { + connectorTypes: connectorTypes, + selectedConnectorType: connectorTypes[0], + isEditing: false, + setConnectorType: action(function (value) { + this.set('selectedConnectorType', value); + }), + wrapperClass: 'w-20', +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + connectorTypes: connectorTypes, + selectedConnectorType: connectorTypes[0], + isEditing: false, + setConnectorType: action(function (value) { + this.set('selectedConnectorType', value); + }), + wrapperClass: null, + isDisabled: true, +}; + +export const CustomOptions = CustomOptionsTemplate.bind({}); +CustomOptions.args = { + connectorTypes: connectorTypesCustom, + selectedConnectorType: connectorTypesCustom[0], + isEditing: false, + setConnectorType: action(function (value) { + this.set('selectedConnectorType', value); + }), + wrapperClass: 'w-48', + categoryKey: 'type', +}; + +export const IconOptions = CustomOptionsBlockTemplate.bind({}); +IconOptions.args = { + resourceTypes: resourceTypes, + selectedResourceType: resourceTypes[0], + isEditing: false, + setResourceType: action(function (value) { + this.set('selectedResourceType', value); + }), + wrapperClass: 'w-48', +}; + +export const DisabledOptions = DisabledOptionsTemplate.bind({}); +DisabledOptions.args = { + connectorTypes: connectorTypesDisabled, + selectedConnectorType: connectorTypesDisabled[0], + isEditing: false, + setConnectorType: action(function (value) { + this.set('selectedConnectorType', value); + }), + wrapperClass: 'w-48', +}; + +export const Errors = Template.bind({}); +Errors.args = { + connectorTypes: connectorTypes, + selectedConnectorType: connectorTypes[0], + isEditing: false, + setConnectorType: action(function (value) { + this.set('selectedConnectorType', value); + }), + wrapperClass: null, + isValid: false, + error: 'Invalid connector', + inputAction: () => {}, +}; diff --git a/stories/mox-select.stories.js b/stories/mox-select.stories.js index a27bafc6..d2b6479f 100644 --- a/stories/mox-select.stories.js +++ b/stories/mox-select.stories.js @@ -28,6 +28,16 @@ export default { parameters: { backgrounds: { default: 'Dark', + values: [ + { + name: 'Dark', + value: '#111827', + }, + { + name: 'Sky', + value: '#06B6D4', + }, + ], }, }, argTypes: { @@ -135,7 +145,7 @@ Default.args = { setConnectorType: action(function (value) { this.set('selectedConnectorType', value); }), - wrapperClass: null, + wrapperClass: 'dark', }; export const Short = Template.bind({}); @@ -146,7 +156,7 @@ Short.args = { setConnectorType: action(function (value) { this.set('selectedConnectorType', value); }), - wrapperClass: 'w-20', + wrapperClass: 'dark w-20', }; export const Disabled = Template.bind({}); @@ -157,7 +167,7 @@ Disabled.args = { setConnectorType: action(function (value) { this.set('selectedConnectorType', value); }), - wrapperClass: null, + wrapperClass: 'dark', isDisabled: true, }; @@ -169,7 +179,7 @@ CustomOptions.args = { setConnectorType: action(function (value) { this.set('selectedConnectorType', value); }), - wrapperClass: 'w-48', + wrapperClass: 'dark w-48', categoryKey: 'type', }; @@ -181,7 +191,7 @@ IconOptions.args = { setResourceType: action(function (value) { this.set('selectedResourceType', value); }), - wrapperClass: 'w-48', + wrapperClass: 'dark w-48', }; export const DisabledOptions = DisabledOptionsTemplate.bind({}); @@ -192,7 +202,7 @@ DisabledOptions.args = { setConnectorType: action(function (value) { this.set('selectedConnectorType', value); }), - wrapperClass: 'w-48', + wrapperClass: 'dark w-48', }; export const Errors = Template.bind({}); @@ -203,7 +213,7 @@ Errors.args = { setConnectorType: action(function (value) { this.set('selectedConnectorType', value); }), - wrapperClass: null, + wrapperClass: 'dark', isValid: false, error: 'Invalid connector', inputAction: () => {}, diff --git a/tests/integration/components/mox/select-test.js b/tests/integration/components/mox/select-test.js index 07f67d1a..a1ab4615 100644 --- a/tests/integration/components/mox/select-test.js +++ b/tests/integration/components/mox/select-test.js @@ -51,114 +51,6 @@ module('Integration | Component | mox/select', function (hooks) { assert.dom('[data-test-select-button]').isDisabled(); }); - test('it highlights the field if it is invalid (dark mode)', async function (assert) { - this.set('onInput', () => {}); - - await render(hbs` -
    - -
    `); - - assert.dom('[data-test-select-button]').hasClass('border-red-800'); - assert.dom('[data-test-select-button]').hasStyle({ - borderColor: 'rgb(153, 27, 27)', - }); - }); - - test('it allows to validate and invalidate the field after rendering (dark mode)', async function (assert) { - this.set('onInput', () => {}); - this.set('isValid', null); - - await render(hbs` -
    - -
    `); - - assert.dom('[data-test-select-button]').doesNotHaveClass('border-red-800'); - assert.dom('[data-test-select-button]').hasStyle({ - borderColor: 'rgb(107, 114, 128)', - }); - - this.set('isValid', false); - - assert.dom('[data-test-select-button]').hasClass('border-red-800'); - assert.dom('[data-test-select-button]').hasStyle({ - borderColor: 'rgb(153, 27, 27)', - }); - - this.set('isValid', true); - - assert.dom('[data-test-select-button]').doesNotHaveClass('border-red-800'); - assert.dom('[data-test-select-button]').hasStyle({ - borderColor: 'rgb(107, 114, 128)', - }); - }); - - test('it may display a validation error alongside the field', async function (assert) { - this.set('onInput', () => {}); - - await render(hbs``); - - assert - .dom('[data-test-mox-select-error]') - .includesText(`Connector missing`); - }); - - test('it is accessible (dark mode)', async function (assert) { - await render(hbs` -
    - -
    `); - - await a11yAudit(); - assert.ok(true, 'no a11y detected'); - }); - - test('the invalid input state is accessible', async function (assert) { - this.set('onInput', () => {}); - - await render(hbs`
    - -
    `); - await a11yAudit(); - assert.ok(true, 'no accessibility errors'); - }); - module('when toggled', function (hooks) { hooks.beforeEach(async function () { await render(hbs` @@ -267,6 +159,56 @@ module('Integration | Component | mox/select', function (hooks) { .includesText('Self-Hosted'); }); + test('it is accessible with custom options (dark mode)', async function (assert) { + await render(hbs` +
    + + {{#each this.options as |option|}} + + {{/each}} + +
    `); + + await click('[data-test-select-button]'); + + await a11yAudit(); + assert.ok(true, 'it has no accessibility errors'); + }); + + test('it is accessible with custom options (light mode)', async function (assert) { + await render(hbs` +
    + + {{#each this.options as |option|}} + + {{/each}} + +
    `); + + await click('[data-test-select-button]'); + + await a11yAudit(); + assert.ok(true, 'it has no accessibility errors'); + }); + test('it allows selecting customized options', async function (assert) { await render(hbs` + - {{#each this.options as |option|}} - - {{/each}} - `); + {{#each this.options as |option|}} + + {{/each}} + + `); + + await click('[data-test-select-button]'); + assert.dom('[data-test-select-option]').exists({ count: 3 }); + const options = this.element.querySelectorAll( + '[data-test-select-option]' + ); + + assert.dom(options[0]).includesText('My environment'); + assert.dom(options[0]).hasClass('dark:text-white'); + + assert.dom(options[2]).includesText('Common'); + assert.dom(options[2]).hasClass('dark:text-white'); + + assert.dom(options[1]).includesText('Meroxa environment'); + assert.dom(options[1]).hasClass('dark:text-gray-500'); + assert.dom(options[1]).hasClass('dark:bg-gray-700'); + }); + + test('it renders disabled options (light mode)', async function (assert) { + await render(hbs` +
    + + {{#each this.options as |option|}} + + {{/each}} + +
    `); await click('[data-test-select-button]'); assert.dom('[data-test-select-option]').exists({ count: 3 }); @@ -338,14 +318,14 @@ module('Integration | Component | mox/select', function (hooks) { ); assert.dom(options[0]).includesText('My environment'); - assert.dom(options[0]).hasClass('text-white'); + assert.dom(options[0]).hasClass('text-gray-800'); assert.dom(options[2]).includesText('Common'); - assert.dom(options[2]).hasClass('text-white'); + assert.dom(options[2]).hasClass('text-gray-800'); assert.dom(options[1]).includesText('Meroxa environment'); - assert.dom(options[1]).hasClass('text-gray-500'); - assert.dom(options[1]).hasClass('bg-gray-700'); + assert.dom(options[1]).hasClass('text-gray-600'); + assert.dom(options[1]).hasClass('bg-gray-200'); }); test('it does not allow selecting disabled options', async function (assert) { @@ -396,4 +376,238 @@ module('Integration | Component | mox/select', function (hooks) { ); }); }); + + module('dark mode', function () { + test('it highlights the field if it is invalid (dark mode)', async function (assert) { + this.set('onInput', () => {}); + + await render(hbs` +
    + +
    `); + + assert.dom('[data-test-select-button]').hasClass('dark:border-red-800'); + assert.dom('[data-test-select-button]').hasStyle({ + borderColor: 'rgb(153, 27, 27)', + }); + }); + + test('it allows to validate and invalidate the field after rendering (dark mode)', async function (assert) { + this.set('onInput', () => {}); + this.set('isValid', null); + + await render(hbs` +
    + +
    `); + + assert + .dom('[data-test-select-button]') + .doesNotHaveClass('dark:border-red-800'); + assert.dom('[data-test-select-button]').hasStyle({ + borderColor: 'rgb(107, 114, 128)', + }); + + this.set('isValid', false); + + assert.dom('[data-test-select-button]').hasClass('dark:border-red-800'); + assert.dom('[data-test-select-button]').hasStyle({ + borderColor: 'rgb(153, 27, 27)', + }); + + this.set('isValid', true); + + assert + .dom('[data-test-select-button]') + .doesNotHaveClass('dark:border-red-800'); + assert.dom('[data-test-select-button]').hasStyle({ + borderColor: 'rgb(107, 114, 128)', + }); + }); + + test('it may display a validation error alongside the field', async function (assert) { + this.set('onInput', () => {}); + + await render(hbs` +
    + +
    `); + + assert + .dom('[data-test-mox-select-error]') + .includesText(`Connector missing`); + }); + + test('it is accessible (dark mode)', async function (assert) { + await render(hbs` +
    + +
    `); + + await a11yAudit(); + assert.ok(true, 'no a11y detected'); + }); + + test('the invalid input state is accessible', async function (assert) { + this.set('onInput', () => {}); + + await render(hbs`
    + +
    `); + await a11yAudit(); + assert.ok(true, 'no accessibility errors'); + }); + }); + + module('light mode', function () { + test('it highlights the field if it is invalid (light mode)', async function (assert) { + this.set('onInput', () => {}); + + await render(hbs` +
    + +
    `); + + assert.dom('[data-test-select-button]').hasClass('dark:border-red-800'); + assert.dom('[data-test-select-button]').hasStyle({ + borderColor: 'rgb(220, 38, 38)', + }); + }); + + test('it allows to validate and invalidate the field after rendering (light mode)', async function (assert) { + this.set('onInput', () => {}); + this.set('isValid', null); + + await render(hbs` +
    + +
    `); + + assert + .dom('[data-test-select-button]') + .doesNotHaveClass('dark:border-red-800'); + assert.dom('[data-test-select-button]').hasStyle({ + borderColor: 'rgb(209, 213, 219)', + }); + + this.set('isValid', false); + + assert.dom('[data-test-select-button]').hasClass('dark:border-red-800'); + assert.dom('[data-test-select-button]').hasStyle({ + borderColor: 'rgb(220, 38, 38)', + }); + + this.set('isValid', true); + + assert + .dom('[data-test-select-button]') + .doesNotHaveClass('dark:border-red-800'); + assert.dom('[data-test-select-button]').hasStyle({ + borderColor: 'rgb(209, 213, 219)', + }); + }); + + test('it may display a validation error alongside the field', async function (assert) { + this.set('onInput', () => {}); + + await render(hbs` +
    + +
    `); + + assert + .dom('[data-test-mox-select-error]') + .includesText(`Connector missing`); + }); + + test('it is accessible (dark mode)', async function (assert) { + await render(hbs` +
    + +
    `); + + await a11yAudit(); + assert.ok(true, 'no a11y detected'); + }); + + test('the invalid input state is accessible', async function (assert) { + this.set('onInput', () => {}); + + await render(hbs`
    + +
    `); + await a11yAudit(); + assert.ok(true, 'no accessibility errors'); + }); + }); }); From da893be041cc0df24b9b00123c4125088c241117 Mon Sep 17 00:00:00 2001 From: Jesse Jordan Date: Tue, 25 Jul 2023 13:59:55 +0200 Subject: [PATCH 2/4] feat(mox:typeahead): add light mode to mox/t-ahead component --- addon/components/mox/input.hbs | 36 ++- addon/components/mox/search-input.hbs | 2 +- addon/components/mox/typeahead-select.hbs | 15 +- stories/mox-typeahead-select-light.stories.js | 129 ++++++++ stories/mox-typeahead-select.stories.js | 38 ++- .../components/mox/typeahead-select-test.js | 306 +++++++++++++----- 6 files changed, 402 insertions(+), 124 deletions(-) create mode 100644 stories/mox-typeahead-select-light.stories.js diff --git a/addon/components/mox/input.hbs b/addon/components/mox/input.hbs index 80cd491b..2065d493 100644 --- a/addon/components/mox/input.hbs +++ b/addon/components/mox/input.hbs @@ -1,20 +1,22 @@ {{#if @label}} {{@label}} {{/if}} - - +
    + + +
    diff --git a/addon/components/mox/search-input.hbs b/addon/components/mox/search-input.hbs index 75fccbdc..374d046b 100644 --- a/addon/components/mox/search-input.hbs +++ b/addon/components/mox/search-input.hbs @@ -1,7 +1,7 @@