@openreachtech/furo-vue 1.5.0 → 1.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 (89) hide show
  1. package/README.ja.md +2 -0
  2. package/README.md +2 -0
  3. package/docs/design-tokens.md +419 -0
  4. package/docs/furo-accordion.md +109 -0
  5. package/docs/furo-alert-dialog.md +107 -0
  6. package/docs/furo-attachment.md +133 -0
  7. package/docs/furo-autocomplete-field.md +719 -0
  8. package/docs/furo-avatar-group.md +87 -0
  9. package/docs/furo-avatar.md +101 -0
  10. package/docs/furo-badge.md +126 -0
  11. package/docs/furo-breadcrumb.md +89 -0
  12. package/docs/furo-button.md +263 -0
  13. package/docs/furo-checkbox.md +384 -0
  14. package/docs/furo-collapsible.md +104 -0
  15. package/docs/furo-control-block.md +102 -0
  16. package/docs/furo-date-picker.md +270 -0
  17. package/docs/furo-date-time-picker.md +110 -0
  18. package/docs/furo-dialog.md +88 -0
  19. package/docs/furo-drawer.md +122 -0
  20. package/docs/furo-dropdown-menu.md +298 -0
  21. package/docs/furo-editable-field.md +391 -0
  22. package/docs/furo-editor.md +304 -0
  23. package/docs/furo-email-field.md +93 -0
  24. package/docs/furo-empty-state.md +97 -0
  25. package/docs/furo-error-state.md +97 -0
  26. package/docs/furo-file-field.md +163 -0
  27. package/docs/furo-icon.md +288 -0
  28. package/docs/furo-number-field.md +397 -0
  29. package/docs/furo-pagination.md +101 -0
  30. package/docs/furo-password-field.md +75 -0
  31. package/docs/furo-popover.md +114 -0
  32. package/docs/furo-radio.md +125 -0
  33. package/docs/furo-scroll-area.md +112 -0
  34. package/docs/furo-search-field.md +94 -0
  35. package/docs/furo-section-header.md +135 -0
  36. package/docs/furo-select.md +443 -0
  37. package/docs/furo-separator.md +87 -0
  38. package/docs/furo-sidebar.md +178 -0
  39. package/docs/furo-skeleton.md +92 -0
  40. package/docs/furo-splitter.md +103 -0
  41. package/docs/furo-stepper.md +127 -0
  42. package/docs/furo-switch.md +107 -0
  43. package/docs/furo-table.md +233 -0
  44. package/docs/furo-tabs.md +173 -0
  45. package/docs/furo-text-field.md +249 -0
  46. package/docs/furo-textarea.md +214 -0
  47. package/docs/furo-time-field.md +276 -0
  48. package/docs/furo-toast.md +217 -0
  49. package/docs/furo-toggle-group.md +123 -0
  50. package/docs/furo-toggle.md +104 -0
  51. package/docs/furo-tool-bar.md +88 -0
  52. package/docs/furo-tooltip.md +107 -0
  53. package/lib/assets/css/furo/0030.variables-semantic-color.css +15 -1
  54. package/lib/components/atoms/FuroButton/FuroButtonContext.js +8 -8
  55. package/lib/components/atoms/FuroCheckbox/FuroCheckboxContext.js +1 -1
  56. package/lib/components/atoms/FuroIcon/FuroIcon.vue +1 -1
  57. package/lib/components/atoms/FuroSwitch/FuroSwitchContext.js +1 -1
  58. package/lib/components/atoms/FuroToggle/FuroToggleContext.js +1 -1
  59. package/lib/components/molecules/FuroAccordion/FuroAccordionContext.js +1 -1
  60. package/lib/components/molecules/FuroAttachment/FuroAttachment.vue +1 -1
  61. package/lib/components/molecules/FuroBreadcrumb/FuroBreadcrumb.vue +1 -1
  62. package/lib/components/molecules/FuroCollapsible/FuroCollapsibleContext.js +1 -1
  63. package/lib/components/molecules/FuroControlBlock/FuroControlBlock.vue +2 -2
  64. package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenu.vue +1 -1
  65. package/lib/components/molecules/FuroDropdownMenu/FuroDropdownMenuContext.js +1 -2
  66. package/lib/components/molecules/FuroErrorState/FuroErrorState.vue +1 -1
  67. package/lib/components/molecules/FuroPopover/FuroPopoverContext.js +1 -1
  68. package/lib/components/molecules/FuroSearchField/FuroSearchField.vue +1 -1
  69. package/lib/components/molecules/FuroSelect/FuroSelect.vue +8 -6
  70. package/lib/components/molecules/FuroSelect/FuroSelectContext.js +35 -0
  71. package/lib/components/molecules/FuroSelect/FuroSelectOptionText.vue +40 -0
  72. package/lib/components/molecules/FuroSelect/FuroSelectOptionTextContext.js +275 -0
  73. package/lib/components/molecules/FuroSplitter/FuroSplitterContext.js +1 -1
  74. package/lib/components/molecules/FuroToggleGroup/FuroToggleGroupContext.js +1 -1
  75. package/lib/components/molecules/FuroTooltip/FuroTooltipContext.js +1 -1
  76. package/lib/components/organisms/FuroSidebar/FuroSidebar.vue +3 -3
  77. package/lib/components/organisms/FuroTable/FuroTable.vue +1 -1
  78. package/lib/components/organisms/FuroTable/FuroTableContext.js +3 -3
  79. package/lib/components/organisms/FuroToast/FuroToastContext.js +6 -2
  80. package/lib/components/organisms/FuroToast/ToastQueue.js +4 -4
  81. package/nuxt/index.js +9 -9
  82. package/package.json +23 -22
  83. package/public/furo-vue/components.json +1 -1
  84. package/types/generated/components/atoms/FuroButton/FuroButtonContext.d.ts +4 -4
  85. package/types/generated/components/molecules/FuroSelect/FuroSelect.vue.d.ts +23 -0
  86. package/types/generated/components/molecules/FuroSelect/FuroSelectContext.d.ts +14 -0
  87. package/types/generated/components/molecules/FuroSelect/FuroSelectOptionText.vue.d.ts +50 -0
  88. package/types/generated/components/molecules/FuroSelect/FuroSelectOptionTextContext.d.ts +141 -0
  89. package/types/generated/components/organisms/FuroTable/FuroTableContext.d.ts +2 -2
