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

View file

@ -24,10 +24,12 @@ export function formatWikiHit(hit) {
const pagePath = hit.kb ? 'how-to' : 'latex'
let pageAnchor = ''
let pageName = hit._highlightResult.pageName.value
if (hit.sectionName) {
pageAnchor = `#${hit.sectionName.replace(/\s/g, '_')}`
pageName += ' - ' + hit.sectionName
const rawPageName = hit._highlightResult.pageName.value
const sectionName = hit.sectionName
let pageName = rawPageName
if (sectionName) {
pageAnchor = `#${sectionName.replace(/\s/g, '_')}`
pageName += ' - ' + sectionName
}
const body = hit._highlightResult.content.value
@ -37,5 +39,5 @@ export function formatWikiHit(hit) {
.join('\n...\n')
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')
iconEl.className = 'fa fa-angle-right'
iconEl.setAttribute('aria-hidden', 'true')
linkEl.append(contentEl)
linkEl.append(iconEl)
resultsEl.append(liEl)
}

View file

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

View file

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

View file

@ -91,9 +91,13 @@ export function DropdownToggle({ ...props }: DropdownToggleProps) {
return <BS5DropdownToggle {...props} />
}
export function DropdownMenu({ as = 'ul', ...props }: DropdownMenuProps) {
return <BS5DropdownMenu as={as} role="menu" {...props} />
}
export const DropdownMenu = forwardRef<
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) {
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'

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 { MergeAndOverride } from '../../../../../../../types/utils'
import MaterialIcon from '@/shared/components/material-icon'
import classnames from 'classnames'
type FormTextProps = MergeAndOverride<
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
}
>
type TextType = 'default' | 'info' | 'success' | 'warning' | 'error'
export const getFormTextColor = ({
isError,
isSuccess,
isWarning,
}: {
isError?: boolean
isSuccess?: boolean
isWarning?: boolean
}) => ({
'text-danger': isError,
'text-success': isSuccess,
'text-warning': isWarning,
})
export type FormTextProps = React.ComponentProps<typeof Form.Text> & {
type?: TextType
}
const typeClassMap: Partial<Record<TextType, string>> = {
error: 'text-danger',
success: 'text-success',
warning: 'text-warning',
}
export const getFormTextClass = (type?: TextType) =>
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({
isInfo,
isError,
isWarning,
isSuccess,
type = 'default',
children,
className,
...rest
}: FormTextProps) {
return (
<Form.Text
className={classnames(
className,
getFormTextColor({ isError, isSuccess, isWarning })
)}
className={classnames(className, getFormTextClass(type))}
{...rest}
>
{isInfo && <MaterialIcon type="info" className="text-info" />}
{isError && <MaterialIcon type="error" />}
{isWarning && <MaterialIcon type="warning" />}
{isSuccess && <MaterialIcon type="check_circle" />}
<FormTextIcon type={type} />
{children}
</Form.Text>
)

View file

@ -11,7 +11,7 @@ type OLFormCheckboxProps = React.ComponentProps<(typeof Form)['Check']> & {
function OLFormCheckbox(props: OLFormCheckboxProps) {
const { bs3Props, inputRef, ...rest } = props
const bs3FormLabelProps: React.ComponentProps<typeof BS3Checkbox> = {
const bs3FormCheckboxProps: React.ComponentProps<typeof BS3Checkbox> = {
children: rest.label,
checked: rest.checked,
required: rest.required,
@ -34,9 +34,9 @@ function OLFormCheckbox(props: OLFormCheckboxProps) {
<BootstrapVersionSwitcher
bs3={
rest.type === 'radio' ? (
<BS3Radio {...bs3FormLabelProps} />
<BS3Radio {...bs3FormCheckboxProps} />
) : (
<BS3Checkbox {...bs3FormLabelProps} />
<BS3Checkbox {...bs3FormCheckboxProps} />
)
}
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 BS3FormControl from '@/features/ui/components/bootstrap-3/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'
type OLFormControlProps = React.ComponentProps<typeof FormControl> & {
type OLFormControlProps = ComponentProps<typeof FormControl> & {
bs3Props?: Record<string, unknown>
'data-ol-dirty'?: unknown
}
type BS3FormControlProps = ComponentProps<typeof BS3FormControl>
const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
(props, ref) => {
const { bs3Props, ...rest } = props
let bs3FormControlProps: React.ComponentProps<typeof BS3FormControl> = {
let bs3FormControlProps: BS3FormControlProps = {
componentClass: rest.as,
id: rest.id,
name: rest.name,
className: rest.className,
style: rest.style,
type: rest.type,
value: rest.value,
defaultValue: rest.defaultValue,
required: rest.required,
disabled: rest.disabled,
placeholder: rest.placeholder,
@ -28,10 +32,10 @@ const OLFormControl = forwardRef<HTMLInputElement, OLFormControlProps>(
autoFocus: rest.autoFocus,
minLength: rest.minLength,
maxLength: rest.maxLength,
onChange: rest.onChange as (e: React.ChangeEvent<unknown>) => void,
onKeyDown: rest.onKeyDown as (e: React.KeyboardEvent<unknown>) => void,
onFocus: rest.onFocus as (e: React.FocusEvent<unknown>) => void,
onInvalid: rest.onInvalid as (e: React.InvalidEvent<unknown>) => void,
onChange: rest.onChange as BS3FormControlProps['onChange'],
onKeyDown: rest.onKeyDown as BS3FormControlProps['onKeyDown'],
onFocus: rest.onFocus as BS3FormControlProps['onFocus'],
onInvalid: rest.onInvalid as BS3FormControlProps['onInvalid'],
inputRef: (inputElement: HTMLInputElement) => {
if (typeof ref === 'function') {
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 { 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 BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
@ -8,19 +11,19 @@ type OLFormGroupProps = FormGroupProps & {
}
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,
controlId: rest.controlId,
className: rest.className,
className,
...bs3Props,
}
return (
<BootstrapVersionSwitcher
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, {
getFormTextColor,
FormTextProps,
getFormTextClass,
} from '@/features/ui/components/bootstrap-5/form/form-text'
import PolymorphicComponent from '@/shared/components/polymorphic-component'
import BootstrapVersionSwitcher from '@/features/ui/components/bootstrap-5/bootstrap-version-switcher'
import classnames from 'classnames'
type OLFormTextProps = React.ComponentProps<typeof FormText> & {
type OLFormTextProps = FormTextProps & {
bs3Props?: Record<string, unknown>
}
@ -14,15 +15,7 @@ function OLFormText(props: OLFormTextProps) {
const bs3HelpBlockProps = {
children: rest.children,
className: classnames(
'small',
rest.className,
getFormTextColor({
isError: rest.isError,
isSuccess: rest.isSuccess,
isWarning: rest.isWarning,
})
),
className: classnames('small', rest.className, getFormTextClass(rest.type)),
as: 'span',
...bs3Props,
} as const satisfies React.ComponentProps<typeof PolymorphicComponent>

View file

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

View file

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

View file

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

View file

@ -1,13 +1,15 @@
import importOverleafModules from '../../../macros/import-overleaf-module.macro'
import { JSXElementConstructor, useCallback, useState } from 'react'
import { HelpSuggestionSearchProvider } from '../../../../modules/support/frontend/js/context/help-suggestion-search-context'
const [contactUsModalModules] = importOverleafModules('contactUsModal')
const ContactUsModal: JSXElementConstructor<{
show: boolean
handleHide: () => void
autofillProjectUrl: boolean
}> = contactUsModalModules?.import.default
export const useContactUsModal = () => {
export const useContactUsModal = (options = { autofillProjectUrl: true }) => {
const [show, setShow] = useState(false)
const hideModal = useCallback((event?: Event) => {
@ -21,7 +23,13 @@ export const useContactUsModal = () => {
}, [])
const modal = ContactUsModal && (
<ContactUsModal show={show} handleHide={hideModal} />
<HelpSuggestionSearchProvider>
<ContactUsModal
show={show}
handleHide={hideModal}
autofillProjectUrl={options.autofillProjectUrl}
/>
</HelpSuggestionSearchProvider>
)
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 FormControl from '@/features/ui/components/bootstrap-5/form/form-control'
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>> = {
title: 'Shared / Components / Bootstrap 5 / Form / Input',
@ -57,7 +58,7 @@ export const Info: Story = {
size="lg"
{...args}
/>
<FormText isInfo>Info</FormText>
<FormText type="info">Info</FormText>
</FormGroup>
<hr />
<FormGroup controlId="id-2">
@ -67,7 +68,7 @@ export const Info: Story = {
defaultValue="Regular input"
{...args}
/>
<FormText isInfo>Info</FormText>
<FormText type="info">Info</FormText>
</FormGroup>
<hr />
<FormGroup controlId="id-3">
@ -78,7 +79,7 @@ export const Info: Story = {
size="sm"
{...args}
/>
<FormText isInfo>Info</FormText>
<FormText type="info">Info</FormText>
</FormGroup>
</>
)
@ -98,7 +99,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
<FormText isError>Error</FormText>
<FormFeedback type="invalid">Error</FormFeedback>
</FormGroup>
<hr />
<FormGroup controlId="id-2">
@ -109,7 +110,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
<FormText isError>Error</FormText>
<FormFeedback type="invalid">Error</FormFeedback>
</FormGroup>
<hr />
<FormGroup controlId="id-3">
@ -121,7 +122,7 @@ export const Error: Story = {
isInvalid
{...args}
/>
<FormText isError>Error</FormText>
<FormFeedback type="invalid">Error</FormFeedback>
</FormGroup>
</>
)
@ -140,7 +141,7 @@ export const Warning: Story = {
size="lg"
{...args}
/>
<FormText isWarning>Warning</FormText>
<FormText type="warning">Warning</FormText>
</FormGroup>
<hr />
<FormGroup controlId="id-2">
@ -150,7 +151,7 @@ export const Warning: Story = {
defaultValue="Regular input"
{...args}
/>
<FormText isWarning>Warning</FormText>
<FormText type="warning">Warning</FormText>
</FormGroup>
<hr />
<FormGroup controlId="id-3">
@ -161,7 +162,7 @@ export const Warning: Story = {
size="sm"
{...args}
/>
<FormText isWarning>Warning</FormText>
<FormText type="warning">Warning</FormText>
</FormGroup>
</>
)
@ -180,7 +181,7 @@ export const Success: Story = {
size="lg"
{...args}
/>
<FormText isSuccess>Success</FormText>
<FormText type="success">Success</FormText>
</FormGroup>
<hr />
<FormGroup controlId="id-2">
@ -190,7 +191,7 @@ export const Success: Story = {
defaultValue="Regular input"
{...args}
/>
<FormText isSuccess>Success</FormText>
<FormText type="success">Success</FormText>
</FormGroup>
<hr />
<FormGroup controlId="id-3">
@ -201,7 +202,7 @@ export const Success: Story = {
size="sm"
{...args}
/>
<FormText isSuccess>Success</FormText>
<FormText type="success">Success</FormText>
</FormGroup>
</>
)

View file

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

View file

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

View file

@ -120,6 +120,7 @@ $form-check-input-disabled-opacity: 1;
// form-feedback-variables
$form-feedback-invalid-color: $bg-danger-01;
$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-invalid-color: $form-feedback-invalid-color;

View file

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

View file

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

View file

@ -85,12 +85,3 @@
.git-bridge-optional-tokens-actions {
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
@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-bottom: 10px;
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

View file

@ -1455,6 +1455,8 @@
"please_enter_email": "Please enter your email address",
"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_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_your_affiliation_before_making_this_primary": "Please confirm your affiliation before making this the primary.",
"please_refresh": "Please refresh the page to continue.",