@inglorious/ui 7.2.0 → 7.2.2

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.
@@ -5,12 +5,46 @@
5
5
 
6
6
  import { html } from "@inglorious/web"
7
7
  import { classMap } from "@inglorious/web/directives/class-map"
8
- import { ref } from "@inglorious/web/directives/ref"
9
8
  import { repeat } from "@inglorious/web/directives/repeat"
9
+ import { when } from "@inglorious/web/directives/when"
10
10
 
11
- import { applyElementProps } from "../../shared/applyElementProps.js"
11
+ import { Icon } from "../../data-display/icon/index.js"
12
12
  import { formatOption, getOptionValue, isOptionSelected } from "./helpers.js"
13
13
 
14
+ /**
15
+ * Opens the native dropdown when the arrow strip is pressed.
16
+ *
17
+ * The arrow is a sibling of the select, not an overlay on it, so a press on it
18
+ * has no control underneath to hit. A native popup only opens from a trusted
19
+ * gesture on the select itself, and the strip is outside it.
20
+ *
21
+ * `showPicker()` is the API for that. It needs transient user activation, which
22
+ * a real click has and a dispatched event does not, and it throws rather than
23
+ * silently doing nothing, so the fallback covers the browsers without it.
24
+ *
25
+ * Deliberately on `click`, not `pointerdown`: focusing from pointerdown gets
26
+ * the focus moved away again by the browser's own mousedown handling, which
27
+ * showed up as a focus immediately followed by a blur.
28
+ *
29
+ * @param {MouseEvent} event
30
+ */
31
+ function openFromArrowStrip(event) {
32
+ const frame = event.currentTarget
33
+ const select = frame?.querySelector("select")
34
+ if (!select || select.disabled || event.target === select) return
35
+
36
+ event.preventDefault()
37
+ if (typeof select.showPicker === "function") {
38
+ try {
39
+ select.showPicker()
40
+ return
41
+ } catch {
42
+ // No user activation, or the popup is already showing.
43
+ }
44
+ }
45
+ select.focus()
46
+ }
47
+
14
48
  /**
15
49
  * Renders a styled native select control with optional single or multi-select modes.
16
50
  * Options can be primitives or objects and are rendered with consistent theming.
@@ -21,77 +55,147 @@ import { formatOption, getOptionValue, isOptionSelected } from "./helpers.js"
21
55
  * @param {SelectProps} props
22
56
  * @returns {TemplateResult}
23
57
  */