@@ -8,6 +8,9 @@
8
8
  --color-foreground-title: var(--palette-gray-900);
9
9
  --color-foreground-secondary: var(--palette-gray-700);
10
10
  --color-foreground-tertiary: var(--palette-gray-600);
11
+ /* The caption colour for an icon, a caret or a separator. A glyph is held to
12
+ 3:1 rather than 4.5:1, so it keeps the step that text may not. See ADR 0043. */
13
+ --color-foreground-glyph: var(--palette-gray-600);
11
14
  --color-foreground-light: var(--palette-gray-400);
12
15
  --color-foreground-extra-light: var(--palette-gray-300);
13
16
  --color-foreground-inverse: var(--palette-gray-050);
@@ -27,10 +30,15 @@
27
30
  --color-accent-foreground: var(--palette-gray-800);
28
31
  --color-destructive: var(--palette-rose-500);
29
32
  --color-destructive-foreground: var(--palette-gray-950);
33
+ /* Text drawn in the danger family. The fill cannot darken to carry it, as
34
+ its label would fall under 4.5:1. See ADR 0043. */
35
+ --color-destructive-text: var(--palette-rose-600);
30
36
  --color-border: var(--palette-gray-200);
31
37
  --color-input: var(--palette-gray-400);
32
38
  --color-ring: var(--palette-blue-500);
