@enonic/ui-kit 0.5.2 → 0.7.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 (42) hide show
  1. package/README.md +71 -24
  2. package/dist/dialog/action-dialog-context.d.ts +7 -3
  3. package/dist/dialog/action-dialog-context.js.map +1 -1
  4. package/dist/dialog/action-dialog.d.ts +4 -1
  5. package/dist/dialog/action-dialog.js +69 -27
  6. package/dist/dialog/action-dialog.js.map +1 -1
  7. package/dist/dialog/dialog-preset.js +1 -1
  8. package/dist/dialog/dialog-preset.js.map +1 -1
  9. package/dist/dialog/use-close-guard.d.ts +3 -3
  10. package/dist/dialog/use-close-guard.js +3 -3
  11. package/dist/dialog/use-close-guard.js.map +1 -1
  12. package/dist/filter/filter-fields.d.ts +1 -1
  13. package/dist/filter/filter-fields.js.map +1 -1
  14. package/dist/filter/filter-input.d.ts +2 -2
  15. package/dist/filter/filter-input.js +14 -17
  16. package/dist/filter/filter-input.js.map +1 -1
  17. package/dist/filter/filter-input.phrases.d.ts +1 -6
  18. package/dist/filter/filter-input.phrases.js +1 -6
  19. package/dist/filter/filter-input.phrases.js.map +1 -1
  20. package/dist/i18n/phrases.d.ts +29 -0
  21. package/dist/i18n/phrases.js +16 -7
  22. package/dist/i18n/phrases.js.map +1 -1
  23. package/dist/index.d.ts +7 -2
  24. package/dist/index.js +7 -2
  25. package/dist/notifications/notification-list.d.ts +18 -0
  26. package/dist/notifications/notification-list.js +92 -0
  27. package/dist/notifications/notification-list.js.map +1 -0
  28. package/dist/notifications/notification-list.phrases.d.ts +9 -0
  29. package/dist/notifications/notification-list.phrases.js +7 -0
  30. package/dist/notifications/notification-list.phrases.js.map +1 -0
  31. package/dist/notifications/use-focus-hand-off.js +43 -0
  32. package/dist/notifications/use-focus-hand-off.js.map +1 -0
  33. package/dist/theme/theme-switcher.d.ts +14 -0
  34. package/dist/theme/theme-switcher.js +71 -0
  35. package/dist/theme/theme-switcher.js.map +1 -0
  36. package/dist/theme/theme-switcher.phrases.d.ts +17 -0
  37. package/dist/theme/theme-switcher.phrases.js +17 -0
  38. package/dist/theme/theme-switcher.phrases.js.map +1 -0
  39. package/dist/theme/use-resolved-theme.d.ts +10 -0
  40. package/dist/theme/use-resolved-theme.js +14 -0
  41. package/dist/theme/use-resolved-theme.js.map +1 -0
  42. package/package.json +11 -10
