@seamapi/react 1.61.1 → 1.62.0

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 (54) hide show
  1. package/README.md +1 -1
  2. package/dist/elements.js +4586 -4403
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +124 -12
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.min.css +1 -1
  7. package/dist/index.min.css.map +1 -1
  8. package/lib/icons/CheckGreen.d.ts +2 -0
  9. package/lib/icons/CheckGreen.js +7 -0
  10. package/lib/icons/CheckGreen.js.map +1 -0
  11. package/lib/icons/CloseWhite.d.ts +2 -0
  12. package/lib/icons/CloseWhite.js +7 -0
  13. package/lib/icons/CloseWhite.js.map +1 -0
  14. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js +21 -8
  15. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js.map +1 -1
  16. package/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.d.ts +2 -1
  17. package/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.js +7 -4
  18. package/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.js.map +1 -1
  19. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +1 -2
  20. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
  21. package/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.d.ts +2 -1
  22. package/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.js +11 -4
  23. package/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.js.map +1 -1
  24. package/lib/seam/components/SupportedDeviceTable/FilterCategoryMenu.js +12 -9
  25. package/lib/seam/components/SupportedDeviceTable/FilterCategoryMenu.js.map +1 -1
  26. package/lib/ui/Menu/Menu.js +32 -25
  27. package/lib/ui/Menu/Menu.js.map +1 -1
  28. package/lib/ui/Snackbar/Snackbar.d.ts +17 -0
  29. package/lib/ui/Snackbar/Snackbar.js +34 -0
  30. package/lib/ui/Snackbar/Snackbar.js.map +1 -0
  31. package/lib/ui/thermostat/ClimateModeMenu.js +4 -4
  32. package/lib/ui/thermostat/ClimateModeMenu.js.map +1 -1
  33. package/lib/version.d.ts +1 -1
  34. package/lib/version.js +1 -1
  35. package/package.json +2 -2
  36. package/src/lib/icons/CheckGreen.tsx +36 -0
  37. package/src/lib/icons/CloseWhite.tsx +36 -0
  38. package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx +96 -58
  39. package/src/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.element.ts +1 -0
  40. package/src/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.tsx +20 -4
  41. package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +1 -6
  42. package/src/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.element.ts +1 -0
  43. package/src/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.tsx +18 -2
  44. package/src/lib/seam/components/SupportedDeviceTable/FilterCategoryMenu.tsx +20 -15
  45. package/src/lib/ui/Menu/Menu.tsx +50 -40
  46. package/src/lib/ui/Snackbar/Snackbar.tsx +93 -0
  47. package/src/lib/ui/thermostat/ClimateModeMenu.tsx +4 -4
  48. package/src/lib/version.ts +1 -1
  49. package/src/styles/_colors.scss +2 -0
  50. package/src/styles/_loading_toast.scss +5 -16
  51. package/src/styles/_main.scss +2 -0
  52. package/src/styles/_motion.scss +34 -0
  53. package/src/styles/_snackbar.scss +110 -0
  54. package/src/styles/_supported-device-table.scss +9 -0
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Automatically generated by SVGR from assets/icons/*.svg.
3
+ * Do not edit this file or add other components to this directory.
4
+ */
5
+ import type { SVGProps } from 'react'
6
+ export function CheckGreenIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
7
+ return (
8
+ <svg
9
+ xmlns='http://www.w3.org/2000/svg'
10
+ width={24}
11
+ height={24}
12
+ fill='none'
13
+ {...props}
14
+ >
15
+ <mask
16
+ id='check-green_svg__a'
17
+ width={24}
18
+ height={24}
19
+ x={0}
20
+ y={0}
21
+ maskUnits='userSpaceOnUse'
22
+ style={{
23
+ maskType: 'alpha',
24
+ }}
25
+ >
26
+ <path fill='#D9D9D9' d='M0 0h24v24H0z' />
27
+ </mask>
28
+ <g mask='url(#check-green_svg__a)'>
29
+ <path
30
+ fill='#27AE60'
31
+ d='m10.6 16.6 7.05-7.05-1.4-1.4-5.65 5.65-2.85-2.85-1.4 1.4 4.25 4.25ZM12 22a9.733 9.733 0 0 1-3.9-.788 10.092 10.092 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.733 9.733 0 0 1 2 12c0-1.383.263-2.683.788-3.9a10.092 10.092 0 0 1 2.137-3.175c.9-.9 1.958-1.613 3.175-2.138A9.743 9.743 0 0 1 12 2c1.383 0 2.683.262 3.9.787a10.105 10.105 0 0 1 3.175 2.138c.9.9 1.612 1.958 2.137 3.175A9.733 9.733 0 0 1 22 12a9.733 9.733 0 0 1-.788 3.9 10.092 10.092 0 0 1-2.137 3.175c-.9.9-1.958 1.612-3.175 2.137A9.733 9.733 0 0 1 12 22Z'
32
+ />
33
+ </g>
34
+ </svg>
35
+ )
36
+ }
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Automatically generated by SVGR from assets/icons/*.svg.
3
+ * Do not edit this file or add other components to this directory.
4
+ */
5
+ import type { SVGProps } from 'react'
6
+ export function CloseWhiteIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
7
+ return (
8
+ <svg
9
+ xmlns='http://www.w3.org/2000/svg'
10
+ width={24}
11
+ height={24}
12
+ fill='none'
13
+ {...props}
14
+ >
15
+ <mask
16
+ id='close-white_svg__a'
17
+ width={24}
18
+ height={24}
19
+ x={0}
20
+ y={0}
21
+ maskUnits='userSpaceOnUse'
22
+ style={{
23
+ maskType: 'alpha',
24
+ }}
25
+ >
26
+ <path fill='#D9D9D9' d='M0 0h24v24H0z' />
27
+ </mask>
28
+ <g mask='url(#close-white_svg__a)'>
29
+ <path
30
+ fill='#fff'
31
+ d='M6.4 19 5 17.6l5.6-5.6L5 6.4 6.4 5l5.6 5.6L17.6 5 19 6.4 13.4 12l5.6 5.6-1.4 1.4-5.6-5.6L6.4 19Z'
32
+ />
33
+ </g>
34
+ </svg>
35
+ )
36
+ }
@@ -24,6 +24,7 @@ import { NestedEditAccessCodeForm } from 'lib/seam/components/EditAccessCodeForm
24
24
  import { IconButton } from 'lib/ui/IconButton.js'
