Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client-app/src/admin/AppModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
FetchApiTestPanel,
GridTestPanel,
gridScrolling,
InputSizingTestPanel,
LocalDateTestPanel,
PanelResizingTestPanel,
SelectTestPanel,
Expand Down Expand Up @@ -52,6 +53,7 @@ export class AppModel extends HoistAdminAppModel {
{name: 'fetchAPI', path: '/fetchAPI'},
{name: 'grid', path: '/grid'},
{name: 'gridScrolling', path: '/gridScrolling'},
{name: 'inputSizing', path: '/inputSizing'},
{name: 'localDate', path: '/localDate'},
{name: 'panelResizing', path: '/panelResizing'},
{name: 'select', path: '/select'},
Expand Down Expand Up @@ -92,6 +94,7 @@ export class AppModel extends HoistAdminAppModel {
{id: 'fetchAPI', title: 'Fetch API', content: FetchApiTestPanel},
{id: 'grid', title: 'Grid', content: GridTestPanel},
{id: 'gridScrolling', content: gridScrolling},
{id: 'inputSizing', title: 'Input Sizing', content: InputSizingTestPanel},
{id: 'localDate', title: 'LocalDate API', content: LocalDateTestPanel},
{id: 'panelResizing', content: PanelResizingTestPanel},
{id: 'select', content: SelectTestPanel},
Expand Down
1 change: 1 addition & 0 deletions client-app/src/admin/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './dataview/DataViewTestPanel';
export * from './fetch/FetchApiTestPanel';
export * from './grids/GridTestPanel';
export * from './gridScrolling/GridScrolling';
export * from './inputSizing/InputSizingTestPanel';
export * from './localDate/LocalDateTestPanel';
export * from './panels/PanelResizingTestPanel';
export * from './select/SelectTestPanel';
Expand Down
100 changes: 100 additions & 0 deletions client-app/src/admin/tests/inputSizing/InputSizingTestModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {HoistModel} from '@xh/hoist/core';
import {bindable, makeObservable} from '@xh/hoist/mobx';
import {LocalDate} from '@xh/hoist/utils/datetime';

// Sentinel string indicating "don't pass this prop at all" (vs. passing it with value `null`).
export const UNSET = '__unset';

// Seed CSS loaded into the editor when the override is first enabled - matches the most
// common dev workaround attempted when inputs don't stretch: force `width: 100%` on the
// Hoist-input root class.
const DEFAULT_CSS_OVERRIDE = `/* Selectors here are auto-scoped to the specimens container. */
.xh-input { width: 100% !important; }
`;

type ContainerShape = 'block' | 'hbox' | 'vbox';

/**
* Model for the Input Sizing Test page.
*
* Drives three layers of control:
* 1. Outer container shape/size - so specimens render in a realistic layout context.
* 2. Layout props applied to every specimen (width, flex, minWidth, maxWidth) - so we can
* verify whether each HoistInput honors them uniformly.
* 3. A "pure CSS override" switch - simulates the user applying CSS that targets
* `.xh-input` in an attempt to stretch the component. Lets us see which inputs can
* actually be stretched via CSS alone today.
*/
export class InputSizingTestModel extends HoistModel {
@bindable containerShape: ContainerShape = 'hbox';
@bindable containerWidth = 600;
@bindable containerHeight = 400;
@bindable containerGap = 12;

// Layout prop controls — each holds either UNSET (don't pass) or a string form of the value.
@bindable widthVal: string = UNSET;
@bindable flexVal: string = UNSET;
@bindable minWidthVal: string = UNSET;
@bindable maxWidthVal: string = UNSET;

// CSS override experiments - free-form CSS text, auto-scoped via CSS nesting to
// `.tb-input-sizing-test__specimen-container` so selectors only affect specimens.
@bindable cssOverrideEnabled = false;
@bindable cssOverrideText: string = DEFAULT_CSS_OVERRIDE;

// Per-specimen value bindings
@bindable textVal = 'hello';
@bindable numberVal: number = 42;
@bindable selectVal: string = 'AZ';
@bindable pickerVal: string = 'AZ';
@bindable multiPickerVal: string[] = ['AZ', 'CA'];
@bindable.ref dateVal: LocalDate = null;
@bindable textAreaVal = 'first line\nsecond line';
@bindable codeVal = '{\n "hello": 1\n}';
@bindable jsonVal = '{"a": 1, "b": [2, 3]}';
@bindable sliderVal: number = 50;
@bindable segVal: string = 'a';
@bindable btnGrpVal: string = 'a';

constructor() {
super();
makeObservable(this);
}

/**
* Resolve the current control values into a plain object of layout props to spread onto
* every specimen. Omits any prop set to UNSET so the component's own default applies.
*/
get layoutProps(): Record<string, any> {
const out: Record<string, any> = {};
out.width = this.resolveWidthLike(this.widthVal, out);
out.flex = this.resolveNumLike(this.flexVal);
out.minWidth = this.resolveNumLike(this.minWidthVal);
out.maxWidth = this.resolveNumLike(this.maxWidthVal);
// Strip UNSET sentinel
for (const k of Object.keys(out)) if (out[k] === UNSET) delete out[k];
return out;
}

resetLayoutProps() {
this.widthVal = UNSET;
this.flexVal = UNSET;
this.minWidthVal = UNSET;
this.maxWidthVal = UNSET;
}

private resolveWidthLike(v: string, _out: any): any {
if (v === UNSET) return UNSET;
if (v === 'null') return null;
if (v.endsWith('%')) return v;
const n = Number(v);
return isNaN(n) ? UNSET : n;
}

private resolveNumLike(v: string): any {
if (v === UNSET) return UNSET;
if (v === 'null') return null;
const n = Number(v);
return isNaN(n) ? UNSET : n;
}
}
124 changes: 124 additions & 0 deletions client-app/src/admin/tests/inputSizing/InputSizingTestPanel.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
.tb-input-sizing-test {
&__body {
flex: 1;
overflow: hidden;
}

&__controls {
width: 300px;
min-width: 300px;
padding: var(--xh-pad-px);
border-right: var(--xh-border-solid);
background-color: var(--xh-bg-alt);
gap: var(--xh-pad-px);
}

&__control-group {
padding: var(--xh-pad-px);
border: var(--xh-border-solid);
border-radius: var(--xh-border-radius-px);
background-color: var(--xh-bg);
gap: var(--xh-pad-half-px);
}

&__control-group-title {
font-weight: 600;
font-size: var(--xh-font-size-small-px);
text-transform: uppercase;
color: var(--xh-text-color-muted);
letter-spacing: 0.04em;
margin-bottom: 2px;
}

&__labeled-control {
align-items: center;
gap: var(--xh-pad-half-px);

.label {
font-family: var(--xh-font-family-mono);
font-size: var(--xh-font-size-small-px);
color: var(--xh-text-color-muted);
}
}

&__help,
&__footnote {
font-size: var(--xh-font-size-small-px);
color: var(--xh-text-color-muted);
line-height: 1.35;
margin: 4px 0 0;
}

&__specimens {
padding: var(--xh-pad-px);
gap: var(--xh-pad-px);
}

&__summary {
padding: var(--xh-pad-half-px) var(--xh-pad-px);
background-color: var(--xh-bg-alt);
border: var(--xh-border-solid);
border-radius: var(--xh-border-radius-px);
font-family: var(--xh-font-family-mono);
font-size: var(--xh-font-size-small-px);
color: var(--xh-text-color-muted);
flex: none;
}

&__specimen {
padding: var(--xh-pad-half-px);
border: var(--xh-border-solid);
border-radius: var(--xh-border-radius-px);
background-color: var(--xh-bg);
gap: 4px;
flex: none;
}

&__specimen-header {
align-items: baseline;
padding-bottom: 2px;
border-bottom: 1px dashed var(--xh-border-color);

.name {
font-family: var(--xh-font-family-mono);
font-size: var(--xh-font-size-px);
font-weight: 600;
}

.default {
font-size: var(--xh-font-size-small-px);
color: var(--xh-text-color-muted);
font-style: italic;
}
}

&__specimen-wrap {
padding: var(--xh-pad-half-px) 0;
}

&__specimen-container {
outline: 1px dashed var(--xh-intent-primary);
outline-offset: 2px;
background-color: color-mix(in srgb, var(--xh-intent-primary) 4%, transparent);
padding: 6px;

&.shape-hbox {
// hbox layout props from Hoist already drive display:flex
}

&.shape-vbox {
// vbox layout props from Hoist already drive display:flex column
}
}

&__specimen-metrics {
font-family: var(--xh-font-family-mono);
font-size: var(--xh-font-size-small-px);
color: var(--xh-text-color-muted);

&--mismatch {
color: var(--xh-intent-danger);
font-weight: 500;
}
}
}
Loading
Loading