@@ -23,8 +23,8 @@ type FilterInputProps = {
23
23
  * The filter of a list: values of predefined fields combined with free text, every term a
24
24
  * tag. The dropdown opens on a click, a keystroke or an arrow — not on focus, so the focus the
25
25
  * library returns to the input on Escape does not reopen it. The fields come first; picking one, or
26
- * typing its label and a colon, opens its values with their hit counts, and the values stay open
27
- * across picks so several can be ticked. Enter picks the highlighted option, or adds typed text as
26
+ * typing its label and a colon, opens its values with their hit counts; picking a value closes the
27
+ * list and returns the focus to the input. Enter picks the highlighted option, or adds typed text as
28
28
  * a tag when no field is entered. The ref is the text input's.
29
29
  */
30
30
  declare const FilterInput: import("preact").FunctionalComponent<import("preact/compat").PropsWithoutRef<FilterInputProps> & {
@@ -1,6 +1,6 @@
1
+ import { filterInputPhrases } from "./filter-input.phrases.js";
1
2
  import { EMPTY_FILTER, fieldTerm, isFieldTerm, termKey, textTerm, toggledTerm, valuesOf, withTerm, withoutTerm } from "./filter-query.js";
2
3
  import { fieldTyped, groupedValues, isOffered, matchingFields, matchingValues, termLabel } from "./filter-fields.js";
3
- import { filterInputPhrases } from "./filter-input.phrases.js";
4
4
  import { forwardRef, useId, useRef, useState } from "react";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { Button, Combobox, IconButton, Listbox, Tag, cn, useCombobox, useComposedRefs, useControlledState, usePhrases } from "@enonic/ui";
@@ -13,8 +13,8 @@ const VALUE_OPTION = "value:";
13
13
  * The filter of a list: values of predefined fields combined with free text, every term a
14
14
  * tag. The dropdown opens on a click, a keystroke or an arrow — not on focus, so the focus the
15
15
  * library returns to the input on Escape does not reopen it. The fields come first; picking one, or
16
- * typing its label and a colon, opens its values with their hit counts, and the values stay open
17
- * across picks so several can be ticked. Enter picks the highlighted option, or adds typed text as
16
+ * typing its label and a colon, opens its values with their hit counts; picking a value closes the
17
+ * list and returns the focus to the input. Enter picks the highlighted option, or adds typed text as
18
18
  * a tag when no field is entered. The ref is the text input's.
19
19
  */
20
20
  const FilterInput = forwardRef(({ fields, value: controlledValue, defaultValue = EMPTY_FILTER, onChange: onValueChange, placeholder: ownPlaceholder, disabled = false, readOnly = false, error = false, className, "data-component": componentName = FILTER_INPUT_NAME }, ref) => {
@@ -22,7 +22,6 @@ const FilterInput = forwardRef(({ fields, value: controlledValue, defaultValue =
22
22
  const composedRef = useComposedRefs(ref, inputRef);
23
23
  const removeRefs = useRef([]);
24
24
  const pickedRef = useRef(false);
25
- const keyboardPickRef = useRef(false);
26
25
  const headingId = useId();
27
26
  const [value, onChange] = useControlledState(controlledValue, defaultValue, onValueChange);
28
27
  const [text, setText] = useState("");
@@ -72,7 +71,10 @@ const FilterInput = forwardRef(({ fields, value: controlledValue, defaultValue =
72
71
  const enterField = (next, typed = "") => {
73
72
  setFieldId(next?.id);
74
73
  setText(typed);
75
- if (next !== void 0) openList();
74
+ if (next !== void 0) {
75
+ setActiveOption(null);
76
+ openList();
77
+ }
76
78
  };
77
79
  const add = (term) => {
78
80
  const next = withTerm(value, term);
@@ -101,8 +103,9 @@ const FilterInput = forwardRef(({ fields, value: controlledValue, defaultValue =
101
103
  announce(t("enonic.uiKit.filterInput.replaced", nameOf(previous), nameOf(term)));
102
104
  }
103
105
  } else add(term);
104
- setActiveOption(`${VALUE_OPTION}${candidate.id}`);
105
- setText("");
106
+ setActiveOption(null);
107
+ enterField(void 0);
108
+ setOpen(false);
106
109
  };
107
110
  const handleTextChange = (typed) => {
108
111
  const named = field === void 0 ? fieldTyped(fields, typed ?? "", fieldPrefixes) : void 0;
@@ -115,20 +118,15 @@ const FilterInput = forwardRef(({ fields, value: controlledValue, defaultValue =
115
118
  };
116
119
  const handlePick = ([id]) => {
117
120
  if (id === void 0) return;
118
- const fromKeyboard = keyboardPickRef.current;
119
- keyboardPickRef.current = false;
120
- pickedRef.current = true;
121
- if (id.startsWith(FIELD_OPTION)) enterField(fields.find((candidate) => candidate.id === id.slice(6)));
122
- else if (field !== void 0) {
121
+ if (id.startsWith(FIELD_OPTION)) {
122
+ pickedRef.current = true;
123
+ enterField(fields.find((candidate) => candidate.id === id.slice(6)));
124
+ } else if (field !== void 0) {
123
125
  const candidate = field.values.find(({ id: valueId }) => valueId === id.slice(6));
124
126
  if (candidate !== void 0) pickValue(candidate);
125
- if (fromKeyboard) return;
126
127
  }
127
128
  focusInput();
128
129
  };
129
- const noteValueKeyDown = (event) => {
130
- keyboardPickRef.current = event.key === "Enter" || event.key === " ";
131
- };
132
130
  const handleOpenChange = (next) => {
133
131
  if (!next && pickedRef.current) {
134
132
  pickedRef.current = false;
@@ -354,7 +352,6 @@ const FilterInput = forwardRef(({ fields, value: controlledValue, defaultValue =
354
352
  disabled: !selected && !isOffered(candidate),
355
353
  "aria-selected": selected,
356
354
  className: "data-[active]:bg-surface-neutral-hover rounded-sm px-3 py-2",
357
- onKeyDown: noteValueKeyDown,
358
355
  children: [
359
356
  /* @__PURE__ */ jsx("span", {
360
357
  className: "grow truncate",
@@ -1 +1 @@
1
- {"version":3,"file":"filter-input.js","names":[],"sources":["../../src/filter/filter-input.tsx"],"sourcesContent":["import {\n Button,\n cn,\n Combobox,\n IconButton,\n Listbox,\n Tag,\n useCombobox,\n useComposedRefs,\n useControlledState,\n usePhrases,\n} from '@enonic/ui';\nimport { Check, ListFilter, X } from 'lucide-react';\nimport {\n forwardRef,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type Ref,\n useId,\n useRef,\n useState,\n} from 'react';\n\nimport {\n fieldTyped,\n groupedValues,\n isOffered,\n matchingFields,\n matchingValues,\n termLabel,\n type FilterField,\n type FilterValue,\n} from './filter-fields';\nimport { filterInputPhrases } from './filter-input.phrases';\nimport {\n EMPTY_FILTER,\n fieldTerm,\n isFieldTerm,\n termKey,\n textTerm,\n toggledTerm,\n valuesOf,\n withoutTerm,\n withTerm,\n type FilterQuery,\n type FilterTerm,\n} from './filter-query';\n\nexport type FilterInputProps = {\n /** What the dropdown offers. None leaves the input a free-text search. */\n fields: readonly FilterField[];\n /** The query, controlled; leave it out and the input keeps its own, starting from `defaultValue`. */\n value?: FilterQuery;\n defaultValue?: FilterQuery;\n onChange?: (query: FilterQuery) => void;\n /** What the empty input invites, named for the list: `Search users`. Defaults to the kit's \"Search\". */\n placeholder?: string;\n disabled?: boolean;\n /** The tags are the whole content: no input, no crosses, no clearing. */\n readOnly?: boolean;\n /** The box reads as invalid; what is wrong with the query is the caller's to say beside it. */\n error?: boolean;\n className?: string;\n 'data-component'?: string;\n};\n\nconst FILTER_INPUT_NAME = 'FilterInput';\n\n// One listbox holds both stages, so the ids say which kind of option was picked.\nconst FIELD_OPTION = 'field:';\nconst VALUE_OPTION = 'value:';\n\n/**\n * The filter of a list: values of predefined fields combined with free text, every term a\n * tag. The dropdown opens on a click, a keystroke or an arrow — not on focus, so the focus the\n * library returns to the input on Escape does not reopen it. The fields come first; picking one, or\n * typing its label and a colon, opens its values with their hit counts, and the values stay open\n * across picks so several can be ticked. Enter picks the highlighted option, or adds typed text as\n * a tag when no field is entered. The ref is the text input's.\n */\nconst FilterInput = forwardRef<HTMLInputElement, FilterInputProps>(\n (\n {\n fields,\n value: controlledValue,\n defaultValue = EMPTY_FILTER,\n onChange: onValueChange,\n placeholder: ownPlaceholder,\n disabled = false,\n readOnly = false,\n error = false,\n className,\n 'data-component': componentName = FILTER_INPUT_NAME,\n },\n ref,\n ): ReactElement => {\n const inputRef = useRef<HTMLInputElement>(null);\n const composedRef = useComposedRefs(ref, inputRef);\n const removeRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const pickedRef = useRef(false);\n const keyboardPickRef = useRef(false);\n const headingId = useId();\n const [value, onChange] = useControlledState(controlledValue, defaultValue, onValueChange);\n const [text, setText] = useState('');\n const [open, setOpen] = useState(false);\n const [fieldId, setFieldId] = useState<string>();\n const [activeOption, setActiveOption] = useState<string | null>(null);\n const [announcement, setAnnouncement] = useState({ text: '', key: 0 });\n\n const t = usePhrases(filterInputPhrases);\n const filterLabel = t('enonic.uiKit.filterInput.label');\n const placeholder = ownPlaceholder ?? t('enonic.uiKit.filterInput.placeholder');\n const valuePlaceholder = t('enonic.uiKit.filterInput.valuePlaceholder');\n const clearLabel = t('enonic.uiKit.filterInput.clear');\n const clearedLabel = t('enonic.uiKit.filterInput.cleared');\n const noMatchesLabel = t('enonic.uiKit.filterInput.noMatches');\n const loadingLabel = t('enonic.uiKit.filterInput.loading');\n\n const field = fields.find(({ id }) => id === fieldId);\n const stage = field === undefined ? 'fields' : 'values';\n const offeredFields = field === undefined ? matchingFields(fields, text) : [];\n const offeredGroups =\n field === undefined ? [] : groupedValues(matchingValues(field.values, text));\n const offeredValues = offeredGroups.flatMap(({ values }) => values);\n const picked = field === undefined ? undefined : valuesOf(value, field.id);\n // The values stage always has something to say — a value, \"no matches\", the field's notice.\n const shown = !readOnly && open && (stage === 'values' || offeredFields.length > 0);\n const inert = disabled || readOnly;\n const active = !inert && (value.length > 0 || text.length > 0 || field !== undefined);\n\n // ? The library highlights an option only while it has the focus, so from the input Enter would act\n // ? on nothing the user can see. The active option is held here instead: the first one the list\n // ? offers, until the user moves it — except in the fields stage with text typed, where Enter is\n // ? the search and nothing is highlighted. It stays put across a pick, so Enter again takes it back.\n const optionIds =\n field === undefined\n ? text.length === 0\n ? offeredFields.map(({ id }) => `${FIELD_OPTION}${id}`)\n : []\n : offeredValues\n .filter((candidate) => isOffered(candidate) || picked?.has(candidate.id) === true)\n .map(({ id }) => `${VALUE_OPTION}${id}`);\n const highlighted =\n activeOption !== null && optionIds.includes(activeOption)\n ? activeOption\n : (optionIds[0] ?? null);\n\n // What a field is typed as to enter it: the form the tags show, and `Label:` whatever the locale.\n const fieldPrefixes = (label: string): string[] => [\n t('enonic.uiKit.filterInput.fieldPrefix', label),\n `${label}:`,\n ];\n\n const nameOf = (term: FilterTerm): string => {\n const { field: fieldName, value: valueName } = termLabel(term, fields);\n return fieldName === undefined\n ? valueName\n : t('enonic.uiKit.filterInput.term', fieldName, valueName);\n };\n\n const focusInput = (): void => {\n inputRef.current?.focus();\n };\n\n // Keyed, so the same message twice in a row is a new node the live region speaks again.\n const announce = (text: string): void => {\n setAnnouncement((previous) => ({ text, key: previous.key + 1 }));\n };\n\n const openList = (): void => {\n setOpen(true);\n };\n\n const enterField = (next: FilterField | undefined, typed = ''): void => {\n setFieldId(next?.id);\n setText(typed);\n if (next !== undefined) {\n openList();\n }\n };\n\n // A term already held changes nothing and says nothing: `withTerm` answers the same query.\n const add = (term: FilterTerm): void => {\n const next = withTerm(value, term);\n if (next === value) {\n return;\n }\n\n onChange(next);\n announce(t('enonic.uiKit.filterInput.added', nameOf(term)));\n };\n\n const remove = (index: number): void => {\n const term = value[index];\n if (term === undefined) {\n return;\n }\n\n onChange(withoutTerm(value, index));\n announce(t('enonic.uiKit.filterInput.removed', nameOf(term)));\n };\n\n // A value already picked is picked again to take it back, the way the tag's own cross does; a\n // single-choice field swaps its value. The stage stays, so the next value is one pick away.\n const pickValue = (candidate: FilterValue): void => {\n if (field === undefined) {\n return;\n }\n\n const term = fieldTerm(field.id, candidate.id);\n if (picked?.has(candidate.id) === true) {\n onChange(toggledTerm(value, term));\n announce(t('enonic.uiKit.filterInput.removed', nameOf(term)));\n } else if (field.multiple === false) {\n const ofField = (held: FilterTerm): boolean => isFieldTerm(held) && held.field === field.id;\n const previous = value.find(ofField);\n if (previous === undefined) {\n add(term);\n } else {\n onChange([...value.filter((held) => !ofField(held)), term]);\n announce(t('enonic.uiKit.filterInput.replaced', nameOf(previous), nameOf(term)));\n }\n } else {\n add(term);\n }\n // The pick stays highlighted once the text that narrowed the list is cleared and the list\n // widens again, so Enter again takes this value back rather than the first of the wider list.\n setActiveOption(`${VALUE_OPTION}${candidate.id}`);\n setText('');\n };\n\n const handleTextChange = (typed: string | undefined): void => {\n const named =\n field === undefined ? fieldTyped(fields, typed ?? '', fieldPrefixes) : undefined;\n if (named !== undefined) {\n enterField(named.field, named.text);\n return;\n }\n\n setText(typed ?? '');\n openList();\n };\n\n const handlePick = ([id]: readonly string[]): void => {\n if (id === undefined) {\n return;\n }\n\n const fromKeyboard = keyboardPickRef.current;\n keyboardPickRef.current = false;\n pickedRef.current = true;\n if (id.startsWith(FIELD_OPTION)) {\n enterField(fields.find((candidate) => candidate.id === id.slice(FIELD_OPTION.length)));\n } else if (field !== undefined) {\n const candidate = field.values.find(\n ({ id: valueId }) => valueId === id.slice(VALUE_OPTION.length),\n );\n if (candidate !== undefined) {\n pickValue(candidate);\n }\n // A value picked from the keyboard stays focused, so the arrows go on to the next value.\n if (fromKeyboard) {\n return;\n }\n }\n // ! Synchronously, before the library's blur check runs: the option that was clicked is about to\n // ! unmount with the focus on it, and a focus that landed on the body would read as leaving.\n focusInput();\n };\n\n // Runs on the item before the listbox's own handler toggles it, so the pick knows its source.\n const noteValueKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n keyboardPickRef.current = event.key === 'Enter' || event.key === ' ';\n };\n\n const handleOpenChange = (next: boolean): void => {\n // ! A single-select combobox closes on a pick. Ours stays open — a pick swaps the stage, and a\n // ! value picked is one of several — so the close a pick asks for is the one to ignore.\n if (!next && pickedRef.current) {\n pickedRef.current = false;\n return;\n }\n\n // Leaving the filter abandons a field no value was picked for: a bare `Field:` prefix left in\n // the input would read as a term that narrows nothing.\n if (!next && field !== undefined) {\n enterField(undefined);\n }\n\n setOpen(next);\n };\n\n // Enter toggles the highlighted option while the list is open — the same option Enter on the\n // focused item toggles — or enters the highlighted field. Typed text with no field entered is a\n // search term. With nothing highlighted the stage stays — the \"no matches\" row says why — and a\n // closed list opens.\n const handleEnter = (listOpen: boolean): void => {\n if (field !== undefined) {\n if (!listOpen) {\n openList();\n return;\n }\n\n const candidate = offeredValues.find(({ id }) => `${VALUE_OPTION}${id}` === highlighted);\n if (candidate !== undefined) {\n pickValue(candidate);\n }\n return;\n }\n\n const term = textTerm(text);\n if (term !== undefined) {\n add(term);\n setText('');\n return;\n }\n\n const named = fields.find(({ id }) => `${FIELD_OPTION}${id}` === highlighted);\n if (listOpen && named !== undefined) {\n enterField(named);\n } else {\n openList();\n }\n };\n\n const handleBackspace = (): void => {\n if (field !== undefined) {\n enterField(undefined);\n } else if (value.length > 0) {\n remove(value.length - 1);\n }\n };\n\n const handleEscape = (): boolean => {\n if (field === undefined) {\n return false;\n }\n\n enterField(undefined);\n return true;\n };\n\n // The tags are one tab stop with the input: arrows walk the crosses, Backspace or Delete on a\n // cross takes its tag back and the focus moves one tag left, or home to the input.\n const focusTag = (index: number): boolean => {\n const cross = removeRefs.current[index];\n if (cross === null || cross === undefined) {\n return false;\n }\n\n cross.focus();\n return true;\n };\n\n const handleArrowLeft = (): boolean => focusTag(value.length - 1);\n\n const handleTagKeyDown = (index: number) => (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault();\n focusTag(index - 1);\n } else if (event.key === 'ArrowRight') {\n event.preventDefault();\n if (!focusTag(index + 1)) {\n focusInput();\n }\n } else if (event.key === 'Backspace' || event.key === 'Delete') {\n event.preventDefault();\n remove(index);\n if (!focusTag(index - 1)) {\n focusInput();\n }\n } else if (event.key === 'Escape') {\n event.preventDefault();\n focusInput();\n }\n };\n\n // A click on the box's own padding is a click on the field: the input takes the focus the body\n // would otherwise get, and the list opens.\n const handleBoxPointerDown = (event: PointerEvent<HTMLDivElement>): void => {\n if (inert || !(event.target instanceof Element) || event.target.closest('button, input')) {\n return;\n }\n\n event.preventDefault();\n focusInput();\n openList();\n };\n\n const clear = (): void => {\n if (value.length > 0) {\n onChange([]);\n }\n announce(clearedLabel);\n enterField(undefined);\n focusInput();\n };\n\n return (\n <Combobox\n open={shown}\n onOpenChange={handleOpenChange}\n value={text}\n onChange={handleTextChange}\n selection={[]}\n onSelectionChange={handlePick}\n active={highlighted}\n setActive={setActiveOption}\n contentType=\"listbox\"\n disabled={disabled}\n error={error}\n >\n {/*\n ! The box is the Content, not the Control: the library anchors its popup to the Control and\n ! gives it the Control's width, so the Control is the input alone — the popup then opens under\n ! the caret, after the tags, and takes the width of what it lists. The border, hover and focus\n ! ring the Control would have drawn move out here, to the box the user sees as the field.\n ! `data-click-outside-ignore` makes the whole box \"inside\" for the library's outside-click\n ! check, which otherwise knows only the Control: a click on a tag's cross or the Filter button\n ! would close the list and drop the field.\n */}\n <Combobox.Content\n data-component={componentName}\n data-click-outside-ignore\n onPointerDown={handleBoxPointerDown}\n className={cn(\n 'bg-surface-neutral border-bdr-subtle flex min-h-12 shrink-0 items-start gap-1.5 rounded-sm border p-1.5',\n 'hover:outline-bdr-subtle hover:outline-2',\n 'focus-within:ring-ring focus-within:ring-offset-ring-offset focus-within:ring-3 focus-within:ring-offset-3 focus-within:outline-none',\n 'transition-highlight',\n shown && 'border-bdr-strong',\n error && 'border-error focus-within:ring-error hover:outline-error',\n disabled && 'pointer-events-none opacity-50',\n className,\n )}\n >\n <Button\n variant=\"text\"\n size=\"sm\"\n startIcon={ListFilter}\n label={filterLabel}\n disabled={inert}\n onClick={() => {\n focusInput();\n openList();\n }}\n className=\"shrink-0\"\n />\n\n {/* Only this middle wraps; the button and the cross stay on the first line, at the edges. */}\n <div className=\"flex min-h-9 min-w-0 flex-1 flex-wrap content-center items-center gap-x-2 gap-y-1.5\">\n {value.map((term, index) => {\n const { field: fieldName, value: valueName } = termLabel(term, fields);\n\n return (\n <Tag key={termKey(term)} disabled={disabled}>\n {fieldName !== undefined && (\n <Tag.Prefix>{t('enonic.uiKit.filterInput.fieldPrefix', fieldName)}</Tag.Prefix>\n )}\n <Tag.Label>{valueName}</Tag.Label>\n {!readOnly && (\n <Tag.Remove\n ref={(node: HTMLButtonElement | null) => {\n removeRefs.current[index] = node;\n }}\n tabIndex={-1}\n aria-label={t('enonic.uiKit.filterInput.remove', nameOf(term))}\n disabled={disabled}\n onClick={() => {\n remove(index);\n focusInput();\n }}\n onKeyDown={handleTagKeyDown(index)}\n />\n )}\n </Tag>\n );\n })}\n\n {field !== undefined && (\n <span className=\"text-subtle shrink-0 whitespace-nowrap\">\n {t('enonic.uiKit.filterInput.fieldPrefix', field.label)}\n </span>\n )}\n\n {/* Read-only, the tags are the whole content: an input that takes nothing would be a\n focus stop with nothing to do, and a placeholder that invites a search it cannot take. */}\n {!readOnly && (\n <Combobox.Control className=\"h-auto min-h-0 min-w-40 flex-1 rounded-none border-0 bg-transparent focus-within:ring-0 focus-within:ring-offset-0\">\n <Combobox.Search className=\"h-auto bg-transparent px-1 py-0 hover:outline-none\">\n <FilterTextInput\n inputRef={composedRef}\n text={text}\n placeholder={field === undefined ? placeholder : valuePlaceholder}\n // The field the values belong to is in the name, since the prefix beside the\n // input is not associated with it.\n label={\n field === undefined\n ? placeholder\n : t('enonic.uiKit.filterInput.term', field.label, valuePlaceholder)\n }\n onOpen={openList}\n onEnter={handleEnter}\n onBackspace={handleBackspace}\n onEscape={handleEscape}\n onArrowLeft={handleArrowLeft}\n />\n </Combobox.Search>\n </Combobox.Control>\n )}\n </div>\n\n {active && (\n <IconButton\n icon={X}\n variant=\"text\"\n iconSize={28}\n iconStrokeWidth={1.25}\n title={clearLabel}\n aria-label={clearLabel}\n disabled={disabled}\n onClick={clear}\n className=\"text-subtle my-1 mr-1 size-7 shrink-0\"\n />\n )}\n\n <div aria-live=\"polite\" className=\"sr-only\">\n <span key={announcement.key}>{announcement.text}</span>\n </div>\n\n <Combobox.Portal>\n {/* ? `width: auto` undoes the Control's width the library sets inline; the list sizes to its labels. */}\n <Combobox.Popup style={{ width: 'auto' }} className=\"max-w-md min-w-72\">\n {/* Keyed by stage, so the list activates its first option again when the options swap. */}\n <Combobox.ListContent\n key={stage}\n aria-label={field === undefined ? filterLabel : field.label}\n // ? The listbox is left in single mode so Enter on a focused option picks it; what the\n // ? values stage is — several ticks at once — is said here instead.\n aria-multiselectable={\n field !== undefined && field.multiple !== false ? true : undefined\n }\n className=\"max-h-72 gap-y-0.5 overflow-y-auto p-2\"\n >\n {offeredFields.map(({ id, label, icon: Icon }) => (\n <Listbox.Item\n key={id}\n value={`${FIELD_OPTION}${id}`}\n className=\"data-[active]:bg-surface-neutral-hover gap-2.5 rounded-sm px-3 py-2\"\n >\n {Icon !== undefined && (\n <Icon\n size={16}\n strokeWidth={1.5}\n className=\"text-subtle shrink-0\"\n aria-hidden\n />\n )}\n <span className=\"truncate\">{label}</span>\n </Listbox.Item>\n ))}\n\n {field?.notice !== undefined && (\n <div role=\"presentation\" className=\"text-subtle px-3 py-2 text-xs\">\n {field.notice}\n </div>\n )}\n\n {field?.loading === true && (\n <div role=\"presentation\" aria-busy className=\"text-subtle px-3 py-2\">\n {loadingLabel}\n </div>\n )}\n\n {field !== undefined && field.loading !== true && offeredValues.length === 0 && (\n <div role=\"presentation\" className=\"text-subtle px-3 py-2\">\n {noMatchesLabel}\n </div>\n )}\n\n {offeredGroups.map(({ label: groupLabel, values }, groupIndex) => {\n const groupHeadingId =\n groupLabel === undefined ? undefined : `${headingId}-group-${groupIndex}`;\n\n return (\n <div\n key={groupLabel === undefined ? 'ungrouped' : `group:${groupLabel}`}\n role=\"group\"\n aria-labelledby={groupHeadingId}\n className=\"contents\"\n >\n {groupLabel !== undefined && (\n <div\n role=\"presentation\"\n className=\"flex w-full items-baseline gap-2.5 px-3 pt-2.5 pb-1\"\n >\n <span\n id={groupHeadingId}\n className=\"text-subtle min-w-0 truncate text-xs tracking-wider uppercase\"\n >\n {groupLabel}\n </span>\n <span className=\"border-bdr-subtle min-w-6 flex-1 border-b\" />\n </div>\n )}\n\n {values.map((candidate) => {\n const selected = picked?.has(candidate.id) === true;\n\n return (\n <Listbox.Item\n key={candidate.id}\n value={`${VALUE_OPTION}${candidate.id}`}\n // Picked or not, the value stays pickable: picking it again takes the term back.\n disabled={!selected && !isOffered(candidate)}\n // ? The listbox's own selection stays empty, so Enter keeps picking the option;\n // ? the item spreads props after its own, which is what lets the state be said here.\n aria-selected={selected}\n className=\"data-[active]:bg-surface-neutral-hover rounded-sm px-3 py-2\"\n onKeyDown={noteValueKeyDown}\n >\n <span className=\"grow truncate\">{candidate.label}</span>\n {selected && (\n <Check size={16} strokeWidth={2} className=\"shrink-0\" aria-hidden />\n )}\n {candidate.count !== undefined && (\n <span className=\"text-subtle tabular-nums\">({candidate.count})</span>\n )}\n </Listbox.Item>\n );\n })}\n </div>\n );\n })}\n </Combobox.ListContent>\n </Combobox.Popup>\n </Combobox.Portal>\n </Combobox.Content>\n </Combobox>\n );\n },\n);\n\nFilterInput.displayName = FILTER_INPUT_NAME;\n\nexport { FilterInput };\n\n//\n// * Internal\n//\n\n/**\n * Whether the key belongs to an IME composition. React keeps the flag on `nativeEvent`, Preact on\n * the event itself; 229 is the key code every browser reports for a composing key.\n */\nfunction isComposing(event: {\n isComposing?: boolean;\n keyCode?: number;\n nativeEvent?: { isComposing?: boolean; keyCode?: number };\n}): boolean {\n const native = event.nativeEvent ?? event;\n return native.isComposing === true || native.keyCode === 229;\n}\n\ntype FilterTextInputProps = {\n inputRef: Ref<HTMLInputElement>;\n text: string;\n placeholder: string;\n label: string;\n onOpen: () => void;\n /** With whether the list is open. */\n onEnter: (listOpen: boolean) => void;\n onBackspace: () => void;\n /** Answers whether it consumed the key; otherwise the library closes the dropdown. */\n onEscape: () => boolean;\n /** Answers whether a tag took the focus. */\n onArrowLeft: () => boolean;\n};\n\n/**\n * The input, with the keys a tag input adds to a combobox's: Enter picks or commits, Backspace on an\n * empty input takes the last tag back, Escape steps out of a field, ArrowLeft at the start walks into\n * the tags. Everything else — the arrows into the list, Escape closing — is the library's, reached\n * through its context. A key pressed mid-composition is the IME's.\n */\nconst FilterTextInput = ({\n inputRef,\n text,\n placeholder,\n label,\n onOpen,\n onEnter,\n onBackspace,\n onEscape,\n onArrowLeft,\n}: FilterTextInputProps): ReactElement => {\n const { keyHandler, open } = useCombobox();\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {\n if (isComposing(event)) {\n return;\n }\n\n if (event.key === 'Enter') {\n event.preventDefault();\n onEnter(open);\n return;\n }\n\n if (event.key === 'Backspace' && text.length === 0 && !event.repeat) {\n onBackspace();\n return;\n }\n\n if (event.key === 'Escape' && onEscape()) {\n event.preventDefault();\n return;\n }\n\n const atStart =\n event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;\n if (event.key === 'ArrowLeft' && atStart && onArrowLeft()) {\n event.preventDefault();\n return;\n }\n\n keyHandler(event);\n };\n\n return (\n <Combobox.Input\n ref={inputRef}\n // The library's input names itself `Search` in English; the caller's own prompt is the name here.\n aria-label={label}\n placeholder={placeholder}\n className=\"bg-transparent\"\n onClick={onOpen}\n onKeyDown={handleKeyDown}\n />\n );\n};\nFilterTextInput.displayName = 'FilterInput.TextInput';\n"],"mappings":"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"}
1
+ {"version":3,"file":"filter-input.js","names":[],"sources":["../../src/filter/filter-input.tsx"],"sourcesContent":["import {\n Button,\n cn,\n Combobox,\n IconButton,\n Listbox,\n Tag,\n useCombobox,\n useComposedRefs,\n useControlledState,\n usePhrases,\n} from '@enonic/ui';\nimport { Check, ListFilter, X } from 'lucide-react';\nimport {\n forwardRef,\n type KeyboardEvent,\n type PointerEvent,\n type ReactElement,\n type Ref,\n useId,\n useRef,\n useState,\n} from 'react';\n\nimport {\n fieldTyped,\n groupedValues,\n isOffered,\n matchingFields,\n matchingValues,\n termLabel,\n type FilterField,\n type FilterValue,\n} from './filter-fields';\nimport { filterInputPhrases } from './filter-input.phrases';\nimport {\n EMPTY_FILTER,\n fieldTerm,\n isFieldTerm,\n termKey,\n textTerm,\n toggledTerm,\n valuesOf,\n withoutTerm,\n withTerm,\n type FilterQuery,\n type FilterTerm,\n} from './filter-query';\n\nexport type FilterInputProps = {\n /** What the dropdown offers. None leaves the input a free-text search. */\n fields: readonly FilterField[];\n /** The query, controlled; leave it out and the input keeps its own, starting from `defaultValue`. */\n value?: FilterQuery;\n defaultValue?: FilterQuery;\n onChange?: (query: FilterQuery) => void;\n /** What the empty input invites, named for the list: `Search users`. Defaults to the kit's \"Search\". */\n placeholder?: string;\n disabled?: boolean;\n /** The tags are the whole content: no input, no crosses, no clearing. */\n readOnly?: boolean;\n /** The box reads as invalid; what is wrong with the query is the caller's to say beside it. */\n error?: boolean;\n className?: string;\n 'data-component'?: string;\n};\n\nconst FILTER_INPUT_NAME = 'FilterInput';\n\n// One listbox holds both stages, so the ids say which kind of option was picked.\nconst FIELD_OPTION = 'field:';\nconst VALUE_OPTION = 'value:';\n\n/**\n * The filter of a list: values of predefined fields combined with free text, every term a\n * tag. The dropdown opens on a click, a keystroke or an arrow — not on focus, so the focus the\n * library returns to the input on Escape does not reopen it. The fields come first; picking one, or\n * typing its label and a colon, opens its values with their hit counts; picking a value closes the\n * list and returns the focus to the input. Enter picks the highlighted option, or adds typed text as\n * a tag when no field is entered. The ref is the text input's.\n */\nconst FilterInput = forwardRef<HTMLInputElement, FilterInputProps>(\n (\n {\n fields,\n value: controlledValue,\n defaultValue = EMPTY_FILTER,\n onChange: onValueChange,\n placeholder: ownPlaceholder,\n disabled = false,\n readOnly = false,\n error = false,\n className,\n 'data-component': componentName = FILTER_INPUT_NAME,\n },\n ref,\n ): ReactElement => {\n const inputRef = useRef<HTMLInputElement>(null);\n const composedRef = useComposedRefs(ref, inputRef);\n const removeRefs = useRef<(HTMLButtonElement | null)[]>([]);\n const pickedRef = useRef(false);\n const headingId = useId();\n const [value, onChange] = useControlledState(controlledValue, defaultValue, onValueChange);\n const [text, setText] = useState('');\n const [open, setOpen] = useState(false);\n const [fieldId, setFieldId] = useState<string>();\n const [activeOption, setActiveOption] = useState<string | null>(null);\n const [announcement, setAnnouncement] = useState({ text: '', key: 0 });\n\n const t = usePhrases(filterInputPhrases);\n const filterLabel = t('enonic.uiKit.filterInput.label');\n const placeholder = ownPlaceholder ?? t('enonic.uiKit.filterInput.placeholder');\n const valuePlaceholder = t('enonic.uiKit.filterInput.valuePlaceholder');\n const clearLabel = t('enonic.uiKit.filterInput.clear');\n const clearedLabel = t('enonic.uiKit.filterInput.cleared');\n const noMatchesLabel = t('enonic.uiKit.filterInput.noMatches');\n const loadingLabel = t('enonic.uiKit.filterInput.loading');\n\n const field = fields.find(({ id }) => id === fieldId);\n const stage = field === undefined ? 'fields' : 'values';\n const offeredFields = field === undefined ? matchingFields(fields, text) : [];\n const offeredGroups =\n field === undefined ? [] : groupedValues(matchingValues(field.values, text));\n const offeredValues = offeredGroups.flatMap(({ values }) => values);\n const picked = field === undefined ? undefined : valuesOf(value, field.id);\n // The values stage always has something to say — a value, \"no matches\", the field's notice.\n const shown = !readOnly && open && (stage === 'values' || offeredFields.length > 0);\n const inert = disabled || readOnly;\n const active = !inert && (value.length > 0 || text.length > 0 || field !== undefined);\n\n // ? The library highlights an option only while it has the focus, so from the input Enter would act\n // ? on nothing the user can see. The active option is held here instead: the first one the list\n // ? offers, until the user moves it — except in the fields stage with text typed, where Enter is\n // ? the search and nothing is highlighted.\n const optionIds =\n field === undefined\n ? text.length === 0\n ? offeredFields.map(({ id }) => `${FIELD_OPTION}${id}`)\n : []\n : offeredValues\n .filter((candidate) => isOffered(candidate) || picked?.has(candidate.id) === true)\n .map(({ id }) => `${VALUE_OPTION}${id}`);\n const highlighted =\n activeOption !== null && optionIds.includes(activeOption)\n ? activeOption\n : (optionIds[0] ?? null);\n\n const fieldPrefixes = (label: string): string[] => [\n t('enonic.uiKit.filterInput.fieldPrefix', label),\n `${label}:`,\n ];\n\n const nameOf = (term: FilterTerm): string => {\n const { field: fieldName, value: valueName } = termLabel(term, fields);\n return fieldName === undefined\n ? valueName\n : t('enonic.uiKit.filterInput.term', fieldName, valueName);\n };\n\n const focusInput = (): void => {\n inputRef.current?.focus();\n };\n\n // Keyed, so the same message twice in a row is a new node the live region speaks again.\n const announce = (text: string): void => {\n setAnnouncement((previous) => ({ text, key: previous.key + 1 }));\n };\n\n const openList = (): void => {\n setOpen(true);\n };\n\n const enterField = (next: FilterField | undefined, typed = ''): void => {\n setFieldId(next?.id);\n setText(typed);\n if (next !== undefined) {\n setActiveOption(null);\n openList();\n }\n };\n\n const add = (term: FilterTerm): void => {\n const next = withTerm(value, term);\n if (next === value) {\n return;\n }\n\n onChange(next);\n announce(t('enonic.uiKit.filterInput.added', nameOf(term)));\n };\n\n const remove = (index: number): void => {\n const term = value[index];\n if (term === undefined) {\n return;\n }\n\n onChange(withoutTerm(value, index));\n announce(t('enonic.uiKit.filterInput.removed', nameOf(term)));\n };\n\n // A value already picked is picked again to take it back, the way the tag's own cross does; a\n // single-choice field swaps its value. Either way the pick leaves the field and closes the list.\n const pickValue = (candidate: FilterValue): void => {\n if (field === undefined) {\n return;\n }\n\n const term = fieldTerm(field.id, candidate.id);\n if (picked?.has(candidate.id) === true) {\n onChange(toggledTerm(value, term));\n announce(t('enonic.uiKit.filterInput.removed', nameOf(term)));\n } else if (field.multiple === false) {\n const ofField = (held: FilterTerm): boolean => isFieldTerm(held) && held.field === field.id;\n const previous = value.find(ofField);\n if (previous === undefined) {\n add(term);\n } else {\n onChange([...value.filter((held) => !ofField(held)), term]);\n announce(t('enonic.uiKit.filterInput.replaced', nameOf(previous), nameOf(term)));\n }\n } else {\n add(term);\n }\n setActiveOption(null);\n enterField(undefined);\n setOpen(false);\n };\n\n const handleTextChange = (typed: string | undefined): void => {\n const named =\n field === undefined ? fieldTyped(fields, typed ?? '', fieldPrefixes) : undefined;\n if (named !== undefined) {\n enterField(named.field, named.text);\n return;\n }\n\n setText(typed ?? '');\n openList();\n };\n\n const handlePick = ([id]: readonly string[]): void => {\n if (id === undefined) {\n return;\n }\n\n if (id.startsWith(FIELD_OPTION)) {\n pickedRef.current = true;\n enterField(fields.find((candidate) => candidate.id === id.slice(FIELD_OPTION.length)));\n } else if (field !== undefined) {\n const candidate = field.values.find(\n ({ id: valueId }) => valueId === id.slice(VALUE_OPTION.length),\n );\n if (candidate !== undefined) {\n pickValue(candidate);\n }\n }\n // ! Synchronously, before the library's blur check runs: the option that was clicked is about to\n // ! unmount with the focus on it, and a focus that landed on the body would read as leaving.\n focusInput();\n };\n\n const handleOpenChange = (next: boolean): void => {\n // ! A single-select combobox closes on a pick. Picking a field swaps the stage instead, so the\n // ! close that pick asks for is the one to ignore.\n if (!next && pickedRef.current) {\n pickedRef.current = false;\n return;\n }\n\n // Leaving the filter abandons a field no value was picked for: a bare `Field:` prefix left in\n // the input would read as a term that narrows nothing.\n if (!next && field !== undefined) {\n enterField(undefined);\n }\n\n setOpen(next);\n };\n\n // Enter toggles the highlighted option while the list is open — the same option Enter on the\n // focused item toggles — or enters the highlighted field. Typed text with no field entered is a\n // search term. With nothing highlighted the stage stays — the \"no matches\" row says why — and a\n // closed list opens.\n const handleEnter = (listOpen: boolean): void => {\n if (field !== undefined) {\n if (!listOpen) {\n openList();\n return;\n }\n\n const candidate = offeredValues.find(({ id }) => `${VALUE_OPTION}${id}` === highlighted);\n if (candidate !== undefined) {\n pickValue(candidate);\n }\n return;\n }\n\n const term = textTerm(text);\n if (term !== undefined) {\n add(term);\n setText('');\n return;\n }\n\n const named = fields.find(({ id }) => `${FIELD_OPTION}${id}` === highlighted);\n if (listOpen && named !== undefined) {\n enterField(named);\n } else {\n openList();\n }\n };\n\n const handleBackspace = (): void => {\n if (field !== undefined) {\n enterField(undefined);\n } else if (value.length > 0) {\n remove(value.length - 1);\n }\n };\n\n const handleEscape = (): boolean => {\n if (field === undefined) {\n return false;\n }\n\n enterField(undefined);\n return true;\n };\n\n // The tags are one tab stop with the input: arrows walk the crosses, Backspace or Delete on a\n // cross takes its tag back and the focus moves one tag left, or home to the input.\n const focusTag = (index: number): boolean => {\n const cross = removeRefs.current[index];\n if (cross === null || cross === undefined) {\n return false;\n }\n\n cross.focus();\n return true;\n };\n\n const handleArrowLeft = (): boolean => focusTag(value.length - 1);\n\n const handleTagKeyDown = (index: number) => (event: KeyboardEvent<HTMLButtonElement>) => {\n if (event.key === 'ArrowLeft') {\n event.preventDefault();\n focusTag(index - 1);\n } else if (event.key === 'ArrowRight') {\n event.preventDefault();\n if (!focusTag(index + 1)) {\n focusInput();\n }\n } else if (event.key === 'Backspace' || event.key === 'Delete') {\n event.preventDefault();\n remove(index);\n if (!focusTag(index - 1)) {\n focusInput();\n }\n } else if (event.key === 'Escape') {\n event.preventDefault();\n focusInput();\n }\n };\n\n // A click on the box's own padding is a click on the field: the input takes the focus the body\n // would otherwise get, and the list opens.\n const handleBoxPointerDown = (event: PointerEvent<HTMLDivElement>): void => {\n if (inert || !(event.target instanceof Element) || event.target.closest('button, input')) {\n return;\n }\n\n event.preventDefault();\n focusInput();\n openList();\n };\n\n const clear = (): void => {\n if (value.length > 0) {\n onChange([]);\n }\n announce(clearedLabel);\n enterField(undefined);\n focusInput();\n };\n\n return (\n <Combobox\n open={shown}\n onOpenChange={handleOpenChange}\n value={text}\n onChange={handleTextChange}\n selection={[]}\n onSelectionChange={handlePick}\n active={highlighted}\n setActive={setActiveOption}\n contentType=\"listbox\"\n disabled={disabled}\n error={error}\n >\n {/*\n ! The box is the Content, not the Control: the library anchors its popup to the Control and\n ! gives it the Control's width, so the Control is the input alone — the popup then opens under\n ! the caret, after the tags, and takes the width of what it lists. The border, hover and focus\n ! ring the Control would have drawn move out here, to the box the user sees as the field.\n ! `data-click-outside-ignore` makes the whole box \"inside\" for the library's outside-click\n ! check, which otherwise knows only the Control: a click on a tag's cross or the Filter button\n ! would close the list and drop the field.\n */}\n <Combobox.Content\n data-component={componentName}\n data-click-outside-ignore\n onPointerDown={handleBoxPointerDown}\n className={cn(\n 'bg-surface-neutral border-bdr-subtle flex min-h-12 shrink-0 items-start gap-1.5 rounded-sm border p-1.5',\n 'hover:outline-bdr-subtle hover:outline-2',\n 'focus-within:ring-ring focus-within:ring-offset-ring-offset focus-within:ring-3 focus-within:ring-offset-3 focus-within:outline-none',\n 'transition-highlight',\n shown && 'border-bdr-strong',\n error && 'border-error focus-within:ring-error hover:outline-error',\n disabled && 'pointer-events-none opacity-50',\n className,\n )}\n >\n <Button\n variant=\"text\"\n size=\"sm\"\n startIcon={ListFilter}\n label={filterLabel}\n disabled={inert}\n onClick={() => {\n focusInput();\n openList();\n }}\n className=\"shrink-0\"\n />\n\n {/* Only this middle wraps; the button and the cross stay on the first line, at the edges. */}\n <div className=\"flex min-h-9 min-w-0 flex-1 flex-wrap content-center items-center gap-x-2 gap-y-1.5\">\n {value.map((term, index) => {\n const { field: fieldName, value: valueName } = termLabel(term, fields);\n\n return (\n <Tag key={termKey(term)} disabled={disabled}>\n {fieldName !== undefined && (\n <Tag.Prefix>{t('enonic.uiKit.filterInput.fieldPrefix', fieldName)}</Tag.Prefix>\n )}\n <Tag.Label>{valueName}</Tag.Label>\n {!readOnly && (\n <Tag.Remove\n ref={(node: HTMLButtonElement | null) => {\n removeRefs.current[index] = node;\n }}\n tabIndex={-1}\n aria-label={t('enonic.uiKit.filterInput.remove', nameOf(term))}\n disabled={disabled}\n onClick={() => {\n remove(index);\n focusInput();\n }}\n onKeyDown={handleTagKeyDown(index)}\n />\n )}\n </Tag>\n );\n })}\n\n {field !== undefined && (\n <span className=\"text-subtle shrink-0 whitespace-nowrap\">\n {t('enonic.uiKit.filterInput.fieldPrefix', field.label)}\n </span>\n )}\n\n {/* Read-only, the tags are the whole content: an input that takes nothing would be a\n focus stop with nothing to do, and a placeholder that invites a search it cannot take. */}\n {!readOnly && (\n <Combobox.Control className=\"h-auto min-h-0 min-w-40 flex-1 rounded-none border-0 bg-transparent focus-within:ring-0 focus-within:ring-offset-0\">\n <Combobox.Search className=\"h-auto bg-transparent px-1 py-0 hover:outline-none\">\n <FilterTextInput\n inputRef={composedRef}\n text={text}\n placeholder={field === undefined ? placeholder : valuePlaceholder}\n // The field the values belong to is in the name, since the prefix beside the\n // input is not associated with it.\n label={\n field === undefined\n ? placeholder\n : t('enonic.uiKit.filterInput.term', field.label, valuePlaceholder)\n }\n onOpen={openList}\n onEnter={handleEnter}\n onBackspace={handleBackspace}\n onEscape={handleEscape}\n onArrowLeft={handleArrowLeft}\n />\n </Combobox.Search>\n </Combobox.Control>\n )}\n </div>\n\n {active && (\n <IconButton\n icon={X}\n variant=\"text\"\n iconSize={28}\n iconStrokeWidth={1.25}\n title={clearLabel}\n aria-label={clearLabel}\n disabled={disabled}\n onClick={clear}\n className=\"text-subtle my-1 mr-1 size-7 shrink-0\"\n />\n )}\n\n <div aria-live=\"polite\" className=\"sr-only\">\n <span key={announcement.key}>{announcement.text}</span>\n </div>\n\n <Combobox.Portal>\n {/* ? `width: auto` undoes the Control's width the library sets inline; the list sizes to its labels. */}\n <Combobox.Popup style={{ width: 'auto' }} className=\"max-w-md min-w-72\">\n {/* Keyed by stage, so the list activates its first option again when the options swap. */}\n <Combobox.ListContent\n key={stage}\n aria-label={field === undefined ? filterLabel : field.label}\n // ? The listbox is left in single mode so Enter on a focused option picks it; what the\n // ? values stage is — several ticks at once — is said here instead.\n aria-multiselectable={\n field !== undefined && field.multiple !== false ? true : undefined\n }\n className=\"max-h-72 gap-y-0.5 overflow-y-auto p-2\"\n >\n {offeredFields.map(({ id, label, icon: Icon }) => (\n <Listbox.Item\n key={id}\n value={`${FIELD_OPTION}${id}`}\n className=\"data-[active]:bg-surface-neutral-hover gap-2.5 rounded-sm px-3 py-2\"\n >\n {Icon !== undefined && (\n <Icon\n size={16}\n strokeWidth={1.5}\n className=\"text-subtle shrink-0\"\n aria-hidden\n />\n )}\n <span className=\"truncate\">{label}</span>\n </Listbox.Item>\n ))}\n\n {field?.notice !== undefined && (\n <div role=\"presentation\" className=\"text-subtle px-3 py-2 text-xs\">\n {field.notice}\n </div>\n )}\n\n {field?.loading === true && (\n <div role=\"presentation\" aria-busy className=\"text-subtle px-3 py-2\">\n {loadingLabel}\n </div>\n )}\n\n {field !== undefined && field.loading !== true && offeredValues.length === 0 && (\n <div role=\"presentation\" className=\"text-subtle px-3 py-2\">\n {noMatchesLabel}\n </div>\n )}\n\n {offeredGroups.map(({ label: groupLabel, values }, groupIndex) => {\n const groupHeadingId =\n groupLabel === undefined ? undefined : `${headingId}-group-${groupIndex}`;\n\n return (\n <div\n key={groupLabel === undefined ? 'ungrouped' : `group:${groupLabel}`}\n role=\"group\"\n aria-labelledby={groupHeadingId}\n className=\"contents\"\n >\n {groupLabel !== undefined && (\n <div\n role=\"presentation\"\n className=\"flex w-full items-baseline gap-2.5 px-3 pt-2.5 pb-1\"\n >\n <span\n id={groupHeadingId}\n className=\"text-subtle min-w-0 truncate text-xs tracking-wider uppercase\"\n >\n {groupLabel}\n </span>\n <span className=\"border-bdr-subtle min-w-6 flex-1 border-b\" />\n </div>\n )}\n\n {values.map((candidate) => {\n const selected = picked?.has(candidate.id) === true;\n\n return (\n <Listbox.Item\n key={candidate.id}\n value={`${VALUE_OPTION}${candidate.id}`}\n // Picked or not, the value stays pickable: picking it again takes the term back.\n disabled={!selected && !isOffered(candidate)}\n // ? The listbox's own selection stays empty, so Enter keeps picking the option;\n // ? the item spreads props after its own, which is what lets the state be said here.\n aria-selected={selected}\n className=\"data-[active]:bg-surface-neutral-hover rounded-sm px-3 py-2\"\n >\n <span className=\"grow truncate\">{candidate.label}</span>\n {selected && (\n <Check size={16} strokeWidth={2} className=\"shrink-0\" aria-hidden />\n )}\n {candidate.count !== undefined && (\n <span className=\"text-subtle tabular-nums\">({candidate.count})</span>\n )}\n </Listbox.Item>\n );\n })}\n </div>\n );\n })}\n </Combobox.ListContent>\n </Combobox.Popup>\n </Combobox.Portal>\n </Combobox.Content>\n </Combobox>\n );\n },\n);\n\nFilterInput.displayName = FILTER_INPUT_NAME;\n\nexport { FilterInput };\n\n//\n// * Internal\n//\n\n/**\n * Whether the key belongs to an IME composition. React keeps the flag on `nativeEvent`, Preact on\n * the event itself; 229 is the key code every browser reports for a composing key.\n */\nfunction isComposing(event: {\n isComposing?: boolean;\n keyCode?: number;\n nativeEvent?: { isComposing?: boolean; keyCode?: number };\n}): boolean {\n const native = event.nativeEvent ?? event;\n return native.isComposing === true || native.keyCode === 229;\n}\n\ntype FilterTextInputProps = {\n inputRef: Ref<HTMLInputElement>;\n text: string;\n placeholder: string;\n label: string;\n onOpen: () => void;\n /** With whether the list is open. */\n onEnter: (listOpen: boolean) => void;\n onBackspace: () => void;\n /** Answers whether it consumed the key; otherwise the library closes the dropdown. */\n onEscape: () => boolean;\n /** Answers whether a tag took the focus. */\n onArrowLeft: () => boolean;\n};\n\n/**\n * The input, with the keys a tag input adds to a combobox's: Enter picks or commits, Backspace on an\n * empty input takes the last tag back, Escape steps out of a field, ArrowLeft at the start walks into\n * the tags. Everything else — the arrows into the list, Escape closing — is the library's, reached\n * through its context. A key pressed mid-composition is the IME's.\n */\nconst FilterTextInput = ({\n inputRef,\n text,\n placeholder,\n label,\n onOpen,\n onEnter,\n onBackspace,\n onEscape,\n onArrowLeft,\n}: FilterTextInputProps): ReactElement => {\n const { keyHandler, open } = useCombobox();\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {\n if (isComposing(event)) {\n return;\n }\n\n if (event.key === 'Enter') {\n event.preventDefault();\n onEnter(open);\n return;\n }\n\n if (event.key === 'Backspace' && text.length === 0 && !event.repeat) {\n onBackspace();\n return;\n }\n\n if (event.key === 'Escape' && onEscape()) {\n event.preventDefault();\n return;\n }\n\n const atStart =\n event.currentTarget.selectionStart === 0 && event.currentTarget.selectionEnd === 0;\n if (event.key === 'ArrowLeft' && atStart && onArrowLeft()) {\n event.preventDefault();\n return;\n }\n\n keyHandler(event);\n };\n\n return (\n <Combobox.Input\n ref={inputRef}\n // The library's input names itself `Search` in English; the caller's own prompt is the name here.\n aria-label={label}\n placeholder={placeholder}\n className=\"bg-transparent\"\n onClick={onOpen}\n onKeyDown={handleKeyDown}\n />\n );\n};\nFilterTextInput.displayName = 'FilterInput.TextInput';\n"],"mappings":"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"}
@@ -1,10 +1,5 @@
1
1
  //#region src/filter/filter-input.phrases.d.ts
2
- /**
3
- * The input's own English, keyed `enonic.uiKit.filterInput.<name>`. An application translates it
4
- * through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no text for renders
5
- * this English. `comparePhrases` from `@enonic/ui-utils` tells an application which keys its
6
- * bundle lacks.
7
- */
2
+ /** The input's own English, keyed `enonic.uiKit.filterInput.<name>`; see `uiKitPhrases`. */
8
3
  declare const filterInputPhrases: {
9
4
  readonly 'enonic.uiKit.filterInput.label': 'Filter';
10
5
  readonly 'enonic.uiKit.filterInput.placeholder': 'Search';
@@ -1,10 +1,5 @@
1
1
  //#region src/filter/filter-input.phrases.ts
2
- /**
3
- * The input's own English, keyed `enonic.uiKit.filterInput.<name>`. An application translates it
4
- * through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no text for renders
5
- * this English. `comparePhrases` from `@enonic/ui-utils` tells an application which keys its
6
- * bundle lacks.
7
- */
2
+ /** The input's own English, keyed `enonic.uiKit.filterInput.<name>`; see `uiKitPhrases`. */
8
3
  const filterInputPhrases = {
9
4
  "enonic.uiKit.filterInput.label": "Filter",
10
5
  "enonic.uiKit.filterInput.placeholder": "Search",
@@ -1 +1 @@
1
- {"version":3,"file":"filter-input.phrases.js","names":[],"sources":["../../src/filter/filter-input.phrases.ts"],"sourcesContent":["/**\n * The input's own English, keyed `enonic.uiKit.filterInput.<name>`. An application translates it\n * through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no text for renders\n * this English. `comparePhrases` from `@enonic/ui-utils` tells an application which keys its\n * bundle lacks.\n */\nexport const filterInputPhrases = {\n 'enonic.uiKit.filterInput.label': 'Filter',\n 'enonic.uiKit.filterInput.placeholder': 'Search',\n 'enonic.uiKit.filterInput.valuePlaceholder': 'Pick a value',\n 'enonic.uiKit.filterInput.clear': 'Clear the filter',\n 'enonic.uiKit.filterInput.cleared': 'Filter cleared',\n 'enonic.uiKit.filterInput.noMatches': 'No matches',\n 'enonic.uiKit.filterInput.loading': 'Loading…',\n 'enonic.uiKit.filterInput.fieldPrefix': '{0}:',\n 'enonic.uiKit.filterInput.term': '{0}: {1}',\n 'enonic.uiKit.filterInput.remove': 'Remove {0}',\n 'enonic.uiKit.filterInput.added': 'Added {0}',\n 'enonic.uiKit.filterInput.removed': 'Removed {0}',\n 'enonic.uiKit.filterInput.replaced': 'Replaced {0} with {1}',\n} as const;\n\nexport type FilterInputPhraseKey = keyof typeof filterInputPhrases;\n"],"mappings":";;;;;;;AAMA,MAAa,qBAAqB;CAChC,kCAAkC;CAClC,wCAAwC;CACxC,6CAA6C;CAC7C,kCAAkC;CAClC,oCAAoC;CACpC,sCAAsC;CACtC,oCAAoC;CACpC,wCAAwC;CACxC,iCAAiC;CACjC,mCAAmC;CACnC,kCAAkC;CAClC,oCAAoC;CACpC,qCAAqC;AACvC"}
1
+ {"version":3,"file":"filter-input.phrases.js","names":[],"sources":["../../src/filter/filter-input.phrases.ts"],"sourcesContent":["/** The input's own English, keyed `enonic.uiKit.filterInput.<name>`; see `uiKitPhrases`. */\nexport const filterInputPhrases = {\n 'enonic.uiKit.filterInput.label': 'Filter',\n 'enonic.uiKit.filterInput.placeholder': 'Search',\n 'enonic.uiKit.filterInput.valuePlaceholder': 'Pick a value',\n 'enonic.uiKit.filterInput.clear': 'Clear the filter',\n 'enonic.uiKit.filterInput.cleared': 'Filter cleared',\n 'enonic.uiKit.filterInput.noMatches': 'No matches',\n 'enonic.uiKit.filterInput.loading': 'Loading…',\n 'enonic.uiKit.filterInput.fieldPrefix': '{0}:',\n 'enonic.uiKit.filterInput.term': '{0}: {1}',\n 'enonic.uiKit.filterInput.remove': 'Remove {0}',\n 'enonic.uiKit.filterInput.added': 'Added {0}',\n 'enonic.uiKit.filterInput.removed': 'Removed {0}',\n 'enonic.uiKit.filterInput.replaced': 'Replaced {0} with {1}',\n} as const;\n\nexport type FilterInputPhraseKey = keyof typeof filterInputPhrases;\n"],"mappings":";;AACA,MAAa,qBAAqB;CAChC,kCAAkC;CAClC,wCAAwC;CACxC,6CAA6C;CAC7C,kCAAkC;CAClC,oCAAoC;CACpC,sCAAsC;CACtC,oCAAoC;CACpC,wCAAwC;CACxC,iCAAiC;CACjC,mCAAmC;CACnC,kCAAkC;CAClC,oCAAoC;CACpC,qCAAqC;AACvC"}
@@ -1,5 +1,34 @@
1
1
  //#region src/i18n/phrases.d.ts
2
+ /**
3
+ * Every text the package renders, in English, keyed `enonic.uiKit.<area>.<name>`. An application
4
+ * translates them through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no
5
+ * text for renders this English. `comparePhrases` from `@enonic/ui-utils` tells an application
6
+ * which of these its bundle lacks.
7
+ */
2
8
  declare const uiKitPhrases: Readonly<{
9
+ readonly 'enonic.uiKit.filterInput.label': 'Filter';
10
+ readonly 'enonic.uiKit.filterInput.placeholder': 'Search';
11
+ readonly 'enonic.uiKit.filterInput.valuePlaceholder': 'Pick a value';
12
+ readonly 'enonic.uiKit.filterInput.clear': 'Clear the filter';
13
+ readonly 'enonic.uiKit.filterInput.cleared': 'Filter cleared';
14
+ readonly 'enonic.uiKit.filterInput.noMatches': 'No matches';
15
+ readonly 'enonic.uiKit.filterInput.loading': 'Loading…';
16
+ readonly 'enonic.uiKit.filterInput.fieldPrefix': '{0}:';
17
+ readonly 'enonic.uiKit.filterInput.term': '{0}: {1}';
18
+ readonly 'enonic.uiKit.filterInput.remove': 'Remove {0}';
19
+ readonly 'enonic.uiKit.filterInput.added': 'Added {0}';
20
+ readonly 'enonic.uiKit.filterInput.removed': 'Removed {0}';
21
+ readonly 'enonic.uiKit.filterInput.replaced': 'Replaced {0} with {1}';
22
+ } & {
23
+ readonly 'enonic.uiKit.notifications.label': 'Notifications';
24
+ } & {
25
+ readonly 'enonic.uiKit.themeSwitcher.light': 'Light theme';
26
+ readonly 'enonic.uiKit.themeSwitcher.dark': 'Dark theme';
27
+ readonly 'enonic.uiKit.themeSwitcher.system': 'System preference';
28
+ readonly 'enonic.uiKit.themeSwitcher.switchToDark': 'Switch to dark theme';
29
+ readonly 'enonic.uiKit.themeSwitcher.switchToSystem': 'Use system theme';
30
+ readonly 'enonic.uiKit.themeSwitcher.switchToLight': 'Switch to light theme';
31
+ } & {
3
32
  readonly 'enonic.uiKit.dialog.cancel': 'Cancel';
4
33
  readonly 'enonic.uiKit.dialog.confirm': 'Confirm';
5
34
  readonly 'enonic.uiKit.dialog.delete': 'Delete';
@@ -1,11 +1,8 @@
1
+ import { filterInputPhrases } from "../filter/filter-input.phrases.js";
2
+ import { notificationListPhrases } from "../notifications/notification-list.phrases.js";
3
+ import { themeSwitcherPhrases } from "../theme/theme-switcher.phrases.js";
1
4
  import { mergePhrases } from "@enonic/ui-utils";
2
5
  //#region src/i18n/phrases.ts
3
- /**
4
- * Every text the package renders, in English, keyed `enonic.uiKit.<area>.<name>`. An application
5
- * translates them through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no
6
- * text for renders this English. `comparePhrases` from `@enonic/ui-utils` tells an application
7
- * which of these its bundle lacks.
8
- */
9
6
  const dialogPhrases = {
10
7
  "enonic.uiKit.dialog.cancel": "Cancel",
11
8
  "enonic.uiKit.dialog.confirm": "Confirm",
@@ -16,7 +13,19 @@ const gatePhrases = {
16
13
  "enonic.uiKit.gate.hint": "Type {0} to confirm",
17
14
  "enonic.uiKit.gate.mismatch": "{1} does not match {0}"
18
15
  };
19
- const uiKitPhrases = mergePhrases([dialogPhrases, gatePhrases]);
16
+ /**
17
+ * Every text the package renders, in English, keyed `enonic.uiKit.<area>.<name>`. An application
18
+ * translates them through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no
19
+ * text for renders this English. `comparePhrases` from `@enonic/ui-utils` tells an application
20
+ * which of these its bundle lacks.
21
+ */
22
+ const uiKitPhrases = mergePhrases([
23
+ dialogPhrases,
24
+ gatePhrases,
25
+ filterInputPhrases,
26
+ themeSwitcherPhrases,
27
+ notificationListPhrases
28
+ ]);
20
29
  //#endregion
21
30
  export { dialogPhrases, gatePhrases, uiKitPhrases };
22
31
 
@@ -1 +1 @@
1
- {"version":3,"file":"phrases.js","names":[],"sources":["../../src/i18n/phrases.ts"],"sourcesContent":["import { mergePhrases } from '@enonic/ui-utils';\n\n/**\n * Every text the package renders, in English, keyed `enonic.uiKit.<area>.<name>`. An application\n * translates them through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no\n * text for renders this English. `comparePhrases` from `@enonic/ui-utils` tells an application\n * which of these its bundle lacks.\n */\nexport const dialogPhrases = {\n 'enonic.uiKit.dialog.cancel': 'Cancel',\n 'enonic.uiKit.dialog.confirm': 'Confirm',\n 'enonic.uiKit.dialog.delete': 'Delete',\n 'enonic.uiKit.dialog.keep': 'Keep editing',\n} as const;\n\nexport const gatePhrases = {\n 'enonic.uiKit.gate.hint': 'Type {0} to confirm',\n 'enonic.uiKit.gate.mismatch': '{1} does not match {0}',\n} as const;\n\nexport const uiKitPhrases = mergePhrases([dialogPhrases, gatePhrases]);\n\nexport type UiKitPhraseKey = keyof typeof uiKitPhrases;\n"],"mappings":";;;;;;;;AAQA,MAAa,gBAAgB;CAC3B,8BAA8B;CAC9B,+BAA+B;CAC/B,8BAA8B;CAC9B,4BAA4B;AAC9B;AAEA,MAAa,cAAc;CACzB,0BAA0B;CAC1B,8BAA8B;AAChC;AAEA,MAAa,eAAe,aAAa,CAAC,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"phrases.js","names":[],"sources":["../../src/i18n/phrases.ts"],"sourcesContent":["import { mergePhrases } from '@enonic/ui-utils';\n\nimport { filterInputPhrases } from '../filter/filter-input.phrases';\nimport { notificationListPhrases } from '../notifications/notification-list.phrases';\nimport { themeSwitcherPhrases } from '../theme/theme-switcher.phrases';\n\nexport const dialogPhrases = {\n 'enonic.uiKit.dialog.cancel': 'Cancel',\n 'enonic.uiKit.dialog.confirm': 'Confirm',\n 'enonic.uiKit.dialog.delete': 'Delete',\n 'enonic.uiKit.dialog.keep': 'Keep editing',\n} as const;\n\nexport const gatePhrases = {\n 'enonic.uiKit.gate.hint': 'Type {0} to confirm',\n 'enonic.uiKit.gate.mismatch': '{1} does not match {0}',\n} as const;\n\n/**\n * Every text the package renders, in English, keyed `enonic.uiKit.<area>.<name>`. An application\n * translates them through the `Translate` it hands `@enonic/ui`'s `I18nProvider`; a key it has no\n * text for renders this English. `comparePhrases` from `@enonic/ui-utils` tells an application\n * which of these its bundle lacks.\n */\nexport const uiKitPhrases = mergePhrases([\n dialogPhrases,\n gatePhrases,\n filterInputPhrases,\n themeSwitcherPhrases,\n notificationListPhrases,\n]);\n\nexport type UiKitPhraseKey = keyof typeof uiKitPhrases;\n"],"mappings":";;;;;AAMA,MAAa,gBAAgB;CAC3B,8BAA8B;CAC9B,+BAA+B;CAC/B,8BAA8B;CAC9B,4BAA4B;AAC9B;AAEA,MAAa,cAAc;CACzB,0BAA0B;CAC1B,8BAA8B;AAChC;;;;;;;AAQA,MAAa,eAAe,aAAa;CACvC;CACA;CACA;CACA;CACA;AACF,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ActionDialogContextValue, ActionDialogOpenChangeDetails, useActionDialog } from "./dialog/action-dialog-context.js";
1
+ import { ActionDialogCloseReason, ActionDialogContextValue, ActionDialogOpenChangeDetails, useActionDialog } from "./dialog/action-dialog-context.js";
2
2
  import { ActionDialog, ActionDialogActionProps, ActionDialogBodyProps, ActionDialogContentProps, ActionDialogFooterProps, ActionDialogRootProps, DialogIntent, DialogSize, FooterQuestion } from "./dialog/action-dialog.js";
3
3
  import { DeleteTarget, deleteExpectation } from "./dialog/delete-expectation.js";
4
4
  import { DeleteConfirm, DeleteConfirmProps } from "./dialog/delete-confirm.js";
@@ -13,4 +13,9 @@ import { FilterInputPhraseKey, filterInputPhrases } from "./filter/filter-input.
13
13
  import { fillPhrase } from "./i18n/fill-phrase.js";
14
14
  import { UiKitPhraseKey, uiKitPhrases } from "./i18n/phrases.js";
15
15
  import { UiKitTranslate, useUiKitPhrases } from "./i18n/use-phrases.js";
16
- export { ActionDialog, type ActionDialogActionProps, type ActionDialogBodyProps, type ActionDialogContentProps, type ActionDialogContextValue, type ActionDialogFooterProps, type ActionDialogOpenChangeDetails, type ActionDialogRootProps, type CloseGuard, type CloseGuardOptions, DeleteConfirm, type DeleteConfirmProps, type DeleteTarget, type DialogIntent, DialogPreset, type DialogPresetConfirmProps, type DialogSize, EMPTY_FILTER, type FieldTerm, type FilterField, FilterInput, type FilterInputPhraseKey, type FilterInputProps, type FilterQuery, type FilterTerm, type FilterValue, type FooterQuestion, Gate, type GateHintProps, type GateInputProps, type TextTerm, type UiKitPhraseKey, type UiKitTranslate, deleteExpectation, fieldTerm, fillPhrase, filterInputPhrases, isFieldTerm, isTextTerm, matchesExpected, sameTerm, termKey, textOf, textTerm, toggledTerm, uiKitPhrases, useActionDialog, useCloseGuard, useUiKitPhrases, valuesOf, withTerm, withoutTerm };
16
+ import { NotificationList, NotificationListProps } from "./notifications/notification-list.js";
17
+ import { NotificationListPhraseKey, notificationListPhrases } from "./notifications/notification-list.phrases.js";
18
+ import { ThemeSwitcher, ThemeSwitcherProps } from "./theme/theme-switcher.js";
19
+ import { ThemeSwitcherPhraseKey, themeSwitcherPhrases } from "./theme/theme-switcher.phrases.js";
20
+ import { useResolvedTheme } from "./theme/use-resolved-theme.js";
21
+ export { ActionDialog, type ActionDialogActionProps, type ActionDialogBodyProps, type ActionDialogCloseReason, type ActionDialogContentProps, type ActionDialogContextValue, type ActionDialogFooterProps, type ActionDialogOpenChangeDetails, type ActionDialogRootProps, type CloseGuard, type CloseGuardOptions, DeleteConfirm, type DeleteConfirmProps, type DeleteTarget, type DialogIntent, DialogPreset, type DialogPresetConfirmProps, type DialogSize, EMPTY_FILTER, type FieldTerm, type FilterField, FilterInput, type FilterInputPhraseKey, type FilterInputProps, type FilterQuery, type FilterTerm, type FilterValue, type FooterQuestion, Gate, type GateHintProps, type GateInputProps, NotificationList, type NotificationListPhraseKey, type NotificationListProps, type TextTerm, ThemeSwitcher, type ThemeSwitcherPhraseKey, type ThemeSwitcherProps, type UiKitPhraseKey, type UiKitTranslate, deleteExpectation, fieldTerm, fillPhrase, filterInputPhrases, isFieldTerm, isTextTerm, matchesExpected, notificationListPhrases, sameTerm, termKey, textOf, textTerm, themeSwitcherPhrases, toggledTerm, uiKitPhrases, useActionDialog, useCloseGuard, useResolvedTheme, useUiKitPhrases, valuesOf, withTerm, withoutTerm };
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { useActionDialog } from "./dialog/action-dialog-context.js";
2
+ import { filterInputPhrases } from "./filter/filter-input.phrases.js";
3
+ import { notificationListPhrases } from "./notifications/notification-list.phrases.js";
4
+ import { themeSwitcherPhrases } from "./theme/theme-switcher.phrases.js";
2
5
  import { uiKitPhrases } from "./i18n/phrases.js";
3
6
  import { useUiKitPhrases } from "./i18n/use-phrases.js";
4
7
  import { ActionDialog } from "./dialog/action-dialog.js";
@@ -10,6 +13,8 @@ import { DeleteConfirm } from "./dialog/delete-confirm.js";
10
13
  import { DialogPreset } from "./dialog/dialog-preset.js";
11
14
  import { useCloseGuard } from "./dialog/use-close-guard.js";
12
15
  import { EMPTY_FILTER, fieldTerm, isFieldTerm, isTextTerm, sameTerm, termKey, textOf, textTerm, toggledTerm, valuesOf, withTerm, withoutTerm } from "./filter/filter-query.js";
13
- import { filterInputPhrases } from "./filter/filter-input.phrases.js";
14
16
  import { FilterInput } from "./filter/filter-input.js";
15
- export { ActionDialog, DeleteConfirm, DialogPreset, EMPTY_FILTER, FilterInput, Gate, deleteExpectation, fieldTerm, fillPhrase, filterInputPhrases, isFieldTerm, isTextTerm, matchesExpected, sameTerm, termKey, textOf, textTerm, toggledTerm, uiKitPhrases, useActionDialog, useCloseGuard, useUiKitPhrases, valuesOf, withTerm, withoutTerm };
17
+ import { NotificationList } from "./notifications/notification-list.js";
18
+ import { ThemeSwitcher } from "./theme/theme-switcher.js";
19
+ import { useResolvedTheme } from "./theme/use-resolved-theme.js";
20
+ export { ActionDialog, DeleteConfirm, DialogPreset, EMPTY_FILTER, FilterInput, Gate, NotificationList, ThemeSwitcher, deleteExpectation, fieldTerm, fillPhrase, filterInputPhrases, isFieldTerm, isTextTerm, matchesExpected, notificationListPhrases, sameTerm, termKey, textOf, textTerm, themeSwitcherPhrases, toggledTerm, uiKitPhrases, useActionDialog, useCloseGuard, useResolvedTheme, useUiKitPhrases, valuesOf, withTerm, withoutTerm };
@@ -0,0 +1,18 @@
1
+ import { ReactElement } from "react";
2
+ import { Notifications } from "@enonic/ui-utils";
3
+ //#region src/notifications/notification-list.d.ts
4
+ type NotificationListProps = {
5
+ /** The store the application created with `createNotifications`. */
6
+ notifications: Pick<Notifications, 'notifications' | 'dismiss' | 'pause' | 'resume'>;
7
+ /** Where the stack sits: the application places it, since where the corner is depends on the shell. */
8
+ className?: string;
9
+ 'data-component'?: string;
10
+ };
11
+ declare const NOTIFICATION_LIST_NAME = "NotificationList";
12
+ declare function NotificationList({ notifications: store, className, 'data-component': componentName }: NotificationListProps): ReactElement;
13
+ declare namespace NotificationList {
14
+ export { NOTIFICATION_LIST_NAME as displayName };
15
+ }
16
+ //#endregion
17
+ export { NotificationList, NotificationListProps };
18
+ //# sourceMappingURL=notification-list.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { notificationListPhrases } from "./notification-list.phrases.js";
2
+ import { hasFocus, useFocusHandOff } from "./use-focus-hand-off.js";
3
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { Toast, cn, usePhrases } from "@enonic/ui";
6
+ import { isUrgent } from "@enonic/ui-utils";
7
+ //#region src/notifications/notification-list.tsx
8
+ const NOTIFICATION_LIST_NAME = "NotificationList";
9
+ /**
10
+ * The toasts on screen, one `Toast` each with its actions and a close, and two `aria-live` regions
11
+ * that announce them. Hover and focus pause a toast's lifetime, so an action is never pulled out
12
+ * from under a pointer or a keyboard user halfway to it; an action taken dismisses the toast. A
13
+ * toast closed from the keyboard hands its focus to the toast next to it, or back to where the
14
+ * focus came from when the stack was entered.
15
+ */
16
+ const NotificationList = ({ notifications: store, className, "data-component": componentName = NOTIFICATION_LIST_NAME }) => {
17
+ const readable = store.notifications;
18
+ const subscribe = useCallback((onChange) => readable.listen(onChange), [readable]);
19
+ const read = useCallback(() => readable.get(), [readable]);
20
+ const notifications = useSyncExternalStore(subscribe, read);
21
+ const t = usePhrases(notificationListPhrases);
22
+ const [mounted, setMounted] = useState(false);
23
+ useEffect(() => {
24
+ setMounted(true);
25
+ }, []);
26
+ const announced = mounted ? notifications : [];
27
+ const wrappers = useRef(/* @__PURE__ */ new Map());
28
+ const { stackProps, handOff } = useFocusHandOff();
29
+ const dismissFrom = (id) => {
30
+ handOff(wrappers.current.get(id));
31
+ store.dismiss(id);
32
+ };
33
+ return /* @__PURE__ */ jsxs("div", {
34
+ "data-component": componentName,
35
+ role: "region",
36
+ "aria-label": t("enonic.uiKit.notifications.label"),
37
+ className: cn("pointer-events-none", className),
38
+ children: [
39
+ /* @__PURE__ */ jsx("div", {
40
+ className: "sr-only",
41
+ "aria-live": "polite",
42
+ children: announced.filter(({ tone }) => !isUrgent(tone)).map(({ id, message }) => /* @__PURE__ */ jsx("p", { children: message }, id))
43
+ }),
44
+ /* @__PURE__ */ jsx("div", {
45
+ className: "sr-only",
46
+ "aria-live": "assertive",
47
+ children: announced.filter(({ tone }) => isUrgent(tone)).map(({ id, message }) => /* @__PURE__ */ jsx("p", { children: message }, id))
48
+ }),
49
+ /* @__PURE__ */ jsx("div", {
50
+ ...stackProps,
51
+ className: "flex flex-col items-center gap-2.5",
52
+ children: notifications.map(({ id, tone, message, actions }) => /* @__PURE__ */ jsx("div", {
53
+ ref: (element) => {
54
+ if (element == null) wrappers.current.delete(id);
55
+ else wrappers.current.set(id, element);
56
+ },
57
+ className: "pointer-events-auto w-full",
58
+ onMouseEnter: () => store.pause(id),
59
+ onMouseLeave: ({ currentTarget }) => !hasFocus(currentTarget) && store.resume(id),
60
+ onFocusCapture: () => store.pause(id),
61
+ onBlurCapture: ({ currentTarget }) => !currentTarget.matches(":hover") && store.resume(id),
62
+ children: /* @__PURE__ */ jsxs(Toast, {
63
+ open: true,
64
+ withClose: true,
65
+ role: "status",
66
+ "aria-live": "off",
67
+ onOpenChange: (open) => !open && dismissFrom(id),
68
+ children: [
69
+ /* @__PURE__ */ jsx(Toast.Icon, { tone }),
70
+ /* @__PURE__ */ jsx(Toast.Description, { children: message }),
71
+ actions.map(({ label, onClick }, index) => /* @__PURE__ */ jsx(Toast.Button, {
72
+ onClick: () => {
73
+ try {
74
+ onClick();
75
+ } finally {
76
+ dismissFrom(id);
77
+ }
78
+ },
79
+ children: label
80
+ }, `${index}-${label}`))
81
+ ]
82
+ })
83
+ }, id))
84
+ })
85
+ ]
86
+ });
87
+ };
88
+ NotificationList.displayName = NOTIFICATION_LIST_NAME;
89
+ //#endregion
90
+ export { NotificationList };
91
+
92
+ //# sourceMappingURL=notification-list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-list.js","names":[],"sources":["../../src/notifications/notification-list.tsx"],"sourcesContent":["import { cn, Toast, usePhrases } from '@enonic/ui';\nimport { isUrgent, type Notifications } from '@enonic/ui-utils';\nimport {\n type ReactElement,\n useCallback,\n useEffect,\n useRef,\n useState,\n useSyncExternalStore,\n} from 'react';\n\nimport { notificationListPhrases } from './notification-list.phrases';\nimport { hasFocus, useFocusHandOff } from './use-focus-hand-off';\n\nexport type NotificationListProps = {\n /** The store the application created with `createNotifications`. */\n notifications: Pick<Notifications, 'notifications' | 'dismiss' | 'pause' | 'resume'>;\n /** Where the stack sits: the application places it, since where the corner is depends on the shell. */\n className?: string;\n 'data-component'?: string;\n};\n\nconst NOTIFICATION_LIST_NAME = 'NotificationList';\n\n/**\n * The toasts on screen, one `Toast` each with its actions and a close, and two `aria-live` regions\n * that announce them. Hover and focus pause a toast's lifetime, so an action is never pulled out\n * from under a pointer or a keyboard user halfway to it; an action taken dismisses the toast. A\n * toast closed from the keyboard hands its focus to the toast next to it, or back to where the\n * focus came from when the stack was entered.\n */\nconst NotificationList = ({\n notifications: store,\n className,\n 'data-component': componentName = NOTIFICATION_LIST_NAME,\n}: NotificationListProps): ReactElement => {\n const readable = store.notifications;\n const subscribe = useCallback((onChange: () => void) => readable.listen(onChange), [readable]);\n const read = useCallback(() => readable.get(), [readable]);\n const notifications = useSyncExternalStore(subscribe, read);\n const t = usePhrases(notificationListPhrases);\n\n // ! Announced reliably only if the region predates its content: the regions stay mounted, the\n // ! toasts themselves are silenced with `aria-live=\"off\"`, and what the store held before the\n // ! list mounted enters the regions one render after they appeared.\n const [mounted, setMounted] = useState(false);\n useEffect(() => {\n setMounted(true);\n }, []);\n const announced = mounted ? notifications : [];\n\n const wrappers = useRef(new Map<number, HTMLDivElement>());\n const { stackProps, handOff } = useFocusHandOff();\n\n const dismissFrom = (id: number): void => {\n handOff(wrappers.current.get(id));\n store.dismiss(id);\n };\n\n return (\n <div\n data-component={componentName}\n role=\"region\"\n aria-label={t('enonic.uiKit.notifications.label')}\n className={cn('pointer-events-none', className)}\n >\n <div className=\"sr-only\" aria-live=\"polite\">\n {announced\n .filter(({ tone }) => !isUrgent(tone))\n .map(({ id, message }) => (\n <p key={id}>{message}</p>\n ))}\n </div>\n <div className=\"sr-only\" aria-live=\"assertive\">\n {announced\n .filter(({ tone }) => isUrgent(tone))\n .map(({ id, message }) => (\n <p key={id}>{message}</p>\n ))}\n </div>\n\n <div {...stackProps} className=\"flex flex-col items-center gap-2.5\">\n {notifications.map(({ id, tone, message, actions }) => (\n <div\n key={id}\n ref={(element) => {\n if (element == null) {\n wrappers.current.delete(id);\n } else {\n wrappers.current.set(id, element);\n }\n }}\n className=\"pointer-events-auto w-full\"\n onMouseEnter={() => store.pause(id)}\n onMouseLeave={({ currentTarget }) => !hasFocus(currentTarget) && store.resume(id)}\n onFocusCapture={() => store.pause(id)}\n onBlurCapture={({ currentTarget }) =>\n !currentTarget.matches(':hover') && store.resume(id)\n }\n >\n <Toast\n open\n withClose\n role=\"status\"\n aria-live=\"off\"\n onOpenChange={(open) => !open && dismissFrom(id)}\n >\n <Toast.Icon tone={tone} />\n <Toast.Description>{message}</Toast.Description>\n {actions.map(({ label, onClick }, index) => (\n <Toast.Button\n key={`${index}-${label}`}\n onClick={() => {\n try {\n onClick();\n } finally {\n dismissFrom(id);\n }\n }}\n >\n {label}\n </Toast.Button>\n ))}\n </Toast>\n </div>\n ))}\n </div>\n </div>\n );\n};\n\nNotificationList.displayName = NOTIFICATION_LIST_NAME;\n\nexport { NotificationList };\n"],"mappings":";;;;;;;AAsBA,MAAM,yBAAyB;;;;;;;;AAS/B,MAAM,oBAAoB,EACxB,eAAe,OACf,WACA,kBAAkB,gBAAgB,6BACO;CACzC,MAAM,WAAW,MAAM;CACvB,MAAM,YAAY,aAAa,aAAyB,SAAS,OAAO,QAAQ,GAAG,CAAC,QAAQ,CAAC;CAC7F,MAAM,OAAO,kBAAkB,SAAS,IAAI,GAAG,CAAC,QAAQ,CAAC;CACzD,MAAM,gBAAgB,qBAAqB,WAAW,IAAI;CAC1D,MAAM,IAAI,WAAW,uBAAuB;CAK5C,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAC5C,gBAAgB;EACd,WAAW,IAAI;CACjB,GAAG,CAAC,CAAC;CACL,MAAM,YAAY,UAAU,gBAAgB,CAAC;CAE7C,MAAM,WAAW,uBAAO,IAAI,IAA4B,CAAC;CACzD,MAAM,EAAE,YAAY,YAAY,gBAAgB;CAEhD,MAAM,eAAe,OAAqB;EACxC,QAAQ,SAAS,QAAQ,IAAI,EAAE,CAAC;EAChC,MAAM,QAAQ,EAAE;CAClB;CAEA,OACE,qBAAC,OAAD;EACE,kBAAgB;EAChB,MAAK;EACL,cAAY,EAAE,kCAAkC;EAChD,WAAW,GAAG,uBAAuB,SAAS;EAJhD,UAAA;GAME,oBAAC,OAAD;IAAK,WAAU;IAAU,aAAU;IAChC,UAAA,UACE,QAAQ,EAAE,WAAW,CAAC,SAAS,IAAI,CAAC,CAAC,CACrC,KAAK,EAAE,IAAI,cACV,oBAAC,KAAD,EAAA,UAAa,QAAW,GAAhB,EAAgB,CACzB;GACA,CAAA;GACL,oBAAC,OAAD;IAAK,WAAU;IAAU,aAAU;IAChC,UAAA,UACE,QAAQ,EAAE,WAAW,SAAS,IAAI,CAAC,CAAC,CACpC,KAAK,EAAE,IAAI,cACV,oBAAC,KAAD,EAAA,UAAa,QAAW,GAAhB,EAAgB,CACzB;GACA,CAAA;GAEL,oBAAC,OAAD;IAAK,GAAI;IAAY,WAAU;IAC5B,UAAA,cAAc,KAAK,EAAE,IAAI,MAAM,SAAS,cACvC,oBAAC,OAAD;KAEE,MAAM,YAAY;MAChB,IAAI,WAAW,MACb,SAAS,QAAQ,OAAO,EAAE;WAE1B,SAAS,QAAQ,IAAI,IAAI,OAAO;KAEpC;KACA,WAAU;KACV,oBAAoB,MAAM,MAAM,EAAE;KAClC,eAAe,EAAE,oBAAoB,CAAC,SAAS,aAAa,KAAK,MAAM,OAAO,EAAE;KAChF,sBAAsB,MAAM,MAAM,EAAE;KACpC,gBAAgB,EAAE,oBAChB,CAAC,cAAc,QAAQ,QAAQ,KAAK,MAAM,OAAO,EAAE;KAGrD,UAAA,qBAAC,OAAD;MACE,MAAA;MACA,WAAA;MACA,MAAK;MACL,aAAU;MACV,eAAe,SAAS,CAAC,QAAQ,YAAY,EAAE;MALjD,UAAA;OAOE,oBAAC,MAAM,MAAP,EAAkB,KAAO,CAAA;OACzB,oBAAC,MAAM,aAAP,EAAA,UAAoB,QAA2B,CAAA;OAC9C,QAAQ,KAAK,EAAE,OAAO,WAAW,UAChC,oBAAC,MAAM,QAAP;QAEE,eAAe;SACb,IAAI;UACF,QAAQ;SACV,UAAU;UACR,YAAY,EAAE;SAChB;QACF;QAEC,UAAA;OACW,GAVP,GAAG,MAAM,GAAG,OAUL,CACf;MACI;;IACJ,GAxCE,EAwCF,CACN;GACE,CAAA;EACF;;AAET;AAEA,iBAAiB,cAAc"}
@@ -0,0 +1,9 @@
1
+ //#region src/notifications/notification-list.phrases.d.ts
2
+ /** The stack's own English, keyed `enonic.uiKit.notifications.<name>`; see `uiKitPhrases`. */
3
+ declare const notificationListPhrases: {
4
+ readonly 'enonic.uiKit.notifications.label': 'Notifications';
5
+ };
6
+ type NotificationListPhraseKey = keyof typeof notificationListPhrases;
7
+ //#endregion
8
+ export { NotificationListPhraseKey, notificationListPhrases };
9
+ //# sourceMappingURL=notification-list.phrases.d.ts.map
@@ -0,0 +1,7 @@
1
+ //#region src/notifications/notification-list.phrases.ts
2
+ /** The stack's own English, keyed `enonic.uiKit.notifications.<name>`; see `uiKitPhrases`. */
3
+ const notificationListPhrases = { "enonic.uiKit.notifications.label": "Notifications" };
4
+ //#endregion
5
+ export { notificationListPhrases };
6
+
7
+ //# sourceMappingURL=notification-list.phrases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-list.phrases.js","names":[],"sources":["../../src/notifications/notification-list.phrases.ts"],"sourcesContent":["/** The stack's own English, keyed `enonic.uiKit.notifications.<name>`; see `uiKitPhrases`. */\nexport const notificationListPhrases = {\n 'enonic.uiKit.notifications.label': 'Notifications',\n} as const;\n\nexport type NotificationListPhraseKey = keyof typeof notificationListPhrases;\n"],"mappings":";;AACA,MAAa,0BAA0B,EACrC,oCAAoC,gBACtC"}