@proef/common-ui 2.6.3 → 2.6.5
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/dist/esm/v1/components/overlays/Dialog/Dialog.js +1 -1
- package/dist/esm/v1/components/overlays/Dialog/Dialog.js.map +1 -1
- package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js +48 -6
- package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js.map +1 -1
- package/dist/style.components.css +1 -1
- package/dist/style.css +1 -1
- package/dist/style.layered.css +1 -1
- package/dist/types/v1/primitives/forms/SelectInput/SelectInput.d.ts +23 -0
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { createPortal } from "react-dom";
|
|
|
4
4
|
const DialogContainerContext = createContext(null);
|
|
5
5
|
const DialogDepthContext = createContext(0);
|
|
6
6
|
const useDialogContainer = () => useContext(DialogContainerContext);
|
|
7
|
-
const WIDTHS = { sm: "max-w-dialog-sm", md: "max-w-dialog-md", lg: "max-w-dialog-lg", xl: "max-w-dialog-xl" };
|
|
7
|
+
const WIDTHS = { sm: "max-w-dialog-sm", md: "max-w-dialog-md", lg: "max-w-dialog-lg", xl: "max-w-dialog-xl max-sm:max-w-none" };
|
|
8
8
|
const abertas = [];
|
|
9
9
|
const silenciados = /* @__PURE__ */ new Set();
|
|
10
10
|
const doTopo = () => abertas[abertas.length - 1];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.js","sources":["../../../../../../src/v1/components/overlays/Dialog/Dialog.tsx"],"sourcesContent":["import React, { createContext, useContext, useEffect, useId, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\n\r\nconst DialogContainerContext = createContext<HTMLElement | null>(null)\r\nconst DialogDepthContext = createContext(0)\r\n\r\n/**\r\n * O elemento da janela, para usar como destino de `createPortal` (ex: avisos,\r\n * confirmações, sobreposições) quando o conteúdo deve ficar junto dela. Um\r\n * portal para `document.body` também funciona, desde que fique por cima\r\n * (`z-index` acima de 200). Fora de um Dialog devolve `null`.\r\n *\r\n * `createPortal(conteudo, useDialogContainer() ?? document.body)`\r\n */\r\nexport const useDialogContainer = () => useContext(DialogContainerContext)\r\n\r\nexport interface DialogProps {\r\n /** Aberta ou fechada. A app guarda este estado. */\r\n open: boolean\r\n /** Pedido para fechar (×, Esc ou clique fora): a app põe `open` a `false`. */\r\n onClose: () => void\r\n /** Normalmente texto; aceita JSX (ex: um nome em negrito). */\r\n title: React.ReactNode\r\n /** Linha por baixo do título (ex: nome · departamento). */\r\n subtitle?: React.ReactNode\r\n /** Botões no rodapé, sempre visíveis (ex: Confirmar, Fechar). */\r\n footer?: React.ReactNode\r\n /**\r\n * Largura máxima: `sm` 480px, `md` 720px, `lg` 1000px, `xl` até 1600px (95% do ecrã, para janelas com\r\n * mapa e tabela lado a lado). Nunca passa o ecrã.\r\n */\r\n size?: 'sm' | 'md' | 'lg' | 'xl'\r\n /**\r\n * Altura: `auto` (por omissão) cresce com o conteúdo; `full` ocupa sempre a\r\n * altura disponível (útil quando o conteúdo muda de tamanho e a janela não\r\n * deve saltar). Em ambos, se o conteúdo não couber, o corpo faz scroll.\r\n */\r\n height?: 'auto' | 'full'\r\n /** Fechar ao clicar fora da janela. Por omissão sim; num formulário longo pode valer a pena desligar. */\r\n closeOnBackdrop?: boolean\r\n /** Nome do botão ×, para leitores de ecrã. Por omissão: \"Fechar\". */\r\n closeLabel?: string\r\n /**\r\n * Corpo sem margens e sem scroll: o conteúdo ocupa-o todo e gere o seu próprio\r\n * scroll (ex: passos que deslizam, rodapé próprio de cada passo).\r\n */\r\n flush?: boolean\r\n children: React.ReactNode\r\n}\r\n\r\nconst WIDTHS = { sm: 'max-w-dialog-sm', md: 'max-w-dialog-md', lg: 'max-w-dialog-lg', xl: 'max-w-dialog-xl' }\r\n\r\ninterface Aberta {\r\n root: HTMLElement\r\n janela: HTMLElement | null\r\n depth: number\r\n}\r\n\r\nconst abertas: Aberta[] = []\r\nconst silenciados = new Set<Element>()\r\n\r\nconst doTopo = () => abertas[abertas.length - 1]\r\n\r\n// Scroll da página bloqueado enquanto houver pelo menos uma janela aberta. Um contador\r\n// partilhado, e não o \"antes\" de cada janela: com duas abertas que fecham fora de ordem\r\n// (ex: a de baixo desmonta primeiro), a última a fechar repunha o `hidden` e a página\r\n// ficava sem scroll até recarregar.\r\nlet bloqueios = 0\r\nlet overflowAntes = ''\r\n\r\nfunction bloquearScroll() {\r\n const html = document.documentElement\r\n if (bloqueios++ === 0) {\r\n overflowAntes = html.style.overflow\r\n html.style.overflow = 'hidden'\r\n }\r\n}\r\n\r\nfunction libertarScroll() {\r\n if (bloqueios === 0) return\r\n if (--bloqueios === 0) document.documentElement.style.overflow = overflowAntes\r\n}\r\n\r\nfunction silenciar() {\r\n for (const el of silenciados) el.removeAttribute('inert')\r\n silenciados.clear()\r\n const topo = doTopo()\r\n if (!topo) return\r\n for (const el of Array.from(document.body.children)) {\r\n if (el === topo.root || el.hasAttribute('inert')) continue\r\n el.setAttribute('inert', '')\r\n silenciados.add(el)\r\n }\r\n}\r\n\r\n// Uma janela aberta dentro de outra fica sempre por cima dela, mesmo que abram juntas.\r\nfunction empilhar(aberta: Aberta) {\r\n const acima = abertas.findIndex((a) => a.depth > aberta.depth)\r\n if (acima === -1) abertas.push(aberta)\r\n else abertas.splice(acima, 0, aberta)\r\n}\r\n\r\n/**\r\n * Janela por cima da página: cabeçalho navy com título, corpo que faz scroll e\r\n * rodapé com os botões ([Button](?path=/docs/v1-primitives-buttons-button--docs)).\r\n *\r\n * É a janela modal da V1 (substitui o `Modal` legacy).\r\n *\r\n * Fundo escurecido, Esc para fechar e, ao fechar, o foco volta ao sítio onde\r\n * estava. Enquanto está aberta, o resto da página fica\r\n * [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert):\r\n * nem rato, nem teclado, nem leitores de ecrã lhe chegam. Os portais abertos\r\n * depois dela (seletores, confirmações) continuam a responder.\r\n *\r\n * O conteúdo fica montado mesmo com a janela fechada, para poder carregar antes\r\n * de ela abrir.\r\n *\r\n * O tamanho adapta-se ao ecrã só com CSS: nunca passa a largura nem a altura\r\n * da janela do browser (com uma margem), e acompanha-a quando é\r\n * redimensionada. O que não couber faz scroll no corpo; o cabeçalho e o\r\n * rodapé ficam sempre visíveis.\r\n *\r\n * **Exemplo completo:** [Formulário em modal](?path=/docs/v1-patterns-forms-formulário-em-modal--docs).\r\n */\r\nexport default function Dialog({\r\n open,\r\n onClose,\r\n title,\r\n subtitle,\r\n footer,\r\n size = 'md',\r\n height = 'auto',\r\n closeOnBackdrop = true,\r\n closeLabel = 'Fechar',\r\n flush = false,\r\n children,\r\n}: DialogProps) {\r\n const [root, setRoot] = useState<HTMLDivElement | null>(null)\r\n const janela = useRef<HTMLDivElement>(null)\r\n const fundo = useRef<HTMLDivElement>(null)\r\n const titleId = useId()\r\n const depth = useContext(DialogDepthContext)\r\n const fechar = useRef(onClose)\r\n fechar.current = onClose\r\n // Clique fora: só conta se começou e acabou no fundo (arrastar a partir de um\r\n // campo para fora não fecha a janela).\r\n const pressedBackdrop = useRef(false)\r\n\r\n useEffect(() => {\r\n if (!open || !root) return\r\n const anterior = document.activeElement as HTMLElement | null\r\n const aberta = { root, janela: janela.current, depth }\r\n empilhar(aberta)\r\n silenciar()\r\n const topo = doTopo()\r\n const dentro = topo.janela?.contains(document.activeElement)\r\n if (!dentro) topo.janela?.focus()\r\n\r\n const onKeyDown = (e: KeyboardEvent) => {\r\n if (e.key !== 'Escape' || e.defaultPrevented) return\r\n if (doTopo() !== aberta) return\r\n fechar.current()\r\n }\r\n document.addEventListener('keydown', onKeyDown)\r\n\r\n return () => {\r\n document.removeEventListener('keydown', onKeyDown)\r\n abertas.splice(abertas.indexOf(aberta), 1)\r\n silenciar()\r\n const voltar = anterior && !anterior.closest('[inert]')\r\n if (voltar) anterior.focus()\r\n else doTopo()?.janela?.focus()\r\n }\r\n }, [open, root, depth])\r\n\r\n // A página por trás não faz scroll enquanto a janela está aberta.\r\n useEffect(() => {\r\n if (!open) return\r\n bloquearScroll()\r\n return libertarScroll\r\n }, [open])\r\n\r\n const noFundo = (e: React.SyntheticEvent) => e.target === fundo.current || e.target === e.currentTarget\r\n\r\n return createPortal(\r\n <div\r\n ref={setRoot}\r\n hidden={!open}\r\n onPointerDown={(e) => {\r\n pressedBackdrop.current = noFundo(e)\r\n }}\r\n onClick={(e) => {\r\n if (closeOnBackdrop && pressedBackdrop.current && noFundo(e)) onClose()\r\n }}\r\n // No telemóvel, a janela de altura total ocupa o ecrã todo (sem margem nem cantos): o cabeçalho, com o ×, fica sempre à vista.\r\n className={`fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${height === 'full' ? 'max-sm:p-0' : ''} ${open ? 'flex' : 'hidden'}`}\r\n >\r\n <div ref={fundo} className=\"absolute inset-0 bg-black/40 motion-safe:animate-backdrop-in\" />\r\n <div\r\n ref={janela}\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby={titleId}\r\n tabIndex={-1}\r\n className={\r\n // No escuro a sombra não se vê: a linha (a mesma dos menus) separa a janela do fundo.\r\n `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none dark:border dark:border-slate-700 ` +\r\n `motion-safe:animate-dialog-in ${height === 'full' ? 'h-full max-sm:rounded-none max-sm:border-0' : ''}`\r\n }\r\n >\r\n <header className=\"flex flex-none items-center justify-between gap-3 bg-gradient-to-br from-primary to-primary-strong px-5 py-3.5\">\r\n <div className=\"min-w-0\">\r\n <h2 id={titleId} className=\"m-0 truncate text-title font-bold text-primary-foreground\">\r\n {title}\r\n </h2>\r\n {subtitle && <p className=\"m-0 mt-0.5 truncate text-caption text-primary-foreground/75\">{subtitle}</p>}\r\n </div>\r\n <button\r\n type=\"button\"\r\n aria-label={closeLabel}\r\n title={closeLabel}\r\n onClick={onClose}\r\n className=\"flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-md border border-primary-foreground/25 bg-primary-foreground/15 text-base leading-none text-primary-foreground transition-colors hover:bg-primary-foreground/25 focus-visible:ring-2 focus-visible:ring-primary-foreground/60 focus-visible:outline-none\"\r\n >\r\n ×\r\n </button>\r\n </header>\r\n\r\n <DialogContainerContext.Provider value={root}>\r\n <DialogDepthContext.Provider value={depth + 1}>\r\n <div className={flush ? 'flex min-h-0 flex-1 flex-col overflow-hidden bg-canvas' : 'min-h-0 flex-1 overflow-y-auto bg-canvas px-5 py-4'}>{children}</div>\r\n\r\n {footer && <footer className=\"flex flex-none flex-wrap justify-end gap-2 border-t border-border bg-muted/40 px-5 py-[18px]\">{footer}</footer>}\r\n </DialogDepthContext.Provider>\r\n </DialogContainerContext.Provider>\r\n </div>\r\n </div>,\r\n document.body,\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Dialog.js","sources":["../../../../../../src/v1/components/overlays/Dialog/Dialog.tsx"],"sourcesContent":["import React, { createContext, useContext, useEffect, useId, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\n\r\nconst DialogContainerContext = createContext<HTMLElement | null>(null)\r\nconst DialogDepthContext = createContext(0)\r\n\r\n/**\r\n * O elemento da janela, para usar como destino de `createPortal` (ex: avisos,\r\n * confirmações, sobreposições) quando o conteúdo deve ficar junto dela. Um\r\n * portal para `document.body` também funciona, desde que fique por cima\r\n * (`z-index` acima de 200). Fora de um Dialog devolve `null`.\r\n *\r\n * `createPortal(conteudo, useDialogContainer() ?? document.body)`\r\n */\r\nexport const useDialogContainer = () => useContext(DialogContainerContext)\r\n\r\nexport interface DialogProps {\r\n /** Aberta ou fechada. A app guarda este estado. */\r\n open: boolean\r\n /** Pedido para fechar (×, Esc ou clique fora): a app põe `open` a `false`. */\r\n onClose: () => void\r\n /** Normalmente texto; aceita JSX (ex: um nome em negrito). */\r\n title: React.ReactNode\r\n /** Linha por baixo do título (ex: nome · departamento). */\r\n subtitle?: React.ReactNode\r\n /** Botões no rodapé, sempre visíveis (ex: Confirmar, Fechar). */\r\n footer?: React.ReactNode\r\n /**\r\n * Largura máxima: `sm` 480px, `md` 720px, `lg` 1000px, `xl` até 1600px (95% do ecrã, para janelas com\r\n * mapa e tabela lado a lado). Nunca passa o ecrã.\r\n */\r\n size?: 'sm' | 'md' | 'lg' | 'xl'\r\n /**\r\n * Altura: `auto` (por omissão) cresce com o conteúdo; `full` ocupa sempre a\r\n * altura disponível (útil quando o conteúdo muda de tamanho e a janela não\r\n * deve saltar). Em ambos, se o conteúdo não couber, o corpo faz scroll.\r\n */\r\n height?: 'auto' | 'full'\r\n /** Fechar ao clicar fora da janela. Por omissão sim; num formulário longo pode valer a pena desligar. */\r\n closeOnBackdrop?: boolean\r\n /** Nome do botão ×, para leitores de ecrã. Por omissão: \"Fechar\". */\r\n closeLabel?: string\r\n /**\r\n * Corpo sem margens e sem scroll: o conteúdo ocupa-o todo e gere o seu próprio\r\n * scroll (ex: passos que deslizam, rodapé próprio de cada passo).\r\n */\r\n flush?: boolean\r\n children: React.ReactNode\r\n}\r\n\r\n// O `xl` é 95% do ecrã: no telemóvel deixava uma faixa do fundo escurecido de cada lado,\r\n// mesmo em `height=\"full\"` (que aí ocupa o ecrã todo). Abaixo de `sm` a margem da janela\r\n// já limita a largura, como nos outros tamanhos.\r\nconst WIDTHS = { sm: 'max-w-dialog-sm', md: 'max-w-dialog-md', lg: 'max-w-dialog-lg', xl: 'max-w-dialog-xl max-sm:max-w-none' }\r\n\r\ninterface Aberta {\r\n root: HTMLElement\r\n janela: HTMLElement | null\r\n depth: number\r\n}\r\n\r\nconst abertas: Aberta[] = []\r\nconst silenciados = new Set<Element>()\r\n\r\nconst doTopo = () => abertas[abertas.length - 1]\r\n\r\n// Scroll da página bloqueado enquanto houver pelo menos uma janela aberta. Um contador\r\n// partilhado, e não o \"antes\" de cada janela: com duas abertas que fecham fora de ordem\r\n// (ex: a de baixo desmonta primeiro), a última a fechar repunha o `hidden` e a página\r\n// ficava sem scroll até recarregar.\r\nlet bloqueios = 0\r\nlet overflowAntes = ''\r\n\r\nfunction bloquearScroll() {\r\n const html = document.documentElement\r\n if (bloqueios++ === 0) {\r\n overflowAntes = html.style.overflow\r\n html.style.overflow = 'hidden'\r\n }\r\n}\r\n\r\nfunction libertarScroll() {\r\n if (bloqueios === 0) return\r\n if (--bloqueios === 0) document.documentElement.style.overflow = overflowAntes\r\n}\r\n\r\nfunction silenciar() {\r\n for (const el of silenciados) el.removeAttribute('inert')\r\n silenciados.clear()\r\n const topo = doTopo()\r\n if (!topo) return\r\n for (const el of Array.from(document.body.children)) {\r\n if (el === topo.root || el.hasAttribute('inert')) continue\r\n el.setAttribute('inert', '')\r\n silenciados.add(el)\r\n }\r\n}\r\n\r\n// Uma janela aberta dentro de outra fica sempre por cima dela, mesmo que abram juntas.\r\nfunction empilhar(aberta: Aberta) {\r\n const acima = abertas.findIndex((a) => a.depth > aberta.depth)\r\n if (acima === -1) abertas.push(aberta)\r\n else abertas.splice(acima, 0, aberta)\r\n}\r\n\r\n/**\r\n * Janela por cima da página: cabeçalho navy com título, corpo que faz scroll e\r\n * rodapé com os botões ([Button](?path=/docs/v1-primitives-buttons-button--docs)).\r\n *\r\n * É a janela modal da V1 (substitui o `Modal` legacy).\r\n *\r\n * Fundo escurecido, Esc para fechar e, ao fechar, o foco volta ao sítio onde\r\n * estava. Enquanto está aberta, o resto da página fica\r\n * [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert):\r\n * nem rato, nem teclado, nem leitores de ecrã lhe chegam. Os portais abertos\r\n * depois dela (seletores, confirmações) continuam a responder.\r\n *\r\n * O conteúdo fica montado mesmo com a janela fechada, para poder carregar antes\r\n * de ela abrir.\r\n *\r\n * O tamanho adapta-se ao ecrã só com CSS: nunca passa a largura nem a altura\r\n * da janela do browser (com uma margem), e acompanha-a quando é\r\n * redimensionada. O que não couber faz scroll no corpo; o cabeçalho e o\r\n * rodapé ficam sempre visíveis.\r\n *\r\n * **Exemplo completo:** [Formulário em modal](?path=/docs/v1-patterns-forms-formulário-em-modal--docs).\r\n */\r\nexport default function Dialog({\r\n open,\r\n onClose,\r\n title,\r\n subtitle,\r\n footer,\r\n size = 'md',\r\n height = 'auto',\r\n closeOnBackdrop = true,\r\n closeLabel = 'Fechar',\r\n flush = false,\r\n children,\r\n}: DialogProps) {\r\n const [root, setRoot] = useState<HTMLDivElement | null>(null)\r\n const janela = useRef<HTMLDivElement>(null)\r\n const fundo = useRef<HTMLDivElement>(null)\r\n const titleId = useId()\r\n const depth = useContext(DialogDepthContext)\r\n const fechar = useRef(onClose)\r\n fechar.current = onClose\r\n // Clique fora: só conta se começou e acabou no fundo (arrastar a partir de um\r\n // campo para fora não fecha a janela).\r\n const pressedBackdrop = useRef(false)\r\n\r\n useEffect(() => {\r\n if (!open || !root) return\r\n const anterior = document.activeElement as HTMLElement | null\r\n const aberta = { root, janela: janela.current, depth }\r\n empilhar(aberta)\r\n silenciar()\r\n const topo = doTopo()\r\n const dentro = topo.janela?.contains(document.activeElement)\r\n if (!dentro) topo.janela?.focus()\r\n\r\n const onKeyDown = (e: KeyboardEvent) => {\r\n if (e.key !== 'Escape' || e.defaultPrevented) return\r\n if (doTopo() !== aberta) return\r\n fechar.current()\r\n }\r\n document.addEventListener('keydown', onKeyDown)\r\n\r\n return () => {\r\n document.removeEventListener('keydown', onKeyDown)\r\n abertas.splice(abertas.indexOf(aberta), 1)\r\n silenciar()\r\n const voltar = anterior && !anterior.closest('[inert]')\r\n if (voltar) anterior.focus()\r\n else doTopo()?.janela?.focus()\r\n }\r\n }, [open, root, depth])\r\n\r\n // A página por trás não faz scroll enquanto a janela está aberta.\r\n useEffect(() => {\r\n if (!open) return\r\n bloquearScroll()\r\n return libertarScroll\r\n }, [open])\r\n\r\n const noFundo = (e: React.SyntheticEvent) => e.target === fundo.current || e.target === e.currentTarget\r\n\r\n return createPortal(\r\n <div\r\n ref={setRoot}\r\n hidden={!open}\r\n onPointerDown={(e) => {\r\n pressedBackdrop.current = noFundo(e)\r\n }}\r\n onClick={(e) => {\r\n if (closeOnBackdrop && pressedBackdrop.current && noFundo(e)) onClose()\r\n }}\r\n // No telemóvel, a janela de altura total ocupa o ecrã todo (sem margem nem cantos): o cabeçalho, com o ×, fica sempre à vista.\r\n className={`fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${height === 'full' ? 'max-sm:p-0' : ''} ${open ? 'flex' : 'hidden'}`}\r\n >\r\n <div ref={fundo} className=\"absolute inset-0 bg-black/40 motion-safe:animate-backdrop-in\" />\r\n <div\r\n ref={janela}\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby={titleId}\r\n tabIndex={-1}\r\n className={\r\n // No escuro a sombra não se vê: a linha (a mesma dos menus) separa a janela do fundo.\r\n `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none dark:border dark:border-slate-700 ` +\r\n `motion-safe:animate-dialog-in ${height === 'full' ? 'h-full max-sm:rounded-none max-sm:border-0' : ''}`\r\n }\r\n >\r\n <header className=\"flex flex-none items-center justify-between gap-3 bg-gradient-to-br from-primary to-primary-strong px-5 py-3.5\">\r\n <div className=\"min-w-0\">\r\n <h2 id={titleId} className=\"m-0 truncate text-title font-bold text-primary-foreground\">\r\n {title}\r\n </h2>\r\n {subtitle && <p className=\"m-0 mt-0.5 truncate text-caption text-primary-foreground/75\">{subtitle}</p>}\r\n </div>\r\n <button\r\n type=\"button\"\r\n aria-label={closeLabel}\r\n title={closeLabel}\r\n onClick={onClose}\r\n className=\"flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-md border border-primary-foreground/25 bg-primary-foreground/15 text-base leading-none text-primary-foreground transition-colors hover:bg-primary-foreground/25 focus-visible:ring-2 focus-visible:ring-primary-foreground/60 focus-visible:outline-none\"\r\n >\r\n ×\r\n </button>\r\n </header>\r\n\r\n <DialogContainerContext.Provider value={root}>\r\n <DialogDepthContext.Provider value={depth + 1}>\r\n <div className={flush ? 'flex min-h-0 flex-1 flex-col overflow-hidden bg-canvas' : 'min-h-0 flex-1 overflow-y-auto bg-canvas px-5 py-4'}>{children}</div>\r\n\r\n {footer && <footer className=\"flex flex-none flex-wrap justify-end gap-2 border-t border-border bg-muted/40 px-5 py-[18px]\">{footer}</footer>}\r\n </DialogDepthContext.Provider>\r\n </DialogContainerContext.Provider>\r\n </div>\r\n </div>,\r\n document.body,\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
@@ -3,12 +3,19 @@ import React, { forwardRef, useImperativeHandle, useState, useMemo, useRef, useE
|
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { ChevronDownIcon } from "../../../foundations/icons/icons.js";
|
|
5
5
|
import { useField, fieldBoxClasses } from "../field.js";
|
|
6
|
+
import Checkbox from "../Checkbox/Checkbox.js";
|
|
6
7
|
import { useFloatingPanel, PANEL_CLASS, itemClass } from "../../../capabilities/overlay/useFloatingPanel.js";
|
|
7
8
|
import { useDialogContainer } from "../../../components/overlays/Dialog/Dialog.js";
|
|
8
9
|
const DEFAULT_SELECT_LABELS = {
|
|
9
10
|
search: "Search…",
|
|
10
11
|
searchIn: "Search in {label}",
|
|
11
|
-
noResults: "No results"
|
|
12
|
+
noResults: "No results",
|
|
13
|
+
selectAll: "Select all",
|
|
14
|
+
selectResults: "Select all (results)",
|
|
15
|
+
clear: "Clear"
|
|
16
|
+
};
|
|
17
|
+
const plain = (text) => text.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase();
|
|
18
|
+
const noop = () => {
|
|
12
19
|
};
|
|
13
20
|
const Check = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "h-4 w-4 shrink-0 text-proef-blue dark:text-sky-300", fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" }) });
|
|
14
21
|
function optionsFromChildren(children) {
|
|
@@ -50,7 +57,9 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
50
57
|
values: controlledValues,
|
|
51
58
|
defaultValues = [],
|
|
52
59
|
onValuesChange,
|
|
53
|
-
allLabel
|
|
60
|
+
allLabel,
|
|
61
|
+
selectionActions = false,
|
|
62
|
+
checkboxes = false
|
|
54
63
|
}, ref) {
|
|
55
64
|
const L = { ...DEFAULT_SELECT_LABELS, ...labels };
|
|
56
65
|
const field = useField();
|
|
@@ -70,8 +79,8 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
70
79
|
const [query, setQuery] = useState("");
|
|
71
80
|
const withSearch = searchable ?? list.length > 10;
|
|
72
81
|
const visible = useMemo(() => {
|
|
73
|
-
const q = query.trim()
|
|
74
|
-
return q ? list.filter((o) => o.label
|
|
82
|
+
const q = plain(query.trim());
|
|
83
|
+
return q ? list.filter((o) => plain(o.label).includes(q) || (o.description ? plain(o.description).includes(q) : false)) : list;
|
|
75
84
|
}, [list, query]);
|
|
76
85
|
const [active, setActive] = useState(0);
|
|
77
86
|
const searchRef = useRef(null);
|
|
@@ -85,6 +94,10 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
85
94
|
useEffect(() => {
|
|
86
95
|
if (p.open) p.panelRef.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView?.({ block: "nearest" });
|
|
87
96
|
}, [active, p.open, p.panelRef]);
|
|
97
|
+
const setPicked = (next) => {
|
|
98
|
+
if (controlledValues === void 0) setOwnValues(next);
|
|
99
|
+
onValuesChange?.(next);
|
|
100
|
+
};
|
|
88
101
|
const choose = (o) => {
|
|
89
102
|
if (o.disabled) return;
|
|
90
103
|
if (multiple) {
|
|
@@ -143,8 +156,9 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
143
156
|
}
|
|
144
157
|
};
|
|
145
158
|
const pill = variant === "pill";
|
|
159
|
+
const withBoxes = multiple && checkboxes;
|
|
146
160
|
const pickedOptions = list.filter((o) => picked.includes(o.value));
|
|
147
|
-
const multiText = allLabel && list.length > 0 && pickedOptions.length === list.length ? allLabel : pickedOptions.map((o) => o.label).join(", ");
|
|
161
|
+
const multiText = allLabel && list.length > 0 && pickedOptions.length === list.length ? allLabel : L.selectedCount && pickedOptions.length > 1 ? L.selectedCount.replace("{n}", String(pickedOptions.length)) : pickedOptions.map((o) => o.label).join(", ");
|
|
148
162
|
const showsPlaceholder = multiple ? pickedOptions.length === 0 : !chosen || chosen.value === "" && placeholder !== void 0 && emptyOption === void 0;
|
|
149
163
|
const buttonText = multiple ? showsPlaceholder ? placeholder ?? "" : multiText : showsPlaceholder ? placeholder ?? chosen?.label ?? "" : chosen?.label;
|
|
150
164
|
return /* @__PURE__ */ jsxs("div", { className: `relative min-w-0 ${className}`, onKeyDown: keys, children: [
|
|
@@ -190,6 +204,32 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
190
204
|
className: "h-8 w-full rounded-lg border border-slate-300 bg-white px-2.5 text-sm outline-none focus:border-proef-blue dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100"
|
|
191
205
|
}
|
|
192
206
|
) }),
|
|
207
|
+
multiple && selectionActions && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-3 pb-1 text-xs", children: [
|
|
208
|
+
/* @__PURE__ */ jsx(
|
|
209
|
+
"button",
|
|
210
|
+
{
|
|
211
|
+
type: "button",
|
|
212
|
+
disabled: !visible.some((o) => !o.disabled),
|
|
213
|
+
onClick: () => {
|
|
214
|
+
const add = visible.filter((o) => !o.disabled).map((o) => o.value);
|
|
215
|
+
const set = /* @__PURE__ */ new Set([...picked, ...add]);
|
|
216
|
+
setPicked(list.filter((x) => set.has(x.value)).map((x) => x.value));
|
|
217
|
+
},
|
|
218
|
+
className: "text-proef-blue hover:underline disabled:opacity-40 dark:text-sky-300",
|
|
219
|
+
children: query.trim() ? L.selectResults : L.selectAll
|
|
220
|
+
}
|
|
221
|
+
),
|
|
222
|
+
/* @__PURE__ */ jsx(
|
|
223
|
+
"button",
|
|
224
|
+
{
|
|
225
|
+
type: "button",
|
|
226
|
+
disabled: picked.length === 0,
|
|
227
|
+
onClick: () => setPicked([]),
|
|
228
|
+
className: "text-proef-blue hover:underline disabled:opacity-40 dark:text-sky-300",
|
|
229
|
+
children: L.clear
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
] }),
|
|
193
233
|
/* @__PURE__ */ jsxs("ul", { role: "listbox", "aria-label": ariaLabel, "aria-multiselectable": multiple || void 0, children: [
|
|
194
234
|
visible.length === 0 && /* @__PURE__ */ jsx("li", { className: "px-3 py-2 text-slate-400", children: L.noResults }),
|
|
195
235
|
visible.map((o, i) => {
|
|
@@ -205,11 +245,13 @@ const SelectInput = forwardRef(function SelectInput2({
|
|
|
205
245
|
onClick: () => choose(o),
|
|
206
246
|
className: `${itemClass(i === active)} ${o.disabled ? "cursor-not-allowed opacity-50" : ""}`,
|
|
207
247
|
children: [
|
|
248
|
+
withBoxes && /* @__PURE__ */ jsx(Checkbox, { checked: selected, onChange: noop, tabIndex: -1, "aria-hidden": true, className: "pointer-events-none" }),
|
|
208
249
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
209
250
|
/* @__PURE__ */ jsx("span", { className: `block truncate ${selected ? "font-semibold" : ""} ${o.value === "" ? "text-slate-500 dark:text-slate-400" : ""}`, children: o.label }),
|
|
210
251
|
o.description && /* @__PURE__ */ jsx("span", { className: "block truncate text-[11px] text-slate-500 dark:text-slate-400", children: o.description })
|
|
211
252
|
] }),
|
|
212
|
-
/* @__PURE__ */ jsx("span", { className: "
|
|
253
|
+
o.count !== void 0 && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs tabular-nums text-slate-500 dark:text-slate-400", children: o.count.toLocaleString() }),
|
|
254
|
+
!withBoxes && /* @__PURE__ */ jsx("span", { className: "w-4 shrink-0", children: selected && /* @__PURE__ */ jsx(Check, {}) })
|
|
213
255
|
]
|
|
214
256
|
},
|
|
215
257
|
o.value || "__empty"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectInput.js","sources":["../../../../../../src/v1/primitives/forms/SelectInput/SelectInput.tsx"],"sourcesContent":["import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport { ChevronDownIcon } from '../../../foundations/icons/icons'\r\nimport { fieldBoxClasses, useField } from '../field'\r\nimport { PANEL_CLASS, itemClass, useFloatingPanel } from '../../../capabilities/overlay/useFloatingPanel'\r\nimport { useDialogContainer } from '../../../components/overlays/Dialog/Dialog'\r\n\r\nexport interface SelectOption {\r\n value: string\r\n label: string\r\n /** Texto pequeno por baixo do rótulo, na lista. */\r\n description?: string\r\n disabled?: boolean\r\n}\r\n\r\nexport interface SelectInputLabels {\r\n /** Texto da caixa de pesquisa. */\r\n search: string\r\n /** Nome da caixa de pesquisa para leitores de ecrã; \"{label}\" é o nome do campo. */\r\n searchIn: string\r\n noResults: string\r\n}\r\n\r\nexport const DEFAULT_SELECT_LABELS: SelectInputLabels = {\r\n search: 'Search…',\r\n searchIn: 'Search in {label}',\r\n noResults: 'No results',\r\n}\r\n\r\nexport interface SelectInputProps {\r\n /** A opção escolhida (controlado). `''` = nenhuma. */\r\n value?: string\r\n defaultValue?: string\r\n /** As opções: texto simples (valor = rótulo) ou `{ value, label, description }`. */\r\n options?: readonly (string | SelectOption)[]\r\n /** Em alternativa a `options`: `<option>` como num `<select>` (compatível com o uso antigo). */\r\n children?: React.ReactNode\r\n /** Recebe o valor escolhido. */\r\n onValueChange?: (value: string) => void\r\n /** Como no `<select>`: `e.target.value` é o valor escolhido. */\r\n onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void\r\n /** Texto do botão sem nada escolhido; com `placeholderOption`, também a primeira opção (vazia). */\r\n placeholder?: string\r\n /** O `placeholder` é também uma opção (vazia) na lista. Por omissão sim, como no `<select>`. */\r\n placeholderOption?: boolean\r\n /** Primeira opção vazia para desfazer a escolha (ex: \"Todas\"). */\r\n emptyOption?: string\r\n /** Caixa de pesquisa no topo da lista. Por omissão, só com mais de 10 opções. */\r\n searchable?: boolean\r\n disabled?: boolean\r\n required?: boolean\r\n /** Marca o campo a vermelho (ex: obrigatório por escolher ao submeter). */\r\n invalid?: boolean\r\n id?: string\r\n /** Nome no formulário (vai num campo escondido). */\r\n name?: string\r\n 'aria-label'?: string\r\n 'aria-labelledby'?: string\r\n /** `field` (por omissão): campo. `pill`: etiqueta arredondada (ex: o estado numa linha de tabela). A lista é a mesma. */\r\n variant?: 'field' | 'pill'\r\n /** Cores da etiqueta (`pill`), normalmente conforme o valor escolhido; pode trazer contorno (ex: `border border-dashed`). */\r\n pillClassName?: string\r\n /** 38px em vez da altura dos controlos (para alinhar com campos de formulários mais altos). */\r\n tall?: boolean\r\n labels?: Partial<SelectInputLabels>\r\n /** Classes da caixa à volta (ex: a largura). */\r\n className?: string\r\n /**\r\n * Escolha múltipla: cada opção leva ✓ e a lista fica aberta a marcar e desmarcar.\r\n * Usa `values` / `onValuesChange` em vez de `value` / `onValueChange`.\r\n */\r\n multiple?: boolean\r\n /** As escolhidas, com `multiple` (controlado). */\r\n values?: string[]\r\n defaultValues?: string[]\r\n /** Recebe as escolhidas, com `multiple`, pela ordem das opções. */\r\n onValuesChange?: (values: string[]) => void\r\n /** Com `multiple`: o texto do botão quando estão todas escolhidas (ex: \"Todos os tipos\"). */\r\n allLabel?: string\r\n}\r\n\r\nconst Check = () => (\r\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4 shrink-0 text-proef-blue dark:text-sky-300\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\r\n <path d=\"M20 6 9 17l-5-5\" />\r\n </svg>\r\n)\r\n\r\n/** As `<option>` dos filhos como opções (aceita também `<optgroup>`, achatado). */\r\nfunction optionsFromChildren(children: React.ReactNode): SelectOption[] {\r\n const out: SelectOption[] = []\r\n React.Children.forEach(children, (child) => {\r\n if (!React.isValidElement(child)) return\r\n const props = child.props as { value?: unknown; children?: React.ReactNode; disabled?: boolean }\r\n if (child.type === 'optgroup') return out.push(...optionsFromChildren(props.children))\r\n if (child.type !== 'option') return\r\n const text = React.Children.toArray(props.children).join('')\r\n out.push({ value: props.value !== undefined ? String(props.value) : text, label: text, disabled: props.disabled })\r\n })\r\n return out\r\n}\r\n\r\n/**\r\n * Lista de escolha: um campo que abre uma lista desenhada pela biblioteca (não a do sistema, que não segue o\r\n * tema nem o aspeto dos portais). A opção escolhida tem ✓; com muitas opções há pesquisa.\r\n *\r\n * Teclado: ↑/↓, Home/End, Enter escolhe, Esc fecha; sem pesquisa, uma letra salta para a opção.\r\n *\r\n * Dentro de um [FormField](?path=/docs/v1-components-forms-formfield--docs) fica ligado à etiqueta\r\n * automaticamente. Clicar não chega a uma linha clicável por baixo (ex: numa tabela).\r\n *\r\n * Usa-se como um `<select>`: `<option>` como filhos e `onChange` com `e.target.value`; `placeholder`\r\n * acrescenta uma primeira opção vazia.\r\n */\r\nconst SelectInput = forwardRef<HTMLButtonElement, SelectInputProps>(function SelectInput(\r\n {\r\n value: controlled,\r\n defaultValue = '',\r\n options,\r\n children,\r\n onValueChange,\r\n onChange,\r\n placeholder,\r\n placeholderOption = true,\r\n emptyOption,\r\n searchable,\r\n disabled = false,\r\n required,\r\n invalid,\r\n id,\r\n name,\r\n 'aria-label': ariaLabel,\r\n 'aria-labelledby': ariaLabelledBy,\r\n variant = 'field',\r\n pillClassName = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-200',\r\n tall = false,\r\n labels,\r\n className = '',\r\n multiple = false,\r\n values: controlledValues,\r\n defaultValues = [],\r\n onValuesChange,\r\n allLabel,\r\n },\r\n ref,\r\n) {\r\n const L = { ...DEFAULT_SELECT_LABELS, ...labels }\r\n const field = useField()\r\n const p = useFloatingPanel('left')\r\n // Dentro de um Dialog a lista abre na janela: no body ficava por baixo dela (z-200)\r\n // e o resto da página está inert — a lista abria mas não se via nem se clicava.\r\n const dialogContainer = useDialogContainer()\r\n useImperativeHandle(ref, () => p.triggerRef.current as HTMLButtonElement)\r\n\r\n const [own, setOwn] = useState(defaultValue)\r\n const value = controlled ?? own\r\n const [ownValues, setOwnValues] = useState(defaultValues)\r\n const picked = controlledValues ?? ownValues\r\n\r\n const list = useMemo<SelectOption[]>(() => {\r\n const base = options ? options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) : optionsFromChildren(children)\r\n // Na escolha múltipla não há opção vazia: desfazer é desmarcar.\r\n if (multiple) return base\r\n const empty = emptyOption ?? (placeholder !== undefined && placeholderOption ? placeholder : undefined)\r\n return empty !== undefined && !base.some((o) => o.value === '') ? [{ value: '', label: empty }, ...base] : base\r\n }, [options, children, emptyOption, placeholder, placeholderOption, multiple])\r\n\r\n const [query, setQuery] = useState('')\r\n const withSearch = searchable ?? list.length > 10\r\n const visible = useMemo(() => {\r\n const q = query.trim().toLowerCase()\r\n return q ? list.filter((o) => o.label.toLowerCase().includes(q) || o.description?.toLowerCase().includes(q)) : list\r\n }, [list, query])\r\n const [active, setActive] = useState(0)\r\n const searchRef = useRef<HTMLInputElement>(null)\r\n const chosen = list.find((o) => o.value === value)\r\n\r\n // Ao abrir: limpa a pesquisa e põe o teclado na opção escolhida.\r\n useEffect(() => {\r\n if (!p.open) return\r\n setQuery('')\r\n setActive(Math.max(0, list.findIndex((o) => (multiple ? picked.includes(o.value) : o.value === value))))\r\n if (withSearch) setTimeout(() => searchRef.current?.focus(), 0)\r\n }, [p.open]) // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // O item ativo fica sempre à vista ao navegar com o teclado.\r\n useEffect(() => {\r\n if (p.open) p.panelRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`)?.scrollIntoView?.({ block: 'nearest' })\r\n }, [active, p.open, p.panelRef])\r\n\r\n const choose = (o: SelectOption) => {\r\n if (o.disabled) return\r\n if (multiple) {\r\n // Marca ou desmarca, sem fechar; a ordem é a das opções.\r\n const set = new Set(picked)\r\n if (set.has(o.value)) set.delete(o.value)\r\n else set.add(o.value)\r\n const next = list.filter((x) => set.has(x.value)).map((x) => x.value)\r\n if (controlledValues === undefined) setOwnValues(next)\r\n onValuesChange?.(next)\r\n return\r\n }\r\n p.close()\r\n p.triggerRef.current?.focus()\r\n if (o.value === value) return\r\n if (controlled === undefined) setOwn(o.value)\r\n onValueChange?.(o.value)\r\n if (onChange) {\r\n const target = { value: o.value, name } as HTMLSelectElement\r\n onChange({ target, currentTarget: target } as React.ChangeEvent<HTMLSelectElement>)\r\n }\r\n }\r\n\r\n const keys = (e: React.KeyboardEvent) => {\r\n if (!p.open) {\r\n if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {\r\n e.preventDefault()\r\n p.setOpen(true)\r\n }\r\n return\r\n }\r\n if (e.key === 'Escape') {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n p.close()\r\n p.triggerRef.current?.focus()\r\n } else if (e.key === 'ArrowDown') {\r\n e.preventDefault()\r\n setActive((i) => Math.min(visible.length - 1, i + 1))\r\n } else if (e.key === 'ArrowUp') {\r\n e.preventDefault()\r\n setActive((i) => Math.max(0, i - 1))\r\n } else if (e.key === 'Home') {\r\n e.preventDefault()\r\n setActive(0)\r\n } else if (e.key === 'End') {\r\n e.preventDefault()\r\n setActive(visible.length - 1)\r\n } else if (e.key === 'Enter') {\r\n e.preventDefault()\r\n const o = visible[active]\r\n if (o) choose(o)\r\n } else if (e.key === 'Tab') {\r\n p.close()\r\n } else if (!withSearch && e.key.length === 1) {\r\n // Sem pesquisa: a letra salta para a primeira opção que começa por ela.\r\n const i = visible.findIndex((o) => o.label.toLowerCase().startsWith(e.key.toLowerCase()))\r\n if (i >= 0) setActive(i)\r\n }\r\n }\r\n\r\n const pill = variant === 'pill'\r\n const pickedOptions = list.filter((o) => picked.includes(o.value))\r\n const multiText =\r\n allLabel && list.length > 0 && pickedOptions.length === list.length ? allLabel : pickedOptions.map((o) => o.label).join(', ')\r\n const showsPlaceholder = multiple\r\n ? pickedOptions.length === 0\r\n : !chosen || (chosen.value === '' && placeholder !== undefined && emptyOption === undefined)\r\n const buttonText = multiple\r\n ? showsPlaceholder ? (placeholder ?? '') : multiText\r\n : showsPlaceholder ? (placeholder ?? chosen?.label ?? '') : chosen?.label\r\n\r\n return (\r\n <div className={`relative min-w-0 ${className}`} onKeyDown={keys}>\r\n <button\r\n ref={p.triggerRef}\r\n id={id ?? field.id}\r\n type=\"button\"\r\n disabled={disabled}\r\n aria-haspopup=\"listbox\"\r\n aria-expanded={p.open}\r\n aria-label={ariaLabel}\r\n aria-labelledby={ariaLabelledBy}\r\n aria-invalid={invalid || undefined}\r\n aria-required={(required ?? field.required) || undefined}\r\n onClick={(e) => {\r\n // Dentro de uma linha clicável (tabelas), abrir a lista não abre a linha.\r\n e.stopPropagation()\r\n p.setOpen((o) => !o)\r\n }}\r\n className={\r\n pill\r\n ? `inline-flex cursor-pointer appearance-none items-center gap-1 rounded-full px-2.5 py-1 text-xs font-bold whitespace-nowrap transition hover:brightness-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-proef-blue disabled:cursor-not-allowed disabled:opacity-50 ${pillClassName}`\r\n : `flex w-full min-w-0 cursor-pointer appearance-none items-center justify-between gap-2 text-left ${tall ? 'h-[38px]' : 'h-control'} ${fieldBoxClasses}`\r\n }\r\n >\r\n <span className={`truncate ${showsPlaceholder && !pill ? 'text-muted-foreground/70' : ''}`}>\r\n {buttonText}\r\n </span>\r\n <ChevronDownIcon size={14} className={`shrink-0 ${pill ? 'opacity-60' : 'text-muted-foreground'}`} />\r\n </button>\r\n {name && <input type=\"hidden\" name={name} value={multiple ? picked.join(',') : value} />}\r\n {p.open &&\r\n createPortal(\r\n <div ref={p.panelRef} style={p.style} className={PANEL_CLASS} onClick={(e) => e.stopPropagation()} onKeyDown={keys}>\r\n {withSearch && (\r\n <div className=\"px-2 pt-0.5 pb-1\">\r\n <input\r\n ref={searchRef}\r\n type=\"search\"\r\n value={query}\r\n onChange={(e) => {\r\n setQuery(e.target.value)\r\n setActive(0)\r\n }}\r\n placeholder={L.search}\r\n aria-label={L.searchIn.replace('{label}', ariaLabel ?? '')}\r\n className=\"h-8 w-full rounded-lg border border-slate-300 bg-white px-2.5 text-sm outline-none focus:border-proef-blue dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100\"\r\n />\r\n </div>\r\n )}\r\n <ul role=\"listbox\" aria-label={ariaLabel} aria-multiselectable={multiple || undefined}>\r\n {visible.length === 0 && <li className=\"px-3 py-2 text-slate-400\">{L.noResults}</li>}\r\n {visible.map((o, i) => {\r\n const selected = multiple ? picked.includes(o.value) : o.value === value\r\n return (\r\n <li\r\n key={o.value || '__empty'}\r\n role=\"option\"\r\n aria-selected={selected}\r\n aria-disabled={o.disabled || undefined}\r\n data-index={i}\r\n onMouseEnter={() => setActive(i)}\r\n onClick={() => choose(o)}\r\n className={`${itemClass(i === active)} ${o.disabled ? 'cursor-not-allowed opacity-50' : ''}`}\r\n >\r\n <span className=\"min-w-0 flex-1\">\r\n <span className={`block truncate ${selected ? 'font-semibold' : ''} ${o.value === '' ? 'text-slate-500 dark:text-slate-400' : ''}`}>{o.label}</span>\r\n {o.description && <span className=\"block truncate text-[11px] text-slate-500 dark:text-slate-400\">{o.description}</span>}\r\n </span>\r\n <span className=\"w-4 shrink-0\">{selected && <Check />}</span>\r\n </li>\r\n )\r\n })}\r\n </ul>\r\n </div>,\r\n dialogContainer ?? document.body,\r\n )}\r\n </div>\r\n )\r\n})\r\n\r\nexport default SelectInput\r\nexport { SelectInput }\r\n"],"names":["SelectInput"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SelectInput.js","sources":["../../../../../../src/v1/primitives/forms/SelectInput/SelectInput.tsx"],"sourcesContent":["import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport { ChevronDownIcon } from '../../../foundations/icons/icons'\r\nimport { fieldBoxClasses, useField } from '../field'\r\nimport Checkbox from '../Checkbox/Checkbox'\r\nimport { PANEL_CLASS, itemClass, useFloatingPanel } from '../../../capabilities/overlay/useFloatingPanel'\r\nimport { useDialogContainer } from '../../../components/overlays/Dialog/Dialog'\r\n\r\nexport interface SelectOption {\r\n value: string\r\n label: string\r\n /** Texto pequeno por baixo do rótulo, na lista. */\r\n description?: string\r\n /** Um número à direita da linha, na lista (ex: quantos registos esta opção daria, como nos filtros facetados). */\r\n count?: number\r\n disabled?: boolean\r\n}\r\n\r\nexport interface SelectInputLabels {\r\n /** Texto da caixa de pesquisa. */\r\n search: string\r\n /** Nome da caixa de pesquisa para leitores de ecrã; \"{label}\" é o nome do campo. */\r\n searchIn: string\r\n noResults: string\r\n /** Com `selectionActions`: escolhe todas as opções (com pesquisa, só as que ela mostra). */\r\n selectAll: string\r\n /** Com `selectionActions` e uma pesquisa escrita: escolhe só as opções do resultado. */\r\n selectResults: string\r\n /** Com `selectionActions`: desfaz a escolha. */\r\n clear: string\r\n /**\r\n * Com `multiple`: o texto do botão com mais de uma opção escolhida, em vez dos rótulos separados por vírgula.\r\n * \"{n}\" é o número de escolhidas (ex: \"{n} selecionados\"). Sem ele, mostram-se os rótulos.\r\n */\r\n selectedCount?: string\r\n}\r\n\r\nexport const DEFAULT_SELECT_LABELS: SelectInputLabels = {\r\n search: 'Search…',\r\n searchIn: 'Search in {label}',\r\n noResults: 'No results',\r\n selectAll: 'Select all',\r\n selectResults: 'Select all (results)',\r\n clear: 'Clear',\r\n}\r\n\r\n/** Sem acentos nem maiúsculas: \"Guimarães\" e \"GUIMARAES\" são o mesmo para a pesquisa. */\r\nconst plain = (text: string): string => text.normalize('NFD').replace(/\\p{M}/gu, '').toLowerCase()\r\n\r\nexport interface SelectInputProps {\r\n /** A opção escolhida (controlado). `''` = nenhuma. */\r\n value?: string\r\n defaultValue?: string\r\n /** As opções: texto simples (valor = rótulo) ou `{ value, label, description }`. */\r\n options?: readonly (string | SelectOption)[]\r\n /** Em alternativa a `options`: `<option>` como num `<select>` (compatível com o uso antigo). */\r\n children?: React.ReactNode\r\n /** Recebe o valor escolhido. */\r\n onValueChange?: (value: string) => void\r\n /** Como no `<select>`: `e.target.value` é o valor escolhido. */\r\n onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void\r\n /** Texto do botão sem nada escolhido; com `placeholderOption`, também a primeira opção (vazia). */\r\n placeholder?: string\r\n /** O `placeholder` é também uma opção (vazia) na lista. Por omissão sim, como no `<select>`. */\r\n placeholderOption?: boolean\r\n /** Primeira opção vazia para desfazer a escolha (ex: \"Todas\"). */\r\n emptyOption?: string\r\n /** Caixa de pesquisa no topo da lista. Por omissão, só com mais de 10 opções. */\r\n searchable?: boolean\r\n disabled?: boolean\r\n required?: boolean\r\n /** Marca o campo a vermelho (ex: obrigatório por escolher ao submeter). */\r\n invalid?: boolean\r\n id?: string\r\n /** Nome no formulário (vai num campo escondido). */\r\n name?: string\r\n 'aria-label'?: string\r\n 'aria-labelledby'?: string\r\n /** `field` (por omissão): campo. `pill`: etiqueta arredondada (ex: o estado numa linha de tabela). A lista é a mesma. */\r\n variant?: 'field' | 'pill'\r\n /** Cores da etiqueta (`pill`), normalmente conforme o valor escolhido; pode trazer contorno (ex: `border border-dashed`). */\r\n pillClassName?: string\r\n /** 38px em vez da altura dos controlos (para alinhar com campos de formulários mais altos). */\r\n tall?: boolean\r\n labels?: Partial<SelectInputLabels>\r\n /** Classes da caixa à volta (ex: a largura). */\r\n className?: string\r\n /**\r\n * Escolha múltipla: cada opção leva ✓ e a lista fica aberta a marcar e desmarcar.\r\n * Usa `values` / `onValuesChange` em vez de `value` / `onValueChange`.\r\n */\r\n multiple?: boolean\r\n /** As escolhidas, com `multiple` (controlado). */\r\n values?: string[]\r\n defaultValues?: string[]\r\n /** Recebe as escolhidas, com `multiple`, pela ordem das opções. */\r\n onValuesChange?: (values: string[]) => void\r\n /** Com `multiple`: o texto do botão quando estão todas escolhidas (ex: \"Todos os tipos\"). */\r\n allLabel?: string\r\n /**\r\n * Com `multiple`: no topo da lista, \"Selecionar todos\" (ou \"…(resultado)\", com uma pesquisa escrita — só as\r\n * opções que ela mostra) e \"Limpar\". Para listas longas, como as dos filtros.\r\n */\r\n selectionActions?: boolean\r\n /**\r\n * Com `multiple`: cada opção leva a caixa de marcar da biblioteca (`Checkbox`) à esquerda, em vez do visto à\r\n * direita — o aspeto das listas de filtros. Marcar continua a ser clicar na linha.\r\n */\r\n checkboxes?: boolean\r\n}\r\n\r\nconst noop = () => {}\r\n\r\nconst Check = () => (\r\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4 shrink-0 text-proef-blue dark:text-sky-300\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\r\n <path d=\"M20 6 9 17l-5-5\" />\r\n </svg>\r\n)\r\n\r\n/** As `<option>` dos filhos como opções (aceita também `<optgroup>`, achatado). */\r\nfunction optionsFromChildren(children: React.ReactNode): SelectOption[] {\r\n const out: SelectOption[] = []\r\n React.Children.forEach(children, (child) => {\r\n if (!React.isValidElement(child)) return\r\n const props = child.props as { value?: unknown; children?: React.ReactNode; disabled?: boolean }\r\n if (child.type === 'optgroup') return out.push(...optionsFromChildren(props.children))\r\n if (child.type !== 'option') return\r\n const text = React.Children.toArray(props.children).join('')\r\n out.push({ value: props.value !== undefined ? String(props.value) : text, label: text, disabled: props.disabled })\r\n })\r\n return out\r\n}\r\n\r\n/**\r\n * Lista de escolha: um campo que abre uma lista desenhada pela biblioteca (não a do sistema, que não segue o\r\n * tema nem o aspeto dos portais). A opção escolhida tem ✓; com muitas opções há pesquisa.\r\n *\r\n * Teclado: ↑/↓, Home/End, Enter escolhe, Esc fecha; sem pesquisa, uma letra salta para a opção.\r\n *\r\n * Dentro de um [FormField](?path=/docs/v1-components-forms-formfield--docs) fica ligado à etiqueta\r\n * automaticamente. Clicar não chega a uma linha clicável por baixo (ex: numa tabela).\r\n *\r\n * Usa-se como um `<select>`: `<option>` como filhos e `onChange` com `e.target.value`; `placeholder`\r\n * acrescenta uma primeira opção vazia.\r\n */\r\nconst SelectInput = forwardRef<HTMLButtonElement, SelectInputProps>(function SelectInput(\r\n {\r\n value: controlled,\r\n defaultValue = '',\r\n options,\r\n children,\r\n onValueChange,\r\n onChange,\r\n placeholder,\r\n placeholderOption = true,\r\n emptyOption,\r\n searchable,\r\n disabled = false,\r\n required,\r\n invalid,\r\n id,\r\n name,\r\n 'aria-label': ariaLabel,\r\n 'aria-labelledby': ariaLabelledBy,\r\n variant = 'field',\r\n pillClassName = 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-200',\r\n tall = false,\r\n labels,\r\n className = '',\r\n multiple = false,\r\n values: controlledValues,\r\n defaultValues = [],\r\n onValuesChange,\r\n allLabel,\r\n selectionActions = false,\r\n checkboxes = false,\r\n },\r\n ref,\r\n) {\r\n const L = { ...DEFAULT_SELECT_LABELS, ...labels }\r\n const field = useField()\r\n const p = useFloatingPanel('left')\r\n // Dentro de um Dialog a lista abre na janela: no body ficava por baixo dela (z-200)\r\n // e o resto da página está inert — a lista abria mas não se via nem se clicava.\r\n const dialogContainer = useDialogContainer()\r\n useImperativeHandle(ref, () => p.triggerRef.current as HTMLButtonElement)\r\n\r\n const [own, setOwn] = useState(defaultValue)\r\n const value = controlled ?? own\r\n const [ownValues, setOwnValues] = useState(defaultValues)\r\n const picked = controlledValues ?? ownValues\r\n\r\n const list = useMemo<SelectOption[]>(() => {\r\n const base = options ? options.map((o) => (typeof o === 'string' ? { value: o, label: o } : o)) : optionsFromChildren(children)\r\n // Na escolha múltipla não há opção vazia: desfazer é desmarcar.\r\n if (multiple) return base\r\n const empty = emptyOption ?? (placeholder !== undefined && placeholderOption ? placeholder : undefined)\r\n return empty !== undefined && !base.some((o) => o.value === '') ? [{ value: '', label: empty }, ...base] : base\r\n }, [options, children, emptyOption, placeholder, placeholderOption, multiple])\r\n\r\n const [query, setQuery] = useState('')\r\n const withSearch = searchable ?? list.length > 10\r\n const visible = useMemo(() => {\r\n const q = plain(query.trim())\r\n return q ? list.filter((o) => plain(o.label).includes(q) || (o.description ? plain(o.description).includes(q) : false)) : list\r\n }, [list, query])\r\n const [active, setActive] = useState(0)\r\n const searchRef = useRef<HTMLInputElement>(null)\r\n const chosen = list.find((o) => o.value === value)\r\n\r\n // Ao abrir: limpa a pesquisa e põe o teclado na opção escolhida.\r\n useEffect(() => {\r\n if (!p.open) return\r\n setQuery('')\r\n setActive(Math.max(0, list.findIndex((o) => (multiple ? picked.includes(o.value) : o.value === value))))\r\n if (withSearch) setTimeout(() => searchRef.current?.focus(), 0)\r\n }, [p.open]) // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // O item ativo fica sempre à vista ao navegar com o teclado.\r\n useEffect(() => {\r\n if (p.open) p.panelRef.current?.querySelector<HTMLElement>(`[data-index=\"${active}\"]`)?.scrollIntoView?.({ block: 'nearest' })\r\n }, [active, p.open, p.panelRef])\r\n\r\n /** Escolhe várias de uma vez (ou nenhuma), pela ordem das opções. */\r\n const setPicked = (next: string[]) => {\r\n if (controlledValues === undefined) setOwnValues(next)\r\n onValuesChange?.(next)\r\n }\r\n\r\n const choose = (o: SelectOption) => {\r\n if (o.disabled) return\r\n if (multiple) {\r\n // Marca ou desmarca, sem fechar; a ordem é a das opções.\r\n const set = new Set(picked)\r\n if (set.has(o.value)) set.delete(o.value)\r\n else set.add(o.value)\r\n const next = list.filter((x) => set.has(x.value)).map((x) => x.value)\r\n if (controlledValues === undefined) setOwnValues(next)\r\n onValuesChange?.(next)\r\n return\r\n }\r\n p.close()\r\n p.triggerRef.current?.focus()\r\n if (o.value === value) return\r\n if (controlled === undefined) setOwn(o.value)\r\n onValueChange?.(o.value)\r\n if (onChange) {\r\n const target = { value: o.value, name } as HTMLSelectElement\r\n onChange({ target, currentTarget: target } as React.ChangeEvent<HTMLSelectElement>)\r\n }\r\n }\r\n\r\n const keys = (e: React.KeyboardEvent) => {\r\n if (!p.open) {\r\n if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(e.key)) {\r\n e.preventDefault()\r\n p.setOpen(true)\r\n }\r\n return\r\n }\r\n if (e.key === 'Escape') {\r\n e.preventDefault()\r\n e.stopPropagation()\r\n p.close()\r\n p.triggerRef.current?.focus()\r\n } else if (e.key === 'ArrowDown') {\r\n e.preventDefault()\r\n setActive((i) => Math.min(visible.length - 1, i + 1))\r\n } else if (e.key === 'ArrowUp') {\r\n e.preventDefault()\r\n setActive((i) => Math.max(0, i - 1))\r\n } else if (e.key === 'Home') {\r\n e.preventDefault()\r\n setActive(0)\r\n } else if (e.key === 'End') {\r\n e.preventDefault()\r\n setActive(visible.length - 1)\r\n } else if (e.key === 'Enter') {\r\n e.preventDefault()\r\n const o = visible[active]\r\n if (o) choose(o)\r\n } else if (e.key === 'Tab') {\r\n p.close()\r\n } else if (!withSearch && e.key.length === 1) {\r\n // Sem pesquisa: a letra salta para a primeira opção que começa por ela.\r\n const i = visible.findIndex((o) => o.label.toLowerCase().startsWith(e.key.toLowerCase()))\r\n if (i >= 0) setActive(i)\r\n }\r\n }\r\n\r\n const pill = variant === 'pill'\r\n const withBoxes = multiple && checkboxes\r\n const pickedOptions = list.filter((o) => picked.includes(o.value))\r\n const multiText =\r\n allLabel && list.length > 0 && pickedOptions.length === list.length\r\n ? allLabel\r\n : L.selectedCount && pickedOptions.length > 1\r\n ? L.selectedCount.replace('{n}', String(pickedOptions.length))\r\n : pickedOptions.map((o) => o.label).join(', ')\r\n const showsPlaceholder = multiple\r\n ? pickedOptions.length === 0\r\n : !chosen || (chosen.value === '' && placeholder !== undefined && emptyOption === undefined)\r\n const buttonText = multiple\r\n ? showsPlaceholder ? (placeholder ?? '') : multiText\r\n : showsPlaceholder ? (placeholder ?? chosen?.label ?? '') : chosen?.label\r\n\r\n return (\r\n <div className={`relative min-w-0 ${className}`} onKeyDown={keys}>\r\n <button\r\n ref={p.triggerRef}\r\n id={id ?? field.id}\r\n type=\"button\"\r\n disabled={disabled}\r\n aria-haspopup=\"listbox\"\r\n aria-expanded={p.open}\r\n aria-label={ariaLabel}\r\n aria-labelledby={ariaLabelledBy}\r\n aria-invalid={invalid || undefined}\r\n aria-required={(required ?? field.required) || undefined}\r\n onClick={(e) => {\r\n // Dentro de uma linha clicável (tabelas), abrir a lista não abre a linha.\r\n e.stopPropagation()\r\n p.setOpen((o) => !o)\r\n }}\r\n className={\r\n pill\r\n ? `inline-flex cursor-pointer appearance-none items-center gap-1 rounded-full px-2.5 py-1 text-xs font-bold whitespace-nowrap transition hover:brightness-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-proef-blue disabled:cursor-not-allowed disabled:opacity-50 ${pillClassName}`\r\n : `flex w-full min-w-0 cursor-pointer appearance-none items-center justify-between gap-2 text-left ${tall ? 'h-[38px]' : 'h-control'} ${fieldBoxClasses}`\r\n }\r\n >\r\n <span className={`truncate ${showsPlaceholder && !pill ? 'text-muted-foreground/70' : ''}`}>\r\n {buttonText}\r\n </span>\r\n <ChevronDownIcon size={14} className={`shrink-0 ${pill ? 'opacity-60' : 'text-muted-foreground'}`} />\r\n </button>\r\n {name && <input type=\"hidden\" name={name} value={multiple ? picked.join(',') : value} />}\r\n {p.open &&\r\n createPortal(\r\n <div ref={p.panelRef} style={p.style} className={PANEL_CLASS} onClick={(e) => e.stopPropagation()} onKeyDown={keys}>\r\n {withSearch && (\r\n <div className=\"px-2 pt-0.5 pb-1\">\r\n <input\r\n ref={searchRef}\r\n type=\"search\"\r\n value={query}\r\n onChange={(e) => {\r\n setQuery(e.target.value)\r\n setActive(0)\r\n }}\r\n placeholder={L.search}\r\n aria-label={L.searchIn.replace('{label}', ariaLabel ?? '')}\r\n className=\"h-8 w-full rounded-lg border border-slate-300 bg-white px-2.5 text-sm outline-none focus:border-proef-blue dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100\"\r\n />\r\n </div>\r\n )}\r\n {multiple && selectionActions && (\r\n <div className=\"flex items-center justify-between px-3 pb-1 text-xs\">\r\n <button\r\n type=\"button\"\r\n disabled={!visible.some((o) => !o.disabled)}\r\n onClick={() => {\r\n const add = visible.filter((o) => !o.disabled).map((o) => o.value)\r\n const set = new Set([...picked, ...add])\r\n setPicked(list.filter((x) => set.has(x.value)).map((x) => x.value))\r\n }}\r\n className=\"text-proef-blue hover:underline disabled:opacity-40 dark:text-sky-300\"\r\n >\r\n {query.trim() ? L.selectResults : L.selectAll}\r\n </button>\r\n <button\r\n type=\"button\"\r\n disabled={picked.length === 0}\r\n onClick={() => setPicked([])}\r\n className=\"text-proef-blue hover:underline disabled:opacity-40 dark:text-sky-300\"\r\n >\r\n {L.clear}\r\n </button>\r\n </div>\r\n )}\r\n <ul role=\"listbox\" aria-label={ariaLabel} aria-multiselectable={multiple || undefined}>\r\n {visible.length === 0 && <li className=\"px-3 py-2 text-slate-400\">{L.noResults}</li>}\r\n {visible.map((o, i) => {\r\n const selected = multiple ? picked.includes(o.value) : o.value === value\r\n return (\r\n <li\r\n key={o.value || '__empty'}\r\n role=\"option\"\r\n aria-selected={selected}\r\n aria-disabled={o.disabled || undefined}\r\n data-index={i}\r\n onMouseEnter={() => setActive(i)}\r\n onClick={() => choose(o)}\r\n className={`${itemClass(i === active)} ${o.disabled ? 'cursor-not-allowed opacity-50' : ''}`}\r\n >\r\n {withBoxes && <Checkbox checked={selected} onChange={noop} tabIndex={-1} aria-hidden className=\"pointer-events-none\" />}\r\n <span className=\"min-w-0 flex-1\">\r\n <span className={`block truncate ${selected ? 'font-semibold' : ''} ${o.value === '' ? 'text-slate-500 dark:text-slate-400' : ''}`}>{o.label}</span>\r\n {o.description && <span className=\"block truncate text-[11px] text-slate-500 dark:text-slate-400\">{o.description}</span>}\r\n </span>\r\n {o.count !== undefined && (\r\n <span className=\"shrink-0 text-xs tabular-nums text-slate-500 dark:text-slate-400\">{o.count.toLocaleString()}</span>\r\n )}\r\n {!withBoxes && <span className=\"w-4 shrink-0\">{selected && <Check />}</span>}\r\n </li>\r\n )\r\n })}\r\n </ul>\r\n </div>,\r\n dialogContainer ?? document.body,\r\n )}\r\n </div>\r\n )\r\n})\r\n\r\nexport default SelectInput\r\nexport { SelectInput }\r\n"],"names":["SelectInput"],"mappings":"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"}
|