Просмотр исходного кода

Merge pull request #15249 from overleaf/rd-odc-radio

Radio buttons for Onboarding Data Collection

GitOrigin-RevId: 8c4d1e965f3263b4c8a41c129c5c3d6e3ef10986
Rebeka Dekany 2 лет назад
Родитель
Сommit
a2946f8601

+ 7 - 0
services/web/frontend/extracted-translations.json

@@ -457,6 +457,7 @@
   "headers": "",
   "help": "",
   "help_improve_overleaf_fill_out_this_survey": "",
+  "helps_us_tailor_your_experience": "",
   "hide_configuration": "",
   "hide_document_preamble": "",
   "hide_outline": "",
@@ -839,6 +840,12 @@
   "premium_plan_label": "",
   "press_shortcut_to_open_advanced_reference_search": "",
   "price": "",
+  "primarily_work_study_question": "",
+  "primarily_work_study_question_government": "",
+  "primarily_work_study_question_nonprofit_ngo": "",
+  "primarily_work_study_question_other": "",
+  "primarily_work_study_question_private_company": "",
+  "primarily_work_study_question_university_school": "",
   "priority_support": "",
   "private": "",
   "problem_talking_to_publishing_service": "",

+ 42 - 0
services/web/frontend/js/shared/components/radio-chip.tsx

@@ -0,0 +1,42 @@
+import React from 'react'
+
+type RadioChipProps<ValueType> = {
+  checked?: boolean
+  disabled?: boolean
+  name: string
+  onChange: (value: ValueType) => void
+  required?: boolean
+  label: React.ReactElement | string
+  value: ValueType
+}
+
+const RadioChip = <T extends string>({
+  checked,
+  disabled,
+  name,
+  onChange,
+  label,
+  required,
+  value,
+}: RadioChipProps<T>) => {
+  const handleChange = () => {
+    onChange(value)
+  }
+
+  return (
+    <label className="radio-chip" data-disabled={disabled ? 'true' : undefined}>
+      <input
+        checked={checked}
+        disabled={disabled}
+        name={name}
+        onChange={handleChange}
+        type="radio"
+        required={required}
+        value={value}
+      />
+      {label}
+    </label>
+  )
+}
+
+export default RadioChip

+ 23 - 0
services/web/frontend/stories/radio-chip.stories.tsx

@@ -0,0 +1,23 @@
+import RadioChip from '../js/shared/components/radio-chip'
+
+type Args = React.ComponentProps<typeof RadioChip>
+
+export const RadioChipDefault = (args: Args) => {
+  return <RadioChip {...args} />
+}
+
+export const RadioChipDisabled = (args: Args) => {
+  return <RadioChip {...args} disabled />
+}
+
+export const RadioChipDisabledSelected = (args: Args) => {
+  return <RadioChip {...args} checked disabled />
+}
+
+export default {
+  title: 'Shared / Components / RadioChip',
+  component: RadioChip,
+  args: {
+    label: 'Option',
+  },
+}

+ 1 - 0
services/web/frontend/stylesheets/_style_includes.less

@@ -66,6 +66,7 @@
 @import 'components/split-menu.less';
 @import 'components/group-members.less';
 @import 'components/stepper.less';
+@import 'components/radio-chip.less';
 
 // Components w/ JavaScript
 @import 'components/modals.less';

+ 57 - 0
services/web/frontend/stylesheets/components/radio-chip.less

@@ -0,0 +1,57 @@
+.radio-chip {
+  background: @white;
+  border: 1px solid @neutral-60;
+  border-radius: 999px;
+  color: @neutral-90;
+  display: inline-flex;
+  font-weight: 400;
+  gap: 4px;
+  inline-size: fit-content;
+  line-height: 1.4;
+  padding: 8px 16px 8px 8px;
+
+  @media only screen and (max-width: @screen-sm-min) {
+    width: 100%;
+  }
+
+  &:hover {
+    background: @neutral-20;
+  }
+
+  &:focus-within {
+    box-shadow: 0px 0px 0px 2px @blue-30;
+  }
+
+  input[type='radio'] {
+    accent-color: @green-50;
+    height: 16px;
+    margin: 4px;
+    width: 15px;
+  }
+}
+
+.radio-chip[data-disabled='true'] {
+  border-color: @neutral-20;
+
+  &:hover {
+    background: @white;
+    cursor: not-allowed;
+  }
+}
+
+.radio-group-col-1 {
+  align-items: flex-start;
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.radio-group-col-2 {
+  display: grid;
+  grid-gap: 16px;
+  grid-template-columns: 1fr 1fr;
+
+  @media (max-width: @screen-sm-min) {
+    grid-template-columns: 1fr;
+  }
+}

+ 1 - 0
services/web/frontend/stylesheets/main-style.less

@@ -77,6 +77,7 @@
 @import 'components/switch.less';
 @import 'components/switcher.less';
 @import 'components/stepper.less';
+@import 'components/radio-chip.less';
 
 // Components w/ JavaScript
 @import 'components/modals.less';

+ 12 - 3
services/web/frontend/stylesheets/modules/overleaf-integration.less

@@ -32,7 +32,7 @@
 }
 
 #onboarding-data-collection {
