Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ The `MentionsInput` component supports the following props:
| onKeyDown | function (event) | empty function | A callback that is invoked when the user presses a key in the mentions input |
| singleLine | boolean | `false` | Renders a single line text input instead of a textarea, if set to `true` |
| autoResize | boolean | `false` | When `true`, resizes the textarea to match its scroll height after each input change (ignored when `singleLine` is `true`) |
| anchorMode | `'caret' \| 'left'` | `'caret'` | Controls whether the overlay follows the caret (`'caret'`) or pins to the control’s leading edge (`'left'`) |
| onMentionBlur | function (event, clickedSuggestion) | `undefined` | Receives an extra `clickedSuggestion` flag when focus left via the suggestions list |
| suggestionsPortalHost | DOM Element | undefined | Render suggestions into the DOM in the supplied host element. |
| inputRef | React ref | undefined | Accepts a React ref to forward to the underlying input element |
Expand Down
2 changes: 2 additions & 0 deletions demo/src/examples/Examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import AllowSpaceInQuery from './AllowSpaceInQuery'
import AlphabetRegexTrigger from './AlphabetRegexTrigger'
import MentionSelection from './MentionSelection'
import AutoResize from './AutoResize'
import LeftAnchored from './LeftAnchored'

const users = [
{
Expand Down Expand Up @@ -91,6 +92,7 @@ export default function Examples() {
<Emojis data={users} onAdd={(addParams) => console.log('onAdd', addParams)} />
<SuggestionPortal data={users} />
<CustomSuggestionsContainer data={users} />
<LeftAnchored data={users} />
</div>
)
}
31 changes: 31 additions & 0 deletions demo/src/examples/LeftAnchored.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useState } from 'react'

import { Mention, MentionsInput } from '../../../src'
import type { MentionDataItem, MentionsInputChangeEvent } from '../../../src'
import ExampleCard from './ExampleCard'

export default function LeftAnchored({ data }: { data: MentionDataItem[] }) {
const [value, setValue] = useState('')

const onMentionsChange = ({ value: nextValue }: MentionsInputChangeEvent) => {
setValue(nextValue)
}

return (
<ExampleCard
title="Left anchored suggestions"
description="Pop the overlay from the input edge instead of the caret, ideal for wide inputs."
>
<MentionsInput
anchorMode="left"
value={value}
onMentionsChange={onMentionsChange}
className="mentions"
placeholder="Start typing '@' to mention"
a11ySuggestionsListLabel="Suggested mentions"
>
<Mention data={data} />
</MentionsInput>
</ExampleCard>
)
}
16 changes: 8 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-mentions-ts",
"private": false,
"version": "5.4.3",
"version": "5.4.4",
"description": "A React component that enables Facebook/Twitter-style @mentions and tagging in textarea inputs with full TypeScript support.",
"type": "module",
"main": "./dist/index.cjs",
Expand Down Expand Up @@ -97,19 +97,19 @@
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^30.0.0",
"@types/node": "^24.9.2",
"@types/node": "^24.10.0",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@vitejs/plugin-react": "^5.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"eslint": "^9.38.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-code-complete": "^1.1.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-package-json": "^0.59.0",
"eslint-plugin-package-json": "^0.64.0",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-react": "^7.37.5",
Expand All @@ -125,19 +125,19 @@
"jest-environment-jsdom": "^30.2.0",
"jiti": "^2.6.1",
"jsonc-eslint-parser": "^2.4.1",
"knip": "^5.66.4",
"knip": "^5.67.1",
"prettier": "^3.6.2",
"publint": "^0.3.15",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"rimraf": "^6.0.1",
"rimraf": "^6.1.0",
"tailwind-merge": "^3.3.1",
"ts-jest": "^29.4.5",
"ts-node": "^10.9.2",
"tsup": "^8.5.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.2",
"vite": "^7.1.12"
"typescript-eslint": "^8.46.3",
"vite": "^7.2.1"
},
"peerDependencies": {
"class-variance-authority": ">=0.6.0",
Expand Down
128 changes: 126 additions & 2 deletions src/MentionsInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@
})

