Repository: Textualize/textual

Issue:
`Placeholder` has no `disabled` `__init__` keyword parameter
It is intended that `disabled` is one of the "standard" keyword parameters for widgets in Textual; this seems to have never been added to `Placeholder`.


Discussion:
Not sure if a disabled placeholder makes much sense.
One would hope that it would dim, as per disabled widgets, and if you're laying things out or testing things, being able to disable from queries where eventually the `Placeholder` would be swapped out for a widget of the same class or ID is great for testing (which is exactly now I just noticed it).
Fair enough. It would be trivial to add.
As a PS: Turns out it doesn't dim; as the global dimming style seems only to be aimed at widgets that can receive focus. Might be worth taking that into account too.

Resolving patch:
diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py
--- a/src/textual/widgets/_placeholder.py
+++ b/src/textual/widgets/_placeholder.py
@@ -73,6 +73,10 @@ class Placeholder(Widget):
         content-align: center middle;
         overflow: hidden;
         color: $text;
+
+        &:disabled {
+            opacity: 0.7;
+        }
     }
     Placeholder.-text {
         padding: 1;
@@ -95,6 +99,7 @@ def __init__(
         name: str | None = None,
         id: str | None = None,
         classes: str | None = None,
+        disabled: bool = False,
     ) -> None:
         """Create a Placeholder widget.
 
@@ -106,6 +111,7 @@ def __init__(
             id: The ID of the placeholder in the DOM.
             classes: A space separated string with the CSS classes
                 of the placeholder, if any.
+            disabled: Whether the placeholder is disabled or not.
         """
         # Create and cache renderables for all the variants.
         self._renderables = {
@@ -114,7 +120,7 @@ def __init__(
             "text": "\n\n".join(_LOREM_IPSUM_PLACEHOLDER_TEXT for _ in range(5)),
         }
 
-        super().__init__(name=name, id=id, classes=classes)
+        super().__init__(name=name, id=id, classes=classes, disabled=disabled)
 
         self.variant = self.validate_variant(variant)
         """The current variant of the placeholder."""


Test patch:
diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
--- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
+++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr
@@ -27897,6 +27897,162 @@
   
   '''
 # ---
+# name: test_placeholder_disabled
+  '''
+  <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
+      <!-- Generated with Rich https://www.textualize.io -->
+      <style>
+  
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Regular"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+          font-style: normal;
+          font-weight: 400;
+      }
+      @font-face {
+          font-family: "Fira Code";
+          src: local("FiraCode-Bold"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+                  url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+          font-style: bold;
+          font-weight: 700;
+      }
+  
+      .terminal-552180741-matrix {
+          font-family: Fira Code, monospace;
+          font-size: 20px;
+          line-height: 24.4px;
+          font-variant-east-asian: full-width;
+      }
+  
+      .terminal-552180741-title {
+          font-size: 18px;
+          font-weight: bold;
+          font-family: arial;
+      }
+  
+      .terminal-552180741-r1 { fill: #c5c8c6 }
+  .terminal-552180741-r2 { fill: #e8e0e7 }
+  .terminal-552180741-r3 { fill: #aba7a8 }
+      </style>
+  
+      <defs>
+      <clipPath id="terminal-552180741-clip-terminal">
+        <rect x="0" y="0" width="975.0" height="584.5999999999999" />
+      </clipPath>
+      <clipPath id="terminal-552180741-line-0">
+      <rect x="0" y="1.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-1">
+      <rect x="0" y="25.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-2">
+      <rect x="0" y="50.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-3">
+      <rect x="0" y="74.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-4">
+      <rect x="0" y="99.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-5">
+      <rect x="0" y="123.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-6">
+      <rect x="0" y="147.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-7">
+      <rect x="0" y="172.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-8">
+      <rect x="0" y="196.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-9">
+      <rect x="0" y="221.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-10">
+      <rect x="0" y="245.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-11">
+      <rect x="0" y="269.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-12">
+      <rect x="0" y="294.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-13">
+      <rect x="0" y="318.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-14">
+      <rect x="0" y="343.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-15">
+      <rect x="0" y="367.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-16">
+      <rect x="0" y="391.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-17">
+      <rect x="0" y="416.3" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-18">
+      <rect x="0" y="440.7" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-19">
+      <rect x="0" y="465.1" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-20">
+      <rect x="0" y="489.5" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-21">
+      <rect x="0" y="513.9" width="976" height="24.65"/>
+              </clipPath>
+  <clipPath id="terminal-552180741-line-22">
+      <rect x="0" y="538.3" width="976" height="24.65"/>
+              </clipPath>
+      </defs>
+  
+      <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-552180741-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">DisabledPlaceholderApp</text>
+              <g transform="translate(26,22)">
+              <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+              <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+              <circle cx="44" cy="0" r="7" fill="#28c840"/>
+              </g>
+          
+      <g transform="translate(9, 41)" clip-path="url(#terminal-552180741-clip-terminal)">
+      <rect fill="#53174a" x="0" y="1.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="25.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="123.5" width="414.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="414.8" y="123.5" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="549" y="123.5" width="427" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#53174a" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="416.3" width="414.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="414.8" y="416.3" width="134.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="549" y="416.3" width="427" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#40222b" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
+      <g class="terminal-552180741-matrix">
+      <text class="terminal-552180741-r1" x="976" y="20" textLength="12.2" clip-path="url(#terminal-552180741-line-0)">
+  </text><text class="terminal-552180741-r1" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-552180741-line-1)">
+  </text><text class="terminal-552180741-r1" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-552180741-line-2)">
+  </text><text class="terminal-552180741-r1" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-552180741-line-3)">
+  </text><text class="terminal-552180741-r1" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-552180741-line-4)">
+  </text><text class="terminal-552180741-r2" x="414.8" y="142" textLength="134.2" clip-path="url(#terminal-552180741-line-5)">Placeholder</text><text class="terminal-552180741-r1" x="976" y="142" textLength="12.2" clip-path="url(#terminal-552180741-line-5)">
+  </text><text class="terminal-552180741-r1" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-552180741-line-6)">
+  </text><text class="terminal-552180741-r1" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-552180741-line-7)">
+  </text><text class="terminal-552180741-r1" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-552180741-line-8)">
+  </text><text class="terminal-552180741-r1" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-552180741-line-9)">
+  </text><text class="terminal-552180741-r1" x="976" y="264" textLength="12.2" clip-path="url(#terminal-552180741-line-10)">
+  </text><text class="terminal-552180741-r1" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-552180741-line-11)">
+  </text><text class="terminal-552180741-r1" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-552180741-line-12)">
+  </text><text class="terminal-552180741-r1" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-552180741-line-13)">
+  </text><text class="terminal-552180741-r1" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-552180741-line-14)">
+  </text><text class="terminal-552180741-r1" x="976" y="386" textLength="12.2" clip-path="url(#terminal-552180741-line-15)">
+  </text><text class="terminal-552180741-r1" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-552180741-line-16)">
+  </text><text class="terminal-552180741-r3" x="414.8" y="434.8" textLength="134.2" clip-path="url(#terminal-552180741-line-17)">Placeholder</text><text class="terminal-552180741-r1" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-552180741-line-17)">
+  </text><text class="terminal-552180741-r1" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-552180741-line-18)">
+  </text><text class="terminal-552180741-r1" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-552180741-line-19)">
+  </text><text class="terminal-552180741-r1" x="976" y="508" textLength="12.2" clip-path="url(#terminal-552180741-line-20)">
+  </text><text class="terminal-552180741-r1" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-552180741-line-21)">
+  </text><text class="terminal-552180741-r1" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-552180741-line-22)">
+  </text>
+      </g>
+      </g>
+  </svg>
+  
+  '''
+# ---
 # name: test_placeholder_render
   '''
   <svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
