diff --git a/app/assets/javascripts/vue_shared/components/multiple_choice_selector.stories.js b/app/assets/javascripts/vue_shared/components/multiple_choice_selector.stories.js new file mode 100644 index 0000000000000000000000000000000000000000..2fc2a868cfea85c23083fa1e3caf6fc180b52593 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/multiple_choice_selector.stories.js @@ -0,0 +1,34 @@ +import { GlBadge, GlIcon } from '@gitlab/ui'; +import MultipleChoiceSelector from './multiple_choice_selector.vue'; +import MultipleChoiceSelectorItem from './multiple_choice_selector_item.vue'; + +export default { + component: MultipleChoiceSelector, + title: 'vue_shared/multiple_choice_selector', +}; + +const data = () => ({ + selected: ['option', 'option-two'], +}); + +const Template = () => ({ + components: { MultipleChoiceSelector, MultipleChoiceSelectorItem, GlBadge, GlIcon }, + data, + template: ` + + + + Option name + Beta + + + + + + + + + `, +}); + +export const Default = Template.bind({}); diff --git a/app/assets/javascripts/vue_shared/components/multiple_choice_selector.vue b/app/assets/javascripts/vue_shared/components/multiple_choice_selector.vue new file mode 100644 index 0000000000000000000000000000000000000000..f0090417206345f14f764a2ab01e4f9b40fa2c02 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/multiple_choice_selector.vue @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/assets/javascripts/vue_shared/components/multiple_choice_selector_item.vue b/app/assets/javascripts/vue_shared/components/multiple_choice_selector_item.vue new file mode 100644 index 0000000000000000000000000000000000000000..766b4c41a239bfd684aba317d8f367c5da804007 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/multiple_choice_selector_item.vue @@ -0,0 +1,55 @@ + + + + + + + + + {{ title }} + + + + {{ disabledMessage }} + + + {{ description }} + + + + diff --git a/app/assets/javascripts/vue_shared/components/single_choice_selector.stories.js b/app/assets/javascripts/vue_shared/components/single_choice_selector.stories.js new file mode 100644 index 0000000000000000000000000000000000000000..8f841de7d8e6cad1d045cead3ded7bc0109a7614 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/single_choice_selector.stories.js @@ -0,0 +1,34 @@ +import { GlBadge, GlIcon } from '@gitlab/ui'; +import SingleChoiceSelector from './single_choice_selector.vue'; +import SingleChoiceSelectorItem from './single_choice_selector_item.vue'; + +export default { + component: SingleChoiceSelector, + title: 'vue_shared/single_choice_selector', +}; + +const data = () => ({ + checked: 'option', +}); + +const Template = () => ({ + components: { SingleChoiceSelector, SingleChoiceSelectorItem, GlBadge, GlIcon }, + data, + template: ` + + + + Option name + Beta + + + + + + + + + `, +}); + +export const Default = Template.bind({}); diff --git a/app/assets/javascripts/vue_shared/components/single_choice_selector.vue b/app/assets/javascripts/vue_shared/components/single_choice_selector.vue new file mode 100644 index 0000000000000000000000000000000000000000..b5c9a5d2cefa6f80183eb780c06a34a4dd42cc16 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/single_choice_selector.vue @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/assets/javascripts/vue_shared/components/single_choice_selector_item.vue b/app/assets/javascripts/vue_shared/components/single_choice_selector_item.vue new file mode 100644 index 0000000000000000000000000000000000000000..95c56c769f83b79e8de530670fca4d9623698b2d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/single_choice_selector_item.vue @@ -0,0 +1,53 @@ + + + + + + + + + {{ title }} + + + + {{ disabledMessage }} + + + {{ description }} + + + + diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss index 16ddec324e8934377055d1cc495c17f1727f06dc..018b1fc49e932e3260411cac0dde9002c6c6a208 100644 --- a/app/assets/stylesheets/components/_index.scss +++ b/app/assets/stylesheets/components/_index.scss @@ -3,6 +3,7 @@ @import './content_editor'; @import './deployment_instance'; @import './detail_page'; +@import './multiple_choice_selector'; @import './ref_selector'; @import './related_items_list'; @import './severity/icons'; diff --git a/app/assets/stylesheets/components/multiple_choice_selector.scss b/app/assets/stylesheets/components/multiple_choice_selector.scss new file mode 100644 index 0000000000000000000000000000000000000000..d4b4b7908f371f5f934d2f0286ff58f53c809d35 --- /dev/null +++ b/app/assets/stylesheets/components/multiple_choice_selector.scss @@ -0,0 +1,51 @@ +.multiple-choice-selector { + &-item { + @include gl-prefers-reduced-motion-transition; + transition: background-color $gl-transition-duration-medium $gl-easing-out-cubic, + border-color $gl-transition-duration-medium $gl-easing-out-cubic; + + &:not(:last-child) { + @apply gl-border-b; + } + + &:first-child { + @apply gl-rounded-t-base; + } + + &:last-child { + @apply gl-rounded-b-base; + } + + // stylelint-disable-next-line gitlab/no-gl-class + &.multiple-choice-selector-item .gl-form-checkbox.gl-form-checkbox label, + &.multiple-choice-selector-item .gl-form-radio.gl-form-radio label { + width: 100%; + margin-bottom: 0; + } + + &:has(input:checked) { + border: 1px solid var(--gl-control-border-color-selected-default); + @apply gl-bg-subtle gl-rounded-base; + } + + &:has(input:checked) + &:has(input:checked) { + @apply gl-rounded-t-none; + } + + &:has(input:checked):has(+ & input:checked) { + @apply gl-rounded-b-none; + } + + &:not(:last-child):has(input:checked) { + margin: -1px -1px 0; + } + + &:last-child:has(input:checked) { + margin: -1px; + } + } + + &-click-area { + @apply gl-absolute -gl-top-5 -gl-left-7 gl-w-full gl-h-full gl-p-5 gl-pl-7 gl-box-content -gl-z-1; + } +} diff --git a/spec/frontend/vue_shared/components/multiple_choice_selector_item_spec.js b/spec/frontend/vue_shared/components/multiple_choice_selector_item_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..cf90635a90c5fba588233aa0c76ca0e0bb18ab58 --- /dev/null +++ b/spec/frontend/vue_shared/components/multiple_choice_selector_item_spec.js @@ -0,0 +1,41 @@ +import { GlFormCheckbox } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import MultipleChoiceSelectorItem from '~/vue_shared/components/multiple_choice_selector_item.vue'; + +describe('MultipleChoiceSelectorItem', () => { + let wrapper; + + function createComponent({ propsData = {} } = {}) { + wrapper = shallowMount(MultipleChoiceSelectorItem, { + propsData: { + ...propsData, + }, + }); + } + + const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); + + it('renders checkbox', () => { + createComponent(); + + expect(findCheckbox().exists()).toBe(true); + }); + + it('renders title', () => { + createComponent({ propsData: { title: 'Option title' } }); + + expect(findCheckbox().text()).toContain('Option title'); + }); + + it('renders description', () => { + createComponent({ propsData: { description: 'Option description' } }); + + expect(wrapper.text()).toContain('Option description'); + }); + + it('renders disabled message', () => { + createComponent({ propsData: { disabledMessage: 'Option disabled message', disabled: true } }); + + expect(wrapper.text()).toContain('Option disabled message'); + }); +}); diff --git a/spec/frontend/vue_shared/components/multiple_choice_selector_spec.js b/spec/frontend/vue_shared/components/multiple_choice_selector_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..8727ab6a2e2c7cb49d7ee480ca8b065d8339edb2 --- /dev/null +++ b/spec/frontend/vue_shared/components/multiple_choice_selector_spec.js @@ -0,0 +1,28 @@ +import { GlFormCheckboxGroup } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import MultipleChoiceSelector from '~/vue_shared/components/multiple_choice_selector.vue'; + +describe('MultipleChoiceSelector', () => { + let wrapper; + + const defaultPropsData = { + selected: ['option'], + }; + + function createComponent({ propsData = {} } = {}) { + wrapper = shallowMount(MultipleChoiceSelector, { + propsData: { + ...defaultPropsData, + ...propsData, + }, + }); + } + + const findCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup); + + it('renders checkbox group', () => { + createComponent(); + + expect(findCheckboxGroup().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/vue_shared/components/single_choice_selector_item_spec.js b/spec/frontend/vue_shared/components/single_choice_selector_item_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..02d46071c3ad8c47c118d84bf1ae434cb6977db4 --- /dev/null +++ b/spec/frontend/vue_shared/components/single_choice_selector_item_spec.js @@ -0,0 +1,41 @@ +import { GlFormRadio } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SingleChoiceSelectorItem from '~/vue_shared/components/single_choice_selector_item.vue'; + +describe('SingleChoiceSelectorItem', () => { + let wrapper; + + function createComponent({ propsData = {} } = {}) { + wrapper = shallowMount(SingleChoiceSelectorItem, { + propsData: { + ...propsData, + }, + }); + } + + const findRadio = () => wrapper.findComponent(GlFormRadio); + + it('renders radio', () => { + createComponent(); + + expect(findRadio().exists()).toBe(true); + }); + + it('renders title', () => { + createComponent({ propsData: { title: 'Option title' } }); + + expect(findRadio().text()).toContain('Option title'); + }); + + it('renders description', () => { + createComponent({ propsData: { description: 'Option description' } }); + + expect(wrapper.text()).toContain('Option description'); + }); + + it('renders disabled message', () => { + createComponent({ propsData: { disabledMessage: 'Option disabled message', disabled: true } }); + + expect(wrapper.text()).toContain('Option disabled message'); + }); +}); diff --git a/spec/frontend/vue_shared/components/single_choice_selector_spec.js b/spec/frontend/vue_shared/components/single_choice_selector_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..67c6317d373a4415ea94236e3fb4c1059ae9e49c --- /dev/null +++ b/spec/frontend/vue_shared/components/single_choice_selector_spec.js @@ -0,0 +1,28 @@ +import { GlFormRadioGroup } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SingleChoiceSelector from '~/vue_shared/components/single_choice_selector.vue'; + +describe('SingleChoice', () => { + let wrapper; + + const defaultPropsData = { + checked: 'option', + }; + + function createComponent({ propsData = {} } = {}) { + wrapper = shallowMount(SingleChoiceSelector, { + propsData: { + ...defaultPropsData, + ...propsData, + }, + }); + } + + const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); + + it('renders radio group', () => { + createComponent(); + + expect(findRadioGroup().exists()).toBe(true); + }); +});
+ {{ disabledMessage }} +
+ {{ description }} +