it('should load suggestions from async data providers.', async () => {
const asyncData = jest.fn(async (query: string) => {

Check failure on line 374 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

'query' is defined but never used. Allowed unused args must match /^_/u
await Promise.resolve()
return [
{ id: 'async-one', display: 'Async One' },
Expand Down Expand Up @@ -661,7 +661,7 @@
</MentionsInput>
)

const computed = window.getComputedStyle(combobox)

Check failure on line 664 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Prefer `globalThis` over `window`
const borderTop = Number.parseFloat(computed.borderTopWidth || '0')
const borderBottom = Number.parseFloat(computed.borderBottomWidth || '0')
expect(Number.parseFloat(combobox.style.height)).toBe(scrollHeight + borderTop + borderBottom)
Expand Down Expand Up @@ -693,7 +693,7 @@
</MentionsInput>
)

const computed = window.getComputedStyle(combobox)

Check failure on line 696 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Prefer `globalThis` over `window`
const borderTop = Number.parseFloat(computed.borderTopWidth || '0')
const borderBottom = Number.parseFloat(computed.borderBottomWidth || '0')
expect(Number.parseFloat(combobox.style.height)).toBe(scrollHeight + borderTop + borderBottom)
Expand Down Expand Up @@ -740,7 +740,7 @@
})

await waitFor(() => {
const computed = window.getComputedStyle(combobox)

Check failure on line 743 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Prefer `globalThis` over `window`
const borderTop = Number.parseFloat(computed.borderTopWidth || '0')
const borderBottom = Number.parseFloat(computed.borderBottomWidth || '0')
expect(Number.parseFloat(combobox.style.height)).toBe(
Expand All @@ -765,7 +765,7 @@

it('adds border widths to the measured height', () => {
const onMentionsChange = jest.fn()
const getComputedStyleSpy = jest.spyOn(window, 'getComputedStyle').mockReturnValue({

Check failure on line 768 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Prefer `globalThis` over `window`
borderTopWidth: '4px',
borderBottomWidth: '6px',
} as unknown as CSSStyleDeclaration)
Expand Down Expand Up @@ -824,7 +824,7 @@
</MentionsInput>
)

const computed = window.getComputedStyle(textarea)

Check failure on line 827 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Prefer `globalThis` over `window`
const borderTop = Number.parseFloat(computed.borderTopWidth || '0')
const borderBottom = Number.parseFloat(computed.borderBottomWidth || '0')
expect(Number.parseFloat(textarea.style.height)).toBe(scrollHeight + borderTop + borderBottom)
Expand Down Expand Up @@ -1396,7 +1396,7 @@
const textarea = screen.getByRole('combobox')
fireEvent.focus(textarea)

const initialIndex = textarea.value.indexOf('First') + 1

Check failure on line 1399 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Invalid operand for a '+' operation. Operands must each be a number or string. Got `any`
expect(initialIndex).toBeGreaterThan(0)
textarea.setSelectionRange(initialIndex, initialIndex)
fireEvent.select(textarea)
Expand All @@ -1412,7 +1412,7 @@
</MentionsInput>
)

const updatedIndex = textarea.value.indexOf('Second') + 1

Check failure on line 1415 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Invalid operand for a '+' operation. Operands must each be a number or string. Got `any`
expect(updatedIndex).toBeGreaterThan(0)
textarea.setSelectionRange(updatedIndex, updatedIndex)
fireEvent.select(textarea)
Expand Down Expand Up @@ -1851,7 +1851,7 @@
const suggestions = document.createElement('div')
const container = document.createElement('div')
highlighter.style.fontSize = '18px'
suggestions.style.marginLeft = '5px'
suggestions.style.marginLeft = '0px'
suggestions.style.marginTop = '7px'
Object.defineProperty(highlighter, 'getBoundingClientRect', {
value: () => ({
Expand All @@ -1871,6 +1871,10 @@
instance.highlighterElement = highlighter
instance.suggestionsElement = suggestions
instance.containerElement = container
highlighter.scrollLeft = 5
highlighter.scrollTop = 3
Object.defineProperty(highlighter, 'offsetWidth', { value: 200, configurable: true })
Object.defineProperty(container, 'offsetWidth', { value: 320, configurable: true })

const setStateMock = jest.spyOn(instance, 'setState').mockImplementation((update, cb) => {
const nextState =
Expand All @@ -1887,9 +1891,92 @@
})

expect(instance.state.suggestionsPosition.position).toBe('fixed')
expect(typeof instance.state.suggestionsPosition.left).toBe('number')
expect(instance.state.suggestionsPosition.left).toBe(9)
expect(typeof instance.state.suggestionsPosition.top).toBe('number')

act(() => {
instance.state.suggestionsPosition = {}
})
Object.assign(instance.props, { anchorMode: 'left' })

act(() => {
instance.updateSuggestionsPosition()
})

expect(instance.state.suggestionsPosition.position).toBe('fixed')
expect(instance.state.suggestionsPosition.left).toBe(4)

highlighter.remove()
suggestions.remove()
container.remove()
setStateMock.mockRestore()
unmount()
})

it('anchors suggestions to the control edge when using anchorMode="left" outside portals.', async () => {
const ref = React.createRef<MentionsInput>()
const { unmount } = render(
<MentionsInput ref={ref} value="" anchorMode="left">
<Mention trigger="@" data={data} />
</MentionsInput>
)

await waitFor(() => {
expect(ref.current).not.toBeNull()
})

const instance = ref.current as unknown as any
Object.defineProperty(instance, 'resolvePortalHost', {
value: () => null,
configurable: true,
writable: true,
})

const highlighter = document.createElement('div')
const suggestions = document.createElement('div')
const container = document.createElement('div')
highlighter.style.fontSize = '16px'
Object.defineProperty(highlighter, 'getBoundingClientRect', {
value: () => ({
left: 2,
top: 8,
right: 0,
bottom: 0,
width: 0,
height: 0,
}),
})
Object.defineProperty(highlighter, 'offsetWidth', { value: 180, configurable: true })
Object.defineProperty(container, 'offsetWidth', { value: 220, configurable: true })
Object.defineProperty(suggestions, 'offsetHeight', { value: 40, configurable: true })
document.body.append(highlighter)
document.body.append(suggestions)
document.body.append(container)

highlighter.scrollLeft = 14
highlighter.scrollTop = 5

instance.highlighterElement = highlighter
instance.suggestionsElement = suggestions
instance.containerElement = container
instance.state.caretPosition = { left: 32, top: 18 }
instance.state.suggestionsPosition = {}

const setStateMock = jest.spyOn(instance, 'setState').mockImplementation((update, cb) => {
const nextState =
typeof update === 'function' ? update(instance.state, instance.props) : update
Object.assign(instance.state, nextState)
cb?.()
})

act(() => {
instance.updateSuggestionsPosition()
})

expect(instance.state.suggestionsPosition.position).toBeUndefined()
expect(instance.state.suggestionsPosition.left).toBe(0)
expect(instance.state.suggestionsPosition.right).toBeUndefined()

highlighter.remove()
suggestions.remove()
container.remove()
Expand Down Expand Up @@ -1941,6 +2028,21 @@
}
;(globalThis as any).ResizeObserver = MockResizeObserver

const originalAdd = window.addEventListener
const originalRemove = window.removeEventListener
const handlers: Partial<Record<string, EventListener>> = {}
const addListener = jest
.spyOn(window, 'addEventListener')

Check failure on line 2035 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Prefer `globalThis` over `window`
.mockImplementation((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => {

Check failure on line 2036 in src/MentionsInput.spec.tsx

View workflow job for this annotation

GitHub Actions / build

Replace `(type:·string,·listener:·EventListenerOrEventListenerObject,·options?:·boolean·|·AddEventListenerOptions` with `⏎··········(⏎············type:·string,⏎············listener:·EventListenerOrEventListenerObject,⏎············options?:·boolean·|·AddEventListenerOptions⏎··········`
handlers[type] = listener as EventListener
return originalAdd.call(window, type, listener, options)
})
const removeListener = jest
.spyOn(window, 'removeEventListener')
.mockImplementation((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions) => {
return originalRemove.call(window, type, listener, options)
})
Comment on lines 2035 to 2044

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix linting and formatting issues.

Static analysis has flagged two issues:

  1. Line 2035: Prefer globalThis over window (ESLint)
  2. Line 2036: Line too long, needs formatting

Apply this diff to address both issues:

-      const originalAdd = window.addEventListener
-      const originalRemove = window.removeEventListener
+      const originalAdd = globalThis.addEventListener
+      const originalRemove = globalThis.removeEventListener
       const handlers: Partial<Record<string, EventListener>> = {}
       const addListener = jest
-        .spyOn(window, 'addEventListener')
-        .mockImplementation((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => {
+        .spyOn(globalThis, 'addEventListener')
+        .mockImplementation(
+          (
+            type: string,
+            listener: EventListenerOrEventListenerObject,
+            options?: boolean | AddEventListenerOptions
+          ) => {
           handlers[type] = listener as EventListener
-          return originalAdd.call(window, type, listener, options)
+          return originalAdd.call(globalThis, type, listener, options)
         })
       const removeListener = jest
-        .spyOn(window, 'removeEventListener')
-        .mockImplementation((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions) => {
-          return originalRemove.call(window, type, listener, options)
+        .spyOn(globalThis, 'removeEventListener')
+        .mockImplementation(
+          (
+            type: string,
+            listener: EventListenerOrEventListenerObject,
+            options?: boolean | EventListenerOptions
+          ) => {
+          return originalRemove.call(globalThis, type, listener, options)
         })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.spyOn(window, 'addEventListener')
.mockImplementation((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => {
handlers[type] = listener as EventListener
return originalAdd.call(window, type, listener, options)
})
const removeListener = jest
.spyOn(window, 'removeEventListener')
.mockImplementation((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions) => {
return originalRemove.call(window, type, listener, options)
})
.spyOn(globalThis, 'addEventListener')
.mockImplementation(
(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions
) => {
handlers[type] = listener as EventListener
return originalAdd.call(globalThis, type, listener, options)
}
)
const removeListener = jest
.spyOn(globalThis, 'removeEventListener')
.mockImplementation(
(
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | EventListenerOptions
) => {
return originalRemove.call(globalThis, type, listener, options)
}
)
🧰 Tools
🪛 GitHub Check: build

[failure] 2036-2036:
Replace (type:·string,·listener:·EventListenerOrEventListenerObject,·options?:·boolean·|·AddEventListenerOptions with ⏎··········(⏎············type:·string,⏎············listener:·EventListenerOrEventListenerObject,⏎············options?:·boolean·|·AddEventListenerOptions⏎··········


[failure] 2035-2035:
Prefer globalThis over window

🤖 Prompt for AI Agents
In src/MentionsInput.spec.tsx around lines 2035 to 2044, replace uses of window
with globalThis to satisfy the ESLint preference and reformat the long
mockImplementation line so it stays under the project's max line length (e.g.,
break the function signature/arrow body across multiple lines or assign the
listener capture to a local variable), ensuring you update both spyOn calls
(addEventListener and removeEventListener) to use globalThis and wrap
arguments/return invocation onto separate lines so the linter no longer flags a
long line.


const bridgeElement = instance.renderMeasurementBridge() as React.ReactElement
const { unmount: unmountBridge } = render(bridgeElement)

Expand All @@ -1957,10 +2059,32 @@
expect(syncScroll.mock.calls.length).toBe(syncCalls + 1)
expect(updatePosition.mock.calls.length).toBe(positionCalls + 1)

act(() => {
window.dispatchEvent(new Event('resize'))
})

expect(syncScroll.mock.calls.length).toBeGreaterThan(syncCalls + 1)
expect(updatePosition.mock.calls.length).toBeGreaterThan(positionCalls + 1)

act(() => {
window.dispatchEvent(new Event('orientationchange'))
})

expect(syncScroll.mock.calls.length).toBeGreaterThan(syncCalls + 2)
expect(updatePosition.mock.calls.length).toBeGreaterThan(positionCalls + 2)
Comment on lines 2062 to 2074

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider using globalThis for event dispatching consistency.

While window.dispatchEvent will work in test environments, for consistency with the ESLint rule that prefers globalThis, consider updating these lines as well:

       act(() => {
-        window.dispatchEvent(new Event('resize'))
+        globalThis.dispatchEvent(new Event('resize'))
       })
 
       expect(syncScroll.mock.calls.length).toBeGreaterThan(syncCalls + 1)
       expect(updatePosition.mock.calls.length).toBeGreaterThan(positionCalls + 1)
 
       act(() => {
-        window.dispatchEvent(new Event('orientationchange'))
+        globalThis.dispatchEvent(new Event('orientationchange'))
       })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
act(() => {
window.dispatchEvent(new Event('resize'))
})
expect(syncScroll.mock.calls.length).toBeGreaterThan(syncCalls + 1)
expect(updatePosition.mock.calls.length).toBeGreaterThan(positionCalls + 1)
act(() => {
window.dispatchEvent(new Event('orientationchange'))
})
expect(syncScroll.mock.calls.length).toBeGreaterThan(syncCalls + 2)
expect(updatePosition.mock.calls.length).toBeGreaterThan(positionCalls + 2)
act(() => {
globalThis.dispatchEvent(new Event('resize'))
})
expect(syncScroll.mock.calls.length).toBeGreaterThan(syncCalls + 1)
expect(updatePosition.mock.calls.length).toBeGreaterThan(positionCalls + 1)
act(() => {
globalThis.dispatchEvent(new Event('orientationchange'))
})
expect(syncScroll.mock.calls.length).toBeGreaterThan(syncCalls + 2)
expect(updatePosition.mock.calls.length).toBeGreaterThan(positionCalls + 2)
🤖 Prompt for AI Agents
In src/MentionsInput.spec.tsx around lines 2062 to 2074, replace uses of
window.dispatchEvent with globalThis.dispatchEvent to satisfy the ESLint
preference for globalThis and ensure consistent event dispatching in all test
environments; update both resize and orientationchange dispatch calls to use
globalThis.dispatchEvent(new Event(...)) so the behavior remains identical but
follows the linting guideline.


unmountBridge()
for (const observer of observers) {
expect(observer.disconnect).toHaveBeenCalled()
}
expect(handlers.resize).toBeDefined()
expect(handlers.orientationchange).toBeDefined()
expect(addListener).toHaveBeenCalledWith('resize', handlers.resize)
expect(addListener).toHaveBeenCalledWith('orientationchange', handlers.orientationchange)
expect(removeListener).toHaveBeenCalledWith('resize', handlers.resize)
expect(removeListener).toHaveBeenCalledWith('orientationchange', handlers.orientationchange)
addListener.mockRestore()
removeListener.mockRestore()
;(globalThis as any).ResizeObserver = originalResizeObserver
unmount()
})
Expand Down
38 changes: 33 additions & 5 deletions src/MentionsInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
MentionSelectionState,
MentionChildConfig,
MentionsInputProps,
MentionsInputAnchorMode,
MentionsInputState,
MentionsInputClassNames,
MentionsInputChangeTrigger,
Expand All @@ -66,7 +67,7 @@
// eslint-disable-next-line @typescript-eslint/require-await
return async (query: string) =>
items.flatMap((item) => {
const index = getSubstringIndex(item.display || String(item.id), query, ignoreAccents)

Check warning on line 70 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
return index >= 0
? [
{
Expand Down Expand Up @@ -192,6 +193,7 @@

const HANDLED_PROPS: Array<keyof MentionsInputProps<any>> = [
'singleLine',
'anchorMode',
'suggestionsPlacement',
'a11ySuggestionsListLabel',
'value',
Expand Down Expand Up @@ -223,6 +225,7 @@
} = {
singleLine: false,
autoResize: false,
anchorMode: 'caret',
suggestionsPlacement: 'below',
onKeyDown: () => null,
onSelect: () => null,
Expand Down Expand Up @@ -560,15 +563,15 @@
value: getPlainText(this.props.value ?? '', this.state.config),
onScroll: this.updateHighlighterScroll,
'data-slot': 'input',
'data-single-line': singleLine ? 'true' : undefined,

Check warning on line 566 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly
'data-multi-line': singleLine ? undefined : 'true',

Check warning on line 567 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly
}

const inlineStyle: CSSProperties = {
background: 'transparent',
}

if (!singleLine && isMobileSafari) {

Check warning on line 574 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly
inlineStyle.marginTop = 1
inlineStyle.marginLeft = -3
}
Expand All @@ -577,7 +580,7 @@
props.style = inlineStyle
}

if (!readOnly && !disabled) {

Check warning on line 583 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly

Check warning on line 583 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly
Object.assign(props, {
onChange: this.handleChange,
onSelect: this.handleSelect,
Expand Down Expand Up @@ -606,7 +609,7 @@
const existingDescribedBy =
typeof props['aria-describedby'] === 'string' ? props['aria-describedby'] : undefined
const describedBy = [existingDescribedBy, this.inlineAutocompleteLiveRegionId]
.filter((value): value is string => Boolean(value && value.trim().length > 0))

Check warning on line 612 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
.join(' ')
props['aria-describedby'] = describedBy || undefined
}
Expand All @@ -627,7 +630,7 @@
...inputProps,
} as any
)
: singleLine

Check warning on line 633 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable boolean value in conditional. Please handle the nullish case explicitly
? this.renderInput(inputProps)
: this.renderTextarea(inputProps)

Expand Down Expand Up @@ -693,7 +696,7 @@
if (globalThis.window !== undefined && typeof globalThis.getComputedStyle === 'function') {
const computed = globalThis.getComputedStyle(element)
const parse = (value: string | null | undefined) =>
value ? Number.parseFloat(value) || 0 : 0

Check warning on line 699 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
borderAdjustment = parse(computed.borderTopWidth) + parse(computed.borderBottomWidth)
}

Expand Down Expand Up @@ -1339,7 +1342,7 @@
if ('isComposing' in native && typeof native.isComposing === 'boolean') {
this._isComposing = native.isComposing
}
const value = this.props.value || ''

Check warning on line 1345 in src/MentionsInput.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

let newPlainTextValue = ev.target.value

Expand Down Expand Up @@ -1603,6 +1606,8 @@
updateSuggestionsPosition = (): void => {
const { caretPosition } = this.state
const { suggestionsPlacement = 'below' } = this.props
const anchorMode: MentionsInputAnchorMode = this.props.anchorMode ?? 'caret'
const anchorToLeft = anchorMode === 'left'
const resolvedPortalHost = this.resolvePortalHost()

const suggestions = this.suggestionsElement
Expand All @@ -1617,7 +1622,7 @@
const caretOffsetParentRect = highlighter.getBoundingClientRect()
const caretHeight = getComputedStyleLengthProp(highlighter, 'font-size')
const viewportRelative = {
left: caretOffsetParentRect.left + caretPosition.left,
left: caretOffsetParentRect.left + (anchorToLeft ? 0 : caretPosition.left),
top: caretOffsetParentRect.top + caretPosition.top + caretHeight,
}
const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0)
Expand All @@ -1636,7 +1641,9 @@
left -= getComputedStyleLengthProp(suggestions, 'margin-left')
top -= getComputedStyleLengthProp(suggestions, 'margin-top')
// take into account highlighter/textinput scrolling:
left -= highlighter.scrollLeft
if (!anchorToLeft) {
left -= highlighter.scrollLeft
}
top -= highlighter.scrollTop
// guard for mentions suggestions list clipped by window edges
const maxLeft = Math.max(0, viewportWidth - width)
Expand All @@ -1655,10 +1662,12 @@
const containerWidth = container.offsetWidth
const width = Math.min(desiredWidth, containerWidth)
position.width = width
const left = caretPosition.left - highlighter.scrollLeft
const left = anchorToLeft ? 0 : caretPosition.left - highlighter.scrollLeft
const top = caretPosition.top - highlighter.scrollTop
// guard for mentions suggestions list clipped by right edge of window
if (left + width > containerWidth) {
if (anchorToLeft) {
position.left = 0
} else if (left + width > containerWidth) {
position.right = 0
} else {
position.left = left
Expand All @@ -1679,7 +1688,8 @@
position.left === this.state.suggestionsPosition.left &&
position.top === this.state.suggestionsPosition.top &&
position.position === this.state.suggestionsPosition.position &&
position.width === this.state.suggestionsPosition.width
position.width === this.state.suggestionsPosition.width &&
position.right === this.state.suggestionsPosition.right
) {
return
}
Expand Down Expand Up @@ -2076,6 +2086,24 @@
[observe, suggestions, updateSuggestions]
)

useLayoutEffect(() => {
if (typeof window === 'undefined') {
return undefined
}

const handleViewportChange = () => {
updateAll()
}

window.addEventListener('resize', handleViewportChange)
window.addEventListener('orientationchange', handleViewportChange)

return () => {
window.removeEventListener('resize', handleViewportChange)
window.removeEventListener('orientationchange', handleViewportChange)
}
}, [updateAll])

useLayoutEffect(() => {
if (!input) {
return undefined
Expand Down
2 changes: 1 addition & 1 deletion src/SuggestionsOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface SuggestionsOverlayProps<Extra extends Record<string, unknown> = Record
}

const overlayStyles = cva(
'z-[100] mt-3 w-full min-w-[16rem] overflow-hidden rounded-2xl border border-border bg-popover shadow-xl ring-1 ring-ring backdrop-blur supports-[backdrop-filter]:bg-popover/95'
'z-[100] mt-3 w-full min-w-[16rem] overflow-hidden rounded-xl border border-border bg-popover shadow-xl ring-1 ring-ring backdrop-blur supports-[backdrop-filter]:bg-popover/95'
)
const listStyles =
'm-0 max-h-64 list-none divide-y divide-border overflow-y-auto scroll-py-1 p-0 focus:outline-none'
Expand Down
Loading
Loading