From a122ac8f3e2c09c76502d68df9a9c86f9b7c1098 Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Mon, 13 Jan 2025 16:48:11 +0100 Subject: [PATCH 1/2] Add multi and single choice components --- .../components/multiple_choice.stories.js | 34 +++++++++++ .../vue_shared/components/multiple_choice.vue | 27 ++++++++ .../components/multiple_choice_item.vue | 55 +++++++++++++++++ .../components/single_choice.stories.js | 34 +++++++++++ .../vue_shared/components/single_choice.vue | 27 ++++++++ .../components/single_choice_item.vue | 53 ++++++++++++++++ app/assets/stylesheets/components/_index.scss | 1 + .../components/multiple_choice.scss | 61 +++++++++++++++++++ .../components/multiple_choice_item_spec.js | 41 +++++++++++++ .../components/multiple_choice_spec.js | 28 +++++++++ .../components/single_choice_item_spec.js | 41 +++++++++++++ .../components/single_choice_spec.js | 28 +++++++++ 12 files changed, 430 insertions(+) create mode 100644 app/assets/javascripts/vue_shared/components/multiple_choice.stories.js create mode 100644 app/assets/javascripts/vue_shared/components/multiple_choice.vue create mode 100644 app/assets/javascripts/vue_shared/components/multiple_choice_item.vue create mode 100644 app/assets/javascripts/vue_shared/components/single_choice.stories.js create mode 100644 app/assets/javascripts/vue_shared/components/single_choice.vue create mode 100644 app/assets/javascripts/vue_shared/components/single_choice_item.vue create mode 100644 app/assets/stylesheets/components/multiple_choice.scss create mode 100644 spec/frontend/vue_shared/components/multiple_choice_item_spec.js create mode 100644 spec/frontend/vue_shared/components/multiple_choice_spec.js create mode 100644 spec/frontend/vue_shared/components/single_choice_item_spec.js create mode 100644 spec/frontend/vue_shared/components/single_choice_spec.js diff --git a/app/assets/javascripts/vue_shared/components/multiple_choice.stories.js b/app/assets/javascripts/vue_shared/components/multiple_choice.stories.js new file mode 100644 index 00000000000000..76a1df7bf7e2cd --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/multiple_choice.stories.js @@ -0,0 +1,34 @@ +import { GlBadge, GlIcon } from '@gitlab/ui'; +import MultipleChoice from './multiple_choice.vue'; +import MultipleChoiceItem from './multiple_choice_item.vue'; + +export default { + component: MultipleChoice, + title: 'vue_shared/multiple_choice', +}; + +const data = () => ({ + selected: ['option', 'option-two'], +}); + +const Template = () => ({ + components: { MultipleChoice, MultipleChoiceItem, GlBadge, GlIcon }, + data, + template: ` + + + + Option name + Beta +
+ + + + +
+
+ +
`, +}); + +export const Default = Template.bind({}); diff --git a/app/assets/javascripts/vue_shared/components/multiple_choice.vue b/app/assets/javascripts/vue_shared/components/multiple_choice.vue new file mode 100644 index 00000000000000..6ef01bcf7e00ba --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/multiple_choice.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/assets/javascripts/vue_shared/components/multiple_choice_item.vue b/app/assets/javascripts/vue_shared/components/multiple_choice_item.vue new file mode 100644 index 00000000000000..99b03f6e9d30ba --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/multiple_choice_item.vue @@ -0,0 +1,55 @@ + + + diff --git a/app/assets/javascripts/vue_shared/components/single_choice.stories.js b/app/assets/javascripts/vue_shared/components/single_choice.stories.js new file mode 100644 index 00000000000000..05a4465eb4c8e4 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/single_choice.stories.js @@ -0,0 +1,34 @@ +import { GlBadge, GlIcon } from '@gitlab/ui'; +import SingleChoice from './single_choice.vue'; +import SingleChoiceItem from './single_choice_item.vue'; + +export default { + component: SingleChoice, + title: 'vue_shared/single_choice', +}; + +const data = () => ({ + checked: 'option', +}); + +const Template = () => ({ + components: { SingleChoice, SingleChoiceItem, GlBadge, GlIcon }, + data, + template: ` + + + + Option name + Beta +
+ + + + +
+
+ +
`, +}); + +export const Default = Template.bind({}); diff --git a/app/assets/javascripts/vue_shared/components/single_choice.vue b/app/assets/javascripts/vue_shared/components/single_choice.vue new file mode 100644 index 00000000000000..39e1c92bc48211 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/single_choice.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/assets/javascripts/vue_shared/components/single_choice_item.vue b/app/assets/javascripts/vue_shared/components/single_choice_item.vue new file mode 100644 index 00000000000000..32f32d2b6b58bb --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/single_choice_item.vue @@ -0,0 +1,53 @@ + + + diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss index 16ddec324e8934..73fd64b30a8b38 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'; @import './ref_selector'; @import './related_items_list'; @import './severity/icons'; diff --git a/app/assets/stylesheets/components/multiple_choice.scss b/app/assets/stylesheets/components/multiple_choice.scss new file mode 100644 index 00000000000000..b26c94d934181e --- /dev/null +++ b/app/assets/stylesheets/components/multiple_choice.scss @@ -0,0 +1,61 @@ +.multiple-choice { + &-item { + @apply gl-p-5; + @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-item .gl-form-checkbox.gl-form-checkbox label, + &.multiple-choice-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); + background-color: var(--gl-background-color-subtle); + @apply 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 { + position: absolute; + top: -$gl-spacing-scale-5; + left: -$gl-spacing-scale-7; + width: 100%; + height: 100%; + @apply gl-p-5; + @apply gl-pl-7; + box-sizing: content-box; + z-index: -1; + } +} diff --git a/spec/frontend/vue_shared/components/multiple_choice_item_spec.js b/spec/frontend/vue_shared/components/multiple_choice_item_spec.js new file mode 100644 index 00000000000000..fb51a97f3f3cef --- /dev/null +++ b/spec/frontend/vue_shared/components/multiple_choice_item_spec.js @@ -0,0 +1,41 @@ +import { GlFormCheckbox } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import MultipleChoiceItem from '~/vue_shared/components/multiple_choice_item.vue'; + +describe('MultipleChoiceItem', () => { + let wrapper; + + function createComponent({ propsData = {} } = {}) { + wrapper = shallowMount(MultipleChoiceItem, { + 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_spec.js b/spec/frontend/vue_shared/components/multiple_choice_spec.js new file mode 100644 index 00000000000000..a2100514068476 --- /dev/null +++ b/spec/frontend/vue_shared/components/multiple_choice_spec.js @@ -0,0 +1,28 @@ +import { GlFormCheckboxGroup } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import MultipleChoice from '~/vue_shared/components/multiple_choice.vue'; + +describe('MultipleChoice', () => { + let wrapper; + + const defaultPropsData = { + selected: ['option'], + }; + + function createComponent({ propsData = {} } = {}) { + wrapper = shallowMount(MultipleChoice, { + 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_item_spec.js b/spec/frontend/vue_shared/components/single_choice_item_spec.js new file mode 100644 index 00000000000000..b805641af781ce --- /dev/null +++ b/spec/frontend/vue_shared/components/single_choice_item_spec.js @@ -0,0 +1,41 @@ +import { GlFormRadio } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SingleChoiceItem from '~/vue_shared/components/single_choice_item.vue'; + +describe('SingleChoiceItem', () => { + let wrapper; + + function createComponent({ propsData = {} } = {}) { + wrapper = shallowMount(SingleChoiceItem, { + 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_spec.js b/spec/frontend/vue_shared/components/single_choice_spec.js new file mode 100644 index 00000000000000..30e1333c15605a --- /dev/null +++ b/spec/frontend/vue_shared/components/single_choice_spec.js @@ -0,0 +1,28 @@ +import { GlFormRadioGroup } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SingleChoice from '~/vue_shared/components/single_choice.vue'; + +describe('SingleChoice', () => { + let wrapper; + + const defaultPropsData = { + checked: 'option', + }; + + function createComponent({ propsData = {} } = {}) { + wrapper = shallowMount(SingleChoice, { + propsData: { + ...defaultPropsData, + ...propsData, + }, + }); + } + + const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup); + + it('renders radio group', () => { + createComponent(); + + expect(findRadioGroup().exists()).toBe(true); + }); +}); -- GitLab From 8741d5fe5db1f79159c179963ccb38efcbda28f2 Mon Sep 17 00:00:00 2001 From: Julia Miocene Date: Wed, 15 Jan 2025 13:32:27 +0100 Subject: [PATCH 2/2] Apply suggestions --- .../components/multiple_choice.stories.js | 34 ------------------- .../multiple_choice_selector.stories.js | 34 +++++++++++++++++++ ...hoice.vue => multiple_choice_selector.vue} | 2 +- ....vue => multiple_choice_selector_item.vue} | 6 ++-- .../components/single_choice.stories.js | 34 ------------------- .../single_choice_selector.stories.js | 34 +++++++++++++++++++ ..._choice.vue => single_choice_selector.vue} | 2 +- ...em.vue => single_choice_selector_item.vue} | 4 +-- app/assets/stylesheets/components/_index.scss | 2 +- ...ice.scss => multiple_choice_selector.scss} | 20 +++-------- ... => multiple_choice_selector_item_spec.js} | 6 ++-- ...ec.js => multiple_choice_selector_spec.js} | 6 ++-- ...js => single_choice_selector_item_spec.js} | 6 ++-- ...spec.js => single_choice_selector_spec.js} | 4 +-- 14 files changed, 92 insertions(+), 102 deletions(-) delete mode 100644 app/assets/javascripts/vue_shared/components/multiple_choice.stories.js create mode 100644 app/assets/javascripts/vue_shared/components/multiple_choice_selector.stories.js rename app/assets/javascripts/vue_shared/components/{multiple_choice.vue => multiple_choice_selector.vue} (85%) rename app/assets/javascripts/vue_shared/components/{multiple_choice_item.vue => multiple_choice_selector_item.vue} (81%) delete mode 100644 app/assets/javascripts/vue_shared/components/single_choice.stories.js create mode 100644 app/assets/javascripts/vue_shared/components/single_choice_selector.stories.js rename app/assets/javascripts/vue_shared/components/{single_choice.vue => single_choice_selector.vue} (84%) rename app/assets/javascripts/vue_shared/components/{single_choice_item.vue => single_choice_selector_item.vue} (89%) rename app/assets/stylesheets/components/{multiple_choice.scss => multiple_choice_selector.scss} (67%) rename spec/frontend/vue_shared/components/{multiple_choice_item_spec.js => multiple_choice_selector_item_spec.js} (82%) rename spec/frontend/vue_shared/components/{multiple_choice_spec.js => multiple_choice_selector_spec.js} (73%) rename spec/frontend/vue_shared/components/{single_choice_item_spec.js => single_choice_selector_item_spec.js} (82%) rename spec/frontend/vue_shared/components/{single_choice_spec.js => single_choice_selector_spec.js} (79%) diff --git a/app/assets/javascripts/vue_shared/components/multiple_choice.stories.js b/app/assets/javascripts/vue_shared/components/multiple_choice.stories.js deleted file mode 100644 index 76a1df7bf7e2cd..00000000000000 --- a/app/assets/javascripts/vue_shared/components/multiple_choice.stories.js +++ /dev/null @@ -1,34 +0,0 @@ -import { GlBadge, GlIcon } from '@gitlab/ui'; -import MultipleChoice from './multiple_choice.vue'; -import MultipleChoiceItem from './multiple_choice_item.vue'; - -export default { - component: MultipleChoice, - title: 'vue_shared/multiple_choice', -}; - -const data = () => ({ - selected: ['option', 'option-two'], -}); - -const Template = () => ({ - components: { MultipleChoice, MultipleChoiceItem, GlBadge, GlIcon }, - data, - template: ` - - - - Option name - Beta -
- - - - -
-
- -
`, -}); - -export const Default = Template.bind({}); 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 00000000000000..2fc2a868cfea85 --- /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.vue b/app/assets/javascripts/vue_shared/components/multiple_choice_selector.vue similarity index 85% rename from app/assets/javascripts/vue_shared/components/multiple_choice.vue rename to app/assets/javascripts/vue_shared/components/multiple_choice_selector.vue index 6ef01bcf7e00ba..f0090417206345 100644 --- a/app/assets/javascripts/vue_shared/components/multiple_choice.vue +++ b/app/assets/javascripts/vue_shared/components/multiple_choice_selector.vue @@ -20,7 +20,7 @@ export default {