diff --git a/tests/snapshot_tests/snapshot_apps/placeholder_disabled.py b/tests/snapshot_tests/snapshot_apps/placeholder_disabled.py
new file mode 100644
--- /dev/null
+++ b/tests/snapshot_tests/snapshot_apps/placeholder_disabled.py
@@ -0,0 +1,18 @@
+from textual.app import App, ComposeResult
+from textual.widgets import Placeholder
+
+class DisabledPlaceholderApp(App[None]):
+
+    CSS = """
+    Placeholder {
+        height: 1fr;
+    }
+    """
+
+    def compose(self) -> ComposeResult:
+        yield Placeholder()
+        yield Placeholder(disabled=True)
+
+if __name__ == "__main__":
+    DisabledPlaceholderApp().run()
+
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py
--- a/tests/snapshot_tests/test_snapshots.py
+++ b/tests/snapshot_tests/test_snapshots.py
@@ -1144,3 +1144,8 @@ def test_app_blur(snap_compare):
     async def run_before(pilot) -> None:
         await pilot.pause()     # Allow the AppBlur message to get processed.
     assert snap_compare(SNAPSHOT_APPS_DIR / "app_blur.py", run_before=run_before)
+
+
+def test_placeholder_disabled(snap_compare):
+    """Test placeholder with diabled set to True."""
+    assert snap_compare(SNAPSHOT_APPS_DIR / "placeholder_disabled.py")