mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat(kit): render the pressed toggles with UiButton
UiButton gains pressedClass, Core's class on a pressed button. The pressed classes toggle through classList, so classes other code adds survive a press, a caller's class change keeps them, a renamed pressedClass leaves the button, and server-rendered markup carries them. The Display, SDR, Radio and Cockpit enable, Cockpit weather and briefing, mission replay, Street Level, TR-3B, overlay, voice and CCTV toggles now render through it with the same ids, classes and aria-pressed values.
This commit is contained in:
1 parent
5019bd9e8a
commit
d98a7c81be
23 files changed
+212
-62
No files matched your search
@@ -0,0 +1,102 @@
|
||||
import { after, before, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { installVueDom } from '../../testSupport/vueDom.mjs';
|
||||
|
||||
let mount;
|
||||
let vue;
|
||||
let renderToString;
|
||||
let UiButton;
|
||||
let uninstall;
|
||||
before(async () => {
|
||||
uninstall = installVueDom();
|
||||
({ mount } = await import('@vue/test-utils'));
|
||||
vue = await import('vue');
|
||||
({ renderToString } = await import('vue/server-renderer'));
|
||||
UiButton = (await import('./UiButton.vue')).default;
|
||||
});
|
||||
after(() => uninstall());
|
||||
|
||||
function mountButton(props = {}, attrs = {}) {
|
||||
return mount(UiButton, {
|
||||
attachTo: document.body,
|
||||
props,
|
||||
attrs,
|
||||
slots: { default: 'Go' },
|
||||
});
|
||||
}
|
||||
|
||||
test('a pressed toggle carries aria-pressed, the kit class and Core’s', async () => {
|
||||
const wrapper = mountButton(
|
||||
{ pressed: false, pressedClass: 'active' },
|
||||
{ id: 'go', class: 'pp-toggle-btn' },
|
||||
);
|
||||
const button = wrapper.get('button');
|
||||
assert.equal(button.attributes('aria-pressed'), 'false');
|
||||
assert.equal(button.classes().includes('active'), false);
|
||||
await wrapper.setProps({ pressed: true });
|
||||
assert.equal(button.attributes('aria-pressed'), 'true');
|
||||
assert.ok(button.classes().includes('active'));
|
||||
assert.ok(button.classes().includes('ui-button--pressed'));
|
||||
assert.ok(button.classes().includes('pp-toggle-btn'));
|
||||
assert.equal(button.attributes('type'), 'button');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('no aria-pressed unless pressed is passed', () => {
|
||||
const wrapper = mountButton();
|
||||
assert.equal(wrapper.get('button').attributes('aria-pressed'), undefined);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a class other code adds survives a press', async () => {
|
||||
const wrapper = mountButton({ pressed: false, pressedClass: 'active' });
|
||||
const button = wrapper.get('button').element;
|
||||
button.classList.add('voice-highlight');
|
||||
await wrapper.setProps({ pressed: true });
|
||||
await wrapper.setProps({ pressed: false });
|
||||
assert.ok(button.classList.contains('voice-highlight'));
|
||||
assert.equal(button.classList.contains('active'), false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a caller’s class change keeps the pressed classes', async () => {
|
||||
const wrapper = mountButton(
|
||||
{ pressed: true, pressedClass: 'active' },
|
||||
{ class: { panoptic: false } },
|
||||
);
|
||||
await wrapper.setProps({ class: { panoptic: true } });
|
||||
const classes = wrapper.get('button').classes();
|
||||
assert.ok(classes.includes('panoptic'));
|
||||
assert.ok(classes.includes('active'));
|
||||
assert.ok(classes.includes('ui-button--pressed'));
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('a renamed pressedClass leaves the button', async () => {
|
||||
const wrapper = mountButton({ pressed: true, pressedClass: 'active' });
|
||||
await wrapper.setProps({ pressedClass: 'selected' });
|
||||
let classes = wrapper.get('button').classes();
|
||||
assert.equal(classes.includes('active'), false);
|
||||
assert.ok(classes.includes('selected'));
|
||||
await wrapper.setProps({ pressed: false });
|
||||
classes = wrapper.get('button').classes();
|
||||
assert.equal(classes.includes('selected'), false);
|
||||
assert.equal(classes.includes('active'), false);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('server-rendered markup carries the pressed classes', async () => {
|
||||
const { createSSRApp, h } = vue;
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(
|
||||
UiButton,
|
||||
{ id: 'fm', pressed: true, pressedClass: 'active' },
|
||||
() => 'FM',
|
||||
),
|
||||
}),
|
||||
);
|
||||
assert.match(html, /<button[^>]*class="[^"]*\bactive\b[^"]*"/);
|
||||
assert.match(html, /aria-pressed="true"/);
|
||||
});
|
||||
+30
-7
@@ -1,11 +1,17 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { vClassToggle } from '../rail/classToggle.js';
|
||||
|
||||
// Core's button, with @halfpixel/ui-vue UiButton's props, classes and
|
||||
// attribute contract so the two kits can merge: the button is always the root,
|
||||
// attributes passed in win over its own (a `title` replaces the label's), and
|
||||
// `pressed` renders aria-pressed only when it is passed. Core styles the
|
||||
// button through the classes its callers pass; the kit classes carry no Core
|
||||
// styles yet. Not mirrored yet: icon, loading, disabledReason and the
|
||||
// missing-label warning.
|
||||
// styles yet. Core adds `pressedClass`, its class on a pressed button (most
|
||||
// toggles use `active`). The pressed classes toggle through `classList`, so
|
||||
// classes other code adds to the button (a voice highlight) survive a press.
|
||||
// Not mirrored yet: icon, loading, disabledReason and the missing-label
|
||||
// warning.
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = defineProps({
|
||||
@@ -14,6 +20,8 @@ const props = defineProps({
|
||||
/** The accessible name (aria-label) and, with nativeTitle, the tip. */
|
||||
label: { type: String, default: undefined },
|
||||
pressed: { type: Boolean, default: undefined },
|
||||
/** Core's class on the button while it is pressed. */
|
||||
pressedClass: { type: String, default: undefined },
|
||||
disabled: { type: Boolean, default: false },
|
||||
type: { type: String, default: 'button' },
|
||||
/** The id of the form a submit button belongs to, where it sits outside it. */
|
||||
@@ -21,6 +29,24 @@ const props = defineProps({
|
||||
nativeTitle: { type: Boolean, default: true },
|
||||
});
|
||||
const emit = defineEmits(['click']);
|
||||
|
||||
// A pressedClass that changes keeps its old name, switched off, so the class
|
||||
// leaves the button with it.
|
||||
const pressedClasses = computed((previous) => {
|
||||
const on = Boolean(props.pressed);
|
||||
const classes = { 'ui-button--pressed': on };
|
||||
for (const name of Object.keys(previous || {}))
|
||||
if (name !== 'ui-button--pressed') classes[name] = false;
|
||||
if (props.pressedClass) classes[props.pressedClass] = on;
|
||||
return classes;
|
||||
});
|
||||
// Server-rendered markup carries the pressed classes in its class attribute.
|
||||
const vPressedClasses = {
|
||||
...vClassToggle,
|
||||
getSSRProps: ({ value }) => ({
|
||||
class: Object.keys(value).filter((name) => value[name]),
|
||||
}),
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -28,11 +54,7 @@ const emit = defineEmits(['click']);
|
||||
:type="props.type"
|
||||
:form="props.form"
|
||||
class="ui-button ui-focus"
|
||||
:class="[
|
||||
`ui-button--${props.variant}`,
|
||||
`ui-button--${props.tone}`,
|
||||
{ 'ui-button--pressed': props.pressed },
|
||||
]"
|
||||
:class="[`ui-button--${props.variant}`, `ui-button--${props.tone}`]"
|
||||
:aria-label="props.label"
|
||||
:title="props.nativeTitle ? props.label : undefined"
|
||||
:aria-pressed="
|
||||
@@ -40,6 +62,7 @@ const emit = defineEmits(['click']);
|
||||
"
|
||||
:disabled="props.disabled"
|
||||
v-bind="$attrs"
|
||||
v-pressed-classes="pressedClasses"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<slot />
|
||||
|
||||
Reference in new issue
Block a user