33
- --color-link: var(--palette-blue-500);
39
+ /* The lightest step clearing 4.5:1 on the page. The step below lands at
40
+ 3.68:1. See ADR 0043. */
41
+ --color-link: var(--palette-blue-600);
34
42
 
35
43
  /* Brand: the design's dark neutral family, distinct from the blue action colour */
36
44
  --color-brand-surface-subtle: var(--palette-gray-100);
@@ -65,12 +73,14 @@
65
73
  design does this deliberately; it is followed rather than normalised. */
66
74
  --color-warning: var(--palette-orange-500);
67
75
  --color-warning-foreground: var(--palette-gray-950);
76
+ --color-warning-text: var(--palette-orange-700);
68
77
  --color-warning-subtle: var(--palette-orange-100);
69
78
  /* One step darker than the other three tint labels, because the tint it sits
70
79
  on is one step darker. The 700 step lands at 4.52:1 here. */
71
80
  --color-warning-subtle-foreground: var(--palette-orange-800);
72
81
  --color-info: var(--palette-blue-500);
73
82
  --color-info-foreground: var(--palette-gray-950);
83
+ --color-info-text: var(--palette-blue-600);
74
84
  --color-info-subtle: var(--palette-blue-050);
75
85
  --color-info-subtle-foreground: var(--palette-blue-700);
76
86
  /* One step lighter than the role itself, for a border that has to read as
@@ -102,6 +112,7 @@
102
112
  --color-foreground-title: var(--palette-white);
103
113
  --color-foreground-secondary: var(--palette-gray-200);
104
114
  --color-foreground-tertiary: var(--palette-gray-300);
115
+ --color-foreground-glyph: var(--palette-gray-300);
105
116
  --color-foreground-light: var(--palette-gray-500);
106
117
  --color-foreground-extra-light: var(--palette-gray-600);
107
118
  --color-foreground-inverse: var(--palette-gray-900);
@@ -119,6 +130,7 @@
119
130
  --color-accent-foreground: var(--palette-gray-050);
120
131
  --color-destructive: var(--palette-rose-500);
121
132
  --color-destructive-foreground: var(--palette-gray-950);
133
+ --color-destructive-text: var(--palette-rose-500);
122
134
  --color-border: var(--palette-gray-800);
123
135
  --color-input: var(--palette-gray-600);
124
136
  --color-ring: var(--palette-blue-500);
@@ -155,12 +167,14 @@
155
167
  --color-success-subtle-foreground: var(--palette-emerald-300);
156
168
  --color-warning: var(--palette-orange-400);
157
169
  --color-warning-foreground: var(--palette-gray-950);
170
+ --color-warning-text: var(--palette-orange-400);
158
171
  /* One step in from 950, mirroring the light block drawing warning one step
159
172
  in from 050 while the other three families sit at the extreme. */
160
173
  --color-warning-subtle: var(--palette-orange-900);
161
174
  --color-warning-subtle-foreground: var(--palette-orange-200);
162
175
  --color-info: var(--palette-blue-400);
163
176
  --color-info-foreground: var(--palette-gray-950);
177
+ --color-info-text: var(--palette-blue-400);
164
178
  --color-info-subtle: var(--palette-blue-950);
165
179
  --color-info-subtle-foreground: var(--palette-blue-300);
166
180
  /* Keeps the light block's rule, one step lighter than the role itself. */
