Merge pull request #19126 from overleaf/td-bs5-contact-modal-react

React version of Contact Us modal

GitOrigin-RevId: 0bef3095f36daa88afdc6172a5531ed11e892047
This commit is contained in:
Tim Down 2024-08-13 12:37:15 +01:00 committed by Copybot
parent fb114a7c44
commit 34fc43d59a
33 changed files with 484 additions and 210 deletions

View file

@ -555,6 +555,7 @@
"have_more_days_to_try": "", "have_more_days_to_try": "",
"headers": "", "headers": "",
"help": "", "help": "",
"help_articles_matching": "",
"help_improve_overleaf_fill_out_this_survey": "", "help_improve_overleaf_fill_out_this_survey": "",
"help_improve_screen_reader_fill_out_this_survey": "", "help_improve_screen_reader_fill_out_this_survey": "",
"hide_configuration": "", "hide_configuration": "",
@ -686,10 +687,12 @@
"joined_team": "", "joined_team": "",
"joining": "", "joining": "",
"justify": "", "justify": "",
"kb_suggestions_enquiry": "",
"keep_current_plan": "", "keep_current_plan": "",
"keep_personal_projects_separate": "", "keep_personal_projects_separate": "",
"keep_your_account_safe_add_another_email": "", "keep_your_account_safe_add_another_email": "",
"keybindings": "", "keybindings": "",
"knowledge_base": "",
"labels_help_you_to_easily_reference_your_figures": "", "labels_help_you_to_easily_reference_your_figures": "",
"labels_help_you_to_reference_your_tables": "", "labels_help_you_to_reference_your_tables": "",
"language_feedback": "", "language_feedback": "",
@ -976,6 +979,8 @@
"please_enter_confirmation_code": "", "please_enter_confirmation_code": "",
"please_get_in_touch": "", "please_get_in_touch": "",
"please_link_before_making_primary": "", "please_link_before_making_primary": "",
"please_provide_a_message": "",
"please_provide_a_subject": "",
"please_reconfirm_institutional_email": "", "please_reconfirm_institutional_email": "",
"please_reconfirm_your_affiliation_before_making_this_primary": "", "please_reconfirm_your_affiliation_before_making_this_primary": "",
"please_refresh": "", "please_refresh": "",

View file

@ -24,10 +24,12 @@ export function formatWikiHit(hit) {
const pagePath = hit.kb ? 'how-to' : 'latex' const pagePath = hit.kb ? 'how-to' : 'latex'
let pageAnchor = '' let pageAnchor = ''
let pageName = hit._highlightResult.pageName.value const rawPageName = hit._highlightResult.pageName.value
if (hit.sectionName) { const sectionName = hit.sectionName
pageAnchor = `#${hit.sectionName.replace(/\s/g, '_')}` let pageName = rawPageName
pageName += ' - ' + hit.sectionName if (sectionName) {
pageAnchor = `#${sectionName.replace(/\s/g, '_')}`
pageName += ' - ' + sectionName
} }
const body = hit._highlightResult.content.value const body = hit._highlightResult.content.value
@ -37,5 +39,5 @@ export function formatWikiHit(hit) {
.join('\n...\n') .join('\n...\n')
const url = `/learn/${pagePath}/${pageSlug}${pageAnchor}` const url = `/learn/${pagePath}/${pageSlug}${pageAnchor}`
return { url, pageName, content } return { url, pageName, rawPageName, sectionName, content }
} }

View file

@ -48,7 +48,7 @@ export function setupSearch(formEl) {
const iconEl = document.createElement('i') const iconEl = document.createElement('i')
iconEl.className = 'fa fa-angle-right' iconEl.className = 'fa fa-angle-right'
iconEl.setAttribute('aria-hidden', 'true') iconEl.setAttribute('aria-hidden', 'true')
linkEl.append(contentEl) linkEl.append(iconEl)
resultsEl.append(liEl) resultsEl.append(liEl)
} }

View file

@ -118,18 +118,20 @@ function AccountInfoSection() {
</OLFormGroup> </OLFormGroup>
) : null} ) : null}
{canUpdateEmail || canUpdateNames ? ( {canUpdateEmail || canUpdateNames ? (
<OLButton <OLFormGroup>
type="submit" <OLButton
variant="primary" type="submit"
form="account-info-form" variant="primary"
disabled={!isFormValid} form="account-info-form"
isLoading={isLoading} disabled={!isFormValid}
bs3Props={{ isLoading={isLoading}
loading: isLoading ? `${t('saving')}` : t('update'), bs3Props={{
}} loading: isLoading ? `${t('saving')}` : t('update'),
> }}
{t('update')} >
</OLButton> {t('update')}
</OLButton>
</OLFormGroup>
) : null} ) : null}
</form> </form>
</> </>
@ -188,7 +190,9 @@ function ReadOrWriteFormGroup({
onChange={handleChangeAndValidity} onChange={handleChangeAndValidity}
onInvalid={handleInvalid} onInvalid={handleInvalid}
/> />
{validationMessage && <FormText isError>{validationMessage}</FormText>} {validationMessage && (
<FormText type="error">{validationMessage}</FormText>
)}
</OLFormGroup> </OLFormGroup>
) )
} }

