@stack-spot/portal-layout 0.0.48 → 0.0.50

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 (95) 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.js +1 -1
  7. package/dist/components/Header.d.ts +1 -1
  8. package/dist/components/Header.js +1 -1
  9. package/dist/components/OverlayContent.d.ts +1 -1
  10. package/dist/components/OverlayContent.js +20 -20
  11. package/dist/components/PortalSwitcher.d.ts +1 -1
  12. package/dist/components/PortalSwitcher.js +54 -54
  13. package/dist/components/SelectionList.d.ts +1 -1
  14. package/dist/components/SelectionList.js +54 -54
  15. package/dist/components/SelectionList.js.map +1 -1
  16. package/dist/components/Toaster.d.ts +1 -1
  17. package/dist/components/Toaster.js +1 -1
  18. package/dist/components/UserMenu.d.ts +1 -1
  19. package/dist/components/UserMenu.js +42 -42
  20. package/dist/components/UserMenu.js.map +1 -1
  21. package/dist/components/error/ErrorBoundary.d.ts +1 -1
  22. package/dist/components/error/ErrorBoundary.js +1 -1
  23. package/dist/components/error/ErrorFeedback.d.ts +1 -1
  24. package/dist/components/error/ErrorFeedback.js +1 -1
  25. package/dist/components/error/SilentErrorBoundary.d.ts +1 -1
  26. package/dist/components/error/SilentErrorBoundary.js +1 -1
  27. package/dist/components/menu/MenuContent.d.ts +12 -10
  28. package/dist/components/menu/MenuContent.d.ts.map +1 -1
  29. package/dist/components/menu/MenuContent.js +147 -147
  30. package/dist/components/menu/MenuContent.js.map +1 -1
  31. package/dist/components/menu/MenuSections.d.ts +1 -1
  32. package/dist/components/menu/MenuSections.js +1 -1
  33. package/dist/components/menu/MenuSections.js.map +1 -1
  34. package/dist/components/menu/PageSelector.d.ts +1 -1
  35. package/dist/components/menu/PageSelector.js +65 -65
  36. package/dist/components/menu/PageSelector.js.map +1 -1
  37. package/dist/components/menu/use-check-text-overflow.js.map +1 -1
  38. package/dist/layout.css +465 -465
  39. package/dist/svg/AI.d.ts +1 -1
  40. package/dist/svg/AI.js +1 -1
  41. package/dist/svg/EDP.d.ts +1 -1
  42. package/dist/svg/EDP.js +1 -1
  43. package/dist/svg/Forbidden.d.ts +1 -1
  44. package/dist/svg/Forbidden.js +1 -1
  45. package/dist/svg/HUB.d.ts +1 -1
  46. package/dist/svg/HUB.js +1 -1
  47. package/dist/svg/Logo.d.ts +1 -1
  48. package/dist/svg/Logo.js +1 -1
  49. package/dist/svg/NotFound.d.ts +1 -1
  50. package/dist/svg/NotFound.js +1 -1
  51. package/dist/svg/ServerError.d.ts +1 -1
  52. package/dist/svg/ServerError.js +1 -1
  53. package/dist/svg/Unauthenticated.d.ts +1 -1
  54. package/dist/svg/Unauthenticated.js +1 -1
  55. package/dist/toaster.js +2 -2
  56. package/dist/toaster.js.map +1 -1
  57. package/dist/utils.js.map +1 -1
  58. package/package.json +2 -2
  59. package/src/Layout.tsx +103 -103
  60. package/src/LayoutOverlayManager.tsx +273 -273
  61. package/src/components/Dialog.tsx +93 -93
  62. package/src/components/Header.tsx +29 -29
  63. package/src/components/OverlayContent.tsx +58 -58
  64. package/src/components/PortalSwitcher.tsx +147 -147
  65. package/src/components/SelectionList.tsx +268 -268
  66. package/src/components/Toaster.tsx +16 -16
  67. package/src/components/UserMenu.tsx +111 -111
  68. package/src/components/error/ErrorBoundary.tsx +38 -38
  69. package/src/components/error/ErrorFeedback.tsx +114 -114
  70. package/src/components/error/ErrorManager.ts +31 -31
  71. package/src/components/error/SilentErrorBoundary.tsx +54 -54
  72. package/src/components/menu/MenuContent.tsx +293 -293
  73. package/src/components/menu/MenuSections.tsx +268 -268
  74. package/src/components/menu/PageSelector.tsx +152 -152
  75. package/src/components/menu/constants.ts +2 -2
  76. package/src/components/menu/types.ts +112 -112
  77. package/src/components/menu/use-check-text-overflow.tsx +26 -26
  78. package/src/components/menu/use-keyboard-controls.tsx +70 -70
  79. package/src/components/types.ts +15 -15
  80. package/src/dictionary.ts +25 -25
  81. package/src/elements.ts +24 -24
  82. package/src/errors.ts +11 -11
  83. package/src/index.ts +17 -17
  84. package/src/layout.css +465 -465
  85. package/src/svg/AI.tsx +37 -37
  86. package/src/svg/EDP.tsx +35 -35
  87. package/src/svg/Forbidden.tsx +22 -22
  88. package/src/svg/HUB.tsx +35 -35
  89. package/src/svg/Logo.tsx +35 -35
  90. package/src/svg/NotFound.tsx +16 -16
  91. package/src/svg/ServerError.tsx +33 -33
  92. package/src/svg/Unauthenticated.tsx +16 -16
  93. package/src/toaster.tsx +76 -76
  94. package/src/utils.ts +114 -114
  95. package/tsconfig.json +8 -8
