Skip to content

Commit d22cc4b

Browse files
fluent-next: drop the blanket disabled dim
The theme no longer dims a disabled widget wholesale. Every component with a surface of its own paints the state from the disabled roles, which is what Fluent specifies and how Blazor's themes are built - they carry no global rule either and keep opacity for a composite dimmed as a whole, such as a scheduler appointment. What stays in the shared rule is the part that is not a colour: no text selection, no pointer cursor on something that cannot be used. This is a fluent-next-only change. Each theme applies the dim from its own widget/_index.scss and base merely defines the mixin, so generic, material and fluent are untouched - fluent still measures 0.38 on a disabled widget. ColorView was the one component the dim was really carrying. It cannot be greyed out, its content is the colour, so it follows Blazor's colour palette: the tile rings, the handles and the captions take the disabled roles and the swatches are left alone. ContextMenu and ActionSheet got the same treatment as the other item lists. Measured on the comparison page, now 59 cases covering the whole base the dim used to reach - 33 components were dimmed by it in legacy. Four of the 59 render no disabled state, and three are Splitter, Drawer and Box: layout containers with no surface, which is also what Blazor does, since their content is made of components that mark themselves. Not verified: ContextMenu and ActionSheet render into portals, so the gallery measures their host rather than their content and cannot confirm them. Their rules are in the bundle; they need a look on real demos. The gate that asserted the dim exists is inverted: no rule may apply an opacity through a selector that reaches any widget at all. It matches on the shape of the selector, so a component-scoped rule like the TimeView reset is not caught.
1 parent 5b3048b commit d22cc4b

10 files changed

Lines changed: 96 additions & 10 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@use "../../../_design-system/variables/ds" as ds;
2+
3+
$action-sheet-content-disabled: ds.$color-content-disabled !default;

packages/devextreme-scss/scss/widgets/fluent-next/actionSheet/_index.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@
1616
margin: 0;
1717
margin-top: var(--dx-action-sheet-item-margin-block-end);
1818
}
19+
20+
/* The sheet's items are buttons that are not disabled themselves, so their text is painted here
21+
rather than through the button tier, which belongs to the button component. */
22+
.dx-actionsheet.dx-state-disabled,
23+
.dx-state-disabled .dx-actionsheet {
24+
.dx-button-text,
25+
.dx-actionsheet-title {
26+
color: var(--dx-action-sheet-content-disabled);
27+
}
28+
}

packages/devextreme-scss/scss/widgets/fluent-next/actionSheet/_public.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@use "sizes" as *;
33

44
@mixin publish {
5+
--dx-action-sheet-content-disabled: #{$action-sheet-content-disabled};
56
--dx-action-sheet-item-margin: #{$action-sheet-item-margin};
67
--dx-action-sheet-item-margin-block-end: #{$action-sheet-item-margin-block-end};
78
}

packages/devextreme-scss/scss/widgets/fluent-next/colorView/_colors.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@ $color-view-border: ds.$color-border !default;
44
$color-view-label-content: ds.$color-content-subtle !default;
55
$color-view-handle-content: ds.$color-bg !default;
66
$color-view-handle-border: ds.$color-border !default;
7+
8+
/* A disabled palette cannot be greyed out: its content is the colour. Blazor marks the tile
9+
outlines and the captions instead (ds-themes/components/color-palette/states.scss) and leaves the
10+
swatches untouched; these three do the same here. */
11+
$color-view-border-disabled: ds.$color-border-disabled !default;
12+
$color-view-label-content-disabled: ds.$color-content-disabled !default;
13+
$color-view-handle-border-disabled: ds.$color-border-disabled !default;

packages/devextreme-scss/scss/widgets/fluent-next/colorView/_index.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,22 @@
110110
}
111111
}
112112
}
113+
114+
.dx-colorview-container.dx-state-disabled,
115+
.dx-state-disabled .dx-colorview-container {
116+
label {
117+
color: var(--dx-color-view-label-content-disabled);
118+
}
119+
120+
.dx-colorview-palette-cell,
121+
.dx-colorview-alpha-channel-border,
122+
.dx-colorview-hue-scale-wrapper,
123+
.dx-colorview-color-preview-container {
124+
box-shadow: 0 0 0 1px var(--dx-color-view-border-disabled); // dx-line-width: 1px hairline ring drawn as a shadow
125+
}
126+
127+
.dx-colorview-palette-handle,
128+
.dx-colorview-hue-scale-handle {
129+
border-color: var(--dx-color-view-handle-border-disabled);
130+
}
131+
}

packages/devextreme-scss/scss/widgets/fluent-next/colorView/_public.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
@use "sizes" as *;
33