25
25
  import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
26
26
  import { LoadingToast } from 'lib/ui/LoadingToast/LoadingToast.js'
27
+ import { Snackbar } from 'lib/ui/Snackbar/Snackbar.js'
27
28
  import { EmptyPlaceholder } from 'lib/ui/Table/EmptyPlaceholder.js'
28
29
  import { TableBody } from 'lib/ui/Table/TableBody.js'
29
30
  import { TableHeader } from 'lib/ui/Table/TableHeader.js'
@@ -130,6 +131,13 @@ export function AccessCodeTable({
130
131
  [setSelectedEditAccessCodeId]
131
132
  )
132
133
 
134
+ const [accessCodeResult, setAccessCodeResult] = useState<
135
+ 'created' | 'updated' | null
136
+ >(null)
137
+
138
+ const accessCodeResultMessage =
139
+ accessCodeResult === 'created' ? t.accesCodeCreated : t.accesCodeUpdated
140
+
133
141
  if (selectedEditAccessCodeId != null) {
134
142
  return (
135
143
  <NestedEditAccessCodeForm
@@ -141,6 +149,9 @@ export function AccessCodeTable({
141
149
  onBack={() => {
142
150
  setSelectedEditAccessCodeId(null)
143
151
  }}
152
+ onSuccess={() => {
153
+ setAccessCodeResult('updated')
154
+ }}
144
155
  className={className}
145
156
  />
146
157
  )
@@ -148,20 +159,31 @@ export function AccessCodeTable({
148
159
 
149
160
  if (selectedViewAccessCodeId != null) {
150
161
  return (
151
- <NestedAccessCodeDetails
152
- accessCodeId={selectedViewAccessCodeId}
153
- onEdit={() => {
154
- setSelectedEditAccessCodeId(selectedViewAccessCodeId)
155
- }}
156
- disableLockUnlock={disableLockUnlock}
157
- disableCreateAccessCode={disableCreateAccessCode}
158
- disableEditAccessCode={disableEditAccessCode}
159
- disableDeleteAccessCode={disableDeleteAccessCode}
160
- onBack={() => {
161
- setSelectedViewAccessCodeId(null)
162
- }}
163
- className={className}
164
- />
162
+ <>
163
+ <Snackbar
164
+ variant='success'
165
+ message={accessCodeResultMessage}
166
+ visible={accessCodeResult != null}
167
+ autoDismiss
168
+ onClose={() => {
169
+ setAccessCodeResult(null)
170
+ }}
171
+ />
172
+ <NestedAccessCodeDetails
173
+ accessCodeId={selectedViewAccessCodeId}
174
+ onEdit={() => {
175
+ setSelectedEditAccessCodeId(selectedViewAccessCodeId)
176
+ }}
177
+ disableLockUnlock={disableLockUnlock}
178
+ disableCreateAccessCode={disableCreateAccessCode}
179
+ disableEditAccessCode={disableEditAccessCode}
180
+ disableDeleteAccessCode={disableDeleteAccessCode}
181
+ onBack={() => {
182
+ setSelectedViewAccessCodeId(null)
183
+ }}
184
+ className={className}
185
+ />
186
+ </>
165
187
  )
166
188
  }
167
189
 
@@ -175,6 +197,9 @@ export function AccessCodeTable({
175
197
  disableDeleteAccessCode={disableDeleteAccessCode}
176
198
  onBack={toggleAddAccessCodeForm}
177
199
  className={className}
200
+ onSuccess={() => {
201
+ setAccessCodeResult('created')
202
+ }}
178
203
  />
179
204
  )
180
205
  }
@@ -184,52 +209,63 @@ export function AccessCodeTable({
184
209
  }
185
210
 
186
211
  return (
187
- <div className={classNames('seam-table', className)}>
188
- <ContentHeader onBack={onBack} />
189
- <TableHeader>
190
- <div className='seam-left'>
191
- {title != null ? (
192
- <TableTitle>
193
- {heading ?? title ?? t.accessCodes}{' '}
194
- <Caption>({filteredAccessCodes.length})</Caption>
195
- </TableTitle>
196
- ) : (
197
- <div className='seam-fragment' />
198
- )}
199
- {!disableCreateAccessCode && (
200
- <IconButton
201
- onClick={toggleAddAccessCodeForm}
202
- className='seam-add-button'
203
- >
204
- <AddIcon />
205
- </IconButton>
212
+ <>
213
+ <Snackbar
214
+ variant='success'
215
+ message={accessCodeResultMessage}
216
+ visible={accessCodeResult != null}
217
+ autoDismiss
218
+ onClose={() => {
219
+ setAccessCodeResult(null)
220
+ }}
221
+ />
222
+ <div className={classNames('seam-table', className)}>
223
+ <ContentHeader onBack={onBack} />
224
+ <TableHeader>
225
+ <div className='seam-left'>
226
+ {title != null ? (
227
+ <TableTitle>
228
+ {heading ?? title ?? t.accessCodes}{' '}
229
+ <Caption>({filteredAccessCodes.length})</Caption>
230
+ </TableTitle>
231
+ ) : (
232
+ <div className='seam-fragment' />
233
+ )}
234
+ {!disableCreateAccessCode && (
235
+ <IconButton
236
+ onClick={toggleAddAccessCodeForm}
237
+ className='seam-add-button'
238
+ >
239
+ <AddIcon />
240
+ </IconButton>
241
+ )}
242
+ </div>
243
+ <div className='seam-table-header-loading-wrap'>
244
+ <LoadingToast
245
+ isLoading={isInitialLoading}
246
+ label={t.loading}
247
+ top={-20}
248
+ />
249
+ </div>
250
+ {!disableSearch && (
251
+ <SearchTextField
252
+ value={searchInputValue}
253
+ onChange={setSearchInputValue}
254
+ disabled={(accessCodes?.length ?? 0) === 0}
255
+ />
206
256
  )}
207
- </div>
208
- <div className='seam-table-header-loading-wrap'>
209
- <LoadingToast
210
- isLoading={isInitialLoading}
211
- label={t.loading}
212
- top={-20}
257
+ </TableHeader>
258
+ <TableBody>
259
+ <Content
260
+ accessCodes={filteredAccessCodes}
261
+ onAccessCodeClick={handleAccessCodeClick}
262
+ onAccessCodeEdit={handleAccessCodeEdit}
263
+ disableEditAccessCode={disableEditAccessCode}
264
+ disableDeleteAccessCode={disableDeleteAccessCode}
213
265
  />
214
- </div>
215
- {!disableSearch && (
216
- <SearchTextField
217
- value={searchInputValue}
218
- onChange={setSearchInputValue}
219
- disabled={(accessCodes?.length ?? 0) === 0}
220
- />
221
- )}
222
- </TableHeader>
223
- <TableBody>
224
- <Content
225
- accessCodes={filteredAccessCodes}
226
- onAccessCodeClick={handleAccessCodeClick}
227
- onAccessCodeEdit={handleAccessCodeEdit}
228
- disableEditAccessCode={disableEditAccessCode}
229
- disableDeleteAccessCode={disableDeleteAccessCode}
230
- />
231
- </TableBody>
232
- </div>
266
+ </TableBody>
267
+ </div>
268
+ </>
233
269
  )
234
270
  }
235
271
 
@@ -296,4 +332,6 @@ const t = {
296
332
  accessCodes: 'Access Codes',
297
333
  noAccessCodesMessage: 'Sorry, no access codes were found',
298
334
  loading: 'Loading access codes',
335
+ accesCodeUpdated: 'Access code updated',
336
+ accesCodeCreated: 'Access code created',
299
337
  }
@@ -5,6 +5,7 @@ export const name = 'seam-create-access-code-form'
5
5
 
6
6
  export const props: ElementProps<CreateAccessCodeFormProps> = {
7
7
  deviceId: 'string',
8
+ onSuccess: 'object',
8
9
  }
9
10
 
10
11
  export { CreateAccessCodeForm as Component } from './CreateAccessCodeForm.js'
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react'
2
- import type { SeamError } from 'seamapi'
2
+ import type { AccessCode, SeamError } from 'seamapi'
3
3
 
4
4
  import { useComponentTelemetry } from 'lib/telemetry/index.js'
5
5
 
@@ -18,6 +18,7 @@ import {
18
18
 
19
19
  export interface CreateAccessCodeFormProps extends CommonProps {
20
20
  deviceId: string
21
+ onSuccess?: (accessCodeId: string) => void
21
22
  }
22
23
 
23
24
  export const NestedCreateAccessCodeForm =
@@ -27,6 +28,7 @@ export function CreateAccessCodeForm({
27
28
  className,
28
29
  onBack,
29
30
  deviceId,
31
+ onSuccess,
30
32
  }: CreateAccessCodeFormProps): JSX.Element | null {
31
33
  useComponentTelemetry('CreateAccessCodeForm')
32
34
 
@@ -38,18 +40,30 @@ export function CreateAccessCodeForm({
38
40
  return null
39
41
  }
40
42
 
41
- return <Content device={device} className={className} onBack={onBack} />
43
+ return (
44
+ <Content
45
+ device={device}
46
+ className={className}
47
+ onBack={onBack}
48
+ onSuccess={onSuccess}
49
+ />
50
+ )
42
51
  }
43
52
 
44
53
  function Content({
45
54
  device,
46
55
  className,
47
56
  onBack,
57
+ onSuccess,
48
58
  }: Omit<CreateAccessCodeFormProps, 'deviceId'> & {
49
59
  device: NonNullable<UseDeviceData>
50
60
  }): JSX.Element {
51
61
  const { submit, isSubmitting, responseErrors } = useSubmitCreateAccessCode({
52
- onSuccess: () => {
62
+ onSuccess: (accessCode: AccessCode) => {
63
+ if (onSuccess != null) {
64
+ onSuccess(accessCode.access_code_id)
65
+ }
66
+
53
67
  if (onBack != null) {
54
68
  onBack()
55
69
  }
@@ -68,7 +82,9 @@ function Content({
68
82
  )
69
83
  }
70
84
 
71
- function useSubmitCreateAccessCode(params: { onSuccess: () => void }): {
85
+ function useSubmitCreateAccessCode(params: {
86
+ onSuccess: (accessCode: AccessCode) => void
87
+ }): {
72
88
  submit: (data: AccessCodeFormSubmitData) => void
73
89
  isSubmitting: boolean
74
90
  responseErrors: ResponseErrors | null
@@ -128,10 +128,7 @@ export function ThermostatDeviceDetails(
128
128
  </DetailRow>
129
129
  </DetailSection>
130
130
 
131
- <DetailSection
132
- label={t.deviceDetails}
133
- tooltipContent={t.deviceDetailsTooltip}
134
- >
131
+ <DetailSection label={t.deviceDetails}>
135
132
  <DetailRow label={t.brand}>
136
133
  <div className='seam-detail-row-hstack'>
137
134
  {device.properties.model.manufacturer_display_name}
@@ -172,8 +169,6 @@ const t = {
172
169
  defaultClimate: 'Default climate',
173
170
  allowManualOverride: 'Allow manual override',
174
171
  deviceDetails: 'Device details',
175
- deviceDetailsTooltip:
176
- 'When a scheduled climate reaches its end time, the default settings will kick in.',
177
172
  brand: 'Brand',
178
173
  linkedAccount: 'Linked account',
179
174
  deviceId: 'Device ID',
@@ -5,6 +5,7 @@ export const name = 'seam-edit-access-code-form'
5
5
 
6
6
  export const props: ElementProps<EditAccessCodeFormProps> = {
7
7
  accessCodeId: 'string',
8
+ onSuccess: 'object',
8
9
  }
9
10
 
10
11
  export { EditAccessCodeForm as Component } from './EditAccessCodeForm.js'
@@ -19,6 +19,7 @@ import {
19
19
 
20
20
  export interface EditAccessCodeFormProps extends CommonProps {
21
21
  accessCodeId: string
22
+ onSuccess?: (accessCodeId: string) => void
22
23
  }
23
24
 
24
25
  export const NestedEditAccessCodeForm =
@@ -28,6 +29,7 @@ export function EditAccessCodeForm({
28
29
  accessCodeId,
29
30
  onBack,
30
31
  className,
32
+ onSuccess,
31
33
  }: EditAccessCodeFormProps): JSX.Element | null {
32
34
  useComponentTelemetry('EditAccessCodeForm')
33
35
 
@@ -40,7 +42,12 @@ export function EditAccessCodeForm({
40
42
  }
41
43
 
42
44
  return (
43
- <Content accessCode={accessCode} className={className} onBack={onBack} />
45
+ <Content
46
+ accessCode={accessCode}
47
+ className={className}
48
+ onBack={onBack}
49
+ onSuccess={onSuccess}
50
+ />
44
51
  )
45
52
  }
46
53
 
@@ -48,6 +55,7 @@ function Content({
48
55
  className,
49
56
  onBack,
50
57
  accessCode,
58
+ onSuccess,
51
59
  }: Omit<EditAccessCodeFormProps, 'accessCodeId'> & {
52
60
  accessCode: NonNullable<UseAccessCodeData>
53
61
  }): JSX.Element | null {
@@ -57,7 +65,15 @@ function Content({
57
65
 
58
66
  const { submit, isSubmitting, responseErrors } = useSubmitEditAccessCode(
59
67
  accessCode,
60
- onBack
68
+ () => {
69
+ if (onSuccess != null) {
70
+ onSuccess(accessCode.access_code_id)
71
+ }
72
+
73
+ if (onBack != null) {
74
+ onBack()
75
+ }
76
+ }
61
77
  )
62
78
 
63
79
  if (device == null) {
@@ -36,7 +36,10 @@ export function FilterCategoryMenu({
36
36
  onAllOptionSelect,
37
37
  buttonLabel,
38
38
  }: FilterCategoryMenuProps): JSX.Element {
39
- const usableOptions = hideAllOption ? options : [allLabel, ...options]
39
+ const sortedOptions = [...options].sort((a, b) => a.localeCompare(b))
40
+ const usableOptions = hideAllOption
41
+ ? sortedOptions
42
+ : [allLabel, ...sortedOptions]
40
43
 
41
44
  return (
42
45
  <div className='seam-supported-device-table-filter-menu-wrap'>
@@ -49,20 +52,22 @@ export function FilterCategoryMenu({
49
52
  </button>
50
53
  )}
51
54
  >
52
- {usableOptions.map((option, index) => (
53
- <MenuItem
54
- key={`${index}:${option}`}
55
- onClick={() => {
56
- if (option === allLabel) {
57
- onAllOptionSelect?.()
58
- } else {
59
- onSelect(option)
60
- }
61
- }}
62
- >
63
- <span>{option}</span>
64
- </MenuItem>
65
- ))}
55
+ <div className='seam-supported-device-table-filter-menu-content'>
56
+ {usableOptions.map((option, index) => (
57
+ <MenuItem
58
+ key={`${index}:${option}`}
59
+ onClick={() => {
60
+ if (option === allLabel) {
61
+ onAllOptionSelect?.()
62
+ } else {
63
+ onSelect(option)
64
+ }
65
+ }}
66
+ >
67
+ <span>{option}</span>
68
+ </MenuItem>
69
+ ))}
70
+ </div>
66
71
  </Menu>
67
72
  </div>
68
73
  )
@@ -42,21 +42,20 @@ export function Menu({
42
42
  backgroundProps,
43
43
  }: MenuProps): JSX.Element | null {
44
44
  const { Provider } = menuContext
45
+ const [documentEl, setDocumentEl] = useState<null | HTMLElement>(null)
46
+ const [bodyEl, setBodyEl] = useState<null | HTMLElement>(null)
45
47
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
46
- const [documentEl, setDocumentEl] = useState<null | Element>(null)
47
48
  const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null)
48
49
  const [top, setTop] = useState(0)
49
50
  const [left, setLeft] = useState(0)
50
51
 
51
52
  useEffect(() => {
52
- const containers = globalThis.document?.querySelectorAll(
53
- seamComponentsClassName
54
- )
55
- if (containers == null) return
56
- const el = containers[containers.length - 1]
57
- if (el != null) {
58
- setDocumentEl(el)
59
- }
53
+ const documentEl = globalThis.document.documentElement
54
+ setDocumentEl(documentEl)
55
+
56
+ const bodyElements = documentEl?.getElementsByTagName('body')
57
+ if (bodyElements[0] == null) return
58
+ setBodyEl(bodyElements[0])
60
59
  }, [setDocumentEl])
61
60
 
62
61
  const handleClose = (): void => {
@@ -68,18 +67,24 @@ export function Menu({
68
67
  }
69
68
 
70
69
  const setPositions = useCallback(() => {
71
- if (anchorEl == null || contentEl == null || documentEl == null) return
72
-
73
- const { right: containerRight, bottom: containerBottom } =
74
- documentEl.getBoundingClientRect()
70
+ if (
71
+ anchorEl == null ||
72
+ contentEl == null ||
73
+ bodyEl == null ||
74
+ documentEl == null
75
+ )
76
+ return
75
77
 
76
- const { height: anchorHeight } = anchorEl.getBoundingClientRect()
78
+ const containerRight = documentEl.offsetLeft + documentEl.clientWidth
79
+ const containerBottom = documentEl.offsetTop + documentEl.clientHeight
77
80
 
78
- const anchorTop = anchorEl.offsetTop
79
- const anchorLeft = anchorEl.offsetLeft
81
+ const anchorBox = anchorEl.getBoundingClientRect()
82
+ const anchorTop = anchorBox.top + bodyEl.clientTop
83
+ const anchorLeft = anchorBox.left + bodyEl.clientLeft
84
+ const anchorHeight = anchorEl.offsetHeight
80
85
 
81
- const { width: contentWidth, height: contentHeight } =
82
- contentEl.getBoundingClientRect()
86
+ const contentWidth = contentEl.offsetWidth
87
+ const contentHeight = contentEl.offsetHeight
83
88
 
84
89
  const anchorBottom = anchorTop + anchorHeight
85
90
 
@@ -97,17 +102,20 @@ export function Menu({
97
102
 
98
103
  // If the content would overflow bottom, position it above the anchor.
99
104
  const isOverFlowingBottom = bottom > containerBottom
100
- const visibleTop = isOverFlowingBottom
101
- ? anchorTop - contentHeight - verticalOffset
102
- : top
105
+ const topWhenAboveAnchor = anchorTop - contentHeight - verticalOffset
106
+
107
+ // Only open the menu above the anchor if it won't get clipped, i.e., not < 0.
108
+ const visibleTop =
109
+ isOverFlowingBottom && topWhenAboveAnchor > 0 ? topWhenAboveAnchor : top
103
110
  setTop(visibleTop)
104
111
  }, [
105
112
  anchorEl,
106
113
  horizontalOffset,
107
114
  verticalOffset,
108
115
  contentEl,
109
- documentEl,
110
116
  edgeOffset,
117
+ bodyEl,
118
+ documentEl,
111
119
  ])
112
120
 
113
121
  useLayoutEffect(() => {
@@ -124,7 +132,7 @@ export function Menu({
124
132
  const hasSetPosition = top !== 0 && left !== 0
125
133
  const visible = isOpen && hasSetPosition
126
134
 
127
- if (documentEl == null) {
135
+ if (bodyEl == null) {
128
136
  return null
129
137
  }
130
138
 
@@ -136,28 +144,30 @@ export function Menu({
136
144
  >
137
145
  {renderButton({ onOpen: handleOpen })}
138
146
  {createPortal(
139
- <div
140
- className={classNames(
141
- 'seam-menu-bg',
142
- backgroundProps?.className,
143
- visible ? 'seam-menu-visible' : 'seam-menu-hidden'
144
- )}
145
- onClick={(event) => {
146
- event.stopPropagation()
147
- handleClose()
148
- }}
149
- >
147
+ <div className={seamComponentsClassName}>
150
148
  <div
151
- className='seam-menu-content'
152
- style={{
153
- top,
154
- left,
149
+ className={classNames(
150
+ 'seam-menu-bg',
151
+ backgroundProps?.className,
152
+ visible ? 'seam-menu-visible' : 'seam-menu-hidden'
153
+ )}
154
+ onClick={(event) => {
155
+ event.stopPropagation()
156
+ handleClose()
155
157
  }}
156
158
  >
157
- {children}
159
+ <div
160
+ className='seam-menu-content'
161
+ style={{
162
+ top,
163
+ left,
164
+ }}
165
+ >
166
+ {children}
167
+ </div>
158
168
  </div>
159
169
  </div>,
160
- documentEl
170
+ bodyEl
161
171
  )}
162
172
 
163
173
  {/*