@@ -1,152 +1,152 @@
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 { ListAction, SelectionList } from '../SelectionList'
9
- import { MENU_CONTENT_PADDING as PADDING } from './constants'
10
- import { Selector } from './types'
11
- import { useCheckTextOverflow } from './use-check-text-overflow'
12
-
13
- const SelectorBox = styled.div`
14
- position: relative;
15
- margin: ${PADDING}px;
16
- margin-bottom: 28px;
17
-
18
- > a {
19
- display: flex;
20
- gap: 8px;
21
- align-items: center;
22
- border-radius: 0.25rem;
23
- border: 1px solid ${theme.color.light['500']};
24
- padding: 8px;
25
- transition: background-color 0.2s;
26
-
27
- &:hover {
28
- background-color: ${theme.color.light['500']};
29
- }
30
-
31
- .label {
32
- flex: 1;
33
- white-space: nowrap;
34
- overflow: hidden;
35
- text-overflow: ellipsis;
36
- }
37
- }
38
-
39
- .selection-list {
40
- position: absolute;
41
- top: 0;
42
- left: 0;
43
- right: 0;
44
- box-shadow: none;
45
- border-radius: 0.25rem;
46
-
47
- .selection-list-content {
48
- padding: 8px;
49
- border-radius: 0.25rem;
50
- border: none;
51
- ul {
52
- display: flex;
53
- flex-direction: column;
54
- gap: 8px;
55
- }
56
- }
57
-
58
- li > a {
59
- border: 1px solid ${theme.color.light['500']};
60
- background-color: ${theme.color.light['400']};
61
-
62
- &:hover {
63
- background-color: ${theme.color.light['500']};
64
- }
65
- }
66
-
67
- .view-all {
68
- background: ${theme.color.light['500']};
69
- border-radius: 0.25rem;
70
- height: 40px;
71
- display: flex;
72
- align-items: center;
73
- justify-content: center;
74
- margin-top: 8px;
75
- }
76
- }
77
- `
78
-
79
- export const PageSelector = ({ options, value, button, loading, title }: Selector) => {
80
- const t = useTranslate(dictionary)
81
- const [visible, setVisible] = useState(false)
82
- const { ref, overflow } = useCheckTextOverflow()
83
- const id = useRef(`pageSelector${title || Math.random()}`)
84
- const { optionsWithIcon, selected } = useMemo(
85
- () => {
86
- let selected = options[0]
87
- const optionsWithIcon = options.map<ListAction>((option) => {
88
- if (option.key === value) {
89
- selected = option
90
- return { ...option, active: true }
91
- }
92
- return { ...option, iconRight: <ArrowRight /> }
93
- })
94
- return { optionsWithIcon, selected }
95
- },
96
- [options, value, button],
97
- )
98
-
99
- const label = selected?.label ?? button?.label ?? value
100
- const isTextLabel = typeof label == 'string'
101
- const labelText = typeof label === 'string' ? label : label.id
102
- const buttonLabelText = typeof button?.label == 'string' ? button?.label : button?.label.id
103
-
104
- return (
105
- <SelectorBox>
106
- {loading
107
- ? <LoadingCircular />
108
- : (
109
- <>
110
- {title && <Text colorScheme="light.700" sx={{ mb: 3 }}>{title}</Text>}
111
- <a
112
- onClick={() => setVisible(true)}
113
- onKeyDown={e => e.key === 'Enter' && setVisible(true)}
114
- title={value}
115
- tabIndex={0}
116
- aria-label={interpolate(t.accessibility, [value])}
117
- aria-expanded={visible}
118
- aria-controls={id.current}
119
- >
120
- {selected?.icon && <IconBox>{selected?.icon}</IconBox>}
121
- {isTextLabel ?
122
- <Text ref={ref} appearance="body2" className="label" title={overflow ? labelText : ''}>{labelText}</Text> :
123
- label.element}
124
- <IconBox size="xs"><Select /></IconBox>
125
- </a>
126
-
127
- <SelectionList
128
- id={id.current}
129
- visible={visible}
130
- items={optionsWithIcon}
131
- onHide={() => setVisible(false)}
132
- after={button ?
133
- <a className="view-all" tabIndex={0} href={button.href} onClick={button.onClick}>{buttonLabelText}</a>
134
- : undefined}
135
- scroll
136
- />
137
- </>
138
- )
139
- }
140
- </SelectorBox>
141
- )
142
- }
143
-
144
- const dictionary = {
145
- en: {
146
- accessibility: 'Current value: $0. Press Enter to change.',
147
- },
148
- pt: {
149
- accessibility: 'Valor atual: $0. Aperte Enter para mudar.',
150
- },
151
- } satisfies Dictionary
152
-
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 { ListAction, SelectionList } from '../SelectionList'
9
+ import { MENU_CONTENT_PADDING as PADDING } from './constants'
10
+ import { Selector } from './types'
11
+ import { useCheckTextOverflow } from './use-check-text-overflow'
12
+
13
+ const SelectorBox = styled.div`
14
+ position: relative;
15
+ margin: ${PADDING}px;
16
+ margin-bottom: 28px;
17
+
18
+ > a {
19
+ display: flex;
20
+ gap: 8px;
21
+ align-items: center;
22
+ border-radius: 0.25rem;
23
+ border: 1px solid ${theme.color.light['500']};
24
+ padding: 8px;
25
+ transition: background-color 0.2s;
26
+
27
+ &:hover {
28
+ background-color: ${theme.color.light['500']};
29
+ }
30
+
31
+ .label {
32
+ flex: 1;
33
+ white-space: nowrap;
34
+ overflow: hidden;
35
+ text-overflow: ellipsis;
36
+ }
37
+ }
38
+
39
+ .selection-list {
40
+ position: absolute;
41
+ top: 0;
42
+ left: 0;
43
+ right: 0;
44
+ box-shadow: none;
45
+ border-radius: 0.25rem;
46
+
47
+ .selection-list-content {
48
+ padding: 8px;
49
+ border-radius: 0.25rem;
50
+ border: none;
51
+ ul {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 8px;
55
+ }
56
+ }
57
+
58
+ li > a {
59
+ border: 1px solid ${theme.color.light['500']};
60
+ background-color: ${theme.color.light['400']};
61
+
62
+ &:hover {
63
+ background-color: ${theme.color.light['500']};
64
+ }
65
+ }
66
+
67
+ .view-all {
68
+ background: ${theme.color.light['500']};
69
+ border-radius: 0.25rem;
70
+ height: 40px;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ margin-top: 8px;
75
+ }
76
+ }
77
+ `
78
+
79
+ export const PageSelector = ({ options, value, button, loading, title }: Selector) => {
80
+ const t = useTranslate(dictionary)
81
+ const [visible, setVisible] = useState(false)
82
+ const { ref, overflow } = useCheckTextOverflow()
83
+ const id = useRef(`pageSelector${title || Math.random()}`)
84
+ const { optionsWithIcon, selected } = useMemo(
85
+ () => {
86
+ let selected = options[0]
87
+ const optionsWithIcon = options.map<ListAction>((option) => {
88
+ if (option.key === value) {
89
+ selected = option
90
+ return { ...option, active: true }
91
+ }
92
+ return { ...option, iconRight: <ArrowRight /> }
93
+ })
94
+ return { optionsWithIcon, selected }
95
+ },
96
+ [options, value, button],
97
+ )
98
+
99
+ const label = selected?.label ?? button?.label ?? value
100
+ const isTextLabel = typeof label == 'string'
101
+ const labelText = typeof label === 'string' ? label : label.id
102
+ const buttonLabelText = typeof button?.label == 'string' ? button?.label : button?.label.id
103
+
104
+ return (
105
+ <SelectorBox>
106
+ {loading
107
+ ? <LoadingCircular />
108
+ : (
109
+ <>
110
+ {title && <Text colorScheme="light.700" sx={{ mb: 3 }}>{title}</Text>}
111
+ <a
112
+ onClick={() => setVisible(true)}
113
+ onKeyDown={e => e.key === 'Enter' && setVisible(true)}
114
+ title={value}
115
+ tabIndex={0}
116
+ aria-label={interpolate(t.accessibility, [value])}
117
+ aria-expanded={visible}
118
+ aria-controls={id.current}
119
+ >
120
+ {selected?.icon && <IconBox>{selected?.icon}</IconBox>}
121
+ {isTextLabel ?
122
+ <Text ref={ref} appearance="body2" className="label" title={overflow ? labelText : ''}>{labelText}</Text> :
123
+ label.element}
124
+ <IconBox size="xs"><Select /></IconBox>
125
+ </a>
126
+
127
+ <SelectionList
128
+ id={id.current}
129
+ visible={visible}
130
+ items={optionsWithIcon}
131
+ onHide={() => setVisible(false)}
132
+ after={button ?
133
+ <a className="view-all" tabIndex={0} href={button.href} onClick={button.onClick}>{buttonLabelText}</a>
134
+ : undefined}
135
+ scroll
136
+ />
137
+ </>
138
+ )
139
+ }
140
+ </SelectorBox>
141
+ )
142
+ }
143
+
144
+ const dictionary = {
145
+ en: {
146
+ accessibility: 'Current value: $0. Press Enter to change.',
147
+ },
148
+ pt: {
149
+ accessibility: 'Valor atual: $0. Aperte Enter para mudar.',
150
+ },
151
+ } satisfies Dictionary
152
+
@@ -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
+ }