44
@mixin publish {
5+
--dx-color-view-border-disabled: #{$color-view-border-disabled};
6+
--dx-color-view-handle-border-disabled: #{$color-view-handle-border-disabled};
7+
--dx-color-view-label-content-disabled: #{$color-view-label-content-disabled};
58
--dx-color-view-alpha-margin: #{$color-view-alpha-margin};
69
--dx-color-view-alpha-width: #{$color-view-alpha-width};
710
--dx-color-view-bg-box-palette-cell-bg: var(--dx-surface-overlay);

packages/devextreme-scss/scss/widgets/fluent-next/contextMenu/_index.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,14 @@
8181
background-color: menuColors.$menu-separator-bg;
8282
}
8383
}
84+
85+
/* A disabled context menu had nothing of its own once the blanket dim was gone: its items are
86+
plain markup, not widgets that mark themselves. */
87+
.dx-context-menu.dx-state-disabled,
88+
.dx-state-disabled .dx-context-menu {
89+
.dx-menu-item-text,
90+
.dx-menu-item-content,
91+
.dx-menu-item-popout {
92+
color: var(--dx-context-menu-content-disabled);
93+
}
94+
}

packages/devextreme-scss/scss/widgets/fluent-next/widget/_index.scss

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,25 @@
22
@use "sizes" as *;
33
@use "../common/mixins" as *;
44
@use "../../base/widget";
5+
@use "../../base/mixins" as *;
56

67
.dx-widget {
78
@include dx-base-typography();
89
}
910

11+
/* No blanket dim. Every component that has a surface of its own paints its disabled state from
12+
the disabled roles, the way Fluent does it and the way Blazor's themes do - they carry no global
13+
rule either, and reserve opacity for a composite that is dimmed as a whole, such as a scheduler
14+
appointment. What is left here is the part that is not a colour: no text selection and no pointer
15+
cursor on something that cannot be used. Layout containers - splitter, drawer, box - show nothing
16+
of their own, which is also what Blazor does: their content is made of components that mark
17+
themselves. The remaining opacity users are named and listed in DISABLED_STATES.md. */
1018
.dx-state-disabled {
1119
&.dx-widget,
1220
.dx-widget {
13-
.dx-widget {
14-
opacity: 1;
15-
}
21+
@include user-select(none);
1622

17-
@include disabled-widget();
23+
cursor: default;
1824
}
1925
}
2026

packages/devextreme-scss/tests/disabled-paint.test.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,25 @@ describe.each(bundleNames)('%s', (bundleName) => {
175175
expect(findBorrowedNames(css)).toEqual([]);
176176
});
177177