View file

@ -196,18 +196,20 @@ function PasswordForm() {
/> />
</OLFormGroup> </OLFormGroup>
) : null} ) : null}
<OLButton <OLFormGroup>
form="password-change-form" <OLButton
type="submit" form="password-change-form"
variant="primary" type="submit"
disabled={!isFormValid} variant="primary"
isLoading={isLoading} disabled={!isFormValid}
bs3Props={{ isLoading={isLoading}
loading: isLoading ? `${t('saving')}` : t('change'), bs3Props={{
}} loading: isLoading ? `${t('saving')}` : t('change'),
> }}
{t('change')} >
</OLButton> {t('change')}
</OLButton>
</OLFormGroup>
</form> </form>
) )
} }

View file

@ -91,9 +91,13 @@ export function DropdownToggle({ ...props }: DropdownToggleProps) {
return <BS5DropdownToggle {...props} /> return <BS5DropdownToggle {...props} />
} }
export function DropdownMenu({ as = 'ul', ...props }: DropdownMenuProps) { export const DropdownMenu = forwardRef<
return <BS5DropdownMenu as={as} role="menu" {...props} /> typeof BS5DropdownMenu,
} DropdownMenuProps
>(({ as = 'ul', ...props }, ref) => {
return <BS5DropdownMenu as={as} role="menu" {...props} ref={ref} />
})
DropdownMenu.displayName = 'DropdownMenu'
export function DropdownDivider({ as = 'li' }: DropdownDividerProps) { export function DropdownDivider({ as = 'li' }: DropdownDividerProps) {
return <BS5DropdownDivider as={as} /> return <BS5DropdownDivider as={as} />

View file

@ -36,7 +36,7 @@ const FormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
) )
} }
return <Form.Control className={className} {...props} /> return <Form.Control ref={ref} className={className} {...props} />
} }
) )
FormControl.displayName = 'FormControl' FormControl.displayName = 'FormControl'

View file

@ -0,0 +1,20 @@
import { Form } from 'react-bootstrap-5'
import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
import { ComponentProps } from 'react'
export type FormFeedbackProps = Pick<
ComponentProps<typeof Form.Control.Feedback>,
'type' | 'className' | 'children'
>
function FormFeedback(props: FormFeedbackProps) {
return (
<Form.Control.Feedback {...props}>
<FormText type={props.type === 'invalid' ? 'error' : 'success'}>
{props.children}
</FormText>
</Form.Control.Feedback>
)
}
export default FormFeedback

View file

