@codeleap/web 3.21.1 → 3.21.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeleap/web",
3
- "version": "3.21.1",
3
+ "version": "3.21.3",
4
4
  "main": "src/index.ts",
5
5
  "repository": {
6
6
  "url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
@@ -1,9 +1,9 @@
1
- import { useCallback, useDefaultComponentStyle } from '@codeleap/common'
1
+ import React from 'react'
2
+ import { onUpdate, useCallback, useDefaultComponentStyle, useState, useValidate } from '@codeleap/common'
2
3
  import { BubbleMenu, FloatingMenu, EditorContent } from '@tiptap/react'
3
- import { FileInput, View } from '../components'
4
+ import { FileInput, Text, View } from '../components'
4
5
  import { TextEditorProps } from './types'
5
6
  import { TextEditorPresets } from './styles'
6
-
7
7
  export * from './styles'
8
8
  export * from './types'
9
9
 
@@ -19,13 +19,43 @@ export const TextEditor = (props: TextEditorProps) => {
19
19
  floatingMenuProps,
20
20
  toolbarComponent,
21
21
  fileInputRef,
22
+ _error,
23
+ validate,
22
24
  } = props
25
+
26
+ const [_isFocused, setIsFocused] = useState(false)
27
+ const validation = useValidate(editor?.getText() ?? '', validate)
28
+
29
+ const hasError = !validation.isValid || _error
30
+ const errorMessage = validation.message || _error
31
+ const isDisabled = !editor?.isEditable ?? null
32
+
23
33
  const variantStyles = useDefaultComponentStyle<'u:TextEditor', typeof TextEditorPresets>('u:TextEditor', {
24
34
  variants,
25
35
  styles,
26
36
  responsiveVariants,
27
37
  })
28
38
 
39
+ const handleBlur = React.useCallback(() => {
40
+ validation?.onInputBlurred()
41
+ setIsFocused(false)
42
+ }, [validation?.onInputBlurred])
43
+
44
+ const handleFocus = React.useCallback(() => {
45
+ validation?.onInputFocused()
46
+ setIsFocused(true)
47
+ }, [validation?.onInputFocused])
48
+
49
+ onUpdate(() => {
50
+ editor?.on('blur', handleBlur)
51
+ editor?.on('focus', handleFocus)
52
+
53
+ return () => {
54
+ editor?.off('blur', handleBlur)
55
+ editor?.off('focus', handleFocus)
56
+ }
57
+ }, [editor, handleBlur, handleFocus])
58
+
29
59
  const _BubbleMenu = useCallback(() => {
30
60
  return (
31
61
  <BubbleMenu css={[variantStyles.bubbleMenu]} {...bubbleMenuProps} editor={editor}>
@@ -46,14 +76,30 @@ export const TextEditor = (props: TextEditorProps) => {
46
76
  )
47
77
  }, [editor])
48
78
 
79
+ const editorStyles = [
80
+ variantStyles.editor,
81
+ hasError && variantStyles['editor:error'],
82
+ isDisabled && variantStyles['editor:disabled'],
83
+ ]
84
+
49
85
  if (!editor) return null
50
86
  return (
51
- <View style={{ ...variantStyles.wrapper, ...style }}>
87
+ <View
88
+ css={[
89
+ variantStyles.wrapper,
90
+ hasError && variantStyles['wrapper:error'],
91
+ {
92
+ '.tiptap': editorStyles,
93
+ },
94
+ style,
95
+ ]}
96
+ >
52
97
  {toolbarComponent}
53
98
  {children}
54
- <_BubbleMenu/>
55
- <_FloatingMenu/>
56
- <EditorContent editor={editor}/>
99
+ <_BubbleMenu />
100
+ <_FloatingMenu />
101
+ <EditorContent editor={editor} />
102
+ {hasError ? <Text text={errorMessage} css={variantStyles['errorMessage:error']} /> : null}
57
103
  <FileInput
58
104
  ref={fileInputRef}
59
105
  />
@@ -1,7 +1,12 @@
1
1
  import { createDefaultVariantFactory, includePresets } from '@codeleap/common'
2
2
 
3
- export type TextEditorComposition = 'wrapper' | 'floatingMenu' | 'bubbleMenu' | 'bubbleMenuInnerWrapper' | 'floatingMenuInnerWrapper'
3
+ export type TextEditorStates = 'error' | 'disabled'
4
+ export type TextEditorParts = 'wrapper' | 'editor' | 'floatingMenu' | 'bubbleMenu' | 'bubbleMenuInnerWrapper' | 'floatingMenuInnerWrapper' | 'errorMessage'
5
+
6
+ export type TextEditorComposition = `${TextEditorParts}:${TextEditorStates}` | TextEditorParts
4
7
 
5
8
  const createTextEditorStyle = createDefaultVariantFactory<TextEditorComposition>()
6
9
 
7
- export const TextEditorPresets = includePresets((styles) => createTextEditorStyle(() => ({ wrapper: styles })))
10
+ export const TextEditorPresets = includePresets((styles) => createTextEditorStyle(() => ({
11
+ wrapper: styles,
12
+ })))
@@ -1,5 +1,5 @@
1
1
  import { RefObject } from 'react'
2
- import { ComponentVariants, StylesOf } from '@codeleap/common'
2
+ import { ComponentVariants, FormTypes, StylesOf, yup } from '@codeleap/common'
3
3
  import { BubbleMenuProps, Editor, FloatingMenuProps } from '@tiptap/react'
4
4
  import { FileInputRef } from '../FileInput'
5
5
  import { TextEditorComposition, TextEditorPresets } from './styles'
@@ -8,8 +8,10 @@ export type TextEditorProps = React.PropsWithChildren<{
8
8
  editor: Editor
9
9
  styles?: StylesOf<TextEditorComposition>
10
10
  style?: React.CSSProperties
11
- bubbleMenuProps?: BubbleMenuProps & {renderContent: React.ReactNode}
12
- floatingMenuProps?: FloatingMenuProps & {renderContent: React.ReactNode}
11
+ bubbleMenuProps?: BubbleMenuProps & { renderContent: React.ReactNode }
12
+ floatingMenuProps?: FloatingMenuProps & { renderContent: React.ReactNode }
13
13
  toolbarComponent?: JSX.Element
14
14
  fileInputRef?: RefObject<FileInputRef>
15
+ _error?: boolean
16
+ validate?: FormTypes.ValidatorWithoutForm<string> | yup.SchemaOf<string>
15
17
  } & ComponentVariants<typeof TextEditorPresets>>
package/src/lib/hooks.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { AnyFunction, onMount, onUpdate, range, useUncontrolled } from '@codeleap/common'
2
- import { useCallback, useMemo, useRef, useState } from 'react'
1
+ import { AnyFunction, onMount, onUpdate, range, TypeGuards, useUncontrolled } from '@codeleap/common'
2
+ import React, { useCallback, useMemo, useRef, useState } from 'react'
3
3
  import { v4 } from 'uuid'
4
4
  import { easeInOut, EasingFunction, AnimationProps, useAnimate, useAnimation, animate } from 'framer-motion'
5
+ import { globalHistory } from '@reach/router'
5
6
 
6
7
  export function useWindowSize() {
7
8
  const [size, setSize] = useState([])
@@ -352,3 +353,76 @@ export function useAnimatedVariantStyles<T extends Record<string|number|symbol,
352
353
 
353
354
  return animated
354
355
  }
356
+
357
+ type UseWindowFocusOptions = {
358
+ onFocus?: AnyFunction
359
+ onBlur?: AnyFunction
360
+ }
361
+
362
+ export const useWindowFocus = (options: UseWindowFocusOptions = {}, deps: Array<any> = []): boolean => {
363
+ const [focused, setFocused] = useState(true)
364
+
365
+ const onFocus = () => {
366
+ setFocused(true)
367
+ if (TypeGuards.isFunction(options?.onFocus)) options?.onFocus()
368
+ }
369
+
370
+ const onBlur = () => {
371
+ setFocused(false)
372
+ if (TypeGuards.isFunction(options?.onBlur)) options?.onBlur()
373
+ }
374
+
375
+ useEffect(() => {
376
+ window.addEventListener('focus', onFocus)
377
+ window.addEventListener('blur', onBlur)
378
+
379
+ return () => {
380
+ window.removeEventListener('focus', onFocus)
381
+ window.removeEventListener('blur', onBlur)
382
+ }
383
+ }, deps)
384
+
385
+ return focused
386
+ }
387
+
388
+ export const usePageExitBlocker = (
389
+ handler: (willLeavePage: boolean) => void,
390
+ deps: Array<any> = [],
391
+ message: string = 'Are you sure you want to leave?'
392
+ ) => {
393
+ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
394
+ if (!event) return null
395
+
396
+ event?.preventDefault()
397
+ event.returnValue = ''
398
+ return
399
+ }
400
+
401
+ React.useEffect(() => {
402
+ if (!window) return null
403
+
404
+ window.addEventListener('beforeunload', handleBeforeUnload)
405
+
406
+ return () => {
407
+ window.removeEventListener('beforeunload', handleBeforeUnload)
408
+ }
409
+ }, deps)
410
+
411
+ React.useEffect(() => {
412
+ return globalHistory.listen((args) => {
413
+ if (!window) return null
414
+
415
+ const historyPathname = args?.location?.pathname
416
+ const windowPathname = window?.location?.pathname
417
+
418
+ const isPopAction = args?.action === 'POP'
419
+ const isLeaveAction = args?.action === 'PUSH' && !historyPathname?.includes(windowPathname)
420
+
421
+ if (isLeaveAction || isPopAction) {
422
+ const willLeavePage = window.confirm(message)
423
+
424
+ handler?.(willLeavePage)
425
+ }
426
+ })
427
+ }, deps)
428
+ }