DEV: <DSelect /> (#30224)
`<DSelect />` is a wrapper similar to our existing `<DButton />` over the html element `<select>`. The code is ported from form kit which is now directly using `<DSelect />`. Note this component has also been used in edit topic timer modal. This component is recommended for a small list of text items (no icons, no rich formatting...). Usage: ```gjs <DSelect class="my-select" @onChange={{this.handleChange}} as |select|> <select.Option @value="foo" class="my-favorite-option">Foo</select.Option> <select.Option @value="bar">Bar</select.Option> </DSelect> ``` This commit comes with a set of assertions: ```gjs import dselect from "discourse/tests/helpers/d-select-helper"; import { select } from "@ember/test-helpers"; assert .dselect(".my-select") .hasOption({ value: "bar", label: "Bar" }) .hasOption({ value: "foo", label: "Foo" }) .hasNoOption("baz"); await select(".my-select", "foo"); assert.dselect(".my-select").hasSelectedOption({value: "foo", label: "Foo"}); ```
This commit is contained in:
parent
622eb9e51c
commit
cbc0ece6e8
|
@ -0,0 +1,67 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { isNone } from "@ember/utils";
|
||||
import { eq } from "truth-helpers";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
export const NO_VALUE_OPTION = "__NONE__";
|
||||
|
||||
export class DSelectOption extends Component {
|
||||
get value() {
|
||||
return isNone(this.args.value) ? NO_VALUE_OPTION : this.args.value;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{! https://github.com/emberjs/ember.js/issues/19115 }}
|
||||
{{#if (eq @selected @value)}}
|
||||
<option
|
||||
class="d-select__option --selected"
|
||||
value={{this.value}}
|
||||
selected
|
||||
...attributes
|
||||
>
|
||||
{{yield}}
|
||||
</option>
|
||||
{{else}}
|
||||
<option class="d-select__option" value={{this.value}} ...attributes>
|
||||
{{yield}}
|
||||
</option>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
||||
|
||||
export default class DSelect extends Component {
|
||||
@action
|
||||
handleInput(event) {
|
||||
// if an option has no value, event.target.value will be the content of the option
|
||||
// this is why we use this magic value to represent no value
|
||||
this.args.onChange(
|
||||
event.target.value === NO_VALUE_OPTION ? undefined : event.target.value
|
||||
);
|
||||
}
|
||||
|
||||
get hasSelectedValue() {
|
||||
return this.args.value && this.args.value !== NO_VALUE_OPTION;
|
||||
}
|
||||
|
||||
<template>
|
||||
<select
|
||||
value={{@value}}
|
||||
...attributes
|
||||
class="d-select"
|
||||
{{on "input" this.handleInput}}
|
||||
>
|
||||
<DSelectOption @value={{NO_VALUE_OPTION}}>
|
||||
{{#if this.hasSelectedValue}}
|
||||
{{i18n "none_placeholder"}}
|
||||
{{else}}
|
||||
{{i18n "select_placeholder"}}
|
||||
{{/if}}
|
||||
</DSelectOption>
|
||||
|
||||
{{yield (hash Option=(component DSelectOption selected=@value))}}
|
||||
</select>
|
||||
</template>
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
<form>
|
||||
<div class="control-group">
|
||||
<ComboBox
|
||||
@onChange={{@onChangeStatusType}}
|
||||
@content={{@timerTypes}}
|
||||
<DSelect
|
||||
@value={{this.statusType}}
|
||||
class="timer-type"
|
||||
/>
|
||||
@onChange={{@onChangeStatusType}}
|
||||
as |select|
|
||||
>
|
||||
{{#each @timerTypes as |timer|}}
|
||||
<select.Option @value={{timer.id}}>{{timer.name}}</select.Option>
|
||||
{{/each}}
|
||||
</DSelect>
|
||||
</div>
|
||||
|
||||
{{#if this.publishToCategory}}
|
||||
|
|
|
@ -1,46 +1,29 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { NO_VALUE_OPTION } from "discourse/form-kit/lib/constants";
|
||||
import { i18n } from "discourse-i18n";
|
||||
import FKControlSelectOption from "./select/option";
|
||||
import DSelect, { DSelectOption } from "discourse/components/d-select";
|
||||
|
||||
const SelectOption = <template>
|
||||
<DSelectOption
|
||||
@value={{@value}}
|
||||
@selected={{@selected}}
|
||||
class="form-kit__control-option"
|
||||
>
|
||||
{{yield}}
|
||||
</DSelectOption>
|
||||
</template>;
|
||||
|
||||
export default class FKControlSelect extends Component {
|
||||
static controlType = "select";
|
||||
|
||||
@action
|
||||
handleInput(event) {
|
||||
// if an option has no value, event.target.value will be the content of the option
|
||||
// this is why we use this magic value to represent no value
|
||||
this.args.field.set(
|
||||
event.target.value === NO_VALUE_OPTION ? undefined : event.target.value
|
||||
);
|
||||
}
|
||||
|
||||
get hasSelectedValue() {
|
||||
return this.args.field.value && this.args.field.value !== NO_VALUE_OPTION;
|
||||
}
|
||||
|
||||
<template>
|
||||
<select
|
||||
value={{@field.value}}
|
||||
disabled={{@field.disabled}}
|
||||
...attributes
|
||||
<DSelect
|
||||
class="form-kit__control-select"
|
||||
{{on "input" this.handleInput}}
|
||||
disabled={{@field.disabled}}
|
||||
@value={{@field.value}}
|
||||
@onChange={{@field.set}}
|
||||
...attributes
|
||||
>
|
||||
<FKControlSelectOption @value={{NO_VALUE_OPTION}}>
|
||||
{{#if this.hasSelectedValue}}
|
||||
{{i18n "form_kit.select.none_placeholder"}}
|
||||
{{else}}
|
||||
{{i18n "form_kit.select.select_placeholder"}}
|
||||
{{/if}}
|
||||
</FKControlSelectOption>
|
||||
|
||||
{{yield
|
||||
(hash Option=(component FKControlSelectOption selected=@field.value))
|
||||
}}
|
||||
</select>
|
||||
{{yield (hash Option=(component SelectOption selected=@field.value))}}
|
||||
</DSelect>
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { isNone } from "@ember/utils";
|
||||
import { eq } from "truth-helpers";
|
||||
import { NO_VALUE_OPTION } from "discourse/form-kit/lib/constants";
|
||||
|
||||
export default class FKControlSelectOption extends Component {
|
||||
get value() {
|
||||
return isNone(this.args.value) ? NO_VALUE_OPTION : this.args.value;
|
||||
}
|
||||
|
||||
<template>
|
||||
{{! https://github.com/emberjs/ember.js/issues/19115 }}
|
||||
{{#if (eq @selected @value)}}
|
||||
<option
|
||||
class="form-kit__control-option --selected"
|
||||
value={{this.value}}
|
||||
selected
|
||||
...attributes
|
||||
>
|
||||
{{yield}}
|
||||
</option>
|
||||
{{else}}
|
||||
<option
|
||||
class="form-kit__control-option"
|
||||
value={{this.value}}
|
||||
...attributes
|
||||
>
|
||||
{{yield}}
|
||||
</option>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import { click, fillIn, select, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import topicFixtures from "discourse/tests/fixtures/topic";
|
||||
import {
|
||||
|
@ -48,7 +48,6 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
await visit("/t/internationalization-localization");
|
||||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
await click("#tap_tile_start_of_next_business_week");
|
||||
|
||||
assert
|
||||
|
@ -62,7 +61,6 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
await visit("/t/internationalization-localization");
|
||||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
await click("#tap_tile_start_of_next_business_week");
|
||||
|
||||
assert
|
||||
|
@ -76,13 +74,12 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
.dom(".edit-topic-timer-modal .topic-timer-info")
|
||||
.matchesText(/will automatically close in/);
|
||||
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
await timerType.expand();
|
||||
await timerType.selectRowByValue("close_after_last_post");
|
||||
await select(".timer-type", "close_after_last_post");
|
||||
|
||||
const interval = selectKit(".select-kit.relative-time-intervals");
|
||||
await interval.expand();
|
||||
await interval.selectRowByValue("hours");
|
||||
|
||||
assert.strictEqual(interval.header().label(), "hours");
|
||||
await fillIn(".relative-time-duration", "2");
|
||||
|
||||
|
@ -115,15 +112,11 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
|
||||
test("close temporarily", async function (assert) {
|
||||
updateCurrentUser({ moderator: true });
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
|
||||
await visit("/t/internationalization-localization");
|
||||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
await timerType.expand();
|
||||
await timerType.selectRowByValue("open");
|
||||
|
||||
await select(".timer-type", "open");
|
||||
await click("#tap_tile_start_of_next_business_week");
|
||||
|
||||
assert
|
||||
|
@ -140,15 +133,12 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
|
||||
test("schedule publish to category - visible for a PM", async function (assert) {
|
||||
updateCurrentUser({ moderator: true });
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
const categoryChooser = selectKit(".d-modal__body .category-chooser");
|
||||
|
||||
await visit("/t/pm-for-testing/12");
|
||||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
await timerType.expand();
|
||||
await timerType.selectRowByValue("publish_to_category");
|
||||
await select(".timer-type", "publish_to_category");
|
||||
const categoryChooser = selectKit(".d-modal__body .category-chooser");
|
||||
|
||||
assert.strictEqual(categoryChooser.header().label(), "category…");
|
||||
assert.strictEqual(categoryChooser.header().value(), null);
|
||||
|
@ -174,16 +164,13 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
|
||||
test("schedule publish to category - visible for a private category", async function (assert) {
|
||||
updateCurrentUser({ moderator: true });
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
const categoryChooser = selectKit(".d-modal__body .category-chooser");
|
||||
|
||||
// has private category id 24 (shared drafts)
|
||||
await visit("/t/some-topic/9");
|
||||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
await timerType.expand();
|
||||
await timerType.selectRowByValue("publish_to_category");
|
||||
await select(".timer-type", "publish_to_category");
|
||||
const categoryChooser = selectKit(".d-modal__body .category-chooser");
|
||||
|
||||
assert.strictEqual(categoryChooser.header().label(), "category…");
|
||||
assert.strictEqual(categoryChooser.header().value(), null);
|
||||
|
@ -209,8 +196,6 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
|
||||
test("schedule publish to category - visible for an unlisted public topic", async function (assert) {
|
||||
updateCurrentUser({ moderator: true });
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
const categoryChooser = selectKit(".d-modal__body .category-chooser");
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
|
@ -221,8 +206,8 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
await timerType.expand();
|
||||
await timerType.selectRowByValue("publish_to_category");
|
||||
await select(".timer-type", "publish_to_category");
|
||||
const categoryChooser = selectKit(".d-modal__body .category-chooser");
|
||||
|
||||
assert.strictEqual(categoryChooser.header().label(), "category…");
|
||||
assert.strictEqual(categoryChooser.header().value(), null);
|
||||
|
@ -278,17 +263,17 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
|
||||
test("schedule publish to category - does not show for a public topic", async function (assert) {
|
||||
updateCurrentUser({ moderator: true });
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
|
||||
await visit("/t/internationalization-localization");
|
||||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
await timerType.expand();
|
||||
assert.false(
|
||||
timerType.rowByValue("publish_to_category").exists(),
|
||||
"publish to category is not shown for a public topic"
|
||||
);
|
||||
assert
|
||||
.dselect(".timer-type")
|
||||
.hasNoOption(
|
||||
"publish_to_category",
|
||||
"publish to category is not shown for a public topic"
|
||||
);
|
||||
});
|
||||
|
||||
test("TL4 can't auto-delete", async function (assert) {
|
||||
|
@ -298,11 +283,7 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
|
||||
await timerType.expand();
|
||||
|
||||
assert.false(timerType.rowByValue("delete").exists());
|
||||
assert.dselect(".timer-type").hasNoOption("delete");
|
||||
});
|
||||
|
||||
test("Category Moderator can auto-delete replies", async function (assert) {
|
||||
|
@ -312,11 +293,10 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
|
||||
await timerType.expand();
|
||||
|
||||
assert.true(timerType.rowByValue("delete_replies").exists());
|
||||
assert.dselect(".timer-type").hasOption({
|
||||
value: "delete_replies",
|
||||
label: i18n("topic.auto_delete_replies.title"),
|
||||
});
|
||||
});
|
||||
|
||||
test("TL4 can't auto-delete replies", async function (assert) {
|
||||
|
@ -326,11 +306,7 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
|
||||
await timerType.expand();
|
||||
|
||||
assert.false(timerType.rowByValue("delete_replies").exists());
|
||||
assert.dselect(".timer-type").hasNoOption("delete_replies");
|
||||
});
|
||||
|
||||
test("Category Moderator can auto-delete", async function (assert) {
|
||||
|
@ -340,24 +316,18 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
|
||||
await timerType.expand();
|
||||
|
||||
assert.true(timerType.rowByValue("delete").exists());
|
||||
assert
|
||||
.dselect(".timer-type")
|
||||
.hasOption({ value: "delete", label: i18n("topic.auto_delete.title") });
|
||||
});
|
||||
|
||||
test("auto delete", async function (assert) {
|
||||
updateCurrentUser({ moderator: true });
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
|
||||
await visit("/t/internationalization-localization");
|
||||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
await timerType.expand();
|
||||
await timerType.selectRowByValue("delete");
|
||||
|
||||
await select(".timer-type", "delete");
|
||||
await click("#tap_tile_two_weeks");
|
||||
|
||||
assert
|
||||
|
@ -412,10 +382,7 @@ acceptance("Topic - Edit timer", function (needs) {
|
|||
await visit("/t/internationalization-localization");
|
||||
await click(".toggle-admin-menu");
|
||||
await click(".admin-topic-timer-update button");
|
||||
|
||||
const timerType = selectKit(".select-kit.timer-type");
|
||||
await timerType.expand();
|
||||
await timerType.selectRowByValue("close_after_last_post");
|
||||
await select(".timer-type", "close_after_last_post");
|
||||
|
||||
assert.dom(".topic-timer-heading").doesNotExist();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import { find } from "@ember/test-helpers";
|
||||
import QUnit from "qunit";
|
||||
|
||||
class DSelect {
|
||||
constructor(selector, context) {
|
||||
this.context = context;
|
||||
if (selector instanceof HTMLElement) {
|
||||
this.element = selector;
|
||||
} else {
|
||||
this.element = find(selector);
|
||||
}
|
||||
}
|
||||
|
||||
hasOption({ value, label }, assertionMessage) {
|
||||
const option = this.element.querySelector(
|
||||
`.d-select__option[value="${value}"]`
|
||||
);
|
||||
|
||||
this.context.dom(option).exists(assertionMessage);
|
||||
this.context.dom(option).hasText(label, assertionMessage);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
hasNoOption(value, assertionMessage) {
|
||||
const option = this.element.querySelector(
|
||||
`.d-select__option[value="${value}"]`
|
||||
);
|
||||
|
||||
this.context.dom(option).doesNotExist(assertionMessage);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
hasSelectedOption({ value, label }, assertionMessage) {
|
||||
this.context
|
||||
.dom(this.element.options[this.element.selectedIndex])
|
||||
.hasText(label, assertionMessage);
|
||||
|
||||
this.context.dom(this.element).hasValue(value, assertionMessage);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
hasNoSelectedOption({ value, label }, assertionMessage) {
|
||||
this.context
|
||||
.dom(this.element.options[this.element.selectedIndex])
|
||||
.hasNoText(label, assertionMessage);
|
||||
|
||||
this.context.dom(this.element).hasNoValue(value, assertionMessage);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export function setupDSelectAssertions() {
|
||||
QUnit.assert.dselect = function (selector = ".d-select") {
|
||||
return new DSelect(selector, this);
|
||||
};
|
||||
}
|
|
@ -103,6 +103,7 @@ import { resetNeedsHbrTopicList } from "discourse-common/lib/raw-templates";
|
|||
import { clearResolverOptions } from "discourse-common/resolver";
|
||||
import I18n from "discourse-i18n";
|
||||
import { _clearSnapshots } from "select-kit/components/composer-actions";
|
||||
import { setupDSelectAssertions } from "./d-select-assertions";
|
||||
import { setupFormKitAssertions } from "./form-kit-assertions";
|
||||
import { cleanupTemporaryModuleRegistrations } from "./temporary-module-helper";
|
||||
|
||||
|
@ -483,6 +484,7 @@ QUnit.assert.containsInstance = function (collection, klass, message) {
|
|||
};
|
||||
|
||||
setupFormKitAssertions();
|
||||
setupDSelectAssertions();
|
||||
|
||||
export async function selectDate(selector, date) {
|
||||
const elem = document.querySelector(selector);
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { render, select } from "@ember/test-helpers";
|
||||
import { module, test } from "qunit";
|
||||
import DSelect, { NO_VALUE_OPTION } from "discourse/components/d-select";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
module("Integration | Component | d-select", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("@onChange", async function (assert) {
|
||||
const handleChange = (value) => {
|
||||
assert.step(value);
|
||||
};
|
||||
|
||||
await render(<template>
|
||||
<DSelect @onChange={{handleChange}} as |s|>
|
||||
<s.Option @value="foo">The real foo</s.Option>
|
||||
</DSelect>
|
||||
</template>);
|
||||
|
||||
await select(".d-select", "foo");
|
||||
|
||||
assert.verifySteps(["foo"]);
|
||||
});
|
||||
|
||||
test("no value", async function (assert) {
|
||||
await render(<template><DSelect /></template>);
|
||||
|
||||
assert.dselect().hasSelectedOption({
|
||||
value: NO_VALUE_OPTION,
|
||||
label: i18n("select_placeholder"),
|
||||
});
|
||||
});
|
||||
|
||||
test("selected value", async function (assert) {
|
||||
await render(<template>
|
||||
<DSelect @value="foo" as |s|>
|
||||
<s.Option @value="foo">The real foo</s.Option>
|
||||
</DSelect>
|
||||
</template>);
|
||||
|
||||
assert.dselect().hasOption({
|
||||
value: NO_VALUE_OPTION,
|
||||
label: i18n("none_placeholder"),
|
||||
});
|
||||
|
||||
assert.dselect().hasSelectedOption({
|
||||
value: "foo",
|
||||
label: "The real foo",
|
||||
});
|
||||
});
|
||||
|
||||
test("select attributes", async function (assert) {
|
||||
await render(<template><DSelect class="test" /></template>);
|
||||
|
||||
assert.dom(".d-select.test").exists();
|
||||
});
|
||||
|
||||
test("option attributes", async function (assert) {
|
||||
await render(<template>
|
||||
<DSelect as |s|>
|
||||
<s.Option @value="foo" class="test">The real foo</s.Option>
|
||||
</DSelect>
|
||||
</template>);
|
||||
|
||||
assert.dom(".d-select__option.test").exists();
|
||||
});
|
||||
});
|
|
@ -3,7 +3,6 @@ import { module, test } from "qunit";
|
|||
import Form from "discourse/components/form";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import formKit from "discourse/tests/helpers/form-kit-helper";
|
||||
import { i18n } from "discourse-i18n";
|
||||
|
||||
module(
|
||||
"Integration | Component | FormKit | Controls | Select",
|
||||
|
@ -38,7 +37,7 @@ module(
|
|||
assert.deepEqual(data, { foo: "option-3" });
|
||||
});
|
||||
|
||||
test("when disabled", async function (assert) {
|
||||
test("@disabled", async function (assert) {
|
||||
await render(<template>
|
||||
<Form as |form|>
|
||||
<form.Field @name="foo" @title="Foo" @disabled={{true}} as |field|>
|
||||
|
@ -51,33 +50,5 @@ module(
|
|||
|
||||
assert.dom(".form-kit__control-select").hasAttribute("disabled");
|
||||
});
|
||||
|
||||
test("no selection", async function (assert) {
|
||||
await render(<template>
|
||||
<Form as |form|>
|
||||
<form.Field @name="foo" @title="Foo" as |field|>
|
||||
<field.Select as |select|>
|
||||
<select.Option @value="option-1">Option 1</select.Option>
|
||||
</field.Select>
|
||||
</form.Field>
|
||||
</Form>
|
||||
</template>);
|
||||
|
||||
assert
|
||||
.dom(".form-kit__control-select option:nth-child(1)")
|
||||
.hasText(
|
||||
i18n("form_kit.select.select_placeholder"),
|
||||
"it shows a placeholder for selection"
|
||||
);
|
||||
|
||||
await formKit().field("foo").select("option-1");
|
||||
|
||||
assert
|
||||
.dom(".form-kit__control-select option:nth-child(1)")
|
||||
.hasText(
|
||||
i18n("form_kit.select.none_placeholder"),
|
||||
"it shows a placeholder for unselection"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
@import "bookmark-modal";
|
||||
@import "bookmark-menu";
|
||||
@import "buttons";
|
||||
@import "d-select";
|
||||
@import "color-input";
|
||||
@import "char-counter";
|
||||
@import "conditional-loading-section";
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
.d-select {
|
||||
width: 100%;
|
||||
height: 2.25em;
|
||||
background: var(--secondary);
|
||||
border: 1px solid var(--primary-400);
|
||||
border-radius: var(--d-input-border-radius);
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
padding: 0 2em 0 0.5em !important;
|
||||
appearance: none;
|
||||
background-image: svg-uri(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='none' stroke='#{$primary-medium}' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m2 5 6 6 6-6'/></svg>"
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 16px 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible,
|
||||
&:focus:focus-visible,
|
||||
&:active {
|
||||
//these importants are another great case for having a button element without that pesky default styling
|
||||
&:not(:disabled) {
|
||||
background-color: var(--secondary) !important;
|
||||
color: var(--primary) !important;
|
||||
border-color: var(--tertiary);
|
||||
outline: 2px solid var(--tertiary);
|
||||
outline-offset: -2px;
|
||||
|
||||
.d-icon {
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
.discourse-no-touch & {
|
||||
background-color: var(--secondary);
|
||||
color: var(--primary);
|
||||
border-color: var(--tertiary);
|
||||
|
||||
.d-icon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
@include default-input;
|
||||
z-index: 1;
|
||||
margin: 0 !important;
|
||||
width: 100% !important;
|
||||
min-width: auto !important;
|
||||
|
||||
.form-kit__field.has-error & {
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
.form-kit__control-select {
|
||||
@include default-input;
|
||||
height: 2em;
|
||||
border: 1px solid var(--primary-low-mid);
|
||||
|
||||
padding: 0 2em 0 0.5em !important;
|
||||
appearance: none;
|
||||
background-image: svg-uri(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><path fill='none' stroke='#{$primary-medium}' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m2 5 6 6 6-6'/></svg>"
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.5rem center;
|
||||
background-size: 16px 12px;
|
||||
cursor: pointer;
|
||||
@include breakpoint(mobile-large) {
|
||||
height: 2.25em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
width: 100% !important;
|
||||
height: 2em;
|
||||
background: var(--secondary);
|
||||
border: 1px solid var(--primary-low-mid);
|
||||
border: 1px solid var(--primary-low-mid) !important;
|
||||
border-radius: var(--d-input-border-radius);
|
||||
padding: 0 0.5em !important;
|
||||
box-sizing: border-box;
|
||||
|
|
|
@ -548,6 +548,9 @@ en:
|
|||
switch_to_anon: "Enter Anonymous Mode"
|
||||
switch_from_anon: "Exit Anonymous Mode"
|
||||
|
||||
select_placeholder: "Select…"
|
||||
none_placeholder: "None"
|
||||
|
||||
banner:
|
||||
close: "Dismiss this banner"
|
||||
edit: "Edit"
|
||||
|
@ -2206,9 +2209,6 @@ en:
|
|||
optional: optional
|
||||
errors_summary_title: "This form contains errors:"
|
||||
dirty_form: "You didn't submit your changes! Are you sure you want to leave?"
|
||||
select:
|
||||
select_placeholder: "Select…"
|
||||
none_placeholder: "None"
|
||||
errors:
|
||||
required: "Required"
|
||||
invalid_url: "Must be a valid URL"
|
||||
|
|
Loading…
Reference in New Issue