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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 56 additions & 183 deletions src/components/checkbox/checkbox.scss
Original file line number Diff line number Diff line change
@@ -1,218 +1,91 @@
/**
* :::important
* The `CheckboxTemplate` can be imported and used in the HTML of
* other components, to render a non-functional and decorative checkbox in
* their UI. An example of this is the list component.
* This means the content of `CheckboxTemplate` will become a part of the
* consumer's DOM structure.
*
* Additionally, the consumer components' also need to import the current `.scss`
* file into their own styles file, for the checkbox to be rendered correctly!
* This means, if the styles in this file are not "specific" enough,
* there is a risk that the consumer component's styles are affected by
* our styles here.
*
* For instance if the consumer has a `<label>` or `<svg>` element,
* it might unintentionally inherit styles from the checkbox; unless we
* make the such styles more specific here.
*
* Naturally, we cannot mitigate all sorts of potential styling problems.
* The consumer component should be aware of this issue too.
* But we can ensure that our styles here both make sense,
* are readable, and are as specific as possible to avoid unintended side effects.
* :::
*/

@use '../../style/mixins';

/**
* @prop --checkbox-unchecked-border-color: Affects the border color of the default state of the checkbox (when it is not checked). Defaults to `--contrast-900`.
*/
$box-size: 1.25rem;
$gap-size: 0.5rem;

@forward '../../style/internal/boolean-input.scss';

:host(limel-checkbox) {
min-height: var(--limel-checkbox-min-height, 2.5rem); // prevents flickering
// when switching between `readonly` and normal states in `limel-checkbox`,
// but not where `CheckboxTemplate` is imported & used.
}

*,
*:before,
*:after {
box-sizing: border-box;
}

.checkbox {
position: relative;
isolation: isolate;

display: flex;
align-items: center;

min-height: var(
--limel-checkbox-min-height,
2.5rem
); // helps align with other fields in the form, or within table rows
width: 100%;
}

input[type='checkbox'] {
// Hide the native checkbox
@include mixins.visually-hidden;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}

label {
// Ensure the label is always clickable, even when empty
min-width: $box-size;
min-height: $box-size;
padding-top: 0.125rem;
// ====

cursor: pointer;
position: relative;
width: 100%;

font-size: var(--limel-theme-default-small-font-size);
color: var(--limel-theme-text-primary-on-background-color);

padding-left: calc($box-size + $gap-size);

.disabled:not([readonly]):not([readonly='true']) & {
cursor: not-allowed;
color: var(--limel-theme-text-disabled-color);
}

.required & {
&:after {
margin-left: 0.0625rem;
content: '*';
}
}

.invalid:not(.readonly) & {
color: var(--limel-theme-error-text-color);
}

:host(limel-checkbox.hide-label) &,
.hide-label & {
// this helper class of `hide-label` can be added for example to the `host` element,
// or if the `CheckboxTemplate` is imported to other components, the class can be
// added to the `checkbox` element itself
// and be used internally by other components such as `limel-table`
// to hide the `<label>`, while still keeping the checkbox
// both clickable for the users, and accessible for screen readers
@include mixins.truncate-text();
opacity: 0;
width: $box-size;
}
}

.box {
position: absolute; // since `label` is the clickable part,
// and thus needs to
// stretch below the checkbox
pointer-events: none;

transition:
border-color 0.4s ease 0.2s,
background-color 0.2s ease,
box-shadow var(--limel-clickable-transform-speed, 0.4s) ease;

display: inline-block;
vertical-align: middle;

width: $box-size;
height: $box-size;

margin-right: $gap-size;
border-radius: 0.25rem;
border: 0.125rem solid;

border-color: var(
--checkbox-unchecked-border-color,
rgb(var(--contrast-900))
);
background-color: var(
--limel-checkbox-background-color,
rgb(var(--contrast-300))
);

.checked &,
.checkbox:has(input[type='checkbox']:checked) & {
background-color: var(
--lime-primary-color,
var(--limel-theme-primary-color)
);
border-color: var(
--lime-primary-color,
var(--limel-theme-primary-color)
);
}

.disabled & {
opacity: 0.4;
}

.checkbox:not(.disabled):has(label:hover) & {
will-change: box-shadow;
box-shadow: var(--button-shadow-hovered);
}

.checkbox:not(.disabled):has(label:active) & {
will-change: box-shadow;
box-shadow: var(--button-shadow-pressed);
}

&:before {
// For indicating the hover or focused state
transition: mixins.$clickable-normal-state-transitions;
content: '';
position: absolute;
inset: -0.1875rem; // 3px
border-radius: inherit;

.checkbox:has(input[type='checkbox']:focus-visible) & {
will-change: box-shadow;

box-shadow: var(--shadow-depth-8-focused);
}
}

&:after {
// For indicating the indeterminate state
transition:
opacity 0.2s ease,
width 0.4s ease;
content: '';
position: absolute;
inset: 0;
margin: auto;

height: 0.125rem;
width: 0.25rem;

border-radius: 1rem;
opacity: 0;

background-color: rgb(var(--color-white));

.indeterminate & {
opacity: 1;
width: calc($box-size - 0.5rem);
width: calc(var(--limel-boolean-input-box-size) - 0.5rem);
}
}
}

