@stack-spot/portal-layout 0.0.54 → 0.0.56

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.
Files changed (99) hide show
  1. package/dist/Layout.d.ts +2 -2
  2. package/dist/Layout.js +1 -1
  3. package/dist/LayoutOverlayManager.js +6 -6
  4. package/dist/LayoutOverlayManager.js.map +1 -1
  5. package/dist/components/Dialog.d.ts +1 -1
  6. package/dist/components/Dialog.d.ts.map +1 -1
  7. package/dist/components/Dialog.js +3 -3
  8. package/dist/components/Dialog.js.map +1 -1
  9. package/dist/components/Header.d.ts +1 -1
  10. package/dist/components/Header.js +1 -1
  11. package/dist/components/OverlayContent.d.ts +1 -1
  12. package/dist/components/OverlayContent.js +20 -20
  13. package/dist/components/PortalSwitcher.d.ts +1 -1
  14. package/dist/components/PortalSwitcher.js +54 -54
  15. package/dist/components/SelectionList.d.ts +1 -1
  16. package/dist/components/SelectionList.js +54 -54
  17. package/dist/components/SelectionList.js.map +1 -1
  18. package/dist/components/Toaster.d.ts +1 -1
  19. package/dist/components/Toaster.js +1 -1
  20. package/dist/components/UserMenu.d.ts +1 -1
  21. package/dist/components/UserMenu.js +41 -41
  22. package/dist/components/error/ErrorBoundary.d.ts +1 -1
  23. package/dist/components/error/ErrorBoundary.js +1 -1
  24. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  25. package/dist/components/error/ErrorFeedback.js +1 -1
  26. package/dist/components/error/SilentErrorBoundary.d.ts +1 -1
  27. package/dist/components/error/SilentErrorBoundary.js +1 -1
  28. package/dist/components/menu/MenuContent.d.ts +12 -10
  29. package/dist/components/menu/MenuContent.d.ts.map +1 -1
  30. package/dist/components/menu/MenuContent.js +146 -146
  31. package/dist/components/menu/MenuContent.js.map +1 -1
  32. package/dist/components/menu/MenuSections.d.ts +1 -1
  33. package/dist/components/menu/MenuSections.js +1 -1
  34. package/dist/components/menu/MenuSections.js.map +1 -1
  35. package/dist/components/menu/PageSelector.d.ts +1 -1
  36. package/dist/components/menu/PageSelector.js +65 -65
  37. package/dist/components/menu/PageSelector.js.map +1 -1
  38. package/dist/components/menu/use-check-text-overflow.js.map +1 -1
  39. package/dist/layout-context.d.ts +1 -1
  40. package/dist/layout-context.js +1 -1
  41. package/dist/layout.css +472 -467
  42. package/dist/svg/AI.d.ts +1 -1
  43. package/dist/svg/AI.js +1 -1
  44. package/dist/svg/EDP.d.ts +1 -1
  45. package/dist/svg/EDP.js +1 -1
  46. package/dist/svg/Forbidden.d.ts +1 -1
  47. package/dist/svg/Forbidden.js +1 -1
  48. package/dist/svg/HUB.d.ts +1 -1
  49. package/dist/svg/HUB.js +1 -1
  50. package/dist/svg/Logo.d.ts +1 -1
  51. package/dist/svg/Logo.js +1 -1
  52. package/dist/svg/NotFound.d.ts +1 -1
  53. package/dist/svg/NotFound.js +1 -1
  54. package/dist/svg/ServerError.d.ts +1 -1
  55. package/dist/svg/ServerError.js +1 -1
  56. package/dist/svg/Unauthenticated.d.ts +1 -1
  57. package/dist/svg/Unauthenticated.js +1 -1
  58. package/dist/toaster.js +4 -4
  59. package/dist/toaster.js.map +1 -1
  60. package/dist/utils.js.map +1 -1
  61. package/package.json +5 -5
  62. package/src/Layout.tsx +106 -106
  63. package/src/LayoutOverlayManager.tsx +273 -273
  64. package/src/components/Dialog.tsx +97 -93
  65. package/src/components/Header.tsx +34 -34
  66. package/src/components/OverlayContent.tsx +58 -58
  67. package/src/components/PortalSwitcher.tsx +147 -147
  68. package/src/components/SelectionList.tsx +272 -272
  69. package/src/components/Toaster.tsx +16 -16
  70. package/src/components/UserMenu.tsx +111 -111
  71. package/src/components/error/ErrorBoundary.tsx +38 -38
  72. package/src/components/error/ErrorFeedback.tsx +114 -114
  73. package/src/components/error/ErrorManager.ts +31 -31
  74. package/src/components/error/SilentErrorBoundary.tsx +54 -54
  75. package/src/components/menu/MenuContent.tsx +296 -296
  76. package/src/components/menu/MenuSections.tsx +270 -270
  77. package/src/components/menu/PageSelector.tsx +154 -154
  78. package/src/components/menu/constants.ts +2 -2
  79. package/src/components/menu/types.ts +112 -112
  80. package/src/components/menu/use-check-text-overflow.tsx +26 -26
  81. package/src/components/menu/use-keyboard-controls.tsx +70 -70
  82. package/src/components/types.ts +15 -15
  83. package/src/dictionary.ts +25 -25
  84. package/src/elements.ts +24 -24
  85. package/src/errors.ts +11 -11
  86. package/src/index.ts +17 -17
  87. package/src/layout-context.tsx +22 -22
  88. package/src/layout.css +472 -467
  89. package/src/svg/AI.tsx +37 -37
  90. package/src/svg/EDP.tsx +35 -35
  91. package/src/svg/Forbidden.tsx +22 -22
  92. package/src/svg/HUB.tsx +35 -35
  93. package/src/svg/Logo.tsx +35 -35
  94. package/src/svg/NotFound.tsx +16 -16
  95. package/src/svg/ServerError.tsx +33 -33
  96. package/src/svg/Unauthenticated.tsx +16 -16
  97. package/src/toaster.tsx +76 -76
  98. package/src/utils.ts +114 -114
  99. package/tsconfig.json +8 -8
