feat(kit): render the chips with UiChip and UiButton

UiChip is a non-interactive label chip and renders the voice
card's chips. The pressable chips and pills are pressed toggles, so the
rail card chips, map source chips and city and place pills render through
UiButton with pressedClass, keeping their classes, attributes and the
classes other code adds. The pills now report aria-pressed.
This commit is contained in:
Sameh Khamis committed 2026-11-01 12:09:38 -08:00
1 parent d98a7c81be
commit 28ca784d49
10 files changed
+81 -16

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
import { after, before, test } from 'node:test';
import assert from 'node:assert/strict';
import { installVueDom } from '../../testSupport/vueDom.mjs';
let mount;
let UiChip;
let uninstall;
before(async () => {
uninstall = installVueDom();
({ mount } = await import('@vue/test-utils'));
UiChip = (await import('./UiChip.vue')).default;
});
after(() => uninstall());
test('a chip is a span with the kit classes and Core’s', async () => {
const wrapper = mount(UiChip, {
attrs: { class: 'gev-voice-card-chip', 'data-tone': 'ok' },
slots: { default: 'ok' },
});
const chip = wrapper.get('span');
assert.deepEqual(chip.classes(), ['ui-chip', 'gev-voice-card-chip']);
assert.equal(chip.attributes('data-tone'), 'ok');
assert.equal(chip.text(), 'ok');
await wrapper.setProps({ selected: true, mono: true });
assert.ok(chip.classes().includes('ui-chip--selected'));
assert.ok(chip.classes().includes('ui-mono'));
wrapper.unmount();
});
+19
View File
@@ -0,0 +1,19 @@
<script setup>
// Core's chip, with @halfpixel/ui-vue UiChip's props and classes so the two
// kits can merge: a label that says what something is, never a control.
// Core styles it through the classes its callers pass; the kit classes carry
// no Core styles yet. A chip that presses is a UiButton with pressedClass.
defineProps({
selected: { type: Boolean, default: false },
mono: { type: Boolean, default: false },
});
</script>
<template>
<span
class="ui-chip"
:class="{ 'ui-chip--selected': selected, 'ui-mono': mono }"
>
<slot />
</span>
</template>
+7 -5
View File
@@ -1,4 +1,6 @@
<script setup>
import UiButton from '../kit/UiButton.vue';
// Keyed chips shared by rail cards; the owner decides what a click does.
defineProps({
chips: { type: Array, default: () => [] },
@@ -6,23 +8,23 @@ defineProps({
});
const chipClass = (chip) =>
`data-toggle-chip chip-${chip.state || (chip.active ? 'active' : 'idle')}${chip.active ? ' active' : ''}`;
`data-toggle-chip chip-${chip.state || (chip.active ? 'active' : 'idle')}`;
</script>
<template>
<button
<UiButton
v-for="chip in chips"
:key="chip.id"
type="button"
:data-chip-id="chip.id"
:class="chipClass(chip)"
:pressed="Boolean(chip.active)"
pressed-class="active"
:style="chip.color ? `--chip-color: ${chip.color}` : undefined"
:data-chip-color="chip.color || undefined"
:title="chip.title || undefined"
:disabled="Boolean(chip.disabled)"
:aria-pressed="String(Boolean(chip.active))"
:aria-busy="String(Boolean(chip.busy))"
@click="onSelect(chip)"
>{{ chip.label }}</button
>{{ chip.label }}</UiButton
>
</template>
+3 -3
View File
@@ -117,7 +117,7 @@ function selectMapSource(chip) {
<span data-i18n id="map-source-label">MAP SOURCE</span>
<span id="map-stack-status" class="pp-stack-status" v-class-toggle="{ warn: !!mapSource?.status.warn }" aria-live="polite">{{ mapSource ? mapSource.status.text : '3D' }}</span>
</div>
<div id="map-stack-chips" class="map-stack-chip-row" role="group" aria-label="Map source" data-i18n-aria-label><button v-for="chip in mapSource?.chips" :key="chip.id" type="button" class="map-stack-chip" v-class-toggle="{ active: chip.active, unavailable: !chip.available }" :data-stack-id="chip.id" :title="chip.title" :aria-pressed="String(chip.active)" :aria-disabled="String(!chip.available)" :aria-label="chip.ariaLabel" @click="selectMapSource(chip)"><span class="map-stack-chip-label">{{ chip.label }}</span><span v-if="chip.requirement" class="map-stack-chip-req">{{ chip.requirement }}</span></button></div>
<div id="map-stack-chips" class="map-stack-chip-row" role="group" aria-label="Map source" data-i18n-aria-label><UiButton v-for="chip in mapSource?.chips" :key="chip.id" class="map-stack-chip" :pressed="Boolean(chip.active)" pressed-class="active" v-class-toggle="{ unavailable: !chip.available }" :data-stack-id="chip.id" :title="chip.title" :aria-disabled="String(!chip.available)" :aria-label="chip.ariaLabel" @click="selectMapSource(chip)"><span class="map-stack-chip-label">{{ chip.label }}</span><span v-if="chip.requirement" class="map-stack-chip-req">{{ chip.requirement }}</span></UiButton></div>
</section>
</div>
<div id="style-mini-status" class="panel-mini-status" aria-live="polite">
@@ -145,10 +145,10 @@ function selectMapSource(chip) {
</div>
<div id="location-bar-popover" class="dock-popover-content">
<UiButton variant="icon" class="dock-pin-btn" data-pin-target="location-bar" :label="t('Pin location tray')" :title="t('Keep location tray open')" :pressed="!!panels['location-bar']?.pinned" @click="pin($event, 'location-bar')"><img src="/pin.svg" alt="" /></UiButton>
<div id="poi-row" class="poi-row-container" v-class-toggle="{ expanded: !!loc?.poisExpanded }"><button v-for="poi in loc?.pois" :key="`${loc.poiCity}:${poi.index}`" type="button" class="poi-pill" v-class-toggle="{ active: poi.active }" :data-poi-index="poi.index" @click="locationControls?.choosePoi(poi.index)"><span class="poi-pill-key">{{ poi.key }}</span><span class="poi-pill-name">{{ poi.name }}</span></button></div>
<div id="poi-row" class="poi-row-container" v-class-toggle="{ expanded: !!loc?.poisExpanded }"><UiButton v-for="poi in loc?.pois" :key="`${loc.poiCity}:${poi.index}`" class="poi-pill" :pressed="Boolean(poi.active)" pressed-class="active" :data-poi-index="poi.index" @click="locationControls?.choosePoi(poi.index)"><span class="poi-pill-key">{{ poi.key }}</span><span class="poi-pill-name">{{ poi.name }}</span></UiButton></div>
<div class="location-bar-divider" id="location-bar-divider" v-class-toggle="{ visible: !!loc?.poisExpanded }"></div>
<div class="location-city-row">
<div id="location-pills" class="location-pills"><button v-for="city in loc?.cities" :key="city.id" type="button" class="location-pill" v-class-toggle="{ active: city.active }" :data-location-id="city.id" @click="locationControls?.chooseCity(city.id)">{{ city.label }}</button></div>
<div id="location-pills" class="location-pills"><UiButton v-for="city in loc?.cities" :key="city.id" class="location-pill" :pressed="Boolean(city.active)" pressed-class="active" :data-location-id="city.id" @click="locationControls?.chooseCity(city.id)">{{ city.label }}</UiButton></div>
<div class="location-search-wrap">
<button id="search-toggle" class="search-toggle-btn" title="Search any location" data-i18n-title>
<span>&#x1F50E;</span>
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup>
import { t } from '../../../i18n/index.js';
import { ASSISTANT_NAME } from '../../../voice/control.js';
import UiChip from '../../kit/UiChip.vue';
// Captions, plan and result, as VoiceCardControls (src/voice/voiceCard.js)
// presents them. Captions update silently; the polite live region carries
@@ -22,7 +23,7 @@ const emit = defineEmits(['dismiss']);
<ol id="gev-voice-card-plan" class="gev-voice-card-plan" :aria-label="t('Plan')" :hidden="card.steps.length === 0"><li v-for="(step, index) in card.steps" :key="step.key ?? index" class="gev-voice-card-step" :data-status="step.status"><span class="gev-voice-card-step-label">{{ step.label }}</span><span class="gev-voice-card-sr">{{ step.statusText ? `, ${step.statusText}` : '' }}</span><span v-if="step.detail" class="gev-voice-card-step-detail">{{ step.detail }}</span></li></ol>
<div id="gev-voice-card-result" class="gev-voice-card-result" :hidden="!card.result.visible">
<div id="gev-voice-card-result-title" class="gev-voice-card-result-title">{{ card.result.title }}</div>
<div id="gev-voice-card-chips" class="gev-voice-card-chips" :hidden="card.result.chips.length === 0"><span v-for="(chip, index) in card.result.chips" :key="index" class="gev-voice-card-chip" :data-tone="chip">{{ chip }}</span></div>
<div id="gev-voice-card-chips" class="gev-voice-card-chips" :hidden="card.result.chips.length === 0"><UiChip v-for="(chip, index) in card.result.chips" :key="index" class="gev-voice-card-chip" :data-tone="chip">{{ chip }}</UiChip></div>
<ul id="gev-voice-card-lines" class="gev-voice-card-lines"><li v-for="(line, index) in card.result.lines" :key="index">{{ line }}</li></ul>
<ol id="gev-voice-card-referents" class="gev-voice-card-referents" :aria-label="t('Referenced items')" :hidden="card.result.referents.length === 0"><li v-for="(ref, index) in card.result.referents" :key="index" :value="ref.n">{{ ref.label }}</li></ol>
<details id="gev-voice-card-notes" class="gev-voice-card-notes" :hidden="card.result.notes.length === 0">