24
- export function render(props) {
25
- const {
26
- id,
27
- type, // eslint-disable-line no-unused-vars
28
- name = "",
29
- value = "",
30
- options = [],
31
- size = "md",
32
- isDisabled = false,
33
- isFullWidth = false,
34
- isMulti = false,
35
- onChange,
36
- onBlur,
37
- onFocus,
38
- ...rest
39
- } = props
40
-
41
- const inputId = id || name
42
-
43
- const selectClasses = {
44
- "iw-select": true,
45
- "iw-select-full-width": !!isFullWidth,
46
- [`iw-select-${size}`]: size !== "md",
47
- }
58
+ export const Select = {
59
+ render(props) {
60
+ return this.renderSelect(props)
61
+ },
62
+
63
+ renderSelect(props) {
64
+ const {
65
+ id,
66
+ label,
67
+ hint,
68
+ error,
69
+ isRequired = false,
70
+ isFullWidth = false,
71
+ } = props
72
+
73
+ const labelId = label && id ? `${id}-label` : ""
74
+ const messageId = (error || hint) && id ? `${id}-message` : ""
75
+
76
+ const selectClasses = {
77
+ "iw-select": true,
78
+ "iw-select-full-width": !!isFullWidth,
79
+ }
80
+
81
+ return html`
82
+ <div class=${classMap(selectClasses)}>
83
+ ${when(
84
+ label,
85
+ () => html`
86
+ <label for=${id || name || ""} class="iw-select-label">
87
+ ${label}
88
+ ${
89
+ isRequired
90
+ ? html`<span class="iw-select-required">*</span>`
91
+ : null
92
+ }
93
+ </label>
94
+ `,
95
+ )}
96
+ ${this.renderControl?.({ ...props, labelId, messageId })}
97
+ ${when(
98
+ error,
99
+ () =>
100
+ html`<span id=${messageId || ""} class="iw-select-error-message"
101
+ >${error}</span
102
+ >`,
103
+ )}
104
+ ${when(
105
+ !error && hint,
106
+ () =>
107
+ html`<span id=${messageId || ""} class="iw-select-hint"
108
+ >${hint}</span
109
+ >`,
110
+ )}
111
+ </div>
112
+ `
113
+ },
114
+
115
+ renderControl(props) {
116
+ const {
117
+ id,
118
+ name,
119
+ options,
120
+ value,
121
+ error,
122
+ icon,
123
+ size = "md",
124
+ isOpen,
125
+ isMulti,
126
+ isRequired,
127
+ isDisabled,
128
+ onChange,
129
+ onBlur,
130
+ onFocus,
131
+ } = props
132
+
133
+ const controlId = id || name
134
+
135
+ const controlClasses = {
136
+ "iw-select-control": true,
137
+ [`iw-select-${size}`]: true,
138
+ "iw-select-control-open": isOpen,
139
+ "iw-select-control-disabled": isDisabled,
140
+ "iw-select-control-error": !!error,
141
+ }
142
+
143
+ const selectedOption = options.find(
144
+ (option) => getOptionValue(option) === value,
145
+ )
146
+
147
+ return html`<div class="iw-select-frame" @click=${openFromArrowStrip}>
148
+ ${icon ? Icon.render({ children: icon, size }) : null}
149
+
150
+ <select
151
+ id=${controlId}
152
+ name=${name}
153
+ ?multiple=${isMulti}
154
+ ?disabled=${isDisabled}
155
+ ?required=${isRequired}
156
+ class=${classMap(controlClasses)}
157
+ @change=${(event) => onChange?.(event.target.value)}
158
+ @blur=${onBlur}
159
+ @focus=${onFocus}
160
+ >
161
+ ${repeat(
162
+ options,
163
+ (option) => getOptionValue(option),
164
+ (option) => {
165
+ const {
166
+ value,
167
+ label,
168
+ isDisabled: isOptionDisabled,
169
+ } = formatOption(option)
170
+ const isSelected = isOptionSelected(
171
+ option,
172
+ selectedOption?.value,
173
+ isMulti,
174
+ )
175
+
176
+ const optionClasses = {
177
+ "iw-select-option": true,
178
+ "iw-select-option-selected": isSelected,
179
+ }
48
180
 
49
- const selectedOption = options.find(
50
- (option) => getOptionValue(option) === value,
51
- )
52
-
53
- return html`
54
- <select
55
- id=${inputId}
56
- name=${name}
57
- ?multiple=${isMulti}
58
- ?disabled=${isDisabled}
59
- class=${classMap(selectClasses)}
60
- @change=${(event) => onChange?.(event.target.value)}
61
- @blur=${onBlur}
62
- @focus=${onFocus}
63
- ${ref((element) => applyElementProps(element, rest))}
64
- >
65
- ${repeat(
66
- options,
67
- (option) => getOptionValue(option),
68
- (option) => {
69
- const {
70
- value,
71
- label,
72
- isDisabled: isOptionDisabled,
73
- } = formatOption(option)
74
- const isSelected = isOptionSelected(
75
- option,
76
- selectedOption?.value,
77
- isMulti,
78
- )
79
-
80
- const optionClasses = {
81
- "iw-select-option": true,
82
- "iw-select-option-selected": isSelected,
83
- }
84
-
85
- return html`<option
86
- value=${value}
87
- .selected=${value === selectedOption?.value}
88
- ?disabled=${isOptionDisabled}
89
- class=${classMap(optionClasses)}
90
- >
91
- ${label}
92
- </option>`
93
- },
94
- )}
95
- </select>
96
- `
181
+ return html`<option
182
+ value=${value}
183
+ .selected=${value === selectedOption?.value}
184
+ ?disabled=${isOptionDisabled}
185
+ class=${classMap(optionClasses)}
186
+ >
187
+ ${label}
188
+ </option>`
189
+ },
190
+ )}
191
+ </select>
192
+ <!-- A native select can only contain options, so the arrow cannot live
193
+ inside it. It sits on a wrapper around the select rather than the
194
+ field, so it stays centred on the control whether or not a label,
195
+ hint or error sits above and below it. Decorative: pointer-events
196
+ none so clicks reach the select, and hidden from assistive tech,
197
+ which announces a dropdown without needing the glyph. -->
198
+ <span class="iw-select-arrow" aria-hidden="true">▼</span>
199
+ </div>`
200
+ },
97
201
  }
@@ -6,7 +6,10 @@ export type ComboboxOption =
6
6
  | {
7
7
  value: string | number
8
8
  label?: string
9
+ hint?: string
10
+ error?: string
9
11
  isDisabled?: boolean
12
+ isRequired?: boolean
10
13
  [key: string]: unknown
11
14
  }
12
15
 
@@ -19,7 +19,11 @@ export interface SelectProps {
19
19
  size?: "sm" | "md" | "lg"
20
20
  isDisabled?: boolean
21
21
  isFullWidth?: boolean
22
+ label?: string
23
+ hint?: string
24
+ error?: string
22
25
  isMulti?: boolean
26
+ isRequired?: boolean
23
27
  onChange?: (value: string) => void
24
28
  onBlur?: () => void
25
29
  onFocus?: () => void