-  width: 720px;
+  max-width: 720px;
   padding: 24px;
   margin: 0 auto;
   background: #fff;
@@ -40,9 +40,13 @@
   flex-direction: column;
   gap: 24px;
 
-  p {
+  .onboarding-question-title {
     font-size: 20px;
-    margin-bottom: 5px;
+  }
+
+  p {
+    font-size: 16px;
+    margin-bottom: 16px;
   }
 
   .logo {
@@ -60,6 +64,11 @@
       justify-content: stretch;
       gap: 24px;
 
+      @media (max-width: @screen-xs-max) {
+        flex-direction: column;
+        gap: 0;
+      }
+
       .form-group {
         flex-grow: 1;
       }

+ 7 - 0
services/web/locales/en.json

@@ -732,6 +732,7 @@
   "help_articles_matching": "Help articles matching your subject",
   "help_improve_overleaf_fill_out_this_survey": "If you would like to help us improve Overleaf, please take a moment to fill out <0>this survey</0>.",
   "help_us_spread_word": "Help us spread the word about __appName__",
+  "helps_us_tailor_your_experience": "This helps us tailor your Overleaf experience.",
   "hide_configuration": "Hide configuration",
   "hide_document_preamble": "Hide document preamble",
   "hide_outline": "Hide File outline",
@@ -1318,6 +1319,12 @@
   "press_and_awards": "Press &amp; awards",
   "press_shortcut_to_open_advanced_reference_search": "Press <strong>__ctrlSpace__</strong> or <strong>__altSpace__</strong> to open <strong>Advanced Reference Search</strong>",
   "price": "Price",
+  "primarily_work_study_question": "Where do you primarily work or study?",
+  "primarily_work_study_question_government": "In the government",
+  "primarily_work_study_question_nonprofit_ngo": "In a nonprofit/NGO",
+  "primarily_work_study_question_other": "Other",
+  "primarily_work_study_question_private_company": "In a private company",
+  "primarily_work_study_question_university_school": "In a university or school",
   "primary_email_check_question": "Is <0>__email__</0> still your email address?",
   "priority_support": "Priority support",
   "priority_support_info": "Our helpful Support team will prioritise and escalate your support requests where necessary.",

+ 57 - 0
services/web/test/frontend/shared/components/radio-chip.test.tsx

@@ -0,0 +1,57 @@
+import { expect } from 'chai'
+import { render, screen } from '@testing-library/react'
+import RadioChip from '@/shared/components/radio-chip'
+
+describe('<RadioChip />', function () {
+  const defaultProps = {
+    name: 'test',
+    label: 'Test',
+    value: 'testValue',
+    onChange: () => {},
+  }
+
+  describe('component renders and with label', function () {
+    it('renders and label is provided', function () {
+      render(<RadioChip {...defaultProps} />)
+      screen.getByText('Test')
+    })
+  })
+
+  describe('props', function () {
+    it('should be checked when the checked prop is provided', function () {
+      render(<RadioChip {...defaultProps} checked />)
+      const radioChip = screen.getByRole('radio') as HTMLInputElement
+      expect(radioChip.checked).to.equal(true)
+    })
+
+    it('should be disabled when the disabled prop is provided', function () {
+      render(<RadioChip {...defaultProps} disabled />)
+      const radioChip = screen.getByRole('radio') as HTMLInputElement
+      expect(radioChip.disabled).to.equal(true)
+    })
+
+    it('should have the required attribute when the required prop is provided', function () {
+      render(<RadioChip {...defaultProps} required />)
+      const radioChip = screen.getByRole('radio') as HTMLInputElement
+      expect(radioChip.required).to.equal(true)
+    })
+
+    it('should use the provided name prop', function () {
+      render(<RadioChip {...defaultProps} name="testName" />)
+      const radioChip = screen.getByRole('radio') as HTMLInputElement
+      expect(radioChip.name).to.equal('testName')
+    })
+
+    it('should use the provided value prop', function () {
+      render(<RadioChip {...defaultProps} />)
+      const radioChip = screen.getByRole('radio') as HTMLInputElement
+      expect(radioChip.value).to.equal('testValue')
+    })
+
+    it('should have the data-disabled attribute when the disabled prop is provided', function () {
+      render(<RadioChip {...defaultProps} disabled />)
+      const label = screen.getByText('Test')?.closest('label')
+      expect(label?.getAttribute('data-disabled')).to.equal('true')
+    })
+  })
+})