@softize/opus 8.7.1 → 8.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softize/opus",
3
- "version": "8.7.1",
3
+ "version": "8.8.0",
4
4
  "description": "End-to-end action protocol for TypeScript. Single package with subpath exports (core + adapters).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import * as React from "react"
2
2
  import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
3
3
  import { DropdownMenu as MenuPrimitive } from "radix-ui"
4
4
 
5
+ import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts"
5
6
  import { cn } from '../../lib/cn.ts'
6
7
 
7
8
  function Menu({
@@ -34,9 +35,13 @@ function MenuContent({
34
35
  sideOffset = 4,
35
36
  ...props
36
37
  }: React.ComponentProps<typeof MenuPrimitive.Content>) {
38
+ // Mesma razão do Popover: clique dentro de iframe não chega aqui fora.
39
+ const ref = React.useRef<HTMLDivElement>(null)
40
+ useDismissOnIframeFocus(ref)
37
41
  return (
38
42
  <MenuPrimitive.Portal>
39
43
  <MenuPrimitive.Content
44
+ ref={ref}
40
45
  data-slot="menu-content"
41
46
  sideOffset={sideOffset}
42
47
  className={cn(
@@ -1,6 +1,7 @@
1
1
  import * as React from "react"
2
2
  import { Popover as PopoverPrimitive } from "radix-ui"
3
3
 
4
+ import { useDismissOnIframeFocus } from "../../lib/dismiss-on-iframe.ts"
4
5
  import { cn } from '../../lib/cn.ts'
5
6
 
6
7
  function Popover({
@@ -21,9 +22,14 @@ function PopoverContent({
21
22
  sideOffset = 4,
22
23
  ...props
23
24
  }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
25
+ // Clique dentro de um iframe não chega no documento de fora — sem isto, o popover fica
26
+ // pendurado sobre o conteúdo. Na cabine, o preview É um iframe.
27
+ const ref = React.useRef<HTMLDivElement>(null)
28
+ useDismissOnIframeFocus(ref)
24
29
  return (
25
30
  <PopoverPrimitive.Portal>
26
31
  <PopoverPrimitive.Content
32
+ ref={ref}
27
33
  data-slot="popover-content"
28
34
  align={align}
29
35
  sideOffset={sideOffset}
@@ -0,0 +1,36 @@
1
+ import * as React from 'react'
2
+
3
+ /**
4
+ * Fecha a camada flutuante quando o clique cai DENTRO de um iframe.
5
+ *
6
+ * O navegador entrega o clique ao documento do iframe, e o documento de fora nunca fica
7
+ * sabendo. Como toda dispensa por "clique fora" escuta eventos do documento de fora, o
8
+ * popover/menu simplesmente NÃO fecha — fica pendurado sobre o conteúdo, e a pessoa
9
+ * precisa achar um pedaço de página livre pra clicar.
10
+ *
11
+ * Não é caso de borda pra nós: a cabine embute o preview num iframe, então metade da tela
12
+ * é justamente a área onde clicar não fecha nada.
13
+ *
14
+ * O sinal utilizável é o `blur` da JANELA somado a "quem está com o foco agora é um
15
+ * iframe". Trocar de aba ou de aplicativo também dispara `blur`, mas ali o elemento ativo
16
+ * não é iframe — e fechar o menu de quem só foi ver outra janela seria pior que o defeito.
17
+ *
18
+ * A dispensa sai como um Escape sintético em vez de uma chamada direta: quem renderiza o
19
+ * conteúdo (Popover, Menu) não tem acesso ao estado de aberto — ele é de quem usa. Escape
20
+ * é o canal que a própria biblioteca já escuta pra dispensar, então funciona sem obrigar
21
+ * cada call site a passar um `onClose`.
22
+ */
23
+ export function useDismissOnIframeFocus(ref: React.RefObject<HTMLElement | null>): void {
24
+ React.useEffect(() => {
25
+ const onBlur = (): void => {
26
+ // `requestAnimationFrame`: no instante do `blur` o `activeElement` ainda pode ser o
27
+ // anterior; o navegador atualiza no tick seguinte.
28
+ requestAnimationFrame(() => {
29
+ if (document.activeElement?.tagName !== 'IFRAME') return
30
+ ref.current?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
31
+ })
32
+ }
33
+ window.addEventListener('blur', onBlur)
34
+ return () => window.removeEventListener('blur', onBlur)
35
+ }, [ref])
36
+ }