178-
test('the blanket dim is still in force for components that do not paint', () => {
179-
const blanket = rules.find((r) => /\.dx-state-disabled\.dx-widget(,|\{|$)/.test(`${r.selector}{`)
180-
&& /opacity\s*:\s*var\(--dx-global-disabled-opacity\)/.test(r.body));
181-
182-
expect(blanket).toBeDefined();
178+
// The theme has no blanket dim any more: every component with a surface of its own paints its
179+
// disabled state from the roles, the way Fluent specifies it and the way Blazor's themes are
180+
// built. A rule that dims whatever it finds would put that back and hide the next gap.
181+
test('no rule dims every widget at once', () => {
182+
// A blanket selector is one that reaches any widget at all: nothing but the state class and
183+
// .dx-widget. A component-scoped rule such as `.dx-timeview .dx-state-disabled .dx-widget`
184+
// is not one, and neither is anything that resets the opacity back to 1.
185+
const isBlanket = (selector: string): boolean => selector
186+
.split(',')
187+
.map((part) => part.trim())
188+
.some((part) => /^\.dx-state-disabled(\.dx-widget)?( \.dx-widget)?$/.test(part));
189+
190+
const dimming = rules
191+
.filter((r) => isBlanket(r.selector))
192+
.filter((r) => /(^|;)\s*opacity\s*:\s*([^;]+)/.test(r.body)
193+
&& !/(^|;)\s*opacity\s*:\s*1\s*(;|$)/.test(r.body))
194+
.map((r) => r.selector);
195+
196+
expect(dimming).toEqual([]);
183197
});
184198
});
185199

packages/devextreme/playground/disabled-readonly-compare-frame.html

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,24 @@
104104
['treeList', 'TreeList', 190, (h, f) => $(h).dxTreeList({ dataSource: [{ id: 1, head: 0, name: 'Root' }, { id: 2, head: 1, name: 'Child' }], keyExpr: 'id', parentIdExpr: 'head', autoExpandAll: true, showBorders: true, ...f })],
105105
['cardView', 'CardView', 200, (h, f) => $(h).dxCardView({ dataSource: [{ id: 1, name: 'Alpha' }], keyExpr: 'id', headerPanel: { visible: true }, ...f })],
106106
['pivotGrid', 'PivotGrid', 200, (h, f) => $(h).dxPivotGrid({ dataSource: { fields: [{ dataField: 'a', area: 'row' }, { dataField: 'v', area: 'data', summaryType: 'sum' }], store: [{ a: 'x', v: 1 }, { a: 'y', v: 2 }] }, height: 170, ...f })],
107-
['recurrenceEditor', 'RecurrenceEditor', 200, (h, f) => $(h).dxRecurrenceEditor({ value: 'FREQ=WEEKLY;BYDAY=MO', startDate: new Date(2026, 0, 5), ...f })],
108107
['splitter', 'Splitter', 130, (h, f) => $(h).dxSplitter({ height: 100, items: [{ text: 'One' }, { text: 'Two' }], ...f })],
109108
['scheduler', 'Scheduler', 300, (h, f) => $(h).dxScheduler({ dataSource: [{ text: 'Event', startDate: new Date(2026, 0, 5, 10), endDate: new Date(2026, 0, 5, 11) }], currentDate: new Date(2026, 0, 5), currentView: 'day', height: 260, ...f })],
110109
['fileManager', 'FileManager', 300, (h, f) => $(h).dxFileManager({ fileSystemProvider: [{ name: 'Folder', isDirectory: true, items: [{ name: 'file.txt', size: 10 }] }], height: 260, ...f })],
111110
['drawer', 'Drawer', 150, (h, f) => $(h).dxDrawer({ opened: true, height: 120, template: () => $('<div>').text('panel'), ...f })],
112111
['box', 'Box', 90, (h, f) => $(h).dxBox({ direction: 'row', height: 60, items: [{ ratio: 1, text: 'One' }, { ratio: 1, text: 'Two' }], ...f })],
112+
113+
// --- the rest of what the blanket dim used to reach, so its whole base is visible here ----
114+
['scrollView', 'ScrollView', 130, (h, f) => { h.innerHTML = '<div style="height:200px">scrollable content</div>'; $(h).dxScrollView({ height: 100, ...f }); }],
115+
['loadIndicator', 'LoadIndicator', 80, (h, f) => $(h).dxLoadIndicator({ visible: true, ...f })],
116+
['speedDialAction', 'SpeedDialAction', 110, (h, f) => $(h).dxSpeedDialAction({ icon: 'add', ...f })],
117+
['contextMenu', 'ContextMenu', 140, (h, f) => $(h).dxContextMenu({ items: [{ text: 'One' }, { text: 'Two' }], visible: true, target: h, ...f })],
118+
['actionSheet', 'ActionSheet', 130, (h, f) => $(h).dxActionSheet({ items: [{ text: 'One' }, { text: 'Two' }], usePopover: false, ...f })],
119+
// Markup-only blocks: the dim reached them through .dx-widget just like a real widget.
120+
['badge', 'Badge (markup)', 70, (h, f) => { h.innerHTML = '<span class="dx-badge">7</span>'; h.classList.add('dx-widget'); if (f.disabled) h.classList.add('dx-state-disabled'); }],
121+
['card', 'Card (markup)', 90, (h, f) => { h.innerHTML = '<div class="dx-card" style="width:180px;padding:8px">Card</div>'; h.classList.add('dx-widget'); if (f.disabled) h.classList.add('dx-state-disabled'); }],
122+
['informer', 'Informer (markup)', 80, (h, f) => { h.innerHTML = '<div class="dx-informer"><div class="dx-informer-icon"></div>Informer</div>'; h.classList.add('dx-widget'); if (f.disabled) h.classList.add('dx-state-disabled'); }],
123+
['fieldset', 'Fieldset (markup)', 110, (h, f) => { h.innerHTML = '<div class="dx-fieldset"><div class="dx-field"><div class="dx-field-label">Label</div><div class="dx-field-value-static">Value</div></div></div>'; h.classList.add('dx-widget'); if (f.disabled) h.classList.add('dx-state-disabled'); }],
124+
['splitterBar', 'SplitterBar (markup)', 70, (h, f) => { h.innerHTML = '<div class="dx-splitter-bar"><div class="dx-splitter-bar-icon"></div></div>'; h.classList.add('dx-widget'); if (f.disabled) h.classList.add('dx-state-disabled'); }],
113125
];
114126

115127
// readonly is not a thing for every component: only those with .dx-state-readonly rules

0 commit comments

Comments
 (0)