svg {
position: absolute;
z-index: 1;
inset: 0;
.checkbox {
--limel-boolean-input-box-border-radius: 0.25rem;

svg.check-mark {
position: absolute;
z-index: 1;
inset: 0;

transform: translate3d(-0.125rem, -0.125rem, 0);
transform: translate3d(-0.125rem, -0.125rem, 0);

width: $box-size;
height: $box-size;
width: var(--limel-boolean-input-box-size);
height: var(--limel-boolean-input-box-size);

padding: 0.25rem;
padding: 0.25rem;

color: rgb(var(--color-white));
opacity: 0;
color: rgb(var(--color-white));
opacity: 0;

stroke-width: 0.1875rem; // 3px
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 0.1875rem; // 3px
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;

path {
stroke-dashoffset: 29.7833;
stroke-dasharray: 29.7833;
transition: stroke-dashoffset 180ms cubic-bezier(0.4, 0, 0.6, 1);
path {
stroke-dashoffset: 29.7833;
stroke-dasharray: 29.7833;
transition: stroke-dashoffset 180ms cubic-bezier(0.4, 0, 0.6, 1);
}
}

.checkbox:not(.indeterminate):has(input[type='checkbox']:checked) & {
opacity: 1;
&:not(.indeterminate):has(input[type='checkbox']:checked) {
svg.check-mark {
opacity: 1;

path {
stroke-dashoffset: 0;
path {
stroke-dashoffset: 0;
}
}
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/components/checkbox/checkbox.template.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const CheckboxTemplate: FunctionalComponent<CheckboxTemplateProps> = (
class={{
'mdc-form-field': true, // required by MDC to work
'mdc-checkbox': true, // required by MDC to work
'boolean-input': true,
checkbox: true,
checked: props.checked,
invalid: props.invalid,
Expand All @@ -76,11 +77,18 @@ export const CheckboxTemplate: FunctionalComponent<CheckboxTemplateProps> = (
{...inputProps}
/>
<div class="box">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<svg
class="check-mark"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path fill="none" d="M1.73,12.91 8.1,19.28 22.79,4.59" />
</svg>
</div>
<label htmlFor={props.id}>{props.label}</label>
<label class="boolean-input-label" htmlFor={props.id}>
{props.label}
</label>
</div>,
<HelperText
text={props.helperText}
Expand Down
55 changes: 41 additions & 14 deletions src/components/list/radio-button/radio-button.scss
Original file line number Diff line number Diff line change
@@ -1,21 +1,48 @@
@use '@material/radio';
@use '@material/radio/styles';
@use '@material/form-field';
/**
* :::important
* The `RadioButtonTemplate` can be imported and used in the HTML of
* other components, to render a non-functional and decorative radio button in
* their UI. An example of this is the list component.
* This means the content of `RadioButtonTemplate` will become a part of the
* consumer's DOM structure.
*
* Additionally, the consumer components' also need to import the current `.scss`
* file into their own styles file, for the radio button to be rendered correctly!
* This means, if the styles in this file are not "specific" enough,
* there is a risk that the consumer component's styles are affected by
* our styles here.
*
* For instance if the consumer has a `<label>`,
* it might unintentionally inherit styles from the radio button; unless we
* make the such styles more specific here.
*
* Naturally, we cannot mitigate all sorts of potential styling problems.
* The consumer component should be aware of this issue too.
* But we can ensure that our styles here both make sense,
* are readable, and are as specific as possible to avoid unintended side effects.
* :::
*/

@include form-field.core-styles;
@use '../../style/mixins';

.mdc-form-field {
display: flex;
@forward '../../style/internal/boolean-input.scss';

.mdc-radio {
@include radio.ink-color(primary);
}
.radio-button {
--limel-boolean-input-box-border-radius: var(
--limel-boolean-input-box-size
);
}

.mdc-radio {
.mdc-radio__native-control:enabled:not(:checked)
+ .mdc-radio__background
.mdc-radio__outer-circle {
border-color: var(--mdc-checkbox-unchecked-color);
.box {
&:after {
width: 100%;
height: 100%;
border-radius: 50%;

.boolean-input:has(input[type='radio']:checked) & {
opacity: 1;
transform: scale(0.6);
box-shadow: var(--shadow-depth-8);
}
}
}
Loading