@inglorious/ui 9.9.0 → 9.10.1

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 (43) hide show
  1. package/package.json +29 -3
  2. package/src/controls/checkbox/template.js +3 -1
  3. package/src/controls/chip-picker/chip-picker.stories.js +22 -0
  4. package/src/controls/chip-picker/index.js +3 -0
  5. package/src/controls/chip-picker/style.css +26 -0
  6. package/src/controls/chip-picker/template.js +53 -0
  7. package/src/controls/combobox/handlers.js +83 -20
  8. package/src/controls/combobox/template.js +3 -1
  9. package/src/controls/controls.css +3 -2
  10. package/src/controls/radio-group/template.js +3 -1
  11. package/src/controls/rating/template.js +4 -1
  12. package/src/controls/select/template.js +6 -12
  13. package/src/controls/switch/template.js +3 -1
  14. package/src/data-display/badge/style.css +1 -0
  15. package/src/data-display/data-display.css +1 -0
  16. package/src/data-display/lightbox/index.js +3 -0
  17. package/src/data-display/lightbox/lightbox.stories.js +25 -0
  18. package/src/data-display/lightbox/style.css +97 -0
  19. package/src/data-display/lightbox/template.js +153 -0
  20. package/src/feedback/confirm-dialog/confirm-dialog.stories.js +33 -0
  21. package/src/feedback/confirm-dialog/index.js +3 -0
  22. package/src/feedback/confirm-dialog/style.css +16 -0
  23. package/src/feedback/confirm-dialog/template.js +70 -0
  24. package/src/feedback/crop-dialog/index.js +3 -0
  25. package/src/feedback/crop-dialog/style.css +43 -0
  26. package/src/feedback/crop-dialog/template.js +82 -0
  27. package/src/feedback/dialog/style.css +2 -1
  28. package/src/feedback/dialog/template.js +5 -2
  29. package/src/feedback/feedback.css +3 -0
  30. package/src/feedback/load-status/index.js +3 -0
  31. package/src/feedback/load-status/load-status.stories.js +16 -0
  32. package/src/feedback/load-status/style.css +10 -0
  33. package/src/feedback/load-status/template.js +43 -0
  34. package/src/index.js +5 -0
  35. package/src/shared/icons.js +12 -0
  36. package/src/shared/live-boolean.js +17 -0
  37. package/types/controls/chip-picker.d.ts +23 -0
  38. package/types/controls/combobox.d.ts +20 -2
  39. package/types/data-display/lightbox.d.ts +35 -0
  40. package/types/feedback/confirm-dialog.d.ts +34 -0
  41. package/types/feedback/crop-dialog.d.ts +35 -0
  42. package/types/feedback/load-status.d.ts +22 -0
  43. package/types/index.d.ts +17 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * @typedef {import('../../../types/data-display/lightbox').LightboxProps} LightboxProps