@@ -1,154 +1,154 @@
1
- import { IconBox, Text } from '@citric/core'
2
- import { ArrowRight, Select } from '@citric/icons'
3
- import { LoadingCircular } from '@citric/ui'
4
- import { theme } from '@stack-spot/portal-theme'
5
- import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
6
- import { useMemo, useRef, useState } from 'react'
7
- import { styled } from 'styled-components'
8
- import { useAnchorTag } from '../../layout-context'
9
- import { ListAction, SelectionList } from '../SelectionList'
10
- import { MENU_CONTENT_PADDING as PADDING } from './constants'
11
- import { Selector } from './types'
12
- import { useCheckTextOverflow } from './use-check-text-overflow'
13
-
14
- const SelectorBox = styled.div`
15
- position: relative;
16
- margin: ${PADDING}px;
17
- margin-bottom: 28px;
18
-
19
- > a {
20
- display: flex;
21
- gap: 8px;
22
- align-items: center;
23
- border-radius: 0.25rem;
24
- border: 1px solid ${theme.color.light['500']};
25
- padding: 8px;
26
- transition: background-color 0.2s;
27
-
28
- &:hover {
29
- background-color: ${theme.color.light['500']};
30
- }
31
-
32
- .label {
33
- flex: 1;
34
- white-space: nowrap;
35
- overflow: hidden;
36
- text-overflow: ellipsis;
37
- }
38
- }
39
-
40
- .selection-list {
41
- position: absolute;
42
- top: 0;
43
- left: 0;
44
- right: 0;
45
- box-shadow: none;
46
- border-radius: 0.25rem;
47
-
48
- .selection-list-content {
49
- padding: 8px;
50
- border-radius: 0.25rem;
51
- border: none;
52
- ul {
53
- display: flex;
54
- flex-direction: column;
55
- gap: 8px;
56
- }
57
- }
58
-
59
- li > a {
60
- border: 1px solid ${theme.color.light['500']};
61
- background-color: ${theme.color.light['400']};
62
-
63
- &:hover {
64
- background-color: ${theme.color.light['500']};
65
- }
66
- }
67
-
68
- .view-all {
69
- background: ${theme.color.light['500']};
70
- border-radius: 0.25rem;
71
- height: 40px;
72
- display: flex;
73
- align-items: center;
74
- justify-content: center;
75
- margin-top: 8px;
76
- }
77
- }
78
- `
79
-
80
- export const PageSelector = ({ options, value, button, loading, title }: Selector) => {
81
- const Link = useAnchorTag()
82
- const t = useTranslate(dictionary)
83
- const [visible, setVisible] = useState(false)
84
- const { ref, overflow } = useCheckTextOverflow()
85
- const id = useRef(`pageSelector${title || Math.random()}`)
86
- const { optionsWithIcon, selected } = useMemo(
87
- () => {
88
- let selected = options[0]
89
- const optionsWithIcon = options.map<ListAction>((option) => {
90
- if (option.key === value) {
91
- selected = option
92
- return { ...option, active: true }
93
- }
94
- return { ...option, iconRight: <ArrowRight /> }
95
- })
96
- return { optionsWithIcon, selected }
97
- },
98
- [options, value, button],
99
- )
100
-
101
- const label = selected?.label ?? button?.label ?? value
102
- const isTextLabel = typeof label == 'string'
103
- const labelText = typeof label === 'string' ? label : label.id
104
- const buttonLabelText = typeof button?.label == 'string' ? button?.label : button?.label.id
105
-
106
- return (
107
- <SelectorBox>
108
- {loading
109
- ? <LoadingCircular />
110
- : (
111
- <>
112
- {title && <Text colorScheme="light.700" sx={{ mb: 3 }}>{title}</Text>}
113
- <a
114
- onClick={() => setVisible(true)}
115
- onKeyDown={e => e.key === 'Enter' && setVisible(true)}
116
- title={value}
117
- tabIndex={0}
118
- aria-label={interpolate(t.accessibility, [value])}
119
- aria-expanded={visible}
120
- aria-controls={id.current}
121
- >
122
- {selected?.icon && <IconBox>{selected?.icon}</IconBox>}
123
- {isTextLabel ?
124
- <Text ref={ref} appearance="body2" className="label" title={overflow ? labelText : ''}>{labelText}</Text> :
125
- label.element}
126
- <IconBox size="xs"><Select /></IconBox>
127
- </a>
128
-
129
- <SelectionList
130
- id={id.current}
131
- visible={visible}
132
- items={optionsWithIcon}
133
- onHide={() => setVisible(false)}
134
- after={button ?
135
- <Link className="view-all" tabIndex={0} href={button.href} onClick={button.onClick}>{buttonLabelText}</Link>
136
- : undefined}
137
- scroll
138
- />
139
- </>
140
- )
141
- }
142
- </SelectorBox>
143
- )
144
- }
145
-
146
- const dictionary = {
147
- en: {
148
- accessibility: 'Current value: $0. Press Enter to change.',
149
- },
150
- pt: {
151
- accessibility: 'Valor atual: $0. Aperte Enter para mudar.',
152
- },
153
- } satisfies Dictionary
154
-
1
+ import { IconBox, Text } from '@citric/core'
2
+ import { ArrowRight, Select } from '@citric/icons'
3
+ import { LoadingCircular } from '@citric/ui'
4
+ import { theme } from '@stack-spot/portal-theme'
5
+ import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
6
+ import { useMemo, useRef, useState } from 'react'
7
+ import { styled } from 'styled-components'
8
+ import { useAnchorTag } from '../../layout-context'
9
+ import { ListAction, SelectionList } from '../SelectionList'
10
+ import { MENU_CONTENT_PADDING as PADDING } from './constants'
11
+ import { Selector } from './types'
12
+ import { useCheckTextOverflow } from './use-check-text-overflow'
13
+
14
+ const SelectorBox = styled.div`
15
+ position: relative;
16
+ margin: ${PADDING}px;
17
+ margin-bottom: 28px;
18
+
19
+ > a {
20
+ display: flex;
21
+ gap: 8px;
22
+ align-items: center;
23
+ border-radius: 0.25rem;
24
+ border: 1px solid ${theme.color.light['500']};
25
+ padding: 8px;
26
+ transition: background-color 0.2s;
27
+
28
+ &:hover {
29
+ background-color: ${theme.color.light['500']};
30
+ }
31
+
32
+ .label {
33
+ flex: 1;
34
+ white-space: nowrap;
35
+ overflow: hidden;
36
+ text-overflow: ellipsis;
37
+ }
38
+ }
39
+
40
+ .selection-list {
41
+ position: absolute;
42
+ top: 0;
43
+ left: 0;
44
+ right: 0;
45
+ box-shadow: none;
46
+ border-radius: 0.25rem;
47
+
48
+ .selection-list-content {
49
+ padding: 8px;
50
+ border-radius: 0.25rem;
51
+ border: none;
52
+ ul {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 8px;
56
+ }
57
+ }
58
+
59
+ li > a {
60
+ border: 1px solid ${theme.color.light['500']};
61
+ background-color: ${theme.color.light['400']};
62
+
63
+ &:hover {
64
+ background-color: ${theme.color.light['500']};
65
+ }
66
+ }
67
+
68
+ .view-all {
69
+ background: ${theme.color.light['500']};
70
+ border-radius: 0.25rem;
71
+ height: 40px;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ margin-top: 8px;
76
+ }
77
+ }
78
+ `
79
+
80
+ export const PageSelector = ({ options, value, button, loading, title }: Selector) => {
81
+ const Link = useAnchorTag()
82
+ const t = useTranslate(dictionary)
83
+ const [visible, setVisible] = useState(false)
84
+ const { ref, overflow } = useCheckTextOverflow()
85
+ const id = useRef(`pageSelector${title || Math.random()}`)
86
+ const { optionsWithIcon, selected } = useMemo(
87
+ () => {
88
+ let selected = options[0]
89
+ const optionsWithIcon = options.map<ListAction>((option) => {
90
+ if (option.key === value) {
91
+ selected = option
92
+ return { ...option, active: true }
93
+ }
94
+ return { ...option, iconRight: <ArrowRight /> }
95
+ })
96
+ return { optionsWithIcon, selected }
97
+ },
98
+ [options, value, button],
99
+ )
100
+
101
+ const label = selected?.label ?? button?.label ?? value
102
+ const isTextLabel = typeof label == 'string'
103
+ const labelText = typeof label === 'string' ? label : label.id
104
+ const buttonLabelText = typeof button?.label == 'string' ? button?.label : button?.label.id
105
+
106
+ return (
107
+ <SelectorBox>
108
+ {loading
109
+ ? <LoadingCircular />
110
+ : (
111
+ <>
112
+ {title && <Text colorScheme="light.700" sx={{ mb: 3 }}>{title}</Text>}
113
+ <a
114
+ onClick={() => setVisible(true)}
115
+ onKeyDown={e => e.key === 'Enter' && setVisible(true)}
116
+ title={value}
117
+ tabIndex={0}
118
+ aria-label={interpolate(t.accessibility, [value])}
119
+ aria-expanded={visible}
120
+ aria-controls={id.current}
121
+ >
122
+ {selected?.icon && <IconBox>{selected?.icon}</IconBox>}
123
+ {isTextLabel ?
124
+ <Text ref={ref} appearance="body2" className="label" title={overflow ? labelText : ''}>{labelText}</Text> :
125
+ label.element}
126
+ <IconBox size="xs"><Select /></IconBox>
127
+ </a>
128
+
129
+ <SelectionList
130
+ id={id.current}
131
+ visible={visible}
132
+ items={optionsWithIcon}
133
+ onHide={() => setVisible(false)}
134
+ after={button ?
135
+ <Link className="view-all" tabIndex={0} href={button.href} onClick={button.onClick}>{buttonLabelText}</Link>
136
+ : undefined}
137
+ scroll
138
+ />
139
+ </>
140
+ )
141
+ }
142
+ </SelectorBox>
143
+ )
144
+ }
145
+
146
+ const dictionary = {
147
+ en: {
148
+ accessibility: 'Current value: $0. Press Enter to change.',
149
+ },
150
+ pt: {
151
+ accessibility: 'Valor atual: $0. Aperte Enter para mudar.',
152
+ },
153
+ } satisfies Dictionary
154
+
@@ -1,2 +1,2 @@
1
- export const MENU_CONTENT_PADDING = 20
2
- export const MENU_CONTENT_ITEM_PADDING = 16
1
+ export const MENU_CONTENT_PADDING = 20
2
+ export const MENU_CONTENT_ITEM_PADDING = 16
@@ -1,112 +1,112 @@
1
- import { ReactElement, ReactNode } from 'react'
2
- import { Action } from '../types'
3
-
4
- interface BaseMenuItem {
5
- hidden?: boolean,
6
- /**
7
- * React element on the left.
8
- */
9
- icon?: React.ReactElement,
10
- /**
11
- * React element on the right.
12
- */
13
- badge?: React.ReactElement,
14
- /**
15
- * Whether to wrap overflowing text, just hide or hide and add an ellipsis (...).
16
- * @default 'wrap'
17
- */
18
- overflow?: 'hidden' | 'wrap' | 'ellipsis',
19
- }
20
-
21
- export interface ItemGroup extends BaseMenuItem {
22
- label: string,
23
- children: MenuItem[],
24
- open?: boolean,
25
- }
26
-
27
- export interface MenuAction extends Action, BaseMenuItem {
28
- active?: boolean,
29
- }
30
-
31
- export type MenuItem = ItemGroup | MenuAction
32
-
33
- export interface MenuButton extends Action {
34
- icon?: ReactElement,
35
- }
36
-
37
- export interface SelectorItem extends Action {
38
- key: string,
39
- icon?: ReactElement,
40
- }
41
-
42
- export interface Selector {
43
- value?: string,
44
- options: SelectorItem[],
45
- button?: Action,
46
- title?: string,
47
- subtitle?: string,
48
- loading?: boolean,
49
- }
50
-
51
- export interface MenuSectionContent {
52
- goBack?: Action,
53
- title?: string,
54
- subtitle?: string,
55
- afterTitle?: React.ReactNode,
56
- pageSelector?: Selector,
57
- options?: MenuItem[],
58
- loading?: boolean,
59
- error?: string,
60
- }
61
-
62
- export interface MenuSection extends Action {
63
- icon: ReactElement,
64
- /**
65
- * The content or a function that creates the content.
66
- * If this is a function, it will be called only when the section is hovered, i.e. only when the content really needs to be rendered.
67
- * Tip: this function can be a React Hook.
68
- */
69
- content?: MenuSectionContent | (() => MenuSectionContent),
70
- /**
71
- * The content or a function that creates the content.
72
- * If this is a function, it will be called only when the section is hovered, i.e. only when the content really needs to be rendered.
73
- * Tip: this function can be a React Hook.
74
- * This should be used when the content you want to render is not the default content
75
- */
76
- customContent?: ReactNode,
77
- active?: boolean,
78
- onOpen?: () => void,
79
- className?: string,
80
- }
81
-
82
- interface BaseMenuProps {
83
- sections?: MenuSection[],
84
- customContent?: ReactNode,
85
- settings?: {
86
- show?: boolean,
87
- onClick?: () => void,
88
- href?: string,
89
- active?: boolean,
90
- className?: string,
91
- },
92
- }
93
-
94
- export interface MenuPropsWithStaticContent extends BaseMenuProps {
95
- content?: MenuSectionContent,
96
- }
97
-
98
- export interface MenuPropsWithDynamicContent extends BaseMenuProps {
99
- /**
100
- * The function that creates the content. It will be called only when the content is rendered, i.e. only when the content really needs to
101
- * be rendered.
102
- *
103
- * Tip: this function can be a React Hook.
104
- */
105
- content: MenuSectionContent | (() => MenuSectionContent),
106
- /**
107
- * Identifies each content that might be rendered by the menu. This prevents React Hook errors when the content is a React Hook function.
108
- */
109
- contentKey: React.Key,
110
- }
111
-
112
- export type MenuProps = MenuPropsWithStaticContent | MenuPropsWithDynamicContent
1
+ import { ReactElement, ReactNode } from 'react'
2
+ import { Action } from '../types'
3
+
4
+ interface BaseMenuItem {
5
+ hidden?: boolean,
6
+ /**
7
+ * React element on the left.
8
+ */
9
+ icon?: React.ReactElement,
10
+ /**
11
+ * React element on the right.
12
+ */
13
+ badge?: React.ReactElement,
14
+ /**
15
+ * Whether to wrap overflowing text, just hide or hide and add an ellipsis (...).
16
+ * @default 'wrap'
17
+ */
18
+ overflow?: 'hidden' | 'wrap' | 'ellipsis',
19
+ }
20
+
21
+ export interface ItemGroup extends BaseMenuItem {
22
+ label: string,
23
+ children: MenuItem[],
24
+ open?: boolean,
25
+ }
26
+
27
+ export interface MenuAction extends Action, BaseMenuItem {
28
+ active?: boolean,
29
+ }
30
+
31
+ export type MenuItem = ItemGroup | MenuAction
32
+
33
+ export interface MenuButton extends Action {
34
+ icon?: ReactElement,
35
+ }
36
+
37
+ export interface SelectorItem extends Action {
38
+ key: string,
39
+ icon?: ReactElement,
40
+ }
41
+
42
+ export interface Selector {
43
+ value?: string,
44
+ options: SelectorItem[],
45
+ button?: Action,
46
+ title?: string,
47
+ subtitle?: string,
48
+ loading?: boolean,
49
+ }
50
+
51
+ export interface MenuSectionContent {
52
+ goBack?: Action,
53
+ title?: string,
54
+ subtitle?: string,
55
+ afterTitle?: React.ReactNode,
56
+ pageSelector?: Selector,
57
+ options?: MenuItem[],
58
+ loading?: boolean,
59
+ error?: string,
60
+ }
61
+
62
+ export interface MenuSection extends Action {
63
+ icon: ReactElement,
64
+ /**
65
+ * The content or a function that creates the content.
66
+ * If this is a function, it will be called only when the section is hovered, i.e. only when the content really needs to be rendered.
67
+ * Tip: this function can be a React Hook.
68
+ */
69
+ content?: MenuSectionContent | (() => MenuSectionContent),
70
+ /**
71
+ * The content or a function that creates the content.
72
+ * If this is a function, it will be called only when the section is hovered, i.e. only when the content really needs to be rendered.
73
+ * Tip: this function can be a React Hook.
74
+ * This should be used when the content you want to render is not the default content
75
+ */
76
+ customContent?: ReactNode,
77
+ active?: boolean,
78
+ onOpen?: () => void,
79
+ className?: string,
80
+ }
81
+
82
+ interface BaseMenuProps {
83
+ sections?: MenuSection[],
84
+ customContent?: ReactNode,
85
+ settings?: {
86
+ show?: boolean,
87
+ onClick?: () => void,
88
+ href?: string,
89
+ active?: boolean,
90
+ className?: string,
91
+ },
92
+ }
93
+
94
+ export interface MenuPropsWithStaticContent extends BaseMenuProps {
95
+ content?: MenuSectionContent,
96
+ }
97
+
98
+ export interface MenuPropsWithDynamicContent extends BaseMenuProps {
99
+ /**
100
+ * The function that creates the content. It will be called only when the content is rendered, i.e. only when the content really needs to
101
+ * be rendered.
102
+ *
103
+ * Tip: this function can be a React Hook.
104
+ */
105
+ content: MenuSectionContent | (() => MenuSectionContent),
106
+ /**
107
+ * Identifies each content that might be rendered by the menu. This prevents React Hook errors when the content is a React Hook function.
108
+ */
109
+ contentKey: React.Key,
110
+ }
111
+
112
+ export type MenuProps = MenuPropsWithStaticContent | MenuPropsWithDynamicContent
@@ -1,26 +1,26 @@
1
- import { useState, useRef, useEffect } from 'react'
2
-
3
- export function useCheckTextOverflow() {
4
- const [overflow, setOverflow] = useState<boolean>(false)
5
- const ref = useRef<HTMLParagraphElement>(null)
6
-
7
- const checkOverflow = () => {
8
- if (!ref.current) {
9
- return
10
- }
11
-
12
- const hasOverflow = ref.current.offsetWidth < ref.current.scrollWidth
13
-
14
- if (hasOverflow === overflow) {
15
- return
16
- }
17
-
18
- setOverflow(hasOverflow)
19
- }
20
-
21
- useEffect(() => {
22
- checkOverflow()
23
- }, [ref.current])
24
-
25
- return { overflow, ref }
26
- }
1
+ import { useState, useRef, useEffect } from 'react'
2
+
3
+ export function useCheckTextOverflow() {
4
+ const [overflow, setOverflow] = useState<boolean>(false)
5
+ const ref = useRef<HTMLParagraphElement>(null)
6
+
7
+ const checkOverflow = () => {
8
+ if (!ref.current) {
9
+ return
10
+ }
11
+
12
+ const hasOverflow = ref.current.offsetWidth < ref.current.scrollWidth
13
+
14
+ if (hasOverflow === overflow) {
15
+ return
16
+ }
17
+
18
+ setOverflow(hasOverflow)
19
+ }
20
+
21
+ useEffect(() => {
22
+ checkOverflow()
23
+ }, [ref.current])
24
+
25
+ return { overflow, ref }
26
+ }