Draw the checkbox and the selection controls' row without Tailwind
<x-checkbox> renders data-md-checkbox, its box, tick and dash, and its row, text and errors as data-md-* attributes; the 18px tick takes a size prop and a label-less box md-touch-target (plan step 36). selection.css moves into material.components as the row, text and state layer the three controls share, and each control's drawing goes to its own file: checkbox.css, radio.css, toggle.css, on the state tokens. The radio and the switch take the renamed shared hooks now and lose their layout classes in their own commits. field.js follows data-md-indeterminate. Renaming the switch's data-handle also stops selection.css matching the slider's handles, which it drew 24px above the track. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
3083fe28b2
commit
f86afc0542
@@ -21,6 +21,9 @@
|
|||||||
@import './components/textarea.css';
|
@import './components/textarea.css';
|
||||||
@import './components/select.css';
|
@import './components/select.css';
|
||||||
@import './components/file.css';
|
@import './components/file.css';
|
||||||
|
@import './components/checkbox.css';
|
||||||
|
@import './components/radio.css';
|
||||||
|
@import './components/toggle.css';
|
||||||
|
|
||||||
/* Containment */
|
/* Containment */
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* <x-checkbox>: M3's checkbox (resources/views/components/checkbox.blade.php), on the row the
|
||||||
|
* selection controls share (components/selection.css).
|
||||||
|
*
|
||||||
|
* CheckboxTokens (androidx Compose Material 3, Apache-2.0): an 18px box with a 2px on-surface-variant
|
||||||
|
* outline and 2px corners, filled with primary and holding an on-primary tick (or dash, for the
|
||||||
|
* mixed state) at the 18px icon size once it is ticked; the state layer a 40px circle around it,
|
||||||
|
* 11px past each edge. The tick grows in and fades in on the fast spatial spring. With the server's
|
||||||
|
* error the outline, the fill and the state layer are error and the tick on-error. Disabled, the
|
||||||
|
* outline and the fill are on-surface at 38% and the tick is surface. Beside a hint the box drops
|
||||||
|
* 3px, to sit on the label's first line.
|
||||||
|
*
|
||||||
|
* [data-md-checkbox] the root: the row and its errors; `class` and `style` land here
|
||||||
|
* [data-md-selection-row]
|
||||||
|
* [data-md-checkbox-box] the box: input, [data-md-checkbox-check], [data-md-checkbox-mixed]
|
||||||
|
* [data-md-selection-support] the errors, 4px under the row
|
||||||
|
*/
|
||||||
|
|
||||||
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
|
|
||||||
|
@import './selection.css';
|
||||||
|
@import './icon.css';
|
||||||
|
|
||||||
|
@layer material.components {
|
||||||
|
[data-md-checkbox] {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
flex: none;
|
||||||
|
place-items: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] > * {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row]:has([data-md-selection-hint]) [data-md-checkbox-box] {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box]::before {
|
||||||
|
inset: -11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box]:has(input:focus-visible)::before {
|
||||||
|
outline: 3px solid var(--md-sys-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid var(--md-sys-color-on-surface-variant);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] input:is(:checked, :indeterminate) {
|
||||||
|
border-color: var(--md-sys-color-primary);
|
||||||
|
background-color: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
|
||||||
|
color: var(--md-sys-color-on-primary);
|
||||||
|
opacity: 0;
|
||||||
|
scale: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
transition-property: opacity, scale;
|
||||||
|
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||||
|
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] input:checked:not(:indeterminate) ~ [data-md-checkbox-check],
|
||||||
|
[data-md-checkbox-box] input:indeterminate ~ [data-md-checkbox-mixed] {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] input {
|
||||||
|
border-color: var(--md-sys-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] input:is(:checked, :indeterminate) {
|
||||||
|
background-color: var(--md-sys-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row][data-md-invalid] [data-md-checkbox-box] :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
|
||||||
|
color: var(--md-sys-color-on-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] input:disabled {
|
||||||
|
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] input:disabled:is(:checked, :indeterminate) {
|
||||||
|
border-color: transparent;
|
||||||
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox-box] input:disabled ~ :is([data-md-checkbox-check], [data-md-checkbox-mixed]) {
|
||||||
|
color: var(--md-sys-color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-checkbox] [data-md-selection-support] {
|
||||||
|
margin-top: var(--md-sys-measurement-space50);
|
||||||
|
color: var(--md-sys-color-error);
|
||||||
|
font: var(--md-sys-typescale-body-sm);
|
||||||
|
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||||
|
font-variation-settings: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* <x-radio>: M3's radio buttons (resources/views/components/radio.blade.php), on the row the
|
||||||
|
* selection controls share (components/selection.css).
|
||||||
|
*
|
||||||
|
* RadioButtonTokens (androidx Compose Material 3, Apache-2.0): a 20px ring with a 2px
|
||||||
|
* on-surface-variant outline, primary once chosen, and a 10px primary dot that grows in on the fast
|
||||||
|
* spatial spring; the state layer a 40px circle around it. With the server's error the ring and
|
||||||
|
* the dot are error. Disabled, both are on-surface at 38%.
|
||||||
|
*
|
||||||
|
* [data-md-radio-button] the ring: input, [data-md-radio-dot]
|
||||||
|
*/
|
||||||
|
|
||||||
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
|
|
||||||
|
@import './selection.css';
|
||||||
|
|
||||||
|
@layer material.components {
|
||||||
|
[data-md-radio-button] {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
flex: none;
|
||||||
|
place-items: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button] > * {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button]::before {
|
||||||
|
inset: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button]:has(input:focus-visible)::before {
|
||||||
|
outline: 3px solid var(--md-sys-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button] input {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--md-sys-color-on-surface-variant);
|
||||||
|
border-radius: var(--md-sys-shape-corner-full);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button] [data-md-radio-dot] {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: var(--md-sys-shape-corner-full);
|
||||||
|
background-color: var(--md-sys-color-primary);
|
||||||
|
scale: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: scale var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button] input:checked {
|
||||||
|
border-color: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button] input:checked ~ [data-md-radio-dot] {
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row][data-md-invalid] [data-md-radio-button] input {
|
||||||
|
border-color: var(--md-sys-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row][data-md-invalid] [data-md-radio-button] [data-md-radio-dot] {
|
||||||
|
background-color: var(--md-sys-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button] input:disabled {
|
||||||
|
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-radio-button] input:disabled ~ [data-md-radio-dot] {
|
||||||
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,70 +1,90 @@
|
|||||||
/*
|
/*
|
||||||
* M3's selection controls: the checkbox, the radio button and the switch (CheckboxTokens,
|
* What M3's three selection controls share: the checkbox (components/checkbox.css), the radio button
|
||||||
* RadioButtonTokens, SwitchTokens, androidx Compose Material 3, Apache-2.0).
|
* (components/radio.css) and the switch (components/toggle.css) — CheckboxTokens, RadioButtonTokens,
|
||||||
|
* SwitchTokens, androidx Compose Material 3, Apache-2.0. Each control's stylesheet imports this one.
|
||||||
*
|
*
|
||||||
* Each is a real `<input>` with its appearance removed and drawn over, so focus, the keyboard
|
* Each is a real `<input>` with its appearance removed and drawn over, so focus, the keyboard
|
||||||
* (Space; the arrow keys between radios), the form value, `wire:model` and what a screen reader
|
* (Space; the arrow keys between radios), the form value, `wire:model` and what a screen reader
|
||||||
* announces stay the browser's. The row around it is a `<label>`, so its text is pressable too.
|
* announces stay the browser's. The row around it is a `<label>`, so its text is pressable too:
|
||||||
* Hooks are data attributes, not class names: `checkbox`, `radio` and `toggle` are daisyUI's, which
|
|
||||||
* the design guard rejects.
|
|
||||||
*
|
*
|
||||||
* [data-selection] the <label> row; `data-invalid` when the server has an error for it
|
* [data-md-selection-row] the <label>: the control and its text, 16px apart; with a hint
|
||||||
* [data-checkbox] the 18px box, its state layer a 40px circle around it
|
* the control aligns to the first line; `data-md-right` puts the
|
||||||
* input, [data-check], [data-mixed]
|
* control at the end; `data-md-invalid` when the server has an error
|
||||||
* [data-radio] the 20px ring and its 10px dot
|
* [data-md-checkbox-box] | [data-md-radio-button] | [data-md-switch] the control
|
||||||
* input, [data-dot]
|
* [data-md-selection-text] the label (body-large, on-surface) and the hint under it
|
||||||
* [data-switch] the 52×32 track; `data-icons` = "both" or "selected"
|
* [data-md-selection-label], [data-md-selection-hint]
|
||||||
* input, [data-handle] (with [data-on] and [data-off] icons)
|
|
||||||
*
|
*
|
||||||
* Every state layer is on-surface when unselected and primary when selected, 8% on hover (only
|
* Every state layer is on-surface when unselected and primary when selected, at M3's hover opacity
|
||||||
* where a pointer can hover) and 10% on focus and press; the focus indicator is the package's
|
* (only where a pointer can hover) and its focus and press opacities (tokens/state.css); the focus
|
||||||
* secondary ring. A disabled control is on-surface at 38% and has no state layer.
|
* indicator is the package's secondary ring. A disabled control is on-surface at 38% and has no
|
||||||
|
* state layer.
|
||||||
*
|
*
|
||||||
* The state layer is a ::before with no pointer events, so it is not a target: a labelled control
|
* The state layer is a ::before with no pointer events, so it is not a target: a labelled control
|
||||||
* is pressed anywhere along its row, and a control with no label wears the `touch-target` utility
|
* is pressed anywhere along its row, and a control with no label wears `md-touch-target`
|
||||||
* (resources/css/tokens/state.css) so its box catches presses over M3's 48px minimum.
|
* (foundation/interaction.css) so its box catches presses over M3's 48px minimum.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@layer components {
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
[data-selection] {
|
|
||||||
|
@layer material.components {
|
||||||
|
[data-md-selection-row] {
|
||||||
|
/* The state layer's tint. */
|
||||||
|
--selection-ink: var(--md-sys-color-on-surface);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--md-sys-measurement-space200);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-selection]:has(input:disabled) {
|
[data-md-selection-row]:has([data-md-selection-hint]) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row][data-md-right] {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row]:has(input:disabled) {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-selection]:has(input:disabled) [data-selection-label] {
|
[data-md-selection-text] {
|
||||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-checkbox],
|
[data-md-selection-label] {
|
||||||
[data-radio] {
|
display: block;
|
||||||
position: relative;
|
color: var(--md-sys-color-on-surface);
|
||||||
display: grid;
|
font: var(--md-sys-typescale-body-lg);
|
||||||
flex: none;
|
letter-spacing: var(--md-sys-typescale-body-lg-tracking);
|
||||||
place-items: center;
|
font-variation-settings: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-checkbox] > *,
|
[data-md-selection-hint] {
|
||||||
[data-radio] > * {
|
display: block;
|
||||||
grid-area: 1 / 1;
|
margin-top: var(--md-sys-measurement-space25);
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
font: var(--md-sys-typescale-body-sm);
|
||||||
|
letter-spacing: var(--md-sys-typescale-body-sm-tracking);
|
||||||
|
font-variation-settings: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-checkbox]::before,
|
[data-md-selection-row]:has(input:disabled) [data-md-selection-label] {
|
||||||
[data-radio]::before,
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||||
[data-handle]::before {
|
}
|
||||||
content: "";
|
|
||||||
|
:is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
||||||
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: var(--md-sys-shape-corner-full);
|
border-radius: var(--md-sys-shape-corner-full);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-checkbox] input,
|
:is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch]) input {
|
||||||
[data-radio] input,
|
|
||||||
[data-switch] input {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
cursor: inherit;
|
cursor: inherit;
|
||||||
@@ -74,275 +94,26 @@
|
|||||||
transition-timing-function: var(--md-sys-motion-effects-fast);
|
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The state layer. `--selection-ink` is what it is tinted with. */
|
[data-md-selection-row]:has(input:checked),
|
||||||
[data-selection] {
|
[data-md-selection-row]:has(input:indeterminate) {
|
||||||
--selection-ink: var(--md-sys-color-on-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-selection]:has(input:checked),
|
|
||||||
[data-selection]:has(input:indeterminate) {
|
|
||||||
--selection-ink: var(--md-sys-color-primary);
|
--selection-ink: var(--md-sys-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-selection][data-invalid] {
|
[data-md-selection-row][data-md-invalid] {
|
||||||
--selection-ink: var(--md-sys-color-error);
|
--selection-ink: var(--md-sys-color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
[data-selection]:hover:not(:has(input:disabled)) :is([data-checkbox], [data-radio], [data-handle])::before {
|
[data-md-selection-row]:hover:not(:has(input:disabled)) :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
||||||
background-color: color-mix(in srgb, var(--selection-ink) 8%, transparent);
|
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-selection]:has(input:focus-visible) :is([data-checkbox], [data-radio], [data-handle])::before,
|
[data-md-selection-row]:has(input:focus-visible) :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
||||||
[data-selection]:not(:has(input:disabled)):active :is([data-checkbox], [data-radio], [data-handle])::before {
|
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent);
|
||||||
background-color: color-mix(in srgb, var(--selection-ink) 10%, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-checkbox]:has(input:focus-visible)::before,
|
[data-md-selection-row]:not(:has(input:disabled)):active :is([data-md-checkbox-box], [data-md-radio-button], [data-md-switch-handle])::before {
|
||||||
[data-radio]:has(input:focus-visible)::before {
|
background-color: color-mix(in srgb, var(--selection-ink) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent);
|
||||||
outline: 3px solid var(--md-sys-color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Checkbox ---------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
[data-checkbox] {
|
|
||||||
width: 1.125rem;
|
|
||||||
height: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-checkbox]::before {
|
|
||||||
inset: -0.6875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-checkbox] input {
|
|
||||||
width: 1.125rem;
|
|
||||||
height: 1.125rem;
|
|
||||||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-checkbox] input:is(:checked, :indeterminate) {
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-checkbox] :is([data-check], [data-mixed]) {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
opacity: 0;
|
|
||||||
scale: 0.5;
|
|
||||||
pointer-events: none;
|
|
||||||
transition-property: opacity, scale;
|
|
||||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
|
||||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-checkbox] input:checked:not(:indeterminate) ~ [data-check],
|
|
||||||
[data-checkbox] input:indeterminate ~ [data-mixed] {
|
|
||||||
opacity: 1;
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-selection][data-invalid] [data-checkbox] input {
|
|
||||||
border-color: var(--md-sys-color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-selection][data-invalid] [data-checkbox] input:is(:checked, :indeterminate) {
|
|
||||||
background-color: var(--md-sys-color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-selection][data-invalid] [data-checkbox] :is([data-check], [data-mixed]) {
|
|
||||||
color: var(--md-sys-color-on-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-checkbox] input:disabled {
|
|
||||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-checkbox] input:disabled:is(:checked, :indeterminate) {
|
|
||||||
border-color: transparent;
|
|
||||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-checkbox] input:disabled ~ :is([data-check], [data-mixed]) {
|
|
||||||
color: var(--md-sys-color-surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Radio button ------------------------------------------------------------------------ */
|
|
||||||
|
|
||||||
[data-radio] {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-radio]::before {
|
|
||||||
inset: -0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-radio] input {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
border: 2px solid var(--md-sys-color-on-surface-variant);
|
|
||||||
border-radius: var(--md-sys-shape-corner-full);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-radio] [data-dot] {
|
|
||||||
width: 0.625rem;
|
|
||||||
height: 0.625rem;
|
|
||||||
border-radius: var(--md-sys-shape-corner-full);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
scale: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
transition: scale var(--md-sys-motion-spatial-fast-duration) var(--md-sys-motion-spatial-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-radio] input:checked {
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-radio] input:checked ~ [data-dot] {
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-selection][data-invalid] [data-radio] input {
|
|
||||||
border-color: var(--md-sys-color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-selection][data-invalid] [data-radio] [data-dot] {
|
|
||||||
background-color: var(--md-sys-color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-radio] input:disabled {
|
|
||||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-radio] input:disabled ~ [data-dot] {
|
|
||||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---- Switch -------------------------------------------------------------------------------
|
|
||||||
*
|
|
||||||
* The handle's centre is 16px from the track's start unselected and 36px selected; it is 16px
|
|
||||||
* across unselected, 24px with an icon or selected, and 28px while pressed. The positions below
|
|
||||||
* are those centres less half the size, measured inside the track's 2px outline. */
|
|
||||||
|
|
||||||
[data-switch] {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
flex: none;
|
|
||||||
width: 3.25rem;
|
|
||||||
height: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 2px solid var(--md-sys-color-outline);
|
|
||||||
border-radius: var(--md-sys-shape-corner-full);
|
|
||||||
background-color: var(--md-sys-color-surface-container-highest);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:focus-visible {
|
|
||||||
outline: 3px solid var(--md-sys-color-secondary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-handle] {
|
|
||||||
--handle: 1rem;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
inset-inline-start: calc(1rem - var(--handle) / 2);
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
width: var(--handle);
|
|
||||||
height: var(--handle);
|
|
||||||
translate: 0 -50%;
|
|
||||||
border-radius: var(--md-sys-shape-corner-full);
|
|
||||||
background-color: var(--md-sys-color-outline);
|
|
||||||
color: var(--md-sys-color-surface-container-highest);
|
|
||||||
pointer-events: none;
|
|
||||||
transition-property: inset-inline-start, width, height, background-color;
|
|
||||||
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
|
||||||
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-handle]::before {
|
|
||||||
inset: 50% auto auto 50%;
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
translate: -50% -50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-handle] > svg {
|
|
||||||
grid-area: 1 / 1;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch][data-icons="both"] [data-handle] {
|
|
||||||
--handle: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch][data-icons="both"] input:not(:checked) ~ [data-handle] [data-off],
|
|
||||||
[data-switch][data-icons] input:checked ~ [data-handle] [data-on] {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:checked {
|
|
||||||
border-color: var(--md-sys-color-primary);
|
|
||||||
background-color: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:checked ~ [data-handle] {
|
|
||||||
--handle: 1.5rem;
|
|
||||||
|
|
||||||
inset-inline-start: calc(2.25rem - var(--handle) / 2);
|
|
||||||
background-color: var(--md-sys-color-on-primary);
|
|
||||||
color: var(--md-sys-color-on-primary-container);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-selection]:active [data-switch] input:not(:disabled) ~ [data-handle] {
|
|
||||||
--handle: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
[data-selection]:hover [data-switch] input:not(:disabled) ~ [data-handle] {
|
|
||||||
background-color: var(--md-sys-color-on-surface-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-selection]:hover [data-switch] input:checked:not(:disabled) ~ [data-handle] {
|
|
||||||
background-color: var(--md-sys-color-primary-container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:focus-visible ~ [data-handle],
|
|
||||||
[data-selection]:active [data-switch] input:not(:disabled) ~ [data-handle] {
|
|
||||||
background-color: var(--md-sys-color-on-surface-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:checked:focus-visible ~ [data-handle],
|
|
||||||
[data-selection]:active [data-switch] input:checked:not(:disabled) ~ [data-handle] {
|
|
||||||
background-color: var(--md-sys-color-primary-container);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:disabled {
|
|
||||||
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
|
||||||
background-color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) 12%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:disabled ~ [data-handle] {
|
|
||||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
||||||
color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) 38%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:disabled:checked {
|
|
||||||
border-color: transparent;
|
|
||||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-switch] input:disabled:checked ~ [data-handle] {
|
|
||||||
background-color: var(--md-sys-color-surface);
|
|
||||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
/*
|
||||||
|
* <x-toggle>: M3's switch (resources/views/components/toggle.blade.php), on the row the selection
|
||||||
|
* controls share (components/selection.css). The hooks say `switch` because `toggle` is daisyUI's
|
||||||
|
* class, which the design guard rejects.
|
||||||
|
*
|
||||||
|
* SwitchTokens (androidx Compose Material 3, Apache-2.0): a 52×32 track with a 2px outline, in
|
||||||
|
* surface-container-highest when off and primary when on. The handle's centre is 16px from the
|
||||||
|
* track's start off and 36px on; it is 16px across off, 24px with an icon or on, and 28px while
|
||||||
|
* pressed, moving and growing on the fast spatial spring. Off it is outline, on-surface-variant
|
||||||
|
* while hovered, focused or pressed; on it is on-primary, primary-container in those states. The
|
||||||
|
* state layer is a 40px circle around the handle. Its icons are 16px, on-primary-container when on.
|
||||||
|
* Disabled, the track is on-surface at 12% and the handle on-surface at 38% (surface when on).
|
||||||
|
*
|
||||||
|
* [data-md-switch] the track; `data-md-icons` = "both" or "selected"
|
||||||
|
* input, [data-md-switch-handle] (with [data-md-switch-on] and [data-md-switch-off] icons)
|
||||||
|
*/
|
||||||
|
|
||||||
|
@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility;
|
||||||
|
|
||||||
|
@import './selection.css';
|
||||||
|
@import './icon.css';
|
||||||
|
|
||||||
|
@layer material.components {
|
||||||
|
/* The positions below are the handle's centres less half its size, measured inside the track's
|
||||||
|
2px outline. */
|
||||||
|
[data-md-switch] {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
flex: none;
|
||||||
|
width: 52px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 2px solid var(--md-sys-color-outline);
|
||||||
|
border-radius: var(--md-sys-shape-corner-full);
|
||||||
|
background-color: var(--md-sys-color-surface-container-highest);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:focus-visible {
|
||||||
|
outline: 3px solid var(--md-sys-color-secondary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch-handle] {
|
||||||
|
--handle: 16px;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
inset-inline-start: calc(16px - var(--handle) / 2);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: var(--handle);
|
||||||
|
height: var(--handle);
|
||||||
|
translate: 0 -50%;
|
||||||
|
border-radius: var(--md-sys-shape-corner-full);
|
||||||
|
background-color: var(--md-sys-color-outline);
|
||||||
|
color: var(--md-sys-color-surface-container-highest);
|
||||||
|
pointer-events: none;
|
||||||
|
transition-property: inset-inline-start, width, height, background-color;
|
||||||
|
transition-duration: var(--md-sys-motion-spatial-fast-duration);
|
||||||
|
transition-timing-function: var(--md-sys-motion-spatial-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch-handle]::before {
|
||||||
|
inset: 50% auto auto 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
translate: -50% -50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch-handle] > svg {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch][data-md-icons='both'] [data-md-switch-handle] {
|
||||||
|
--handle: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch][data-md-icons='both'] input:not(:checked) ~ [data-md-switch-handle] [data-md-switch-off],
|
||||||
|
[data-md-switch][data-md-icons] input:checked ~ [data-md-switch-handle] [data-md-switch-on] {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:checked {
|
||||||
|
border-color: var(--md-sys-color-primary);
|
||||||
|
background-color: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:checked ~ [data-md-switch-handle] {
|
||||||
|
--handle: 24px;
|
||||||
|
|
||||||
|
inset-inline-start: calc(36px - var(--handle) / 2);
|
||||||
|
background-color: var(--md-sys-color-on-primary);
|
||||||
|
color: var(--md-sys-color-on-primary-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row]:active [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
|
||||||
|
--handle: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
[data-md-selection-row]:hover [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
|
||||||
|
background-color: var(--md-sys-color-on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-selection-row]:hover [data-md-switch] input:checked:not(:disabled) ~ [data-md-switch-handle] {
|
||||||
|
background-color: var(--md-sys-color-primary-container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:focus-visible ~ [data-md-switch-handle],
|
||||||
|
[data-md-selection-row]:active [data-md-switch] input:not(:disabled) ~ [data-md-switch-handle] {
|
||||||
|
background-color: var(--md-sys-color-on-surface-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:checked:focus-visible ~ [data-md-switch-handle],
|
||||||
|
[data-md-selection-row]:active [data-md-switch] input:checked:not(:disabled) ~ [data-md-switch-handle] {
|
||||||
|
background-color: var(--md-sys-color-primary-container);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:disabled {
|
||||||
|
border-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||||
|
background-color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:disabled ~ [data-md-switch-handle] {
|
||||||
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||||
|
color: color-mix(in srgb, var(--md-sys-color-surface-container-highest) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:disabled:checked {
|
||||||
|
border-color: transparent;
|
||||||
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-container-opacity) * 100%), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-md-switch] input:disabled:checked ~ [data-md-switch-handle] {
|
||||||
|
background-color: var(--md-sys-color-surface);
|
||||||
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
@import './components/menu.css';
|
@import './components/menu.css';
|
||||||
|
|
||||||
/* Inputs, selection and data — leaving in step 36 */
|
/* Inputs, selection and data — leaving in step 36 */
|
||||||
@import './components/selection.css';
|
|
||||||
@import './components/search.css';
|
@import './components/search.css';
|
||||||
@import './components/datepicker.css';
|
@import './components/datepicker.css';
|
||||||
@import './components/timepicker.css';
|
@import './components/timepicker.css';
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
* in.
|
* in.
|
||||||
*
|
*
|
||||||
* `<x-checkbox indeterminate>`: HTML has no attribute for a checkbox's mixed state, only the
|
* `<x-checkbox indeterminate>`: HTML has no attribute for a checkbox's mixed state, only the
|
||||||
* `indeterminate` property, so the component marks the input `data-indeterminate` and this keeps
|
* `indeterminate` property, so the component marks the input `data-md-indeterminate` and this keeps
|
||||||
* the property in step with the mark — when the page loads, and when a render adds or removes the
|
* the property in step with the mark — when the page loads, and when a render adds or removes the
|
||||||
* mark. A person pressing the box clears the property as the browser always does; the mark stays
|
* mark. A person pressing the box clears the property as the browser always does; the mark stays
|
||||||
* until the server decides again.
|
* until the server decides again.
|
||||||
*/
|
*/
|
||||||
const GROWING = 'textarea[data-md-autogrow]'
|
const GROWING = 'textarea[data-md-autogrow]'
|
||||||
const MIXED = 'input[type="checkbox"][data-indeterminate]'
|
const MIXED = 'input[type="checkbox"][data-md-indeterminate]'
|
||||||
|
|
||||||
const growsByItself = window.CSS?.supports?.('field-sizing', 'content') ?? false
|
const growsByItself = window.CSS?.supports?.('field-sizing', 'content') ?? false
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ if (! growsByItself) {
|
|||||||
new MutationObserver((records) => {
|
new MutationObserver((records) => {
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
if (record.type === 'attributes') {
|
if (record.type === 'attributes') {
|
||||||
if (record.attributeName === 'data-indeterminate' && record.target instanceof HTMLInputElement) {
|
if (record.attributeName === 'data-md-indeterminate' && record.target instanceof HTMLInputElement) {
|
||||||
record.target.indeterminate = record.target.hasAttribute('data-indeterminate')
|
record.target.indeterminate = record.target.hasAttribute('data-md-indeterminate')
|
||||||
} else if (! growsByItself && record.target.matches(GROWING)) {
|
} else if (! growsByItself && record.target.matches(GROWING)) {
|
||||||
grow(record.target)
|
grow(record.target)
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ new MutationObserver((records) => {
|
|||||||
subtree: true,
|
subtree: true,
|
||||||
childList: true,
|
childList: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ['data-indeterminate', 'data-md-autogrow'],
|
attributeFilter: ['data-md-indeterminate', 'data-md-autogrow'],
|
||||||
})
|
})
|
||||||
|
|
||||||
markAll(document)
|
markAll(document)
|
||||||
|
|||||||
@@ -4,19 +4,20 @@
|
|||||||
The label and its hint sit beside it and are its accessible name — the whole row is the
|
The label and its hint sit beside it and are its accessible name — the whole row is the
|
||||||
`<label>`, so either can be pressed. `right` puts the box at the end of the row, for a setting
|
`<label>`, so either can be pressed. `right` puts the box at the end of the row, for a setting
|
||||||
read left to right. `indeterminate` shows the mixed state of a "select all" whose items are
|
read left to right. `indeterminate` shows the mixed state of a "select all" whose items are
|
||||||
partly ticked: HTML has no attribute for it, so the input is marked `data-indeterminate` and
|
partly ticked: HTML has no attribute for it, so the input is marked `data-md-indeterminate` and
|
||||||
resources/js/field.js keeps the property in step, on load and after every morph. Errors are
|
resources/js/field.js keeps the property in step, on load and after every morph. Errors are
|
||||||
read from the bag under the `wire:model` name. Every other attribute reaches the `<input>`
|
read from the bag under the `wire:model` name. Every other attribute reaches the `<input>`.
|
||||||
(resources/css/components/selection.css).
|
The drawing is resources/css/components/checkbox.css, on the row the three selection controls
|
||||||
|
share (resources/css/components/selection.css).
|
||||||
|
|
||||||
Without a label — a "select all" in a table header, a row's tick — the row is only the 18px box,
|
Without a label — a "select all" in a table header, a row's tick — the row is only the 18px box,
|
||||||
so the box carries `touch-target` and catches presses over M3's 48px minimum.
|
so the box carries `md-touch-target` and catches presses over M3's 48px minimum.
|
||||||
|
|
||||||
Grouping is the caller's, not this component's: M3 asks that from `expanded` (840px) a set of
|
Grouping is the caller's, not this component's: M3 asks that from `expanded` (840px) a set of
|
||||||
related checkboxes be gathered into a contained region rather than left as one long column
|
related checkboxes be gathered into a contained region rather than left as one long column
|
||||||
(docs/reference/m3/components-navigation-selection-inputs.md § Checkbox, Behaviour). Lay the
|
(docs/reference/m3/components-navigation-selection-inputs.md § Checkbox, Behaviour). Lay the
|
||||||
group out yourself — `<div class="grid gap-4 expanded:grid-cols-2">` around the checkboxes, or a
|
group out yourself — a grid of one column that takes two from `expanded`, or an `<x-card>` or
|
||||||
`<x-card>` or side sheet holding them — and give the group a heading that names what it asks. --}}
|
side sheet holding them — and give the group a heading that names what it asks. --}}
|
||||||
|
|
||||||
@props([
|
@props([
|
||||||
'label' => null,
|
'label' => null,
|
||||||
@@ -33,42 +34,34 @@
|
|||||||
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
$messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : [];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
|
<div data-md-checkbox {{ $attributes->only(['class', 'style', 'wire:key']) }}>
|
||||||
<label for="{{ $id }}" data-selection @if ($messages !== []) data-invalid @endif @class([
|
<label for="{{ $id }}" data-md-selection-row @if ($right) data-md-right @endif @if ($messages !== []) data-md-invalid @endif>
|
||||||
'flex gap-4',
|
<span data-md-checkbox-box @if (blank($label) && blank($hint)) class="md-touch-target" @endif>
|
||||||
'items-start' => filled($hint),
|
|
||||||
'items-center' => blank($hint),
|
|
||||||
'flex-row-reverse justify-between' => $right,
|
|
||||||
])>
|
|
||||||
<span data-checkbox @class([
|
|
||||||
'mt-[3px]' => filled($hint),
|
|
||||||
'touch-target' => blank($label) && blank($hint),
|
|
||||||
])>
|
|
||||||
<input
|
<input
|
||||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
{{ $attributes->except(['class', 'style', 'id', 'wire:key']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@if ($indeterminate) data-indeterminate @endif
|
@if ($indeterminate) data-md-indeterminate @endif
|
||||||
@if ($messages !== []) aria-invalid="true" aria-describedby="{{ $id }}-support" @endif
|
@if ($messages !== []) aria-invalid="true" aria-describedby="{{ $id }}-support" @endif
|
||||||
/>
|
/>
|
||||||
<x-livewire-material::icon name="check" class="size-4.5" optical="20" data-check />
|
<x-livewire-material::icon name="check" size="18" data-md-checkbox-check />
|
||||||
<x-livewire-material::icon name="remove" class="size-4.5" optical="20" data-mixed />
|
<x-livewire-material::icon name="remove" size="18" data-md-checkbox-mixed />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@if (filled($label) || filled($hint))
|
@if (filled($label) || filled($hint))
|
||||||
<span class="min-w-0">
|
<span data-md-selection-text>
|
||||||
@if (filled($label))
|
@if (filled($label))
|
||||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
|
<span data-md-selection-label>{{ $label }}</span>
|
||||||
@endif
|
@endif
|
||||||
@if (filled($hint))
|
@if (filled($hint))
|
||||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
<span data-md-selection-hint>{{ $hint }}</span>
|
||||||
@endif
|
@endif
|
||||||
</span>
|
</span>
|
||||||
@endif
|
@endif
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@if ($messages !== [])
|
@if ($messages !== [])
|
||||||
<div id="{{ $id }}-support" class="mt-1 type-body-sm text-error" role="alert">
|
<div id="{{ $id }}-support" data-md-selection-support role="alert">
|
||||||
@foreach ($messages as $message)
|
@foreach ($messages as $message)
|
||||||
<p>{{ $message }}</p>
|
<p>{{ $message }}</p>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
(600px, M3's compact/medium boundary — compact always stacks), and
|
(600px, M3's compact/medium boundary — compact always stacks), and
|
||||||
`value` for the option chosen when the page loads (with `wire:model`, Livewire sets it).
|
`value` for the option chosen when the page loads (with `wire:model`, Livewire sets it).
|
||||||
Errors are read from the bag under the `wire:model` name
|
Errors are read from the bag under the `wire:model` name
|
||||||
(resources/css/components/selection.css).
|
(resources/css/components/radio.css).
|
||||||
|
|
||||||
M3 stacks radio buttons vertically and cautions against a row at any width, so `inline` is for
|
M3 stacks radio buttons vertically and cautions against a row at any width, so `inline` is for
|
||||||
a group of two or three short labels that would waste a column, not the shape to reach for.
|
a group of two or three short labels that would waste a column, not the shape to reach for.
|
||||||
@@ -46,12 +46,12 @@
|
|||||||
@foreach ($options as $option)
|
@foreach ($options as $option)
|
||||||
@php($optionHintText = data_get($option, $optionHint))
|
@php($optionHintText = data_get($option, $optionHint))
|
||||||
|
|
||||||
<label data-selection @if ($messages !== []) data-invalid @endif @class([
|
<label data-md-selection-row @if ($messages !== []) data-md-invalid @endif @class([
|
||||||
'flex gap-4',
|
'flex gap-4',
|
||||||
'items-start' => filled($optionHintText),
|
'items-start' => filled($optionHintText),
|
||||||
'items-center' => blank($optionHintText),
|
'items-center' => blank($optionHintText),
|
||||||
])>
|
])>
|
||||||
<span data-radio @class([
|
<span data-md-radio-button @class([
|
||||||
'mt-0.5' => filled($optionHintText),
|
'mt-0.5' => filled($optionHintText),
|
||||||
'touch-target' => blank(data_get($option, $optionLabel)) && blank($optionHintText),
|
'touch-target' => blank(data_get($option, $optionLabel)) && blank($optionHintText),
|
||||||
])>
|
])>
|
||||||
@@ -64,11 +64,11 @@
|
|||||||
@checked($value !== null && (string) $value === (string) data_get($option, $optionValue))
|
@checked($value !== null && (string) $value === (string) data_get($option, $optionValue))
|
||||||
@if ($messages !== []) aria-invalid="true" @endif
|
@if ($messages !== []) aria-invalid="true" @endif
|
||||||
/>
|
/>
|
||||||
<span data-dot></span>
|
<span data-md-radio-dot></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="min-w-0">
|
<span class="min-w-0">
|
||||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ data_get($option, $optionLabel) }}</span>
|
<span class="block type-body-lg text-on-surface" data-md-selection-label>{{ data_get($option, $optionLabel) }}</span>
|
||||||
@if (filled($optionHintText))
|
@if (filled($optionHintText))
|
||||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $optionHintText }}</span>
|
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $optionHintText }}</span>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
its name — a switch without `label` needs an `aria-label`. `right` puts it at the end of the
|
its name — a switch without `label` needs an `aria-label`. `right` puts it at the end of the
|
||||||
row, where a setting's switch usually is. `icons` draws a check on the handle when on and a
|
row, where a setting's switch usually is. `icons` draws a check on the handle when on and a
|
||||||
cross when off; `icons="selected"` only the check. Every other attribute reaches the `<input>`
|
cross when off; `icons="selected"` only the check. Every other attribute reaches the `<input>`
|
||||||
(resources/css/components/selection.css).
|
(resources/css/components/toggle.css).
|
||||||
|
|
||||||
Without a label the row is only the 52×32 track, so the track carries `touch-target` and catches
|
Without a label the row is only the 52×32 track, so the track carries `touch-target` and catches
|
||||||
presses over M3's 48px minimum. --}}
|
presses over M3's 48px minimum. --}}
|
||||||
@@ -24,23 +24,23 @@
|
|||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
|
<div {{ $attributes->only(['class', 'wire:key'])->class(['min-w-0']) }}>
|
||||||
<label for="{{ $id }}" data-selection @class([
|
<label for="{{ $id }}" data-md-selection-row @class([
|
||||||
'flex gap-4',
|
'flex gap-4',
|
||||||
'items-start' => filled($hint),
|
'items-start' => filled($hint),
|
||||||
'items-center' => blank($hint),
|
'items-center' => blank($hint),
|
||||||
'flex-row-reverse justify-between' => $right,
|
'flex-row-reverse justify-between' => $right,
|
||||||
])>
|
])>
|
||||||
<span data-switch @if ($icons) data-icons="{{ $icons }}" @endif @class(['touch-target' => blank($label) && blank($hint)])>
|
<span data-md-switch @if ($icons) data-md-icons="{{ $icons }}" @endif @class(['touch-target' => blank($label) && blank($hint)])>
|
||||||
<input
|
<input
|
||||||
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
{{ $attributes->except(['class', 'id', 'wire:key']) }}
|
||||||
id="{{ $id }}"
|
id="{{ $id }}"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
role="switch"
|
role="switch"
|
||||||
/>
|
/>
|
||||||
<span data-handle>
|
<span data-md-switch-handle>
|
||||||
@if ($icons)
|
@if ($icons)
|
||||||
<x-livewire-material::icon name="check" class="size-4" optical="20" data-on />
|
<x-livewire-material::icon name="check" class="size-4" optical="20" data-md-switch-on />
|
||||||
<x-livewire-material::icon name="close" class="size-4" optical="20" data-off />
|
<x-livewire-material::icon name="close" class="size-4" optical="20" data-md-switch-off />
|
||||||
@endif
|
@endif
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
@if (filled($label) || filled($hint))
|
@if (filled($label) || filled($hint))
|
||||||
<span class="min-w-0">
|
<span class="min-w-0">
|
||||||
@if (filled($label))
|
@if (filled($label))
|
||||||
<span class="block type-body-lg text-on-surface" data-selection-label>{{ $label }}</span>
|
<span class="block type-body-lg text-on-surface" data-md-selection-label>{{ $label }}</span>
|
||||||
@endif
|
@endif
|
||||||
@if (filled($hint))
|
@if (filled($hint))
|
||||||
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
<span class="mt-0.5 block type-body-sm text-on-surface-variant">{{ $hint }}</span>
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ class FieldProbe extends Component
|
|||||||
<x-input id="off-field" label="Off" value="Not editable" disabled />
|
<x-input id="off-field" label="Off" value="Not editable" disabled />
|
||||||
<x-input id="plain-name-field" label="Name, plain" wire:model="name" />
|
<x-input id="plain-name-field" label="Name, plain" wire:model="name" />
|
||||||
<x-file id="upload-field" label="Upload" />
|
<x-file id="upload-field" label="Upload" />
|
||||||
|
<div style="display: flex; gap: 48px; padding: 24px">
|
||||||
|
<x-checkbox id="bare-check" aria-label="Select every row" />
|
||||||
|
<x-toggle id="bare-switch" aria-label="Bare switch" />
|
||||||
|
<x-radio name="bare-radio" :options="[['id' => 'only', 'name' => '']]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<x-button label="Save" wire:click="save" />
|
<x-button label="Save" wire:click="save" />
|
||||||
</div>
|
</div>
|
||||||
@@ -146,12 +151,12 @@ it('keeps a partly ticked checkbox in step with the server', function () {
|
|||||||
|
|
||||||
$page->click('label[for="file-a"]')
|
$page->click('label[for="file-a"]')
|
||||||
->assertSeeIn('#files', 'a')
|
->assertSeeIn('#files', 'a')
|
||||||
->assertScript("{$all}.hasAttribute('data-indeterminate') && {$all}.indeterminate");
|
->assertScript("{$all}.hasAttribute('data-md-indeterminate') && {$all}.indeterminate");
|
||||||
|
|
||||||
$page->click('label[for="file-b"]')
|
$page->click('label[for="file-b"]')
|
||||||
->click('label[for="file-c"]')
|
->click('label[for="file-c"]')
|
||||||
->assertSeeIn('#files', 'a,b,c')
|
->assertSeeIn('#files', 'a,b,c')
|
||||||
->assertScript("! {$all}.hasAttribute('data-indeterminate') && ! {$all}.indeterminate");
|
->assertScript("! {$all}.hasAttribute('data-md-indeterminate') && ! {$all}.indeterminate");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves between radio buttons with the arrow keys', function () {
|
it('moves between radio buttons with the arrow keys', function () {
|
||||||
@@ -289,3 +294,21 @@ it('draws the field at M3\'s geometry and the file picker\'s button as a tonal p
|
|||||||
->assertScript("getComputedStyle(document.querySelector('#upload-field'), '::file-selector-button').borderTopLeftRadius !== '0px'")
|
->assertScript("getComputedStyle(document.querySelector('#upload-field'), '::file-selector-button').borderTopLeftRadius !== '0px'")
|
||||||
->assertScript("getComputedStyle(document.querySelector('#upload-field')).color === 'rgba(0, 0, 0, 0)'");
|
->assertScript("getComputedStyle(document.querySelector('#upload-field')).color === 'rgba(0, 0, 0, 0)'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('lets a selection control without a label be pressed at the edge of its 48px target', function () {
|
||||||
|
// A press 22px from the centre, past the drawn box but inside M3's 48px, lands on the control.
|
||||||
|
$pressAtEdge = fn (string $input, string $dx, string $dy): string => "(() => {
|
||||||
|
const input = document.querySelector('{$input}');
|
||||||
|
input.scrollIntoView({ block: 'center' });
|
||||||
|
const box = input.parentElement.getBoundingClientRect();
|
||||||
|
const target = document.elementFromPoint(box.left + box.width / 2 + {$dx}, box.top + box.height / 2 + {$dy});
|
||||||
|
target.click();
|
||||||
|
|
||||||
|
return input.checked;
|
||||||
|
})()";
|
||||||
|
|
||||||
|
fieldProbe()
|
||||||
|
->assertScript($pressAtEdge('#bare-check', '22', '0'))
|
||||||
|
->assertScript($pressAtEdge('#bare-switch', '0', '22'))
|
||||||
|
->assertScript($pressAtEdge('input[name=\"bare-radio\"]', '0', '-22'));
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,38 +1,40 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
it('draws a checkbox whose row is its label', function () {
|
it('draws a checkbox whose row is its label', function () {
|
||||||
$html = (string) $this->blade('<x-checkbox id="notify" label="Notify me" hint="On every download" wire:model="notify" class="mt-2" />');
|
$html = (string) $this->blade('<x-checkbox id="notify" label="Notify me" hint="On every download" wire:model="notify" class="mt-2" style="order: 2" />');
|
||||||
|
|
||||||
expect($html)
|
expect($html)
|
||||||
->toContain('<div class="min-w-0 mt-2">')
|
->toContain('<div data-md-checkbox class="mt-2" style="order: 2">')
|
||||||
->toContain('<label for="notify" data-selection')
|
->toMatch('/<label for="notify" data-md-selection-row\s*>/')
|
||||||
->toContain('data-checkbox')
|
->toContain('<span data-md-checkbox-box >')
|
||||||
->toContain('type="checkbox"')
|
->toContain('type="checkbox"')
|
||||||
->toContain('wire:model="notify"')
|
->toContain('wire:model="notify"')
|
||||||
->toContain('data-check')
|
->toContain('data-md-checkbox-check')
|
||||||
->toContain('data-mixed')
|
->toContain('data-md-checkbox-mixed')
|
||||||
->toContain('Notify me')
|
->toContain('<span data-md-selection-label>Notify me</span>')
|
||||||
->toContain('On every download')
|
->toContain('<span data-md-selection-hint>On every download</span>')
|
||||||
->not->toContain('data-indeterminate');
|
->not->toContain('data-md-indeterminate')
|
||||||
|
->not->toContain('min-w-0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fills the box with an 18px tick from the 20 optical cut', function () {
|
it('fills the box with an 18px tick from the 20 optical cut', function () {
|
||||||
expect((string) $this->blade('<x-checkbox label="Notify me" />'))
|
expect((string) $this->blade('<x-checkbox label="Notify me" />'))
|
||||||
->toContain((string) $this->blade('<x-icon name="check" class="size-4.5" optical="20" data-check />'))
|
->toContain((string) $this->blade('<x-icon name="check" size="18" data-md-checkbox-check />'))
|
||||||
->toContain((string) $this->blade('<x-icon name="remove" class="size-4.5" optical="20" data-mixed />'));
|
->toContain((string) $this->blade('<x-icon name="remove" size="18" data-md-checkbox-mixed />'))
|
||||||
|
->toContain('--md-icon-size: 18px');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('marks a checkbox that is partly ticked', function () {
|
it('marks a checkbox that is partly ticked', function () {
|
||||||
expect((string) $this->blade('<x-checkbox label="Select all" indeterminate />'))->toContain('data-indeterminate');
|
expect((string) $this->blade('<x-checkbox label="Select all" indeterminate />'))->toContain('data-md-indeterminate');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gives a selection control without a label a 48px target', function () {
|
it('gives a selection control without a label a 48px target', function () {
|
||||||
expect((string) $this->blade('<x-checkbox aria-label="Select all" />'))->toContain('touch-target')
|
expect((string) $this->blade('<x-checkbox aria-label="Select all" />'))->toMatch('/<span data-md-checkbox-box\s+class="md-touch-target"\s*>/')
|
||||||
->and((string) $this->blade('<x-toggle aria-label="Public link" />'))->toContain('touch-target')
|
->and((string) $this->blade('<x-toggle aria-label="Public link" />'))->toContain('touch-target')
|
||||||
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'\']]" />'))->toContain('touch-target');
|
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'\']]" />'))->toContain('touch-target');
|
||||||
|
|
||||||
// A labelled control is pressed anywhere along its row, so it needs no extra target.
|
// A labelled control is pressed anywhere along its row, so it needs no extra target.
|
||||||
expect((string) $this->blade('<x-checkbox label="Notify me" />'))->not->toContain('touch-target')
|
expect((string) $this->blade('<x-checkbox label="Notify me" />'))->not->toContain('md-touch-target')
|
||||||
->and((string) $this->blade('<x-toggle label="Public link" />'))->not->toContain('touch-target')
|
->and((string) $this->blade('<x-toggle label="Public link" />'))->not->toContain('touch-target')
|
||||||
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))->not->toContain('touch-target');
|
->and((string) $this->blade('<x-radio :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))->not->toContain('touch-target');
|
||||||
});
|
});
|
||||||
@@ -52,7 +54,7 @@ it('leaves a checkbox group to the caller and says how M3 wants it laid out', fu
|
|||||||
->toContain('class="grid gap-4 expanded:grid-cols-2"')
|
->toContain('class="grid gap-4 expanded:grid-cols-2"')
|
||||||
->toContain('Downloads')
|
->toContain('Downloads')
|
||||||
->toContain('Comments')
|
->toContain('Comments')
|
||||||
->and(substr_count($html, 'data-checkbox'))->toBe(2);
|
->and(substr_count($html, 'data-md-checkbox-box'))->toBe(2);
|
||||||
|
|
||||||
// The rule is guidance, not markup, so the component's header has to carry it.
|
// The rule is guidance, not markup, so the component's header has to carry it.
|
||||||
expect(file_get_contents(__DIR__.'/../../../resources/views/components/checkbox.blade.php'))
|
expect(file_get_contents(__DIR__.'/../../../resources/views/components/checkbox.blade.php'))
|
||||||
@@ -61,7 +63,8 @@ it('leaves a checkbox group to the caller and says how M3 wants it laid out', fu
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('puts a checkbox at the end of its row on request', function () {
|
it('puts a checkbox at the end of its row on request', function () {
|
||||||
expect((string) $this->blade('<x-checkbox label="Show" right />'))->toContain('flex-row-reverse justify-between');
|
expect((string) $this->blade('<x-checkbox label="Show" right />'))->toMatch('/<label for="check-[0-9a-f]{12}" data-md-selection-row\s+data-md-right\s*>/')
|
||||||
|
->and((string) $this->blade('<x-checkbox label="Show" />'))->not->toContain('data-md-right');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a checkbox\'s errors', function () {
|
it('shows a checkbox\'s errors', function () {
|
||||||
@@ -69,7 +72,8 @@ it('shows a checkbox\'s errors', function () {
|
|||||||
->blade('<x-checkbox id="terms" label="I accept the terms" wire:model="terms" />');
|
->blade('<x-checkbox id="terms" label="I accept the terms" wire:model="terms" />');
|
||||||
|
|
||||||
expect($html)
|
expect($html)
|
||||||
->toContain('data-invalid')
|
->toContain('data-md-invalid')
|
||||||
|
->toContain('<div id="terms-support" data-md-selection-support role="alert">')
|
||||||
->toContain('aria-invalid="true"')
|
->toContain('aria-invalid="true"')
|
||||||
->toContain('aria-describedby="terms-support"')
|
->toContain('aria-describedby="terms-support"')
|
||||||
->toContain('Accept the terms to continue.');
|
->toContain('Accept the terms to continue.');
|
||||||
@@ -108,7 +112,7 @@ it('names unbound radios after their group and checks the given value', function
|
|||||||
it('shows the errors of a radio group', function () {
|
it('shows the errors of a radio group', function () {
|
||||||
expect((string) $this->withViewErrors(['audience' => ['Choose who can open it.']])
|
expect((string) $this->withViewErrors(['audience' => ['Choose who can open it.']])
|
||||||
->blade('<x-radio wire:model="audience" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))
|
->blade('<x-radio wire:model="audience" :options="[[\'id\' => \'a\', \'name\' => \'A\']]" />'))
|
||||||
->toContain('data-invalid')
|
->toContain('data-md-invalid')
|
||||||
->toContain('Choose who can open it.');
|
->toContain('Choose who can open it.');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,17 +120,31 @@ it('draws a switch on a checkbox with the switch role', function () {
|
|||||||
$html = (string) $this->blade('<x-toggle id="public" label="Public link" wire:model.live="public" right />');
|
$html = (string) $this->blade('<x-toggle id="public" label="Public link" wire:model.live="public" right />');
|
||||||
|
|
||||||
expect($html)
|
expect($html)
|
||||||
->toContain('<label for="public" data-selection')
|
->toContain('<label for="public" data-md-selection-row')
|
||||||
->toContain('data-switch')
|
->toContain('data-md-switch')
|
||||||
->toContain('role="switch"')
|
->toContain('role="switch"')
|
||||||
->toContain('type="checkbox"')
|
->toContain('type="checkbox"')
|
||||||
->toContain('wire:model.live="public"')
|
->toContain('wire:model.live="public"')
|
||||||
->toContain('data-handle')
|
->toContain('data-md-switch-handle')
|
||||||
->toContain('flex-row-reverse')
|
->toContain('flex-row-reverse')
|
||||||
->not->toContain('data-icons');
|
->not->toContain('data-md-icons');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('puts icons on a switch\'s handle', function () {
|
it('puts icons on a switch\'s handle', function () {
|
||||||
expect((string) $this->blade('<x-toggle label="Wi-Fi" icons />'))->toContain('data-icons="both"')->toContain('data-on')->toContain('data-off')
|
expect((string) $this->blade('<x-toggle label="Wi-Fi" icons />'))->toContain('data-md-icons="both"')->toContain('data-md-switch-on')->toContain('data-md-switch-off')
|
||||||
->and((string) $this->blade('<x-toggle label="Wi-Fi" icons="selected" />'))->toContain('data-icons="selected"');
|
->and((string) $this->blade('<x-toggle label="Wi-Fi" icons="selected" />'))->toContain('data-md-icons="selected"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('draws the checkbox from its stylesheet, on the row the selection controls share', function () {
|
||||||
|
$css = (string) file_get_contents(__DIR__.'/../../../resources/css/components/checkbox.css');
|
||||||
|
|
||||||
|
expect($css)->toContain("@import './selection.css';")
|
||||||
|
->toContain("@import './icon.css';")
|
||||||
|
->toContain('@layer material.components')
|
||||||
|
->toMatch('/\\[data-md-checkbox-box\\] \\{[^}]*width: 18px;[^}]*height: 18px;/')
|
||||||
|
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components/selection.css'))
|
||||||
|
->toContain('[data-md-selection-row]')
|
||||||
|
->toContain('@layer material.components')
|
||||||
|
->and((string) file_get_contents(__DIR__.'/../../../resources/css/components.css'))->toContain("@import './components/checkbox.css';")
|
||||||
|
->and((string) file_get_contents(__DIR__.'/../../../resources/css/tailwind.css'))->not->toContain('selection.css');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user