3
+ * @typedef {import('../../../types/data-display/lightbox').LightboxImage} LightboxImage
4
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
5
+ */
6
+
7
+ import { html, nothing } from "@inglorious/web"
8
+ import { ref } from "@inglorious/web/directives/ref"
9
+ import { when } from "@inglorious/web/directives/when"
10
+
11
+ import { IconButton } from "../../controls/icon-button/index.js"
12
+ import { chevronLeft, chevronRight, close } from "../../shared/icons.js"
13
+ import { syncDialog } from "../../shared/top-layer.js"
14
+ import { Icon } from "../icon/index.js"
15
+
16
+ const SWIPE_PX = 40
17
+ const NEXT = 1
18
+ const PREVIOUS = -1
19
+
20
+ /**
21
+ * Where each lightbox's current swipe started.
22
+ * @type {WeakMap<HTMLDialogElement, number>}
23
+ */
24
+ const swipeStarts = new WeakMap()
25
+
26
+ /**
27
+ * Renders images full screen in a modal `<dialog>`, one at a time. With more
28
+ * than one, the arrows, the arrow keys and a horizontal swipe step through
29
+ * them, following the writing direction. Esc, the close button and a press
30
+ * outside the image close it.
31
+ * @param {LightboxProps} props
32
+ * @returns {TemplateResult | typeof nothing}
33
+ */
34
+ export function render(props) {
35
+ const {
36
+ id,
37
+ images = [],
38
+ index = 0,
39
+ isOpen = false,
40
+ label = "Photos",
41
+ closeLabel = "Close",
42
+ previousLabel = "Previous photo",
43
+ nextLabel = "Next photo",
44
+ className = "",
45
+ onStep,
46
+ onClose,
47
+ } = props
48
+
49
+ const total = images.length
50
+ if (!total) return nothing
51
+
52
+ const current = ((index % total) + total) % total
53
+ const image = toImage(images[current])
54
+ const isNavigable = total > 1 && Boolean(onStep)
55
+
56
+ return html`<dialog
57
+ id=${id || nothing}
58
+ class=${["iw-lightbox", className].filter(Boolean).join(" ")}
59
+ aria-label=${label}
60
+ @click=${(event) => {
61
+ if (event.target === event.currentTarget) onClose?.()
62
+ }}
63
+ @cancel=${(event) => {
64
+ if (!onClose) event.preventDefault()
65
+ }}
66
+ @keydown=${(event) => {
67
+ if (!isNavigable) return
68
+ const step = arrowStep(event)
69
+ if (!step) return
70
+ event.preventDefault()
71
+ onStep(step)
72
+ }}
73
+ @pointerdown=${(event) => {
74
+ swipeStarts.set(event.currentTarget, event.clientX)
75
+ }}
76
+ @pointerup=${(event) => {
77
+ const startX = swipeStarts.get(event.currentTarget)
78
+ swipeStarts.delete(event.currentTarget)
79
+ if (!isNavigable || startX == null) return
80
+ const distance = event.clientX - startX
81
+ if (Math.abs(distance) < SWIPE_PX) return
82
+ const towardsEnd = isRtl(event.currentTarget)
83
+ ? distance > 0
84
+ : distance < 0
85
+ onStep(towardsEnd ? NEXT : PREVIOUS)
86
+ }}
87
+ @pointercancel=${(event) => swipeStarts.delete(event.currentTarget)}
88
+ ${ref((element) => syncDialog(element, { isOpen, onClose }))}
89
+ >
90
+ ${when(onClose, () =>
91
+ IconButton.render({
92
+ icon: Icon.render({ icon: close, size: "lg" }),
93
+ variant: "ghost",
94
+ shape: "round",
95
+ ariaLabel: closeLabel,
96
+ className: "iw-lightbox-close",
97
+ onClick: onClose,
98
+ }),
99
+ )}
100
+ <div class="iw-lightbox-stage">
101
+ <img
102
+ class="iw-lightbox-image"
103
+ src=${image.src}
104
+ alt=${image.alt ?? ""}
105
+ draggable="false"
106
+ />
107
+ ${when(
108
+ isNavigable,
109
+ () => html`
110
+ ${IconButton.render({
111
+ icon: Icon.render({ icon: chevronLeft, size: "lg" }),
112
+ variant: "ghost",
113
+ shape: "round",
114
+ ariaLabel: previousLabel,
115
+ className: "iw-lightbox-nav iw-lightbox-previous",
116
+ onClick: () => onStep(PREVIOUS),
117
+ })}
118
+ ${IconButton.render({
119
+ icon: Icon.render({ icon: chevronRight, size: "lg" }),
120
+ variant: "ghost",
121
+ shape: "round",
122
+ ariaLabel: nextLabel,
123
+ className: "iw-lightbox-nav iw-lightbox-next",
124
+ onClick: () => onStep(NEXT),
125
+ })}
126
+ <div class="iw-lightbox-counter" aria-hidden="true">
127
+ ${current + 1}/${total}
128
+ </div>
129
+ `,
130
+ )}
131
+ </div>
132
+ </dialog>`
133
+ }
134
+
135
+ /**
136
+ * @param {string | LightboxImage} image
137
+ * @returns {LightboxImage}
138
+ */
139
+ function toImage(image) {
140
+ return typeof image === "string" ? { src: image } : image
141
+ }
142
+
143
+ function arrowStep(event) {
144
+ const towardsEnd = isRtl(event.currentTarget) ? "ArrowLeft" : "ArrowRight"
145
+ const towardsStart = isRtl(event.currentTarget) ? "ArrowRight" : "ArrowLeft"
146
+ if (event.key === towardsEnd) return NEXT
147
+ if (event.key === towardsStart) return PREVIOUS
148
+ return 0
149
+ }
150
+
151
+ function isRtl(element) {
152
+ return getComputedStyle(element).direction === "rtl"
153
+ }
@@ -0,0 +1,33 @@
1
+ import { createRender } from "../../stories/notify-story.js"
2
+ import { ConfirmDialog } from "."
3
+
4
+ export default {
5
+ title: "Feedback/ConfirmDialog",
6
+ tags: ["autodocs"],
7
+ render: createRender(ConfirmDialog),
8
+ argTypes: {
9
+ tone: { control: "select", options: ["danger", "primary"] },
10
+ isBusy: { control: "boolean" },
11
+ onConfirm: { action: "onConfirm" },
12
+ onCancel: { action: "onCancel" },
13
+ },
14
+ }
15
+
16
+ export const Danger = {
17
+ args: {
18
+ isOpen: true,
19
+ message: "Delete this event? This cannot be undone.",
20
+ confirmLabel: "Delete",
21
+ busyLabel: "Deleting…",
22
+ cancelLabel: "Keep it",
23
+ },
24
+ }
25
+
26
+ export const Primary = {
27
+ args: {
28
+ isOpen: true,
29
+ tone: "primary",
30
+ message: "Publish this event now?",
31
+ confirmLabel: "Publish",
32
+ },
33
+ }
@@ -0,0 +1,3 @@
1
+ import * as renderers from "./template.js"
2
+
3
+ export const ConfirmDialog = { ...renderers }
@@ -0,0 +1,16 @@
1
+ .iw-confirm-dialog-actions {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--iw-space-2);
5
+ width: 100%;
6
+
7
+ > * {
8
+ flex: 1;
9
+ }
10
+ }
11
+
12
+ @media (min-width: 48rem) {
13
+ .iw-confirm-dialog-actions {
14
+ flex-direction: row;
15
+ }
16
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @typedef {import('../../../types/feedback/confirm-dialog').ConfirmDialogProps} ConfirmDialogProps
3
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
4
+ */
5
+
6
+ import { html } from "@inglorious/web"
7
+
8
+ import { Button } from "../../controls/button/index.js"
9
+ import { Dialog } from "../dialog/index.js"
10
+
11
+ /**
12
+ * Renders a sheet asking to confirm an action, with the confirm button first.
13
+ * While `isBusy` it cannot be dismissed, and the confirm button shows
14
+ * `busyLabel`.
15
+ * @param {ConfirmDialogProps} props
16
+ * @returns {TemplateResult}
17
+ */
18
+ export function render(props) {
19
+ const {
20
+ id,
21
+ isOpen = false,
22
+ message,
23
+ children,
24
+ confirmLabel,
25
+ busyLabel = confirmLabel,
26
+ cancelLabel = "Cancel",
27
+ closeLabel,
28
+ tone = "danger",
29
+ isBusy = false,
30
+ isDisabled = false,
31
+ className = "",
32
+ onConfirm,
33
+ onCancel,
34
+ } = props
35
+
36
+ const isDanger = tone === "danger"
37
+
38
+ return Dialog.render({
39
+ id,
40
+ isOpen,
41
+ variant: "sheet",
42
+ size: "sm",
43
+ description: message,
44
+ children,
45
+ className: ["iw-confirm-dialog", className].filter(Boolean).join(" "),
46
+ closeLabel,
47
+ actions: html`<div class="iw-confirm-dialog-actions">
48
+ ${Button.render({
49
+ children: isBusy ? busyLabel : confirmLabel,
50
+ color: isDanger ? "error" : "primary",
51
+ variant: isDanger ? "outline" : "default",
52
+ shape: "pill",
53
+ size: "md",
54
+ isDisabled: isBusy || isDisabled,
55
+ onClick: () => {
56
+ if (!isBusy && !isDisabled) onConfirm?.()
57
+ },
58
+ })}
59
+ ${Button.render({
60
+ children: cancelLabel,
61
+ variant: "outline",
62
+ shape: "pill",
63
+ size: "md",
64
+ isDisabled: isBusy,
65
+ onClick: onCancel,
66
+ })}
67
+ </div>`,
68
+ onClose: isBusy ? undefined : onCancel,
69
+ })
70
+ }
@@ -0,0 +1,3 @@
1
+ import * as renderers from "./template.js"
2
+
3
+ export const CropDialog = { ...renderers }
@@ -0,0 +1,43 @@
1
+ .iw-dialog.iw-crop-dialog {
2
+ width: min(48rem, calc(100vw - 2 * var(--iw-space-4)));
3
+ min-width: 0;
4
+ max-width: none;
5
+ max-height: calc(
6
+ 100dvh - 2 * var(--iw-space-4) - var(--safe-area-top, 0px) -
7
+ var(--safe-area-bottom, 0px)
8
+ );
9
+ overflow-x: hidden;
10
+ overflow-y: auto;
11
+ overscroll-behavior: contain;
12
+ }
13
+
14
+ .iw-crop-dialog-viewport {
15
+ width: 100%;
16
+ --iw-image-cropper-max-height: 55dvh;
17
+ }
18
+
19
+ @media (max-height: 32rem) {
20
+ .iw-crop-dialog-viewport {
21
+ --iw-image-cropper-max-height: 35dvh;
22
+ }
23
+ }
24
+
25
+ .iw-crop-dialog-error {
26
+ margin: var(--iw-space-3) 0 0;
27
+ }
28
+
29
+ .iw-crop-dialog-actions {
30
+ display: flex;
31
+ justify-content: flex-end;
32
+ align-items: center;
33
+ gap: var(--iw-space-2);
34
+ width: 100%;
35
+
36
+ .iw-button {
37
+ flex: 0 1 auto;
38
+ min-width: 0;
39
+ padding-inline: var(--iw-space-3);
40
+ font-size: var(--iw-font-size-sm);
41
+ white-space: nowrap;
42
+ }
43
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @typedef {import('../../../types/feedback/crop-dialog').CropDialogProps} CropDialogProps
3
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
4
+ * @typedef {import('@inglorious/web').Api} Api
5
+ */
6
+
7
+ import { html } from "@inglorious/web"
8
+ import { when } from "@inglorious/web/directives/when"
9
+
10
+ import { Button } from "../../controls/button/index.js"
11
+ import { Alert } from "../alert/index.js"
12
+ import { Dialog } from "../dialog/index.js"
13
+
14
+ /**
15
+ * Renders an ImageCropper entity in a dialog, with cancel and apply buttons.
16
+ * Apply stays disabled until the image has loaded, and passes the cropper's
17
+ * `rect`.
18
+ * @param {CropDialogProps} props
19
+ * @param {Api} api
20
+ * @returns {TemplateResult}
21
+ */
22
+ export function render(props, api) {
23
+ const {
24
+ id,
25
+ isOpen = false,
26
+ cropperId,
27
+ title,
28
+ description,
29
+ applyLabel = "Apply",
30
+ savingLabel = "Saving…",
31
+ cancelLabel = "Cancel",
32
+ closeLabel,
33
+ loadErrorText = "The image could not be loaded.",
34
+ error,
35
+ isSaving = false,
36
+ className = "",
37
+ onCancel,
38
+ onApply,
39
+ } = props
40
+
41
+ const cropper = api.getEntity(cropperId)
42
+ const isLoaded = Boolean(cropper?.naturalWidth)
43
+ const shownError = error || (cropper?.hasError ? loadErrorText : null)
44
+
45
+ return Dialog.render({
46
+ id,
47
+ isOpen,
48
+ title,
49
+ description,
50
+ closeLabel,
51
+ className: ["iw-crop-dialog", className].filter(Boolean).join(" "),
52
+ onClose: onCancel,
53
+ children: html`
54
+ <div class="iw-crop-dialog-viewport">${api.render(cropperId)}</div>
55
+ ${when(shownError, () =>
56
+ Alert.render({
57
+ severity: "error",
58
+ className: "iw-crop-dialog-error",
59
+ children: shownError,
60
+ }),
61
+ )}
62
+ `,
63
+ actions: html`<div class="iw-crop-dialog-actions">
64
+ ${Button.render({
65
+ children: cancelLabel,
66
+ variant: "ghost",
67
+ shape: "pill",
68
+ size: "sm",
69
+ isDisabled: isSaving,
70
+ onClick: onCancel,
71
+ })}
72
+ ${Button.render({
73
+ children: isSaving ? savingLabel : applyLabel,
74
+ color: "primary",
75
+ shape: "pill",
76
+ size: "sm",
77
+ isDisabled: isSaving || !isLoaded,
78
+ onClick: () => onApply?.(cropper.rect),
79
+ })}
80
+ </div>`,
81
+ })
82
+ }
@@ -43,8 +43,9 @@
43
43
  font-size: 1.1rem;
44
44
  }
45
45
 
46
- .iw-dialog-close {
46
+ .iw-dialog-header .iw-dialog-close {
47
47
  flex-shrink: 0;
48
+ margin-inline-start: auto;
48
49
  }
49
50
 
50
51
  .iw-dialog-description {
@@ -53,6 +53,9 @@ export function render(props) {
53
53
 
54
54
  const titleId = id && title ? `${id}-title` : ""
55
55
  const descriptionId = id && description ? `${id}-description` : ""
56
+ // Without a title, the description is what the dialog asks: its name.
57
+ const labelId = titleId || descriptionId
58
+ const describedById = titleId ? descriptionId : ""
56
59
  const onBackdrop = onBackdropClick ?? onClose
57
60
 
58
61
  return html`<dialog
@@ -63,8 +66,8 @@ export function render(props) {
63
66
  "iw-dialog-sheet": variant === "sheet",
64
67
  ...extraClasses,
65
68
  })}
66
- aria-labelledby=${titleId || nothing}
67
- aria-describedby=${descriptionId || nothing}
69
+ aria-labelledby=${labelId || nothing}
70
+ aria-describedby=${describedById || nothing}
68
71
  @pointerdown=${(event) => {
69
72
  backdropPresses.set(
70
73
  event.currentTarget,
@@ -1,6 +1,9 @@
1
1
  @import "./alert/style.css";
2
2
  @import "./backdrop/style.css";
3
+ @import "./confirm-dialog/style.css";
4
+ @import "./crop-dialog/style.css";
3
5
  @import "./dialog/style.css";
6
+ @import "./load-status/style.css";
4
7
  @import "./progress/style.css";
5
8
  @import "./skeleton/style.css";
6
9
  @import "./snackbar/style.css";
@@ -0,0 +1,3 @@
1
+ import * as renderers from "./template.js"
2
+
3
+ export const LoadStatus = { ...renderers }
@@ -0,0 +1,16 @@
1
+ import { createRender } from "../../stories/notify-story.js"
2
+ import { LoadStatus } from "."
3
+
4
+ export default {
5
+ title: "Feedback/LoadStatus",
6
+ tags: ["autodocs"],
7
+ render: createRender(LoadStatus),
8
+ }
9
+
10
+ export const Loading = { args: { isLoading: true } }
11
+
12
+ export const Empty = { args: { isEmpty: true, emptyText: "No events yet." } }
13
+
14
+ export const Error = {
15
+ args: { isError: true, error: "The events could not be loaded." },
16
+ }
@@ -0,0 +1,10 @@
1
+ .iw-load-status {
2
+ display: flex;
3
+ flex-direction: column;
4
+ align-items: center;
5
+ grid-column: 1 / -1;
6
+ padding: var(--iw-space-10) 0;
7
+ text-align: center;
8
+ font-size: var(--iw-font-size-sm);
9
+ color: var(--iw-color-text-muted, var(--iw-color-text));
10
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @typedef {import('../../../types/feedback/load-status').LoadStatusProps} LoadStatusProps
3
+ * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
4
+ */
5
+
6
+ import { html, nothing } from "@inglorious/web"
7
+
8
+ import { Progress } from "../progress/index.js"
9
+
10
+ /**
11
+ * Renders where a load stands instead of what it loads: an error, a loader or
12
+ * an empty message, in that order of precedence. Otherwise it renders
13
+ * `content`, a function so that it runs only once there is something to show.
14
+ * @param {LoadStatusProps} props
15
+ * @returns {import('../../../types/shared/renderable').Renderable}
16
+ */
17
+ export function render(props) {
18
+ const {
19
+ isError = false,
20
+ error,
21
+ isLoading = false,
22
+ loader,
23
+ loadingLabel = "Loading…",
24
+ isEmpty = false,
25
+ emptyText,
26
+ content,
27
+ className = "",
28
+ } = props
29
+
30
+ const classes = ["iw-load-status", className].filter(Boolean).join(" ")
31
+
32
+ if (isError) return html`<div class=${classes} role="alert">${error}</div>`
33
+ if (isLoading) {
34
+ return html`<div class=${classes} role="status">
35
+ ${
36
+ loader ??
37
+ Progress.render({ variant: "circular", ariaLabel: loadingLabel })
38
+ }
39
+ </div>`
40
+ }
41
+ if (isEmpty) return html`<div class=${classes}>${emptyText}</div>`
42
+ return content ? content() : nothing
43
+ }
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { Button } from "./controls/button/index.js"
2
2
  export { ButtonGroup } from "./controls/button-group/index.js"
3
3
  export { Checkbox } from "./controls/checkbox/index.js"
4
+ export { ChipPicker } from "./controls/chip-picker/index.js"
4
5
  export { Fab } from "./controls/fab/index.js"
5
6
  export { FileUpload } from "./controls/file-upload/index.js"
6
7
  export { IconButton } from "./controls/icon-button/index.js"
@@ -25,6 +26,7 @@ export { Chip } from "./data-display/chip/index.js"
25
26
  export { DataGrid } from "./data-display/data-grid/index.js"
26
27
  export { Divider } from "./data-display/divider/index.js"
27
28
  export { Icon } from "./data-display/icon/index.js"
29
+ export { Lightbox } from "./data-display/lightbox/index.js"
28
30
  export { List } from "./data-display/list/index.js"
29
31
  export { Table } from "./data-display/table/index.js"
30
32
  export { Tooltip } from "./data-display/tooltip/index.js"
@@ -32,7 +34,10 @@ export { Typography } from "./data-display/typography/index.js"
32
34
  export { VirtualList } from "./data-display/virtual-list/index.js"
33
35
  export { Alert } from "./feedback/alert/index.js"
34
36
  export { Backdrop } from "./feedback/backdrop/index.js"
37
+ export { ConfirmDialog } from "./feedback/confirm-dialog/index.js"
38
+ export { CropDialog } from "./feedback/crop-dialog/index.js"
35
39
  export { Dialog } from "./feedback/dialog/index.js"
40
+ export { LoadStatus } from "./feedback/load-status/index.js"
36
41
  export { Progress } from "./feedback/progress/index.js"
37
42
  export { Skeleton } from "./feedback/skeleton/index.js"
38
43
  export { Snackbar } from "./feedback/snackbar/index.js"
@@ -9,3 +9,15 @@ export const leftPanelClose = {
9
9
  height: 24,
10
10
  body: '<path fill="currentColor" d="M16.5 16V8l-4 4zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm3-2V5H5v14zm2 0h9V5h-9zm-2 0H5z"/>',
11
11
  }
12
+
13
+ export const chevronLeft = {
14
+ width: 24,
15
+ height: 24,
16
+ body: '<path fill="currentColor" d="m14 18l-6-6l6-6l1.4 1.4l-4.6 4.6l4.6 4.6z"/>',
17
+ }
18
+
19
+ export const chevronRight = {
20
+ width: 24,
21
+ height: 24,
22
+ body: '<path fill="currentColor" d="M12.6 12L8 7.4L9.4 6l6 6l-6 6L8 16.6z"/>',
23
+ }
@@ -0,0 +1,17 @@
1
+ import { ref } from "@inglorious/web/directives/ref"
2
+
3
+ /**
4
+ * Keeps an element's `checked` or `selected` property in step with the state.
5
+ *
6
+ * A `.checked=${false}` binding can't be used instead: server rendering
7
+ * reflects it as `checked="false"`, which HTML reads as checked. Templates pair
8
+ * this with the boolean attribute (`?checked=${value}`), which renders right on
9
+ * the server but stops counting once the user has touched the control.
10
+ * @param {"checked" | "selected"} name
11
+ * @param {boolean} value
12
+ */
13
+ export function liveBoolean(name, value) {
14
+ return ref((element) => {
15
+ if (element) element[name] = value
16
+ })
17
+ }
@@ -0,0 +1,23 @@
1
+ import type { TemplateResult } from "@inglorious/web"
2
+
3
+ import type { ComboboxOption } from "./combobox"
4
+
5
+ export interface ChipPickerProps {
6
+ /** The fieldset's legend. */
7
+ label?: TemplateResult | string
8
+ hint?: TemplateResult | string
9
+ /** Strings, numbers or `{ value, label, isDisabled }`, as in Combobox. */
10
+ options?: ComboboxOption[]
11
+ /** The values picked. */
12
+ value?: (string | number)[]
13
+ isDisabled?: boolean
14
+ className?: string
15
+ /** Called with the value of the chip pressed, picked or not. */
16
+ onToggle?: (value: string | number) => void
17
+ }
18
+
19
+ export type ChipPickerType = {
20
+ render(props: ChipPickerProps): TemplateResult
21
+ }
22
+
23
+ export declare const ChipPicker: ChipPickerType
@@ -45,6 +45,16 @@ export interface ComboboxProps {
45
45
  noOptionsMessage?: string
46
46
  loadingMessage?: string
47
47
 
48
+ /** A validation message, shown under the control. */
49
+ error?: string | null
50
+ /**
51
+ * An event to notify when the user picks or clears a value, such as
52
+ * `"#form:fieldChange"`. The payload is `changePayload` with the new `value`.
53
+ */
54
+ changeEvent?: string
55
+ /** Merged into `changeEvent`'s payload, such as `{ path: "country" }`. */
56
+ changePayload?: Record<string, unknown>
57
+
48
58
  isFullWidth?: boolean
49
59
  size?: "sm" | "md" | "lg"
50
60
  onToggle?: () => void
@@ -68,13 +78,21 @@ export type ComboboxType = {
68
78
  open(entity: ComboboxProps): void
69
79
  close(entity: ComboboxProps): void
70
80
  toggle(entity: ComboboxProps): void
71
- optionSelect(entity: ComboboxProps, option: ComboboxOption): void
72
- clear(entity: ComboboxProps): void
81
+ optionSelect(entity: ComboboxProps, option: ComboboxOption, api?: Api): void
82
+ clear(entity: ComboboxProps, payload?: unknown, api?: Api): void
73
83
  searchChange(entity: ComboboxProps, searchTerm: string): void
74
84
  focusNext(entity: ComboboxProps): void
75
85
  focusPrev(entity: ComboboxProps): void
76
86
  focusFirst(entity: ComboboxProps): void
77
87
  focusLast(entity: ComboboxProps): void
88
+ focusSet(entity: ComboboxProps, index: number): void
89
+ /** Sets the value from outside, without notifying `changeEvent`. An empty string is no value. */
90
+ valueSet(
91
+ entity: ComboboxProps,
92
+ value: string | number | (string | number)[] | null,
93
+ ): void
94
+ optionsSet(entity: ComboboxProps, options: ComboboxOption[]): void
95
+ errorSet(entity: ComboboxProps, error: string | null): void
78
96
  }
79
97
 
80
98
  export declare const Combobox: ComboboxType