@conduction/nextcloud-vue 2.65.0 → 2.66.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.
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +99 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +438 -383
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +78 -21
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +129 -96
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +63 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +2 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/mixins/pageViews.js +400 -0
- package/dist/esm/mixins/pageViews.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +18 -0
- package/dist/esm/utils/actionsDispatcher.js +11 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +27 -3
- package/dist/esm/utils/columnFilters.js.map +1 -1
- package/dist/esm/utils/schema.js +40 -2
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1521 -596
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +18 -0
- package/l10n/en.json +3 -0
- package/l10n/nl.json +3 -0
- package/package.json +1 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +165 -8
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
- package/src/components/CnDetailPage/CnDetailPage.vue +94 -5
- package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +67 -8
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
- package/src/css/detail-page.css +5 -0
- package/src/mixins/pageViews.js +396 -0
- package/src/schemas/app-manifest-v2.schema.json +29 -1
- package/src/utils/actionsDispatcher.js +11 -2
- package/src/utils/columnFilters.js +29 -2
- package/src/utils/schema.js +40 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -513,6 +513,7 @@ import { draftKey, formDraftMixin, readDraft } from '../../composables/useFormDr
|
|
|
513
513
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
514
514
|
import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
|
|
515
515
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
516
|
+
import { resolveCreateOverrideHandler } from '../../utils/actionsDispatcher.js'
|
|
516
517
|
import {
|
|
517
518
|
definitionQueryParams,
|
|
518
519
|
DYNAMIC_KEY_PREFIX,
|
|
@@ -757,6 +758,18 @@ export default {
|
|
|
757
758
|
* identity when used standalone (no CnAppRoot ancestor).
|
|
758
759
|
*/
|
|
759
760
|
cnTranslate: { default: () => (key) => key },
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* The app manifest, the v2 registry, the legacy customComponents map and
|
|
764
|
+
* the registry-modal opener, all provided by CnAppRoot. A select-or-create
|
|
765
|
+
* picker reads them to create an object the way the app's own index page
|
|
766
|
+
* does (`config.createOverride` / `config.createModal`). Standalone (no
|
|
767
|
+
* CnAppRoot) they are absent and the generic nested form is used.
|
|
768
|
+
*/
|
|
769
|
+
cnManifest: { default: null },
|
|
770
|
+
cnRegistry: { default: null },
|
|
771
|
+
cnCustomComponents: { default: null },
|
|
772
|
+
cnOpenModal: { default: null },
|
|
760
773
|
},
|
|
761
774
|
|
|
762
775
|
props: {
|
|
@@ -2649,6 +2662,16 @@ export default {
|
|
|
2649
2662
|
store.registerObjectType(slug, schemaRef, register)
|
|
2650
2663
|
}
|
|
2651
2664
|
const labelField = this.referenceLabelField(field)
|
|
2665
|
+
// The app may own how this schema is created (pipelinq's client and
|
|
2666
|
+
// contact need a contact-first endpoint; a plain save 400s). A
|
|
2667
|
+
// create override keeps this form and saves through the app's
|
|
2668
|
+
// handler; a create modal (only when no override exists) opens the
|
|
2669
|
+
// app's own dialog instead of the generic form.
|
|
2670
|
+
const appCreate = this.appCreateFor(register, schemaRef)
|
|
2671
|
+
if (!appCreate.override && appCreate.modal) {
|
|
2672
|
+
return this.openAppCreateModal(appCreate.modal, store, slug, labelField, term)
|
|
2673
|
+
}
|
|
2674
|
+
const override = appCreate.override
|
|
2652
2675
|
let schema = null
|
|
2653
2676
|
try {
|
|
2654
2677
|
schema = await store.fetchSchema(slug)
|
|
@@ -2656,14 +2679,113 @@ export default {
|
|
|
2656
2679
|
schema = null
|
|
2657
2680
|
}
|
|
2658
2681
|
if (!schema || !schema.properties) {
|
|
2659
|
-
|
|
2682
|
+
const payload = { [labelField]: term }
|
|
2683
|
+
return override
|
|
2684
|
+
? override(payload, { register, schema: schemaRef, objectType: slug, effectiveSchema: null })
|
|
2685
|
+
: store.saveObject(slug, payload)
|
|
2660
2686
|
}
|
|
2661
2687
|
const initialData = {}
|
|
2662
2688
|
if (term && schema.properties[labelField]) {
|
|
2663
2689
|
initialData[labelField] = term
|
|
2664
2690
|
}
|
|
2665
2691
|
return new Promise((resolve) => {
|
|
2666
|
-
this.nestedCreate = { field, schema, register, slug, initialData, resolve }
|
|
2692
|
+
this.nestedCreate = { field, schema, register, slug, schemaRef, initialData, override, resolve }
|
|
2693
|
+
})
|
|
2694
|
+
},
|
|
2695
|
+
|
|
2696
|
+
/**
|
|
2697
|
+
* How the app creates objects of a schema, read off the manifest page
|
|
2698
|
+
* that lists them: the index page whose `config.schema` (and, when it
|
|
2699
|
+
* names one, `config.register`) matches. Its `config.createOverride` is
|
|
2700
|
+
* resolved to the registered handler; its `config.createModal` is the
|
|
2701
|
+
* registry key of a `kind: 'modal'` dialog.
|
|
2702
|
+
*
|
|
2703
|
+
* @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
|
|
2704
|
+
* @param {string} register The reference's register.
|
|
2705
|
+
* @param {string} schemaRef The reference's schema slug or id.
|
|
2706
|
+
* @return {{override: (((formData: object, ctx: object) => Promise<object>)|null), modal: (string|null)}} What the app declared.
|
|
2707
|
+
*/
|
|
2708
|
+
appCreateFor(register, schemaRef) {
|
|
2709
|
+
const none = { override: null, modal: null }
|
|
2710
|
+
const pages = this.cnManifest && Array.isArray(this.cnManifest.pages) ? this.cnManifest.pages : []
|
|
2711
|
+
const wanted = String(schemaRef).toLowerCase()
|
|
2712
|
+
const page = pages.find((p) => {
|
|
2713
|
+
const config = p && p.config
|
|
2714
|
+
if (!config || config.schema === undefined || config.schema === null) {
|
|
2715
|
+
return false
|
|
2716
|
+
}
|
|
2717
|
+
if (String(config.schema).toLowerCase() !== wanted) {
|
|
2718
|
+
return false
|
|
2719
|
+
}
|
|
2720
|
+
return !config.register || String(config.register) === String(register)
|
|
2721
|
+
})
|
|
2722
|
+
if (!page) {
|
|
2723
|
+
return none
|
|
2724
|
+
}
|
|
2725
|
+
const config = page.config
|
|
2726
|
+
const override = typeof config.createOverride === 'function'
|
|
2727
|
+
? config.createOverride
|
|
2728
|
+
: resolveCreateOverrideHandler(config.createOverride, this.cnRegistry, this.cnCustomComponents)
|
|
2729
|
+
const modalEntry = typeof config.createModal === 'string' && config.createModal !== ''
|
|
2730
|
+
? (this.cnRegistry || {})[config.createModal]
|
|
2731
|
+
: null
|
|
2732
|
+
const modal = modalEntry && modalEntry.kind === 'modal' && typeof this.cnOpenModal === 'function'
|
|
2733
|
+
? config.createModal
|
|
2734
|
+
: null
|
|
2735
|
+
return { override: override || null, modal }
|
|
2736
|
+
},
|
|
2737
|
+
|
|
2738
|
+
/**
|
|
2739
|
+
* Open the app's own create dialog (a registry modal) for a picker and
|
|
2740
|
+
* wait for it. The dialog reports the new object (or its id) with a
|
|
2741
|
+
* `created` event; `close` without one resolves null so the field stays
|
|
2742
|
+
* as it was. A reported id is fetched so the picker can label it.
|
|
2743
|
+
*
|
|
2744
|
+
* @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
|
|
2745
|
+
* @param {string} key The registry key of the modal.
|
|
2746
|
+
* @param {object} store The object store.
|
|
2747
|
+
* @param {string} slug The object type slug.
|
|
2748
|
+
* @param {string} labelField The property the typed term belongs in.
|
|
2749
|
+
* @param {string} term The typed search term.
|
|
2750
|
+
* @return {Promise<object|null>} The created object, or null.
|
|
2751
|
+
*/
|
|
2752
|
+
openAppCreateModal(key, store, slug, labelField, term) {
|
|
2753
|
+
return new Promise((resolve) => {
|
|
2754
|
+
let settled = false
|
|
2755
|
+
let reported = false
|
|
2756
|
+
const settle = (value) => {
|
|
2757
|
+
if (!settled) {
|
|
2758
|
+
settled = true
|
|
2759
|
+
resolve(value)
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
this.cnOpenModal(key, {
|
|
2763
|
+
initialData: term ? { [labelField]: term } : {},
|
|
2764
|
+
onCreated: async (created) => {
|
|
2765
|
+
if (created && typeof created === 'object') {
|
|
2766
|
+
settle(created)
|
|
2767
|
+
return
|
|
2768
|
+
}
|
|
2769
|
+
if (typeof created !== 'string' && typeof created !== 'number') {
|
|
2770
|
+
return
|
|
2771
|
+
}
|
|
2772
|
+
reported = true
|
|
2773
|
+
let obj
|
|
2774
|
+
try {
|
|
2775
|
+
obj = await store.fetchObject(slug, String(created))
|
|
2776
|
+
} catch {
|
|
2777
|
+
obj = null
|
|
2778
|
+
}
|
|
2779
|
+
settle(obj || { id: String(created), [labelField]: term })
|
|
2780
|
+
},
|
|
2781
|
+
onClose: () => {
|
|
2782
|
+
// A dialog often emits `close` right after `created`,
|
|
2783
|
+
// while the id is still being fetched: that fetch settles.
|
|
2784
|
+
if (!reported) {
|
|
2785
|
+
settle(null)
|
|
2786
|
+
}
|
|
2787
|
+
},
|
|
2788
|
+
})
|
|
2667
2789
|
})
|
|
2668
2790
|
},
|
|
2669
2791
|
|
|
@@ -2682,10 +2804,28 @@ export default {
|
|
|
2682
2804
|
return
|
|
2683
2805
|
}
|
|
2684
2806
|
const store = this.getObjectStore()
|
|
2685
|
-
|
|
2807
|
+
let created = null
|
|
2808
|
+
let thrown = null
|
|
2809
|
+
if (pending.override) {
|
|
2810
|
+
// The app's create override owns persistence (same contract as
|
|
2811
|
+
// CnIndexPage's `createOverride`).
|
|
2812
|
+
try {
|
|
2813
|
+
created = await pending.override(formData, {
|
|
2814
|
+
register: pending.register,
|
|
2815
|
+
schema: pending.schemaRef,
|
|
2816
|
+
objectType: pending.slug,
|
|
2817
|
+
effectiveSchema: pending.schema,
|
|
2818
|
+
})
|
|
2819
|
+
} catch (err) {
|
|
2820
|
+
thrown = (err && err.response && err.response.data && err.response.data.error) || (err && err.message) || null
|
|
2821
|
+
}
|
|
2822
|
+
} else {
|
|
2823
|
+
created = store ? await store.saveObject(pending.slug, formData) : null
|
|
2824
|
+
}
|
|
2686
2825
|
if (!created) {
|
|
2687
2826
|
const error = store && store.errors ? store.errors[pending.slug] : null
|
|
2688
|
-
const message =
|
|
2827
|
+
const message = thrown
|
|
2828
|
+
|| (!pending.override && error && error.message)
|
|
2689
2829
|
|| t('nextcloud-vue', 'The item could not be created.')
|
|
2690
2830
|
const dialog = this.$refs.nestedCreateDialog
|
|
2691
2831
|
if (dialog && typeof dialog.setValidationErrors === 'function') {
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
</a>
|
|
36
36
|
</div>
|
|
37
37
|
<!-- A view switch at the right of the heading (`content.views`): "My
|
|
38
|
-
work / My team" as a segmented control whose options are routes
|
|
38
|
+
work / My team" as a segmented control whose options are routes,
|
|
39
|
+
or views of the page the widget sits on (`option.view`); a view
|
|
40
|
+
option controls the page's view region. -->
|
|
39
41
|
<div v-if="viewOptions.length > 0" class="cn-header-widget__views">
|
|
40
42
|
<CnSegmentedControl
|
|
41
43
|
:options="viewOptions"
|
|
42
44
|
:modelValue="activeView"
|
|
43
45
|
:ariaLabel="viewsLabel"
|
|
46
|
+
:controls="viewsRegionId"
|
|
44
47
|
data-testid="cn-header-widget-views"
|
|
45
48
|
@update:modelValue="onViewChange" />
|
|
46
49
|
</div>
|
|
@@ -66,6 +69,9 @@ const ALLOWED_TEXT_ALIGN = ['left', 'center', 'right']
|
|
|
66
69
|
const ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom']
|
|
67
70
|
const ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost']
|
|
68
71
|
|
|
72
|
+
/** Marks a switch option that selects a page view rather than a route. */
|
|
73
|
+
const VIEW_OPTION_PREFIX = 'view:'
|
|
74
|
+
|
|
69
75
|
const HEIGHT_PIXELS = Object.freeze({
|
|
70
76
|
small: 120,
|
|
71
77
|
medium: 200,
|
|
@@ -121,6 +127,12 @@ export default {
|
|
|
121
127
|
* untranslated key renders as itself.
|
|
122
128
|
*/
|
|
123
129
|
cnTranslate: { default: () => (key) => key },
|
|
130
|
+
/**
|
|
131
|
+
* The views of the page this widget sits on, provided by
|
|
132
|
+
* CnDashboardPage / CnDetailPage (`views`). An option with a `view`
|
|
133
|
+
* selects one of them. Null outside such a page.
|
|
134
|
+
*/
|
|
135
|
+
cnPageViews: { default: null },
|
|
124
136
|
},
|
|
125
137
|
|
|
126
138
|
props: {
|
|
@@ -131,9 +143,12 @@ export default {
|
|
|
131
143
|
* greeting, showDate, plain, ground, views}`. All fields are optional except
|
|
132
144
|
* `title` (or `greeting`); unknown enum values collapse to documented
|
|
133
145
|
* defaults and the renderer never throws. `views` is
|
|
134
|
-
* `{ ariaLabel?, options: [{ label, route, params? }] }`:
|
|
135
|
-
* control at the right of the heading whose checked option
|
|
136
|
-
* current route; choosing another pushes its route.
|
|
146
|
+
* `{ ariaLabel?, options: [{ label, route, params? } | { label, view }] }`:
|
|
147
|
+
* a segmented control at the right of the heading whose checked option
|
|
148
|
+
* is the current route; choosing another pushes its route. An option
|
|
149
|
+
* with `view` selects that view of the page the widget sits on (the
|
|
150
|
+
* page's `views`) instead, and the page then draws no switch of its
|
|
151
|
+
* own. `ground: true`
|
|
137
152
|
* draws the greeting on the page ground: the plain look with no
|
|
138
153
|
* padding, a 32px heading and the date line 6px above it (a dashboard
|
|
139
154
|
* also drops the widget's card for it).
|
|
@@ -229,12 +244,45 @@ export default {
|
|
|
229
244
|
*/
|
|
230
245
|
viewOptions() {
|
|
231
246
|
const raw = this.content && this.content.views && this.content.views.options
|
|
232
|
-
if (!Array.isArray(raw)
|
|
247
|
+
if (!Array.isArray(raw)) {
|
|
233
248
|
return []
|
|
234
249
|
}
|
|
250
|
+
const pageViewIds = this.pageViewIds
|
|
235
251
|
return raw
|
|
236
|
-
.filter((o) => o && typeof o.label === 'string' && o.label !== ''
|
|
237
|
-
.map((o) =>
|
|
252
|
+
.filter((o) => o && typeof o.label === 'string' && o.label !== '')
|
|
253
|
+
.map((o) => {
|
|
254
|
+
if (typeof o.view === 'string' && o.view !== '') {
|
|
255
|
+
return pageViewIds.includes(o.view) ? { value: VIEW_OPTION_PREFIX + o.view, label: this.cnTranslate(o.label) } : null
|
|
256
|
+
}
|
|
257
|
+
if (typeof o.route === 'string' && o.route !== '' && this.$router) {
|
|
258
|
+
return { value: o.route, label: this.cnTranslate(o.label) }
|
|
259
|
+
}
|
|
260
|
+
return null
|
|
261
|
+
})
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The ids of the page's views, empty outside a page with views.
|
|
267
|
+
*
|
|
268
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views
|
|
269
|
+
* @return {string[]}
|
|
270
|
+
*/
|
|
271
|
+
pageViewIds() {
|
|
272
|
+
const views = this.cnPageViews && Array.isArray(this.cnPageViews.views) ? this.cnPageViews.views : []
|
|
273
|
+
return views.map((view) => view.id)
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The id of the page's view region when an option selects a view,
|
|
278
|
+
* for the options' `aria-controls`; '' otherwise.
|
|
279
|
+
*
|
|
280
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
|
|
281
|
+
* @return {string}
|
|
282
|
+
*/
|
|
283
|
+
viewsRegionId() {
|
|
284
|
+
const hasViewOption = this.viewOptions.some((o) => o.value.startsWith(VIEW_OPTION_PREFIX))
|
|
285
|
+
return hasViewOption && this.cnPageViews ? (this.cnPageViews.regionId || '') : ''
|
|
238
286
|
},
|
|
239
287
|
|
|
240
288
|
/**
|
|
@@ -249,6 +297,11 @@ export default {
|
|
|
249
297
|
if (options.length === 0) {
|
|
250
298
|
return null
|
|
251
299
|
}
|
|
300
|
+
const activeView = this.cnPageViews ? this.cnPageViews.activeId : null
|
|
301
|
+
const viewMatch = activeView ? options.find((o) => o.value === VIEW_OPTION_PREFIX + activeView) : null
|
|
302
|
+
if (viewMatch) {
|
|
303
|
+
return viewMatch.value
|
|
304
|
+
}
|
|
252
305
|
const current = this.$route && this.$route.name
|
|
253
306
|
const match = options.find((o) => o.value === current)
|
|
254
307
|
return match ? match.value : options[0].value
|
|
@@ -263,7 +316,7 @@ export default {
|
|
|
263
316
|
*/
|
|
264
317
|
viewsLabel() {
|
|
265
318
|
const label = this.content && this.content.views && this.content.views.ariaLabel
|
|
266
|
-
return label ? this.cnTranslate(label) : t('nextcloud-vue', '
|
|
319
|
+
return label ? this.cnTranslate(label) : t('nextcloud-vue', 'Views')
|
|
267
320
|
},
|
|
268
321
|
|
|
269
322
|
/**
|
|
@@ -697,6 +750,12 @@ export default {
|
|
|
697
750
|
* @return {void}
|
|
698
751
|
*/
|
|
699
752
|
onViewChange(route) {
|
|
753
|
+
if (typeof route === 'string' && route.startsWith(VIEW_OPTION_PREFIX)) {
|
|
754
|
+
if (this.cnPageViews && typeof this.cnPageViews.select === 'function') {
|
|
755
|
+
this.cnPageViews.select(route.slice(VIEW_OPTION_PREFIX.length))
|
|
756
|
+
}
|
|
757
|
+
return
|
|
758
|
+
}
|
|
700
759
|
if (!this.$router || !route || route === (this.$route && this.$route.name)) {
|
|
701
760
|
return
|
|
702
761
|
}
|
|
@@ -43,4 +43,4 @@ export default {
|
|
|
43
43
|
</script>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Props: `options` lists the choices, `modelValue` is the chosen value (use `v-model`), `aria-label` names the group (or point `aria-labelledby` at a visible heading), and `stretch` fills the width.
|
|
46
|
+
Props: `options` lists the choices, `modelValue` is the chosen value (use `v-model`), `aria-label` names the group (or point `aria-labelledby` at a visible heading), `controls` names the id of the element the options switch (set as `aria-controls` on every option), and `stretch` fills the width.
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
:aria-checked="isChecked(option) ? 'true' : 'false'"
|
|
21
21
|
:tabindex="index === focusableIndex ? 0 : -1"
|
|
22
22
|
:disabled="option.disabled || null"
|
|
23
|
+
:aria-controls="controls || null"
|
|
23
24
|
:data-value="option.key"
|
|
24
25
|
@click="select(option)">
|
|
25
26
|
<!-- @slot option Replaces the content of one option. -->
|
|
@@ -95,6 +96,17 @@ export default {
|
|
|
95
96
|
default: '',
|
|
96
97
|
},
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Id of the element the options switch, such as the region holding a
|
|
101
|
+
* page view's widgets. Set as `aria-controls` on every option.
|
|
102
|
+
*
|
|
103
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
|
|
104
|
+
*/
|
|
105
|
+
controls: {
|
|
106
|
+
type: String,
|
|
107
|
+
default: '',
|
|
108
|
+
},
|
|
109
|
+
|
|
98
110
|
/** Stretch the control to the full width, with options of equal width. */
|
|
99
111
|
stretch: {
|
|
100
112
|
type: Boolean,
|
package/src/css/detail-page.css
CHANGED