@ -1,71 +1,49 @@
import { Form } from 'react-bootstrap-5' import { Form } from 'react-bootstrap-5'
import { MergeAndOverride } from '../../../../../../../types/utils'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
import classnames from 'classnames' import classnames from 'classnames'
type FormTextProps = MergeAndOverride< type TextType = 'default' | 'info' | 'success' | 'warning' | 'error'
React.ComponentProps<(typeof Form)['Text']>,
| {
isInfo?: boolean
isError?: never
isWarning?: never
isSuccess?: never
}
| {
isInfo?: never
isError?: boolean
isWarning?: never
isSuccess?: never
}
| {
isInfo?: never
isError?: never
isWarning?: boolean
isSuccess?: never
}
| {
isInfo?: never
isError?: never
isWarning?: never
isSuccess?: boolean
}
>
export const getFormTextColor = ({ export type FormTextProps = React.ComponentProps<typeof Form.Text> & {
isError, type?: TextType
isSuccess, }
isWarning,
}: { const typeClassMap: Partial<Record<TextType, string>> = {
isError?: boolean error: 'text-danger',
isSuccess?: boolean success: 'text-success',
isWarning?: boolean warning: 'text-warning',
}) => ({ }
'text-danger': isError,
'text-success': isSuccess, export const getFormTextClass = (type?: TextType) =>
'text-warning': isWarning, typeClassMap[type || 'default']
})
function FormTextIcon({ type }: { type?: TextType }) {
switch (type) {
case 'info':
return <MaterialIcon type="info" className="text-info" />
case 'success':
return <MaterialIcon type="check_circle" />
case 'warning':
return <MaterialIcon type="warning" />
case 'error':
return <MaterialIcon type="error" />
default:
return null
}
}
function FormText({ function FormText({
isInfo, type = 'default',
isError,
isWarning,
isSuccess,
children, children,
className, className,
...rest ...rest
}: FormTextProps) { }: FormTextProps) {
return ( return (
<Form.Text <Form.Text
className={classnames( className={classnames(className, getFormTextClass(type))}
className,
getFormTextColor({ isError, isSuccess, isWarning })
)}
{...rest} {...rest}
> >
{isInfo && <MaterialIcon type="info" className="text-info" />} <FormTextIcon type={type} />
{isError && <MaterialIcon type="error" />}
{isWarning && <MaterialIcon type="warning" />}
{isSuccess && <MaterialIcon type="check_circle" />}
{children} {children}
</Form.Text> </Form.Text>
) )

View file

@ -11,7 +11,7 @@ type OLFormCheckboxProps = React.ComponentProps<(typeof Form)['Check']> & {
function OLFormCheckbox(props: OLFormCheckboxProps) { function OLFormCheckbox(props: OLFormCheckboxProps) {
const { bs3Props, inputRef, ...rest } = props const { bs3Props, inputRef, ...rest } = props
const bs3FormLabelProps: React.ComponentProps<typeof BS3Checkbox> = { const bs3FormCheckboxProps: React.ComponentProps<typeof BS3Checkbox> = {
children: rest.label, children: rest.label,
checked: rest.checked, checked: rest.checked,
required: rest.required, required: rest.required,
@ -34,9 +34,9 @@ function OLFormCheckbox(props: OLFormCheckboxProps) {
<BootstrapVersionSwitcher <BootstrapVersionSwitcher
bs3={ bs3={
rest.type === 'radio' ? ( rest.type === 'radio' ? (
<BS3Radio {...bs3FormLabelProps} /> <BS3Radio {...bs3FormCheckboxProps} />
) : ( ) : (
<BS3Checkbox {...bs3FormLabelProps} /> <BS3Checkbox {...bs3FormCheckboxProps} />
) )
} }
bs5={<Form.Check ref={inputRef} {...rest} />} bs5={<Form.Check ref={inputRef} {...rest} />}

View file

@ -1,25 +1,29 @@
import { forwardRef } from 'react' import { forwardRef, ComponentProps } from 'react'
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5' import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
import BS3FormControl from '@/features/ui/components/bootstrap-3/form/form-control'
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control' import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
import BS3FormControl from '@/features/ui/components/bootstrap-3/form/form-control'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
type OLFormControlProps = React.ComponentProps<typeof FormControl> & { type OLFormControlProps = ComponentProps<typeof FormControl> & {
bs3Props?: Record<string, unknown> bs3Props?: Record<string, unknown>
'data-ol-dirty'?: unknown 'data-ol-dirty'?: unknown
} }
type BS3FormControlProps = ComponentProps<typeof BS3FormControl>
const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>( const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
(props, ref) => { (props, ref) => {
const { bs3Props, ...rest } = props const { bs3Props, ...rest } = props
let bs3FormControlProps: React.ComponentProps<typeof BS3FormControl> = { let bs3FormControlProps: BS3FormControlProps = {
componentClass: rest.as,
id: rest.id, id: rest.id,
name: rest.name, name: rest.name,
className: rest.className, className: rest.className,
style: rest.style, style: rest.style,
type: rest.type, type: rest.type,
value: rest.value, value: rest.value,
defaultValue: rest.defaultValue,
required: rest.required, required: rest.required,
disabled: rest.disabled, disabled: rest.disabled,
placeholder: rest.placeholder, placeholder: rest.placeholder,
@ -28,10 +32,10 @@ const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
autoFocus: rest.autoFocus, autoFocus: rest.autoFocus,
minLength: rest.minLength, minLength: rest.minLength,
maxLength: rest.maxLength, maxLength: rest.maxLength,
onChange: rest.onChange as (e: React.ChangeEvent<unknown>) => void, onChange: rest.onChange as BS3FormControlProps['onChange'],
onKeyDown: rest.onKeyDown as (e: React.KeyboardEvent<unknown>) => void, onKeyDown: rest.onKeyDown as BS3FormControlProps['onKeyDown'],
onFocus: rest.onFocus as (e: React.FocusEvent<unknown>) => void, onFocus: rest.onFocus as BS3FormControlProps['onFocus'],
onInvalid: rest.onInvalid as (e: React.InvalidEvent<unknown>) => void, onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'],
inputRef: (inputElement: HTMLInputElement) => { inputRef: (inputElement: HTMLInputElement) => {
if (typeof ref === 'function') { if (typeof ref === 'function') {
ref(inputElement) ref(inputElement)

View file

@ -0,0 +1,38 @@
import { Form } from 'react-bootstrap-5'
import {
HelpBlock as BS3HelpBlock,
HelpBlockProps as BS3HelpBlockProps,
} from 'react-bootstrap'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { ComponentProps } from 'react'
import classnames from 'classnames'
import FormFeedback from '@/features/ui/components/bootstrap-5/form/form-feedback'
type OLFormFeedbackProps = Pick<
ComponentProps<typeof Form.Control.Feedback>,
'type' | 'className' | 'children'
> & {
bs3Props?: Record<string, unknown>
}
function OLFormFeedback(props: OLFormFeedbackProps) {
const { bs3Props, children, ...bs5Props } = props
const bs3HelpBlockProps: BS3HelpBlockProps = {
className: classnames(
bs5Props.className,
bs5Props.type === 'invalid' ? 'invalid-only' : null
),
children,
...bs3Props,
}
return (
<BootstrapVersionSwitcher
bs3={<BS3HelpBlock {...bs3HelpBlockProps} />}
bs5={<FormFeedback {...bs5Props}>{children}</FormFeedback>}
/>
)
}
export default OLFormFeedback

View file

@ -1,5 +1,8 @@
import { FormGroupProps } from 'react-bootstrap-5' import { FormGroupProps } from 'react-bootstrap-5'
import { FormGroup as BS3FormGroup } from 'react-bootstrap' import {
FormGroup as BS3FormGroup,
FormGroupProps as BS3FormGroupProps,
} from 'react-bootstrap'
import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group' import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
@ -8,19 +11,19 @@ type OLFormGroupProps = FormGroupProps & {
} }
function OLFormGroup(props: OLFormGroupProps) { function OLFormGroup(props: OLFormGroupProps) {
const { bs3Props, ...rest } = props const { bs3Props, className, ...rest } = props
const bs3FormGroupProps: React.ComponentProps<typeof BS3FormGroup> = { const bs3FormGroupProps: BS3FormGroupProps = {
children: rest.children, children: rest.children,
controlId: rest.controlId, controlId: rest.controlId,
className: rest.className, className,
...bs3Props, ...bs3Props,
} }
return ( return (
<BootstrapVersionSwitcher <BootstrapVersionSwitcher
bs3={<BS3FormGroup {...bs3FormGroupProps} />} bs3={<BS3FormGroup {...bs3FormGroupProps} />}
bs5={<FormGroup {...rest} />} bs5={<FormGroup className={className} {...rest} />}
/> />
) )
} }

View file

@ -0,0 +1,45 @@
import { Form, FormSelectProps } from 'react-bootstrap-5'
import {
FormControl as BS3FormControl,
FormControlProps as BS3FormControlProps,
} from 'react-bootstrap'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
type OLFormSelectProps = FormSelectProps & {
bs3Props?: Record<string, unknown>
}
function OLFormSelect(props: OLFormSelectProps) {
const { bs3Props, ...bs5Props } = props
const bs3FormSelectProps: BS3FormControlProps = {
bsSize: bs5Props.size,
name: bs5Props.name,
value: bs5Props.value,
disabled: bs5Props.disabled,
onChange: bs5Props.onChange as BS3FormControlProps['onChange'],
required: bs5Props.required,
placeholder: bs5Props.placeholder,
className: bs5Props.className,
...bs3Props,
}
// Get all `aria-*` and `data-*` attributes
const extraProps = getAriaAndDataProps(bs5Props)
return (
<BootstrapVersionSwitcher
bs3={
<BS3FormControl
componentClass="select"
{...bs3FormSelectProps}
{...extraProps}
/>
}
bs5={<Form.Select {...bs5Props} />}
/>
)
}
export default OLFormSelect

View file

@ -1,11 +1,12 @@
import FormText, { import FormText, {
getFormTextColor, FormTextProps,
getFormTextClass,
} from '@/features/ui/components/bootstrap-5/form/form-text' } from '@/features/ui/components/bootstrap-5/form/form-text'
import PolymorphicComponent from '@/shared/components/polymorphic-component' import PolymorphicComponent from '@/shared/components/polymorphic-component'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import classnames from 'classnames' import classnames from 'classnames'
type OLFormTextProps = React.ComponentProps<typeof FormText> & { type OLFormTextProps = FormTextProps & {
bs3Props?: Record<string, unknown> bs3Props?: Record<string, unknown>
} }
@ -14,15 +15,7 @@ function OLFormText(props: OLFormTextProps) {
const bs3HelpBlockProps = { const bs3HelpBlockProps = {
children: rest.children, children: rest.children,
className: classnames( className: classnames('small', rest.className, getFormTextClass(rest.type)),
'small',
rest.className,
getFormTextColor({
isError: rest.isError,
isSuccess: rest.isSuccess,
isWarning: rest.isWarning,
})
),
as: 'span', as: 'span',
...bs3Props, ...bs3Props,
} as const satisfies React.ComponentProps<typeof PolymorphicComponent> } as const satisfies React.ComponentProps<typeof PolymorphicComponent>

View file

@ -1,27 +1,42 @@
import { Form } from 'react-bootstrap-5' import { Form } from 'react-bootstrap-5'
import { Form as BS3Form } from 'react-bootstrap' import { Form as BS3Form, FormProps as BS3FormProps } from 'react-bootstrap'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher' import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import { ComponentProps } from 'react'
import classnames from 'classnames'
import { getAriaAndDataProps } from '@/features/utils/bootstrap-5'
type OLFormProps = React.ComponentProps<typeof Form> & { type OLFormProps = ComponentProps<typeof Form> & {
bs3Props?: React.ComponentProps<typeof BS3Form> bs3Props?: ComponentProps<typeof BS3Form>
} }
function OLForm(props: OLFormProps) { function OLForm(props: OLFormProps) {
const { bs3Props, ...rest } = props const { bs3Props, ...rest } = props
const bs3FormProps: React.ComponentProps<typeof BS3Form> = { const bs3FormProps: BS3FormProps = {
componentClass: rest.as, componentClass: rest.as,
children: rest.children, children: rest.children,
id: rest.id, id: rest.id,
onSubmit: rest.onSubmit as React.FormEventHandler<BS3Form> | undefined, onSubmit: rest.onSubmit as BS3FormProps['onSubmit'],
className: rest.className, onClick: rest.onClick as BS3FormProps['onClick'],
name: rest.name,
noValidate: rest.noValidate,
role: rest.role, role: rest.role,
...bs3Props, ...bs3Props,
} }
const bs3ClassName = classnames(
rest.className,
rest.validated ? 'was-validated' : null
)
// Get all `aria-*` and `data-*` attributes
const extraProps = getAriaAndDataProps(rest)
return ( return (
<BootstrapVersionSwitcher <BootstrapVersionSwitcher
bs3={<BS3Form {...bs3FormProps} />} bs3={
<BS3Form className={bs3ClassName} {...bs3FormProps} {...extraProps} />
}
bs5={<Form {...rest} />} bs5={<Form {...rest} />}
/> />
) )

View file

@ -102,7 +102,7 @@ export function OLModalBody({ children, ...props }: OLModalBodyProps) {
const bs3ModalProps: BS3ModalBodyProps = { const bs3ModalProps: BS3ModalBodyProps = {
componentClass: bs5Props.as, componentClass: bs5Props.as,
bsClass: bs5Props.className, className: bs5Props.className,
} }
return ( return (
@ -118,7 +118,7 @@ export function OLModalFooter({ children, ...props }: OLModalFooterProps) {
const bs3ModalProps: BS3ModalFooterProps = { const bs3ModalProps: BS3ModalFooterProps = {
componentClass: bs5Props.as, componentClass: bs5Props.as,
bsClass: bs5Props.className, className: bs5Props.className,
} }
return ( return (

View file

@ -21,7 +21,7 @@ export type DropdownProps = {
export type DropdownItemProps = PropsWithChildren<{ export type DropdownItemProps = PropsWithChildren<{
active?: boolean active?: boolean
as?: ElementType as?: ElementType
description?: string description?: ReactNode
disabled?: boolean disabled?: boolean
eventKey?: string | number eventKey?: string | number
href?: string href?: string
@ -32,6 +32,7 @@ export type DropdownItemProps = PropsWithChildren<{
className?: string className?: string
role?: string role?: string
tabIndex?: number tabIndex?: number
target?: string
}> }>
export type DropdownToggleProps = PropsWithChildren<{ export type DropdownToggleProps = PropsWithChildren<{

View file

@ -1,13 +1,15 @@
import importOverleafModules from '../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../macros/import-overleaf-module.macro'
import { JSXElementConstructor, useCallback, useState } from 'react' import { JSXElementConstructor, useCallback, useState } from 'react'
import { HelpSuggestionSearchProvider } from '../../../../modules/support/frontend/js/context/help-suggestion-search-context'
const [contactUsModalModules] = importOverleafModules('contactUsModal') const [contactUsModalModules] = importOverleafModules('contactUsModal')
const ContactUsModal: JSXElementConstructor<{ const ContactUsModal: JSXElementConstructor<{
show: boolean show: boolean
handleHide: () => void handleHide: () => void
autofillProjectUrl: boolean
}> = contactUsModalModules?.import.default }> = contactUsModalModules?.import.default
export const useContactUsModal = () => { export const useContactUsModal = (options = { autofillProjectUrl: true }) => {
const [show, setShow] = useState(false) const [show, setShow] = useState(false)
const hideModal = useCallback((event?: Event) => { const hideModal = useCallback((event?: Event) => {
@ -21,7 +23,13 @@ export const useContactUsModal = () => {
}, []) }, [])
const modal = ContactUsModal && ( const modal = ContactUsModal && (
<ContactUsModal show={show} handleHide={hideModal} /> <HelpSuggestionSearchProvider>
<ContactUsModal
show={show}
handleHide={hideModal}
autofillProjectUrl={options.autofillProjectUrl}
/>
</HelpSuggestionSearchProvider>
) )
return { modal, hideModal, showModal } return { modal, hideModal, showModal }

View file

@ -1,37 +0,0 @@
import { useState } from 'react'
import useFetchMock from './hooks/use-fetch-mock'
import ContactUsModal from '../../modules/support/frontend/js/components/contact-us-modal'
import { ScopeDecorator } from './decorators/scope'
export const Generic = () => {
const [show, setShow] = useState(true)
const handleHide = () => setShow(false)
useFetchMock(fetchMock => {
fetchMock.post('/support', { status: 200 }, { delay: 1000 })
})
return <ContactUsModal show={show} handleHide={handleHide} />
}
export const RequestError = args => {
useFetchMock(fetchMock => {
fetchMock.post('/support', { status: 404 }, { delay: 250 })
})
return <ContactUsModal {...args} />
}
export default {
title: 'Shared / Modals / Contact Us',
component: ContactUsModal,
args: {
show: true,
handleHide: () => {},
},
argTypes: {
handleHide: { action: 'close modal' },
},
decorators: [ScopeDecorator],
}

View file

@ -0,0 +1,93 @@
import { ComponentProps } from 'react'
import useFetchMock from './hooks/use-fetch-mock'
import ContactUsModal from '../../modules/support/frontend/js/components/contact-us-modal'
import { ScopeDecorator } from './decorators/scope'
import { StoryObj } from '@storybook/react'
import { FixedHelpSuggestionSearchProvider } from '../../modules/support/test/frontend/helpers/contact-us-modal-base-tests'
type Story = StoryObj<typeof ContactUsModal>
type ContactUsModalProps = ComponentProps<typeof ContactUsModal>
function bootstrap3Story(render: Story['render']): Story {
return {
render,
decorators: [
story => {
return ScopeDecorator(story)
},
],
}
}
function bootstrap5Story(render: Story['render']): Story {
return {
render,
decorators: [
story => {
return ScopeDecorator(story, undefined, {
'ol-bootstrapVersion': 5,
})
},
],
parameters: {
bootstrap5: true,
},
}
}
function GenericContactUsModal(args: ContactUsModalProps) {
useFetchMock(fetchMock => {
fetchMock.post('/support', { status: 200 }, { delay: 1000 })
})
return (
<FixedHelpSuggestionSearchProvider>
<ContactUsModal {...args} />
</FixedHelpSuggestionSearchProvider>
)
}
export const Generic: Story = bootstrap3Story(args => (
<GenericContactUsModal {...args} />
))
export const GenericBootstrap5: Story = bootstrap5Story(args => (
<GenericContactUsModal {...args} />
))
const ContactUsModalWithRequestError = (args: ContactUsModalProps) => {
useFetchMock(fetchMock => {
fetchMock.post('/support', { status: 404 }, { delay: 250 })
})
return (
<FixedHelpSuggestionSearchProvider>
<ContactUsModal {...args} />
</FixedHelpSuggestionSearchProvider>
)
}
const renderContactUsModalWithRequestError = (args: ContactUsModalProps) => (
<ContactUsModalWithRequestError {...args} />
)
export const RequestError: Story = bootstrap3Story(
renderContactUsModalWithRequestError
)
export const RequestErrorBootstrap5: Story = bootstrap5Story(
renderContactUsModalWithRequestError
)
export default {
title: 'Shared / Modals / Contact Us',
component: ContactUsModal,
args: {
show: true,
handleHide: () => {},
autofillProjectUrl: true,
},
argTypes: {
handleHide: { action: 'close modal' },
},
}

View file

@ -4,6 +4,7 @@ import FormGroup from '@/features/ui/components/bootstrap-5/form/form-group'
import FormText from '@/features/ui/components/bootstrap-5/form/form-text' import FormText from '@/features/ui/components/bootstrap-5/form/form-text'
import FormControl from '@/features/ui/components/bootstrap-5/form/form-control' import FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
import MaterialIcon from '@/shared/components/material-icon' import MaterialIcon from '@/shared/components/material-icon'
import FormFeedback from '@/features/ui/components/bootstrap-5/form/form-feedback'
const meta: Meta<React.ComponentProps<typeof FormControl>> = { const meta: Meta<React.ComponentProps<typeof FormControl>> = {
title: 'Shared / Components / Bootstrap 5 / Form / Input', title: 'Shared / Components / Bootstrap 5 / Form / Input',
@ -57,7 +58,7 @@ export const Info: Story = {
size="lg" size="lg"
{...args} {...args}
/> />
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -67,7 +68,7 @@ export const Info: Story = {
defaultValue="Regular input" defaultValue="Regular input"
{...args} {...args}
/> />
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -78,7 +79,7 @@ export const Info: Story = {
size="sm" size="sm"
{...args} {...args}
/> />
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
</> </>
) )
@ -98,7 +99,7 @@ export const Error: Story = {
isInvalid isInvalid
{...args} {...args}
/> />
<FormText isError>Error</FormText> <FormFeedback type="invalid">Error</FormFeedback>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -109,7 +110,7 @@ export const Error: Story = {
isInvalid isInvalid
{...args} {...args}
/> />
<FormText isError>Error</FormText> <FormFeedback type="invalid">Error</FormFeedback>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -121,7 +122,7 @@ export const Error: Story = {
isInvalid isInvalid
{...args} {...args}
/> />
<FormText isError>Error</FormText> <FormFeedback type="invalid">Error</FormFeedback>
</FormGroup> </FormGroup>
</> </>
) )
@ -140,7 +141,7 @@ export const Warning: Story = {
size="lg" size="lg"
{...args} {...args}
/> />
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -150,7 +151,7 @@ export const Warning: Story = {
defaultValue="Regular input" defaultValue="Regular input"
{...args} {...args}
/> />
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -161,7 +162,7 @@ export const Warning: Story = {
size="sm" size="sm"
{...args} {...args}
/> />
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
</> </>
) )
@ -180,7 +181,7 @@ export const Success: Story = {
size="lg" size="lg"
{...args} {...args}
/> />
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -190,7 +191,7 @@ export const Success: Story = {
defaultValue="Regular input" defaultValue="Regular input"
{...args} {...args}
/> />
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -201,7 +202,7 @@ export const Success: Story = {
size="sm" size="sm"
{...args} {...args}
/> />
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
</> </>
) )

View file

@ -70,7 +70,7 @@ export const Info: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -81,7 +81,7 @@ export const Info: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -92,7 +92,7 @@ export const Info: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
</> </>
) )
@ -111,7 +111,7 @@ export const Error: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isError>Error</FormText> <FormText type="error">Error</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -122,7 +122,7 @@ export const Error: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isError>Error</FormText> <FormText type="error">Error</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -133,7 +133,7 @@ export const Error: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isError>Error</FormText> <FormText type="error">Error</FormText>
</FormGroup> </FormGroup>
</> </>
) )
@ -152,7 +152,7 @@ export const Warning: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -163,7 +163,7 @@ export const Warning: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -174,7 +174,7 @@ export const Warning: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
</> </>
) )
@ -193,7 +193,7 @@ export const Success: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -204,7 +204,7 @@ export const Success: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -215,7 +215,7 @@ export const Success: Story = {
<option value="2">Two</option> <option value="2">Two</option>
<option value="3">Three</option> <option value="3">Three</option>
</Form.Select> </Form.Select>
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
</> </>
) )

View file

@ -67,7 +67,7 @@ export const Info: Story = {
size="lg" size="lg"
{...args} {...args}
/> />
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -78,7 +78,7 @@ export const Info: Story = {
defaultValue="Regular input" defaultValue="Regular input"
{...args} {...args}
/> />
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -90,7 +90,7 @@ export const Info: Story = {
size="sm" size="sm"
{...args} {...args}
/> />
<FormText isInfo>Info</FormText> <FormText type="info">Info</FormText>
</FormGroup> </FormGroup>
</> </>
) )
@ -111,7 +111,7 @@ export const Error: Story = {
isInvalid isInvalid
{...args} {...args}
/> />
<FormText isError>Error</FormText> <FormText type="error">Error</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -123,7 +123,7 @@ export const Error: Story = {
isInvalid isInvalid
{...args} {...args}
/> />
<FormText isError>Error</FormText> <FormText type="error">Error</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -136,7 +136,7 @@ export const Error: Story = {
isInvalid isInvalid
{...args} {...args}
/> />
<FormText isError>Error</FormText> <FormText type="error">Error</FormText>
</FormGroup> </FormGroup>
</> </>
) )
@ -156,7 +156,7 @@ export const Warning: Story = {
size="lg" size="lg"
{...args} {...args}
/> />
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -167,7 +167,7 @@ export const Warning: Story = {
defaultValue="Regular input" defaultValue="Regular input"
{...args} {...args}
/> />
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -179,7 +179,7 @@ export const Warning: Story = {
size="sm" size="sm"
{...args} {...args}
/> />
<FormText isWarning>Warning</FormText> <FormText type="warning">Warning</FormText>
</FormGroup> </FormGroup>
</> </>
) )
@ -199,7 +199,7 @@ export const Success: Story = {
size="lg" size="lg"
{...args} {...args}
/> />
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-2"> <FormGroup controlId="id-2">
@ -210,7 +210,7 @@ export const Success: Story = {
defaultValue="Regular input" defaultValue="Regular input"
{...args} {...args}
/> />
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
<hr /> <hr />
<FormGroup controlId="id-3"> <FormGroup controlId="id-3">
@ -222,7 +222,7 @@ export const Success: Story = {
size="sm" size="sm"
{...args} {...args}
/> />
<FormText isSuccess>Success</FormText> <FormText type="success">Success</FormText>
</FormGroup> </FormGroup>
</> </>
) )

View file

@ -120,6 +120,7 @@ $form-check-input-disabled-opacity: 1;
// form-feedback-variables // form-feedback-variables
$form-feedback-invalid-color: $bg-danger-01; $form-feedback-invalid-color: $bg-danger-01;
$form-feedback-icon-invalid: null; $form-feedback-icon-invalid: null;
$form-feedback-margin-top: 0; // Our feedback component wraps Form.Text, which takes care of top margin
// form-validation-colors // form-validation-colors
$form-invalid-color: $form-feedback-invalid-color; $form-invalid-color: $form-feedback-invalid-color;

View file

@ -3,3 +3,6 @@ $footer-height: 50px;
// Header // Header
$header-height: 68px; $header-height: 68px;
// Forms
$form-group-margin-bottom: $spacing-06;

View file

@ -90,7 +90,7 @@
} }
.form-group { .form-group {
margin-bottom: var(--spacing-06); margin-bottom: $form-group-margin-bottom;
} }
.form-control-wrapper { .form-control-wrapper {

View file

@ -85,12 +85,3 @@
.git-bridge-optional-tokens-actions { .git-bridge-optional-tokens-actions {
margin-top: var(--spacing-05); margin-top: var(--spacing-05);
} }
// Contact us modal
.contact-us-modal-textarea {
height: 120px;
}
.modal-form-messages .notification {
margin-bottom: var(--spacing-05);
}

View file

@ -32,3 +32,9 @@ $is-overleaf-light: false;
// Page layout that isn't related to a particular component or page // Page layout that isn't related to a particular component or page
@import 'base/layout'; @import 'base/layout';
// Modals
@import 'modals/all';
// Pages custom style
@import 'pages/all';

View file

@ -0,0 +1 @@
@import 'contact-us-modal';

View file

@ -0,0 +1,82 @@
.contact-us-modal-textarea {
height: 120px;
}
.modal-form-messages .notification {
margin-bottom: var(--spacing-05);
}
.contact-suggestions {
@include body-sm;
margin: 0 calc(-1 * var(--bs-modal-padding)) var(--spacing-05);
padding: var(--spacing-05) 0;
color: var(--content-secondary);
background-color: var(--bg-light-secondary);
border-top: solid 1px var(--border-primary-dark);
border-bottom: solid 1px var(--border-primary-dark);
}
.contact-suggestion-label {
margin-bottom: var(--spacing-05);
padding: 0 var(--spacing-07);
}
.contact-suggestion-list {
padding-left: 0;
list-style: none;
background-color: var(--white);
border-top: solid 1px var(--border-primary-dark);
border-bottom: solid 1px var(--border-primary-dark);
margin: 0;
li:last-child .contact-suggestion-list-item {
border-bottom: none;
}
}
.contact-suggestion-list-item {
display: flex;
justify-content: space-between;
color: var(--content-secondary);
padding: var(--spacing-05) var(--spacing-07);
border-bottom: solid 1px var(--border-divider);
cursor: pointer;
text-decoration: none;
&:hover,
&:focus {
background-color: var(--bg-light-secondary);
span {
text-decoration: underline;
}
.fa {
color: inherit;
text-decoration: none;
}
}
.fa {
color: var(--neutral-30);
}
}
.contact-suggestions-dropdown {
width: calc(100% - 2 * var(--bs-modal-padding));
.dropdown-header {
@include body-sm;
padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
}
.dropdown-item {
white-space: normal;
}
.form-group + & {
margin-top: $spacing-02 - $form-group-margin-bottom;
}
}

View file

@ -357,6 +357,15 @@ input[type='checkbox'],
margin-top: 5px; margin-top: 5px;
margin-bottom: 10px; margin-bottom: 10px;
color: lighten(@text-color, 25%); // lighten the text some for contrast color: lighten(@text-color, 25%); // lighten the text some for contrast
// Hide help blocks used as validation messages by default
&.invalid-only {
display: none;
.has-error & {
display: block;
}
}
} }
// Inline forms // Inline forms

View file

@ -1455,6 +1455,8 @@
"please_enter_email": "Please enter your email address", "please_enter_email": "Please enter your email address",
"please_get_in_touch": "Please get in touch", "please_get_in_touch": "Please get in touch",
"please_link_before_making_primary": "Please confirm your email by linking to your institutional account before making it the primary email.", "please_link_before_making_primary": "Please confirm your email by linking to your institutional account before making it the primary email.",
"please_provide_a_message": "Please provide a message",
"please_provide_a_subject": "Please provide a subject",
"please_reconfirm_institutional_email": "Please take a moment to confirm your institutional email address or <0>remove it</0> from your account.", "please_reconfirm_institutional_email": "Please take a moment to confirm your institutional email address or <0>remove it</0> from your account.",
"please_reconfirm_your_affiliation_before_making_this_primary": "Please confirm your affiliation before making this the primary.", "please_reconfirm_your_affiliation_before_making_this_primary": "Please confirm your affiliation before making this the primary.",
"please_refresh": "Please refresh the page to continue.", "please_refresh": "Please refresh the page to continue.",