@@ -215,22 +215,22 @@ export default class FuroButtonContext extends BaseFuroContext {
215
215
  * Extract ARIA attributes for the button root.
216
216
  *
217
217
  * @returns {{
218
- * 'aria-disabled': 'true' | undefined
219
- * 'aria-busy': 'true' | undefined
218
+ * 'aria-disabled'?: 'true'
219
+ * 'aria-busy'?: 'true'
220
220
  * }}
221
221
  */
222
222
  extractAriaAttributes () {
223
223
  const ariaDisabled = this.isDisabled()
224
- ? 'true'
225
- : undefined
224
+ ? { 'aria-disabled': 'true' }
225
+ : {}
226
226
 
227
227
  const ariaBusy = this.isLoading()
228
- ? 'true'
229
- : undefined
228
+ ? { 'aria-busy': 'true' }
229
+ : {}
230
230
 
231
231
  return {
232
- 'aria-disabled': ariaDisabled,
233
- 'aria-busy': ariaBusy,
232
+ ...ariaDisabled,
233
+ ...ariaBusy,
234
234
  }
235
235
  }
236
236
 
@@ -251,7 +251,7 @@ export default class FuroCheckboxContext extends BaseFuroContext {
251
251
 
252
252
  const { value: attrsValue } = this.attrs ?? {}
253
253
 
254
- if (attrsValue !== undefined) {
254
+ if (typeof attrsValue !== 'undefined') {
255
255
  return Boolean(attrsValue)
256
256
  }
257
257
 
@@ -68,7 +68,7 @@ export default defineComponent({
68
68
  }
69
69
 
70
70
  .unit-icon.unresolved {
71
- color: var(--color-warning);
71
+ color: var(--color-warning-text);
72
72
  }
73
73
  }
74
74
  </style>
@@ -225,7 +225,7 @@ export default class FuroSwitchContext extends BaseFuroContext {
225
225
 
226
226
  const { value: attrsValue } = this.attrs ?? {}
227
227
 
228
- if (attrsValue !== undefined) {
228
+ if (typeof attrsValue !== 'undefined') {
229
229
  return Boolean(attrsValue)
230
230
  }
231
231
 
@@ -191,7 +191,7 @@ export default class FuroToggleContext extends BaseFuroContext {
191
191
 
192
192
  const { value: attrsValue } = this.attrs ?? {}
193
193
 
194
- if (attrsValue !== undefined) {
194
+ if (typeof attrsValue !== 'undefined') {
195
195
  return Boolean(attrsValue)
196
196
  }
197
197
 
@@ -267,7 +267,7 @@ export default class FuroAccordionContext extends BaseFuroContext {
267
267
 
268
268
  const { value: attrsValue } = this.attrs ?? {}
269
269
 
270
- if (attrsValue !== undefined) {
270
+ if (typeof attrsValue !== 'undefined') {
271
271
  return /** @type {FuroAccordionValue} */ (attrsValue)
272
272
  }
273
273
 
@@ -348,7 +348,7 @@ export default defineComponent({
348
348
  width: 1.5rem;
349
349
  height: 1.5rem;
350
350
 
351
- color: var(--color-info);
351
+ color: var(--color-info-text);
352
352
  }
353
353
 
354
354
  .unit-attachment .action-icon {
@@ -207,7 +207,7 @@ export default defineComponent({
207
207
  display: inline-flex;
208
208
  align-items: center;
209
209
 
210
- color: var(--color-foreground-tertiary);
210
+ color: var(--color-foreground-glyph);
211
211
  }
212
212
 
213
213
  .unit-breadcrumb .separator-icon,
@@ -192,7 +192,7 @@ export default class FuroCollapsibleContext extends BaseFuroContext {
192
192
 
193
193
  const { value: attrsValue } = this.attrs ?? {}
194
194
 
195
- if (attrsValue !== undefined) {
195
+ if (typeof attrsValue !== 'undefined') {
196
196
  return Boolean(attrsValue)
197
197
  }
198
198
 
@@ -125,7 +125,7 @@ export default defineComponent({
125
125
  .unit-form-control-block > .label > .required-mark {
126
126
  margin-inline-start: 0.125rem;
127
127
 
128
- color: var(--color-destructive);
128
+ color: var(--color-destructive-text);
129
129
  }
130
130
 
131
131
  .unit-form-control-block.horizontal > .control {
@@ -138,7 +138,7 @@ export default defineComponent({
138
138
 
139
139
  list-style: none;
140
140
 
141
- color: var(--color-destructive);
141
+ color: var(--color-destructive-text);
142
142
  font: var(--font-body-small);
143
143
  }
144
144
 
@@ -329,7 +329,7 @@ export default defineComponent({
329
329
  }
330
330
 
331
331
  .unit-dropdown-menu-content > .menu-option.destructive {
332
- color: var(--color-destructive);
332
+ color: var(--color-destructive-text);
333
333
  }
334
334
 
335
335
  .unit-dropdown-menu-content > .menu-option.destructive[data-highlighted]:not([data-disabled]) {
@@ -170,7 +170,6 @@ export default class FuroDropdownMenuContext extends BaseFuroContext {
170
170
  */
171
171
  get parcel () {
172
172
  return {
173
- open: undefined,
174
173
  disabled: false,
175
174
  side: 'bottom',
176
175
  align: 'start',
@@ -199,7 +198,7 @@ export default class FuroDropdownMenuContext extends BaseFuroContext {
199
198
  * @returns {boolean}
200
199
  */
201
200
  isOpenControlled () {
202
- return this.parcel?.open !== undefined
201
+ return typeof this.parcel?.open !== 'undefined'
203
202
  }
204
203
 
205
204
  /**
@@ -98,7 +98,7 @@ export default defineComponent({
98
98
  .unit-error-state > .icon {
99
99
  display: flex;
100
100
 
101
- color: var(--color-destructive);
101
+ color: var(--color-destructive-text);
102
102
  font-size: 2rem;
103
103
  }
104
104
 
@@ -125,7 +125,7 @@ export default class FuroPopoverContext extends BaseFuroContext {
125
125
  * @returns {boolean} true when `parcel.open` is supplied.
126
126
  */
127
127
  isOpenControlled () {
128
- return this.parcel?.open !== undefined
128
+ return typeof this.parcel?.open !== 'undefined'
129
129
  && this.parcel?.open !== null
130
130
  }
131
131
 
@@ -128,7 +128,7 @@ export default defineComponent({
128
128
  display: inline-flex;
129
129
  align-items: center;
130
130
 
131
- color: var(--color-foreground-tertiary);
131
+ color: var(--color-foreground-glyph);
132
132
  }
133
133
 
134
134
  .unit-form-control.search > .leading > .leading-icon {
@@ -9,12 +9,14 @@ import {
9
9
 
10
10
  import FuroIcon from '../../atoms/FuroIcon/FuroIcon.vue'
11
11
  import FuroSelectContext from './FuroSelectContext.js'
12
+ import FuroSelectOptionText from './FuroSelectOptionText.vue'
12
13
 
13
14
  export default defineComponent({
14
15
  name: 'FuroSelect',
15
16
 
16
17
  components: {
17
18
  FuroIcon,
19
+ FuroSelectOptionText,
18
20
  SelectRoot: RekaSelect['Root'],
19
21
  SelectTrigger: RekaSelect['Trigger'],
20
22
  SelectValue: RekaSelect['Value'],
@@ -168,7 +170,7 @@ export default defineComponent({
168
170
  <SelectViewport class="viewport">
169
171
  <template
170
172
  v-for="(option, index) in context.parcel?.options"
171
- :key="index"
173
+ :key="context.generateOptionKey({ option, index })"
172
174
  >
173
175
  <template v-if="context.shouldShowGroupOption({ option })">
174
176
  <SelectGroup
@@ -192,7 +194,7 @@ export default defineComponent({
192
194
 
193
195
  <template
194
196
  v-for="(groupChildOption, groupChildIndex) in context.extractGroupOptions({ option })"
195
- :key="groupChildIndex"
197
+ :key="context.generateOptionKey({ option: groupChildOption, index: groupChildIndex })"
196
198
  >
197
199
  <SelectOption
198
200
  class="option"
@@ -206,7 +208,7 @@ export default defineComponent({
206
208
  :option="groupChildOption"
207
209
  :option-index="groupChildIndex"
208
210
  >
209
- <SelectOptionText>
211
+ <FuroSelectOptionText>
210
212
  <slot
211
213
  name="option-text"
212
214
  :option="groupChildOption"
@@ -214,7 +216,7 @@ export default defineComponent({
214
216
  >
215
217
  {{ context.resolveOptionLabel({ option: groupChildOption }) }}
216
218
  </slot>
217
- </SelectOptionText>
219
+ </FuroSelectOptionText>
218
220
 
219
221
  <SelectOptionIndicator class="select-indicator">
220
222
  <slot
@@ -252,7 +254,7 @@ export default defineComponent({
252
254
  :option="option"
253
255
  :option-index="index"
254
256
  >
255
- <SelectOptionText>
257
+ <FuroSelectOptionText>
256
258
  <slot
257
259
  name="option-text"
258
260
  :option="option"
@@ -260,7 +262,7 @@ export default defineComponent({
260
262
  >
261
263
  {{ context.resolveOptionLabel({ option }) }}
262
264
  </slot>
263
- </SelectOptionText>
265
+ </FuroSelectOptionText>
264
266
 
265
267
  <SelectOptionIndicator class="select-indicator">
266
268
  <slot
@@ -295,6 +295,41 @@ export default class FuroSelectContext extends BaseFuroContext {
295
295
  ?? null
296
296
  }
297
297
 
298
+ /**
299
+ * Build an option's render key from its value and label, so a change to either
300
+ * remounts the item, because the item records both only when it mounts.
301
+ *
302
+ * @param {{
303
+ * option: FuroSelectOption
304
+ * index: number
305
+ * }} params - Parameters of this method.
306
+ * @returns {string} Render key.
307
+ */
308
+ generateOptionKey ({
309
+ option,
310
+ index,
311
+ }) {
312
+ if (this.shouldShowGroupOption({ option })) {
313
+ return `group-${index}`
314
+ }
315
+
316
+ return JSON.stringify(
317
+ [
318
+ this.resolveOptionValue({
319
+ option,
320
+ }),
321
+ this.resolveOptionLabel({
322
+ option,
323
+ }),
324
+ ],
325
+ (key, value) => (
326
+ typeof value === 'bigint'
327
+ ? `${value}n`
328
+ : value
329
+ )
330
+ )
331
+ }
332
+
298
333
  /**
299
334
  * Setup component context.
300
335
  *
@@ -0,0 +1,40 @@
1
+ <script>
2
+ import {
3
+ defineComponent,
4
+ } from 'vue'
5
+
6
+ import {
7
+ Select as RekaSelect,
8
+ } from 'reka-ui/namespaced'
9
+
10
+ import FuroSelectOptionTextContext from './FuroSelectOptionTextContext.js'
11
+
12
+ export default defineComponent({
13
+ name: 'FuroSelectOptionText',
14
+
15
+ components: {
16
+ SelectOptionText: RekaSelect['ItemText'],
17
+ },
18
+
19
+ setup (
20
+ props,
21
+ componentContext
22
+ ) {
23
+ const context = FuroSelectOptionTextContext.create({
24
+ props,
25
+ componentContext,
26
+ })
27
+ .setupComponent()
28
+
29
+ return {
30
+ context,
31
+ }
32
+ },
33
+ })
34
+ </script>
35
+
36
+ <template>
37
+ <SelectOptionText :ref="context.textComponentRef">
38
+ <slot />
39
+ </SelectOptionText>
40
+ </template>
@@ -0,0 +1,275 @@
1
+ import {
2
+ onBeforeUnmount,
3
+ onMounted,
4
+ ref,
5
+ shallowRef,
6
+ } from 'vue'
7
+
8
+ import {
9
+ injectSelectItemContext,
10
+ injectSelectRootContext,
11
+ } from 'reka-ui'
12
+
13
+ import BaseFuroContext from '../../../contexts/BaseFuroContext.js'
14
+
15
+ /**
16
+ * Context class for the text of one FuroSelect option.
17
+ *
18
+ * @extends {BaseFuroContext<null, {}, never>}
19
+ */
20
+ export default class FuroSelectOptionTextContext extends BaseFuroContext {
21
+ /**
22
+ * Constructor.
23
+ *
24
+ * @param {FuroSelectOptionTextContextParams} params - Parameters of this constructor.
25
+ */
26
+ constructor ({
27
+ props,
28
+ componentContext,
29
+ refHash,
30
+ selectRootContext,
31
+ selectItemContext,
32
+ }) {
33
+ super({
34
+ props,
35
+ componentContext,
36
+ })
37
+
38
+ this.refHash = refHash
39
+ this.selectRootContext = selectRootContext
40
+ this.selectItemContext = selectItemContext
41
+ }
42
+
43
+ /**
44
+ * Factory method.
45
+ *
46
+ * @template {X extends typeof FuroSelectOptionTextContext ? X : never} T, X
47
+ * @override
48
+ * @param {FuroSelectOptionTextContextFactoryParams} params - Parameters of this factory method.
49
+ * @returns {InstanceType<T>} An instance of this class.
50
+ * @this {T}
51
+ */
52
+ static create ({
53
+ props,
54
+ componentContext,
55
+
56
+ refHash = this.buildRefHash(),
57
+ selectRootContext = this.injectSelectRootContext(),
58
+ selectItemContext = this.injectSelectItemContext(),
59
+ }) {
60
+ return /** @type {InstanceType<T>} */ (
61
+ new this({
62
+ props,
63
+ componentContext,
64
+
65
+ refHash,
66
+ selectRootContext,
67
+ selectItemContext,
68
+ })
69
+ )
70
+ }
71
+
72
+ /**
73
+ * Build ref hash.
74
+ *
75
+ * @override
76
+ * @returns {FuroSelectOptionTextContextParams['refHash']}
77
+ */
78
+ static buildRefHash () {
79
+ return {
80
+ textComponentRef: shallowRef(null),
81
+ observerRef: shallowRef(null),
82
+ recordedTextRef: ref(null),
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Inject the select root context, which holds the text the trigger shows.
88
+ *
89
+ * @returns {FuroSelectRootContext}
90
+ */
91
+ static injectSelectRootContext () {
92
+ return injectSelectRootContext()
93
+ }
94
+
95
+ /**
96
+ * Inject the context of the option that holds this text.
97
+ *
98
+ * @returns {FuroSelectItemContext}
99
+ */
100
+ static injectSelectItemContext () {
101
+ return injectSelectItemContext()
102
+ }
103
+
104
+ /**
105
+ * get: Ctor
106
+ *
107
+ * @override
108
+ * @returns {typeof FuroSelectOptionTextContext}
109
+ */
110
+ get Ctor () {
111
+ return /** @type {typeof FuroSelectOptionTextContext} */ (
112
+ this.constructor
113
+ )
114
+ }
115
+
116
+ /**
117
+ * get: textComponentRef
118
+ *
119
+ * @returns {import('vue').ShallowRef<{ $el: HTMLElement } | null>}
120
+ */
121
+ get textComponentRef () {
122
+ return this.refHash.textComponentRef
123
+ }
124
+
125
+ /**
126
+ * get: observerRef
127
+ *
128
+ * @returns {import('vue').ShallowRef<MutationObserver | null>}
129
+ */
130
+ get observerRef () {
131
+ return this.refHash.observerRef
132
+ }
133
+
134
+ /**
135
+ * get: recordedTextRef
136
+ *
137
+ * @returns {import('vue').Ref<string | null>}
138
+ */
139
+ get recordedTextRef () {
140
+ return this.refHash.recordedTextRef
141
+ }
142
+
143
+ /**
144
+ * Setup component context.
145
+ *
146
+ * @template {X extends FuroSelectOptionTextContext ? X : never} T, X
147
+ * @override
148
+ * @returns {T} - For method chaining.
149
+ * @this {T}
150
+ */
151
+ setupComponent () {
152
+ onMounted(() => {
153
+ this.recordedTextRef.value = this.extractText()
154
+ this.observeTextElement()
155
+ })
156
+
157
+ onBeforeUnmount(() => {
158
+ this.disconnectObserver()
159
+ })
160
+
161
+ return this
162
+ }
163
+
164
+ /**
165
+ * Extract the rendered text element.
166
+ *
167
+ * @returns {HTMLElement | null}
168
+ */
169
+ extractTextElement () {
170
+ return this.textComponentRef.value?.$el
171
+ ?? null
172
+ }
173
+
174
+ /**
175
+ * Extract the rendered text.
176
+ *
177
+ * @returns {string | null}
178
+ */
179
+ extractText () {
180
+ return this.extractTextElement()?.textContent
181
+ ?? null
182
+ }
183
+
184
+ /**
185
+ * Watch the rendered text for changes.
186
+ */
187
+ observeTextElement () {
188
+ const textElement = this.extractTextElement()
189
+
190
+ if (!textElement) {
191
+ return
192
+ }
193
+
194
+ const observer = this.Ctor.createMutationObserver({
195
+ handler: () => this.syncText(),
196
+ })
197
+
198
+ observer.observe(textElement, {
199
+ subtree: true,
200
+ childList: true,
201
+ characterData: true,
202
+ })
203
+
204
+ this.observerRef.value = observer
205
+ }
206
+
207
+ /**
208
+ * Replace the recorded option text when the rendered text differs from it.
209
+ */
210
+ syncText () {
211
+ const text = this.extractText()
212
+
213
+ if (text === null) {
214
+ return
215
+ }
216
+
217
+ if (text === this.recordedTextRef.value) {
218
+ return
219
+ }
220
+
221
+ this.recordedTextRef.value = text
222
+ this.selectRootContext.onOptionAdd({
223
+ value: this.selectItemContext.value,
224
+ disabled: this.selectItemContext.disabled.value,
225
+ textContent: text,
226
+ })
227
+ }
228
+
229
+ /**
230
+ * Stop watching the rendered text.
231
+ */
232
+ disconnectObserver () {
233
+ this.observerRef.value?.disconnect()
234
+ this.observerRef.value = null
235
+ }
236
+ }
237
+
238
+ /**
239
+ * @typedef {{
240
+ * onOptionAdd: (option: {
241
+ * value: unknown
242
+ * disabled: boolean
243
+ * textContent: string
244
+ * }) => void
245
+ * }} FuroSelectRootContext
246
+ */
247
+
248
+ /**
249
+ * @typedef {{
250
+ * value: unknown
251
+ * disabled: import('vue').Ref<boolean>
252
+ * }} FuroSelectItemContext
253
+ */
254
+
255
+ /**
256
+ * @typedef {import('../../../contexts/BaseFuroContext.js').BaseFuroContextParams & {
257
+ * props: {}
258
+ * componentContext: import('vue').SetupContext
259
+ * refHash: {
260
+ * textComponentRef: import('vue').ShallowRef<{ $el: HTMLElement } | null>
261
+ * observerRef: import('vue').ShallowRef<MutationObserver | null>
262
+ * recordedTextRef: import('vue').Ref<string | null>
263
+ * }
264
+ * selectRootContext: FuroSelectRootContext
265
+ * selectItemContext: FuroSelectItemContext
266
+ * }} FuroSelectOptionTextContextParams
267
+ */
268
+
269
+ /**
270
+ * @typedef {Omit<FuroSelectOptionTextContextParams, 'refHash' | 'selectRootContext' | 'selectItemContext'> & {
271
+ * refHash?: FuroSelectOptionTextContextParams['refHash']
272
+ * selectRootContext?: FuroSelectRootContext
273
+ * selectItemContext?: FuroSelectItemContext
274
+ * }} FuroSelectOptionTextContextFactoryParams
275
+ */