mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Merge branch 'main' into fix/b-awareness-marker-km
This commit is contained in:
commit
950d1b78ce
8 files changed
+198
-9
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
- The side panel rails show each panel as a labelled pill while no panel on
|
||||||
|
that side is open, and fold to icons once one opens. `UiRail` takes
|
||||||
|
`collapsible` for this (samehkhamis, #1501).
|
||||||
@@ -1582,6 +1582,10 @@ Imagery (while shown), Street Level and Context on the right. Selecting an item
|
|||||||
opens its panel through `setPanelCollapsed(id, false, {explicit: true})`;
|
opens its panel through `setPanelCollapsed(id, false, {explicit: true})`;
|
||||||
selecting the open panel's item closes it. The item of the open panel is
|
selecting the open panel's item closes it. The item of the open panel is
|
||||||
pressed (`aria-pressed`), and its tooltip is suppressed while the panel is open.
|
pressed (`aria-pressed`), and its tooltip is suppressed while the panel is open.
|
||||||
|
While no panel on its side is open, the rail shows each item as a labelled pill
|
||||||
|
(icon and title, `UiRail`'s `collapsible`); opening a panel folds the labels away
|
||||||
|
to the icon rail. The rail is placed by its screen-side edge, so the pills grow
|
||||||
|
toward the middle of the screen, over the closed side's empty corridor.
|
||||||
|
|
||||||
One panel is open per desktop side, in every HUD layout. Opening a side panel
|
One panel is open per desktop side, in every HUD layout. Opening a side panel
|
||||||
by any route (rail, header, keyboard, voice) collapses its peers; restored or
|
by any route (rail, header, keyboard, voice) collapses its peers; restored or
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const label = computed(() =>
|
|||||||
:side="side"
|
:side="side"
|
||||||
:label="label"
|
:label="label"
|
||||||
:panel-open="active !== null"
|
:panel-open="active !== null"
|
||||||
|
collapsible
|
||||||
@select="onSelect"
|
@select="onSelect"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -71,3 +71,75 @@ test('tips open toward the screen, and not for the open panel', async () => {
|
|||||||
await other.trigger('focusout');
|
await other.trigger('focusout');
|
||||||
assert.equal(wrapper.find('[role="tooltip"]').exists(), false);
|
assert.equal(wrapper.find('[role="tooltip"]').exists(), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a collapsible rail shows labelled pills until a panel opens', async () => {
|
||||||
|
const wrapper = mountRail({ collapsible: true });
|
||||||
|
const nav = wrapper.get('nav');
|
||||||
|
assert.ok(nav.classes().includes('ui-rail--collapsible'));
|
||||||
|
assert.ok(nav.classes().includes('ui-rail--expanded'));
|
||||||
|
assert.deepEqual(
|
||||||
|
wrapper.findAll('.ui-rail__label').map((label) => label.text()),
|
||||||
|
['DATA LAYERS', 'AUTHORING', 'CCTV'],
|
||||||
|
);
|
||||||
|
// The label is on screen, so its tip would only repeat it.
|
||||||
|
await wrapper.findAll('.ui-tooltip')[0].trigger('focusin');
|
||||||
|
assert.equal(wrapper.find('[role="tooltip"]').exists(), false);
|
||||||
|
// The button keeps its own accessible name; the visible label is hidden
|
||||||
|
// from assistive technology so the name is not read twice.
|
||||||
|
const button = wrapper.get('[data-rail-item="layers"]');
|
||||||
|
assert.equal(button.attributes('aria-label'), 'DATA LAYERS');
|
||||||
|
assert.equal(button.get('.ui-rail__label').attributes('aria-hidden'), 'true');
|
||||||
|
|
||||||
|
await wrapper.setProps({ active: 'layers', panelOpen: true });
|
||||||
|
assert.ok(nav.classes().includes('ui-rail--collapsible'));
|
||||||
|
assert.equal(nav.classes().includes('ui-rail--expanded'), false);
|
||||||
|
// Collapsed, the other items' tips come back.
|
||||||
|
await wrapper.findAll('.ui-tooltip')[1].trigger('focusin');
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 450));
|
||||||
|
assert.equal(wrapper.get('[role="tooltip"]').text(), 'AUTHORING');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a label cut short by the pill keeps its tip', async (t) => {
|
||||||
|
const observers = [];
|
||||||
|
const original = globalThis.ResizeObserver;
|
||||||
|
globalThis.ResizeObserver = class {
|
||||||
|
constructor(callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.targets = [];
|
||||||
|
observers.push(this);
|
||||||
|
}
|
||||||
|
observe(target) {
|
||||||
|
this.targets.push(target);
|
||||||
|
}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
};
|
||||||
|
t.after(() => (globalThis.ResizeObserver = original));
|
||||||
|
const wrapper = mountRail({ collapsible: true });
|
||||||
|
const [observer] = observers;
|
||||||
|
const [layers, scenes] = observer.targets;
|
||||||
|
Object.defineProperties(layers, {
|
||||||
|
scrollWidth: { value: 260 },
|
||||||
|
clientWidth: { value: 220 },
|
||||||
|
});
|
||||||
|
Object.defineProperties(scenes, {
|
||||||
|
scrollWidth: { value: 90 },
|
||||||
|
clientWidth: { value: 90 },
|
||||||
|
});
|
||||||
|
observer.callback([{ target: layers }, { target: scenes }]);
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
await wrapper.findAll('.ui-tooltip')[0].trigger('focusin');
|
||||||
|
assert.equal(wrapper.get('[role="tooltip"]').text(), 'DATA LAYERS');
|
||||||
|
await wrapper.findAll('.ui-tooltip')[0].trigger('focusout');
|
||||||
|
await wrapper.findAll('.ui-tooltip')[1].trigger('focusin');
|
||||||
|
assert.equal(wrapper.find('[role="tooltip"]').exists(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a plain rail has no labels', () => {
|
||||||
|
const wrapper = mountRail();
|
||||||
|
assert.equal(wrapper.find('.ui-rail__label').exists(), false);
|
||||||
|
assert.equal(
|
||||||
|
wrapper.get('nav').classes().includes('ui-rail--expanded'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
+58
-3
@@ -1,9 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed, onBeforeUnmount, reactive } from 'vue';
|
||||||
import UiIcon from './UiIcon.vue';
|
import UiIcon from './UiIcon.vue';
|
||||||
import UiTooltip from './UiTooltip.vue';
|
import UiTooltip from './UiTooltip.vue';
|
||||||
import { RAIL_PANEL_OPEN_TOOLTIP_DELAY_MS } from './rail.js';
|
import { RAIL_PANEL_OPEN_TOOLTIP_DELAY_MS } from './rail.js';
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
/** @type {import('vue').PropType<import('./rail.js').UiRailItem[]>} */
|
/** @type {import('vue').PropType<import('./rail.js').UiRailItem[]>} */
|
||||||
items: { type: Array, required: true },
|
items: { type: Array, required: true },
|
||||||
/** The selected item's id, or null. */
|
/** The selected item's id, or null. */
|
||||||
@@ -17,18 +18,65 @@ defineProps({
|
|||||||
* panel's title, and the other tips wait for the pointer to rest.
|
* panel's title, and the other tips wait for the pointer to rest.
|
||||||
*/
|
*/
|
||||||
panelOpen: { type: Boolean, default: false },
|
panelOpen: { type: Boolean, default: false },
|
||||||
|
/**
|
||||||
|
* Show each item as a labelled pill while no panel is open, and collapse to
|
||||||
|
* icons once one is, making room for the panel.
|
||||||
|
*/
|
||||||
|
collapsible: { type: Boolean, default: false },
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['select']);
|
const emit = defineEmits(['select']);
|
||||||
|
const expanded = computed(() => props.collapsible && !props.panelOpen);
|
||||||
|
|
||||||
|
// A label cut short by the pill's width keeps its tip, so the full name can
|
||||||
|
// still be read. Each label is measured whenever its box changes size, which
|
||||||
|
// includes the moment it settles after expanding.
|
||||||
|
const truncated = reactive(new Set());
|
||||||
|
const labelIds = new WeakMap();
|
||||||
|
const labelElements = new Map();
|
||||||
|
const labelObserver =
|
||||||
|
typeof ResizeObserver === 'function'
|
||||||
|
? new ResizeObserver((entries) => {
|
||||||
|
for (const { target } of entries) {
|
||||||
|
const id = labelIds.get(target);
|
||||||
|
if (target.scrollWidth > target.clientWidth) truncated.add(id);
|
||||||
|
else truncated.delete(id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
function trackLabel(id, element) {
|
||||||
|
const previous = labelElements.get(id);
|
||||||
|
if (previous === element) return;
|
||||||
|
if (previous) {
|
||||||
|
labelObserver?.unobserve(previous);
|
||||||
|
labelElements.delete(id);
|
||||||
|
truncated.delete(id);
|
||||||
|
}
|
||||||
|
if (!element) return;
|
||||||
|
labelElements.set(id, element);
|
||||||
|
labelIds.set(element, id);
|
||||||
|
labelObserver?.observe(element);
|
||||||
|
}
|
||||||
|
onBeforeUnmount(() => labelObserver?.disconnect());
|
||||||
|
const tipHidden = (item) =>
|
||||||
|
(expanded.value && !truncated.has(item.id)) ||
|
||||||
|
(props.panelOpen && item.id === props.active);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="ui-rail" :class="`ui-rail--${side}`" :aria-label="label">
|
<nav
|
||||||
|
class="ui-rail"
|
||||||
|
:class="[
|
||||||
|
`ui-rail--${side}`,
|
||||||
|
{ 'ui-rail--collapsible': collapsible, 'ui-rail--expanded': expanded },
|
||||||
|
]"
|
||||||
|
:aria-label="label"
|
||||||
|
>
|
||||||
<template v-for="item in items" :key="item.id">
|
<template v-for="item in items" :key="item.id">
|
||||||
<i v-if="item.separatorBefore" class="ui-rail__sep" aria-hidden="true" />
|
<i v-if="item.separatorBefore" class="ui-rail__sep" aria-hidden="true" />
|
||||||
<UiTooltip
|
<UiTooltip
|
||||||
:text="item.label"
|
:text="item.label"
|
||||||
:side="side === 'left' ? 'right' : 'left'"
|
:side="side === 'left' ? 'right' : 'left'"
|
||||||
:disabled="panelOpen && item.id === active"
|
:disabled="tipHidden(item)"
|
||||||
:delay="panelOpen ? RAIL_PANEL_OPEN_TOOLTIP_DELAY_MS : 0"
|
:delay="panelOpen ? RAIL_PANEL_OPEN_TOOLTIP_DELAY_MS : 0"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -42,6 +90,13 @@ const emit = defineEmits(['select']);
|
|||||||
@click="emit('select', item.id)"
|
@click="emit('select', item.id)"
|
||||||
>
|
>
|
||||||
<UiIcon :name="item.icon" />
|
<UiIcon :name="item.icon" />
|
||||||
|
<span
|
||||||
|
v-if="collapsible"
|
||||||
|
:ref="(element) => trackLabel(item.id, element)"
|
||||||
|
class="ui-rail__label"
|
||||||
|
aria-hidden="true"
|
||||||
|
>{{ item.label }}</span
|
||||||
|
>
|
||||||
<span v-if="item.badge !== undefined" class="ui-rail__badge">{{
|
<span v-if="item.badge !== undefined" class="ui-rail__badge">{{
|
||||||
item.badge
|
item.badge
|
||||||
}}</span>
|
}}</span>
|
||||||
|
|||||||
@@ -66,13 +66,19 @@ test('the Cyber cockpit keeps Data Layers on the left edge in every language', (
|
|||||||
|
|
||||||
test('the panel rails stay on the outer side of their stacks in right-to-left', () => {
|
test('the panel rails stay on the outer side of their stacks in right-to-left', () => {
|
||||||
const layers = sheet('layers.css');
|
const layers = sheet('layers.css');
|
||||||
// Each rail is placed against its stack's outer edge; a physical side
|
// Each rail is placed by its screen-side edge; a physical side would put
|
||||||
// would put it over the panels once the stacks swap sides.
|
// it over the panels once the stacks swap sides.
|
||||||
const left = ruleBody(layers, '.gev-panel-rail--left');
|
const left = ruleBody(layers, '.gev-panel-rail--left');
|
||||||
assert.match(left, /inset-inline-end: calc\(100% \+ 6px\);/);
|
assert.match(
|
||||||
|
left,
|
||||||
|
/inset-inline-start: calc\(-1 \* var\(--panel-rail-clearance\)\);/,
|
||||||
|
);
|
||||||
assert.doesNotMatch(left, /(?:^|\s)(?:left|right):/);
|
assert.doesNotMatch(left, /(?:^|\s)(?:left|right):/);
|
||||||
const right = ruleBody(layers, '.gev-panel-rail--right');
|
const right = ruleBody(layers, '.gev-panel-rail--right');
|
||||||
assert.match(right, /inset-inline-start: calc\(100% \+ 6px\);/);
|
assert.match(
|
||||||
|
right,
|
||||||
|
/inset-inline-end: calc\(-1 \* var\(--panel-rail-clearance\)\);/,
|
||||||
|
);
|
||||||
assert.doesNotMatch(right, /(?:^|\s)(?:left|right):/);
|
assert.doesNotMatch(right, /(?:^|\s)(?:left|right):/);
|
||||||
// Their tips open toward the middle of the screen.
|
// Their tips open toward the middle of the screen.
|
||||||
const kit = sheet('kit.css');
|
const kit = sheet('kit.css');
|
||||||
|
|||||||
@@ -116,6 +116,51 @@
|
|||||||
opacity: 0.35;
|
opacity: 0.35;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A collapsible rail shows each item as an icon and its label while no panel
|
||||||
|
is open, and folds the labels away once one is. The collapsed item keeps
|
||||||
|
the plain rail's 32px square. */
|
||||||
|
.ui-rail--collapsible {
|
||||||
|
transition: border-radius var(--transition-fast);
|
||||||
|
}
|
||||||
|
.ui-rail--collapsible .ui-rail__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 32px;
|
||||||
|
padding: 0 6px;
|
||||||
|
transition:
|
||||||
|
color var(--transition-fast),
|
||||||
|
background var(--transition-fast),
|
||||||
|
border-color var(--transition-fast),
|
||||||
|
border-radius var(--transition-fast);
|
||||||
|
}
|
||||||
|
.ui-rail__label {
|
||||||
|
max-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font: var(--dock-label-font-weight) var(--dock-label-font-size) / 1
|
||||||
|
var(--font-sans);
|
||||||
|
letter-spacing: var(--dock-label-letter-spacing);
|
||||||
|
transition:
|
||||||
|
max-width var(--transition-fast),
|
||||||
|
margin var(--transition-fast),
|
||||||
|
opacity var(--transition-fast);
|
||||||
|
}
|
||||||
|
.ui-rail--expanded {
|
||||||
|
border-radius: 21px;
|
||||||
|
}
|
||||||
|
.ui-rail--expanded .ui-rail__item {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
.ui-rail--expanded .ui-rail__label {
|
||||||
|
max-width: 220px;
|
||||||
|
margin-inline: 8px 8px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
.ui-rail__badge {
|
.ui-rail__badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1px;
|
top: 1px;
|
||||||
|
|||||||
@@ -255,11 +255,14 @@
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
transition: top var(--transition-fast);
|
transition: top var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
/* Each rail keeps its outer edge at the screen-side clearance, so a rail that
|
||||||
|
widens (labelled pills while its side has no open panel) grows toward the
|
||||||
|
middle of the screen. Collapsed, it sits 6px outside its stack. */
|
||||||
.gev-panel-rail--left {
|
.gev-panel-rail--left {
|
||||||
inset-inline-end: calc(100% + 6px);
|
inset-inline-start: calc(-1 * var(--panel-rail-clearance));
|
||||||
}
|
}
|
||||||
.gev-panel-rail--right {
|
.gev-panel-rail--right {
|
||||||
inset-inline-start: calc(100% + 6px);
|
inset-inline-end: calc(-1 * var(--panel-rail-clearance));
|
||||||
}
|
}
|
||||||
body.mobile-shell #left-panel-stack > .gev-panel-rail,
|
body.mobile-shell #left-panel-stack > .gev-panel-rail,
|
||||||
body.mobile-shell #right-context-rail > .gev-panel-rail,
|
body.mobile-shell #right-context-rail > .gev-panel-rail,
|
||||||
|
|||||||
Reference in new issue
Block a user